Просмотр исходного кода

refactor(document): name Office PDF conversion explicitly

yudshj 4 дней назад
Родитель
Сommit
fd3cdf539b
57 измененных файлов с 331 добавлено и 326 удалено
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.i18n.yaml
  5. 3 1
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.md
  6. 3 1
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.zh.md
  7. 2 2
      apps/desktop/README.i18n.yaml
  8. 1 1
      apps/desktop/README.md
  9. 1 1
      apps/desktop/README.zh.md
  10. 2 2
      docs/capability-seams.i18n.yaml
  11. 6 6
      docs/capability-seams.md
  12. 6 6
      docs/capability-seams.zh.md
  13. 2 2
      docs/config-catalog.i18n.yaml
  14. 4 4
      docs/config-catalog.md
  15. 4 4
      docs/config-catalog.zh.md
  16. 2 2
      docs/subsystems/README.i18n.yaml
  17. 1 1
      docs/subsystems/README.md
  18. 1 1
      docs/subsystems/README.zh.md
  19. 3 3
      docs/subsystems/document-convert.i18n.yaml
  20. 13 13
      docs/subsystems/document-convert.md
  21. 13 13
      docs/subsystems/document-convert.zh.md
  22. 2 2
      packages/bundle/web-app/README.i18n.yaml
  23. 1 1
      packages/bundle/web-app/README.md
  24. 1 1
      packages/bundle/web-app/README.zh.md
  25. 2 2
      packages/bundle/web-app/cordis.patch.yml
  26. 2 2
      packages/bundle/web-app/package.json
  27. 12 12
      packages/bundle/web-app/tests/document-conversion.e2e.ts
  28. 2 2
      packages/document/README.i18n.yaml
  29. 6 6
      packages/document/README.md
  30. 6 6
      packages/document/README.zh.md
  31. 3 3
      packages/document/document-convert-libreoffice/README.i18n.yaml
  32. 5 5
      packages/document/document-convert-libreoffice/README.md
  33. 5 5
      packages/document/document-convert-libreoffice/README.zh.md
  34. 4 4
      packages/document/document-convert-libreoffice/package.json
  35. 15 15
      packages/document/document-convert-libreoffice/src/index.ts
  36. 6 6
      packages/document/document-convert-libreoffice/src/output.ts
  37. 25 25
      packages/document/document-convert-libreoffice/src/queue.ts
  38. 0 0
      packages/document/document-convert-libreoffice/tests/output.spec.ts
  39. 39 39
      packages/document/document-convert-libreoffice/tests/provider.spec.ts
  40. 10 10
      packages/document/document-convert-libreoffice/tests/queue.spec.ts
  41. 1 1
      packages/document/document-convert-libreoffice/tsconfig.json
  42. 3 3
      packages/document/document-convert/README.i18n.yaml
  43. 4 4
      packages/document/document-convert/README.md
  44. 4 4
      packages/document/document-convert/README.zh.md
  45. 2 2
      packages/document/document-convert/package.json
  46. 44 0
      packages/document/document-convert/src/index.ts
  47. 14 14
      packages/document/document-convert/src/types.ts
  48. 0 0
      packages/document/document-convert/tsconfig.json
  49. 0 44
      packages/document/document-render/src/index.ts
  50. 1 1
      packages/experimental/webworker-runtime/src/node/external_packages/libreoffice-kit.ts
  51. 17 17
      packages/extensions/tool-cordis/src/api-catalog.ts
  52. 8 8
      pnpm-lock.yaml
  53. 5 5
      scripts/gen-cordis-catalog.ts
  54. 2 2
      scripts/gen-doc-graphs.ts
  55. 2 2
      scripts/verify-package-readme-model-experience.ts
  56. 3 2
      tsconfig.base.json
  57. 2 2
      tsconfig.host.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
-2026-09-14-independent-libreoffice-kit.md: aaecb7468c16451ed812c9b2441e55e00abf6c98
-2026-09-14-independent-libreoffice-kit.zh.md: 8cea9890596bc53ddcbe346c60c654b49e0362c8
+2026-09-14-independent-libreoffice-kit.md: f9f27585d157025477e5f5d5f6d74dfca142ce83
+2026-09-14-independent-libreoffice-kit.zh.md: 18bf20e28c8f66ea581ac7823f7b004e89153b90

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md

@@ -10,7 +10,7 @@ LibreOffice compilation, source patches, platform qualification, and large binar
 
 ## Decision
 
-The `deepseek-harness/libreoffice-kit` repository owns the reusable `@deepseek-ai/libreoffice-kit` Node API, its Worker, font handling, engine selection, build recipes, patches, tests, and releases. The API has no Cordis dependency. Harness owns the adapter from its `DocumentRenderer` service to this API, Session authorization, conversion lifetime, transport, Web UI, and application packaging.
+The `deepseek-harness/libreoffice-kit` repository owns the reusable `@deepseek-ai/libreoffice-kit` Node API, its Worker, font handling, engine selection, build recipes, patches, tests, and releases. The API has no Cordis dependency. Harness owns the adapter from its `DocumentConverter` service to this API, Session authorization, conversion lifetime, transport, Web UI, and application packaging.
 
 Kit releases run independently of Harness releases. The kit repository qualifies and publishes the Node API and engine npm packages at a shared version, starting at `0.0.1`. Harness consumes an exact npm version and commits its dependency resolution in `pnpm-lock.yaml`; Harness releases neither build nor publish kit packages.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md

@@ -10,7 +10,7 @@ LibreOffice 编译、源码补丁、平台资格验证和大型二进制发布
 
 ## Decision
 
-`deepseek-harness/libreoffice-kit` 仓库维护可复用的 `@deepseek-ai/libreoffice-kit` Node API、Worker、字体处理、引擎选择、构建配方、补丁、测试和发布。API 不依赖 Cordis。Harness 负责将自己的 `DocumentRenderer` 服务适配到此 API,以及 Session 授权、转换生命周期、传输、Web UI 和应用打包。
+`deepseek-harness/libreoffice-kit` 仓库维护可复用的 `@deepseek-ai/libreoffice-kit` Node API、Worker、字体处理、引擎选择、构建配方、补丁、测试和发布。API 不依赖 Cordis。Harness 负责将自己的 `DocumentConverter` 服务适配到此 API,以及 Session 授权、转换生命周期、传输、Web UI 和应用打包。
 
 kit 发布流程独立于 Harness 发布流程。kit 仓库验证并以统一版本发布 Node API 和引擎 npm 包,起始版本为 `0.0.1`。Harness 消费精确的 npm 版本,并在 `pnpm-lock.yaml` 中提交依赖解析结果;Harness 发布既不构建也不发布 kit 包。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.md
-2026-09-15-bounded-office-rendering.md: 539eb7e4824897861f350f294365f283678a7fee
-2026-09-15-bounded-office-rendering.zh.md: 928d93a6500d6c7818f28dd20029648453d9f9cf
+2026-09-15-bounded-office-rendering.md: c8ffd5f476e9c5a7c7a0f85706872b2da1690681
+2026-09-15-bounded-office-rendering.zh.md: a03a605dd66142d6457d35154a0e1628dcfa7632

+ 3 - 1
.agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.md

@@ -10,7 +10,9 @@ Office preview and explicit document inspection can request the same conversion.
 
 ## Decision
 
-The [Host provider](../../../../packages/document/document-render-libreoffice/README.md) owns a shared conversion queue and transient content cache. Authorized source metadata enters admission before source bytes are loaded. The source callback receives reserved byte capacity and returns its read version; changed sources fail without publishing aliases. Exact source bytes and Office extension determine the digest. Each renderer lifetime adds a generation so engine/font/configuration replacement invalidates reuse.
+The `document-convert` service returns complete PDF bytes. Page rasterization and user presentation remain separate consumers, so conversion naming does not imply image rendering or preview UI.
+
+The [Host provider](../../../../packages/document/document-convert-libreoffice/README.md) owns a shared conversion queue and transient content cache. Authorized source metadata enters admission before source bytes are loaded. The source callback receives reserved byte capacity and returns its read version; changed sources fail without publishing aliases. Exact source bytes and Office extension determine the digest. Each renderer lifetime adds a generation so engine/font/configuration replacement invalidates reuse.
 
 A bounded source-version index avoids repeated reads after authorization; the digest remains the identity for sharing conversion across distinct paths. Ready PDFs use an entry/byte-bounded LRU. Queued jobs contain metadata and deferred callbacks. Reader, queue, source-byte, and conversion limits also apply before work completes. Each active source locator belongs to live readers, so cancellation cannot grow retained source metadata independently of reader admission. Unknown source sizes reserve the input cap; cancellation retains active capacity until actual read/conversion cleanup settles.
 

+ 3 - 1
.agents/notes/implemented/architecture/2026-09-15-bounded-office-rendering.zh.md

@@ -10,7 +10,9 @@ Office 预览和显式文档检查可能请求相同转换。仅缓存已完成
 
 ## 决策
 
-[宿主提供方](../../../../packages/document/document-render-libreoffice/README.zh.md)拥有共享转换队列和临时内容缓存。已授权的源文件元数据在加载字节之前进入准入流程。源回调接收预留的字节容量并返回读取版本;源文件变化会导致失败,不发布别名。确切的源字节和 Office 扩展名决定摘要。每个渲染器生命周期附加代次,因此引擎、字体或配置替换会使复用失效。
+`document-convert` 服务返回完整 PDF 字节。页面栅格化和用户展示由独立消费方负责,因此转换命名不隐含图片渲染或预览 UI。
+
+[宿主提供方](../../../../packages/document/document-convert-libreoffice/README.zh.md)拥有共享转换队列和临时内容缓存。已授权的源文件元数据在加载字节之前进入准入流程。源回调接收预留的字节容量并返回读取版本;源文件变化会导致失败,不发布别名。确切的源字节和 Office 扩展名决定摘要。每个渲染器生命周期附加代次,因此引擎、字体或配置替换会使复用失效。
 
 有界的源版本索引在授权后避免重复读取;摘要仍是不同路径间共享转换的身份。已就绪 PDF 使用按条目与字节限制的 LRU。排队任务包含元数据和延迟回调。读取方、队列、源字节与转换限制在工作完成前也适用。每个在途源定位信息归属于活跃读取方,因此取消操作不能让保留的源元数据脱离读取方准入限制增长。未知源大小预留输入上限;取消后仍保留活动容量,直至实际读取、转换与清理结束。
 

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: 7d89ad7f5be786c74fe4584a1c29e64a181e7b2e
-README.zh.md: 3eb1bcb570ac8d9056573ba5e10e85b9df06546a
+README.md: f08fab5911450a615fc9b5cf750268b029a24f5d
+README.zh.md: d82a4f21e1bfcd9fbc56376033e4811bf4bc644d

+ 1 - 1
apps/desktop/README.md

@@ -116,7 +116,7 @@ Each target owns its packed package inputs, prepared runtime, package set, dsh t
 
 Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. It preserves runtime JavaScript, native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files.
 
-The [Office conversion provider](../../packages/document/document-render-libreoffice/README.md) carries one matching native engine on macOS/Windows or the WASM engine on Linux. Preparation rejects a missing target engine before packaging. Engine assets, licenses, and notices remain in the runtime dependency tree.
+The [Office conversion provider](../../packages/document/document-convert-libreoffice/README.md) carries one matching native engine on macOS/Windows or the WASM engine on Linux. Preparation rejects a missing target engine before packaging. Engine assets, licenses, and notices remain in the runtime dependency tree.
 
 The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; `prepare:dsh` runs the [payload smoke](tests/fixtures/runtime-payload-smoke.mjs) under Electron RunAsNode before the Host smoke and final inventory verification.
 

+ 1 - 1
apps/desktop/README.zh.md

@@ -116,7 +116,7 @@ macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 
 
 Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。它保留运行时 JavaScript、原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。
 
-[Office 转换提供方](../../packages/document/document-render-libreoffice/README.zh.md)在 macOS/Windows 携带一个匹配的原生引擎,在 Linux 携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。引擎资源、许可证和 notices 保留在运行时依赖树中。
+[Office 转换提供方](../../packages/document/document-convert-libreoffice/README.zh.md)在 macOS/Windows 携带一个匹配的原生引擎,在 Linux 携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。引擎资源、许可证和 notices 保留在运行时依赖树中。
 
 打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;`prepare:dsh` 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行[产物 smoke](tests/fixtures/runtime-payload-smoke.mjs)。
 

+ 2 - 2
docs/capability-seams.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/capability-seams.md
-capability-seams.md: 8cfa30e16a3ef782719d2360f2e6cbdc4a4560b8
-capability-seams.zh.md: a709b14dd36f0f0b2f7be036cf99603f69631716
+capability-seams.md: ebe0bb074ad2d0c3739b83ddb3f08d1165e85277
+capability-seams.zh.md: 16707057fcd3f654c635f950c5f7fb77f23a3248

+ 6 - 6
docs/capability-seams.md

@@ -26,9 +26,9 @@ flowchart LR
   svc_computerUse["ctx.computerUse<br/>Computer-use provider registration"]
   pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
   pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"]
-  pkg_document_render["document-render"]
-  svc_documentRender["ctx.documentRender<br/>Office to PDF rendering"]
-  pkg_document_render_libreoffice["document-render-libreoffice"]
+  pkg_document_convert["document-convert"]
+  svc_documentConvert["ctx.documentConvert<br/>Office to PDF conversion"]
+  pkg_document_convert_libreoffice["document-convert-libreoffice"]
   pkg_attachment["attachment"]
   svc_attachments["ctx.attachments<br/>Durable binary attachment storage"]
   pkg_attachment_local["attachment-local"]
@@ -283,8 +283,8 @@ flowchart LR
   pkg_credentials --> svc_credentials
   pkg_credentials_local --> svc_credentials
   pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
-  pkg_document_render --> svc_documentRender
-  pkg_document_render_libreoffice --> svc_documentRender
+  pkg_document_convert --> svc_documentConvert
+  pkg_document_convert_libreoffice --> svc_documentConvert
   pkg_experimental_agent_team --> svc_agentTeams
   pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse
   pkg_experimental_browser_use_playwright_mcp --> svc_browserUse
@@ -533,7 +533,7 @@ flowchart LR
 | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | Connection-owned providers serve shared resource tools in the calling agent scope. |
 | `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | One provider-owned name per service instance. Providers own their tools and browser resources per live Session; the shared service has no browser operation API. |
 | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | One provider-owned name per service instance. Each provider also owns its model tools; the service has no common action API, runtime selection, or Session workflow lock. |
-| `ctx.documentRender` | `seam` | [`document-render`](../packages/document/document-render) | [`document-render-libreoffice`](../packages/document/document-render-libreoffice) | - | - | Authorized Office bytes are converted on the Host using native engines on macOS/Windows and Node WASM on Linux. |
+| `ctx.documentConvert` | `seam` | [`document-convert`](../packages/document/document-convert) | [`document-convert-libreoffice`](../packages/document/document-convert-libreoffice) | - | - | Authorized Office bytes are converted on the Host using native engines on macOS/Windows and Node WASM on Linux. |
 | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. |
 | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | Owns streaming intake, durable storage, and staged receipt lifetime; the Session controller binds receipts to accepted submissions. |
 | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |

+ 6 - 6
docs/capability-seams.zh.md

@@ -28,9 +28,9 @@ flowchart LR
   svc_computerUse["ctx.computerUse<br/>Computer-use provider registration"]
   pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
   pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"]
-  pkg_document_render["document-render"]
-  svc_documentRender["ctx.documentRender<br/>Office to PDF rendering"]
-  pkg_document_render_libreoffice["document-render-libreoffice"]
+  pkg_document_convert["document-convert"]
+  svc_documentConvert["ctx.documentConvert<br/>Office to PDF conversion"]
+  pkg_document_convert_libreoffice["document-convert-libreoffice"]
   pkg_attachment["attachment"]
   svc_attachments["ctx.attachments<br/>Durable binary attachment storage"]
   pkg_attachment_local["attachment-local"]
@@ -285,8 +285,8 @@ flowchart LR
   pkg_credentials --> svc_credentials
   pkg_credentials_local --> svc_credentials
   pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
-  pkg_document_render --> svc_documentRender
-  pkg_document_render_libreoffice --> svc_documentRender
+  pkg_document_convert --> svc_documentConvert
+  pkg_document_convert_libreoffice --> svc_documentConvert
   pkg_experimental_agent_team --> svc_agentTeams
   pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse
   pkg_experimental_browser_use_playwright_mcp --> svc_browserUse
@@ -535,7 +535,7 @@ flowchart LR
 | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | 连接所有者提供的操作在调用 agent 的作用域内服务于共享资源工具。 |
 | `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | 每个服务实例注册一个提供方拥有的名称。提供方按实时 Session 拥有自己的工具与浏览器资源;共享服务不提供浏览器操作 API。 |
 | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | 每个服务实例只注册一个提供方自定的名称。各提供方也拥有自己的模型工具;服务不提供通用操作 API、运行时选择或 Session 流程锁。 |
-| `ctx.documentRender` | `seam` | [`document-render`](../packages/document/document-render) | [`document-render-libreoffice`](../packages/document/document-render-libreoffice) | - | - | 已授权的 Office 字节在宿主上转换;macOS/Windows 使用原生引擎,Linux 使用 Node WASM。 |
+| `ctx.documentConvert` | `seam` | [`document-convert`](../packages/document/document-convert) | [`document-convert-libreoffice`](../packages/document/document-convert-libreoffice) | - | - | 已授权的 Office 字节在宿主上转换;macOS/Windows 使用原生引擎,Linux 使用 Node WASM。 |
 | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 |
 | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | 负责流式接收、持久存储和暂存回执生命周期;Session Controller 将回执绑定到已接受的提交。 |
 | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 58eb43a6fea63c8e4ef85e22d45d89c875c62a2e
-config-catalog.zh.md: 78de098bee885fb9133264a80fb16c50ed373dc0
+config-catalog.md: ffe4d39af3a87b76d4bc6100c9575d8d6975696d
+config-catalog.zh.md: 62031ac825baf929b4465a1c608978558facf3c8

+ 4 - 4
docs/config-catalog.md

@@ -548,9 +548,9 @@ export interface Config {
 
 Source: [`packages/credentials/credentials-local/src/index.ts:64`](../packages/credentials/credentials-local/src/index.ts)
 
-<a id="deepseek-aidsh-document-render-libreoffice"></a>
+<a id="deepseek-aidsh-document-convert-libreoffice"></a>
 
-## `@deepseek-ai/dsh-document-render-libreoffice`
+## `@deepseek-ai/dsh-document-convert-libreoffice`
 
 ```ts config-catalog
 /** Provider concurrency and kit rendering/font configuration. */
@@ -596,7 +596,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/document/document-render-libreoffice/src/index.ts:14`](../packages/document/document-render-libreoffice/src/index.ts)
+Source: [`packages/document/document-convert-libreoffice/src/index.ts:14`](../packages/document/document-convert-libreoffice/src/index.ts)
 
 <a id="deepseek-aidsh-experimental-agent-team"></a>
 
@@ -3789,7 +3789,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
 - `@deepseek-ai/dsh-attachment` — abstract `AttachmentStore` ([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
 - `@deepseek-ai/dsh-compaction` — abstract `CompactionEngine` ([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
 - `@deepseek-ai/dsh-credentials` — abstract `CredentialProvider` ([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
-- `@deepseek-ai/dsh-document-render` — abstract `DocumentRenderer` ([`packages/document/document-render/src/index.ts`](../packages/document/document-render/src/index.ts))
+- `@deepseek-ai/dsh-document-convert` — abstract `DocumentConverter` ([`packages/document/document-convert/src/index.ts`](../packages/document/document-convert/src/index.ts))
 - `@deepseek-ai/dsh-file-reference` — abstract `FileReferenceService` ([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
 - `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
 - `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))

+ 4 - 4
docs/config-catalog.zh.md

@@ -550,9 +550,9 @@ export interface Config {
 
 来源:[`packages/credentials/credentials-local/src/index.ts:64`](../packages/credentials/credentials-local/src/index.ts)
 
-<a id="deepseek-aidsh-document-render-libreoffice"></a>
+<a id="deepseek-aidsh-document-convert-libreoffice"></a>
 
-## `@deepseek-ai/dsh-document-render-libreoffice`
+## `@deepseek-ai/dsh-document-convert-libreoffice`
 
 ```ts config-catalog
 /** Provider concurrency and kit rendering/font configuration. */
@@ -598,7 +598,7 @@ export interface Config {
 }
 ```
 
-源码: [`packages/document/document-render-libreoffice/src/index.ts:14`](../packages/document/document-render-libreoffice/src/index.ts)
+源码: [`packages/document/document-convert-libreoffice/src/index.ts:14`](../packages/document/document-convert-libreoffice/src/index.ts)
 
 <a id="deepseek-aidsh-experimental-agent-team"></a>
 
@@ -3791,7 +3791,7 @@ export interface Config {
 - `@deepseek-ai/dsh-attachment` — 抽象 `AttachmentStore`([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
 - `@deepseek-ai/dsh-compaction` — 抽象 `CompactionEngine`([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
 - `@deepseek-ai/dsh-credentials` — 抽象 `Credentials`([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
-- `@deepseek-ai/dsh-document-render` — 抽象 `DocumentRenderer`([`packages/document/document-render/src/index.ts`](../packages/document/document-render/src/index.ts))
+- `@deepseek-ai/dsh-document-convert` — 抽象 `DocumentConverter`([`packages/document/document-convert/src/index.ts`](../packages/document/document-convert/src/index.ts))
 - `@deepseek-ai/dsh-file-reference` — 抽象 `FileReferenceService`([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
 - `@deepseek-ai/dsh-fs` — 抽象 `FileSystem`([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
 - `@deepseek-ai/dsh-host-directory-picker` — 抽象 `DirectoryPicker`([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))

+ 2 - 2
docs/subsystems/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/README.md
-README.md: 7f39f52de2cd8fca3bbcb9973e7980de979d4385
-README.zh.md: facb05de4078b8033178e40e2ae18424b9183938
+README.md: f3593271de63f0436ccd0e704e787c7436b1ac00
+README.zh.md: 4f5ffba4b757e2d835a630cea14c09c7ecdb4980

+ 1 - 1
docs/subsystems/README.md

@@ -29,7 +29,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
 | [mcp.md](mcp.md) | external MCP connections, scoped tools and resources, server instructions, protocol results, and configuration ownership |
 | [user-questions.md](user-questions.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
 | [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit events, and answerer contracts |
-| [document-render.md](document-render.md) | authorized Office-to-PDF conversion, native/WASM engines, and bounded shared reuse |
+| [document-convert.md](document-convert.md) | authorized Office-to-PDF conversion, native/WASM engines, and bounded shared reuse |
 | [attachment.md](attachment.md) | durable image identity and metadata, validation inputs, verified reads, and the `AttachmentStore` seam |
 | [shell.md](shell.md) | the shell executor seam: `ShellExecRequest`/`Spec`, `ShellRunResult`, background `ShellProcess` handles |
 | [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary |

+ 1 - 1
docs/subsystems/README.zh.md

@@ -29,7 +29,7 @@
 | [mcp.md](mcp.zh.md) | 外部 MCP 连接、作用域工具与资源、服务器指令、协议结果与配置归属 |
 | [user-questions.md](user-questions.zh.md) | UI 支持的人工问答 seam:`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 |
 | [approval.md](approval.zh.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计事件和 answerer 约定 |
-| [document-render.md](document-render.zh.md) | 已授权的 Office 到 PDF 转换、原生/WASM 引擎与有界共享复用 |
+| [document-convert.md](document-convert.zh.md) | 已授权的 Office 到 PDF 转换、原生/WASM 引擎与有界共享复用 |
 | [attachment.md](attachment.zh.md) | 持久图片标识与元数据、校验输入、经校验读取,以及 `AttachmentStore` seam |
 | [shell.md](shell.zh.md) | shell 执行器 seam:`ShellExecRequest`/`Spec`、`ShellRunResult`、后台 `ShellProcess` 句柄 |
 | [subprocess.md](subprocess.zh.md) | 子进程 seam:完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 |

+ 3 - 3
docs/subsystems/document-render.i18n.yaml → docs/subsystems/document-convert.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write docs/subsystems/document-render.md
-document-render.md: f3e49fb45f2cc19956b6385cf7390825fa9d4c60
-document-render.zh.md: 79028858e9e3f95ac1a4618d3b9342b78aa29cf2
+#   pnpm run verify-translation-pairing --write docs/subsystems/document-convert.md
+document-convert.md: 2dea31918e92524606ad85826de276c2af05f06d
+document-convert.zh.md: 22f0ec54615ebbf86743de6320ec4e8cc3fc8113

+ 13 - 13
docs/subsystems/document-render.md → docs/subsystems/document-convert.md

@@ -1,6 +1,6 @@
-# Document rendering
+# Document conversion
 
-English | [中文](document-render.zh.md)
+English | [中文](document-convert.zh.md)
 
 The [document package family](../../packages/document/README.md) converts Office files to PDFs on the Node Host. Consumers authorize source reads and own presentation; the shared provider owns conversion, bounded admission, and transient PDF reuse. This subsystem creates no model-facing tool or Session event.
 
@@ -8,21 +8,21 @@ The [document package family](../../packages/document/README.md) converts Office
 
 | Owner | Responsibility |
 |---|---|
-| [document-render](../../packages/document/document-render/README.md) | `ctx.documentRender`: authorized Office bytes and complete PDF results |
-| [document-render-libreoffice](../../packages/document/document-render-libreoffice/README.md) | Host concurrency, reusable kit converters, private scratch files, and bounded PDF reads |
+| [document-convert](../../packages/document/document-convert/README.md) | `ctx.documentConvert`: authorized Office bytes and complete PDF results |
+| [document-convert-libreoffice](../../packages/document/document-convert-libreoffice/README.md) | Host concurrency, reusable kit converters, private scratch files, and bounded PDF reads |
 | [Web bundle](../../packages/bundle/web-app/README.md) | One configurable conversion provider shared by Host consumers |
 
 ## Requests and results
 
-[`DocumentRenderRequest`](../../packages/document/document-render/src/types.ts) contains an already-authorized source key/version, optional stat size, a deferred `read(signal, maxBytes)` callback, foreground/background priority, and a `DocumentExtension`: `doc`, `docx`, `xls`, `xlsx`, `ppt`, or `pptx`. `DocumentRenderer.render(request, signal?)` returns one complete PDF result. Cancellation follows the caller and provider lifetimes; validation, output, and engine failures reject with a classified `DocumentRenderError`.
+[`DocumentConvertRequest`](../../packages/document/document-convert/src/types.ts) contains an already-authorized source key/version, optional stat size, a deferred `read(signal, maxBytes)` callback, foreground/background priority, and a `DocumentExtension`: `doc`, `docx`, `xls`, `xlsx`, `ppt`, or `pptx`. `DocumentConverter.convert(request, signal?)` returns one complete PDF result. Cancellation follows the caller and provider lifetimes; validation, output, and engine failures reject with a classified `DocumentConvertError`.
 
-`DocumentRenderPriority` is `foreground` for requested preview/QA and `background` for speculation. `DocumentSourceKey` brands the caller-owned authorized source locator. `DocumentRendererGeneration` brands a provider lifetime, and `DocumentRenderKey` brands its content identity; neither opaque value is parsed by consumers.
+`DocumentConvertPriority` is `foreground` for requested preview/QA and `background` for speculation. `DocumentSourceKey` brands the caller-owned authorized source locator. `DocumentConverterGeneration` brands a provider lifetime, and `DocumentConvertKey` brands its content identity; neither opaque value is parsed by consumers.
 
 | Result field | Meaning |
 |---|---|
 | `pdf` | Caller-owned `Uint8Array` containing the complete PDF |
 | `missingFonts` | Requested document font families unavailable to this conversion |
-| `cacheKey` | Opaque renderer generation plus extension/source-content identity |
+| `cacheKey` | Opaque converter generation plus extension/source-content identity |
 | `generation` | Provider lifetime; replacement invalidates cached PDF reuse |
 
 The provider admits the deferred read before allocating source bytes, shares conversions by content identity, and removes its private scratch directory before returning. Returned PDF bytes remain valid after provider disposal. Source and PDF bytes do not enter Session storage. Consumers can use [Workspace Files](../../packages/api/workspace-files/README.md) for authorized bounded reads.
@@ -31,7 +31,7 @@ The provider admits the deferred read before allocating source bytes, shares con
 
 The external [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) Node API selects its precompiled engines. The kit has an independent version and release workflow, defined by the [release ownership decision](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md). Application builds install the published npm packages. macOS and Windows require their matching ARM64 or x64 native engine; Linux uses Node WASM. The [platform engine decision](../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md) defines installation and packaging. Invalid metadata, missing required assets, and conversion errors reject without switching engines. Conversion uses disk input and output paths on the Host, with no browser conversion engine or font RPC.
 
-The [Host provider configuration](../../packages/document/document-render-libreoffice/README.md#use-this-package) owns concurrency, deadlines, input/output limits, archive limits, image resolution, and font access. Native/WASM implementation and asset distribution belong to the kit workspace. System LibreOffice discovery, runtime engine downloads, persistent PDF caching, and model-facing rendering are outside this provider.
+The [Host provider configuration](../../packages/document/document-convert-libreoffice/README.md#use-this-package) owns concurrency, deadlines, input/output limits, archive limits, image resolution, and font access. Native/WASM implementation and asset distribution belong to the kit workspace. System LibreOffice discovery, runtime engine downloads, persistent PDF caching, and model-facing rendering are outside this provider.
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
@@ -41,9 +41,9 @@ The [Host provider configuration](../../packages/document/document-render-libreo
 
 Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
 
-<a id="ctxdocumentrender--documentrenderer-abstract-seam"></a>
+<a id="ctxdocumentconvert--documentconverter-abstract-seam"></a>
 
-### `ctx.documentRender` — `DocumentRenderer` (abstract seam)
+### `ctx.documentConvert` — `DocumentConverter` (abstract seam)
 
 Load one provider subclass per context; consumers own source authorization.
 
@@ -53,10 +53,10 @@ Load one provider subclass per context; consumers own source authorization.
  * @param request - authorized metadata and deferred bounded source read.
  * @param signal - caller cancellation; provider disposal also stops active work.
  * @returns caller-owned PDF bytes after conversion and scratch cleanup settle; canceled readers reject independently.
- * @throws {DocumentRenderError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.
+ * @throws {DocumentConvertError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.
  */
-abstract render(request: DocumentRenderRequest, signal?: AbortSignal): Promise<DocumentRenderResult>
+abstract convert(request: DocumentConvertRequest, signal?: AbortSignal): Promise<DocumentConvertResult>
 ```
 
-Source: [`packages/document/document-render/src/index.ts`](../../packages/document/document-render/src/index.ts)
+Source: [`packages/document/document-convert/src/index.ts`](../../packages/document/document-convert/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 13 - 13
docs/subsystems/document-render.zh.md → docs/subsystems/document-convert.zh.md

@@ -1,6 +1,6 @@
-# 文档渲染
+# 文档转换
 
-[English](document-render.md) | 中文
+[English](document-convert.md) | 中文
 
 [document 包族](../../packages/document/README.zh.md) 在 Node 宿主上将 Office 文件转换为 PDF。消费者负责源文件读取授权与展示;共享提供方负责转换、有界准入和临时 PDF 复用。此子系统不创建面向模型的工具或 Session 事件。
 
@@ -8,21 +8,21 @@
 
 | 所有者 | 职责 |
 |---|---|
-| [document-render](../../packages/document/document-render/README.zh.md) | `ctx.documentRender`:已授权 Office 字节和完整 PDF 结果 |
-| [document-render-libreoffice](../../packages/document/document-render-libreoffice/README.zh.md) | Host 并发、可复用 kit 转换器、私有临时文件和有上限的 PDF 读取 |
+| [document-convert](../../packages/document/document-convert/README.zh.md) | `ctx.documentConvert`:已授权 Office 字节和完整 PDF 结果 |
+| [document-convert-libreoffice](../../packages/document/document-convert-libreoffice/README.zh.md) | Host 并发、可复用 kit 转换器、私有临时文件和有上限的 PDF 读取 |
 | [Web bundle](../../packages/bundle/web-app/README.zh.md) | 由宿主消费者共享的单个可配置转换提供方 |
 
 ## 请求和结果
 
-[`DocumentRenderRequest`](../../packages/document/document-render/src/types.ts)包含已授权的源键与版本、可选 stat 大小、延迟的 `read(signal, maxBytes)` 回调、前台或后台优先级以及 `DocumentExtension`:`doc`、`docx`、`xls`、`xlsx`、`ppt` 或 `pptx`。`DocumentRenderer.render(request, signal?)` 返回一个完整 PDF 结果。取消遵循调用方和提供方生命周期;校验、输出和引擎失败以分类的 `DocumentRenderError` 拒绝。
+[`DocumentConvertRequest`](../../packages/document/document-convert/src/types.ts)包含已授权的源键与版本、可选 stat 大小、延迟的 `read(signal, maxBytes)` 回调、前台或后台优先级以及 `DocumentExtension`:`doc`、`docx`、`xls`、`xlsx`、`ppt` 或 `pptx`。`DocumentConverter.convert(request, signal?)` 返回一个完整 PDF 结果。取消遵循调用方和提供方生命周期;校验、输出和引擎失败以分类的 `DocumentConvertError` 拒绝。
 
-`DocumentRenderPriority` 对请求的预览或 QA 使用 `foreground`,对推测工作使用 `background`。`DocumentSourceKey` 为调用方拥有的已授权源定位符增加品牌类型。`DocumentRendererGeneration` 表示提供方生命周期,`DocumentRenderKey` 表示其内容身份;消费者不解析这两种不透明值。
+`DocumentConvertPriority` 对请求的预览或 QA 使用 `foreground`,对推测工作使用 `background`。`DocumentSourceKey` 为调用方拥有的已授权源定位符增加品牌类型。`DocumentConverterGeneration` 表示提供方生命周期,`DocumentConvertKey` 表示其内容身份;消费者不解析这两种不透明值。
 
 | 结果字段 | 含义 |
 |---|---|
 | `pdf` | 调用方拥有的 `Uint8Array`,包含完整 PDF |
 | `missingFonts` | 本次转换无法使用的文档请求字体名称 |
-| `cacheKey` | 不透明的渲染 generation 加扩展名与源内容身份 |
+| `cacheKey` | 不透明的转换 generation 加扩展名与源内容身份 |
 | `generation` | 提供方生命周期;替换后缓存 PDF 不再可复用 |
 
 提供方先准入延迟读取,再分配源文件字节;按内容身份共享转换,并在返回前删除私有临时目录。返回的 PDF 字节在提供方释放后仍有效。源文件和 PDF 字节不会进入 Session 存储。消费者可通过[工作区文件](../../packages/api/workspace-files/README.zh.md)执行已授权的有界读取。
@@ -31,7 +31,7 @@
 
 外部 [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) Node API 选择其预编译引擎。kit 独立维护版本和发布流程,具体归属由[发布归属决策](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md)定义。应用构建时安装已发布的 npm 包。macOS 和 Windows 要求匹配的 ARM64 或 x64 原生引擎;Linux 使用 Node WASM。[平台引擎决策](../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md)定义安装和打包规则。元数据无效、必需资源缺失和转换错误都会拒绝请求,不切换引擎。转换在 Host 使用磁盘输入输出路径,不使用浏览器转换引擎或字体 RPC。
 
-[Host 提供方配置](../../packages/document/document-render-libreoffice/README.zh.md#use-this-package)负责并发、期限、输入输出上限、归档上限、图像分辨率和字体访问。原生/WASM 实现和资产分发属于 kit 工作区。系统 LibreOffice 探测、运行时引擎下载、持久 PDF 缓存和面向模型的渲染不属于此提供方。
+[Host 提供方配置](../../packages/document/document-convert-libreoffice/README.zh.md#use-this-package)负责并发、期限、输入输出上限、归档上限、图像分辨率和字体访问。原生/WASM 实现和资产分发属于 kit 工作区。系统 LibreOffice 探测、运行时引擎下载、持久 PDF 缓存和面向模型的渲染不属于此提供方。
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
@@ -41,9 +41,9 @@
 
 Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
 
-<a id="ctxdocumentrender--documentrenderer-abstract-seam"></a>
+<a id="ctxdocumentconvert--documentconverter-abstract-seam"></a>
 
-### `ctx.documentRender` — `DocumentRenderer` (abstract seam)
+### `ctx.documentConvert` — `DocumentConverter` (abstract seam)
 
 Load one provider subclass per context; consumers own source authorization.
 
@@ -53,10 +53,10 @@ Load one provider subclass per context; consumers own source authorization.
  * @param request - authorized metadata and deferred bounded source read.
  * @param signal - caller cancellation; provider disposal also stops active work.
  * @returns caller-owned PDF bytes after conversion and scratch cleanup settle; canceled readers reject independently.
- * @throws {DocumentRenderError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.
+ * @throws {DocumentConvertError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.
  */
-abstract render(request: DocumentRenderRequest, signal?: AbortSignal): Promise<DocumentRenderResult>
+abstract convert(request: DocumentConvertRequest, signal?: AbortSignal): Promise<DocumentConvertResult>
 ```
 
-Source: [`packages/document/document-render/src/index.ts`](../../packages/document/document-render/src/index.ts)
+Source: [`packages/document/document-convert/src/index.ts`](../../packages/document/document-convert/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
packages/bundle/web-app/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
-README.md: 0057516fb5bfba2708923e08af20f6e59f53f772
-README.zh.md: 1be494520d18311cae1f8dfca00c682b597f889d
+README.md: 2b5c4f48f95fc40b7e25c33e4b14a510c0ace34f
+README.zh.md: a8117f8f7d1c3f12016d37c37b81e5a8734531aa

+ 1 - 1
packages/bundle/web-app/README.md

@@ -73,7 +73,7 @@ Each browser session composes its own agent from the shipped presets (the `stand
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-The bundle is one patch plus one runtime glue plugin. The storage stack and projection cache come from `dsh-base`; the web overlay's workspace and message-feedback rows consume that shared `storageDomain` service. The patch restates the surface-specific values the base deliberately omits, inserts the web-only host rows and browser roster, then moves the agent plane behind presets. The glue plugin owns dist serving, trust sampling, prompt sections, the bash variable, and the readiness announcements. The `document-render` row mounts one lazy [Office conversion provider](../../document/document-render-libreoffice/README.md) for Host consumers, including Desktop compositions using this bundle.
+The bundle is one patch plus one runtime glue plugin. The storage stack and projection cache come from `dsh-base`; the web overlay's workspace and message-feedback rows consume that shared `storageDomain` service. The patch restates the surface-specific values the base deliberately omits, inserts the web-only host rows and browser roster, then moves the agent plane behind presets. The glue plugin owns dist serving, trust sampling, prompt sections, the bash variable, and the readiness announcements. The `document-convert` row mounts one lazy [Office conversion provider](../../document/document-convert-libreoffice/README.md) for Host consumers, including Desktop compositions using this bundle.
 
 ### Patch semantics
 

+ 1 - 1
packages/bundle/web-app/README.zh.md

@@ -73,7 +73,7 @@ dsh --profile web --no-open --port 8080
 <details>
 <summary>实现细节——点击展开</summary>
 
-此 bundle 由一个补丁和一个运行时胶水插件组成。存储栈与投影缓存来自 `dsh-base`;Web 叠加层的工作区和消息反馈条目消费共享的 `storageDomain` 服务。补丁重述 base 有意省略的界面专用值,插入 Web 专用宿主条目和浏览器插件列表,再将 Agent 层移到预设后面。胶水插件负责 dist 服务、信任采样、提示词段落、bash 变量和就绪通知。`document-render` 条目为宿主消费者挂载一个延迟创建引擎的 [Office 转换提供方](../../document/document-render-libreoffice/README.zh.md),使用此 bundle 的 Desktop 组合也共享该提供方。
+此 bundle 由一个补丁和一个运行时胶水插件组成。存储栈与投影缓存来自 `dsh-base`;Web 叠加层的工作区和消息反馈条目消费共享的 `storageDomain` 服务。补丁重述 base 有意省略的界面专用值,插入 Web 专用宿主条目和浏览器插件列表,再将 Agent 层移到预设后面。胶水插件负责 dist 服务、信任采样、提示词段落、bash 变量和就绪通知。`document-convert` 条目为宿主消费者挂载一个延迟创建引擎的 [Office 转换提供方](../../document/document-convert-libreoffice/README.zh.md),使用此 bundle 的 Desktop 组合也共享该提供方。
 
 ### patch 语义
 

+ 2 - 2
packages/bundle/web-app/cordis.patch.yml

@@ -225,8 +225,8 @@
       name: '@deepseek-ai/dsh-client-ui-sidebar-right'
 
 
-    - id: document-render
-      name: '@deepseek-ai/dsh-document-render-libreoffice'
+    - id: document-convert
+      name: '@deepseek-ai/dsh-document-convert-libreoffice'
 
     # The right Sidebar's document tab: bounded file reads with selectable
     # Markdown, code, HTML, PDF, and plain-text renderers.

+ 2 - 2
packages/bundle/web-app/package.json

@@ -122,8 +122,8 @@
     "open": "^11.0.0",
     "@deepseek-ai/dsh-api-terminal-controller": "workspace:^",
     "@deepseek-ai/dsh-client-ui-sidebar-terminal": "workspace:^",
-    "@deepseek-ai/dsh-document-render": "workspace:^",
-    "@deepseek-ai/dsh-document-render-libreoffice": "workspace:^"
+    "@deepseek-ai/dsh-document-convert": "workspace:^",
+    "@deepseek-ai/dsh-document-convert-libreoffice": "workspace:^"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",

+ 12 - 12
packages/bundle/web-app/tests/document-conversion.e2e.ts

@@ -7,8 +7,8 @@ import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include, { applyEntryPatches } from '@deepseek-ai/cordis-plugin-include'
 import { loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
-import { DocumentSourceKey, type DocumentRenderRequest } from '@deepseek-ai/dsh-document-render'
-import * as LibreOfficeProvider from '@deepseek-ai/dsh-document-render-libreoffice'
+import { DocumentSourceKey, type DocumentConvertRequest } from '@deepseek-ai/dsh-document-convert'
+import * as LibreOfficeProvider from '@deepseek-ai/dsh-document-convert-libreoffice'
 import { expect, it, onTestFinished } from 'vitest'
 
 it('loads one shared conversion row and retains caller-owned PDFs after disposal', async () => {
@@ -20,9 +20,9 @@ it('loads one shared conversion row and retains caller-owned PDFs after disposal
   })
   const rows = loadOverlayPatches('conversion-test', fileURLToPath(new URL('../cordis.patch.yml', import.meta.url)))
     .flatMap(patch => patch.insert ?? [])
-    .filter(row => row.name === '@deepseek-ai/dsh-document-render-libreoffice')
-  expect(rows.map(row => row.id)).toEqual(['document-render'])
-  const configured = applyEntryPatches(rows, [{ id: 'document-render', config: { maxConcurrentConversions: 1 } }],
+    .filter(row => row.name === '@deepseek-ai/dsh-document-convert-libreoffice')
+  expect(rows.map(row => row.id)).toEqual(['document-convert'])
+  const configured = applyEntryPatches(rows, [{ id: 'document-convert', config: { maxConcurrentConversions: 1 } }],
     (message) => { throw new Error(message) })
   const configPath = join(directory, 'cordis.yml')
   await writeFile(configPath, JSON.stringify(configured))
@@ -33,19 +33,19 @@ it('loads one shared conversion row and retains caller-owned PDFs after disposal
   ctx.loader.internal = {
     version: 'v2',
     async import(specifier: string) {
-      if (specifier !== '@deepseek-ai/dsh-document-render-libreoffice') throw new Error(`Unexpected plugin: ${specifier}`)
+      if (specifier !== '@deepseek-ai/dsh-document-convert-libreoffice') throw new Error(`Unexpected plugin: ${specifier}`)
       return LibreOfficeProvider
     },
   } as unknown as NonNullable<typeof ctx.loader.internal>
   await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
   await ctx.loader.await()
-  const entry = [...ctx.loader.entries()].find(candidate => candidate.options.id === 'document-render')!
+  const entry = [...ctx.loader.entries()].find(candidate => candidate.options.id === 'document-convert')!
   await entry.fiber!.await()
   expect(ctx.get('documentRenderController')).toBeUndefined()
   expect(ctx.get('skills')).toBeUndefined()
   const bytes = await readFile(new URL('./fixtures/document-conversion.docx', import.meta.url))
   let reads = 0
-  const request = (key: string): DocumentRenderRequest => ({
+  const request = (key: string): DocumentConvertRequest => ({
     extension: 'docx', priority: 'foreground',
     source: { key: DocumentSourceKey(key), version: 'fixture', bytes: bytes.length,
       async read(signal, maxBytes) {
@@ -56,8 +56,8 @@ it('loads one shared conversion row and retains caller-owned PDFs after disposal
       },
     },
   })
-  const provider = ctx.documentRender
-  const [first, second] = await Promise.all([provider.render(request('first')), provider.render(request('second'))])
+  const provider = ctx.documentConvert
+  const [first, second] = await Promise.all([provider.convert(request('first')), provider.convert(request('second'))])
   expect(reads).toBe(2)
   expect(Buffer.from(first.pdf).subarray(0, 5).toString()).toBe('%PDF-')
   expect(first.cacheKey).toBe(second.cacheKey)
@@ -66,11 +66,11 @@ it('loads one shared conversion row and retains caller-owned PDFs after disposal
   expect(first.missingFonts).not.toBe(second.missingFonts)
   first.pdf.fill(0)
   first.missingFonts.push('caller-owned')
-  const cached = await provider.render(request('first'))
+  const cached = await provider.convert(request('first'))
   expect(reads).toBe(2)
   expect(cached.pdf).toEqual(second.pdf)
   expect(cached.missingFonts).toEqual(second.missingFonts)
   await ctx.fiber.dispose()
   expect(Buffer.from(cached.pdf).subarray(0, 5).toString()).toBe('%PDF-')
-  await expect(provider.render(request('first'))).rejects.toMatchObject({ code: 'unavailable' })
+  await expect(provider.convert(request('first'))).rejects.toMatchObject({ code: 'unavailable' })
 })

+ 2 - 2
packages/document/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/document/README.md
-README.md: 4ccf8e9c7267b7442ee08a218b51e50a4909aae3
-README.zh.md: c91beff7d3f6a028d9a1553308714c0328ab35de
+README.md: 0f9d3f1ae0cb154a445e34debe0bf49ef5400d1e
+README.zh.md: 0325634d6c435b6d8aeebf202d04f84fa4975e33

+ 6 - 6
packages/document/README.md

@@ -3,13 +3,13 @@ description: "Package map for Host Office conversion and reusable PDF results."
 kind: "package-group"
 ---
 
-# document/ — Office rendering
+# document/ — Office conversion
 
 English | [中文](README.zh.md)
 
 ## Summary
 
-Convert authorized Office files to reusable PDFs on the Host. The rendering family provides a shared conversion operation and a LibreOffice kit provider. macOS and Windows use their matching native engine; Linux uses Node WASM.
+Convert authorized Office files to reusable PDFs on the Host. The conversion family provides a shared conversion operation and a LibreOffice kit provider. macOS and Windows use their matching native engine; Linux uses Node WASM.
 
 ## Table of Contents
 
@@ -22,12 +22,12 @@ Convert authorized Office files to reusable PDFs on the Host. The rendering fami
 <a id="packages"></a>
 ## Packages
 
-Each package owns its configuration and lifetime rules; the subsystem reference describes their shared rendering operation.
+Each package owns its configuration and lifetime rules; the subsystem reference describes their shared conversion operation.
 
 | Package | Role | ctx key |
 |---|---|---|
-| [document-render](document-render/README.md) | Authorized Office bytes to complete PDF results | `ctx.documentRender` |
-| [document-render-libreoffice](document-render-libreoffice/README.md) | Host kit provider with cancellable concurrency and private scratch files | `ctx.documentRender` |
+| [document-convert](document-convert/README.md) | Authorized Office bytes to complete PDF results | `ctx.documentConvert` |
+| [document-convert-libreoffice](document-convert-libreoffice/README.md) | Host kit provider with cancellable concurrency and private scratch files | `ctx.documentConvert` |
 
 -----
 
@@ -36,7 +36,7 @@ Each package owns its configuration and lifetime rules; the subsystem reference
 
 Consumers own source authorization and presentation.
 
-- [Document rendering](../../docs/subsystems/document-render.md) — shared operation and generated service reference.
+- [Document conversion](../../docs/subsystems/document-convert.md) — shared operation and generated service reference.
 - [Independent kit ownership](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md) — engine distribution and application integration.
 - [Workspace Files](../api/workspace-files/README.md) — authorized bounded source reads.
 

+ 6 - 6
packages/document/README.zh.md

@@ -3,13 +3,13 @@ description: "Host Office 转换和可复用 PDF 结果的包索引。"
 kind: "package-group"
 ---
 
-# document/ — Office 渲染
+# document/ — Office 转换
 
 [English](README.md) | 中文
 
 ## 摘要
 
-在宿主上将已授权的 Office 文件转换为可复用的 PDF。渲染包族提供共享转换操作和 LibreOffice kit 提供方。macOS 和 Windows 使用匹配的原生引擎;Linux 使用 Node WASM。
+在宿主上将已授权的 Office 文件转换为可复用的 PDF。转换包族提供共享转换操作和 LibreOffice kit 提供方。macOS 和 Windows 使用匹配的原生引擎;Linux 使用 Node WASM。
 
 ## 目录
 
@@ -22,12 +22,12 @@ kind: "package-group"
 <a id="packages"></a>
 ## 包
 
-每个包负责自身配置和生命周期规则;子系统参考描述共享渲染操作。
+每个包负责自身配置和生命周期规则;子系统参考描述共享转换操作。
 
 | 包 | 职责 | ctx 键 |
 |---|---|---|
-| [document-render](document-render/README.zh.md) | 将已授权 Office 字节转换为完整 PDF 结果 | `ctx.documentRender` |
-| [document-render-libreoffice](document-render-libreoffice/README.zh.md) | 使用可取消并发和私有临时文件的 Host kit 提供方 | `ctx.documentRender` |
+| [document-convert](document-convert/README.zh.md) | 将已授权 Office 字节转换为完整 PDF 结果 | `ctx.documentConvert` |
+| [document-convert-libreoffice](document-convert-libreoffice/README.zh.md) | 使用可取消并发和私有临时文件的 Host kit 提供方 | `ctx.documentConvert` |
 
 -----
 
@@ -36,7 +36,7 @@ kind: "package-group"
 
 消费者负责源文件授权与展示。
 
-- [文档渲染](../../docs/subsystems/document-render.zh.md) — 共享操作和生成的服务参考。
+- [文档转换](../../docs/subsystems/document-convert.zh.md) — 共享操作和生成的服务参考。
 - [独立 kit 所有权](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md) — 引擎分发与应用集成。
 - [工作区文件](../api/workspace-files/README.zh.md) — 已授权的有界源文件读取。
 

+ 3 - 3
packages/document/document-render-libreoffice/README.i18n.yaml → packages/document/document-convert-libreoffice/README.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write packages/document/document-render-libreoffice/README.md
-README.md: 3315498c928a5613809ad3f75a1d9fe3e4bc07d2
-README.zh.md: e39f5824d5f969d997914848ebdd6dc767186dc7
+#   pnpm run verify-translation-pairing --write packages/document/document-convert-libreoffice/README.md
+README.md: 1c287b714ace289fa03f8be6da722fbe78e64065
+README.zh.md: 796d14c3bd873a3a6a24d4691f0d406d0894a122

+ 5 - 5
packages/document/document-render-libreoffice/README.md → packages/document/document-convert-libreoffice/README.md

@@ -3,7 +3,7 @@ description: "Host Office conversion with the independently published LibreOffic
 kind: "package-reference"
 ---
 
-# @deepseek-ai/dsh-document-render-libreoffice
+# @deepseek-ai/dsh-document-convert-libreoffice
 
 English | [中文](README.zh.md)
 
@@ -25,7 +25,7 @@ Convert Office documents to PDFs on the Host computer. macOS and Windows use the
 <a id="use-this-package"></a>
 ## Use this package
 
-The [Web bundle](../../bundle/web-app/README.md) mounts this provider as `document-render`. Independent compositions mount `@deepseek-ai/dsh-document-render-libreoffice` as a `cordis.yml` row.
+The [Web bundle](../../bundle/web-app/README.md) mounts this provider as `document-convert`. Independent compositions mount `@deepseek-ai/dsh-document-convert-libreoffice` as a `cordis.yml` row.
 
 The provider depends on the independently published [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit/tree/main/packages/entry) npm API at kit version `0.0.1`. npm selects the matching macOS/Windows native package or the Linux-only WASM package. A missing native engine rejects conversion on macOS/Windows without selecting WASM. The [platform engine decision](../../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md) defines installation and packaging; the [release ownership decision](../../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md) defines the independent kit and Harness responsibilities.
 
@@ -38,9 +38,9 @@ The provider depends on the independently published [`@deepseek-ai/libreoffice-k
 | `maxImageResolution` | `192` | Maximum raster-image DPI; overrides the kit default of `144`. |
 | `fontFallbacks` | Kit defaults | Ordered font-family preference groups; each group requires at least two names containing non-whitespace characters. |
 
-The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-document-render-libreoffice) owns the full font, archive, and image settings. `fontDirectories` accepts absolute directories; omission uses the kit platform defaults. Explicit `fontFallbacks` replaces the kit's default groups. Installed requested fonts retain precedence, and other system fonts remain eligible for uncovered glyphs. Native engines can select installed metric-compatible fonts before these preferences.
+The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-document-convert-libreoffice) owns the full font, archive, and image settings. `fontDirectories` accepts absolute directories; omission uses the kit platform defaults. Explicit `fontFallbacks` replaces the kit's default groups. Installed requested fonts retain precedence, and other system fonts remain eligible for uncovered glyphs. Native engines can select installed metric-compatible fonts before these preferences.
 
-The provider retains successful PDFs by renderer generation, Office extension, and SHA-256 of the exact source bytes. A bounded source-version index avoids rereading known content after an authorized stat; content identity also shares conversion across different source paths. Least-recently-used PDFs leave at either retention limit, together with their aliases. Failures and oversized cache entries are not retained. Every result has independent PDF/font buffers. Ready alias hits consume no reader slot; active source locators are released when their last reader leaves. Reopening a source after its final reader cancels rereads its bytes before sharing by digest, even if another source kept the conversion alive or its PDF is ready.
+The provider retains successful PDFs by converter generation, Office extension, and SHA-256 of the exact source bytes. A bounded source-version index avoids rereading known content after an authorized stat; content identity also shares conversion across different source paths. Least-recently-used PDFs leave at either retention limit, together with their aliases. Failures and oversized cache entries are not retained. Every result has independent PDF/font buffers. Ready alias hits consume no reader slot; active source locators are released when their last reader leaves. Reopening a source after its final reader cancels rereads its bytes before sharing by digest, even if another source kept the conversion alive or its PDF is ready.
 
 Admission bounds queued metadata, outstanding readers, active source-byte reservations, and conversions before invoking a source read. Unknown source sizes reserve `maxInputBytes`; known sizes reserve their stat size. Reads receive that capacity and may read one overflow sentinel byte. `maxSourceBytes` must cover `maxInputBytes`. The final reader allowance is reserved for foreground work. Setting `maxBackgroundConversions` to zero rejects background joins to queued and running work; completed alias hits remain available. Background jobs wait while any foreground job is queued, including when it awaits source capacity. Foreground joins promote queued prewarming; when its last foreground reader leaves, the queued job returns to background priority and eligible work can start immediately. Foreground admission can evict queued speculation. Background concurrency leaves a foreground slot when total concurrency exceeds one. A running prewarm keeps its background admission slot until settlement, even after promotion. The final reader cancels shared work. Removing a queued foreground blocker immediately admits other eligible work; active reservations remain held until actual read/conversion cleanup settles.
 
@@ -65,7 +65,7 @@ Each concurrent slot lazily creates and reuses one kit converter. The provider w
 <a id="further-exploration"></a>
 ## Further Exploration
 
-- [Rendering service](../document-render/README.md) — input and result ownership.
+- [Rendering service](../document-convert/README.md) — input and result ownership.
 - [Workspace Files](../../api/workspace-files/README.md) — Session file authorization and bounded reads.
 
 -----

+ 5 - 5
packages/document/document-render-libreoffice/README.zh.md → packages/document/document-convert-libreoffice/README.zh.md

@@ -3,7 +3,7 @@ description: "通过独立发布的 LibreOffice kit 在 Host 转换 Office 文
 kind: "package-reference"
 ---
 
-# @deepseek-ai/dsh-document-render-libreoffice
+# @deepseek-ai/dsh-document-convert-libreoffice
 
 [English](README.md) | 中文
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用此包
 
-[Web bundle](../../bundle/web-app/README.zh.md)以 `document-render` 条目挂载此提供方。独立组合通过 `cordis.yml` 条目挂载 `@deepseek-ai/dsh-document-render-libreoffice`。
+[Web bundle](../../bundle/web-app/README.zh.md)以 `document-convert` 条目挂载此提供方。独立组合通过 `cordis.yml` 条目挂载 `@deepseek-ai/dsh-document-convert-libreoffice`。
 
 此 provider 依赖独立发布的 [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit/tree/main/packages/entry) npm API,kit 版本为 `0.0.1`。npm 选择匹配的 macOS/Windows 原生包或仅限 Linux 的 WASM 包。macOS/Windows 缺少原生引擎时拒绝转换,不会选择 WASM。[平台引擎决策](../../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md)定义安装与打包策略;[发布归属决策](../../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md)定义独立 kit 与 Harness 各自的职责。
 
@@ -38,9 +38,9 @@ kind: "package-reference"
 | `maxImageResolution` | `192` | 最大光栅图像 DPI;覆盖 kit 的默认值 `144`。 |
 | `fontFallbacks` | kit 默认值 | 有序字体族优先组;每组至少包含两个含非空白字符的名称。 |
 
-[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-document-render-libreoffice)定义全部字体、归档和图像设置。`fontDirectories` 接受绝对目录;省略时使用 kit 的平台默认值。显式 `fontFallbacks` 替换 kit 的默认分组。已安装的请求字体仍优先使用,缺失字形仍可由其他系统字体提供。原生引擎可能在应用这些优先规则前选中已安装的度量兼容字体。
+[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-document-convert-libreoffice)定义全部字体、归档和图像设置。`fontDirectories` 接受绝对目录;省略时使用 kit 的平台默认值。显式 `fontFallbacks` 替换 kit 的默认分组。已安装的请求字体仍优先使用,缺失字形仍可由其他系统字体提供。原生引擎可能在应用这些优先规则前选中已安装的度量兼容字体。
 
-提供方按渲染 generation、Office 扩展名和精确源字节的 SHA-256 保留成功 PDF。有界的源版本索引在授权 stat 后避免重读已知内容;内容标识也会在不同源路径之间共享转换。达到任一保留上限时,最近最少使用的 PDF 及其别名一同移除。不保留失败或超过缓存上限的结果。每个结果具有独立的 PDF 与字体缓冲区。已就绪别名命中不占用读取方名额;同一源的最后一个读取方离开时,立即释放其在途定位信息。源的最后一个读取方取消后,再次打开该源会重新读取字节,再按内容摘要共享转换,即使其他源仍保持该转换运行或其 PDF 已就绪。
+提供方按转换 generation、Office 扩展名和精确源字节的 SHA-256 保留成功 PDF。有界的源版本索引在授权 stat 后避免重读已知内容;内容标识也会在不同源路径之间共享转换。达到任一保留上限时,最近最少使用的 PDF 及其别名一同移除。不保留失败或超过缓存上限的结果。每个结果具有独立的 PDF 与字体缓冲区。已就绪别名命中不占用读取方名额;同一源的最后一个读取方离开时,立即释放其在途定位信息。源的最后一个读取方取消后,再次打开该源会重新读取字节,再按内容摘要共享转换,即使其他源仍保持该转换运行或其 PDF 已就绪。
 
 准入在调用源读取前限制排队元数据、未完成读取方、活动源字节预留和转换。未知源大小预留 `maxInputBytes`;已知大小预留 stat 字节数。读取收到该容量,最多额外读取一个超限哨兵字节。`maxSourceBytes` 必须覆盖 `maxInputBytes`。最后一个读取方额度预留给前台。`maxBackgroundConversions` 设为零时,拒绝后台读取方加入排队或运行中的工作;仍可命中已完成的别名缓存。只要仍有前台任务排队,后台任务就继续等待,包括前台正在等待源容量的情况。前台加入会提升排队预热;最后一个前台读取方离开后,排队任务恢复后台优先级,符合条件的工作可立即开始。前台准入也可移除排队推测工作。总并发大于一时,后台并发为前台保留一个槽位。正在运行的预热即使被提权,也保留后台准入槽位直至结束。最后一个读取方取消共享工作。移除排队的前台阻塞任务后,其他符合条件的工作立即准入;实际读取、转换和清理完成前仍保留活动预留容量。
 
@@ -65,7 +65,7 @@ kind: "package-reference"
 <a id="further-exploration"></a>
 ## 延伸阅读
 
-- [渲染服务](../document-render/README.zh.md) — 输入和结果所有权。
+- [渲染服务](../document-convert/README.zh.md) — 输入和结果所有权。
 - [工作区文件](../../api/workspace-files/README.zh.md) — Session 文件授权与有界读取。
 
 -----

+ 4 - 4
packages/document/document-render-libreoffice/package.json → packages/document/document-convert-libreoffice/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "@deepseek-ai/dsh-document-render-libreoffice",
+  "name": "@deepseek-ai/dsh-document-convert-libreoffice",
   "description": "Host Office conversion with native macOS/Windows engines and Linux Node WASM",
   "version": "0.1.6-alpha.1",
   "publishConfig": {
@@ -12,7 +12,7 @@
   "repository": {
     "type": "git",
     "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/document/document-render-libreoffice"
+    "directory": "packages/document/document-convert-libreoffice"
   },
   "exports": {
     ".": {
@@ -28,11 +28,11 @@
   ],
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/dsh-document-render": "workspace:^"
+    "@deepseek-ai/dsh-document-convert": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/dsh-document-render": "workspace:^",
+    "@deepseek-ai/dsh-document-convert": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^"
   },

+ 15 - 15
packages/document/document-render-libreoffice/src/index.ts → packages/document/document-convert-libreoffice/src/index.ts

@@ -5,10 +5,10 @@ import { tmpdir } from 'node:os'
 import { isAbsolute, join } from 'node:path'
 import type { Context } from '@deepseek-ai/cordis'
 import { createConverter, type Converter, type ConverterOptions } from '@deepseek-ai/libreoffice-kit'
-import { DocumentRenderer, DocumentRenderError, DocumentRendererGeneration, type DocumentExtension, type DocumentRenderRequest, type DocumentRenderResult } from '@deepseek-ai/dsh-document-render'
+import { DocumentConverter, DocumentConvertError, DocumentConverterGeneration, type DocumentExtension, type DocumentConvertRequest, type DocumentConvertResult } from '@deepseek-ai/dsh-document-convert'
 import z from '@deepseek-ai/schemastery'
 import { readPdf } from './output.ts'
-import { RenderQueue } from './queue.ts'
+import { ConversionQueue } from './queue.ts'
 
 /** Provider concurrency and kit rendering/font configuration. */
 export interface Config {
@@ -81,11 +81,11 @@ interface Slot {
 }
 
 /** A provider lifetime owns all converters, queued calls, and temporary files. */
-export class LibreOfficeRenderer extends DocumentRenderer {
+export class LibreOfficeConverter extends DocumentConverter {
   static Config = Config
-  readonly generation = DocumentRendererGeneration(randomUUID())
+  readonly generation = DocumentConverterGeneration(randomUUID())
   private readonly slots: Slot[] = []
-  private readonly queue: RenderQueue
+  private readonly queue: ConversionQueue
   private readonly options: ConverterOptions
 
   /**
@@ -103,7 +103,7 @@ export class LibreOfficeRenderer extends DocumentRenderer {
       ...(fontDirectories === undefined ? {} : { fontDirectories }),
       ...(fontFallbacks === undefined ? {} : { fontFallbacks }),
     }
-    this.queue = new RenderQueue(config, this.generation, (bytes, extension, signal) => this.convert(bytes, extension, signal))
+    this.queue = new ConversionQueue(config, this.generation, (bytes, extension, signal) => this.convertBytes(bytes, extension, signal))
     ctx.effect(() => async () => {
       await this.queue.dispose()
       const results = await Promise.allSettled(this.slots.map(async (slot) => {
@@ -115,11 +115,11 @@ export class LibreOfficeRenderer extends DocumentRenderer {
     })
   }
 
-  render(request: DocumentRenderRequest, signal?: AbortSignal): Promise<DocumentRenderResult> {
+  convert(request: DocumentConvertRequest, signal?: AbortSignal): Promise<DocumentConvertResult> {
     return this.queue.read(request, signal)
   }
 
-  private async convert(bytes: Uint8Array, extension: DocumentExtension, signal: AbortSignal): Promise<Pick<DocumentRenderResult, 'pdf' | 'missingFonts'>> {
+  private async convertBytes(bytes: Uint8Array, extension: DocumentExtension, signal: AbortSignal): Promise<Pick<DocumentConvertResult, 'pdf' | 'missingFonts'>> {
     signal.throwIfAborted()
     let slot = this.slots.find(candidate => !candidate.busy)
     if (slot === undefined) { slot = { busy: false }; this.slots.push(slot) }
@@ -134,7 +134,7 @@ export class LibreOfficeRenderer extends DocumentRenderer {
       }
       const converter = await slot.converter
       signal.throwIfAborted()
-      directory = await mkdtemp(join(tmpdir(), 'dsh-document-render-'))
+      directory = await mkdtemp(join(tmpdir(), 'dsh-document-convert-'))
       const inputPath = join(directory, `source.${extension}`)
       const outputPath = join(directory, 'converted.pdf')
       await writeFile(inputPath, bytes, { flag: 'wx', mode: 0o600, signal })
@@ -144,20 +144,20 @@ export class LibreOfficeRenderer extends DocumentRenderer {
       let pdf: Uint8Array
       try { pdf = await readPdf(outputPath, this.config.maxOutputBytes, signal) }
       catch (cause) {
-        if (cause instanceof DocumentRenderError) throw cause
-        throw new DocumentRenderError('invalid-output', 'The converter PDF could not be read.', { cause })
+        if (cause instanceof DocumentConvertError) throw cause
+        throw new DocumentConvertError('invalid-output', 'The converter PDF could not be read.', { cause })
       }
       signal.throwIfAborted()
       return { pdf, missingFonts: result.missingFonts }
     } catch (cause) {
       signal.throwIfAborted()
-      if (cause instanceof DocumentRenderError) throw cause
+      if (cause instanceof DocumentConvertError) throw cause
       const code = typeof cause === 'object' && cause !== null && 'code' in cause ? cause.code : undefined
       switch (code) {
         case 'input-too-large': case 'output-too-large': case 'invalid-document': case 'unsupported-format':
         case 'invalid-output': case 'timeout': case 'unavailable':
-          throw new DocumentRenderError(code, 'LibreOffice conversion failed.', { cause })
-        default: throw new DocumentRenderError('failed', 'LibreOffice conversion failed.', { cause })
+          throw new DocumentConvertError(code, 'LibreOffice conversion failed.', { cause })
+        default: throw new DocumentConvertError('failed', 'LibreOffice conversion failed.', { cause })
       }
     } finally {
       try { if (directory !== undefined) await rm(directory, { recursive: true, force: true }) }
@@ -166,4 +166,4 @@ export class LibreOfficeRenderer extends DocumentRenderer {
   }
 }
 
-export default LibreOfficeRenderer
+export default LibreOfficeConverter

+ 6 - 6
packages/document/document-render-libreoffice/src/output.ts → packages/document/document-convert-libreoffice/src/output.ts

@@ -1,7 +1,7 @@
 /** Read only the bounded, regular PDF created inside a private task directory. */
 import { constants } from 'node:fs'
 import { lstat, open } from 'node:fs/promises'
-import { DocumentRenderError } from '@deepseek-ai/dsh-document-render'
+import { DocumentConvertError } from '@deepseek-ai/dsh-document-convert'
 
 /**
  * Refuse missing, link-shaped, oversized, truncated, and non-PDF output.
@@ -13,12 +13,12 @@ import { DocumentRenderError } from '@deepseek-ai/dsh-document-render'
 export async function readPdf(path: string, limit: number, signal: AbortSignal): Promise<Uint8Array> {
   signal.throwIfAborted()
   const entry = await lstat(path)
-  if (!entry.isFile()) throw new DocumentRenderError('invalid-output', 'The converter output is not a regular file.')
+  if (!entry.isFile()) throw new DocumentConvertError('invalid-output', 'The converter output is not a regular file.')
   const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW)
   try {
     const info = await file.stat()
-    if (!info.isFile()) throw new DocumentRenderError('invalid-output', 'The converter output is not a regular file.')
-    if (info.size > limit) throw new DocumentRenderError('output-too-large', 'The converted PDF exceeds maxOutputBytes.')
+    if (!info.isFile()) throw new DocumentConvertError('invalid-output', 'The converter output is not a regular file.')
+    if (info.size > limit) throw new DocumentConvertError('output-too-large', 'The converted PDF exceeds maxOutputBytes.')
     const bytes = Buffer.alloc(info.size + 1)
     let length = 0
     while (length < bytes.length) {
@@ -28,11 +28,11 @@ export async function readPdf(path: string, limit: number, signal: AbortSignal):
       length += read.bytesRead
     }
     signal.throwIfAborted()
-    if (length > limit) throw new DocumentRenderError('output-too-large', 'The converted PDF exceeds maxOutputBytes.')
+    if (length > limit) throw new DocumentConvertError('output-too-large', 'The converted PDF exceeds maxOutputBytes.')
     const pdf = bytes.subarray(0, length)
     if (length !== info.size || !pdf.subarray(0, 8).toString('ascii').match(/^%PDF-\d\.\d/u)
       || !pdf.subarray(-1024).toString('ascii').trimEnd().endsWith('%%EOF')) {
-      throw new DocumentRenderError('invalid-output', 'The converter did not produce a complete PDF.')
+      throw new DocumentConvertError('invalid-output', 'The converter did not produce a complete PDF.')
     }
     return pdf
   } finally { await file.close() }

+ 25 - 25
packages/document/document-render-libreoffice/src/queue.ts → packages/document/document-convert-libreoffice/src/queue.ts

@@ -1,34 +1,34 @@
 /** Bounded source admission, shared content conversion, and caller-owned PDF delivery. */
 import { createHash } from 'node:crypto'
-import { DocumentRenderError, DocumentRenderKey, type DocumentExtension, type DocumentRendererGeneration,
-  type DocumentRenderRequest, type DocumentRenderResult } from '@deepseek-ai/dsh-document-render'
+import { DocumentConvertError, DocumentConvertKey, type DocumentExtension, type DocumentConverterGeneration,
+  type DocumentConvertRequest, type DocumentConvertResult } from '@deepseek-ai/dsh-document-convert'
 import type { Config } from './index.ts'
 
-type Converted = Pick<DocumentRenderResult, 'pdf' | 'missingFonts'>
+type Converted = Pick<DocumentConvertResult, 'pdf' | 'missingFonts'>
 type Convert = (bytes: Uint8Array, extension: DocumentExtension, signal: AbortSignal) => Promise<Converted>
 interface Reader {
   job: Job
   readonly source: string
-  readonly priority: DocumentRenderRequest['priority']
-  readonly resolve: (value: DocumentRenderResult) => void
+  readonly priority: DocumentConvertRequest['priority']
+  readonly resolve: (value: DocumentConvertResult) => void
   readonly reject: (error: unknown) => void
   readonly cleanup: () => void
 }
 interface Job {
-  readonly request: DocumentRenderRequest
+  readonly request: DocumentConvertRequest
   readonly controller: AbortController
   readonly readers: Set<Reader>
   readonly sources: Set<string>
-  priority: DocumentRenderRequest['priority']
+  priority: DocumentConvertRequest['priority']
   state: 'queued' | 'running' | 'finished'
 }
 
-/** One renderer generation owns every queued source, conversion, reader, and retained PDF. */
-export class RenderQueue {
-  private readonly ready = new Map<DocumentRenderKey, DocumentRenderResult>()
-  private readonly aliases = new Map<string, DocumentRenderKey>()
+/** One converter generation owns every queued source, conversion, reader, and retained PDF. */
+export class ConversionQueue {
+  private readonly ready = new Map<DocumentConvertKey, DocumentConvertResult>()
+  private readonly aliases = new Map<string, DocumentConvertKey>()
   private readonly sources = new Map<string, Job>()
-  private readonly digests = new Map<DocumentRenderKey, Job>()
+  private readonly digests = new Map<DocumentConvertKey, Job>()
   private readonly queue: Job[] = []
   private readonly tasks = new Set<Promise<void>>()
   private readonly jobs = new Set<Job>()
@@ -44,7 +44,7 @@ export class RenderQueue {
    * @param generation - provider lifetime; prevents reuse after engine or font replacement.
    * @param convert - executes one admitted conversion and settles after scratch cleanup.
    */
-  constructor(private readonly config: Config, private readonly generation: DocumentRendererGeneration,
+  constructor(private readonly config: Config, private readonly generation: DocumentConverterGeneration,
     private readonly convert: Convert) {}
 
   /**
@@ -53,11 +53,11 @@ export class RenderQueue {
    * @param signal - this reader's cancellation; the final reader cancels shared work.
    * @returns independent PDF bytes; busy or canceled readers reject without releasing active engine capacity early.
    */
-  async read(request: DocumentRenderRequest, signal?: AbortSignal): Promise<DocumentRenderResult> {
+  async read(request: DocumentConvertRequest, signal?: AbortSignal): Promise<DocumentConvertResult> {
     signal?.throwIfAborted()
     if (this.disposed) throw this.unavailable()
     if (request.source.bytes !== undefined && request.source.bytes > this.config.maxInputBytes) {
-      throw new DocumentRenderError('input-too-large', 'The Office source exceeds maxInputBytes.')
+      throw new DocumentConvertError('input-too-large', 'The Office source exceeds maxInputBytes.')
     }
     const source = JSON.stringify([request.source.key, request.source.version, request.extension])
     const alias = this.aliases.get(source)
@@ -87,7 +87,7 @@ export class RenderQueue {
     const shared = job
     if (request.priority === 'foreground') shared.priority = 'foreground'
     this.readers++
-    const promise = new Promise<DocumentRenderResult>((resolve, reject) => {
+    const promise = new Promise<DocumentConvertResult>((resolve, reject) => {
       const abort = (): void => {
         this.release(reader)
         const reason: unknown = signal?.reason
@@ -114,9 +114,9 @@ export class RenderQueue {
     await Promise.allSettled(this.tasks)
   }
 
-  private busy(): DocumentRenderError { return new DocumentRenderError('busy', 'The document renderer has reached its admission limit.') }
-  private unavailable(): DocumentRenderError { return new DocumentRenderError('unavailable', 'The document renderer is unavailable.') }
-  private copy(result: DocumentRenderResult): DocumentRenderResult {
+  private busy(): DocumentConvertError { return new DocumentConvertError('busy', 'The document converter has reached its admission limit.') }
+  private unavailable(): DocumentConvertError { return new DocumentConvertError('unavailable', 'The document converter is unavailable.') }
+  private copy(result: DocumentConvertResult): DocumentConvertResult {
     return { ...result, pdf: Uint8Array.from(result.pdf), missingFonts: [...result.missingFonts] }
   }
   private release(reader: Reader): void {
@@ -177,10 +177,10 @@ export class RenderQueue {
     signal.throwIfAborted()
     const input = await job.request.source.read(signal, reserved)
     signal.throwIfAborted()
-    if (input.version !== job.request.source.version) throw new DocumentRenderError('source-changed', 'The source changed while waiting for conversion.')
-    if (input.bytes.byteLength > reserved) throw new DocumentRenderError('input-too-large', 'The source exceeds its reserved read capacity.')
+    if (input.version !== job.request.source.version) throw new DocumentConvertError('source-changed', 'The source changed while waiting for conversion.')
+    if (input.bytes.byteLength > reserved) throw new DocumentConvertError('input-too-large', 'The source exceeds its reserved read capacity.')
     const digest = createHash('sha256').update(job.request.extension).update('\0').update(input.bytes).digest('hex')
-    const key = DocumentRenderKey(`${this.generation}:${digest}`)
+    const key = DocumentConvertKey(`${this.generation}:${digest}`)
     const cached = this.ready.get(key)
     if (cached !== undefined) {
       this.ready.delete(key)
@@ -207,7 +207,7 @@ export class RenderQueue {
     } finally { if (this.digests.get(key) === job) this.digests.delete(key) }
   }
 
-  private finish(job: Job, result: DocumentRenderResult): void {
+  private finish(job: Job, result: DocumentConvertResult): void {
     for (const source of job.sources) this.sources.delete(source)
     if (this.ready.has(result.cacheKey)) {
       for (const source of job.sources) {
@@ -218,10 +218,10 @@ export class RenderQueue {
     }
     for (const reader of job.readers) { this.release(reader); reader.resolve(this.copy(result)) }
   }
-  private retain(result: DocumentRenderResult): void {
+  private retain(result: DocumentConvertResult): void {
     if (result.pdf.byteLength > this.config.maxCachedBytes) return
     while (this.ready.size >= this.config.maxCachedEntries || this.cachedBytes + result.pdf.byteLength > this.config.maxCachedBytes) {
-      const [key, oldest] = this.ready.entries().next().value as [DocumentRenderKey, DocumentRenderResult]
+      const [key, oldest] = this.ready.entries().next().value as [DocumentConvertKey, DocumentConvertResult]
       this.ready.delete(key)
       this.cachedBytes -= oldest.pdf.byteLength
       for (const [source, digest] of this.aliases) if (digest === key) this.aliases.delete(source)

+ 0 - 0
packages/document/document-render-libreoffice/tests/output.spec.ts → packages/document/document-convert-libreoffice/tests/output.spec.ts


+ 39 - 39
packages/document/document-render-libreoffice/tests/provider.spec.ts → packages/document/document-convert-libreoffice/tests/provider.spec.ts

@@ -1,22 +1,22 @@
 /** Disk output, resource bounds, and cancellation around the external kit. */
 import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
 import { dirname } from 'node:path'
-import { DocumentSourceKey, type DocumentRenderRequest } from '@deepseek-ai/dsh-document-render'
+import { DocumentSourceKey, type DocumentConvertRequest } from '@deepseek-ai/dsh-document-convert'
 import { Context } from '@deepseek-ai/cordis'
 import type { Converter, ConverterOptions } from '@deepseek-ai/libreoffice-kit'
 import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import LibreOfficeRenderer, { Config } from '../src/index.ts'
+import LibreOfficeConverter, { Config } from '../src/index.ts'
 
 const kit = vi.hoisted(() => ({ create: vi.fn<(options?: ConverterOptions) => Promise<Converter>>() }))
 vi.mock('@deepseek-ai/libreoffice-kit', () => ({ createConverter: kit.create }))
 
 const pdf = Buffer.from('%PDF-1.7\npreview\n%%EOF\n')
 const input = new Uint8Array([80, 75, 3, 4])
-const request: DocumentRenderRequest = { extension: 'docx', priority: 'foreground', source: {
+const request: DocumentConvertRequest = { extension: 'docx', priority: 'foreground', source: {
   key: DocumentSourceKey('source'), version: 'v1', bytes: input.length,
   read: async () => ({ bytes: input, version: 'v1' }),
 } }
-function distinct(index: number): DocumentRenderRequest {
+function distinct(index: number): DocumentConvertRequest {
   return { ...request, source: { ...request.source, key: DocumentSourceKey(`source-${index}`),
     read: async () => ({ bytes: new Uint8Array([80, 75, 3, index]), version: 'v1' }) } }
 }
@@ -35,14 +35,14 @@ beforeEach(() => {
 })
 afterEach(async () => { await ctx.fiber.dispose() })
 
-async function mount(config: Partial<Config> = {}): Promise<LibreOfficeRenderer> {
-  await ctx.plugin(LibreOfficeRenderer, config)
-  return ctx.documentRender as LibreOfficeRenderer
+async function mount(config: Partial<Config> = {}): Promise<LibreOfficeConverter> {
+  await ctx.plugin(LibreOfficeConverter, config)
+  return ctx.documentConvert as LibreOfficeConverter
 }
 
 it('preserves the kit font defaults when font configuration is omitted', async () => {
-  const renderer = await mount()
-  await renderer.render(request)
+  const provider = await mount()
+  await provider.convert(request)
   expect(kit.create.mock.calls[0]![0]).not.toHaveProperty('fontDirectories')
   expect(kit.create.mock.calls[0]![0]).not.toHaveProperty('fontFallbacks')
   expect(Config({ fontDirectories: [] }).fontDirectories).toEqual([])
@@ -65,7 +65,7 @@ it('passes configured limits to the kit, reuses the converter, and removes all s
     maxImageResolution: 144, maxArchiveEntries: 32, maxUncompressedBytes: 4096,
     timeoutMs: 321, fontDirectories: [], fontFallbacks: [['sans-serif', 'Arial'], ['serif', 'Times New Roman']],
     maxFontFiles: 12, maxFontFileBytes: 4096, maxLoadedFontBytes: 8192 })
-  const renderer = await mount(config)
+  const provider = await mount(config)
   let scratch = ''
   render.mockImplementation(async ({ inputPath, outputPath }) => {
     scratch = dirname(inputPath)
@@ -77,10 +77,10 @@ it('passes configured limits to the kit, reuses the converter, and removes all s
     await writeFile(outputPath, pdf)
     return { backend: 'native', missingFonts: ['Missing Serif'] }
   })
-  const result = await renderer.render(request)
+  const result = await provider.convert(request)
   expect(result).toMatchObject({ pdf: Uint8Array.from(pdf), missingFonts: ['Missing Serif'] })
   await expect(access(scratch)).rejects.toMatchObject({ code: 'ENOENT' })
-  await renderer.render(request)
+  await provider.convert(request)
   const { maxConcurrentConversions: _count, maxQueuedJobs: _queued, maxReaders: _readers, maxSourceBytes: _source,
     maxBackgroundConversions: _background, maxCachedEntries: _entries, maxCachedBytes: _cached,
     maxSourceEntries: _aliases, ...options } = config
@@ -91,13 +91,13 @@ it('passes configured limits to the kit, reuses the converter, and removes all s
 })
 
 it('refuses an oversized input before allocating a converter', async () => {
-  const renderer = await mount({ maxInputBytes: 3 })
-  await expect(renderer.render(request)).rejects.toMatchObject({ code: 'input-too-large' })
+  const provider = await mount({ maxInputBytes: 3 })
+  await expect(provider.convert(request)).rejects.toMatchObject({ code: 'input-too-large' })
   expect(kit.create).not.toHaveBeenCalled()
 })
 
 it.each(['missing', 'directory', 'not-pdf', 'incomplete', 'too-large'] as const)('rejects %s kit output and removes its directory', async (kind) => {
-  const renderer = await mount({ maxOutputBytes: pdf.length })
+  const provider = await mount({ maxOutputBytes: pdf.length })
   let scratch = ''
   render.mockImplementation(async ({ inputPath, outputPath }) => {
     scratch = dirname(inputPath)
@@ -107,31 +107,31 @@ it.each(['missing', 'directory', 'not-pdf', 'incomplete', 'too-large'] as const)
     else if (kind === 'too-large') await writeFile(outputPath, Buffer.concat([pdf, pdf]))
     return { backend: 'wasm', missingFonts: [] }
   })
-  await expect(renderer.render(request)).rejects.toMatchObject({ code: kind === 'too-large' ? 'output-too-large' : 'invalid-output' })
+  await expect(provider.convert(request)).rejects.toMatchObject({ code: kind === 'too-large' ? 'output-too-large' : 'invalid-output' })
   await expect(access(scratch)).rejects.toMatchObject({ code: 'ENOENT' })
 })
 
 it('retries initialization after a failed kit factory and reports unclassified errors', async () => {
-  const renderer = await mount()
+  const provider = await mount()
   kit.create.mockRejectedValueOnce(Object.assign(new Error('absent asset'), { code: 'unavailable' }))
-  await expect(renderer.render(request)).rejects.toMatchObject({ code: 'unavailable' })
-  expect(await renderer.render(request)).toMatchObject({ pdf: Uint8Array.from(pdf), missingFonts: ['Missing Serif'] })
+  await expect(provider.convert(request)).rejects.toMatchObject({ code: 'unavailable' })
+  expect(await provider.convert(request)).toMatchObject({ pdf: Uint8Array.from(pdf), missingFonts: ['Missing Serif'] })
   expect(kit.create).toHaveBeenCalledTimes(2)
   render.mockRejectedValueOnce(new Error('unexpected engine failure'))
-  await expect(renderer.render(distinct(9))).rejects.toMatchObject({ code: 'failed' })
+  await expect(provider.convert(distinct(9))).rejects.toMatchObject({ code: 'failed' })
 })
 
 it.each([
   'input-too-large', 'output-too-large', 'invalid-document', 'unsupported-format', 'invalid-output', 'timeout', 'unavailable',
 ] as const)('preserves the kit %s failure for the document consumer', async (code) => {
-  const renderer = await mount()
+  const provider = await mount()
   const cause = Object.assign(new Error('conversion failed'), { code })
   render.mockRejectedValueOnce(cause)
-  await expect(renderer.render(request)).rejects.toMatchObject({ code, cause })
+  await expect(provider.convert(request)).rejects.toMatchObject({ code, cause })
 })
 
 it('cancels a queued caller without starting or stopping another conversion', async () => {
-  const renderer = await mount({ maxConcurrentConversions: 1 })
+  const provider = await mount({ maxConcurrentConversions: 1 })
   const entered = Promise.withResolvers<AbortSignal>()
   const release = Promise.withResolvers<undefined>()
   render.mockImplementationOnce(async ({ outputPath }, signal) => {
@@ -140,11 +140,11 @@ it('cancels a queued caller without starting or stopping another conversion', as
     await writeFile(outputPath, pdf)
     return { backend: 'wasm', missingFonts: [] }
   })
-  const first = renderer.render(request)
+  const first = provider.convert(request)
   try {
     const activeSignal = await entered.promise
     const controller = new AbortController()
-    const second = renderer.render(request, controller.signal)
+    const second = provider.convert(request, controller.signal)
     const rejected = expect(second).rejects.toMatchObject({ name: 'AbortError' })
     await Promise.resolve(undefined)
     controller.abort()
@@ -152,11 +152,11 @@ it('cancels a queued caller without starting or stopping another conversion', as
     expect(render).toHaveBeenCalledOnce()
     expect(activeSignal.aborted).toBe(false)
   } finally { release.resolve(undefined); await first }
-  expect(await renderer.render(request)).toHaveProperty('pdf')
+  expect(await provider.convert(request)).toHaveProperty('pdf')
 })
 
 it('bounds active converters and resumes queued work when a slot becomes free', async () => {
-  const renderer = await mount({ maxConcurrentConversions: 2 })
+  const provider = await mount({ maxConcurrentConversions: 2 })
   const both = Promise.withResolvers<undefined>()
   const release = Promise.withResolvers<undefined>()
   let calls = 0
@@ -166,7 +166,7 @@ it('bounds active converters and resumes queued work when a slot becomes free',
     await writeFile(outputPath, pdf)
     return { backend: 'native', missingFonts: [] }
   })
-  const work = [renderer.render(distinct(1)), renderer.render(distinct(2)), renderer.render(distinct(3))]
+  const work = [provider.convert(distinct(1)), provider.convert(distinct(2)), provider.convert(distinct(3))]
   try {
     await both.promise
     expect(kit.create).toHaveBeenCalledTimes(2)
@@ -177,7 +177,7 @@ it('bounds active converters and resumes queued work when a slot becomes free',
 })
 
 it.each(['caller', 'provider'] as const)('joins late kit completion and scratch cleanup after %s cancellation', async (owner) => {
-  const renderer = await mount()
+  const provider = await mount()
   const entered = Promise.withResolvers<{ signal: AbortSignal; scratch: string }>()
   const release = Promise.withResolvers<undefined>()
   render.mockImplementation(async ({ inputPath, outputPath }, signal) => {
@@ -187,7 +187,7 @@ it.each(['caller', 'provider'] as const)('joins late kit completion and scratch
     return { backend: 'native', missingFonts: [] }
   })
   const caller = new AbortController()
-  const work = renderer.render(request, caller.signal)
+  const work = provider.convert(request, caller.signal)
   const rejected = expect(work).rejects.toMatchObject(owner === 'caller' ? { name: 'AbortError' } : { code: 'unavailable' })
   const { signal, scratch } = await entered.promise
   let settled = false
@@ -207,11 +207,11 @@ it.each(['caller', 'provider'] as const)('joins late kit completion and scratch
 })
 
 it('joins initialization during disposal and never starts a render after cancellation', async () => {
-  const renderer = await mount()
+  const provider = await mount()
   const entered = Promise.withResolvers<undefined>()
   const release = Promise.withResolvers<Converter>()
   kit.create.mockImplementationOnce(() => { entered.resolve(undefined); return release.promise })
-  const work = renderer.render(request)
+  const work = provider.convert(request)
   const rejected = expect(work).rejects.toMatchObject({ code: 'unavailable' })
   await entered.promise
   const closing = ctx.fiber.dispose()
@@ -220,29 +220,29 @@ it('joins initialization during disposal and never starts a render after cancell
   await closing
   expect(render).not.toHaveBeenCalled()
   expect(dispose).toHaveBeenCalledOnce()
-  await expect(renderer.render(request)).rejects.toMatchObject({ code: 'unavailable' })
+  await expect(provider.convert(request)).rejects.toMatchObject({ code: 'unavailable' })
 })
 
 it('rejects caller cancellation before allocating any conversion resources', async () => {
-  const renderer = await mount()
+  const provider = await mount()
   const reason = new Error('caller cancelled')
-  await expect(renderer.render(request, AbortSignal.abort(reason))).rejects.toBe(reason)
+  await expect(provider.convert(request, AbortSignal.abort(reason))).rejects.toBe(reason)
   expect(kit.create).not.toHaveBeenCalled()
 })
 
 it('rejects relative font directories during provider configuration', async () => {
-  expect(() => new LibreOfficeRenderer(ctx, Config({ fontDirectories: ['relative/fonts'] })))
+  expect(() => new LibreOfficeConverter(ctx, Config({ fontDirectories: ['relative/fonts'] })))
     .toThrow('fontDirectories must contain absolute paths')
 })
 
 it('rejects source capacity below one permitted input before allocating a converter', () => {
-  expect(() => new LibreOfficeRenderer(ctx, Config({ maxInputBytes: 4, maxSourceBytes: 3 })))
+  expect(() => new LibreOfficeConverter(ctx, Config({ maxInputBytes: 4, maxSourceBytes: 3 })))
     .toThrow('maxSourceBytes must be at least maxInputBytes')
   expect(kit.create).not.toHaveBeenCalled()
 })
 
 it('joins every converter disposal before reporting an engine cleanup failure', async () => {
-  const renderer = await mount({ maxConcurrentConversions: 2 })
+  const provider = await mount({ maxConcurrentConversions: 2 })
   const entered = Promise.withResolvers<undefined>()
   const released = Promise.withResolvers<undefined>()
   const cleanupEntered = Promise.withResolvers<undefined>()
@@ -258,7 +258,7 @@ it('joins every converter disposal before reporting an engine cleanup failure',
   kit.create.mockReset()
     .mockResolvedValueOnce({ backend: 'native', render, dispose: async () => { throw failed } })
     .mockResolvedValueOnce({ backend: 'native', render, dispose: async () => { cleanupEntered.resolve(undefined); await cleanupRelease.promise } })
-  const work = [renderer.render(distinct(1)), renderer.render(distinct(2))]
+  const work = [provider.convert(distinct(1)), provider.convert(distinct(2))]
   try { await entered.promise } finally { released.resolve(undefined); await Promise.all(work) }
   let disposed = false
   const closing = ctx.fiber.dispose().then(() => { disposed = true })

+ 10 - 10
packages/document/document-render-libreoffice/tests/queue.spec.ts → packages/document/document-convert-libreoffice/tests/queue.spec.ts

@@ -1,18 +1,18 @@
 /** Controlled source and engine completions exercise admission and shared content ownership. */
-import { DocumentSourceKey, DocumentRendererGeneration, type DocumentRenderRequest } from '@deepseek-ai/dsh-document-render'
+import { DocumentSourceKey, DocumentConverterGeneration, type DocumentConvertRequest } from '@deepseek-ai/dsh-document-convert'
 import { expect, it, onTestFinished, vi } from 'vitest'
-import { RenderQueue } from '../src/queue.ts'
+import { ConversionQueue } from '../src/queue.ts'
 import { Config } from '../src/index.ts'
 
 const output = { pdf: new Uint8Array([37, 80, 68, 70]), missingFonts: ['Font'] }
-function source(key: string, byte = 1, priority: DocumentRenderRequest['priority'] = 'foreground') {
-  const read = vi.fn<DocumentRenderRequest['source']['read']>().mockResolvedValue({ bytes: new Uint8Array([byte]), version: 'v1' })
-  const request: DocumentRenderRequest = { extension: 'docx', priority, source: { key: DocumentSourceKey(key), version: 'v1', bytes: 1, read } }
+function source(key: string, byte = 1, priority: DocumentConvertRequest['priority'] = 'foreground') {
+  const read = vi.fn<DocumentConvertRequest['source']['read']>().mockResolvedValue({ bytes: new Uint8Array([byte]), version: 'v1' })
+  const request: DocumentConvertRequest = { extension: 'docx', priority, source: { key: DocumentSourceKey(key), version: 'v1', bytes: 1, read } }
   return { read, request }
 }
 function harness(config: Partial<Config> = {}) {
-  const convert = vi.fn<ConstructorParameters<typeof RenderQueue>[2]>().mockResolvedValue(output)
-  const queue = new RenderQueue(Config(config), DocumentRendererGeneration('test'), convert)
+  const convert = vi.fn<ConstructorParameters<typeof ConversionQueue>[2]>().mockResolvedValue(output)
+  const queue = new ConversionQueue(Config(config), DocumentConverterGeneration('test'), convert)
   onTestFinished(() => queue.dispose())
   return { queue, convert }
 }
@@ -290,7 +290,7 @@ it('keeps a synchronous abort replacement shareable after the old conversion set
   const h = harness({ maxConcurrentConversions: 2 })
   const firstEntered = Promise.withResolvers<undefined>(), firstComplete = Promise.withResolvers<typeof output>()
   const replacementEntered = Promise.withResolvers<undefined>(), replacementComplete = Promise.withResolvers<typeof output>()
-  const replacementSpawned = Promise.withResolvers<{ work: ReturnType<RenderQueue['read']> }>()
+  const replacementSpawned = Promise.withResolvers<{ work: ReturnType<ConversionQueue['read']> }>()
   const a = source('a'), caller = new AbortController()
   const pending: Promise<unknown>[] = []
   h.convert.mockImplementationOnce((_bytes, _extension, signal) => {
@@ -332,11 +332,11 @@ it('keeps a synchronous abort replacement shareable after the old conversion set
   }
 })
 
-it('separates Office extensions and renderer generations in content identity', async () => {
+it('separates Office extensions and converter generations in content identity', async () => {
   const h = harness(), a = source('a')
   const first = await h.queue.read(a.request)
   const otherFormat = await h.queue.read({ ...a.request, extension: 'pptx' })
-  const other = new RenderQueue(Config({}), DocumentRendererGeneration('replacement'), h.convert)
+  const other = new ConversionQueue(Config({}), DocumentConverterGeneration('replacement'), h.convert)
   onTestFinished(() => other.dispose())
   const replacement = await other.read(a.request)
   expect(first.cacheKey).not.toBe(otherFormat.cacheKey)

+ 1 - 1
packages/document/document-render-libreoffice/tsconfig.json → packages/document/document-convert-libreoffice/tsconfig.json

@@ -15,7 +15,7 @@
       "path": "../../../vendor/schemastery"
     },
     {
-      "path": "../document-render"
+      "path": "../document-convert"
     }
   ]
 }

+ 3 - 3
packages/document/document-render/README.i18n.yaml → packages/document/document-convert/README.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write packages/document/document-render/README.md
-README.md: ead6c62b25dcca326ad362af4e28a6431472f55c
-README.zh.md: 45138e306a367833e0acade97b7d13f46fdcefa8
+#   pnpm run verify-translation-pairing --write packages/document/document-convert/README.md
+README.md: fbc98feef30c4e32c255b82f57ea1aad5a68940d
+README.zh.md: 3fa18652036d4823d45e3ccc38d03f599b3c4326

+ 4 - 4
packages/document/document-render/README.md → packages/document/document-convert/README.md

@@ -3,7 +3,7 @@ description: "Authorized Office input and complete PDF output for Host conversio
 kind: "package-reference"
 ---
 
-# @deepseek-ai/dsh-document-render
+# @deepseek-ai/dsh-document-convert
 
 English | [中文](README.zh.md)
 
@@ -25,7 +25,7 @@ Convert authorized `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, and `.pptx` bytes t
 <a id="use-this-package"></a>
 ## Use this package
 
-Mount the [LibreOffice provider](../document-render-libreoffice/README.md) to supply `ctx.documentRender`. The abstract service has no mountable implementation or configuration. Callers authorize and stat their source before passing its identity, version, optional byte size, deferred bounded read, Office extension, and scheduling priority to `render()`. The provider admits metadata before calling the read callback. A changed source version rejects conversion.
+Mount the [LibreOffice provider](../document-convert-libreoffice/README.md) to supply `ctx.documentConvert`. The abstract service has no mountable implementation or configuration. Callers authorize and stat their source before passing its identity, version, optional byte size, deferred bounded read, Office extension, and scheduling priority to `convert()`. The provider admits metadata before calling the read callback. A changed source version rejects conversion.
 
 -----
 
@@ -35,7 +35,7 @@ Mount the [LibreOffice provider](../document-render-libreoffice/README.md) to su
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-Providers return independent caller-owned PDF bytes, a conversion cache key, and their rendering generation. Consumers check `generation` before reusing cached PDFs; configuration replacement creates a new generation. Individual reader cancellation rejects promptly; provider disposal waits for actual reads, conversion, and scratch cleanup. Cancellation rejects with its reason; classified conversion failures use `DocumentRenderError`. Providers and consumers share this package as a peer dependency so `instanceof DocumentRenderError` recognizes their failures. No runtime invariant companion is published because this service declares operations without an independent retained observation.
+Providers return independent caller-owned PDF bytes, a conversion cache key, and their rendering generation. Consumers check `generation` before reusing cached PDFs; configuration replacement creates a new generation. Individual reader cancellation rejects promptly; provider disposal waits for actual reads, conversion, and scratch cleanup. Cancellation rejects with its reason; classified conversion failures use `DocumentConvertError`. Providers and consumers share this package as a peer dependency so `instanceof DocumentConvertError` recognizes their failures. No runtime invariant companion is published because this service declares operations without an independent retained observation.
 
 </details>
 
@@ -44,7 +44,7 @@ Providers return independent caller-owned PDF bytes, a conversion cache key, and
 <a id="further-exploration"></a>
 ## Further Exploration
 
-- [Document rendering](../../../docs/subsystems/document-render.md) — composition and ownership.
+- [Document conversion](../../../docs/subsystems/document-convert.md) — composition and ownership.
 - [Workspace Files](../../api/workspace-files/README.md) — Session file authorization and bounded reads.
 
 -----

+ 4 - 4
packages/document/document-render/README.zh.md → packages/document/document-convert/README.zh.md

@@ -3,7 +3,7 @@ description: "宿主转换提供方使用的已授权 Office 输入与完整 PDF
 kind: "package-reference"
 ---
 
-# @deepseek-ai/dsh-document-render
+# @deepseek-ai/dsh-document-convert
 
 [English](README.md) | 中文
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用此包
 
-挂载 [LibreOffice 提供方](../document-render-libreoffice/README.zh.md)以提供 `ctx.documentRender`。抽象服务没有可挂载实现或配置。调用方须先授权并 stat 源文件,再向 `render()` 传入标识、版本、可选字节数、延迟的有界读取、Office 扩展名和调度优先级。提供方在调用读取回调前准入元数据。源版本变化时拒绝转换。
+挂载 [LibreOffice 提供方](../document-convert-libreoffice/README.zh.md)以提供 `ctx.documentConvert`。抽象服务没有可挂载实现或配置。调用方须先授权并 stat 源文件,再向 `convert()` 传入标识、版本、可选字节数、延迟的有界读取、Office 扩展名和调度优先级。提供方在调用读取回调前准入元数据。源版本变化时拒绝转换。
 
 -----
 
@@ -35,7 +35,7 @@ kind: "package-reference"
 <details>
 <summary>实现内部细节 — 点击展开</summary>
 
-提供方返回各调用方独立拥有的 PDF 字节、转换缓存键和渲染 generation。消费者在复用缓存 PDF 前检查 `generation`;替换配置会创建新的 generation。单个读取方取消时立即拒绝;提供方销毁会等待实际读取、转换和临时文件清理。取消以其原因拒绝;已分类的转换失败使用 `DocumentRenderError`。提供方和消费者通过 peer dependency 共享此包,使 `instanceof DocumentRenderError` 能识别双方的失败。此服务仅声明操作,没有独立保留的观测,因此不发布运行时不变量伴随入口。
+提供方返回各调用方独立拥有的 PDF 字节、转换缓存键和转换 generation。消费者在复用缓存 PDF 前检查 `generation`;替换配置会创建新的 generation。单个读取方取消时立即拒绝;提供方销毁会等待实际读取、转换和临时文件清理。取消以其原因拒绝;已分类的转换失败使用 `DocumentConvertError`。提供方和消费者通过 peer dependency 共享此包,使 `instanceof DocumentConvertError` 能识别双方的失败。此服务仅声明操作,没有独立保留的观测,因此不发布运行时不变量伴随入口。
 
 </details>
 
@@ -44,7 +44,7 @@ kind: "package-reference"
 <a id="further-exploration"></a>
 ## 延伸阅读
 
-- [文档渲染](../../../docs/subsystems/document-render.zh.md) — 组合和所有权。
+- [文档转换](../../../docs/subsystems/document-convert.zh.md) — 组合和所有权。
 - [工作区文件](../../api/workspace-files/README.zh.md) — Session 文件授权与有界读取。
 
 -----

+ 2 - 2
packages/document/document-render/package.json → packages/document/document-convert/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "@deepseek-ai/dsh-document-render",
+  "name": "@deepseek-ai/dsh-document-convert",
   "description": "Authorized Office bytes to PDF rendering service",
   "version": "0.1.6-alpha.1",
   "publishConfig": {
@@ -12,7 +12,7 @@
   "repository": {
     "type": "git",
     "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/document/document-render"
+    "directory": "packages/document/document-convert"
   },
   "exports": {
     ".": {

+ 44 - 0
packages/document/document-convert/src/index.ts

@@ -0,0 +1,44 @@
+/** Host document conversion: authorized Office bytes become a complete PDF. */
+import { Context, Service } from '@deepseek-ai/cordis'
+import type { DocumentConvertErrorCode, DocumentConvertRequest, DocumentConvertResult, DocumentConverterGeneration } from './types.ts'
+
+export * from './types.ts'
+
+/** Classified conversion failure; engine details stay in the cause. */
+export class DocumentConvertError extends Error {
+  /**
+   * @param code - category suitable for a conversion consumer.
+   * @param message - diagnostic explaining the failed conversion.
+   * @param options - underlying engine or filesystem failure.
+   */
+  constructor(readonly code: DocumentConvertErrorCode, message: string, options?: ErrorOptions) {
+    super(message, options)
+    this.name = 'DocumentConvertError'
+  }
+}
+
+declare module '@deepseek-ai/cordis' {
+  interface Context {
+    /** One Host converter for already-authorized Office bytes. */
+    documentConvert: DocumentConverter
+  }
+}
+
+/** Load one provider subclass per context; consumers own source authorization. */
+export abstract class DocumentConverter extends Service {
+  constructor(ctx: Context) { super(ctx, 'documentConvert') }
+
+  /** Changes whenever engine, font, or conversion configuration is replaced. */
+  abstract readonly generation: DocumentConverterGeneration
+
+  /**
+   * Convert Office bytes without modifying the source or writing Session events.
+   * @param request - authorized metadata and deferred bounded source read.
+   * @param signal - caller cancellation; provider disposal also stops active work.
+   * @returns caller-owned PDF bytes after conversion and scratch cleanup settle; canceled readers reject independently.
+   * @throws {DocumentConvertError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.
+   */
+  abstract convert(request: DocumentConvertRequest, signal?: AbortSignal): Promise<DocumentConvertResult>
+}
+
+export default DocumentConverter

+ 14 - 14
packages/document/document-render/src/types.ts → packages/document/document-convert/src/types.ts

@@ -14,32 +14,32 @@ export type DocumentSourceKey = Branded<'DocumentSourceKey'>
 export function DocumentSourceKey(key: string): DocumentSourceKey { return brandString<DocumentSourceKey>(key) }
 
 /** One provider lifetime, including its engine, rendering settings, and font configuration. */
-export type DocumentRendererGeneration = Branded<'DocumentRendererGeneration'>
+export type DocumentConverterGeneration = Branded<'DocumentConverterGeneration'>
 /**
  * Label a provider lifetime.
  * @param value - unique generation created by the provider.
- * @returns branded renderer generation.
+ * @returns branded converter generation.
  */
-export function DocumentRendererGeneration(value: string): DocumentRendererGeneration {
-  return brandString<DocumentRendererGeneration>(value)
+export function DocumentConverterGeneration(value: string): DocumentConverterGeneration {
+  return brandString<DocumentConverterGeneration>(value)
 }
 
 /** Provider generation and source-content digest; consumers must not parse it. */
-export type DocumentRenderKey = Branded<'DocumentRenderKey'>
+export type DocumentConvertKey = Branded<'DocumentConvertKey'>
 /**
- * Label a renderer-owned content identity.
+ * Label a converter-owned content identity.
  * @param value - generation and content identity created by the provider.
  * @returns branded conversion identity.
  */
-export function DocumentRenderKey(value: string): DocumentRenderKey { return brandString<DocumentRenderKey>(value) }
+export function DocumentConvertKey(value: string): DocumentConvertKey { return brandString<DocumentConvertKey>(value) }
 
 /** Foreground previews and explicit QA precede speculative background conversion. */
-export type DocumentRenderPriority = 'foreground' | 'background'
+export type DocumentConvertPriority = 'foreground' | 'background'
 
 /** Source authorization and metadata lookup must finish before submitting a request. */
-export interface DocumentRenderRequest {
+export interface DocumentConvertRequest {
   readonly extension: DocumentExtension
-  readonly priority: DocumentRenderPriority
+  readonly priority: DocumentConvertPriority
   readonly source: {
     readonly key: DocumentSourceKey
     readonly version: string
@@ -56,16 +56,16 @@ export interface DocumentRenderRequest {
 }
 
 /** Successful conversion; failed and interrupted conversions reject instead. */
-export interface DocumentRenderResult {
+export interface DocumentConvertResult {
   /** Caller-owned complete PDF, valid after provider disposal. */
   readonly pdf: Uint8Array
   /** Requested OOXML font families unavailable to this conversion; binary Office formats return an empty list. */
   readonly missingFonts: string[]
-  readonly cacheKey: DocumentRenderKey
-  readonly generation: DocumentRendererGeneration
+  readonly cacheKey: DocumentConvertKey
+  readonly generation: DocumentConverterGeneration
 }
 
 /** Failures a conversion consumer can present without exposing engine diagnostics. */
-export type DocumentRenderErrorCode =
+export type DocumentConvertErrorCode =
   | 'input-too-large' | 'output-too-large' | 'invalid-document' | 'unsupported-format'
   | 'invalid-output' | 'timeout' | 'unavailable' | 'failed' | 'busy' | 'source-changed'

+ 0 - 0
packages/document/document-render/tsconfig.json → packages/document/document-convert/tsconfig.json


+ 0 - 44
packages/document/document-render/src/index.ts

@@ -1,44 +0,0 @@
-/** Host document rendering: authorized Office bytes become a complete PDF. */
-import { Context, Service } from '@deepseek-ai/cordis'
-import type { DocumentRenderErrorCode, DocumentRenderRequest, DocumentRenderResult, DocumentRendererGeneration } from './types.ts'
-
-export * from './types.ts'
-
-/** Classified conversion failure; engine details stay in the cause. */
-export class DocumentRenderError extends Error {
-  /**
-   * @param code - category suitable for a conversion consumer.
-   * @param message - diagnostic explaining the failed conversion.
-   * @param options - underlying engine or filesystem failure.
-   */
-  constructor(readonly code: DocumentRenderErrorCode, message: string, options?: ErrorOptions) {
-    super(message, options)
-    this.name = 'DocumentRenderError'
-  }
-}
-
-declare module '@deepseek-ai/cordis' {
-  interface Context {
-    /** One Host renderer for already-authorized Office bytes. */
-    documentRender: DocumentRenderer
-  }
-}
-
-/** Load one provider subclass per context; consumers own source authorization. */
-export abstract class DocumentRenderer extends Service {
-  constructor(ctx: Context) { super(ctx, 'documentRender') }
-
-  /** Changes whenever engine, font, or rendering configuration is replaced. */
-  abstract readonly generation: DocumentRendererGeneration
-
-  /**
-   * Convert Office bytes without modifying the source or writing Session events.
-   * @param request - authorized metadata and deferred bounded source read.
-   * @param signal - caller cancellation; provider disposal also stops active work.
-   * @returns caller-owned PDF bytes after conversion and scratch cleanup settle; canceled readers reject independently.
-   * @throws {DocumentRenderError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.
-   */
-  abstract render(request: DocumentRenderRequest, signal?: AbortSignal): Promise<DocumentRenderResult>
-}
-
-export default DocumentRenderer

+ 1 - 1
packages/experimental/webworker-runtime/src/node/external_packages/libreoffice-kit.ts

@@ -3,7 +3,7 @@ import { notAvailableError } from '../notImplementedFail.ts'
 
 /**
  * Refuse converter creation with the kit's unavailable error code.
- * @returns A rejected promise consumed by the Host document-render provider.
+ * @returns A rejected promise consumed by the Host document-convert provider.
  */
 export function createConverter(): Promise<never> {
   return Promise.reject(Object.assign(notAvailableError('@deepseek-ai/libreoffice-kit', 'createConverter'), {

+ 17 - 17
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -875,21 +875,21 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
     ],
   },
   {
-    key: 'documentRender',
+    key: 'documentConvert',
     summary: 'Load one provider subclass per context; consumers own source authorization.',
     description: 'Load one provider subclass per context; consumers own source authorization.',
     methods: [
       {
-        signature: 'abstract readonly generation: DocumentRendererGeneration',
-        description: 'Changes whenever engine, font, or rendering configuration is replaced.',
+        signature: 'abstract readonly generation: DocumentConverterGeneration',
+        description: 'Changes whenever engine, font, or conversion configuration is replaced.',
         parameters: [],
       },
       {
-        signature: 'abstract render(request: DocumentRenderRequest, signal?: AbortSignal): Promise<DocumentRenderResult>',
+        signature: 'abstract convert(request: DocumentConvertRequest, signal?: AbortSignal): Promise<DocumentConvertResult>',
         description: 'Convert Office bytes without modifying the source or writing Session events.',
         parameters: [{ name: 'request', description: 'authorized metadata and deferred bounded source read.' }, { name: 'signal', description: 'caller cancellation; provider disposal also stops active work.' }],
         returns: 'caller-owned PDF bytes after conversion and scratch cleanup settle; canceled readers reject independently.',
-        throws: ['{DocumentRenderError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.'],
+        throws: ['{DocumentConvertError} Invalid input, unusable output, or engine failure; cancellation rejects with its reason.'],
       },
     ],
   },
@@ -4443,28 +4443,28 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     declaration: 'export interface DirectoryRegistrationHandle {\n    (): void;\n    replace(entries: readonly LlmConfigurableProvider[]): void;\n}',
   },
   {
-    name: 'DocumentExtension',
-    declaration: 'export type DocumentExtension = \'doc\' | \'docx\' | \'xls\' | \'xlsx\' | \'ppt\' | \'pptx\';',
+    name: 'DocumentConverterGeneration',
+    declaration: 'export type DocumentConverterGeneration = Branded<\'DocumentConverterGeneration\'>;',
   },
   {
-    name: 'DocumentRendererGeneration',
-    declaration: 'export type DocumentRendererGeneration = Branded<\'DocumentRendererGeneration\'>;',
+    name: 'DocumentConvertKey',
+    declaration: 'export type DocumentConvertKey = Branded<\'DocumentConvertKey\'>;',
   },
   {
-    name: 'DocumentRenderKey',
-    declaration: 'export type DocumentRenderKey = Branded<\'DocumentRenderKey\'>;',
+    name: 'DocumentConvertPriority',
+    declaration: 'export type DocumentConvertPriority = \'foreground\' | \'background\';',
   },
   {
-    name: 'DocumentRenderPriority',
-    declaration: 'export type DocumentRenderPriority = \'foreground\' | \'background\';',
+    name: 'DocumentConvertRequest',
+    declaration: 'export interface DocumentConvertRequest {\n    readonly extension: DocumentExtension;\n    readonly priority: DocumentConvertPriority;\n    readonly source: {\n        readonly key: DocumentSourceKey;\n        readonly version: string;\n        readonly bytes?: number;\n        read(signal: AbortSignal, maxBytes: number): Promise<{\n            readonly bytes: Uint8Array;\n            readonly version: string;\n        }>;\n    };\n}',
   },
   {
-    name: 'DocumentRenderRequest',
-    declaration: 'export interface DocumentRenderRequest {\n    readonly extension: DocumentExtension;\n    readonly priority: DocumentRenderPriority;\n    readonly source: {\n        readonly key: DocumentSourceKey;\n        readonly version: string;\n        readonly bytes?: number;\n        read(signal: AbortSignal, maxBytes: number): Promise<{\n            readonly bytes: Uint8Array;\n            readonly version: string;\n        }>;\n    };\n}',
+    name: 'DocumentConvertResult',
+    declaration: 'export interface DocumentConvertResult {\n    readonly pdf: Uint8Array;\n    readonly missingFonts: string[];\n    readonly cacheKey: DocumentConvertKey;\n    readonly generation: DocumentConverterGeneration;\n}',
   },
   {
-    name: 'DocumentRenderResult',
-    declaration: 'export interface DocumentRenderResult {\n    readonly pdf: Uint8Array;\n    readonly missingFonts: string[];\n    readonly cacheKey: DocumentRenderKey;\n    readonly generation: DocumentRendererGeneration;\n}',
+    name: 'DocumentExtension',
+    declaration: 'export type DocumentExtension = \'doc\' | \'docx\' | \'xls\' | \'xlsx\' | \'ppt\' | \'pptx\';',
   },
   {
     name: 'DocumentSourceKey',

+ 8 - 8
pnpm-lock.yaml

@@ -2199,12 +2199,12 @@ importers:
       '@deepseek-ai/dsh-cordis-host-runner':
         specifier: workspace:^
         version: link:../../extensions/cordis-host-runner
-      '@deepseek-ai/dsh-document-render':
+      '@deepseek-ai/dsh-document-convert':
         specifier: workspace:^
-        version: link:../../document/document-render
-      '@deepseek-ai/dsh-document-render-libreoffice':
+        version: link:../../document/document-convert
+      '@deepseek-ai/dsh-document-convert-libreoffice':
         specifier: workspace:^
-        version: link:../../document/document-render-libreoffice
+        version: link:../../document/document-convert-libreoffice
       '@deepseek-ai/dsh-file-reference':
         specifier: workspace:^
         version: link:../../context/file-reference
@@ -5805,7 +5805,7 @@ importers:
         specifier: workspace:^
         version: link:../../util/launch-environment
 
-  packages/document/document-render:
+  packages/document/document-convert:
     dependencies:
       '@deepseek-ai/dsh-brand':
         specifier: workspace:^
@@ -5815,7 +5815,7 @@ importers:
         specifier: workspace:^
         version: link:../../../vendor/cordis
 
-  packages/document/document-render-libreoffice:
+  packages/document/document-convert-libreoffice:
     dependencies:
       '@deepseek-ai/libreoffice-kit':
         specifier: 0.0.1
@@ -5833,9 +5833,9 @@ importers:
       '@deepseek-ai/cordis-plugin-loader':
         specifier: workspace:^
         version: link:../../../vendor/loader
-      '@deepseek-ai/dsh-document-render':
+      '@deepseek-ai/dsh-document-convert':
         specifier: workspace:^
-        version: link:../document-render
+        version: link:../document-convert
 
   packages/experimental/agent-team:
     dependencies:

+ 5 - 5
scripts/gen-cordis-catalog.ts

@@ -63,7 +63,7 @@ export const SERVICE_PAGE: Record<string, string> = {
   agentPresets: 'core.md',
   agents: 'core.md',
   approval: 'approval.md',
-  documentRender: 'document-render.md',
+  documentConvert: 'document-convert.md',
   attachments: 'attachment.md',
   shell: 'shell.md',
   shellEnv: 'shell.md',
@@ -274,10 +274,10 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
   PluginInstallProgress: 'boot.md',
   BrowserUseProviderName: 'browser-use.md',
   ComputerUseProviderName: 'computer-use.md',
-  DocumentRenderRequest: 'document-render.md',
-  DocumentRenderResult: 'document-render.md',
-  DocumentRenderPriority: 'document-render.md',
-  DocumentRendererGeneration: 'document-render.md',
+  DocumentConvertRequest: 'document-convert.md',
+  DocumentConvertResult: 'document-convert.md',
+  DocumentConvertPriority: 'document-convert.md',
+  DocumentConverterGeneration: 'document-convert.md',
   Agent: 'core.md',
   AgentCancelCause: 'core.md',
   AgentFactory: 'core.md',

+ 2 - 2
scripts/gen-doc-graphs.ts

@@ -151,8 +151,8 @@ const SERVICE_ROLES: ServiceRole[] = [
     note: 'One provider-owned name per service instance. Each provider also owns its model tools; the service has no common action API, runtime selection, or Session workflow lock.',
   },
   {
-    key: 'documentRender', pkg: 'document-render', title: 'Office to PDF rendering',
-    mode: 'seam', implementations: ['document-render-libreoffice'], consumers: [],
+    key: 'documentConvert', pkg: 'document-convert', title: 'Office to PDF conversion',
+    mode: 'seam', implementations: ['document-convert-libreoffice'], consumers: [],
     note: 'Authorized Office bytes are converted on the Host using native engines on macOS/Windows and Node WASM on Linux.',
   },
   {

+ 2 - 2
scripts/verify-package-readme-model-experience.ts

@@ -53,8 +53,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
   'packages/browser-use/browser-use': { kind: 'none', reason: 'The registry only reserves a provider name; providers own browser tools and Session resources.' },
   'packages/experimental/browser-use-runtime': { kind: 'indirect', reason: 'Browser providers call the library to own resources and expose upstream MCP tools.' },
   'packages/computer-use/computer-use': { kind: 'none', reason: 'The registry only reserves provider names; providers own all model-facing tools and guidance.' },
-  'packages/document/document-render': { kind: 'none', reason: 'Conversion callers own model tools and messages.' },
-  'packages/document/document-render-libreoffice': { kind: 'none', reason: 'The provider returns PDF bytes without constructing model input.' },
+  'packages/document/document-convert': { kind: 'none', reason: 'Conversion callers own model tools and messages.' },
+  'packages/document/document-convert-libreoffice': { kind: 'none', reason: 'The provider returns PDF bytes without constructing model input.' },
   'packages/attachment/attachment': { kind: 'indirect', reason: 'The storage seam delegates model request rendering to provider adapters.' },
   'packages/attachment/attachment-local': { kind: 'indirect', reason: 'The local backend delegates model request rendering to provider adapters.' },
   'packages/shell/shell': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },

+ 3 - 2
tsconfig.base.json

@@ -37,6 +37,7 @@
       "@deepseek-ai/dsh-experimental-browser-use-stagehand-native": ["./packages/experimental/browser-use-stagehand-native/src/index.ts"],
       "@deepseek-ai/dsh-experimental-browser-use-runtime/mcp": ["./packages/experimental/browser-use-runtime/src/mcp.ts"],
 
+      "@deepseek-ai/dsh-document-convert/types": ["./packages/document/document-convert/src/types.ts"],
       "@deepseek-ai/dsh-api-terminal-controller": ["./packages/api/terminal-controller/src/index.ts"],
       "@deepseek-ai/dsh-api-terminal-controller/types": ["./packages/api/terminal-controller/src/types.ts"],
       "@deepseek-ai/dsh-api-terminal-controller/client": ["./packages/api/terminal-controller/src/client/index.ts"],
@@ -324,8 +325,8 @@
       "@deepseek-ai/dsh-credentials-local": ["./packages/credentials/credentials-local/src"],
       "@deepseek-ai/dsh-deepseek-llm-api-extensions": ["./packages/llm/deepseek-llm-api-extensions/src"],
       "@deepseek-ai/dsh-deque": ["./packages/util/deque/src"],
-      "@deepseek-ai/dsh-document-render": ["./packages/document/document-render/src"],
-      "@deepseek-ai/dsh-document-render-libreoffice": ["./packages/document/document-render-libreoffice/src"],
+      "@deepseek-ai/dsh-document-convert": ["./packages/document/document-convert/src"],
+      "@deepseek-ai/dsh-document-convert-libreoffice": ["./packages/document/document-convert-libreoffice/src"],
       "@deepseek-ai/dsh-file-reference": ["./packages/context/file-reference/src"],
       "@deepseek-ai/dsh-file-reference-local": ["./packages/context/file-reference-local/src"],
       "@deepseek-ai/dsh-fs": ["./packages/fs/fs/src"],

+ 2 - 2
tsconfig.host.json

@@ -158,8 +158,8 @@
     { "path": "./packages/computer-use/computer-use" },
     { "path": "./packages/experimental/computer-use-cua-driver-mcp" },
     { "path": "./packages/experimental/computer-use-cua-driver-native" },
-    { "path": "./packages/document/document-render" },
-    { "path": "./packages/document/document-render-libreoffice" },
+    { "path": "./packages/document/document-convert" },
+    { "path": "./packages/document/document-convert-libreoffice" },
     { "path": "./packages/client/ui-deliverables/tsconfig.host.json" },
     { "path": "./vendor/cosmokit" },
     { "path": "./vendor/schemastery" },