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:
rootandClaude Opus 5.5 committed 2026-09-30 20:38:48 +08:00
1 parent 322ecb2f8c
commit b1d30bd6ee
16 files changed
+433 -27

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ site.baseUrl; site.cookie; // 请求 Cookie 需包含 site.cookie
site.setLoggedIn(false); // 全站返回登录页(或 302 login.php)
site.failNext(1, 429, "7"); // 限流 / 5xx
site.setBrokenLayout(true); // 改版模拟
site.setHeaderLayout("no_logout"); // 页头布局:classic / no_logout(观众 2026 版无 logout 链接)/ bare(无登录标志)
site.infoHashOf("812345"); // 下载 .torrent 的 infohash
```
+1
View File
@@ -17,6 +17,7 @@ export {
} from "./mock-mteam.ts";
export {
createMockNexusPhpSite,
type MockHeaderLayout,
type MockLoginGuard,
type MockNexusPhpOptions,
type MockNexusPhpSite,
+34 -6
View File
@@ -22,6 +22,7 @@ export interface MockNexusPhpOptions {
}
export type MockLoginGuard = "none" | "captcha" | "two_factor" | "blocked";
export type MockHeaderLayout = "classic" | "no_logout" | "bare";
export interface MockNexusPhpSite extends StartedServer {
cookie: string;
@@ -34,6 +35,12 @@ export interface MockNexusPhpSite extends StartedServer {
setBrokenLayout(v: boolean): void;
/** 切换登录关卡 */
setLoginGuard(v: MockLoginGuard): void;
/**
* 已登录页头布局:classic(info_block 里有 logout 链接,缺省);
* no_logout(观众 2026 版:退出只在 userbar 的 data-uploader-actions JSON 里,页面没有 logout 链接);
* bare(已登录但没有 info_block / usercp / logout 任何登录标志)
*/
setHeaderLayout(v: MockHeaderLayout): void;
/** takelogin.php 收到的 POST 次数 */
readonly loginAttempts: number;
/** 各 .torrent 的 infohash,供断言 */
@@ -60,13 +67,30 @@ const AUDIENCES_USERBAR = `<div class="site-userbar">
<a class="site-userbar__compact-tool" href="messages.php" title="收件箱 12/0">信</a>
</div>`;
function shell(siteName: string, title: string, body: string, loggedIn: boolean): string {
const userbar = loggedIn
? `<div id="info_block"><a href="userdetails.php?id=10001">mockuser</a>
/** 观众 2026 版 info_block:退出 / 控制面板在 data-uploader-actions JSON 里由脚本渲染(脱敏结构样本) */
const AUDIENCES_NO_LOGOUT_BLOCK = `<table id="info_block"><tr><td><table><tr><td class="bottom">
<div class="site-userbar site-userbar--torrents"><div class="site-userbar__compact-identity">
<span class="torrent-uploader-wrap site-userbar__name" title="mockuser" data-uploader-actions="[{&quot;href&quot;:&quot;usercp.php&quot;,&quot;title&quot;:&quot;控制面板&quot;},{&quot;href&quot;:&quot;logout.php&quot;,&quot;title&quot;:&quot;退出&quot;}]"><span class="nowrap"><a href="userdetails.php?id=10001" class="VIP_Name"><b>mockuser</b></a></span></span>
<a href="https://audiences.example/usercp.php?action=personal">设置</a></div></div>
</td><td class="bottom"></td></tr></table></td></tr></table>`;
function shell(
siteName: string,
title: string,
body: string,
loggedIn: boolean,
layout: MockHeaderLayout = "classic",
): string {
const userbar = !loggedIn
? ""
: layout === "no_logout"
? `${AUDIENCES_NO_LOGOUT_BLOCK}${AUDIENCES_USERBAR}`
: layout === "bare"
? `<div class="welcome">mockuser</div>`
: `<div id="info_block"><a href="userdetails.php?id=10001">mockuser</a>
<span class="site-userbar__compact-metric--uploaded">12.5 TB</span>
<span class="site-userbar__compact-metric--downloaded">3.2 TB</span>
<a href="logout.php">退出</a></div>${AUDIENCES_USERBAR}`
: "";
<a href="logout.php">退出</a></div>${AUDIENCES_USERBAR}`;
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(siteName)} :: ${esc(title)}</title></head>
<body>${userbar}<div id="outer">${body}</div></body></html>`;
}
@@ -166,6 +190,7 @@ export async function createMockNexusPhpSite(opts: MockNexusPhpOptions = {}): Pr
const username = opts.username ?? "mockuser";
const password = opts.password ?? "mock-password";
let broken = false;
let layout: MockHeaderLayout = "classic";
const failures: Array<{ status: number; retryAfter?: string }> = [];
const torrents = new Map(releases.map((r) => [r.id, buildFakeTorrent({ name: r.title, length: 4 * 1024 * 1024 })]));
@@ -212,7 +237,7 @@ export async function createMockNexusPhpSite(opts: MockNexusPhpOptions = {}): Pr
switch (url.pathname) {
case "/index.php":
case "/":
return send(res, 200, shell(siteName, "首页", "<h2>最新公告</h2>", true));
return send(res, 200, shell(siteName, "首页", "<h2>最新公告</h2>", true, layout));
case "/userdetails.php":
return send(res, 200, USER_DETAILS);
case "/getusertorrentlistajax.php":
@@ -278,6 +303,9 @@ export async function createMockNexusPhpSite(opts: MockNexusPhpOptions = {}): Pr
setLoginGuard(v) {
guard = v;
},
setHeaderLayout(v) {
layout = v;
},
get loginAttempts() {
return loginAttempts;
},