feat(transcript): 归档详情内回放执行/复审转录 (tsk_O9q0_u78_NPo)

- API:新增 GET /api/runs/:id/transcript,读 run.transcriptRef 指向的
  jsonl,逐行解析成脱敏后的结构化消息数组。校验 runId 合法且关联任务/
  项目存在;路径只允许在 transcriptDir 下(防目录穿越)。
- 脱敏:只透传 assistant 文本 / tool_use(名+摘要参数) / tool_result(截断)
  / 结论 / 模型回退;命中 secret/token/env 等敏感键名的值剔除为 [已脱敏]。
- 看板 archiveModal:执行历史从"文件路径"换成可展开的消息流,按 type
  分类渲染(文本/工具调用/工具结果/结论/回退),长内容折叠,复用 mdToHtml。
  executor 与 reviewer/security run 并列可回放。
- Store:新增 getRun(runId)。
- 测试:transcript 解析(样例 jsonl/脱敏/截断/坏行容错/路径穿越)+ 端点
  inject(合法/未知 runId/无转录/越界路径)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 10:42:57 +08:00
parent 872354cf7c
commit 0957a9049a
6 changed files with 512 additions and 6 deletions
+22
View File
@@ -10,6 +10,7 @@ import { resolvedExecutorModels } from '../executor/models.js';
import { mergeBranch } from '../executor/merge.js';
import { git, removeWorktree } from '../executor/worktree.js';
import { resolveLogo, LOGO_MIME } from './logo.js';
import { readTranscript, TranscriptError } from '../executor/transcript.js';
import { createReadStream } from 'node:fs';
import { createUsageFetcher, type UsageInfo } from '../daemon/usage.js';
@@ -196,6 +197,27 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
return store.listRuns(id);
});
// 执行转录回放:读 run.transcriptRef 指向的 jsonl,逐行解析成脱敏后的结构化消息数组。
// 校验:runId 合法 → 关联任务/项目存在(可访问)→ 有转录记录 → 路径在 transcriptDir 下(防穿越)。
app.get('/api/runs/:id/transcript', (req, reply) => {
const { id } = req.params as { id: string };
const run = store.getRun(id);
if (!run) return reply.code(404).send({ error: `run 不存在: ${id}` });
const task = store.getTask(run.taskId);
if (!task) return reply.code(404).send({ error: `run 关联任务不存在: ${run.taskId}` });
if (!store.getProject(task.projectId)) return reply.code(404).send({ error: `run 关联项目不存在: ${task.projectId}` });
if (!run.transcriptRef) return reply.code(404).send({ error: '该 run 没有转录记录' });
try {
const messages = readTranscript(run.transcriptRef);
return { runId: run.id, taskId: run.taskId, kind: run.kind, status: run.status, messages };
} catch (e) {
if (e instanceof TranscriptError) {
return reply.code(e.code === 'invalid_path' ? 400 : 404).send({ error: e.message });
}
throw e;
}
});
// 单任务全量事件(升序):归档详情的状态流转时间线
app.get('/api/tasks/:id/events', (req) => {
const { id } = req.params as { id: string };