dsh-plugin-dev-kb 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.en.md +6 -6
- package/README.md +6 -6
- package/kb/INDEX.md +4 -2
- package/kb/README.md +4 -4
- package/kb/extra/AGENTS.md +1 -1
- package/kb/extra/cookbook/adding-a-remote-api.md +197 -0
- package/kb/extra/cookbook/adding-a-remote-api.zh.md +197 -0
- package/kb/extra/deepseek-llm-api-wire-extensions.md +18 -14
- package/kb/extra/deepseek-llm-api-wire-extensions.zh.md +18 -14
- package/kb/extra/event-producer-consumer.md +44 -43
- package/kb/extra/event-producer-consumer.zh.md +47 -46
- package/kb/extra/i18n/style-samples.md +2 -2
- package/kb/extra/module-graph.md +444 -991
- package/kb/extra/module-graph.zh.md +446 -993
- package/kb/extra/subsystems/agent-team.md +4 -3
- package/kb/extra/subsystems/agent-team.zh.md +4 -3
- package/kb/extra/subsystems/attachment.md +157 -4
- package/kb/extra/subsystems/attachment.zh.md +157 -4
- package/kb/extra/subsystems/feedback.md +2 -2
- package/kb/extra/subsystems/feedback.zh.md +2 -2
- package/kb/extra/testing.md +6 -2
- package/kb/extra/testing.zh.md +6 -2
- package/kb/extra/web-styling.md +4 -0
- package/kb/extra/web-styling.zh.md +4 -0
- package/kb/meta/search-index.json +135 -111
- package/kb/meta/site-pages.txt +184 -182
- package/kb/meta/source.json +5 -5
- package/kb/meta/topics.md +4 -4
- package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +3 -2
- package/kb/site/develop/practice/llm-adapter.md +4 -3
- package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +3 -2
- package/kb/site/en/develop/practice/llm-adapter.md +4 -3
- package/kb/site/en/guide/network-proxy.md +87 -0
- package/kb/site/en/guide/providers.md +70 -17
- package/kb/site/en/guide/schedule.md +2 -0
- package/kb/site/en/reference/agent-lifecycle.md +5 -3
- package/kb/site/en/reference/api-gateway.md +2 -2
- package/kb/site/en/reference/capability-seams.md +14 -7
- package/kb/site/en/reference/config-catalog.md +157 -230
- package/kb/site/en/reference/cookbook/adding-a-package.md +1 -2
- package/kb/site/en/reference/cookbook/adding-a-settings-card.md +10 -8
- package/kb/site/en/reference/cookbook/extension-cookbook.md +15 -12
- package/kb/site/en/reference/index.md +11 -4
- package/kb/site/en/reference/persistence-catalog.md +91 -70
- package/kb/site/en/reference/subsystems/code-runtime.md +7 -3
- package/kb/site/en/reference/subsystems/commands.md +25 -16
- package/kb/site/en/reference/subsystems/compaction.md +9 -9
- package/kb/site/en/reference/subsystems/conversation.md +11 -4
- package/kb/site/en/reference/subsystems/core.md +96 -15
- package/kb/site/en/reference/subsystems/credentials.md +5 -4
- package/kb/site/en/reference/subsystems/goal.md +1 -1
- package/kb/site/en/reference/subsystems/index.md +1 -1
- package/kb/site/en/reference/subsystems/llm-streaming.md +24 -3
- package/kb/site/en/reference/subsystems/permission-presets.md +5 -5
- package/kb/site/en/reference/subsystems/persistence.md +185 -194
- package/kb/site/en/reference/subsystems/plan.md +3 -3
- package/kb/site/en/reference/subsystems/sandbox.md +2 -0
- package/kb/site/en/reference/subsystems/schedule.md +9 -3
- package/kb/site/en/reference/subsystems/session-projection.md +50 -24
- package/kb/site/en/reference/subsystems/session-query.md +19 -13
- package/kb/site/en/reference/subsystems/session-reference.md +25 -1
- package/kb/site/en/reference/subsystems/session-telemetry.md +6 -5
- package/kb/site/en/reference/subsystems/session-title.md +6 -6
- package/kb/site/en/reference/subsystems/session.md +156 -73
- package/kb/site/en/reference/subsystems/settings.md +30 -12
- package/kb/site/en/reference/subsystems/skills.md +1 -1
- package/kb/site/en/reference/subsystems/slots.md +1 -0
- package/kb/site/en/reference/subsystems/storage.md +28 -5
- package/kb/site/en/reference/subsystems/subagent.md +43 -97
- package/kb/site/en/reference/subsystems/system-prompt.md +16 -3
- package/kb/site/en/reference/subsystems/token-meter.md +2 -2
- package/kb/site/en/reference/subsystems/typert.md +18 -18
- package/kb/site/en/reference/subsystems/web-client.md +1 -1
- package/kb/site/en/reference/subsystems/workspace.md +8 -8
- package/kb/site/en/reference/tool-catalog.md +10 -64
- package/kb/site/guide/network-proxy.md +87 -0
- package/kb/site/guide/providers.md +70 -17
- package/kb/site/guide/schedule.md +2 -0
- package/kb/site/reference/agent-lifecycle.md +5 -3
- package/kb/site/reference/api-gateway.md +2 -2
- package/kb/site/reference/capability-seams.md +14 -7
- package/kb/site/reference/config-catalog.md +156 -229
- package/kb/site/reference/cookbook/adding-a-package.md +1 -2
- package/kb/site/reference/cookbook/adding-a-settings-card.md +10 -8
- package/kb/site/reference/cookbook/extension-cookbook.md +15 -12
- package/kb/site/reference/index.md +11 -4
- package/kb/site/reference/persistence-catalog.md +91 -70
- package/kb/site/reference/subsystems/code-runtime.md +7 -3
- package/kb/site/reference/subsystems/commands.md +25 -16
- package/kb/site/reference/subsystems/compaction.md +9 -9
- package/kb/site/reference/subsystems/conversation.md +11 -4
- package/kb/site/reference/subsystems/core.md +96 -15
- package/kb/site/reference/subsystems/credentials.md +5 -4
- package/kb/site/reference/subsystems/goal.md +1 -1
- package/kb/site/reference/subsystems/index.md +1 -1
- package/kb/site/reference/subsystems/llm-streaming.md +24 -3
- package/kb/site/reference/subsystems/permission-presets.md +5 -5
- package/kb/site/reference/subsystems/persistence.md +184 -193
- package/kb/site/reference/subsystems/plan.md +3 -3
- package/kb/site/reference/subsystems/schedule.md +9 -3
- package/kb/site/reference/subsystems/session-projection.md +50 -24
- package/kb/site/reference/subsystems/session-query.md +19 -13
- package/kb/site/reference/subsystems/session-reference.md +25 -1
- package/kb/site/reference/subsystems/session-telemetry.md +6 -5
- package/kb/site/reference/subsystems/session-title.md +6 -6
- package/kb/site/reference/subsystems/session.md +156 -73
- package/kb/site/reference/subsystems/settings.md +30 -12
- package/kb/site/reference/subsystems/skills.md +1 -1
- package/kb/site/reference/subsystems/slots.md +1 -0
- package/kb/site/reference/subsystems/storage.md +28 -5
- package/kb/site/reference/subsystems/subagent.md +43 -97
- package/kb/site/reference/subsystems/system-prompt.md +16 -3
- package/kb/site/reference/subsystems/token-meter.md +2 -2
- package/kb/site/reference/subsystems/typert.md +18 -18
- package/kb/site/reference/subsystems/web-client.md +1 -1
- package/kb/site/reference/subsystems/workspace.md +8 -8
- package/kb/site/reference/tool-catalog.md +9 -63
- package/package.json +2 -2
- package/skills/dsh-plugin-dev-kb.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
- 知识库同步至上游 master `d347e703`(2026-09-05,对应 dsh `0.1.2-rc.1`):站点镜像 182 → 184 页
|
|
6
|
+
(中英 92/92),89 页内容更新(config-catalog / tool-catalog / api-gateway / 全部 46 个子系统页 /
|
|
7
|
+
cookbook 3 篇 / guide providers+schedule / cordis-tutorial 07 / llm-adapter 等);
|
|
8
|
+
新增 `guide/network-proxy`(代理后运行 DSH,中英一对)。
|
|
9
|
+
- 补充文档 56 → 58 篇:新增 `cookbook/adding-a-remote-api`(ctx.remote 端点五步操作,中英一对)。
|
|
10
|
+
- 更新 `kb/meta/source.json` 与 `site-pages.txt`;重建检索索引(241 → 245 文件)与 INDEX.md;
|
|
11
|
+
`topics.md` 新增 network-proxy 入口与 adding-a-remote-api 扩展条目。
|
|
12
|
+
|
|
3
13
|
## [1.0.9] - 2026-08-30
|
|
4
14
|
|
|
5
15
|
- 知识库同步至上游 master `cd5ef814`(2026-08-30):站点镜像 168 → 182 页(中英 91/91),
|
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
> **
|
|
9
|
+
> **184-page official mirror · 58 supplementary docs · 245-file search index · 0 runtime dependencies**
|
|
10
10
|
|
|
11
11
|
**The official docs at your side whenever you write dsh plugins**: the entire content of the DeepSeek Harness official documentation site
|
|
12
12
|
<https://deepseek-harness.github.io/deepseek-harness/> is organized into a form natively usable by dsh —
|
|
@@ -15,16 +15,16 @@ once this plugin is installed, the agent automatically loads the knowledge base
|
|
|
15
15
|
## What is this
|
|
16
16
|
|
|
17
17
|
- **Complete site mirror**: the official docs (VitePress) are generated as a projection of the raw Markdown in
|
|
18
|
-
`docs/` of the `deepseek-ai/deepseek-harness` repository (links rewritten to site routes, verbatim identical to the live site).
|
|
19
|
-
- **Supplementary docs**:
|
|
18
|
+
`docs/` of the `deepseek-ai/deepseek-harness` repository (links rewritten to site routes, verbatim identical to the live site). 184 pages in both Chinese and English.
|
|
19
|
+
- **Supplementary docs**: 58 development references from the repo's `docs/` that were not published to the site (glossary, defensive patterns, module graph, testing strategy, incident postmortems, i18n specs, etc.).
|
|
20
20
|
- **Agent-friendly**: the `skills/dsh-plugin-dev-kb.md` skill lets dsh auto-load it during plugin development tasks, providing the knowledge base location,
|
|
21
21
|
topic navigation, and search strategy; `kb/meta/topics.md` maps task scenarios to the files to read; `kb/meta/search-index.json` provides full search coverage.
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
25
|
- **Complete mirror of the official docs**: the entire content of the deepseek-ai/deepseek-harness official documentation site, organized into a form natively usable by dsh,
|
|
26
|
-
|
|
27
|
-
- **Repo supplementary docs**:
|
|
26
|
+
184 pages in both Chinese and English, links rewritten to site routes, verbatim identical to the live site;
|
|
27
|
+
- **Repo supplementary docs**: 58 development references not published to the site (glossary, defensive patterns, module graph, testing strategy, incident postmortems, i18n specs, etc.);
|
|
28
28
|
- **Agent-friendly**: the `dsh-plugin-dev-kb` skill lets dsh auto-load the knowledge base during plugin development tasks;
|
|
29
29
|
`kb/meta/topics.md` maps task scenarios to the files to read, and `kb/meta/search-index.json` provides full search coverage;
|
|
30
30
|
- **Human-usable**: browse the `kb/` directory directly, or open `kb/INDEX.md` to look things up by URL mapping;
|
|
@@ -90,7 +90,7 @@ dsh-plugin-dev-kb/
|
|
|
90
90
|
│ ├── extra/ repo supplementary docs: glossary, defensive-patterns, module-graph, postmortem/, i18n/ …
|
|
91
91
|
│ ├── meta/
|
|
92
92
|
│ │ ├── topics.md ★ topic navigation: task scenario → files
|
|
93
|
-
│ │ ├── search-index.json full index (
|
|
93
|
+
│ │ ├── search-index.json full index (245 files)
|
|
94
94
|
│ │ ├── source.json source commit / time / stats
|
|
95
95
|
│ │ └── site-pages.txt list of live site pages
|
|
96
96
|
│ ├── INDEX.md site URL ↔ local file mapping
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[**中文**](./README.md) | [English](./README.en.md)
|
|
8
8
|
|
|
9
|
-
> **
|
|
9
|
+
> **184 页官方镜像 · 58 篇补充文档 · 245 文件搜索索引 · 0 运行时依赖**
|
|
10
10
|
|
|
11
11
|
**写 dsh 插件时的随身官方文档**:把 DeepSeek Harness 官方文档站点
|
|
12
12
|
<https://deepseek-harness.github.io/deepseek-harness/> 的全部内容整理为 dsh 原生可用的形态——
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
## 这是什么
|
|
16
16
|
|
|
17
17
|
- **站点完整镜像**:官方文档(VitePress)由仓库 `deepseek-ai/deepseek-harness` 的 `docs/` 原始 Markdown
|
|
18
|
-
投影生成(链接已按站点路由重写,与线上逐字一致)。中英双语共
|
|
19
|
-
- **补充文档**:仓库 `docs/` 中未发布到站点的
|
|
18
|
+
投影生成(链接已按站点路由重写,与线上逐字一致)。中英双语共 184 页。
|
|
19
|
+
- **补充文档**:仓库 `docs/` 中未发布到站点的 58 篇开发参考(术语表、防御模式、模块图、测试策略、事故复盘、i18n 规范等)。
|
|
20
20
|
- **agent 友好**:`skills/dsh-plugin-dev-kb.md` 技能让 dsh 在插件开发任务中自动加载,获知知识库位置、
|
|
21
21
|
主题导航与检索策略;`kb/meta/topics.md` 按任务场景映射要读的文件;`kb/meta/search-index.json` 提供全量检索。
|
|
22
22
|
|
|
23
23
|
## 功能
|
|
24
24
|
|
|
25
25
|
- **官方文档完整镜像**:deepseek-ai/deepseek-harness 官方文档站全部内容整理为 dsh 原生可用的形态,
|
|
26
|
-
中英双语共
|
|
27
|
-
- **仓库补充文档**:未发布到站点的
|
|
26
|
+
中英双语共 184 页,链接按站点路由重写,与线上逐字一致;
|
|
27
|
+
- **仓库补充文档**:未发布到站点的 58 篇开发参考(术语表、防御模式、模块图、测试策略、事故复盘、i18n 规范等);
|
|
28
28
|
- **agent 友好**:`dsh-plugin-dev-kb` 技能让 dsh 在插件开发任务中自动加载知识库;
|
|
29
29
|
`kb/meta/topics.md` 按任务场景映射要读的文件,`kb/meta/search-index.json` 提供全量检索;
|
|
30
30
|
- **人侧可用**:直接浏览 `kb/` 目录,或打开 `kb/INDEX.md` 按 URL 对照查阅;
|
|
@@ -90,7 +90,7 @@ dsh-plugin-dev-kb/
|
|
|
90
90
|
│ ├── extra/ 仓库补充文档:glossary、defensive-patterns、module-graph、postmortem/、i18n/ …
|
|
91
91
|
│ ├── meta/
|
|
92
92
|
│ │ ├── topics.md ★ 主题导航:任务场景 → 文件
|
|
93
|
-
│ │ ├── search-index.json 全量索引(
|
|
93
|
+
│ │ ├── search-index.json 全量索引(245 文件)
|
|
94
94
|
│ │ ├── source.json 来源 commit / 时间 / 统计
|
|
95
95
|
│ │ └── site-pages.txt 线上页面清单
|
|
96
96
|
│ ├── INDEX.md 站点 URL ↔ 本地文件 对照
|
package/kb/INDEX.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DeepSeek Harness 文档索引
|
|
2
2
|
|
|
3
|
-
> 由 `scripts/rebuild-index.mjs` 自动生成。共
|
|
3
|
+
> 由 `scripts/rebuild-index.mjs` 自动生成。共 184 个站点页面(中英双语),另有 58 个补充文档在 `extra/`。
|
|
4
4
|
|
|
5
5
|
## 中文站点(根路径)
|
|
6
6
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| --- | --- | --- |
|
|
17
17
|
| 通过 GitHub Webhook 创建评审会话 | [https://deepseek-harness.github.io/deepseek-harness/guide/github-review](https://deepseek-harness.github.io/deepseek-harness/guide/github-review) | `site/guide/github-review.md` |
|
|
18
18
|
| 连接第三方记忆 MCP 服务 | [https://deepseek-harness.github.io/deepseek-harness/guide/mcp-memory](https://deepseek-harness.github.io/deepseek-harness/guide/mcp-memory) | `site/guide/mcp-memory.md` |
|
|
19
|
+
| 在网络代理后面运行 DSH | [https://deepseek-harness.github.io/deepseek-harness/guide/network-proxy](https://deepseek-harness.github.io/deepseek-harness/guide/network-proxy) | `site/guide/network-proxy.md` |
|
|
19
20
|
| 配置模型 | [https://deepseek-harness.github.io/deepseek-harness/guide/providers](https://deepseek-harness.github.io/deepseek-harness/guide/providers) | `site/guide/providers.md` |
|
|
20
21
|
| Python SDK 入门 | [https://deepseek-harness.github.io/deepseek-harness/guide/python-sdk](https://deepseek-harness.github.io/deepseek-harness/guide/python-sdk) | `site/guide/python-sdk.md` |
|
|
21
22
|
| 使用 Web UI | [https://deepseek-harness.github.io/deepseek-harness/guide/quickstart](https://deepseek-harness.github.io/deepseek-harness/guide/quickstart) | `site/guide/quickstart.md` |
|
|
@@ -147,6 +148,7 @@
|
|
|
147
148
|
| LLM adapters | [https://deepseek-harness.github.io/deepseek-harness/en/develop/practice/llm-adapter](https://deepseek-harness.github.io/deepseek-harness/en/develop/practice/llm-adapter) | `site/en/develop/practice/llm-adapter.md` |
|
|
148
149
|
| Create review Sessions from GitHub webhooks | [https://deepseek-harness.github.io/deepseek-harness/en/guide/github-review](https://deepseek-harness.github.io/deepseek-harness/en/guide/github-review) | `site/en/guide/github-review.md` |
|
|
149
150
|
| Connect a third-party memory MCP server | [https://deepseek-harness.github.io/deepseek-harness/en/guide/mcp-memory](https://deepseek-harness.github.io/deepseek-harness/en/guide/mcp-memory) | `site/en/guide/mcp-memory.md` |
|
|
151
|
+
| Run DSH behind a network proxy | [https://deepseek-harness.github.io/deepseek-harness/en/guide/network-proxy](https://deepseek-harness.github.io/deepseek-harness/en/guide/network-proxy) | `site/en/guide/network-proxy.md` |
|
|
150
152
|
| Configure models | [https://deepseek-harness.github.io/deepseek-harness/en/guide/providers](https://deepseek-harness.github.io/deepseek-harness/en/guide/providers) | `site/en/guide/providers.md` |
|
|
151
153
|
| Get started with the Python SDK | [https://deepseek-harness.github.io/deepseek-harness/en/guide/python-sdk](https://deepseek-harness.github.io/deepseek-harness/en/guide/python-sdk) | `site/en/guide/python-sdk.md` |
|
|
152
154
|
| Use the Web UI | [https://deepseek-harness.github.io/deepseek-harness/en/guide/quickstart](https://deepseek-harness.github.io/deepseek-harness/en/guide/quickstart) | `site/en/guide/quickstart.md` |
|
|
@@ -220,5 +222,5 @@
|
|
|
220
222
|
|
|
221
223
|
---
|
|
222
224
|
|
|
223
|
-
*站点页面:
|
|
225
|
+
*站点页面:184(中文 92,英文 92)*
|
|
224
226
|
|
package/kb/README.md
CHANGED
|
@@ -28,7 +28,7 @@ knowledge/deepseek-harness/
|
|
|
28
28
|
│ └── i18n/ 文档双语规范与术语
|
|
29
29
|
└── meta/
|
|
30
30
|
├── source.json 来源信息:仓库 commit、抓取时间、统计
|
|
31
|
-
└── site-pages.txt 线上站点页面清单(由投影结果生成,
|
|
31
|
+
└── site-pages.txt 线上站点页面清单(由投影结果生成,184 页)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## 插件开发学习路径(从 `site/` 开始)
|
|
@@ -76,6 +76,6 @@ node scripts/rebuild-index.mjs
|
|
|
76
76
|
|
|
77
77
|
## 统计
|
|
78
78
|
|
|
79
|
-
- 站点页面:
|
|
80
|
-
- 补充文档:
|
|
81
|
-
- 来源 commit:`
|
|
79
|
+
- 站点页面:184 个 Markdown(中文 92 + 英文 92),4 张图片
|
|
80
|
+
- 补充文档:58 个 Markdown(`extra/`)
|
|
81
|
+
- 来源 commit:`d347e703908d0406b7a7ef80e3a0e594d86b2215`(master)
|
package/kb/extra/AGENTS.md
CHANGED
|
@@ -54,7 +54,7 @@ When the gate goes red:
|
|
|
54
54
|
2. **Condense** content that belongs here but can be shorter.
|
|
55
55
|
3. **Raise** the ceiling only when the words need the space; justify the manifest diff in the PR. A too-low ceiling is a budget bug.
|
|
56
56
|
|
|
57
|
-
Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room. Targets: root `AGENTS.md` ≤ 1,950; `architecture.md` ≤ 2,400; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤
|
|
57
|
+
Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room. Targets: root `AGENTS.md` ≤ 1,950; `architecture.md` ≤ 2,400; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 750 and this file ≤ 1,320; `packages/README.md` ≤ 994; plus `cordis-primer.md` 600, `defensive-patterns.md` 550, `testing.md` 1,300, `examples/AGENTS.md` 310. Review governs unbudgeted tiers.
|
|
58
58
|
|
|
59
59
|
## The slop checklist
|
|
60
60
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Cookbook: adding a Remote API
|
|
2
|
+
|
|
3
|
+
English | [中文](adding-a-remote-api.zh.md)
|
|
4
|
+
|
|
5
|
+
Adding or changing a `ctx.remote` endpoint takes the five steps on this page: declare the method, declare its failures, register it on the package, consume it on the Client, and test it. Decorator semantics, lookup resolution, the generation pipeline, and the `/api` route are the mechanism and belong to the [API Gateway reference](../api-gateway.md); this page gives the action for each step and the conventions it must satisfy. Why the programming interface looks like this is in the [Typert Remote method calls Agent Note](../../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md), and why a failure is one `RemoteError` plus a code table is in the [failure vocabulary Agent Note](../../.agents/notes/implemented/architecture/2026-08-28-ctx-remote-failure-vocabulary.md).
|
|
6
|
+
|
|
7
|
+
## 1. Declare the API
|
|
8
|
+
|
|
9
|
+
The owner is a Host-side Cordis service: extend `TypertRemoteService` so the service key and the wire namespace are bound together, then mark the exposed methods with `@Remote`. Mark the business method itself when its signature already satisfies the wire conventions; write a `remoteExport*` adapter only when the shape has to change (adding `signal`, reordering parameters, exporting another name), and let that adapter call the unrenamed business method. Lookup objects (`Agent`, `Session`) may only occupy top-level parameter positions, and a method that supports cooperative cancellation takes `signal: AbortSignal` as its final parameter.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
14
|
+
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
15
|
+
|
|
16
|
+
/** One stored note as a Client reads it. */
|
|
17
|
+
export interface NoteRow {
|
|
18
|
+
readonly noteId: string
|
|
19
|
+
readonly title: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module '@deepseek-ai/cordis' {
|
|
23
|
+
interface Context {
|
|
24
|
+
notesController: NotesController
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class NotesController extends TypertRemoteService {
|
|
29
|
+
constructor(ctx: Context) {
|
|
30
|
+
super(ctx, 'notesController', { namespace: 'notes' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param agent - lookup parameter the Gateway resolves from its wire identity.
|
|
35
|
+
* @param signal - carrier cancellation, always the final parameter.
|
|
36
|
+
* @returns the notes this Agent's session owns.
|
|
37
|
+
*/
|
|
38
|
+
@Remote('list')
|
|
39
|
+
async remoteExportList(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
|
40
|
+
return await this.list(agent, signal)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The in-process API the adapter above delegates to, unchanged by it. */
|
|
44
|
+
async list(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
|
45
|
+
signal.throwIfAborted()
|
|
46
|
+
return await Promise.resolve([{ noteId: `${agent.id}-1`, title: 'draft' }])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 2. Declare the failures
|
|
52
|
+
|
|
53
|
+
A Remote failure is one class, `RemoteError`: merge the domain codes into `RemoteErrorDetailsMap` through declaration merging and `throw new RemoteError(code, message, details)` at the failure point. Do not build a family of domain error classes, and do not write an exit-mapping function; an exception unrelated to this endpoint is not pre-classified, because the Gateway folds it into `gateway/internal`. Write a `catch` only to classify an arbitrary provider exception as one domain code, and attach the original exception as `cause`.
|
|
54
|
+
|
|
55
|
+
A code reads `<domain>/<reason>`, and its declaration has four placement rules:
|
|
56
|
+
|
|
57
|
+
- One producer only: declare it in the producing package, next to the throw.
|
|
58
|
+
- Several packages produce it: declare it in the lowest domain package both depend on (`session/not-found` in `core/session`, `workspace/not-found` in `dsh-workspace`).
|
|
59
|
+
- The carrier codes `gateway/bad-request`, `gateway/cancelled`, and `gateway/internal` are declared in protocol, and the Gateway infrastructure codes in gateway — use them, never copy them.
|
|
60
|
+
- A local failure that never crosses the wire stays out of the code table; express it with the caller's own type.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
|
64
|
+
|
|
65
|
+
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
66
|
+
interface RemoteErrorDetailsMap {
|
|
67
|
+
/** No stored note carries that id. */
|
|
68
|
+
'note/not-found': { readonly noteId: string }
|
|
69
|
+
/** The store refused an otherwise valid write. */
|
|
70
|
+
'note/rejected': { readonly noteId: string }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare const stored: ReadonlyMap<string, string>
|
|
75
|
+
declare function persist(noteId: string, title: string): Promise<void>
|
|
76
|
+
|
|
77
|
+
export async function rename(noteId: string, title: string): Promise<void> {
|
|
78
|
+
if (!stored.has(noteId)) {
|
|
79
|
+
throw new RemoteError('note/not-found', `no note "${noteId}"`, { noteId })
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await persist(noteId, title)
|
|
83
|
+
} catch (error: unknown) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
85
|
+
throw new RemoteError('note/rejected', message, { noteId }, { cause: error })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 3. Register it on the package
|
|
91
|
+
|
|
92
|
+
`@Remote` must live in a Loader entry plugin package; when the owner is an abstract seam, the controller goes in the matching package under `packages/api/`. The manifest gains the two generated entries and the protocol peer dependency, while on the Client side the `@deepseek-ai/dsh-api-remotes` assembly mounts the contribution and re-exports the type vocabulary that consumers need. Which generated artifact each entry points at, and how the generation pipeline is ordered, are in the [API Gateway reference](../api-gateway.md).
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"exports": {
|
|
97
|
+
"./typert": { "types": "./lib/typert.host.d.ts", "default": "./lib/typert.host.js" },
|
|
98
|
+
"./remote": { "types": "./lib/typert.remote-client.d.ts", "default": "./lib/typert.remote-client.js" }
|
|
99
|
+
},
|
|
100
|
+
"peerDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" },
|
|
101
|
+
"devDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" }
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Rerun `pnpm run build:lib` after changing a signature, the code table, the namespace, or an export name, because that is what hands the Client its new declarations and codecs; changing only an implementation body needs no regeneration.
|
|
106
|
+
|
|
107
|
+
## 4. Consume it on the Client
|
|
108
|
+
|
|
109
|
+
The calling plugin declares both `remote` and `remote.<namespace>` in its `inject`, and the call site writes `ctx.remote.<namespace>.<method>(...)` directly: no `Pick<ClientRemote, …>` narrowing, no hand-written method signature, no wire relay object. The result is a `RemoteResult<T>`, so branch on `if (!result.ok)` in place and discriminate by `code` rather than `instanceof` — a code branch narrows `details` on its own. An exception-flow site writes `throw result.error` (it is a real Error); whoever catches it uses `isRemoteFailure` to tell a Remote failure from a local defect and rethrows the defect. Do not write a defensive catch: a Remote call does not reject, and an assembly mistake should crash.
|
|
110
|
+
|
|
111
|
+
Fixed Host facts come from `ctx.remote.$host`: `home` and `isLoopback` are plain reads with no subscription and no generation counter, and `home` is `undefined` until the first ready frame. Refresh after a reconnect through `ctx.on('connection/reset')` or a domain's own remote event. When the caller aborts a unary call, the outcome is `gateway/cancelled` on the error branch rather than a throw.
|
|
112
|
+
|
|
113
|
+
```ts ignore-check
|
|
114
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
115
|
+
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
|
116
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
117
|
+
|
|
118
|
+
export const inject = ['remote', 'remote.notes']
|
|
119
|
+
|
|
120
|
+
declare const ctx: Context
|
|
121
|
+
|
|
122
|
+
/** Store-side read: the error branch is handled where the code is meaningful. */
|
|
123
|
+
export async function noteTitles(): Promise<readonly string[]> {
|
|
124
|
+
const result = await ctx.remote.notes.list()
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
if (result.error.code === 'note/not-found') return []
|
|
127
|
+
throw result.error
|
|
128
|
+
}
|
|
129
|
+
return result.value.map(row => row.title)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Action-side: a Remote failure becomes copy; a local fault keeps crashing. */
|
|
133
|
+
export async function renderTitles(): Promise<string> {
|
|
134
|
+
try {
|
|
135
|
+
return (await noteTitles()).join(', ')
|
|
136
|
+
} catch (error: unknown) {
|
|
137
|
+
if (!isRemoteFailure(error)) throw error
|
|
138
|
+
return `unavailable (${error.code})`
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fixed Host facts as plain reads. */
|
|
143
|
+
export function hostLabel(): string {
|
|
144
|
+
const { home, isLoopback } = ctx.remote.$host
|
|
145
|
+
return home ?? (isLoopback ? 'local host' : 'remote host')
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 5. Test it
|
|
150
|
+
|
|
151
|
+
On the owner side, assert the code that was thrown: recover the failure with `remoteErrorOf` after catching, then compare `code` and the details fields you care about with `toMatchObject` — never deep-compare the error object with `toEqual`, and never assert `instanceof`.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
|
155
|
+
import { expect, it } from 'vitest'
|
|
156
|
+
|
|
157
|
+
declare function rename(noteId: string, title: string): Promise<void>
|
|
158
|
+
|
|
159
|
+
it('refuses an unknown note before writing', async () => {
|
|
160
|
+
const failure = await rename('n-404', 'fresh title').catch((error: unknown) => error)
|
|
161
|
+
|
|
162
|
+
expect(remoteErrorOf(failure)).toMatchObject({
|
|
163
|
+
code: 'note/not-found',
|
|
164
|
+
details: { noteId: 'n-404' },
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A Client-side double returns real instances: take the `RemoteError` and `TestRemote` value imports from `@deepseek-ai/dsh-client-test-runtime`, because a value import from the `api-remotes` facade would load the unbuilt assembly chain. `TestRemote.$host` is a plain field a spec assigns directly.
|
|
170
|
+
|
|
171
|
+
```ts ignore-check
|
|
172
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
173
|
+
import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
|
174
|
+
import { expect, it } from 'vitest'
|
|
175
|
+
|
|
176
|
+
it('renders the failure code the Host reported', async () => {
|
|
177
|
+
const ctx = new Context()
|
|
178
|
+
const remote = new TestRemote(ctx, {
|
|
179
|
+
notes: {
|
|
180
|
+
list: () => Promise.resolve({
|
|
181
|
+
ok: false as const,
|
|
182
|
+
error: new RemoteError('note/not-found', 'no note "n-404"', { noteId: 'n-404' }),
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
remote.$host = { home: '/home/fixture', isLoopback: true }
|
|
187
|
+
|
|
188
|
+
await expect(ctx.remote.notes.list()).resolves.toMatchObject({ error: { code: 'note/not-found' } })
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Verify
|
|
193
|
+
|
|
194
|
+
1. `pnpm run build:lib`: mandatory once a signature, the code table, the namespace, or an export name changed, because it produces the Client declarations and codecs.
|
|
195
|
+
2. `pnpm run typecheck`: both the Host and the Client program, where a code merged into an unreachable package turns red.
|
|
196
|
+
3. Run both sides' specs by name: `npx vitest run <owner spec> <client spec>`.
|
|
197
|
+
4. Add a recorded-session snapshot when the endpoint reaches a product-visible surface, per the [testing policy](../testing.md).
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# 实操手册:新增一个 Remote API
|
|
2
|
+
|
|
3
|
+
[English](adding-a-remote-api.md) | 中文
|
|
4
|
+
|
|
5
|
+
新增或改动一个 `ctx.remote` 端点按本页五步走:声明方法、声明失败、在包上注册、在 Client 消费、写测试。decorator 语义、lookup 解析、生成管线与 `/api` 路由属于机制,由 [API Gateway 参考](../api-gateway.zh.md)负责;本页给的是每一步的动作与必须遵守的约定。为什么是这套编程面,见 [Typert Remote 方法调用 Agent Note](../../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md);为什么失败面是单个 `RemoteError` 加一张码表,见[失败词汇 Agent Note](../../.agents/notes/implemented/architecture/2026-08-28-ctx-remote-failure-vocabulary.zh.md)。
|
|
6
|
+
|
|
7
|
+
## 1. 声明 API
|
|
8
|
+
|
|
9
|
+
owner 是一个 Host 侧 Cordis 服务:继承 `TypertRemoteService` 把 service 键与 wire namespace 一起绑定,再用 `@Remote` 标注对外暴露的方法。业务方法的签名若已符合 wire 约定就直接标注它本身;只有形态需要调整(补 `signal`、换参数顺序、换导出名)才写一个 `remoteExport*` adapter,由它调用不改名的业务方法。lookup 对象(`Agent`、`Session`)只能占顶层参数位,支持协作式取消的方法把 `signal: AbortSignal` 放在最后一位。
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
14
|
+
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
15
|
+
|
|
16
|
+
/** One stored note as a Client reads it. */
|
|
17
|
+
export interface NoteRow {
|
|
18
|
+
readonly noteId: string
|
|
19
|
+
readonly title: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module '@deepseek-ai/cordis' {
|
|
23
|
+
interface Context {
|
|
24
|
+
notesController: NotesController
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class NotesController extends TypertRemoteService {
|
|
29
|
+
constructor(ctx: Context) {
|
|
30
|
+
super(ctx, 'notesController', { namespace: 'notes' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param agent - lookup parameter the Gateway resolves from its wire identity.
|
|
35
|
+
* @param signal - carrier cancellation, always the final parameter.
|
|
36
|
+
* @returns the notes this Agent's session owns.
|
|
37
|
+
*/
|
|
38
|
+
@Remote('list')
|
|
39
|
+
async remoteExportList(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
|
40
|
+
return await this.list(agent, signal)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The in-process API the adapter above delegates to, unchanged by it. */
|
|
44
|
+
async list(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
|
45
|
+
signal.throwIfAborted()
|
|
46
|
+
return await Promise.resolve([{ noteId: `${agent.id}-1`, title: 'draft' }])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 2. 声明失败
|
|
52
|
+
|
|
53
|
+
Remote 失败只有一个类 `RemoteError`:域码经 declaration merging 进 `RemoteErrorDetailsMap`,失败点直接 `throw new RemoteError(code, message, details)`。不要建域异常类家族,也不要写出口映射函数;与本端点无关的异常不预先归类,Gateway 会兜底折成 `gateway/internal`。只有"把任意 provider 异常归为一个域码"这一种场景才写 `catch`,并把原始异常挂在 `cause` 上。
|
|
54
|
+
|
|
55
|
+
码名是 `<域>/<理由>`,声明落点四条:
|
|
56
|
+
|
|
57
|
+
- 只有一个生产者:声明落生产者包,紧挨抛出点。
|
|
58
|
+
- 多个包共同生产:落双方共同依赖的最低层域包(`session/not-found` 在 `core/session`,`workspace/not-found` 在 `dsh-workspace`)。
|
|
59
|
+
- 载体码 `gateway/bad-request`、`gateway/cancelled`、`gateway/internal` 已在 protocol 声明,Gateway 基础设施码已在 gateway 声明——直接用,不要复制。
|
|
60
|
+
- 不上 wire 的本地失败不进码表,用调用方自己的类型表达。
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
|
64
|
+
|
|
65
|
+
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
66
|
+
interface RemoteErrorDetailsMap {
|
|
67
|
+
/** No stored note carries that id. */
|
|
68
|
+
'note/not-found': { readonly noteId: string }
|
|
69
|
+
/** The store refused an otherwise valid write. */
|
|
70
|
+
'note/rejected': { readonly noteId: string }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare const stored: ReadonlyMap<string, string>
|
|
75
|
+
declare function persist(noteId: string, title: string): Promise<void>
|
|
76
|
+
|
|
77
|
+
export async function rename(noteId: string, title: string): Promise<void> {
|
|
78
|
+
if (!stored.has(noteId)) {
|
|
79
|
+
throw new RemoteError('note/not-found', `no note "${noteId}"`, { noteId })
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await persist(noteId, title)
|
|
83
|
+
} catch (error: unknown) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
85
|
+
throw new RemoteError('note/rejected', message, { noteId }, { cause: error })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 3. 在包上注册
|
|
91
|
+
|
|
92
|
+
`@Remote` 必须落在一个 Loader entry 插件包里;owner 是抽象 seam 时把控制器放进 `packages/api/` 下的对应包。包清单要补两个生成入口与 protocol 的 peer 依赖,Client 侧则由 `@deepseek-ai/dsh-api-remotes` 的 assembly 挂载该贡献并按需转口类型词汇。两个入口分别指向哪个生成产物、生成管线如何排序,见 [API Gateway 参考](../api-gateway.zh.md)。
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"exports": {
|
|
97
|
+
"./typert": { "types": "./lib/typert.host.d.ts", "default": "./lib/typert.host.js" },
|
|
98
|
+
"./remote": { "types": "./lib/typert.remote-client.d.ts", "default": "./lib/typert.remote-client.js" }
|
|
99
|
+
},
|
|
100
|
+
"peerDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" },
|
|
101
|
+
"devDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" }
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
改动了签名、码表、namespace 或导出名之后重跑 `pnpm run build:lib`,Client 才拿得到新的声明与 codec;只改实现体不需要重新生成。
|
|
106
|
+
|
|
107
|
+
## 4. 在 Client 消费
|
|
108
|
+
|
|
109
|
+
调用插件在 `inject` 里同时声明 `remote` 与 `remote.<namespace>`,调用点直写 `ctx.remote.<namespace>.<method>(...)`:不要用 `Pick<ClientRemote, …>` 窄化、不要手写方法签名、不要造 wire 中转对象。结果是 `RemoteResult<T>`,就地 `if (!result.ok)` 分支,判 `code` 而不是 `instanceof`——code 分支会自动窄化 `details`。异常流的站点写 `throw result.error`(它是真 Error);接住它的上层用 `isRemoteFailure` 区分 Remote 失败与本地缺陷,本地缺陷继续往上抛。不要写防御性 catch:Remote 调用不 reject,装配错误就该炸。
|
|
110
|
+
|
|
111
|
+
Host 的固定事实读 `ctx.remote.$host`:`home` 与 `isLoopback` 是普通值读取,没有订阅也没有 generation 计数器,`home` 在第一帧 ready 之前是 `undefined`;重连后的刷新走 `ctx.on('connection/reset')` 或各域自己的 remote 事件。调用方 abort 掉一次一元调用时,结果落在错误分支上的 `gateway/cancelled`,而不是抛出。
|
|
112
|
+
|
|
113
|
+
```ts ignore-check
|
|
114
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
115
|
+
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
|
116
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
117
|
+
|
|
118
|
+
export const inject = ['remote', 'remote.notes']
|
|
119
|
+
|
|
120
|
+
declare const ctx: Context
|
|
121
|
+
|
|
122
|
+
/** Store-side read: the error branch is handled where the code is meaningful. */
|
|
123
|
+
export async function noteTitles(): Promise<readonly string[]> {
|
|
124
|
+
const result = await ctx.remote.notes.list()
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
if (result.error.code === 'note/not-found') return []
|
|
127
|
+
throw result.error
|
|
128
|
+
}
|
|
129
|
+
return result.value.map(row => row.title)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Action-side: a Remote failure becomes copy; a local fault keeps crashing. */
|
|
133
|
+
export async function renderTitles(): Promise<string> {
|
|
134
|
+
try {
|
|
135
|
+
return (await noteTitles()).join(', ')
|
|
136
|
+
} catch (error: unknown) {
|
|
137
|
+
if (!isRemoteFailure(error)) throw error
|
|
138
|
+
return `unavailable (${error.code})`
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fixed Host facts as plain reads. */
|
|
143
|
+
export function hostLabel(): string {
|
|
144
|
+
const { home, isLoopback } = ctx.remote.$host
|
|
145
|
+
return home ?? (isLoopback ? 'local host' : 'remote host')
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 5. 测试
|
|
150
|
+
|
|
151
|
+
owner 侧断言抛出的码:捕获后用 `remoteErrorOf` 取出失败,再用 `toMatchObject` 比对 `code` 与需要的 `details` 字段——不要用 `toEqual` 深比对错误对象,也不要断言 `instanceof`。
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
|
155
|
+
import { expect, it } from 'vitest'
|
|
156
|
+
|
|
157
|
+
declare function rename(noteId: string, title: string): Promise<void>
|
|
158
|
+
|
|
159
|
+
it('refuses an unknown note before writing', async () => {
|
|
160
|
+
const failure = await rename('n-404', 'fresh title').catch((error: unknown) => error)
|
|
161
|
+
|
|
162
|
+
expect(remoteErrorOf(failure)).toMatchObject({
|
|
163
|
+
code: 'note/not-found',
|
|
164
|
+
details: { noteId: 'n-404' },
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Client 侧的替身返回真实例:`RemoteError` 与 `TestRemote` 的值 import 一律取自 `@deepseek-ai/dsh-client-test-runtime`,因为从 `api-remotes` facade 值 import 会拉起尚未构建的装配链。`TestRemote.$host` 是普通字段,spec 直接赋值即可。
|
|
170
|
+
|
|
171
|
+
```ts ignore-check
|
|
172
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
173
|
+
import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
|
174
|
+
import { expect, it } from 'vitest'
|
|
175
|
+
|
|
176
|
+
it('renders the failure code the Host reported', async () => {
|
|
177
|
+
const ctx = new Context()
|
|
178
|
+
const remote = new TestRemote(ctx, {
|
|
179
|
+
notes: {
|
|
180
|
+
list: () => Promise.resolve({
|
|
181
|
+
ok: false as const,
|
|
182
|
+
error: new RemoteError('note/not-found', 'no note "n-404"', { noteId: 'n-404' }),
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
remote.$host = { home: '/home/fixture', isLoopback: true }
|
|
187
|
+
|
|
188
|
+
await expect(ctx.remote.notes.list()).resolves.toMatchObject({ error: { code: 'note/not-found' } })
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 验证
|
|
193
|
+
|
|
194
|
+
1. `pnpm run build:lib`:签名、码表、namespace 或导出名变过就必须重跑,Client 声明与 codec 由它产出。
|
|
195
|
+
2. `pnpm run typecheck`:Host 与 Client 两个 program 都过一遍,码表的 merge 落点错了会在这里红。
|
|
196
|
+
3. 点名跑两侧 spec:`npx vitest run <owner spec> <client spec>`。
|
|
197
|
+
4. 端点属于产品可见面时补一条录制会话快照,规则见[测试策略](../testing.zh.md)。
|