// 公开页面:检测页(/)、报告页(/r/)、找不到、源代码目录(/source)。所有来自用户或报告的值都过 esc()。 import type { DetectionReport, DetectorResult } from "../models.js"; import { esc, langHref, layout } from "./html.js"; import { GROUP_ORDER, ITEMS, copy, fill, type Lang } from "./i18n.js"; export const MODEL_CHOICES = [ "claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1", "claude-opus-4-8", "claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5-20251001", ]; export function toolPage(lang: Lang): string { const t = copy(lang); const groups = GROUP_ORDER.map((g) => { const lis = ITEMS.filter((i) => i.group === g) .map((i) => { const c = t.items[i.name as keyof typeof t.items]; return `
  • ${esc(c.name)}${lang === "zh" ? ":" : ": "}${esc(c.what)}
  • `; }) .join(""); return `

    ${esc(t.groups[g])}

    `; }).join(""); const modes = (["quick", "standard", "full"] as const) .map((m) => ``) .join(""); const options = MODEL_CHOICES.map((m) => ``).join(""); const body = `

    ${esc(t.siteTitle)}

    ${esc(t.lead)}

    ${esc(t.protocolNote)}

    ${esc(t.form.baseUrlHint)}

    ${esc(t.form.apiKeyHint)}

    ${options}

    ${esc(t.form.modelHint)}

    ${esc(t.limitsNote)}

    ${esc(t.checksTitle)}

    ${groups}
    `; return layout(t, lang, { title: t.siteTitle, path: "/", body }); } function hostOf(baseUrl: string): string { try { return new URL(baseUrl).host; } catch { return baseUrl; } } /** UTC 时间,到分钟:2026-11-08 02:03 UTC。 */ function utcMinute(iso: string): string { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return d.toISOString().slice(0, 16).replace("T", " ") + " UTC"; } function orderResults(results: readonly DetectorResult[]): DetectorResult[] { const rank = new Map(ITEMS.map((i, n) => [i.name, n])); return [...results].sort((a, b) => (rank.get(a.name) ?? 99) - (rank.get(b.name) ?? 99)); } function itemName(lang: Lang, r: DetectorResult): string { const c = copy(lang).items as Record; return c[r.name]?.name ?? r.display_name ?? r.name; } function round1(n: number): string { return String(Math.round(n * 10) / 10); } export function reportPage(lang: Lang, rep: DetectionReport & { id: string }): string { const t = copy(lang); const r = t.report; const summary = lang === "en" ? r.summaries[rep.summary] ?? rep.summary : rep.summary; const rows = orderResults(rep.results) .map((x) => { const hasDetails = Object.keys(x.details ?? {}).length > 0; const expandable = x.status !== "pass" && (hasDetails || x.error); const detail = expandable ? `
    ${esc(r.details)}${x.error ? `
    ${esc(r.errorLabel)}: ${esc(x.error)}
    ` : ""}` + (hasDetails ? `
    ${esc(JSON.stringify(x.details, null, 2))}
    ` : "") + `
    ` : ""; return `${esc(itemName(lang, x))}${detail} ${esc(r.statuses[x.status] ?? x.status)} ${x.status === "skip" ? "–" : esc(round1(x.score))}${esc(x.weight)}`; }) .join(""); const p = rep.performance; const meta: [string, string][] = [ [r.model, rep.target_model], [r.host, hostOf(rep.base_url)], [r.key, rep.api_key_masked], [r.mode, r.modes[rep.mode] ?? rep.mode], [r.time, utcMinute(rep.timestamp)], [r.requests, String(p?.request_count ?? "–")], [r.ttft, p?.ttft_ms == null ? "–" : fill(r.ms, { ms: p.ttft_ms })], [r.duration, p?.total_latency_ms == null ? "–" : fill(r.seconds, { s: round1(p.total_latency_ms / 1000) })], [r.backoff, String(p?.backoff_events ?? 0)], ]; const body = `

    ${esc(r.title)}

    ${esc(Math.round(rep.total_score))}/ 100 ${esc(summary)}
    ${rep.run_error ? `

    ${esc(fill(r.runError, { msg: rep.run_error }))}

    ` : ""}
    ${meta.map(([k, v]) => `
    ${esc(k)}
    ${esc(v)}
    `).join("")}

    ${esc(r.item)}

    ${rows}
    ${esc(r.item)}${esc(r.status)}${esc(r.points)}${esc(r.weight)}

    ${esc(r.note)}

    ${esc(r.again)}${esc(r.json)}

    `; return layout(t, lang, { title: `${r.title} · ${rep.target_model}`, path: `/r/${rep.id}`, body }); } export function notFoundPage(lang: Lang, path: string): string { const t = copy(lang); const body = `

    ${esc(t.notFound.title)}

    ${esc(t.notFound.body)}

    ${esc(t.notFound.back)}

    `; return layout(t, lang, { title: t.notFound.title, path, body }); } export function sourceIndexPage(lang: Lang, files: readonly string[]): string { const t = copy(lang); const items = files.map((f) => `
  • ${esc(f)}
  • `).join(""); const body = `

    ${esc(t.sourceTitle)}

    ${esc(t.sourceLead)}

      ${items}
    `; return layout(t, lang, { title: t.sourceTitle, path: "/source", body }); }