feat(web): 站点页技术信息里显示最近的失败页面样本
站点概览「站点」一栏的技术信息里列出最近 5 份失败样本(搜索 / 列表 / 详情 · 人机验证 / 页面结构无法识别 · HTTP 状态),展开看脱敏地址、错误、命中规则、关键响应头和页面正文,可下载文本;没有样本不显示。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
bd475a4ce1
commit
ec8b224482
5 files changed
+228
-2
No files matched your search
@@ -6,6 +6,8 @@ import {
|
||||
type SiteAuthKind,
|
||||
type SiteCapability,
|
||||
type SiteDefinitionView,
|
||||
type SiteFailureSample,
|
||||
type SiteFailureSampleSummary,
|
||||
type SiteKind,
|
||||
type SiteLastHealth,
|
||||
type SiteListItem,
|
||||
@@ -240,6 +242,58 @@ export function cloudflareTechItems(config: unknown): { label: string; value: st
|
||||
].filter((it) => it.value);
|
||||
}
|
||||
|
||||
const SAMPLE_KIND: Record<string, string> = { parse_error: "页面结构无法识别", challenge: "人机验证" };
|
||||
const SAMPLE_REQUEST: Record<string, string> = {
|
||||
search: "搜索",
|
||||
list: "列表",
|
||||
detail: "详情",
|
||||
health: "健康检测",
|
||||
other: "请求",
|
||||
};
|
||||
|
||||
/** 失败样本一行:"搜索 · 人机验证 · HTTP 200" */
|
||||
export function failureSampleTitle(s: Pick<SiteFailureSampleSummary, "kind" | "requestKind" | "httpStatus">): string {
|
||||
return [
|
||||
SAMPLE_REQUEST[s.requestKind] ?? s.requestKind,
|
||||
SAMPLE_KIND[s.kind] ?? s.kind,
|
||||
s.httpStatus !== null ? `HTTP ${s.httpStatus}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
/** 失败样本的技术信息条目(地址、响应头、规则、文案) */
|
||||
export function failureSampleItems(s: SiteFailureSampleSummary): { label: string; value: string; mono?: boolean }[] {
|
||||
return [
|
||||
{ label: "地址", value: s.url, mono: true },
|
||||
{ label: "错误", value: s.message },
|
||||
{ label: "命中规则", value: s.rule ?? "", mono: true },
|
||||
...Object.entries(s.headers).map(([k, v]) => ({ label: k, value: v, mono: true })),
|
||||
{ label: "页面", value: `${formatBytes(s.bodyBytes)}${s.truncated ? "(只留了开头)" : ""}` },
|
||||
].filter((it) => it.value);
|
||||
}
|
||||
|
||||
/** 下载用的文本:头部是请求信息,后面是脱敏后的页面 */
|
||||
export function failureSampleText(s: SiteFailureSample): string {
|
||||
const head = [
|
||||
`# 站点失败样本(已脱敏)`,
|
||||
`# 时间:${s.createdAt}`,
|
||||
`# 类型:${failureSampleTitle(s)}`,
|
||||
`# 地址:${s.url}`,
|
||||
...Object.entries(s.headers).map(([k, v]) => `# ${k}: ${v}`),
|
||||
...(s.rule ? [`# 规则:${s.rule}`] : []),
|
||||
`# 错误:${s.message}`,
|
||||
...(s.truncated ? ["# 页面超过 64 KB,只留了开头"] : []),
|
||||
];
|
||||
return `${head.join("\n")}\n\n${s.body}`;
|
||||
}
|
||||
|
||||
/** 下载文件名:site-sample-<请求类型>-<类型>-<时间>.html.txt */
|
||||
export function failureSampleFileName(s: Pick<SiteFailureSample, "kind" | "requestKind" | "createdAt">): string {
|
||||
const t = s.createdAt.replace(/[-:]/g, "").replace(/\..*$/, "").replace("T", "-");
|
||||
return `site-sample-${s.requestKind}-${s.kind}-${t}.html.txt`;
|
||||
}
|
||||
|
||||
/** 限流:"20 次/分";空对象 = 默认 */
|
||||
export function rateLimitText(r: SiteRateLimit | null | undefined): string {
|
||||
if (!r || (!r.requestsPerMinute && !r.minIntervalMs && !r.concurrency)) return "默认";
|
||||
|
||||
Reference in new issue
Block a user