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
@@ -0,0 +1,81 @@
|
||||
import type { SiteFailureSample, SiteFailureSampleSummary } from "@umt/contracts";
|
||||
import { Download } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useSiteFailureSample } from "~/api/sites.ts";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { KvList } from "~/components/console/kv-list.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { failureSampleFileName, failureSampleItems, failureSampleText, failureSampleTitle } from "./site-fields.ts";
|
||||
|
||||
// 站点请求失败留样(技术信息里):最近几次「页面结构无法识别」/「人机验证」时站点返回的页面(已脱敏),
|
||||
// 用来分清是站点改版还是被人机验证拦了。列表不带正文,展开时再取。
|
||||
|
||||
function download(s: SiteFailureSample) {
|
||||
const url = URL.createObjectURL(new Blob([failureSampleText(s)], { type: "text/plain;charset=utf-8" }));
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = failureSampleFileName(s);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function SampleBody({ siteId, sampleId }: { siteId: string; sampleId: string }) {
|
||||
const q = useSiteFailureSample(siteId, sampleId, true);
|
||||
if (q.isPending) return <LoadingState rows={2} />;
|
||||
if (q.error) return <ErrorState plain title="读取样本失败" error={q.error} onRetry={() => void q.refetch()} />;
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<div>
|
||||
<Button size="xs" variant="outline" onClick={() => download(q.data)}>
|
||||
<Download aria-hidden />
|
||||
下载文本
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="max-h-80 overflow-auto rounded-lg bg-muted/40 p-2 font-mono text-caption break-all whitespace-pre-wrap text-foreground">
|
||||
{q.data.body || "(页面是空的)"}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 一份样本:标题一行,展开看请求信息与页面 */
|
||||
export function FailureSampleRow({ siteId, sample }: { siteId: string; sample: SiteFailureSampleSummary }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<li className="min-w-0 rounded-lg border border-border/60 px-3 py-2" data-slot="failure-sample">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex w-full flex-wrap items-baseline justify-between gap-x-3 text-left text-sm text-foreground"
|
||||
>
|
||||
<span>{failureSampleTitle(sample)}</span>
|
||||
<span className="text-meta text-muted-foreground">
|
||||
{relTime(sample.createdAt)} · {open ? "收起" : "查看"}
|
||||
</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="mt-2 grid gap-3">
|
||||
<KvList columns={1} items={failureSampleItems(sample)} />
|
||||
<SampleBody siteId={siteId} sampleId={sample.id} />
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** 最近的失败样本(列表由调用方用 useSiteFailureSamples 取);没有就不渲染 */
|
||||
export function SiteFailureSamples({ siteId, items }: { siteId: string; items: SiteFailureSampleSummary[] }) {
|
||||
if (!items.length) return null;
|
||||
return (
|
||||
<section className="grid gap-2">
|
||||
<h3 className="text-meta text-muted-foreground">最近的失败页面(已脱敏,留最近 5 份)</h3>
|
||||
<ul className="grid gap-2">
|
||||
{items.map((s) => (
|
||||
<FailureSampleRow key={s.id} siteId={siteId} sample={s} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user