docs/design/web-shell/chat-transcript-contract-prevalidation.md
文档地位:本方案的唯一规范性设计文档
实施方式:两个 MR 按顺序合入
当前状态:MR1 契约预验证已在当前分支准备;MR2 生产迁移尚未进入当前分支
当前门禁:overall: "fail",selectedVscodePath: null
本文档同时定义最终目标架构、公共契约、安全约束、两个 MR 的实施边界和退出门禁。代码虽然拆成两个 MR,但不会为 MR2 新建另一份设计文档。
后续规则如下:
本方案不会新建独立 ChatPanel 包,也不会在第一版发布新的跨宿主消息模型。四端共享的最小运行时语义继续建立在现有 DaemonTranscriptBlock[] 上;ChatTranscriptModel 是本文档对该只读边界的逻辑名称,不要求 MR1 新增生产类型。
最终数据关系是:
native source
→ source adapter / canonical projector
→ ChatTranscriptModel (readonly DaemonTranscriptBlock[])
→ live/readonly renderer
ChatTranscriptModel
→ document/export allowlist projector
→ ExportTranscriptDocumentV1
→ WebShellTranscript document mode
→ version-bound HTML
其中:
ChatRecord[] 复用规范投影得到 ChatTranscriptModel,再单向转换为安全的 ExportTranscriptDocumentV1;rawInput、rawOutput 和现有 Turn Output 语义不得改变;preview/resultPreview 的安全消费只在 document/export 路径启用;整个工作按顺序拆成两个 MR:
| 消费端 | 当前事实 | 本方案处理 |
|---|---|---|
| Web/Qwen Server | daemon state 经 SDK reducer 产生 DaemonTranscriptBlock[],完整 WebShell 渲染 | 保持生产路径不变;作为语义和兼容基线 |
| Qwen Tauri Desktop | 构建并复制同一 WebShell 产物 | 不增加 Desktop adapter;MR1 不认证安装产物行为 |
| VS Code | QwenAgentManager 仍以 ACP、自有消息状态和现有 Webview 时间线为主;仓库存在 daemon connection spike | MR2 选择 direct-daemon 或 ACP 薄转换,只替换时间线 |
| HTML Export | 产品和 integration runner 仍有独立 HTML/ChatViewer 路径 | MR2 收敛到版本绑定的 WebShellTranscript document mode |
| OpenWork/Craft Electron | 独立聊天实现 | 本方案范围外 |
当前 WebShellTranscript:
readonly DaemonTranscriptBlock[];readonly render mode;document render mode。| 范围 | 当前状态 | 结论 |
|---|---|---|
| MR1 fixtures/schema/hash/capability matrix | 当前分支已准备 | PASS |
| ChatRecord → SDK → Web Shell 默认 adapter 等价性 | 当前分支已准备 | PASS |
write_file → Turn Output 完整 diff 回归 | 当前分支已准备 | PASS |
| direct-daemon stable identity | partial-prepend 可重复失败 | FAIL |
| ACP stable identity | partial-prepend 可重复失败 | FAIL |
| VS Code 路径选择 | 前置 identity 未通过 | BLOCKED |
| Export document schema | V1 目标 schema 已冻结 | DEFERRED implementation |
| Export builder/document mode/HTML wiring | 当前分支无生产代码 | DEFERRED to MR2 |
“MR1 测试通过”表示当前事实和 FAIL blocker 能稳定复现,不表示迁移门禁已经通过。
projectChatRecordsToDaemonTranscript(),不复制 replay 规则;@qwen-code/web-shell/chat-panel 或新的通用 ChatPanel framework;flowchart LR
DE["daemon events"] --> DR["SDK normalizer/reducer"]
ACP["ACP session/update"] --> AA["ACP thin source adapter"]
CR["ChatRecord[]"] --> RP["record export policy"]
RP --> CP["canonical ChatRecord projector"]
DR --> MODEL["ChatTranscriptModel\nreadonly DaemonTranscriptBlock[]"]
AA --> MODEL
CP --> MODEL
MODEL --> WEB["Web/Qwen full WebShell"]
MODEL --> DESKTOP["Tauri packaged WebShell"]
MODEL --> VST["VS Code WebShellTranscript timeline"]
MODEL --> EP["document/export allowlist projector"]
EP --> EDOC["ExportTranscriptDocumentV1"]
EDOC --> VALIDATE["schema + budget validation"]
VALIDATE --> DOC["WebShellTranscript document mode"]
DOC --> HTML["version-bound HTML export"]
VSHOST["VS Code host actions/composer/session"] -. callbacks .-> VST
| 层级 | 负责 | 不负责 |
|---|---|---|
| source adapter | 协议归一化、source provenance、scope/generation admission | UI、宿主副作用 |
| ChatTranscriptModel | 有序只读消息语义、稳定 block identity、展示所需层级 | composer、活动权限响应、传输、session mutation |
| WebShellTranscript | Markdown、thinking、工具、计划、图片和只读时间线展示 | daemon 连接、持久化、权限 API |
| VS Code host adapter | 连接路径、scope/generation、callbacks、feature flag、原生操作 | 复制聊天 renderer |
| export projector | record policy、逐字段 allowlist、ID 重写、预算与 diagnostic | live side-channel、raw payload 透传 |
| HTML shell | schema/version 校验、CSP、document mode、主题/打印 | 工具执行、远程 runtime 下载 |
宿主始终是传输、session 和副作用的事实来源。共享 renderer 不得通过 DOM 反向恢复业务状态。
import type { DaemonTranscriptBlock } from '@qwen-code/sdk/daemon';
interface ChatTranscriptModel {
readonly blocks: readonly DaemonTranscriptBlock[];
}
interface TranscriptAdapterContext {
readonly scopeKey: string;
readonly generation: number;
}
ChatTranscriptModel 是逻辑契约名。除非 MR2 的真实消费者证明现有类型无法表达必需语义,否则生产代码继续直接传递 DaemonTranscriptBlock[],不发布上述 wrapper。
版本系统必须分离:
fixtureVersion;schemaVersion;rendererVersion。scopeKey 和 generation 属于 adapter context,不进入 block 列表,也不进入导出文档。
| 能力 | block 语义 | 必测状态 |
|---|---|---|
| 用户/assistant 文本 | user / assistant | streaming、空 delta、usage、replay |
| thinking/commentary | thought | 与 assistant 交错、结束、折叠 |
| 图片 | text block images | 多图、非法 MIME、缺失/超限资源 |
| 工具 | tool + tool identity + typed preview | pending、完成、失败、取消、并行、嵌套、后台、replay |
| shell | shell / user_shell | stdout/stderr、增量、退出、重连 |
| 计划/Todo | tool block 的类型化计划展示语义 | revision、priority、依赖、完成、失败 |
| 权限历史 | permission | pending、approved、rejected、cancelled、expired、resolved |
| 状态与错误 | status / error / prompt_cancelled | 取消、截断、不完整 replay、连接/模型错误 |
| 未知输入 | 安全 fallback、明确排除或阻断 diagnostic | 不得静默丢失用户可见内容 |
计划当前不是独立 block kind。不能仅凭工具名称声明支持;fixture 必须证明用户可见的标题、步骤、状态、revision 和依赖仍存在。
permission block 只描述时间线历史,不授予调用权限 API 的能力。
遇到未知输入时只能选择:
禁止把任意 unknown、meta、details、content 或 raw object 作为逃生口。diagnostic 对外只包含 code、severity、count 和完整性标记,不能回显 prompt、token、绝对路径或工具参数。
最终定义三种模式:
| 模式 | 输入 | raw 语义 | 交互与资源策略 |
|---|---|---|---|
interactive | live runtime blocks | 保持现状,以 raw 为完整工具事实来源 | 完整 WebShell 交互 |
readonly | live/replayed runtime blocks | 保持现状,以 raw 为完整工具事实来源 | 无 composer/permission response;宿主 callback 可选 |
document | ExportTranscriptDocumentV1 的安全 renderer input | 禁止 raw;只消费 typed safe preview/result | 无宿主动作、无虚拟化、无主动远程资源 |
兼容性不变量:
rawInput、rawOutput、content 和 permission toolCall 读取优先级;preview/resultPreview 即使同时存在,也不能改变默认 adapter 输出;write_file Turn Output 继续优先使用完整 tool.args.content,不能被截断的 preview.newText 替代;如果 ExportTranscriptBlockV1 不能类型安全地直接交给 WebShellTranscript,MR2 只允许增加一个纯函数 document adapter。该 adapter 只能把安全 DTO 映射为 renderer input,不能恢复 raw payload、复制 reducer 或演变成第二套消息模型。
在同一 scopeKey 和同一原生语义事件链中,同一个 block 的 identity 必须在以下操作后保持不变:
不同 session、branch、原生协议或独立导出文件不要求产生相同字符串 ID。跨 adapter 比较使用语义与 provenance,不虚构全局 ID。
scopeKey 是宿主为 session + branch 分配的稳定不透明 key;同一 session 重连保持不变,切换 session/branch 必须变化;generation 是同一 scope 每次重新绑定 transport 时递增的本地代数;generation 不参与 block ID;{scopeKey, generation};| block | 首选 source identity | 缺失处理 |
|---|---|---|
| tool | toolCallId + scope | 无 tool identity 则失败 |
| permission | requestId + scope | 无 request identity 则失败 |
| persisted text/thought | sourceRecordIds + lane + 持久化 segment identity | 无法确定性得到则失败 |
| live text/thought | prompt identity + lane + producer-stamped segment identity | 禁止用 ordinal/content hash 兜底 |
| shell/user_shell | 权威 shell/event identity + scope | 缺失则失败 |
| status/error/cancelled | 权威 event identity + scope | 缺失则失败或并入已有稳定 block |
event cursor 只表示传输顺序。对由多个 delta 合并的文本 block,cursor 会随最后一个 delta 改变,因此不能单独作为 segment identity。
MR2 在 source producer/admission 边界建立 segment identity:
segmentId;segmentId 的相邻文本不能仅因当前窗口相邻而合并成同一 identity block;稳定 block ID 由版本化确定性函数从 {scopeKey, blockKind, nativeSourceIdentity} 派生。父子 block 引用必须同步重写。默认 Web/Tauri reducer 的 ordinal runtime ID 可保持兼容;稳定投影只进入明确需要它的 VS Code adapter/probe,除非后续单独证明全局替换无回归。
MR1 的 read-only probe 使用当前 normalizeDaemonEvent 和 reduceDaemonTranscriptEvents:
当前结果:
| Candidate | partial-prepend | 原生文本 identity | MR1 gate |
|---|---|---|---|
| direct-daemon | ordinal block ID 漂移 | user/thought/assistant 缺失 | FAIL |
| ACP | ordinal block ID 漂移 | user/thought/assistant 缺失 | FAIL |
MR2 合入前,两条候选都必须运行完整 identity matrix 并通过;若要永久放弃其中一条,必须先在本文档中记录范围变更与理由,不能只从测试中删除失败候选。
稳定 block ID 只是必要条件。renderer 会合并 assistant 文本、合组相邻工具并嵌套 thought/sub-agent,因此一个 block 不一定对应一个 DOM 节点。
测试和宿主接缝使用以下逻辑证据:
interface TranscriptRenderedItemEvidence {
readonly renderedItemId: string;
readonly sourceBlockIds: readonly string[];
readonly sourceToolCallIds: readonly string[];
readonly capabilities: readonly (
| 'copy'
| 'copy-all'
| 'copy-last-reply'
| 'edit-user-message'
| 'open-file'
)[];
}
规则:
renderedItemId 来自稳定 source IDs、tool call identity 和固定分组边界;sourceBlockIds 必须保留,每个 tool call 可单独寻址;MR2 只增加由失败 fixture 证明必要的最小 callback/handle,不能借此创建通用宿主框架。
daemon events
→ SDK normalizer/reducer
→ stable identity projection
→ scoped ChatTranscriptModel
→ WebShellTranscript readonly timeline
必须验证:loopback/auth/workspace scope、session 生命周期、SSE replay、permission ownership、VSIX bundle、CSP、sidebar/editor tab、callbacks 和 feature flag。direct-daemon 不能因仓库已有 connection spike 就自动成为生产路径。
ACP session/update
→ thin source normalizer
→ SDK transcript reducer
→ stable identity projection
→ scoped ChatTranscriptModel
→ WebShellTranscript readonly timeline
ACP adapter 只做协议归一化和 provenance 传递:
两条候选先使用相同 fixture、identity matrix 和 render/action probe。MR2 只能选择满足以下条件的路径:
ACP 是当前生产基线,因此在两条路径同等可行时优先 ACP 薄转换;这不是 MR1 的预选结果。最终选择及舍弃理由写入 capability matrix 和本文档状态表。
ChatRecord[]
→ record-level export policy
→ projectChatRecordsToDaemonTranscript()
→ ChatTranscriptModel
→ safe document projector
→ ExportTranscriptDocumentV1
→ schema/budget validation
→ WebShellTranscript document mode
→ version-bound HTML
HTML 路径不能复制 ChatRecord replay/reducer,也不能把完整 ChatRecord、DaemonTranscriptState 或 runtime blocks 直接序列化进文件。
ExportTranscriptDocumentV1 是 ChatTranscriptModel 的单向安全派生物:
ExportTranscriptDocumentV1 = projectForDocument(ChatTranscriptModel)
它是版本化文档 DTO,不是第二套运行时 transcript model,也不回流到 live session。
interface ExportTranscriptDocumentV1 {
readonly schemaVersion: 1;
readonly rendererVersion: string;
readonly blocks: readonly ExportTranscriptBlockV1[];
readonly diagnostics: readonly {
readonly code: string;
readonly severity: 'info' | 'warning' | 'error';
readonly count: number;
}[];
readonly metadata: ExportMetadataPresentationV1;
}
顺序不能调换:
若拒绝的记录是后续可见记录的必要因果节点,导出标记不完整或直接失败,不能重连 parent 伪造会话。
JSON Schema 以 additionalProperties: false 封闭每种 block:
| block | V1 允许字段 |
|---|---|
| 所有 kind | 文档内 opaque id、kind;三个 block 时间字段固定为 0 |
user/assistant/thought | text、安全 raster images、collapsed、安全 parent 引用、数值 allowlist 后的 usage;streaming=false |
tool | opaque toolCallId、title、终态 status、安全 toolName/toolKind、typed preview/resultPreview、安全父子引用、短 subagentType |
shell | text、stream |
user_shell | text、command、脱敏 workspace-relative cwd、stream |
permission | opaque requestId、title、label/description、raw:null options、typed preview、opaque tool identity、安全 resolved 终态 |
status/error | text、已知 code/errorKind、安全 source |
prompt_cancelled | 安全 reason |
debug | V1 默认拒绝;产生 code/count diagnostic |
以下字段永不直接导出:
eventId、serverTimestamp、sourceRecordIds、promptId、branchRecordId;content、locations、details、rawInput、rawOutput;sessionId、toolCall 和原始 response token;data、debugReason;preview 进入文档前必须按 kind 重新构造并递归应用长度、URL、path、image 和数组限制;ask_user_question/permission 的 raw 固定为 null;resultPreview,不能写入 raw result;原生 ID 不直接进入导出文档。projector 按最终 canonical traversal 为 block、tool、permission 和父子引用建立文档内 opaque 映射,并同步重写所有引用:
这套 ID 与 live stable identity 是两个域:前者服务单个离线文档,后者服务同一 session scope 的重放和动作寻址。
interface ExportMetadataPresentationV1 {
readonly title?: string;
readonly startedAt?: string;
readonly exportedAt: string;
readonly complete: boolean;
readonly truncated: boolean;
readonly projectName?: string;
readonly repository?: string;
readonly gitBranch?: string;
readonly model?: string;
readonly channel?: string;
readonly promptCount?: number;
readonly contextUsagePercent?: number;
readonly contextWindowSize?: number;
readonly totalTokens?: number;
readonly filesWritten?: number;
readonly linesAdded?: number;
readonly linesRemoved?: number;
}
metadata 逐字段构造:
projectName 只能是 basename 或显式安全标签;exportedAt 只用于文档元数据,不能参与 identity 或语义快照。| 预算 | V1 上限 |
|---|---|
| transcript blocks | 1,000 |
| 单文本字段 | 400 KiB UTF-8;schema maxLength 另设 409,600 结构上限 |
| 全部可见文本 | 8 MiB UTF-8 |
| 单 raster 解码后 | 8 MiB |
| 全部 raster 解码后 | 16 MiB |
| JSON envelope | 32 MiB UTF-8 |
| JSON/preview depth | 16 |
| 单数组元素 | 1,000 |
| 单对象属性 | 1,000 |
| Mermaid/chart/diff/highlight 富任务 | 合计 100,超出显示源码 fallback |
JSON Schema 无法表达 UTF-8 总字节、总文本、总图片和 envelope 预算,因此 Node builder 与浏览器 validator 都必须执行显式预算检查;schema maxLength 只是结构上限,不能替代字节预算。
资源规则:
image/png、image/jpeg、image/gif、image/webp 的受限 base64 raster;V1 拒绝动画内容;file:、blob:、远程图片和非预期 data URI;document mode 必须:
ExportTranscriptDocumentV1 的安全 renderer input;unsafe-eval、远程 WASM、远程 grammar、字体或动态 renderer 的资源。HTML 使用与 CLI build 精确绑定的 renderer。禁止 latest、版本范围和运行时远程解析。
最低安全要求:
default-src 'none';connect-src 'none'、object-src 'none'、frame-src 'none'、media-src 'none';base-uri 'none'、form-action 'none';data: 或明确登记的同包资源;style-src-attr 的最小例外,DTO 不接受 style 字段;truncated=true、complete=false;complete=true,同时记录 warning;schemaVersion;rendererVersion 必须是精确 CLI 版本或内容 hash,禁止 latest、~、^、比较范围和 *;fixtureVersion 只描述测试录制格式,不代替 export schema version。integration-tests/fixtures/chat-transcript-contract/v1/
├── capability-matrix.md
├── cases/
│ └── representative/
│ ├── manifest.json
│ ├── daemon-events.jsonl
│ ├── acp-session-updates.jsonl
│ ├── chat-records.jsonl
│ ├── expected-model.json
│ ├── expected-render-items.json
│ ├── expected-export.json
│ └── expected-gate.json
└── schema/
├── manifest.schema.json
└── export-transcript-document-v1.schema.json
规则:
complete: true 表示 fixture case 完整,不代表迁移 gate PASS。toolCall 仍可安全显示历史;write_file 同时存在完整 raw content 与截断 preview;MR1 的紧凑矩阵每项记录 Capability、当前 source/path、Fixture/Evidence、Owner 和 Gate;其中 path 合并 native source 与当前 mapping,evidence 合并 contract/render 证据。MR2 增加真实消费者时再拆出 Consumers 和 Render/action mapping,不能用省略列隐藏未验证能力。
required 项不能以 unknown、TBD、人工截图或“测试能运行”通过。PASS、FAIL、BLOCKED、DEFERRED 必须分别使用,不能把预计后续修复写成当前 PASS。
只包含:
write_file → Turn Output 完整 diff 回归;.js ESM import specifier;这些改动不得改变 Web Shell runtime 语义。明确不包含:
ExportTranscriptDocumentV1 builder;document render mode、Mermaid/CSP/browser probe;MR1 验收:测试通过,同时 expected-gate.json 保持 overall: "fail"、两候选 FAIL、selectedVscodePath: null。
MR2 中每项生产代码必须有真实消费者。实施顺序:
WebShellTranscript,保留现有 composer/permission/session/host actions;MR2 不能只修改 expected-gate.json 或恢复拆分前整包代码。应按上述消费者顺序选择性迁移备份实现,并重新对照当前 main。
flowchart TD
INPUTS["daemon / ACP / ChatRecord fixtures"] --> SEM["semantic projection"]
INPUTS --> IDS["block identity matrix"]
SEM --> RENDER["renderer item/action probe"]
SEM --> EXPORT["export allowlist projector"]
EXPORT --> SCHEMA["schema + budget + canary"]
SCHEMA --> BROWSER["document browser probe"]
RENDER --> HOSTS["Web / Tauri / VS Code"]
BROWSER --> HTML["HTML Export"]
IDS --> GATE{"overall gate"}
RENDER --> GATE
HOSTS --> GATE
HTML --> GATE
npm run build
npm run typecheck
cd integration-tests && npx tsc -p tsconfig.json --pretty false
npx vitest run chat-transcript-contract.test.ts
cd ../packages/web-shell && npx vitest run client/components/artifacts/turnOutputSelectors.test.ts
MR1 证明:fixture/hash/schema 可重复、默认 raw runtime 兼容、Turn Output 完整 diff 不回归,以及两条 identity blocker 可重复。
MR1 不以源码文本断言认证 Desktop 打包行为。Web/Tauri 的现有构建检查继续作为回归信号;安装产物中 Web Shell 文件布局与可加载性的行为 smoke 属于 MR2 Packaging gate,在当前矩阵中保持 DEFERRED。
| 范围 | 必须覆盖 |
|---|---|
| SDK/source | segment provenance、append/prepend/replay、parent refs、默认 ordinal 兼容 |
| ACP | live/history 同 identity、缺失 provenance fail closed、迟到 update |
| VS Code | direct/ACP probes、选定路径、scope/generation、callbacks、feature flag、legacy parity |
| Web Shell | interactive/readonly raw 兼容、document safe-only、render/action identity |
| Export builder | record policy、per-kind allowlist、opaque IDs、metadata、diagnostic、version |
| Browser | schema failure、zero network、CSP、canary、find/copy/print、最大预算 |
| Packaging | Web/Tauri regression、VSIX 三平台、CLI renderer 版本绑定、integration runner 收敛 |
Passing test 也必须反向审计:测试是否断言了正确语义、是否加载当前构建产物、是否真的覆盖真实消费者,不能用静态 source assertion 替代浏览器或 VSIX 行为验证。
MR1 的正确结论是 FAIL evidence:
{
"overall": "fail",
"selectedVscodePath": null
}
MR2 只有在上述三组门禁和真实消费者验证全部通过后才能改为 PASS。任何 required 组失败都阻断 MR2 合入;不能人工豁免,也不能先 assert false、合入生产代码后在同一证据缺失状态下只把期望改成 true。
Web/Qwen 和 Tauri 不迁移。若 MR2 对共享组件的改动导致默认模式回归,应回滚 MR2,而不是为两端增加兼容 adapter。
| 风险 | 控制 |
|---|---|
| 证据 MR 膨胀成生产实现 | MR1 文件范围白名单;生产变更全部留给 MR2 真实消费者 |
| 测试模型变成第二套 model | ChatTranscriptModel 只命名现有 blocks;不发布 wrapper |
| ordinal ID 在简单 replay 中假稳定 | 强制 multi-delta、partial-prepend、overlap replay |
| block ID 稳定但 render/action 不稳定 | 单独 item/source/action probe |
| ACP 与 direct 只验证一条 | 两候选共用 matrix;删除候选必须更新本文档 |
| document projection 污染 runtime | mode 隔离;默认 raw compatibility tests |
| preview 截断破坏 Turn Output | write_file 完整 raw content 回归 |
| raw/metadata 泄漏 | 两层 allowlist、closed schema、canary 和字节扫描 |
| Markdown/图片绕过网络策略 | 统一资源 policy、CSP 和浏览器全请求拦截 |
| Mermaid 全局配置污染 | 仅 document context 启用限制,缓存按 mode 隔离 |
| document 无虚拟化导致资源耗尽 | builder/browser 双预算、源码 fallback、最大文档测试 |
| snapshot update 掩盖 blocker | hash、显式 fixture diff、gate 由测试生成 |
| 备份实现与最新 main 漂移 | MR2 选择性迁移并重新审计,不整包恢复 |
整个总体设计完成需要同时满足:
ExportTranscriptDocumentV1、版本绑定 renderer 和 document mode;