mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 06:51:59 +00:00
265ba0c91d
两件事,一条心:把「网页上点数字看得到的效果」带出系统外。
① 报告导出:新增 GET /api/runs/{run_id}/report.html(app/report_export:build + 模板 + 浏览器端脚本)。
正文由服务端按报告文档的块、句、片段渲染成 HTML——不执行脚本也读得到整份报告(附件预览常常不执行脚本);
执行脚本后,带出处的数字、无证据的数字、可疑的表名字段名都点得开,面板是网页上证据面板同一个拼法的
出处链(api/evidence._chain):指标步骤带口径卡名与版本、原式、代入式、每个输入取自哪次查询的哪一格、
复算、双盲复算;查询步骤带 SQL 和被引用的行;数据源遮罩列照网页上的规矩写「已遮罩」。封存状态逐项标明;
没封存的运行照常导出(和网页上读报告一个道理)。同样的引用共用一条链,长报告不把整份快照抄几十遍。
入口在问数据答案和运行详情成果上的「导出」(带证据的报告给 HTML,Markdown 留一个小的;旧运行照旧只有
Markdown)——api.reportExport + AssistantStream 的 AnswerActions。
② 可验证报告(bundle)的数字详情补齐:点开数字原来只有出处和值,现在多一节口径卡——指标(名 + id)、
单位、原式、代入式、输入(取值 ← 路径、取自哪次查询的哪一行哪一列)、复算;输入指向的查询快照也随文件
附带(遮罩规则同嵌入那一套,build._card_input_queries),④ 引用链按「已核对的口径卡的输入」认它们
(哈希对不上的卡不认,免得替被改过的卡圆)。收件人不连服务也能看到每个数怎么算出来的。
测试:后端 tests/test_report_export.py(真跑运行导出,正文、出处链、遮罩、只读、404/409、共用链);
bundle_samples 的口径卡换成带原式/代入式/输入/复算的真实形状,另加 metric-only 样本页(只引用指标的
报告,快照从口径卡的输入这一条路附带);check-bundle 断言数字详情和这条引用链,加上篡改;新增
scripts/check-report-export.mjs(真导出断网点开、不执行脚本也读得到、成果上的导出按钮、旧答案没有
HTML 按钮、409 提示),挂进 check-all。README 补「导出两份」一节。全量 pytest 通过;check-bundle、
check-report-export、check-stream、check-evidence、check-chat 全部通过(check-runs 5 项失败为基线
上就有的沙箱数据形态问题,与本次无关)。
33 lines
1.8 KiB
TypeScript
33 lines
1.8 KiB
TypeScript
/**
|
||
* 报告导出的接口(/api/runs/{run_id}/report.html),挂在 api.reportExport 上。
|
||
*
|
||
* 请求函数(get / post / del……)和 ApiError 从 ./http 导入,不要从 ./client 导入:client.ts 要导入
|
||
* 这个文件把它挂到 api 上,反过来导入就成了循环(见 http.ts 开头的说明)。Content-Disposition 的文件名
|
||
* 解析和可验证报告(./bundle)是同一个拼法,直接复用那边导出的 dispositionName。
|
||
*/
|
||
import { ApiError, BASE, NETWORK_MESSAGE, actorHeader, failure, report } from './http'
|
||
import { dispositionName } from './bundle'
|
||
|
||
export const reportExport = {
|
||
/**
|
||
* 下载报告导出(GET /runs/{id}/report.html,附件):回来的是文件本身和服务端给的文件名,没给就是 null,
|
||
* 由调用方按问题或答案补。reportNode 点名报告撰写节点;点名的节点这次运行没有、或者这次运行没有报告文档时
|
||
* 服务端回 404,报告文档与哈希不一致回 409,ApiError 的 message 是给人看的原因
|
||
*/
|
||
download: async (runId: string, reportNode?: string): Promise<{ blob: Blob; name: string | null }> => {
|
||
const path = `/runs/${encodeURIComponent(runId)}/report.html`
|
||
+ (reportNode ? `?report=${encodeURIComponent(reportNode)}` : '')
|
||
let res: Response
|
||
try {
|
||
res = await fetch(BASE + path, { headers: actorHeader() })
|
||
} catch (e: any) {
|
||
const err = new ApiError(0, NETWORK_MESSAGE, { kind: 'network', raw: `${e?.name ?? 'Error'}: ${e?.message ?? e}` })
|
||
report(false, err)
|
||
throw err
|
||
}
|
||
if (!res.ok) throw failure(res.status, res.statusText, await res.text().catch(() => ''), path)
|
||
report(true)
|
||
return { blob: await res.blob(), name: dispositionName(res.headers.get('Content-Disposition')) }
|
||
},
|
||
}
|