fix(sites): 观众健康检查误报未登录,健康失败原因细分(HC-1)
观众 2026 版首页没有 <a href=logout>:退出按钮在 userbar 的 data-uploader-actions JSON 里由脚本渲染,旧登录标志 a[href*="logout"] 永远不命中,健康检查报"未登录"而用户数据正常。 - audiences 定义 loginSuccess 改为 #info_block / usercp.php / logout 任一(0.2.1) - 健康检查判定细分:登录页 / 权限页 / 疑似人机验证 / 找不到登录标志(带选择器), 原始判定写入 last_health.detail,站点概览技术信息展示 - 首页只是找不到登录标志时,以 24 小时内用户数据解析成功为已登录证据(同 MP) - 新 SiteError 类 challenge(cf-mitigated: challenge),不算凭据错误;健康状态 challenge - testkit mock 增加 setHeaderLayout;加观众首页脱敏结构样本的差分测试 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
322ecb2f8c
commit
b1d30bd6ee
16 files changed
+433
-27
No files matched your search
@@ -5,6 +5,7 @@ import {
|
||||
type MteamDefinition,
|
||||
mteamApiBase,
|
||||
refreshSiteUserData,
|
||||
type SiteHealthDetail,
|
||||
SiteLoginError,
|
||||
type SiteLoginFailure,
|
||||
type SiteUserInfo,
|
||||
@@ -789,8 +790,12 @@ const HEALTH_OF: Record<string, SiteHealth> = {
|
||||
unreachable: "degraded",
|
||||
site_maintenance: "degraded",
|
||||
parse_error: "degraded",
|
||||
challenge: "degraded",
|
||||
};
|
||||
|
||||
/** 用户数据多久内解析成功可以当作"已登录"的证据(与 MP 一致:user_info 解析成功即判登录) */
|
||||
const USER_DATA_EVIDENCE_MS = 24 * 3_600_000;
|
||||
|
||||
/** 首页连通 + 登录态(≤ 20 s,走站点限流);写 site.last_health* 与 health_status(已关闭的站点保持 disabled) */
|
||||
export async function checkSiteHealth(
|
||||
rt: SourcesRuntime,
|
||||
@@ -802,6 +807,7 @@ export async function checkSiteHealth(
|
||||
let status = "unreachable";
|
||||
let latencyMs: number | null = null;
|
||||
let message: string | null = null;
|
||||
let detail: SiteHealthDetail | null = null;
|
||||
const def = getSiteDefinition(site.key);
|
||||
if (!def) {
|
||||
message = "站点没有适配器定义,无法检测";
|
||||
@@ -813,6 +819,30 @@ export async function checkSiteHealth(
|
||||
status = h.status;
|
||||
latencyMs = h.latencyMs;
|
||||
message = h.message ?? null;
|
||||
detail = h.detail ?? null;
|
||||
// 首页不是登录页、只是没找到登录标志:以用户数据解析成功为准(避免与"测试登录"两处判定打架)
|
||||
if (!ok && detail?.reason === "marker_missing") {
|
||||
const acc = await rt.ctx.db
|
||||
.selectFrom("site_account")
|
||||
.select(["id", "user_data_at"])
|
||||
.where("site_id", "=", site.id)
|
||||
.where("enabled", "=", true)
|
||||
.where("status", "=", "active")
|
||||
.where("user_data_at", ">=", new Date(now.getTime() - USER_DATA_EVIDENCE_MS))
|
||||
.orderBy("user_data_at", "desc")
|
||||
.executeTakeFirst();
|
||||
if (acc?.user_data_at) {
|
||||
ok = true;
|
||||
status = "ok";
|
||||
message = null;
|
||||
detail = {
|
||||
...detail,
|
||||
reason: "user_data",
|
||||
homepage: "marker_missing",
|
||||
userDataAt: acc.user_data_at.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const loggedIn = ok ? true : status.startsWith("auth_") ? false : null;
|
||||
const healthStatus: SiteHealth = !site.enabled
|
||||
@@ -820,7 +850,7 @@ export async function checkSiteHealth(
|
||||
: def
|
||||
? (HEALTH_OF[status] ?? "degraded")
|
||||
: site.health_status;
|
||||
const last = { ok, status, latencyMs, message };
|
||||
const last = { ok, status, latencyMs, message, detail };
|
||||
await rt.ctx.db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.updateTable("site")
|
||||
|
||||
@@ -218,9 +218,35 @@ describe.skipIf(!up)("站点中心(mock NexusPHP)", () => {
|
||||
mock.setLoggedIn(false);
|
||||
const bad = await json(await h.req(`/sites/${siteId}/health-check`, { method: "POST" }));
|
||||
expect(bad).toMatchObject({ ok: false, loggedIn: false, healthStatus: "failing" });
|
||||
expect(bad.detail).toMatchObject({ reason: "login_page", path: "index.php" });
|
||||
expect(bad.message).toBe("健康检查:首页返回登录页,Cookie 已失效");
|
||||
mock.setLoggedIn(true);
|
||||
});
|
||||
|
||||
it("HC-1 健康检测:首页不是登录页、只是找不到登录标志时,以近期用户数据解析成功为准", async () => {
|
||||
mock.setHeaderLayout("bare");
|
||||
try {
|
||||
const r = await json(await h.req(`/sites/${siteId}/health-check`, { method: "POST" }));
|
||||
expect(r).toMatchObject({ ok: true, status: "ok", loggedIn: true, healthStatus: "healthy", message: null });
|
||||
expect(r.detail).toMatchObject({ reason: "user_data", homepage: "marker_missing" });
|
||||
expect(r.detail.userDataAt).toBeTruthy();
|
||||
// 没有近期用户数据:照实报"找不到登录标志(选择器 …)"
|
||||
await h.db
|
||||
.updateTable("site_account")
|
||||
.set({ user_data_at: new Date(Date.now() - 3 * 86_400_000) })
|
||||
.where("site_id", "=", siteId)
|
||||
.execute();
|
||||
const bad = await json(await h.req(`/sites/${siteId}/health-check`, { method: "POST" }));
|
||||
expect(bad).toMatchObject({ ok: false, status: "auth_expired", loggedIn: false });
|
||||
expect(bad.message).toContain("首页里找不到登录标志(选择器 #info_block");
|
||||
expect(bad.detail).toMatchObject({ reason: "marker_missing" });
|
||||
const site = await json(await h.req(`/sites/${siteId}`));
|
||||
expect(site.lastHealth.detail).toMatchObject({ reason: "marker_missing" });
|
||||
} finally {
|
||||
mock.setHeaderLayout("classic");
|
||||
}
|
||||
});
|
||||
|
||||
it("删除站点:账号与凭证删除,资源保留并标站点已关闭;同一定义可再登记", async () => {
|
||||
const rel = await h.db
|
||||
.insertInto("source_release")
|
||||
|
||||
Reference in new issue
Block a user