# 开发计划 > 每个里程碑都必须有**可观察的验收证据**才能算完成。见 `AGENTS.md` 的验收口径。 --- ## 进度总览 > **验收进展(2026-10-18 复核)**:M1 / M3 已过**真机验收**——重启后跑通"建画布 → 读回 → 改标签 > → 渲染 → **模型真的看见了图**",原始记录见 > `~/Projects/Workspaces/DSH/ops/records/2026-10-17-dsh-graphics-e2e-verification.md`, > 验收产物归档在 `examples/`。 > > 宿主之外另有两层护栏:`tests/integration.mjs` 用假服务层驱动 `apply()` 与工具的 `execute()` > (**16 项集成测试全过**),其中假服务层**忠实复刻了 `fs-sandbox` 的兜底行为** > (不传 per-call 策略就用无 session 的部署默认策略),所以 M1 那个"工作区内写入被拒"的 bug > 被固化成了回归测试——谁再把 `standingPolicy()` 从 `writeText` 调用里拿掉,测试立刻失败。 > > M4 编辑面板已落地,并在真机使用反馈后迭代(手动保存 / 引用到输入框 / 输入框菜单快捷建画布)。 > 剩余收尾集中在 M5:把"用户在面板里改 → Agent 读到改动"的闭环留档,并补用法截图。 | 里程碑 | 内容 | 状态 | |---|---|---| | **M0** | 项目结构与设计文档 | ✅ 完成(`74fadb2`) | | **M1** | 宿主侧最小可用:工具注册 + 画布读写 | ✅ 真机验收通过(`7a1d948`) | | **M2** | 渲染管线:scene → SVG → PNG,确定性 | ✅ 完成(10 项测试) | | **M3** | 图像回传:模型真的能"看见"图 | ✅ 真机验收通过(`7a1d948`) | | **M4** | 客户端 UI:Excalidraw 编辑面板 | ✅ 代码完成(`405d39f` 落地,`b5390a3` 按真机反馈迭代);交互验收未留档 | | **M5** | 端到端验收 + 文档收尾 | 🔄 进行中(用法/限制已补;闭环留档与截图待收尾) | --- ## M0 · 项目结构与设计文档 ✅ **产出**:项目结构、`package.json` 骨架、`cordis.patch.yml`、`README.md`、`AGENTS.md`、 `docs/design.md`、`docs/plan.md`、`docs/research.md`。 **验收**:文件存在且内容自洽;`package.json` 是合法 JSON;`cordis.patch.yml` 是合法 YAML。 --- ## M1 · 宿主侧最小可用 > **状态(2026-10-18 复核)**:✅ 真机验收通过。25 项纯函数冒烟测试全通过;插件已装入 `desktop` > profile,`graphics_list` / `graphics_read` / `graphics_create` / `graphics_update` 四个工具 > (加上 M3 的 `graphics_render`,现共 5 个)经 `cordis_inspect_query` > (`platform: host`、`provider: Tool`、`method: listTools`)确认已注册。 > > 重启后真机跑通:`graphics_create` 在工作区内建出合法 `.excalidraw`;`graphics_update` 改标签生效 > (`+0 ~2 -0`);越界路径被 `FS_SANDBOX_DENIED` 拒绝,且诊断行里的 `workspaceRoot` 正是会话工作区。 > 原始记录见 `~/Projects/Workspaces/DSH/ops/records/2026-10-17-dsh-graphics-e2e-verification.md`, > 画布产物见 `examples/`。 > > 已定位并修复的问题:`ctx.fs.writeText` 不传 per-call 沙箱策略时,`fs-sandbox` 会退到 > **无 session 的默认策略**,其可写根不含会话工作区 → **工作区内的写入也被拒**。 > 修复在 `lib/tools.mjs` 的 `standingPolicy()`,真机重启后验证通过。 ### 任务 1. 解决 **Q2 / Q3 / Q5**(`ctx.fs` 的写与列目录签名、`defineTool` schema DSL 细节)—— 读 npm 上 `@deepseek-ai/dsh-fs@0.2.0-rc.2` 与 `@deepseek-ai/dsh-tools@0.2.0-rc.2` 的 `.d.ts`。 2. `lib/scene.mjs`: - 语义化元素描述 → 规范化 Excalidraw 元素(补齐 `id` / `seed` / `version` / `versionNonce` / `updated` / `boundElements`) - 读画布 → 结构化摘要(元素按类型统计、包围盒、文字、frame 分组) - 写画布:经 `ctx.fs`,带版本防护 3. `lib/tools.mjs` + `lib/index.mjs`:注册 `graphics_list` / `graphics_read` / `graphics_create` / `graphics_update` 4. 安装进 `desktop` profile 并确认工具行真的装载。 ### 验收 - [x] `cordis_inspect_query` 能查到 `graphics_*` 工具行(现共 5 个,含 M3 的 `graphics_render`) - [x] 让 Agent 调 `graphics_create` 建一张画布 → 工作区里真的出现合法 `.excalidraw` 文件, 用 `jq` 能解析、`elements` 非空、每个元素都有 `id`/`seed`/`version`(见 `examples/demo.excalidraw`) - [x] `graphics_read` 返回的摘要在会话日志里可见,且**不包含整份元素数组**(无 `seed`/`versionNonce` 泄漏) - [x] `graphics_update` 改一处文字后,文件内容真的变了(`+0 ~2 -0`),且其它元素字节未动 - [x] 越界路径(`../../etc/passwd`)被 `ctx.fs` 拒绝,错误码是 `FS_*` 而不是崩溃(`FS_SANDBOX_DENIED`) --- ## M2 · 渲染管线 > **状态(2026-10-17)**:✅ 完成。`lib/render.mjs` + `tests/render.mjs`,**10 项测试全过**。 > 三条关键断言都有真实证据:中文文字确实渲染出非白像素、**不给字体文件时像素归零**(反向验证)、 > 同一场景两次渲染 SVG 与 PNG 的 SHA-256 完全相同。 > > 测试自带一个极简 PNG 解码器(`zlib.inflate` + 逐行反滤波), > 所以"文字画出来了"是数出来的像素,不是靠第三方库的返回值。 ### 任务 1. 装依赖(锁版本):`@excalidraw/utils@0.1.5`、`jsdom@30`、`@resvg/resvg-js@2.6.2` 2. `lib/render.mjs`: - `installDom()`:全局只调用一次(DOM 全局是进程级的) - `sceneToSvg()`:`exportToSvg({ data, config })` + 去重 `xmlns` - `svgToPng()`:resvg,`loadSystemFonts:false` + `fontFiles` 取 `@excalidraw/utils` 自带 assets - 字体 1/3(Virgil/Cascadia)规范化成 5 3. 写死确定性策略:固定 canvas 桩、固定字体集、固定 padding/scale。 ### 验收 - [x] 同一 scene 连续渲染两次,**SVG 与 PNG 的 SHA-256 都相同** - [x] 渲染一张含**中文文字**的画布,PNG 里文字区域**非空像素**(这一条能立刻抓到字体回归) - [x] PNG 的 IHDR 尺寸与预期一致(不是 0×0,也不是被意外缩放) - [x] 含 rectangle / ellipse / diamond / arrow / line / text / freedraw 的画布全部渲染成功 - [x] 故意去掉 `fontFiles` 重跑 → 文字像素归零(**反向验证字体确实是必需的**) --- ## M3 · 图像回传(本项目的核心价值) > **状态(2026-10-18 复核)**:✅ 真机验收通过。`lib/render-tool.mjs` + `tests/render-tool.mjs` > (**13 项纯函数测试全过**);重启后真机跑通:会话日志里的 `tool/result` 含 > `{type:'image', attachment:{attachmentId:'sha256:5ef7…'}}`,该附件在 `~/.dsh/attachments/` 下 > 落盘且字节与 ref 一致(37510 B),模型在下一轮准确描述了画面。 > > 实现时发现并修正了一处设计错误:**PNG 无法导出到工作区** —— > `ctx.fs` 的变更操作是 **UTF-8 文本专用**(`dsh-fs` README:"二进制安全的变更操作仍延期"), > 把 PNG 字节塞进 `writeText` 会静默损坏文件。因此: > **SVG 可导出为工作区文件;PNG 只走附件服务**(作为图像交给模型 + 返回附件库的真实宿主路径)。 ### 任务 1. 解决 **Q1**(`react-dom` 是否在浏览器模块表里)与 **Q6**(`attachments.imageLimits` 实际值)。 2. `graphics_render` 工具: - 执行前做**路由能力闸门**(`ctx.llm.resolveModelInfo` 检查 `image` 输入能力) - PNG 字节 → `ctx.attachments.saveImage()` → `ImageAttachmentRef` - `output.render()` 返回 `[text 信封, image 块]` - `ctx.inject(['attachments'], ...)` 包裹注册(没挂附件服务时不注册) 3. 可选导出:`outputPath` 把 SVG 写到工作区(经 `ctx.fs`;PNG 只走附件服务)。 ### 验收(这是最关键的一组) - [x] 会话日志里该次 `tool/result` 事件的 `message.content` **确实含** `{type:'image', attachment:{attachmentId:'sha256:5ef7…'}}` - [x] 该 `attachmentId` 在 `~/.dsh/attachments/` 下能找到对应文件,且尺寸/字节与 ref 一致(37510 B == 37510 B) - [x] **模型在下一轮真的"看见"了图**:让它描述自己刚画的内容,描述与画面一致 (这是端到端证明,前面几条只是链路证明) - [x] 用一个不声明 image 输入的模型调 `graphics_render` → 得到明确的能力错误,而不是适配器层的失败 (`tests/integration.mjs` 覆盖) - [x] 导出到工作区的 SVG 文件真实存在且可打开(`examples/e2e-login.svg`,15430 B,`xmlns` 恰好 1 个; PNG 按设计只走附件服务) --- ## M4 · 客户端 UI > **状态(2026-10-18 复核)**:✅ 代码完成,并已在真机使用中迭代。 > 依据:`405d39f` 落地面板;`b5390a3` 按真机使用反馈改成**手动保存**、新增「引用到输入框」 > 与输入框菜单快捷建画布(该提交同时记录了实测结论:`ctx.fs` 的写操作会自动创建父目录); > `package.json` 已声明 `dsh.client`,`lib/client.js` 与 `editor/dist/`(约 21MB)均已构建。 > **live 证据(2026-10-18)**:`cordis_inspect_query`(`platform: client`、`provider: Slots`、 > `method: listSubTree`、`root: "sidebar.right.pane.tab"`)返回的 `selected.occupants` 里含 > `{ registrant: "Kb", key: "dsh-graphics-editor", active: true }` —— 标签页注册确实存在于 > 运行中的浏览器里。(注意:同一结果里 `keyDomain` 的 "none are taken yet" 是静态声明字段, > 不是实时占用,别拿它当反证。) > 这条由 lead 会话取得;独立验证员在没有连接页面的会话里 3 次查询全部超时,**未能独立复现**, > 因此按「lead 单方证据」看待。 > > 面板交互仍没有像 M1 / M3 那样归档验收记录(无截图 / 会话日志),下面验收清单中 > **5 条全部未打勾**:2 条只有推断旁证,3 条完全没有证据。M4 标「完成」指的是代码落地并 > 已在真机使用中迭代,**不等于交互验收已留档**。 > > 落地形态与设计文档 §10 的决策一致: > - **宿主**(`lib/editor-assets.mjs`)伺服 `editor/dist/`,并提供画布读写的 HTTP API。 > 工作区根**只**从会话解析(`ctx.sessions.get` → `sandboxPolicy.resolve({session})`), > 不接受请求方指定的根——照抄 `dsh-drawioedit` 的既有范式。 > - **浏览器侧**(`lib/client.js`)注册一个右侧栏标签页,`patterns: ['*.excalidraw']`。 > 用户在文件树里点开画布即打开编辑器;地址里的 sessionId 从 > `dsh-resource://file/session//` 解出,**文件树就是画布选择器**。 > - **编辑器**(`editor/`)是 Vite 打包的独立应用,`EXCALIDRAW_ASSET_PATH='./'` > + 构建后拷贝 13MB 字体,**完全离线**,不依赖任何 CDN。 > > 为什么用 iframe 而不是原生 React 面板:客户端模块表里**没有 react-dom** > (只有 `ctx` / `React` / `host` / `styles` / `console`),见 §10。 ### 任务 1. **先做 Q1 探针**,据此在方案 A(直接挂载)与方案 B(伺服 + iframe)之间定案(见 `design.md` §7.2)。 2. 实现编辑面板:打开工作区 `.excalidraw`、编辑、手动保存回原文件(带版本冲突保护)。 3. 主题与语言跟随宿主;文案走 `ctx.locale`。 4. 资源清理:`apply` 里所有注册用 `ctx.effect` 并返回清理函数。 ### 验收 - [ ] 页面里 Excalidraw **真的加载出来了**(不是空白/报错占位) (**未单独留档**:旁证只有两条——`b5390a3` 的真机使用反馈,以及 live slot 里该标签页注册为 `active`; 两条都推不出 iframe 里真的渲染出了 Excalidraw) - [ ] 在面板里改一处文字 → 原文件真的被改 (**未单独留档**:`b5390a3` 记录自动保存曾被真机使用、因"手一滑就覆盖了上一版"改成手动保存, 可推出面板写过盘,但不是逐条证据) - [ ] 模型用 `graphics_read` 能读到 UI 里刚改的内容(**未单独留档**;「引用到输入框」就是为这条铺路) - [ ] 控制台无 `slot entry crashed`(**未单独抓控制台日志**;live 证据只到该 entry 注册为 `active: true`, 能说明它没在激活阶段失败,不等于控制台干净) - [ ] 明暗主题下都可读;组件只用主题 token,不 require 任何 `@deepseek-ai/dsh-client-*` 包 (**未单独留档**;已知限制:iframe 不继承宿主主题 token) --- ## M5 · 端到端验收与收尾 - [ ] 走一遍完整闭环:**说需求 → Agent 画 → Agent 渲染并看图 → Agent 自我修正 → 用户在面板里改 → Agent 读到用户的改动** ("Agent 画 → 渲染 → 看图"已真机验收;"用户在面板改 → Agent 读到"未单独留档) - [ ] 卸载/重装后行为一致(`remove_bundle` → `install_bundle`) (已实测返回 `applied`,但宿主模块不重新导入——见 [host-integration-notes.md](host-integration-notes.md) §2, "行为一致"与"根本没重载"无法区分) - [ ] `README.md` 补上真实用法与截图(用法与已知限制已补,**截图未补**) - [x] 按工作台规范,在 `~/Projects/Workspaces/DSH/ops/records/` 留一次全局操作记录 (安装插件属于 profile 级变更)——已有 `2026-10-17-dsh-graphics-m1-profile-install.md` 与 `2026-10-17-dsh-graphics-e2e-verification.md` --- ## 风险登记 | 风险 | 影响 | 应对 | |---|---|---| | `react-dom` 不在平台模块表 | UI 方案 A 不可行 | 退到方案 B(drawioedit 已验证) | | `@excalidraw/utils` ~101MB 依赖 | 包体积大 | 锁版本;打包时剔除 `dist/dev`;先接受 | | 附件服务把 PNG 重编码成 JPEG | 线条图质量下降、字节与预期不符 | 自己先缩到 ≤1600px;对结果以 ref 的 `mediaType` 为准,不假设是 PNG | | `ctx.fs` 无删除原语 | 无法实现 `graphics_delete` | v1 不做;交给 DSH 既有文件工具 | | 上游 `@excalidraw/utils` 签名再变 | 渲染突然失败 | 锁版本 + 一条端到端 smoke test | | measureText 桩导致换行偏差 | SVG/PNG 文本排版与浏览器略有差异 | v1 接受(确定性优先);需要时再评估真实 canvas 后端 |