feat: 增加求解器验证书 Web (#7)

Co-authored-by: yige <yige@yigedeMacBook-Neo.local>
This commit is contained in:
2026-08-30 11:02:51 -04:00
committed by GitHub
parent 27a6934c63
commit af338dc13b
20 changed files with 3777 additions and 1 deletions

View File

@@ -0,0 +1,107 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
<meta name="color-scheme" content="light" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src-elem 'self'; style-src-attr 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'none'"
/>
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%23101b35'/%3E%3Ctext x='32' y='44' text-anchor='middle' font-size='40' fill='%2358d4c7'%3EΣ%3C/text%3E%3C/svg%3E"
/>
<title>通用求解器智能体团队 · 真实场景验证书</title>
<link rel="stylesheet" href="assets/katex.min.css" />
<link rel="stylesheet" href="assets/report.css" />
</head>
<body>
<a class="skip-link" href="#chapter">跳到当前章节</a>
<div id="app" class="app-shell" aria-busy="true">
<aside class="rail" aria-label="验证书目录">
<div class="brand">
<span class="brand-mark">Σ</span>
<div><strong>Solver Guild</strong><small>真实场景验证书</small></div>
</div>
<div class="rail-summary" id="rail-summary"></div>
<label class="search"
><span>搜索章节</span><input id="chapter-search" type="search" placeholder="场景、问题族、引擎"
/></label>
<div class="filters" id="filters" aria-label="问题族筛选"></div>
<nav id="chapter-nav" class="chapter-nav" aria-label="场景章节"></nav>
<div class="rail-foot"><span id="build-id"></span><span>零埋点 · 可离线</span></div>
</aside>
<main class="main">
<header class="topbar">
<button id="menu-button" class="icon-button" aria-label="打开目录"></button>
<div class="progress">
<span id="progress-label">载入证据…</span>
<div><i id="progress-bar"></i></div>
</div>
<div class="top-actions">
<button id="latest-button">新版回归</button><button id="print-button">打印 / PDF</button
><button id="integrity-button">完整性</button>
</div>
</header>
<article id="chapter" class="chapter" tabindex="-1"></article>
<footer class="pager">
<button id="prev-button">← 上一章</button><span id="pager-label"></span
><button id="next-button">下一章 →</button>
</footer>
</main>
</div>
<dialog id="lightbox" class="lightbox" aria-labelledby="lightbox-title" aria-describedby="lightbox-meta">
<div class="lightbox-bar">
<div class="lightbox-heading">
<strong id="lightbox-title"></strong><small id="lightbox-meta"></small>
</div>
<button id="lightbox-close" aria-label="关闭原图">×</button>
</div>
<div class="lightbox-viewport">
<div
id="lightbox-stage"
class="lightbox-stage"
tabindex="0"
aria-label="可缩放和平移的截图预览"
aria-describedby="lightbox-zoom-hint"
>
<div id="lightbox-canvas" class="lightbox-canvas">
<img id="lightbox-image" alt="" draggable="false" />
</div>
</div>
<span id="lightbox-zoom-hint" class="lightbox-zoom-hint"
><span class="lightbox-zoom-hint-desktop">双击放大 · Ctrl/⌘ + 滚轮缩放 · 拖动平移</span
><span class="lightbox-zoom-hint-mobile">双指捏合缩放 · 单指拖动平移</span></span
>
</div>
<div class="lightbox-controls">
<button id="lightbox-prev">← 上一张</button><a id="lightbox-original" download>下载原图</a
><div class="lightbox-zoom-controls" role="group" aria-label="图片缩放">
<button id="lightbox-zoom-out" aria-label="缩小图片"></button
><output id="lightbox-zoom" aria-live="polite">适应窗口</output
><button id="lightbox-zoom-in" aria-label="放大图片"></button>
</div>
<button id="lightbox-fit" aria-label="让图片适应窗口">适应</button
><button id="lightbox-actual" aria-label="按原始像素显示图片">1:1</button
><button id="lightbox-next">下一张 →</button>
</div>
</dialog>
<dialog id="integrity-dialog" class="integrity-dialog">
<button class="dialog-close" aria-label="关闭">×</button>
<h2>证据完整性</h2>
<div id="integrity-content"></div>
</dialog>
<dialog id="latest-dialog" class="latest-dialog">
<button class="dialog-close" aria-label="关闭">×</button>
<div id="latest-content"></div>
</dialog>
<noscript
><p class="noscript">
本验证书需要 JavaScript 载入分章证据。所有数据均包含在离线包内,不会连接第三方服务。
</p></noscript
>
<script src="data/manifest.js"></script>
<script src="assets/report.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
;(() => {
'use strict'
const manifest = window.__REPORT_MANIFEST__
if (!manifest) throw new Error('report manifest missing')
window.__REPORT_CHAPTERS__ = window.__REPORT_CHAPTERS__ || {}
const $ = (selector) => document.querySelector(selector)
const esc = (value) =>
String(value ?? '').replace(
/[&<>"']/g,
(char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]
)
const statusTone = (status) =>
['infeasible', 'fail', 'blocked_engine_unavailable'].includes(status) ? 'guard' : 'pass'
const statusLabel = (status) =>
({
optimal: '最优解',
infeasible: '不可行 · 已诊断',
pass: '验证通过',
fail: '验证失败 · 已阻断',
recovered: '恢复通过',
idle: '已完成',
})[status] ||
status ||
'已验证'
const familyLabel = (family) =>
({ lp: 'LP', milp: 'MILP', qp: 'QP', miqp: 'MIQP', qcp: 'QCP', miqcp: 'MIQCP', cp: 'CP' })[
String(family).toLowerCase()
] || String(family).toUpperCase()
const shortHash = (value) => (value ? `${value.slice(0, 10)}${value.slice(-8)}` : '不适用')
const formatTime = (value) => (value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '未记录')
const state = { current: 0, filter: 'ALL', query: '', chapter: null, lightboxIndex: 0, visible: manifest.chapters }
const viewer = {
zoom: 1,
fitScale: 1,
mode: 'fit',
pointers: new Map(),
drag: null,
pinch: null,
resizeFrame: 0,
}
const MIN_VIEWER_ZOOM = 0.5
const MAX_VIEWER_ZOOM = 12
const clamp = (value, min, max) => Math.min(max, Math.max(min, value))
const loadScript = (src) =>
new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
async function loadChapter(index) {
const item = manifest.chapters[index]
if (!item) return
if (!window.__REPORT_CHAPTERS__[item.scenarioId]) {
if (location.protocol === 'file:') await loadScript(item.script)
else {
const response = await fetch(item.json)
if (!response.ok) throw new Error(`chapter load failed: ${response.status}`)
window.__REPORT_CHAPTERS__[item.scenarioId] = await response.json()
}
}
state.current = index
state.chapter = window.__REPORT_CHAPTERS__[item.scenarioId]
renderChapter()
renderNav()
updatePager()
updateHash()
$('#chapter').focus({ preventScroll: true })
window.scrollTo({ top: 0, behavior: 'instant' })
}
function updateHash() {
const desired = `#case-${state.current + 1}`
if (location.hash !== desired) history.replaceState(null, '', desired)
}
function hashIndex() {
const caseMatch = /^#case-(\d+)$/.exec(location.hash)
if (caseMatch) return Math.max(0, Math.min(manifest.chapters.length - 1, Number(caseMatch[1]) - 1))
const id = decodeURIComponent(location.hash.slice(1))
const index = manifest.chapters.findIndex((item) => item.scenarioId === id)
return index >= 0 ? index : 0
}
function renderShell() {
$('#rail-summary').innerHTML =
`<span><b>${manifest.counts.chapters}</b>场景</span><span><b>${manifest.counts.screenshots}</b>真机图</span><span><b>${manifest.counts.representativeStabilityPasses}</b>稳定运行</span>`
$('#build-id').textContent = manifest.buildId
const families = ['ALL', ...new Set(manifest.chapters.map((item) => familyLabel(item.problemFamily)))]
$('#filters').innerHTML = families
.map(
(family) =>
`<button class="filter${family === 'ALL' ? ' active' : ''}" data-family="${esc(family)}">${family === 'ALL' ? '全部' : family}</button>`
)
.join('')
$('#filters').addEventListener('click', (event) => {
const button = event.target.closest('[data-family]')
if (!button) return
state.filter = button.dataset.family
document.querySelectorAll('.filter').forEach((item) => item.classList.toggle('active', item === button))
applyNavFilter()
})
$('#chapter-search').addEventListener('input', (event) => {
state.query = event.target.value.trim().toLowerCase()
applyNavFilter()
})
$('#print-button').addEventListener('click', () => window.print())
$('#menu-button').addEventListener('click', () => $('.rail').classList.toggle('open'))
$('#prev-button').addEventListener('click', () =>
loadChapter((state.current - 1 + manifest.chapters.length) % manifest.chapters.length)
)
$('#next-button').addEventListener('click', () => loadChapter((state.current + 1) % manifest.chapters.length))
$('#latest-button').addEventListener('click', openLatestRegression)
$('#integrity-button').addEventListener('click', openIntegrity)
$('#integrity-dialog .dialog-close').addEventListener('click', () => $('#integrity-dialog').close())
$('#latest-dialog .dialog-close').addEventListener('click', () => $('#latest-dialog').close())
renderNav()
}
function renderLatestRegressionBanner() {
const regression = manifest.latestPlatformRegression
if (!regression) return ''
const scenarios = regression.runtime?.scenarios ?? []
const passed = scenarios.filter((item) => item.status === 'passed').length
return `<section class="regression-banner"><div class="regression-mark">NEW</div><div class="regression-copy"><small>最新平台定向回归 · ${esc(formatTime(regression.testedAt))}</small><strong>${esc(regression.platform.commit.slice(0, 10))} 已通过 ${passed}/${scenarios.length} 个真实场景与决策工作台真机检查</strong><span>下方 24 章仍保持原始证据提交 ${esc(manifest.evidencePlatformCommit)} 的归属,不把历史截图冒充为新版重跑。</span></div><button type="button" data-open-regression>查看范围与结论</button></section>`
}
function applyNavFilter() {
state.visible = manifest.chapters.filter((item) => {
const familyOk = state.filter === 'ALL' || familyLabel(item.problemFamily) === state.filter
const haystack = `${item.title} ${item.story} ${item.problemFamily} ${item.engineId}`.toLowerCase()
return familyOk && (!state.query || haystack.includes(state.query))
})
renderNav()
}
function renderNav() {
$('#chapter-nav').innerHTML =
state.visible
.map((item) => {
const actualIndex = manifest.chapters.indexOf(item)
return `<button class="nav-item${actualIndex === state.current ? ' active' : ''}" data-index="${actualIndex}"><span class="nav-index">${String(item.order).padStart(2, '0')}</span><span class="nav-copy"><strong>${esc(item.title)}</strong><small>${familyLabel(item.problemFamily)} · ${esc(item.engineId || '独立校核')}</small></span><i class="nav-status ${statusTone(item.resultStatus)}"></i></button>`
})
.join('') || '<p class="empty">没有匹配的章节</p>'
$('#chapter-nav')
.querySelectorAll('[data-index]')
.forEach((button) =>
button.addEventListener('click', () => {
$('.rail').classList.remove('open')
loadChapter(Number(button.dataset.index))
})
)
$('#chapter-nav .active')?.scrollIntoView({ block: 'nearest' })
}
function renderTimeline(chapter) {
const visible = chapter.timeline.filter((item) => item.kind === 'tool' || !item.empty)
const finalMessageSequence = Math.max(
...visible.filter((item) => item.kind === 'message' && item.role === 'assistant').map((item) => item.sequence)
)
return visible
.map((item) => {
if (item.kind === 'tool')
return `<div class="tool-event"><span>受治理工具</span><strong>${esc(item.tool)}</strong><span class="status ${item.status === 'success' ? 'pass' : 'guard'}">${esc(item.status)}</span>${item.durationMs == null ? '' : `<span>${item.durationMs} ms</span>`}</div>`
const isFinal = item.role === 'assistant' && item.sequence === finalMessageSequence
const body = isFinal
? `<details class="full-final"><summary>智能体最终完整报告(原始 Markdown 1:1 渲染)</summary><div class="markdown">${item.html}</div></details>`
: `<div class="message-body markdown">${item.html}</div>`
return `<div class="message ${item.role}"><div class="message-meta"><strong>${item.role === 'user' ? '用户' : '求解决策顾问'}</strong><span>消息 ${item.sequence} · SHA ${shortHash(item.markdownSha256)}</span></div>${body}</div>`
})
.join('')
}
function renderChapter() {
const c = state.chapter
const tone = statusTone(c.resultStatus)
const hard = c.validation?.satisfaction_report?.hard_constraints
const soft = c.validation?.satisfaction_report?.soft_constraints
$('#chapter').innerHTML = `${renderLatestRegressionBanner()}
<section class="chapter-hero">
<div class="eyebrow"><span>CHAPTER ${String(c.scenarioOrder).padStart(2, '0')}</span><span class="badge">${familyLabel(c.problemFamily)}</span><span class="badge">${statusLabel(c.resultStatus)}</span><span class="badge">${esc(c.claimBoundary)}</span></div>
<h1>${esc(c.title)}</h1><p class="story">${esc(c.story)}</p>
<div class="hero-grid"><div class="hero-card"><small>决策问题</small><strong>${esc(c.decision)}</strong></div><div class="hero-card"><small>专业结论</small><strong>${esc(c.professionalSummary)}</strong></div></div>
</section>
<section class="kpis">
<div class="kpi"><small>问题族</small><strong>${familyLabel(c.problemFamily)}</strong></div>
<div class="kpi"><small>实际引擎</small><strong>${esc(c.engineId || '独立校核')}</strong></div>
<div class="kpi"><small>结果 / 校核</small><strong class="status ${tone}">${statusLabel(c.resultStatus)} / ${esc(c.validationVerdict || '不适用')}</strong></div>
<div class="kpi"><small>真实消息 / 工具</small><strong>${c.counts.rawConversationMessages} / ${c.counts.toolEvents}</strong></div>
<div class="kpi"><small>真机截图</small><strong>${c.counts.screenshots} 张 · 2x</strong></div>
</section>
<section class="section-shell"><div class="section-head"><div><h2>场景故事与意义</h2><p>为什么这个问题具有代表性,以及哪些业务规则决定可行性。</p></div><span class="evidence-tag">PACK ${esc(c.scenarioVersion)} · ${shortHash(c.contentSha256)}</span></div>
<div class="hero-grid"><div class="validation-card"><small>目标</small><strong>${c.objectives.map(esc).join('')}</strong></div><div class="validation-card"><small>基线</small><strong>${esc(c.baseline)}</strong></div></div>
<div class="validation-card" style="margin-top:12px"><small>硬规则</small><strong>${c.constraints.map(esc).join('')}</strong></div>
</section>
<section class="section-shell"><div class="section-head"><div><h2>真实多轮对话</h2><p>普通消息逐条来自 messages.jsonl工具事件来自持久回执未用离线脚本改写对话。</p></div><span class="evidence-tag">RUN ${shortHash(c.evidence.runId)}</span></div><div class="timeline">${renderTimeline(c)}</div></section>
<section class="section-shell"><div class="section-head"><div><h2>智能体最终十部分报告</h2><p>十个 section ID、数量和顺序固定正文由最终消息 Markdown 原样构建。</p></div><span class="evidence-tag">${c.formulaStats.rendered} FORMULAS · HTML+MATHML</span></div><div class="report-grid">${c.sections.map((section) => `<section class="report-part" id="${section.id}"><div class="part-number">${section.number}</div><div class="part-body"><h3>${esc(section.title)}</h3><div class="markdown">${section.html}</div></div></section>`).join('')}</div></section>
<section class="section-shell"><div class="section-head"><div><h2>专业校核与稳定性</h2><p>求解结果、独立验证、等价 formulation 和重复运行分别留证。</p></div><span class="evidence-tag">${esc(c.stability.policy)}</span></div>
<div class="validation-grid"><div class="validation-card"><small>硬约束</small><strong>${hard ? `${hard.satisfied}/${hard.total} 满足,最大违反 ${hard.max_violation}` : '诊断/候选模式,详见第 8 节'}</strong></div><div class="validation-card"><small>软约束</small><strong>${soft ? `${soft.violated}/${soft.total} 违反,罚分 ${soft.weighted_penalty_sum}` : '无或不适用'}</strong></div><div class="validation-card"><small>稳定性</small><strong>${c.stability.effectivePasses} 次有效通过${c.stability.semanticConvergence ? ',语义收敛' : ''}</strong></div></div>
<div class="validation-grid" style="margin-top:12px"><div class="validation-card"><small>OptimizationSpec</small><strong>${shortHash(c.evidence.optimizationSpecSha256)}</strong></div><div class="validation-card"><small>Result payload</small><strong>${shortHash(c.evidence.resultPayloadSha256)}</strong></div><div class="validation-card"><small>Validation report</small><strong>${shortHash(c.evidence.validationReportSha256)}</strong></div></div>
</section>
<section class="section-shell"><div class="section-head"><div><h2>真机完整过程</h2><p>DesireCore 原生窗口 backing store应用尺寸不变点击可查看 3024×1824 原图。</p></div><span class="evidence-tag">DEVTOOLS CLOSED</span></div><div class="gallery">${c.media.map((shot, index) => `<button type="button" class="shot" data-shot="${index}" aria-haspopup="dialog" aria-label="查看原图:${esc(shot.alt)}"><picture><source srcset="${shot.thumb1512} 1512w, ${shot.thumb756} 756w" type="image/webp"><img src="${shot.thumb756}" loading="lazy" decoding="async" width="756" height="456" alt="${esc(shot.alt)}"></picture><span class="shot-caption"><span>${index + 1}/${c.media.length} · ${esc(shot.stage)}</span><span>${shortHash(shot.sha256)}</span></span></button>`).join('')}</div></section>`
$('#chapter')
.querySelectorAll('[data-shot]')
.forEach((item) => {
item.addEventListener('click', () => openLightbox(Number(item.dataset.shot)))
item.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
openLightbox(Number(item.dataset.shot))
}
})
})
$('#chapter [data-open-regression]')?.addEventListener('click', openLatestRegression)
document.title = `${String(c.scenarioOrder).padStart(2, '0')} · ${c.title}${manifest.title}`
$('#app').setAttribute('aria-busy', 'false')
}
function updatePager() {
$('#progress-label').textContent = `${state.current + 1} / ${manifest.chapters.length}`
$('#progress-bar').style.width = `${((state.current + 1) / manifest.chapters.length) * 100}%`
$('#pager-label').textContent = state.chapter.title
}
function currentShot() {
return state.chapter?.media?.[state.lightboxIndex] ?? null
}
function calculateFitScale() {
const shot = currentShot()
const stage = $('#lightbox-stage')
if (!shot || !stage.clientWidth || !stage.clientHeight) return 1
const inset = window.matchMedia('(max-width: 920px)').matches ? 16 : 36
return Math.min(
Math.max(1, stage.clientWidth - inset) / shot.width,
Math.max(1, stage.clientHeight - inset) / shot.height,
1
)
}
function updateViewerStatus() {
const stage = $('#lightbox-stage')
const image = $('#lightbox-image')
const actualScale = viewer.fitScale * viewer.zoom
const percent = Math.max(1, Math.round(actualScale * 100))
$('#lightbox-zoom').textContent =
viewer.mode === 'fit' ? `${percent}% · 适应` : viewer.mode === 'actual' ? '100% · 1:1' : `${percent}%`
$('#lightbox-zoom-out').disabled = viewer.zoom <= MIN_VIEWER_ZOOM + 0.001
$('#lightbox-zoom-in').disabled = viewer.zoom >= MAX_VIEWER_ZOOM - 0.001
$('#lightbox-fit').setAttribute('aria-pressed', String(viewer.mode === 'fit'))
$('#lightbox-actual').setAttribute('aria-pressed', String(viewer.mode === 'actual'))
const rect = image.getBoundingClientRect()
stage.classList.toggle('is-pannable', rect.width > stage.clientWidth + 1 || rect.height > stage.clientHeight + 1)
}
function setViewerZoom(nextZoom, options = {}) {
const shot = currentShot()
const stage = $('#lightbox-stage')
const image = $('#lightbox-image')
if (!shot || !image.complete || !image.naturalWidth) return
const before = image.getBoundingClientRect()
const stageRect = stage.getBoundingClientRect()
const focusClientX = options.clientX ?? stageRect.left + stage.clientWidth / 2
const focusClientY = options.clientY ?? stageRect.top + stage.clientHeight / 2
const imageRatioX = before.width ? clamp((focusClientX - before.left) / before.width, 0, 1) : 0.5
const imageRatioY = before.height ? clamp((focusClientY - before.top) / before.height, 0, 1) : 0.5
viewer.zoom = clamp(nextZoom, MIN_VIEWER_ZOOM, MAX_VIEWER_ZOOM)
viewer.mode = options.mode ?? 'manual'
const actualScale = viewer.fitScale * viewer.zoom
image.style.width = `${Math.max(1, Math.round(shot.width * actualScale))}px`
image.style.height = `${Math.max(1, Math.round(shot.height * actualScale))}px`
const after = image.getBoundingClientRect()
stage.scrollLeft += after.left + imageRatioX * after.width - focusClientX
stage.scrollTop += after.top + imageRatioY * after.height - focusClientY
updateViewerStatus()
}
function resetViewer(mode = 'fit') {
const shot = currentShot()
const image = $('#lightbox-image')
const stage = $('#lightbox-stage')
if (!shot || !image.complete || !image.naturalWidth) return
viewer.fitScale = calculateFitScale()
const nextZoom = mode === 'actual' ? 1 / viewer.fitScale : 1
viewer.zoom = clamp(nextZoom, MIN_VIEWER_ZOOM, MAX_VIEWER_ZOOM)
viewer.mode = mode === 'actual' && viewer.zoom !== nextZoom ? 'manual' : mode
const actualScale = viewer.fitScale * viewer.zoom
image.style.width = `${Math.max(1, Math.round(shot.width * actualScale))}px`
image.style.height = `${Math.max(1, Math.round(shot.height * actualScale))}px`
stage.scrollTo({ left: 0, top: 0 })
updateViewerStatus()
}
function zoomViewerBy(factor, event) {
setViewerZoom(viewer.zoom * factor, {
clientX: event?.clientX,
clientY: event?.clientY,
mode: 'manual',
})
}
function openLightbox(index) {
state.lightboxIndex = index
const shot = state.chapter.media[index]
$('#lightbox-title').textContent = `${state.chapter.title} · ${index + 1}/${state.chapter.media.length}`
$('#lightbox-meta').textContent = `${shot.width}×${shot.height} · SHA-256 ${shot.sha256}`
const image = $('#lightbox-image')
image.alt = shot.alt
image.onload = () => resetViewer('fit')
image.src = shot.original
$('#lightbox-original').href = shot.original
if (!$('#lightbox').open) $('#lightbox').showModal()
if (image.complete && image.naturalWidth) requestAnimationFrame(() => resetViewer('fit'))
}
$('#lightbox-close').addEventListener('click', () => $('#lightbox').close())
$('#lightbox-prev').addEventListener('click', () =>
openLightbox((state.lightboxIndex - 1 + state.chapter.media.length) % state.chapter.media.length)
)
$('#lightbox-next').addEventListener('click', () =>
openLightbox((state.lightboxIndex + 1) % state.chapter.media.length)
)
$('#lightbox-zoom-out').addEventListener('click', () => zoomViewerBy(1 / 1.25))
$('#lightbox-zoom-in').addEventListener('click', () => zoomViewerBy(1.25))
$('#lightbox-fit').addEventListener('click', () => resetViewer('fit'))
$('#lightbox-actual').addEventListener('click', () => resetViewer('actual'))
$('#lightbox-stage').addEventListener(
'wheel',
(event) => {
if (!event.ctrlKey && !event.metaKey) return
event.preventDefault()
zoomViewerBy(event.deltaY < 0 ? 1.18 : 1 / 1.18, event)
},
{ passive: false }
)
$('#lightbox-stage').addEventListener('dblclick', (event) => {
if (viewer.mode === 'fit') setViewerZoom(2, { clientX: event.clientX, clientY: event.clientY })
else resetViewer('fit')
})
$('#lightbox-stage').addEventListener('pointerdown', (event) => {
if (event.pointerType === 'mouse' && event.button !== 0) return
const stage = $('#lightbox-stage')
if (viewer.pointers.size >= 2) return
viewer.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
stage.setPointerCapture?.(event.pointerId)
if (viewer.pointers.size === 2) {
const [first, second] = [...viewer.pointers.values()]
viewer.pinch = {
distance: Math.hypot(second.x - first.x, second.y - first.y),
zoom: viewer.zoom,
}
viewer.drag = null
stage.classList.remove('is-dragging')
} else if (stage.classList.contains('is-pannable')) {
viewer.drag = {
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
left: stage.scrollLeft,
top: stage.scrollTop,
}
stage.classList.add('is-dragging')
}
})
$('#lightbox-stage').addEventListener('pointermove', (event) => {
if (!viewer.pointers.has(event.pointerId)) return
const stage = $('#lightbox-stage')
viewer.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
if (viewer.pointers.size >= 2 && viewer.pinch) {
const [first, second] = [...viewer.pointers.values()]
const distance = Math.hypot(second.x - first.x, second.y - first.y)
setViewerZoom(viewer.pinch.zoom * (distance / Math.max(1, viewer.pinch.distance)), {
clientX: (first.x + second.x) / 2,
clientY: (first.y + second.y) / 2,
})
return
}
if (viewer.drag?.pointerId === event.pointerId) {
stage.scrollLeft = viewer.drag.left - (event.clientX - viewer.drag.x)
stage.scrollTop = viewer.drag.top - (event.clientY - viewer.drag.y)
}
})
const releaseViewerPointer = (event) => {
const stage = $('#lightbox-stage')
if (!viewer.pointers.has(event.pointerId)) return
viewer.pointers.delete(event.pointerId)
viewer.drag = null
viewer.pinch = null
stage.classList.remove('is-dragging')
if (viewer.pointers.size === 1 && stage.classList.contains('is-pannable')) {
const [[pointerId, point]] = [...viewer.pointers.entries()]
viewer.drag = {
pointerId,
x: point.x,
y: point.y,
left: stage.scrollLeft,
top: stage.scrollTop,
}
stage.classList.add('is-dragging')
}
}
$('#lightbox-stage').addEventListener('pointerup', releaseViewerPointer)
$('#lightbox-stage').addEventListener('pointercancel', releaseViewerPointer)
$('#lightbox-stage').addEventListener('lostpointercapture', releaseViewerPointer)
$('#lightbox').addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') $('#lightbox-prev').click()
else if (event.key === 'ArrowRight') $('#lightbox-next').click()
else if (event.key === '+' || event.key === '=') zoomViewerBy(1.25)
else if (event.key === '-') zoomViewerBy(1 / 1.25)
else if (event.key === '0') resetViewer('fit')
else if (event.key === '1') resetViewer('actual')
else return
event.preventDefault()
})
$('#lightbox').addEventListener('close', () => {
viewer.pointers.clear()
viewer.drag = null
viewer.pinch = null
$('#lightbox-stage').classList.remove('is-dragging', 'is-pannable')
})
window.addEventListener('resize', () => {
if (!$('#lightbox').open) return
cancelAnimationFrame(viewer.resizeFrame)
viewer.resizeFrame = requestAnimationFrame(() => {
const previousActualScale = viewer.fitScale * viewer.zoom
viewer.fitScale = calculateFitScale()
const nextZoom =
viewer.mode === 'fit' ? 1 : viewer.mode === 'actual' ? 1 / viewer.fitScale : previousActualScale / viewer.fitScale
setViewerZoom(nextZoom, { mode: viewer.mode })
})
})
async function openIntegrity() {
let integrity
if (location.protocol === 'file:')
integrity = {
entries: [],
note: '离线模式下逐文件哈希见 integrity.json',
contentRootSha256: manifest.contentRootSha256,
}
else integrity = await (await fetch('integrity.json')).json()
$('#integrity-content').innerHTML =
`<div class="integrity-list"><div class="integrity-row"><strong>报告内容根哈希</strong><code>${esc(manifest.contentRootSha256)}</code></div><div class="integrity-row"><strong>历史证据根哈希</strong><code>${esc(manifest.historicalEvidenceContentRootSha256)}</code></div><div class="integrity-row"><strong>最新版回归哈希</strong><code>${esc(manifest.latestPlatformRegressionSha256)}</code></div><div class="integrity-row"><strong>章节</strong><span>${manifest.counts.chapters}</span></div><div class="integrity-row"><strong>截图</strong><span>${manifest.counts.screenshots}</span></div><div class="integrity-row"><strong>完整性条目</strong><span>${integrity.entries?.length || '见离线 integrity.json'}</span></div><div class="integrity-row"><strong>历史证据平台 / Skill</strong><span>${esc(manifest.evidencePlatformCommit)} / ${esc(manifest.agentSkillVersion)}</span></div><div class="integrity-row"><strong>最新回归平台</strong><span>${esc(manifest.latestPlatformRegression.platform.commit)}</span></div></div>`
$('#integrity-dialog').showModal()
}
function openLatestRegression() {
const regression = manifest.latestPlatformRegression
const scenarios = regression.runtime.scenarios
.map((item) => {
const verdict =
item.expectedValidationVerdict === 'fail' && item.observedValidationVerdict === 'fail'
? '违规候选按预期被阻断'
: `独立验证 ${item.observedValidationVerdict}`
return `<article class="regression-case"><div><span class="status pass">通过</span><small>${esc(item.scenarioId)}</small></div><h3>${esc(item.label)}</h3><p>${esc(verdict)} · Solve ${item.settledSolveCalls} 次 · 十部分报告 ${item.tenPartReport ? '完整' : '不完整'}</p><code>${esc(shortHash(item.caseRunSha256))}</code></article>`
})
.join('')
const workspace = regression.runtime.decisionWorkspace
$('#latest-content').innerHTML =
`<div class="regression-dialog-head"><span class="status pass">PASS</span><p>最新平台定向回归</p><h2>${esc(regression.platform.commit.slice(0, 10))} · DesireCore ${esc(regression.platform.version)}</h2><small>${esc(formatTime(regression.testedAt))} · ${esc(regression.platform.source)}</small></div><div class="regression-cases">${scenarios}<article class="regression-case"><div><span class="status pass">通过</span><small>Decision Workspace r${workspace.revision}</small></div><h3>人 + Agent 共管决策工作台</h3><p>业务、模型、证据三视图可用;定义通过,缺少模型映射时保持阻断;${workspace.viewport.join('×')} 无横向溢出。</p><code>review: ${esc(workspace.reviewState)}</code></article></div><section class="regression-boundary"><h3>证据边界</h3><ul>${regression.claimBoundary.map((item) => `<li>${esc(item)}</li>`).join('')}</ul><p><strong>历史全量证据:</strong>${regression.historicalEvidence.chapters} 章、${regression.historicalEvidence.screenshots} 张截图、${regression.historicalEvidence.representativeStabilityPasses} 次代表性稳定运行,归属于平台 ${esc(regression.historicalEvidence.platformCommit)}。</p></section>`
$('#latest-dialog').showModal()
}
window.addEventListener('hashchange', () => {
const index = hashIndex()
if (index !== state.current) loadChapter(index)
})
renderShell()
loadChapter(hashIndex()).catch((error) => {
$('#chapter').innerHTML =
`<section class="section-shell"><h1>报告载入失败</h1><pre>${esc(error.stack || error.message)}</pre></section>`
throw error
})
})()