# 开发与测试说明 面向要改这个插件的人。用户向的功能介绍看 [../README.md](../README.md), 设计文档看 [DESIGN.md](DESIGN.md)。 --- ## 目录结构 ``` dsh-tool-diagram/ (仓库根 = 包根) ├── package.json ├── cordis.patch.yml 组合包自带的一层(插入 tool-diagram 条目) ├── LICENSE ├── lib/ │ ├── index.js Cordis 插件入口(注册 3 个工具) │ ├── cli.js 脱离 DSH 的命令行入口 │ └── core/ │ ├── measure.js TrueType 解析 + 真实字宽(零依赖) │ ├── spec.js 归一化 / 自动尺寸 / 中文折行 + 禁则 │ ├── geometry.js 包围盒 / 边界求交 / 指定边锚点 │ ├── layout.js 边的路径与标签落位 │ ├── detect.js 自检:10 类问题 + 修法建议 │ ├── render.js spec → SVG(13 种图元) │ └── index.js buildDiagram / measureDiagram / applyOps ├── examples/ 可直接跑的样例(8 组 spec + 渲染结果) ├── docs/ └── test/ ├── verify.mjs 核心核验(67 条断言) ├── harness.mjs 插件入口核验(35 条断言,用真实 defineTool) ├── stress.mjs 压力与边界(17 条断言) ├── bundle.mjs 组合包合规(15 条断言) └── run-all.mjs 一把跑完 ``` --- ## 测试 ```powershell node test/run-all.mjs ``` 四套共 **134 条断言**,全部写死期望值——不拿实现自己的输出当标准答案。 | 套件 | 条数 | 覆盖 | |---|---|---| | `verify.mjs` | 67 | 字体度量真值(SimHei 是严格两宽度字体:CJK/全角标点 1.000em、ASCII 0.500em,`unitsPerEm=256`)、中文折行与禁则、自动尺寸、边界求交与指定边锚点、十类自检逐类构造必然触发的例子、干净图必须 0 error、`applyOps` 全部操作与不可变性、渲染尺寸与像素对齐、`class_box` 栏数/宽度/高度/分隔线数 | | `harness.mjs` | 35 | 用 DSH 真实的 `defineTool` + 桩 ctx 把插件注册一遍,再跑 `create` / `patch` / `measure` 的端到端与错误路径(含图片块形状、附件调用次数、非法输入) | | `stress.mjs` | 17 | 180 节点/333 边的性能、自动画布、超长中文/无空格英文/中英混排、NaN 与重复 id 等非法输入、自环边、dpi 缩放线性 | | `bundle.mjs` | 15 | 组合包合规,见下节 | `bundle.mjs` 需要 profile 目录,默认取 `~/.dsh/profiles/desktop`, 可用环境变量 `DSH_PROFILE_DIR` 覆盖;目录不存在时该套件自动跳过而非失败。 ### 验收测试:重画「图书管理第 2 层 DFD」 这是本项目诞生时返工最多的一张图(5 加工 × 2 数据存储、14 条边、3 条折线)。用插件的全过程: | 步骤 | 结果 | |---|---| | 写 spec | 一次写成 | | `create` | 自检 15ms + 渲染 89ms → **0 错误 2 警告** | | 第 1 轮 patch(3 条操作) | **0 错误 0 警告** | | 总墙钟 | 约 300ms(含进程启动) | 关键在于那 2 个警告**都是真的**,其中一条(「修改库存文件」标签压在 E14 上) 在手工绘制的那一版里同样存在,当时并没有被看出来。 --- ## 组合包合规(发布相关) `dshmarket` 对 profile 里的 bundle 有三道校验,实现见 `dshmarket/lib/check.js`: 1. `package.json` 里必须有 `dsh.bundle.patch`(字符串或字符串数组) 2. 每个声明的 patch 文件必须在包目录下真实存在 3. 每个文件必须能解析成合法的 entry list(顶层数组) **凡是被列进 `dsh.profile.bundles` 的包,都必须自己声明 `dsh.bundle.patch`**, 否则插件管理器判定为 `bundle declares no dsh.bundle.patch — the profile will fail to boot`。 只往 profile 的 `cordis.patch.yml` 里手工加配置层是不够的——那样能加载,但管理器不认。 ### 版本兼容按「桌面版号」判,不是按 profile 里的包版本 判定逻辑在 `dshmarket/lib/discovery-compatibility.js` 的 `deriveHostCompatibility`: ```js // 只看 engines.dsh,以及名字匹配这个正则的 host peer if (!hostPackages.has(name) || !/^@deepseek-ai\/dsh(?:-|$)/.test(name)) continue; ``` - `@deepseek-ai/cordis`、`@deepseek-ai/schemastery` **被明确排除**,不参与判定 - 比对的是 **hostVersion**,即 `dsh-runtimes//runtime.json` 里的 `desktopVersion` ——**不是** profile 里 `node_modules/@deepseek-ai/dsh-tools` 的实际版本 - 任一声明不满足就是 `incompatible` 所以 `@deepseek-ai/dsh-tools` 必须写成跨版本区间: ```json "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1" ``` `test/bundle.mjs` 第 7 关**直接 import `dshmarket/lib/check.js` 调它自己的 `satisfiesRange`** 复现这个判定,而不是自己重写一遍 semver。 ### 卸载是半截子的 插件管理器点"卸载"只会把包名从 `dsh.profile.bundles` 里去掉, `dependencies` 和 `node_modules` 里的链接都留着。要重装只补 `bundles` 那一行即可。 --- ## 实现注意(踩过的坑) 按代价排序,前三条各花掉半小时以上: 1. **SVG 里必须显式写 `font-family`,而且要和测量用的字体文件是同一个字体。** 不写的话 librsvg 会用默认衬线字体,ASCII 全部静默回退——`+` 直接渲染成空白, 而测量仍按 SimHei 算宽度。图看着"差不多对",但每个字的位置都是错的。 `verify.mjs` 有断言守着这条。 2. **viewBox 单位是厘米**,所以 `stroke-width` / `font-size` 必须先由 pt 换算成厘米再写进 SVG, 否则线宽会变成 1.2 **厘米**。箭头路径尤其容易漏——它有自己的 ``, 默认 `stroke-width = 1` 用户单位 = 1 cm,会画成一个巨大三角。 3. **SVG 的 width/height 要写成 px(按 72 dpi 换算),不要写 `15.6cm`。** 写物理单位时 librsvg 会把 sharp 的 `density` **平方**一次——传 300 实际得到 1250 dpi, 1536px 的图变成 7677px。 4. **`parameters.<字段>.items` 里不能写 `required`。** dsh-tools 的 value schema DSL 不支持,会在插件注册期抛 `JsonSchemaError`、 整个插件起不来。`required` 只能写在 `properties.*` 里面。 5. **y 轴向下**,"上方"对应椭圆参数方程的 `sinθ < 0`,锚点选边时极易写反。 6. **给了显式 `size` 时,文字会按框宽折行**,所以"宽度不超"是必然的—— 必须再查一次**高度**,否则会漏掉最典型的"字太多、框装不下"。 7. **结构性图元必须豁免重叠判定。** `line_h` / `line_v` / `chevron` / `box_span` 默认豁免;画框式的 `rect` 可以用 `"structural": true` 显式豁免。 否则 N-S 图这类"区域被反复切分"的画法会被报几十条 `node_overlap`。 8. **`textAlign: left/right` 时 `at` 是锚点而不是中心。** 否则左对齐标签会算出负坐标、 报 `outside_canvas`。 ### pnpm 安装时的两个现象 - `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: sharp@0.34.0` —— 可以忽略。 pnpm 11 默认拦下依赖的安装脚本,而 sharp 的原生二进制走 `optionalDependencies`, 实测拦下之后仍可用(vips 8.16.1)。 - pnpm 的隔离布局下,`node_modules/dsh-tool-diagram` 是软链,`sharp` 在软链**真实路径**的同级。 所以要从包入口去用;从项目根直接 `import "sharp"` 会找不到,这是正常的。 --- ## 已验证 / 未实现 **已验证**:中文渲染(SimHei)、真实字宽、中文折行与禁则、13 种图元、 指定边锚点与手工折线、10 类自检、增量 patch、300dpi PNG + SVG 导出、 180 节点/333 边的性能、从 GitHub 安装(`pnpm add github:`)。 **未实现**(对应 DESIGN.md 的分期 v1/v2): - 正交路由与交叉最小化(现在交叉只统计,靠 `waypoints` 手工绕) - 自动布局(`tree` / `layered` / `grid`) - 容器与子节点联动(`containers` 只渲染,不做布局约束) - `docx_insert`(插图 + 居中 + 图题 + 与下段同页) - 英文断词(连字符断词);现在只对中文做了折行与禁则优化 **未复核**:运行时装的是桌面版自带的 `@deepseek-ai/dsh-tools`, 而本地测试是拿 profile 里那份 `0.1.0-rc.6` 的 `defineTool` 验证的。 用到的接口很少(`name` / `description` / `parameters` / `output` / `execute` / `presentCall`), 变动可能性不大,但这是唯一没能提前排除的风险点。