title: “v0.8.9 - 插件加载器加固” description: “docmd v0.8.9 发布说明 - 新增构建时插件注册表、docmd doctor 命令、docmd 命名空间标准化、404 页面和模板资源修复,以及安全加固。” date: “2026-06-26”
一次专注的加固版本。三个主题:
- 插件和模板生态获得了唯一的事实来源 —— 每个官方
@docmd/*包现在在其package.json中携带docmd命名空间,构建时注册表生成器读取这些命名空间来生成运行时加载器消费的目录。添加一个新的官方插件现在只需要在packages/{plugins,templates,engines}/下编辑一个package.json加一个目录。 - 插件自动安装器现在对提供
import-onlyexports字段的包具有弹性,并且新增的docmd doctor预检命令在构建前捕获配置漂移。 - 已发布的 tarball 中两个原本静默的错误被修复:
@docmd/ui的translations/和@docmd/template-summer的templates/+assets/现在都在已发布的文件中。404 页面以夏季模板和翻译后的字符串渲染;以前会回退到默认模板并显示原始的翻译键。
没有公开的 API 更改。没有破坏性的配置更改。纯粹的加固版本。
✨ 新增
构建时插件注册表(唯一事实来源)
一个新的工作区级脚本 —— scripts/build-plugin-registry.mjs —— 遍历 packages/{plugins,templates,engines}/*,读取每个包的 package.json#docmd 命名空间,并生成一个 JSON 目录 (packages/api/registry/plugins.generated.json)。将其作为 @docmd/api 的 prebuild 步骤挂钩,以便在每次构建时重新生成注册表。
运行时加载器(packages/api/src/hooks.ts getPluginRegistry)现在从 生成的文件中读取,具有两种解析路径(已发布布局 <pkg>/registry/... 和 monorepo 开发布局 <repo>/packages/api/registry/...)。 安装程序中曾经维护的手工 packages/plugins/installer/registry/plugins.json 在运行时不再被查询。它作为已弃用的回退保留给那些在没有 @docmd/api 的情况下安装 @docmd/plugin-installer 的用户,并在 0.9.0 中删除(见下面的迁移说明)。
docmd 命名空间标准化
每个官方 @docmd/* 包现在在其 package.json 中携带 docmd 命名空间。 该命名空间是注册表生成器读取的合约,也是加载器在加载时与 JS 描述符 进行交叉检查的依据。字段:
| 字段 | 是否必需 | 描述 |
|---|---|---|
key |
推荐 | 面向用户的标识符(config.plugins.<key>)。省略时从包名派生。 |
kind |
推荐 | plugin、template 或 engine 之一。省略时从目录布局派生。 |
displayName |
推荐 | 目录和 docmd doctor 输出中显示的人类可读名称。 |
tagline |
推荐 | 一行说明;作为 npm description 的回退。 |
capabilities |
插件和模板必需 | JS 描述符声明的钩子能力。构建时交叉检查会在出现漂移时发出警告。 |
preview |
可选 | (仅模板)预览资源的路径。 |
引擎具有相同的形状但没有 capabilities —— 它们不参与钩子系统, 只参与引擎加载器。自动安装器检查 kind === 'engine' 并拒绝安装它们, 因此引擎会出现在目录中但永远不会被自动安装。
docmd doctor —— 预检
一个新的用于诊断的 CLI 子命令。不写文件,没有构建副作用 —— 纯粹 用于诊断。
npx @docmd/core doctor [选项]
| 选项 | 说明 |
|---|---|
--config <路径> |
指向非默认 docmd.config.json(或 .ts/.js/.mjs)的路径。 |
--fix |
自动安装 doctor 标记为缺失的官方插件或模板。 |
--json |
将完整报告以机器可读的 JSON 形式输出。 |
默认情况下,doctor 会打印一份人类可读的摘要,涵盖:已安装的 @docmd/core 版本、每个已配置的插件(附带版本和 ✓ installed / ⚠ missing 状态)、当前激活的模板、请求的引擎(js 始终启用,rust 可选),以及一份自动安装候选清单。带上 --fix,它会调用项目所用的 包管理器(pnpm add、npm install --save、yarn add 或 bun add) 来安装这些候选,并在全部解决后以退出码 0 结束。带上 --json, 同样的数据会作为一个 JSON 对象输出 —— 适合接入 pre-commit 钩子 和 CI 闸门。
退出码 0 表示项目处于健康状态;非 0 表示即使经过 --fix 仍有未解决的 问题。
也可作为 pnpm doctor 使用(在 monorepo 中通过 workspace 的 docmd 脚本路由)。
Monorepo 中的新 pnpm 脚本
monorepo 的 package.json 新增了一批 pnpm 脚本,全部通过现有的 --cwd 标志指向 playground(与 pnpm dev 和 pnpm live 相同的模式):
pnpm doctor # → docmd doctor
pnpm validate # → docmd validate
pnpm migrate # → docmd migrate
pnpm gen:deploy # → docmd deploy
pnpm mcp # → docmd mcp
pnpm plugin:add foo # → docmd add foo
pnpm plugin:remove foo # → docmd remove foo
pnpm build:playground # → docmd build(与 `pnpm build` 不同,后者是 monorepo 范围的构建)
🐛 错误修复
插件自动安装:对 import-only exports 具有弹性
@docmd/api 中的自动安装器此前使用 Node 的 CommonJS require.resolve 解析已安装的包,然后用 await import(file://path) 加载解析后的 路径。对于那些在 exports 字段中只携带 import 条件的包, require.resolve 会抛出 ERR_PACKAGE_PATH_NOT_EXPORTED,构建会打印:
⬢ Could not load @docmd/template-summer after auto-install
尽管包已经正确安装。retry 路径现在直接使用 await import(name),原生 支持 exports 字段,能与任何条件一起工作。
在 retry 路径中执行一次注册表的纵深防御式重新检查。可以自动安装的名称 集合保持不变 —— 只是它们可以携带的 exports 条件集合变得更大了。
插件加载器:能力缓存与清单漂移检查
对加载器的两处后续加固:
- 按 key 缓存能力集合。 避免在每次 dev-server 重新构建时重新 遍历注册表。对于首次构建影响可忽略,在拥有 15 项条目的注册表的 热重载循环中能感觉到。
- 清单 / 描述符能力漂移检查。 当注册表项有
capabilities且 JS 描述符也有capabilities时,加载器在任一侧声明的钩子另一侧 没有时会发出警告。这修复了静默的钩子丢弃 bug —— 即实现了一个钩子 (例如onPostBuild)但忘记在 JS 描述符中声明相应capability的插件,其钩子会被静默跳过。
更好的 “Could not load X after auto-install” 错误信息
post-install retry 的 catch 块现在会显示 err.code(如 ERR_PACKAGE_PATH_NOT_EXPORTED、ERR_MODULE_NOT_FOUND)和 err.message 的第一行。先前那条 “Could not load X after auto-install” 信息看起来像是 docmd 的 bug,而实际原因是依赖中的 package.json 有问题。autoInstallPlugin 的 catch 块也会显示包管理器的底层 stderr,并为最常见的情况打印提示(模板 → 添加到 dependencies; 插件 → docmd add <name>)。
@docmd/ui:将 translations/ 加入发布的 tarball
packages/ui/src/index.ts:loadTranslations 中的服务端翻译加载器 会在 __dirname/../translations/ 查找翻译 JSON 文件。而 package.json#files 当时是 ["dist", "templates", "assets"] —— 缺少 "translations",因此 npm 在打包时把翻译文件漏掉了。服务端的 readFileSync 抛出异常,catch 块把 translationsCache[locale] 置为 {},t() 函数就回退为直接返回键字面量。
这个 bug 在本地不可见,因为用户的 --offline 构建(monorepo 开发) 能够访问源码树。它只在已部署的文档站点上才显现 —— 在那里 @docmd/ui@0.8.x 是从 npm 安装的,翻译目录在 tarball 中缺失。
404 页面是最显眼的症状:它由已部署站点的静态文件回退机制在用户访问 缺失路径时懒加载渲染。404 页面会为标题和正文调用 t(),而此时翻译 缓存是空的。键就漏出来了。
修复:在 packages/ui/package.json#files 中加入 "translations"。 现在发布的 tarball 携带全部 7 个语言文件(de、en、es、fr、hi、ja、zh)。
@docmd/template-summer:将 templates/ 与 assets/ 加入发布的 tarball
夏季模板的运行时会用到 new URL('../templates/...', import.meta.url), 在 dist/index.js 里它解析到 <package-root>/templates/...(不是 <package-root>/dist/templates/...)。package.json#files 当时只有 ["dist"],因此发布的 tarball 中只有 dist/。解析器找不到任何模板 片段,便静默地回退到默认模板。
404 页面的症状(叠加翻译问题之上):在已部署的站点上,用户看到 的是默认的 404 模板(仍然是 docmd 品牌,但外观是默认的),不是夏季 风格的 404。
修复:在 packages/templates/summer/package.json#files 中加入 "templates" 和 "assets"。现在发布的 tarball 与 monorepo 开发 布局一致,夏季模板能正确渲染。
Dev-Server:在 safePath() 之前去掉前导斜杠
0.8.9 中的 CWE-22 修复(用 safePath(rootAbs, ...) 取代 filePath.startsWith(rootAbs))在 dev-server 上引发了一个回归: URL 路径名总是以 / 开头(例如 /index.html),而 path.resolve('/abs/root', '/index.html') 会返回 /index.html(把 第二个参数视作绝对路径)—— 总是让 safePath 边界检查失败, 对每一个合法请求都返回 403 Forbidden。
修复:在把 URL 路径名传给 safePath() 之前去掉前导的 /。空情况 ('/' → '')用 || '.' 处理,让它解析到根目录自身,而 safePath 会通过它的 resolved !== root 例外接受它。已验证:合法路径返回 200,路径穿越仍被阻止(403),编码后的 ../ 在路径标准化之后 仍然解析为 404。
🔒 安全性
本版本发布前收到的冷邮件告警已全部处理:
| 问题 | 状态 | 解决方案 |
|---|---|---|
packages/core/src/utils/dev-utils.ts:118 —— filePath.startsWith(rootAbs) CWE-22 |
已修复(0.8.9) | 使用 safePath() 配合严格的 root + path.sep 边界。该改动引入的 “Forbidden” 回归也已修复(见上)。 |
packages/live/src/index.ts —— server.listen(port, '0.0.0.0') CWE-668 |
已修复(0.8.9) | 默认 127.0.0.1。通过 DOCMD_HOST=0.0.0.0 或 --host 0.0.0.0 显式开启 LAN 访问,激活时显示 TUI 警告。端口探测也改为绑定到 127.0.0.1 而非 0.0.0.0。 |
packages/utils/src/html-escape.ts —— scriptLiteral/jsonInject 是裸 JSON.stringify |
已修复(0.8.9) | 二者现在都能正确转义 </script、<!--、U+2028 和 U+2029。该加固对非冲突字符串静默生效;通过 JSON.parse 的往返仍然可用,因为转义使用 JSON 安全的序列。 |
docker/DOCKER.md —— 示例使用 command: dev --host 0.0.0.0 |
已修复(0.8.9) | 三个示例改用新的回环默认值。“Network Issues” 排错章节记录了显式开启路径并附有安全提示。 |
@docmd/plugin-openapi CWE-22(0.8.5 的旧告警) |
已于 0.8.8 关闭 | renderSpec 中使用 safePath(rootDir, asUserPath(specPath))。已验证。 |
| Dev-Server WebSocket CWE-1385(0.8.5 的旧告警) | 已于 0.8.8 关闭 | WebSocketServer 中使用 verifyClient: createOriginVerify()。已验证。 |
两条旧告警(OpenAPI 与 WebSocket)可标记为已解决。它们已在 0.8.8 中修复 —— 本次发布再次确认了修复,并新增了加载器中的清单交叉检查, 以便尽早发现未来的漂移。
📚 文档
- 开发 → 构建插件 与 开发 → 构建模板 —— 新增 “The
docmdNamespace” 章节,记录每个官方插件必须满足的构建时合约,以及 “Bundled registry removal in 0.9.0” 的提示。新增 “ESM Exports — thedefaultCondition” 小节,覆盖插件与模板。 - 插件 → 使用插件 —— 关于弹性自动安装的提示。新的
import(name)retry 路径被记录为注册表在纵深防御层重新检查的原因。 - 插件 → 搜索 —— 关于解析器健壮性的提示。
import → default → require → main的回退链与对node_modules的人工遍历均有解释,并附上docmd-search自动安装时的--foreground-scripts说明。 - 参考 → CLI 命令 —— 新增
docmd doctor条目,三个选项(--config、--fix、--json)均有文档。 - 发布说明 → 0.8.9 —— 本文件。
🔌 兼容性
| 表面 | 状态 |
|---|---|
@docmd/plugin-search@>=0.8.5 |
✓ 可用。收紧后的 >=0.1.0 对 docmd-search 的 peer dep 干净解析。 |
docmd-search@>=0.1.0 |
✓ 首个非 alpha 版本。去掉 ^0.1.0-alpha.1 范围,落到 latest。 |
@docmd/engine-js@>=0.8.5 |
✓ 可选 peer,用于分块/量化。 |
@docmd/engine-rust@>=0.8.5 |
✓ 可选 peer,在场时加速分块/量化。 |
@huggingface/transformers@^4.2.0 |
✓ 可选 peer,嵌入模型所必需。 |
onnxruntime-node@^1.26.0 |
✓ 可选 peer,设备端推理后端所必需。 |
| Node.js | >=18(与 @docmd/* 系列其余包一致) |
| 浏览器(搜索客户端) | 支持 WebAssembly、Atomics、SharedArrayBuffer 的现代浏览器(cross-origin isolated) |
🔄 迁移说明
@docmd/plugin-search@>=0.8.5用户:无需任何操作。收紧后的 peer dep>=0.1.0会自动取到docmd-search@0.1.0。@docmd/plugin-search@0.8.5-0.8.8用户:将插件升级到>=0.8.9,获得default导出的兼容性以及弹性自动安装。旧版本仍能用 alpha 安装,但自动安装 retry 会打印警告。- 直接使用
docmd-search的用户:将package.json从^0.1.0-alpha.1改为^0.1.0。两个 alpha 已弃用;npm install docmd-search解析到0.1.0并落到latest。 - 插件作者(新增或更新官方插件):按 [开发 → 构建插件] 指南所写,给
package.json添加docmd命名空间。没有它,构建时注册表生成器会大声失败。 - 没有
@docmd/api的下游@docmd/plugin-installer用户:bundledregistry/plugins.json在 0.8.9 仍作为已弃用回退被包含。0.9.0 删除 —— 将@docmd/api安装为 peer dep。 - monorepo 中的
pnpm用户:一旦docmd-search@0.1.0上了 npm,就移除pnpm.overrides["docmd-search"]的file:../docmd-search临时绕路。收紧后的>=0.1.0peer dep 会自然从注册表解析。
🛠 验证
pnpm prep—— 0 个错误,1 个警告(与本版本无关的已有 lint 噪音)。全部 30 个包构建通过;372 个测试通过。- 在干净的
/tmp安装里端到端冒烟测试:docmd build成功;docmd doctor --json正确报告;dev-server 对合法路径返回200,对穿越尝试返回403;404 页面以夏季模板和翻译字符串渲染。
📋 发布检查清单(给发布者)
- 确认本地
pnpm prep是绿的。 - 打标签:
git tag 0.8.9 && git push origin 0.8.9。 - 发布 workflow 会先跑 Rust 矩阵构建,再按依赖顺序发布全部 30 个包。
- 在 npm 上验证:
npm view @docmd/core dist-tags显示{ latest: '0.8.9' }。 - 删除
package.json#pnpm.overrides里的file:../docmd-search(根 devDep 可保持^0.1.0-alpha.1或改为^0.1.0)。