dsh-plugin-dev-kb 1.0.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.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/cordis.patch.yml +12 -0
  4. package/kb/INDEX.md +210 -0
  5. package/kb/README.md +69 -0
  6. package/kb/extra/AGENTS.md +75 -0
  7. package/kb/extra/api-gateway.md +164 -0
  8. package/kb/extra/api-gateway.zh.md +164 -0
  9. package/kb/extra/cookbook/adding-a-vendored-package.md +59 -0
  10. package/kb/extra/cookbook/adding-a-vendored-package.zh.md +59 -0
  11. package/kb/extra/cookbook/maintaining-dsh-code-review.md +64 -0
  12. package/kb/extra/cookbook/maintaining-dsh-code-review.zh.md +64 -0
  13. package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.md +32 -0
  14. package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.zh.md +32 -0
  15. package/kb/extra/defensive-patterns.md +33 -0
  16. package/kb/extra/defensive-patterns.zh.md +33 -0
  17. package/kb/extra/development.md +171 -0
  18. package/kb/extra/development.zh.md +171 -0
  19. package/kb/extra/event-producer-consumer.md +76 -0
  20. package/kb/extra/event-producer-consumer.zh.md +78 -0
  21. package/kb/extra/glossary.md +45 -0
  22. package/kb/extra/glossary.zh.md +45 -0
  23. package/kb/extra/graph-atlas.md +24 -0
  24. package/kb/extra/graph-atlas.zh.md +26 -0
  25. package/kb/extra/i18n/README.md +60 -0
  26. package/kb/extra/i18n/README.zh.md +60 -0
  27. package/kb/extra/i18n/style-samples.md +87 -0
  28. package/kb/extra/i18n/terminology.md +214 -0
  29. package/kb/extra/i18n/translation-prompt.md +263 -0
  30. package/kb/extra/i18n/translation-rules.md +69 -0
  31. package/kb/extra/i18n/translation-rules.zh.md +69 -0
  32. package/kb/extra/module-graph.md +1641 -0
  33. package/kb/extra/module-graph.zh.md +1643 -0
  34. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.md +113 -0
  35. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.zh.md +113 -0
  36. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.md +47 -0
  37. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md +47 -0
  38. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.md +53 -0
  39. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.zh.md +53 -0
  40. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +55 -0
  41. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +55 -0
  42. package/kb/extra/postmortem/README.md +18 -0
  43. package/kb/extra/postmortem/README.zh.md +18 -0
  44. package/kb/extra/rescope.md +53 -0
  45. package/kb/extra/rescope.zh.md +53 -0
  46. package/kb/extra/subsystems/attachment.md +125 -0
  47. package/kb/extra/subsystems/attachment.zh.md +125 -0
  48. package/kb/extra/subsystems/extensions.md +364 -0
  49. package/kb/extra/subsystems/extensions.zh.md +364 -0
  50. package/kb/extra/subsystems/feedback.md +266 -0
  51. package/kb/extra/subsystems/feedback.zh.md +266 -0
  52. package/kb/extra/testing.md +49 -0
  53. package/kb/extra/testing.zh.md +49 -0
  54. package/kb/extra/web-styling.md +25 -0
  55. package/kb/extra/web-styling.zh.md +25 -0
  56. package/kb/meta/search-index.json +1328 -0
  57. package/kb/meta/site-pages.txt +168 -0
  58. package/kb/meta/source.json +13 -0
  59. package/kb/meta/topics.md +75 -0
  60. package/kb/site/develop/basic/config.md +108 -0
  61. package/kb/site/develop/basic/index.md +146 -0
  62. package/kb/site/develop/basic/publish.md +185 -0
  63. package/kb/site/develop/basic/tool.md +54 -0
  64. package/kb/site/develop/cordis-tutorial/01-first-plugin.md +95 -0
  65. package/kb/site/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
  66. package/kb/site/develop/cordis-tutorial/03-services.md +98 -0
  67. package/kb/site/develop/cordis-tutorial/04-events.md +144 -0
  68. package/kb/site/develop/cordis-tutorial/05-config.md +84 -0
  69. package/kb/site/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
  70. package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +107 -0
  71. package/kb/site/develop/cordis-tutorial/index.md +62 -0
  72. package/kb/site/develop/framework/events.md +145 -0
  73. package/kb/site/develop/framework/index.md +139 -0
  74. package/kb/site/develop/framework/service.md +152 -0
  75. package/kb/site/develop/practice/index.md +157 -0
  76. package/kb/site/develop/practice/llm-adapter.md +190 -0
  77. package/kb/site/en/develop/basic/config.md +108 -0
  78. package/kb/site/en/develop/basic/index.md +146 -0
  79. package/kb/site/en/develop/basic/publish.md +185 -0
  80. package/kb/site/en/develop/basic/tool.md +54 -0
  81. package/kb/site/en/develop/cordis-tutorial/01-first-plugin.md +95 -0
  82. package/kb/site/en/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
  83. package/kb/site/en/develop/cordis-tutorial/03-services.md +98 -0
  84. package/kb/site/en/develop/cordis-tutorial/04-events.md +144 -0
  85. package/kb/site/en/develop/cordis-tutorial/05-config.md +84 -0
  86. package/kb/site/en/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
  87. package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +107 -0
  88. package/kb/site/en/develop/cordis-tutorial/index.md +60 -0
  89. package/kb/site/en/develop/framework/events.md +145 -0
  90. package/kb/site/en/develop/framework/index.md +139 -0
  91. package/kb/site/en/develop/framework/service.md +150 -0
  92. package/kb/site/en/develop/practice/index.md +157 -0
  93. package/kb/site/en/develop/practice/llm-adapter.md +190 -0
  94. package/kb/site/en/guide/providers-custom-form.png +0 -0
  95. package/kb/site/en/guide/providers-models-page.png +0 -0
  96. package/kb/site/en/guide/providers.md +100 -0
  97. package/kb/site/en/guide/python-sdk.md +106 -0
  98. package/kb/site/en/guide/quickstart.md +32 -0
  99. package/kb/site/en/index.md +8 -0
  100. package/kb/site/en/reference/agent-lifecycle.md +86 -0
  101. package/kb/site/en/reference/capability-seams.md +475 -0
  102. package/kb/site/en/reference/config-catalog.md +3155 -0
  103. package/kb/site/en/reference/cookbook/adding-a-conversation-node.md +235 -0
  104. package/kb/site/en/reference/cookbook/adding-a-package.md +120 -0
  105. package/kb/site/en/reference/cookbook/adding-a-settings-card.md +102 -0
  106. package/kb/site/en/reference/cookbook/adding-a-tool.md +96 -0
  107. package/kb/site/en/reference/cookbook/adding-an-llm-adapter.md +45 -0
  108. package/kb/site/en/reference/cookbook/extension-cookbook.md +131 -0
  109. package/kb/site/en/reference/cordis-api/context.md +368 -0
  110. package/kb/site/en/reference/cordis-api/events.md +211 -0
  111. package/kb/site/en/reference/cordis-api/fiber.md +379 -0
  112. package/kb/site/en/reference/cordis-api/inherited.md +43 -0
  113. package/kb/site/en/reference/cordis-api/registry.md +156 -0
  114. package/kb/site/en/reference/cordis-api/service.md +106 -0
  115. package/kb/site/en/reference/cordis-primer.md +46 -0
  116. package/kb/site/en/reference/index.md +131 -0
  117. package/kb/site/en/reference/persistence-catalog.md +949 -0
  118. package/kb/site/en/reference/subsystems/approval.md +173 -0
  119. package/kb/site/en/reference/subsystems/client-modules.md +121 -0
  120. package/kb/site/en/reference/subsystems/code-runtime.md +194 -0
  121. package/kb/site/en/reference/subsystems/commands.md +190 -0
  122. package/kb/site/en/reference/subsystems/compaction.md +241 -0
  123. package/kb/site/en/reference/subsystems/core.md +1073 -0
  124. package/kb/site/en/reference/subsystems/credentials.md +136 -0
  125. package/kb/site/en/reference/subsystems/filesystem.md +498 -0
  126. package/kb/site/en/reference/subsystems/goal.md +280 -0
  127. package/kb/site/en/reference/subsystems/index.md +58 -0
  128. package/kb/site/en/reference/subsystems/invariants.md +91 -0
  129. package/kb/site/en/reference/subsystems/jobs.md +293 -0
  130. package/kb/site/en/reference/subsystems/llm-streaming.md +920 -0
  131. package/kb/site/en/reference/subsystems/lsp.md +205 -0
  132. package/kb/site/en/reference/subsystems/permission-presets.md +134 -0
  133. package/kb/site/en/reference/subsystems/persistence.md +388 -0
  134. package/kb/site/en/reference/subsystems/plan.md +90 -0
  135. package/kb/site/en/reference/subsystems/sandbox.md +221 -0
  136. package/kb/site/en/reference/subsystems/schedule.md +189 -0
  137. package/kb/site/en/reference/subsystems/scope.md +62 -0
  138. package/kb/site/en/reference/subsystems/session-projection.md +265 -0
  139. package/kb/site/en/reference/subsystems/session-query.md +498 -0
  140. package/kb/site/en/reference/subsystems/session-reference.md +111 -0
  141. package/kb/site/en/reference/subsystems/session-telemetry.md +197 -0
  142. package/kb/site/en/reference/subsystems/session-title.md +207 -0
  143. package/kb/site/en/reference/subsystems/session.md +852 -0
  144. package/kb/site/en/reference/subsystems/settings.md +313 -0
  145. package/kb/site/en/reference/subsystems/shell.md +306 -0
  146. package/kb/site/en/reference/subsystems/skills.md +334 -0
  147. package/kb/site/en/reference/subsystems/spill.md +120 -0
  148. package/kb/site/en/reference/subsystems/storage.md +232 -0
  149. package/kb/site/en/reference/subsystems/subagent.md +737 -0
  150. package/kb/site/en/reference/subsystems/subprocess.md +327 -0
  151. package/kb/site/en/reference/subsystems/system-prompt.md +210 -0
  152. package/kb/site/en/reference/subsystems/terminal.md +187 -0
  153. package/kb/site/en/reference/subsystems/token-meter.md +93 -0
  154. package/kb/site/en/reference/subsystems/tools.md +723 -0
  155. package/kb/site/en/reference/subsystems/typert.md +339 -0
  156. package/kb/site/en/reference/subsystems/user-questions.md +181 -0
  157. package/kb/site/en/reference/subsystems/web-server.md +111 -0
  158. package/kb/site/en/reference/subsystems/web.md +202 -0
  159. package/kb/site/en/reference/subsystems/workflow.md +281 -0
  160. package/kb/site/en/reference/subsystems/workspace.md +231 -0
  161. package/kb/site/en/reference/tool-catalog.md +1877 -0
  162. package/kb/site/en/reference/tool-execution-pipeline.md +66 -0
  163. package/kb/site/guide/providers-custom-form.zh.png +0 -0
  164. package/kb/site/guide/providers-models-page.zh.png +0 -0
  165. package/kb/site/guide/providers.md +100 -0
  166. package/kb/site/guide/python-sdk.md +106 -0
  167. package/kb/site/guide/quickstart.md +32 -0
  168. package/kb/site/index.md +8 -0
  169. package/kb/site/reference/agent-lifecycle.md +86 -0
  170. package/kb/site/reference/capability-seams.md +475 -0
  171. package/kb/site/reference/config-catalog.md +3154 -0
  172. package/kb/site/reference/cookbook/adding-a-conversation-node.md +235 -0
  173. package/kb/site/reference/cookbook/adding-a-package.md +120 -0
  174. package/kb/site/reference/cookbook/adding-a-settings-card.md +102 -0
  175. package/kb/site/reference/cookbook/adding-a-tool.md +98 -0
  176. package/kb/site/reference/cookbook/adding-an-llm-adapter.md +45 -0
  177. package/kb/site/reference/cookbook/extension-cookbook.md +133 -0
  178. package/kb/site/reference/cordis-api/context.md +368 -0
  179. package/kb/site/reference/cordis-api/events.md +211 -0
  180. package/kb/site/reference/cordis-api/fiber.md +379 -0
  181. package/kb/site/reference/cordis-api/inherited.md +43 -0
  182. package/kb/site/reference/cordis-api/registry.md +156 -0
  183. package/kb/site/reference/cordis-api/service.md +106 -0
  184. package/kb/site/reference/cordis-primer.md +52 -0
  185. package/kb/site/reference/index.md +135 -0
  186. package/kb/site/reference/persistence-catalog.md +949 -0
  187. package/kb/site/reference/subsystems/approval.md +173 -0
  188. package/kb/site/reference/subsystems/client-modules.md +121 -0
  189. package/kb/site/reference/subsystems/code-runtime.md +194 -0
  190. package/kb/site/reference/subsystems/commands.md +190 -0
  191. package/kb/site/reference/subsystems/compaction.md +241 -0
  192. package/kb/site/reference/subsystems/core.md +1081 -0
  193. package/kb/site/reference/subsystems/credentials.md +136 -0
  194. package/kb/site/reference/subsystems/filesystem.md +498 -0
  195. package/kb/site/reference/subsystems/goal.md +280 -0
  196. package/kb/site/reference/subsystems/index.md +58 -0
  197. package/kb/site/reference/subsystems/invariants.md +91 -0
  198. package/kb/site/reference/subsystems/jobs.md +293 -0
  199. package/kb/site/reference/subsystems/llm-streaming.md +926 -0
  200. package/kb/site/reference/subsystems/lsp.md +205 -0
  201. package/kb/site/reference/subsystems/permission-presets.md +134 -0
  202. package/kb/site/reference/subsystems/persistence.md +388 -0
  203. package/kb/site/reference/subsystems/plan.md +90 -0
  204. package/kb/site/reference/subsystems/sandbox.md +221 -0
  205. package/kb/site/reference/subsystems/schedule.md +189 -0
  206. package/kb/site/reference/subsystems/scope.md +62 -0
  207. package/kb/site/reference/subsystems/session-projection.md +265 -0
  208. package/kb/site/reference/subsystems/session-query.md +498 -0
  209. package/kb/site/reference/subsystems/session-reference.md +111 -0
  210. package/kb/site/reference/subsystems/session-telemetry.md +197 -0
  211. package/kb/site/reference/subsystems/session-title.md +207 -0
  212. package/kb/site/reference/subsystems/session.md +854 -0
  213. package/kb/site/reference/subsystems/settings.md +313 -0
  214. package/kb/site/reference/subsystems/shell.md +306 -0
  215. package/kb/site/reference/subsystems/skills.md +334 -0
  216. package/kb/site/reference/subsystems/spill.md +120 -0
  217. package/kb/site/reference/subsystems/storage.md +232 -0
  218. package/kb/site/reference/subsystems/subagent.md +739 -0
  219. package/kb/site/reference/subsystems/subprocess.md +327 -0
  220. package/kb/site/reference/subsystems/system-prompt.md +210 -0
  221. package/kb/site/reference/subsystems/terminal.md +187 -0
  222. package/kb/site/reference/subsystems/token-meter.md +93 -0
  223. package/kb/site/reference/subsystems/tools.md +723 -0
  224. package/kb/site/reference/subsystems/typert.md +339 -0
  225. package/kb/site/reference/subsystems/user-questions.md +181 -0
  226. package/kb/site/reference/subsystems/web-server.md +111 -0
  227. package/kb/site/reference/subsystems/web.md +202 -0
  228. package/kb/site/reference/subsystems/workflow.md +281 -0
  229. package/kb/site/reference/subsystems/workspace.md +231 -0
  230. package/kb/site/reference/tool-catalog.md +1880 -0
  231. package/kb/site/reference/tool-execution-pipeline.md +66 -0
  232. package/package.json +40 -0
  233. package/scripts/rebuild-index.mjs +88 -0
  234. package/skills/dsh-plugin-dev-kb.md +66 -0
@@ -0,0 +1,185 @@
1
+ ---
2
+ editSource: "docs/user/develop/basic/publish.zh.md"
3
+ ---
4
+
5
+ # 打包与安装插件
6
+
7
+ 前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。本文假设 `dsh` CLI 已安装。请先完成[插件配置](./config.md)。
8
+
9
+ 如果改用全新的源码 checkout,请先按照[从源码运行章节](https://github.com/deepseek-ai/deepseek-harness/blob/master/README.md#run-from-source)完成准备,将本教程的 `hello-plugin` 目录放在仓库根目录,并从该目录把下文的 `dsh ...` 命令改为 `pnpm dsh ...`。构建与启动器行为见[源码执行](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md#source-execution)。
10
+
11
+ ## 两个概念,两种 manifest
12
+
13
+ 安装机制建立在两个概念之上。二者都由一份 `package.json` 描述,但它们在 `dsh` 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同:
14
+
15
+ - **组合包**是附带一个配置层的 npm 包。它的 manifest 声明 `dsh.bundle`,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。
16
+ - **profile** 是位于 `$DSH_HOME/profiles/<name>` 下、描述一份可启动组合的目录。它的 manifest 声明 `dsh.profile`,回答的是"这套配置由哪些组合包按什么顺序组成?"。
17
+
18
+ 组合包是你编写并分发的东西;profile 是用户用 `dsh --profile <name>` 启动的东西。没有东西同时是两者。
19
+
20
+ ### 组合包 manifest
21
+
22
+ 创建包目录:
23
+
24
+ ```sh
25
+ mkdir -p hello-plugin
26
+ ```
27
+
28
+ ```
29
+ hello-plugin/
30
+ ├── package.json # declares dsh.bundle
31
+ ├── cordis.patch.yml # the layer applied when a profile lists this bundle
32
+ └── index.js # plugin modules the patch rows reference
33
+ ```
34
+
35
+ 创建 `hello-plugin/package.json`:
36
+
37
+ ```json
38
+ {
39
+ "name": "dsh-hello-plugin",
40
+ "version": "0.1.0",
41
+ "type": "module",
42
+ "main": "index.js",
43
+ "files": ["index.js", "cordis.patch.yml"],
44
+ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
45
+ }
46
+ ```
47
+
48
+ 创建 `hello-plugin/index.js`,写入插件入口:
49
+
50
+ ```js
51
+ export const name = 'hello-plugin'
52
+
53
+ export function apply() {
54
+ console.log('[hello-plugin] plugin loaded!')
55
+ }
56
+ ```
57
+
58
+ 创建 `hello-plugin/cordis.patch.yml`。这个 patch 与一直在写的 `--patch` overlay 一样,是一个 patch 条目的 YAML 数组;区别是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码:
59
+
60
+ ```yaml
61
+ - insert:
62
+ - id: hello
63
+ name: dsh-hello-plugin
64
+ ```
65
+
66
+ 没有 `dsh.bundle` 声明的包仍然可以安装,但只作为普通依赖:`dsh plugin` 会打印警告,且不激活任何层。如果一个库供插件包 import,而不是供用户启用,就使用这种包格式。
67
+
68
+ ### profile manifest
69
+
70
+ profile 目录包含两个文件:
71
+
72
+ - `package.json` — profile 的树外插件依赖(由 pnpm 管理),加上 `dsh.profile` manifest 及其有序的 `bundles` 列表。
73
+ - `cordis.patch.yml` — 用户自己的 patch 层,在每个组合包层之后应用。
74
+
75
+ profile manifest 从不需要手写:`dsh plugin` 负责创建和维护它。下一节展示其结果。
76
+
77
+ ## 安装进 profile
78
+
79
+ `dsh plugin --profile <name> <args...>` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。在包含 `hello-plugin` 的目录中安装该包的 checkout:
80
+
81
+ ```sh
82
+ dsh plugin --profile demo add ./hello-plugin
83
+ ```
84
+
85
+ 首次使用会初始化 profile(`@deepseek-ai/dsh-base` 作为它的第一个组合包),pnpm 链接该 checkout,而 `dsh` 因为这个包声明了 `dsh.bundle`,把它追加进 `dsh.profile.bundles`:
86
+
87
+ ```json
88
+ {
89
+ "name": "dsh-profile-demo",
90
+ "private": true,
91
+ "dependencies": {
92
+ "dsh-hello-plugin": "link:/path/to/hello-plugin"
93
+ },
94
+ "dsh": {
95
+ "profile": {
96
+ "bundles": [
97
+ "@deepseek-ai/dsh-base",
98
+ "dsh-hello-plugin"
99
+ ]
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ 先不启动、只验证该层,再启动:
106
+
107
+ ```sh
108
+ dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer
109
+ dsh --profile demo
110
+ ```
111
+
112
+ `dsh plugin --profile demo remove dsh-hello-plugin` 会同时移除依赖和对应的层。
113
+
114
+ ## 加载顺序
115
+
116
+ 生效配置在空根之上按以下顺序逐层组合:
117
+
118
+ 1. profile 的 `dsh.profile.bundles` 列表所列的各个组合包 patch,按列表顺序——先是 `@deepseek-ai/dsh-base`,然后是每个已安装组合包,按其加入顺序。
119
+ 2. profile 自己的 `cordis.patch.yml`。
120
+ 3. home 级的 `$DSH_HOME/cordis.patch.yml`——各 profile 共享的机器本地偏好。
121
+ 4. 每个 `--patch <path>` overlay,按 argv 顺序。
122
+
123
+ 应用参数不是另一层 patch。表层组合包可以通过下文所述的普通应用自有服务解析它们。
124
+
125
+ 后应用的层按行胜出,且 patch 会替换目标行的整个 `config` 值,而不是深度合并各键。这给组合包作者带来两个推论:
126
+
127
+ - 你的 patch 可以按 `id` 覆盖前面各层的行——就像 [`dsh-web-app` 组合包](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/web-app/cordis.patch.yml)覆盖 `dsh-base` 的行那样——但必须重述该行需要的每一个键,而不是只写改动的那个。
128
+ - 用户可以在自己 profile 的 `cordis.patch.yml` 中覆盖你的行,无需改动你的包,所以优先给出用户大概率会保留的配置默认值,其余交给 schema 承担。
129
+
130
+ 内置组合包名称始终从 dsh 安装目录本身解析;pnpm 只管理树外的包,所以你的组合包可以放心依赖 `@deepseek-ai/dsh-base` 存在且与安装保持一致。
131
+
132
+ ## 让表层组合包持有自己的命令行
133
+
134
+ 定义了可运行应用的组合包挂载一个普通提供方插件:
135
+
136
+ ```yaml
137
+ - id: hello-startup
138
+ name: 'dsh-hello-plugin/startup'
139
+ ```
140
+
141
+ 该插件导出 `inject = ['cmdlineArgs']`,使用自己的 commander program 调用 [`@deepseek-ai/dsh-cmdline`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/boot/cmdline/README.md) 中的 `parseCmdline`,再在 program 自己的 action 中把应用自有服务提供出去。启动器把自身 flag 之后的同一份不可变参数交给每个插件,因此添加应用专属 flag 无需修改启动器,多个插件也可以解析该快照。Loader 行不需要启动器标记或特殊类型。
142
+
143
+ 受这些参数配置的行会注入提供方服务,并在自己的 `!!js` 选项中读取它,同时把部署取值写在旁边作为回退:
144
+
145
+ ```yaml
146
+ - id: my-app
147
+ name: '@example/my-app'
148
+ inject: [myAppStartup]
149
+ config:
150
+ port: !!js ctx.myAppStartup.port ?? 8080
151
+ ```
152
+
153
+ 遇到 `--help` 时,提供方不会发布该服务,所以这些行不会激活。Loader 只挂载一次组合,等待每一行的普通注入,再基于其已注入的上下文求值该行的 `!!js` 配置。
154
+
155
+ ## 从 GitHub 安装:构建脚本这道坎
156
+
157
+ 发布到注册表不是必须的——用户可以直接从 git 托管安装:
158
+
159
+ ```sh
160
+ dsh plugin --profile demo add github:you/hello-plugin
161
+ ```
162
+
163
+ 但 git 安装拉取的是**源码,不是构建产物**:没有任何环节运行你的 `build` 脚本,因此 TypeScript 包到手时没有 `lib/` 输出,加载会失败。必须两边各做一件事:
164
+
165
+ - **作者**提供一个 `prepare` 脚本——pnpm 在 git 安装后运行它——从源码构建出发布入口,且必须自包含:不能假设仅开发环境才有的上下文,例如旁边有一份 monorepo checkout。[turtle-ui](https://github.com/deepseek-harness/turtle-ui) 是一个可用的例子:它的 `prepare` 运行一份专用的 tsdown 配置,直接转译 `src/`,不用项目引用,也不做类型检查。
166
+ - **用户**为构建授权。pnpm ≥10 在得到显式允许之前拒绝运行 git 依赖的 `prepare` 脚本,所以第一次 `add` 会失败;`dsh` 会指出修法——把 pnpm 打印的确切包键复制进该 profile 的 `pnpm-workspace.yaml`:
167
+
168
+ ```yaml
169
+ allowBuilds:
170
+ dsh-hello-plugin: true
171
+ ```
172
+
173
+ 然后重新执行 `add`。
174
+
175
+ 请如实看待这项授权:**允许该包的代码在安装时于你的机器上执行**,且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(`github:you/hello-plugin#<sha>`),让后续推送无法悄悄改变实际运行的内容。
176
+
177
+ 如果不想让用户做这项授权,就改为分发构建产物——以下两种形式都不需要任何构建权限:
178
+
179
+ - **发布到 npm**,在 `pnpm publish` 时构建好 `lib/`;`dsh plugin add your-package` 安装的就是预构建代码。
180
+ - **交付 tarball**:用 `pnpm pack` 打包;用户执行 `dsh plugin add ./hello-plugin-0.1.0.tgz`。
181
+
182
+ ## 下一步
183
+
184
+ - [插件与生命周期](../framework/index.md) — 插件的完整生命周期
185
+ - [CLI(命令行界面)行为参考](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md) — 确切的层优先级、flag 与 profile 机制
@@ -0,0 +1,54 @@
1
+ ---
2
+ editSource: "docs/user/develop/basic/tool.zh.md"
3
+ ---
4
+
5
+ # 开发一个工具
6
+
7
+ 本教程会在 Web UI 中添加一个 `greet` 工具。请先完成[第一个插件](./index.md),并保留其中的 `scratch-plugin` 目录。
8
+
9
+ ## 创建工具插件
10
+
11
+ 将 `scratch-plugin/src/my-plugin.ts` 替换为:
12
+
13
+ ```ts
14
+ import type { Context } from '@deepseek-ai/cordis'
15
+ import { defineTool } from '@deepseek-ai/dsh-tools'
16
+
17
+ export const name = 'greet-tool'
18
+ export const inject = ['tools']
19
+
20
+ export function apply(ctx: Context) {
21
+ ctx.tools.register(defineTool({
22
+ name: 'greet',
23
+ description: 'Greet someone by name.',
24
+ parameters: {
25
+ name: { type: 'string', required: true, description: 'The name to greet' },
26
+ },
27
+ output: {
28
+ schema: { type: 'string' },
29
+ render: (_args, value) => [{ type: 'text', text: value }],
30
+ },
31
+ async execute(args) {
32
+ return `Hello, ${args.name}!`
33
+ },
34
+ }))
35
+ }
36
+ ```
37
+
38
+ `inject` 让 Cordis 等待工具注册表就绪。`defineTool` 根据 `parameters` 推导并校验 `args`;`execute` 返回 `output.schema` 声明的规范值,`output.render` 再将该值转换为面向模型的内容。
39
+
40
+ ## 运行并调用工具
41
+
42
+ 如果开发命令未在运行,请重新启动:
43
+
44
+ ```sh
45
+ pnpm dsh web --patch ./scratch-plugin/cordis.yml
46
+ ```
47
+
48
+ 打开 `http://127.0.0.1:3080`,然后输入:`Use the greet tool to greet Ada.` 模型可以调用 `greet`,并收到 `Hello, Ada!` 这一工具结果。
49
+
50
+ ## 下一步
51
+
52
+ - [插件配置](./config.md) — 让问候语可配置。
53
+ - [工具编写参考](../../reference/cookbook/adding-a-tool.md) — 查阅嵌套 schema、规范值、后台工作、策略钩子、Code Mode 和 UI 卡片。
54
+ - [能力分层](../practice/index.md) — 将可替换能力拆分为 Service Definition、Service Provider 和 Consumer 三类包。
@@ -0,0 +1,95 @@
1
+ ---
2
+ editSource: "docs/cordis-tutorial/01-first-plugin.zh.md"
3
+ ---
4
+
5
+ # 1. 编写第一个插件
6
+
7
+ 在本教程使用的 loader 配置中,Cordis 插件模块通过命名导出提供 `apply` 函数。Cordis 加载模块时,会用一个 **上下文** 调用 `apply`;该上下文就是 `ctx` 对象,插件通过它注册自己贡献的所有内容。
8
+
9
+ ## 编写插件
10
+
11
+ 在 `tmp/cordis-tutorial` 目录中(参见[环境设置](./index.md#setup))创建 `hello.ts`:
12
+
13
+ ```ts
14
+ import type { Context } from '@deepseek-ai/cordis'
15
+
16
+ export const name = 'hello'
17
+
18
+ export function apply(ctx: Context) {
19
+ console.log('hello from my first plugin')
20
+ }
21
+ ```
22
+
23
+ `name` 导出项是可选的显示元数据;它用于在诊断信息中标识插件。
24
+
25
+ ## 组合应用
26
+
27
+ 本教程的启动器通过配置组装应用。创建 `cordis.yml`:
28
+
29
+ ```yaml
30
+ - name: './hello.ts'
31
+ ```
32
+
33
+ 该文件是一组 Cordis 配置项的列表。`name` 是模块指定符,可以是相对路径或 NPM 包名;loader 会挂载每个配置项。各项会并发启动,因此它们在列表中的位置不保证插件的加载先后;顺序由服务依赖(`inject`,参见[第 3 章](./03-services.md))决定,而非文件中的位置。
34
+
35
+ ## 运行
36
+
37
+ ```sh
38
+ node --import tsx ../../vendor/cordis/bin.js
39
+ ```
40
+
41
+ 预期输出:
42
+
43
+ ```
44
+ hello from my first plugin
45
+ ```
46
+
47
+ 当没有任何内容继续运行时,进程会自行退出。具体过程如下:
48
+
49
+ 1. 启动器创建根 `Context`,并挂载 **Loader** 插件。
50
+ 2. Loader 读取 `cordis.yml`,解析 `./hello.ts`,然后将其作为子插件挂载。
51
+ 3. Cordis 调用你的 `apply(ctx)`。
52
+
53
+ 你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[`dsh` base](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/base/cordis.patch.yml) 就是一份更长的插件组合,由部署 overlay 对它进行修补。
54
+
55
+ ## 其他两种插件形态
56
+
57
+ 函数是最常见的形式,但 Cordis 接受三种形式:
58
+
59
+ ```ts
60
+ import { Service, type Context } from '@deepseek-ai/cordis'
61
+
62
+ // 1. Function plugin (what you just wrote).
63
+ export function apply(ctx: Context) {}
64
+
65
+ // 2. Object plugin: an object with an `apply` method.
66
+ export const objectPlugin = {
67
+ name: 'object-plugin',
68
+ apply(ctx: Context) {},
69
+ }
70
+
71
+ // 3. Class plugin: a Service subclass (covered in chapter 3).
72
+ export class MyService extends Service {
73
+ constructor(ctx: Context) {
74
+ super(ctx, 'myTutorialService')
75
+ }
76
+ }
77
+ ```
78
+
79
+ 在你需要公开服务之前,请一直使用函数形态;[第 3 章](./03-services.md)介绍了何时应当使用类形态。
80
+
81
+ ## 尝试制造错误
82
+
83
+ 让 `apply` 抛出异常:
84
+
85
+ ```ts ignore-check
86
+ export function apply(ctx: Context) {
87
+ throw new Error('apply exploded')
88
+ }
89
+ ```
90
+
91
+ 再次运行:进程会因该错误而终止。插件加载失败会明确报错,不会仅跳过该配置项。
92
+
93
+ 还需要尽早了解一个例外:如果某个配置项的模块无法被 **解析**,例如路径或包名拼写错误,Cordis 会通过 logger 服务报告错误,而不会使进程崩溃。在启动阶段,这条报告可能在 console 导出器开始观察之前丢失。如果新增配置项似乎没有任何效果,请先检查拼写。
94
+
95
+ 下一章:[生命周期与 effect](./02-lifecycle-and-effects.md):插件卸载时会发生什么。
@@ -0,0 +1,98 @@
1
+ ---
2
+ editSource: "docs/cordis-tutorial/02-lifecycle-and-effects.zh.md"
3
+ ---
4
+
5
+ # 2. 生命周期与 effect
6
+
7
+ Cordis 插件可能因修改配置、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 `ctx.effect()` 中。
8
+
9
+ ## Effect
10
+
11
+ 对于 Cordis 尚未管理的资源,例如定时器、连接或 watcher,应将其包装在 `ctx.effect()` 中并返回 disposer(资源释放函数):
12
+
13
+ 创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
14
+
15
+ ```ts
16
+ import type { Context } from '@deepseek-ai/cordis'
17
+
18
+ export const name = 'lifecycle-demo'
19
+
20
+ function heartbeat(ctx: Context) {
21
+ console.log('heartbeat plugin loading')
22
+ ctx.effect(() => {
23
+ const timer = setInterval(() => console.log('tick'), 200)
24
+ return () => {
25
+ clearInterval(timer)
26
+ console.log('heartbeat cleaned up')
27
+ }
28
+ })
29
+ }
30
+
31
+ export function apply(ctx: Context) {
32
+ // Mount a child plugin and keep its fiber to dispose it later.
33
+ const fiber = ctx.plugin(heartbeat)
34
+ // The demo timer is itself an effect: if THIS plugin is unloaded first,
35
+ // the pending callback is cancelled instead of firing on a dead app.
36
+ ctx.effect(() => {
37
+ const timer = setTimeout(async () => {
38
+ await fiber.dispose()
39
+ console.log('disposed')
40
+ process.exit(0)
41
+ }, 700)
42
+ return () => clearTimeout(timer)
43
+ })
44
+ }
45
+ ```
46
+
47
+ 让 `cordis.yml` 指向该文件:
48
+
49
+ ```yaml
50
+ - name: './lifecycle.ts'
51
+ ```
52
+
53
+ 运行(`node --import tsx ../../vendor/cordis/bin.js`)后会得到:
54
+
55
+ ```
56
+ heartbeat plugin loading
57
+ tick
58
+ tick
59
+ tick
60
+ heartbeat cleaned up
61
+ disposed
62
+ ```
63
+
64
+ 请留意三点:
65
+
66
+ - `ctx.plugin(heartbeat)` 会把一个**来自代码**的函数挂载为插件,这与 YAML loader 为每个配置项执行的操作相同。函数插件不需要 `apply` 方法:Cordis 会直接调用该函数,其名称只用于诊断。只有对象形态才要求 `apply` 方法,例如 `ctx.plugin({ apply(ctx) { /* ... */ } })`。调用会返回一个 **fiber**,即一个已加载插件实例的运行时句柄。
67
+ - effect 主体在加载期间运行;它返回的 disposer 在卸载期间运行。对于生命周期与插件一致的资源,你绝不需要自行调用 disposer。
68
+ - `fiber.dispose()` 会等该插件的所有清理工作(包括异步 disposer)完成后才结束,并递归卸载它挂载的所有子插件。
69
+
70
+ ## Fiber 状态机
71
+
72
+ 每个已加载插件实例都拥有一个 fiber,并在以下状态之间转换:
73
+
74
+ ```
75
+ PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
76
+ ↘ FAILED
77
+ ```
78
+
79
+ - **PENDING**:已经声明,但所需服务(第 3 章)尚不可用。
80
+ - **LOADING / ACTIVE**:`apply` 正在运行/已经完成。
81
+ - **FAILED**:`apply` 或配置校验抛出异常。
82
+ - **UNLOADING / DISPOSED**:disposer 正在运行/一切均已拆除。
83
+
84
+ 你会在[第 6 章](./06-composition-and-hmr.md)再次遇到 PENDING,它通常就是「为什么我的插件没有输出」的答案。
85
+
86
+ ## 已经属于 effect 的操作
87
+
88
+ 你很少需要亲自编写 `ctx.effect()`,因为内置注册 API 本身已经是 effect:
89
+
90
+ - `ctx.on(event, listener)`:监听器会在卸载时移除([第 4 章](./04-events.md))。
91
+ - `ctx.plugin(child)`:子插件会随父插件一同 dispose(资源释放)。
92
+ - 服务注册属于 effect。`ctx.tools.register(...)` 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动撤销([第 7 章](./07-into-the-harness.md))。
93
+
94
+ 对于 Cordis 不管理的资源,应在 `ctx.effect()` 内获取它,并返回用于释放资源的 disposer。此后 Cordis 会在卸载期间调用该释放逻辑,热重载时也不例外。
95
+
96
+ 有一项顺序注意事项:disposer 会按注册顺序的逆序启动,但多个**异步** disposer 会并发运行。如果拆除步骤必须按顺序执行,请把它们放在同一个 disposer 中,并在其中依次等待每步完成。
97
+
98
+ 下一章:[服务](./03-services.md):插件如何共享功能。
@@ -0,0 +1,98 @@
1
+ ---
2
+ editSource: "docs/cordis-tutorial/03-services.zh.md"
3
+ ---
4
+
5
+ # 3. 服务
6
+
7
+ **服务**是一个插件提供、其他插件通过 `ctx` 消费的具名能力。在 harness 中,`ctx.tools`、`ctx.llm` 和 `ctx.agents` 都是服务。消费方只指定 `'tools'` 之类的能力,而不导入其提供方,因此配置可以选择提供方,无需修改消费方。
8
+
9
+ ## 提供服务
10
+
11
+ 创建 `greeter.ts`,将它放在 `tmp/cordis-tutorial` 中:
12
+
13
+ ```ts
14
+ import { Service, type Context } from '@deepseek-ai/cordis'
15
+
16
+ declare module '@deepseek-ai/cordis' {
17
+ interface Context {
18
+ greeter: GreeterService
19
+ }
20
+ }
21
+
22
+ export class GreeterService extends Service {
23
+ constructor(ctx: Context) {
24
+ super(ctx, 'greeter')
25
+ }
26
+
27
+ greet(who: string) {
28
+ return `Hello, ${who}!`
29
+ }
30
+ }
31
+
32
+ export const name = 'greeter'
33
+
34
+ export function apply(ctx: Context) {
35
+ ctx.plugin(GreeterService)
36
+ }
37
+ ```
38
+
39
+ 两部分协同工作:
40
+
41
+ - **运行时**:`super(ctx, 'greeter')` 以名称 `greeter` 注册该实例。此后,任何插件都可以通过 `ctx.greeter` 访问它。注册属于 effect,卸载提供方时会移除该服务。
42
+ - **编译时**:`declare module '@deepseek-ai/cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
43
+
44
+ `Service` 子类本身就是插件(第 1 章介绍的类形态),因此 `ctx.plugin(GreeterService)` 会像挂载其他插件一样挂载它。
45
+
46
+ ## 使用 `inject` 消费服务
47
+
48
+ 创建 `consumer.ts`:
49
+
50
+ ```ts
51
+ import type { Context } from '@deepseek-ai/cordis'
52
+
53
+ export const name = 'consumer'
54
+ export const inject = ['greeter']
55
+
56
+ export function apply(ctx: Context) {
57
+ console.log(ctx.greeter.greet('world'))
58
+ }
59
+ ```
60
+
61
+ `inject` 列出该插件需要的服务。Cordis 会让插件保持 PENDING,直到列出的每项服务都存在,因此在 `apply` 内可以保证 `ctx.greeter` 已经就绪。`cordis.yml` 中的加载顺序无关紧要:决定插件何时启动的是依赖关系,而不是文件顺序。
62
+
63
+ 组合并运行:
64
+
65
+ ```yaml
66
+ - name: './greeter.ts'
67
+ - name: './consumer.ts'
68
+ ```
69
+
70
+ ```
71
+ Hello, world!
72
+ ```
73
+
74
+ 交换 `cordis.yml` 中两行的顺序后重新运行,输出仍然相同。尝试彻底移除 `./greeter.ts`:消费方会保持 PENDING,不输出任何内容,既不崩溃,也不会只运行一部分。处于 PENDING 的 fiber 也不会让 Node 的事件循环保持活跃,因此如果组合中没有其他运行项,进程会静默地以状态码 0 退出。[第 6 章](./06-composition-and-hmr.md)介绍如何诊断这种状态。
75
+
76
+ ## 加载后仍会跟踪依赖关系
77
+
78
+ `inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect([第 2 章](./02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会撤销。
79
+
80
+ 这也是配置中可以替换服务的原因:卸载 Cordis 配置项 `dsh-bash-local`,挂载另一个 `shell` 提供方,所有注入 `'shell'` 的插件都会重新启动并使用新实现。
81
+
82
+ ## 可选依赖
83
+
84
+ `inject` 用于硬性依赖。如果某项功能缺失时插件仍可运行,请跳过 `inject`,并在使用处探测:
85
+
86
+ ```ts ignore-check
87
+ export function apply(ctx: Context) {
88
+ // undefined when no provider is loaded; the plugin still runs.
89
+ const greeter = ctx.get('greeter')
90
+ console.log(greeter?.greet('maybe') ?? 'no greeter available')
91
+ }
92
+ ```
93
+
94
+ ## 命名
95
+
96
+ 每个应用中的服务名称共用一个扁平命名空间。请为自有服务添加有辨识度的前缀或命名空间(harness 已占用 `tools` 和 `llm` 等普通名称);[子系统页面](../../reference/subsystems/core.md)上生成的 `cordis-surface` 区块列出 harness 注册的每个名称。
97
+
98
+ 下一章:[事件](./04-events.md):无需共享服务即可通信。
@@ -0,0 +1,144 @@
1
+ ---
2
+ editSource: "docs/cordis-tutorial/04-events.zh.md"
3
+ ---
4
+
5
+ # 4. 事件
6
+
7
+ 服务支持直接调用;**事件**让插件无需知道有哪些插件正在监听,就能发出通知。harness 使用事件处理工具结果、模型请求和审批决定等交互。
8
+
9
+ ## 声明、发出与监听
10
+
11
+ 创建 `stats.ts`,将它放在 `tmp/cordis-tutorial` 中。它是一项负责计数并在每次变化时发出通知的服务:
12
+
13
+ ```ts
14
+ import { Service, type Context } from '@deepseek-ai/cordis'
15
+
16
+ declare module '@deepseek-ai/cordis' {
17
+ interface Context {
18
+ stats: StatsService
19
+ }
20
+ interface Events {
21
+ 'stats/report'(name: string, count: number): void
22
+ }
23
+ }
24
+
25
+ export class StatsService extends Service {
26
+ private counts = new Map<string, number>()
27
+
28
+ constructor(ctx: Context) {
29
+ super(ctx, 'stats')
30
+ }
31
+
32
+ bump(name: string) {
33
+ const next = (this.counts.get(name) ?? 0) + 1
34
+ this.counts.set(name, next)
35
+ this.ctx.emit('stats/report', name, next)
36
+ }
37
+ }
38
+
39
+ export const name = 'stats'
40
+
41
+ export function apply(ctx: Context) {
42
+ ctx.plugin(StatsService)
43
+ }
44
+ ```
45
+
46
+ `interface Events` 合并与第 3 章的 `interface Context` 合并在事件系统中相互对应:它声明事件名称及其监听器签名,因此 `ctx.emit` 和 `ctx.on` 都具有完整类型。`namespace/action` 命名约定让扁平的事件命名空间保持易读。
47
+
48
+ 创建 `reporter.ts`:
49
+
50
+ ```ts ignore-check
51
+ import type { Context } from '@deepseek-ai/cordis'
52
+ import type {} from './stats.ts'
53
+
54
+ export const name = 'reporter'
55
+ export const inject = ['stats']
56
+
57
+ export function apply(ctx: Context) {
58
+ ctx.on('stats/report', (name, count) => {
59
+ console.log(`[stats] ${name} -> ${count}`)
60
+ })
61
+ ctx.stats.bump('tool_call')
62
+ ctx.stats.bump('tool_call')
63
+ ctx.stats.bump('prompt')
64
+ }
65
+ ```
66
+
67
+ `import type {} from './stats.ts'` 行不会在运行时导入任何内容;它的作用是让 TypeScript 看到声明合并。组合并运行:
68
+
69
+ ```yaml
70
+ - name: './stats.ts'
71
+ - name: './reporter.ts'
72
+ ```
73
+
74
+ ```
75
+ [stats] tool_call -> 1
76
+ [stats] tool_call -> 2
77
+ [stats] prompt -> 1
78
+ ```
79
+
80
+ 因为 `ctx.on()` 属于 effect,监听器会随插件一同消失,绝不需要手动维护 `removeListener`。
81
+
82
+ ## 分发模式
83
+
84
+ `emit` 是 5 种分发模式之一。事件采用哪种模式是其约定的一部分,决定了监听器能否返回值、能否并发运行,以及能否彼此短路:
85
+
86
+ | 模式 | 调用 | 语义 |
87
+ |---|---|---|
88
+ | emit | `ctx.emit(name, ...args)` | 同步广播;不会等待或收集返回的 promise 与值。 |
89
+ | parallel | `await ctx.parallel(name, ...args)` | 所有监听器并发运行,并一同等待。 |
90
+ | serial | `await ctx.serial(name, ...args)` | 监听器按顺序运行并等待;第一个非 `null`/`false`/`undefined` 返回值胜出,并停止后续监听器。 |
91
+ | bail | `ctx.bail(name, ...args)` | serial 的同步版本。 |
92
+ | waterfall(瀑布式事件) | `ctx.waterfall(name, ...args, next)` | 环绕中间件,见下文。 |
93
+
94
+ 每个 harness 事件都会在其所属[子系统页面](../../reference/subsystems/core.md)自动生成的参考文档中记录其模式。
95
+
96
+ ## waterfall:转换或短路
97
+
98
+ waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation;它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回,从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`:
99
+
100
+ ```ts
101
+ import type { Context } from '@deepseek-ai/cordis'
102
+
103
+ declare module '@deepseek-ai/cordis' {
104
+ interface Events {
105
+ 'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
106
+ }
107
+ }
108
+
109
+ export const name = 'waterfall-demo'
110
+
111
+ export function apply(ctx: Context) {
112
+ // Listener 1: wrap the downstream result.
113
+ ctx.on('demo/transform', async (input, next) => {
114
+ const downstream = await next()
115
+ return downstream.toUpperCase()
116
+ })
117
+
118
+ // Listener 2: short-circuit when it owns the decision.
119
+ ctx.on('demo/transform', async (input, next) => {
120
+ if (input.includes('blocked')) return '** blocked **'
121
+ return next()
122
+ })
123
+
124
+ void (async () => {
125
+ console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))
126
+ console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words'))
127
+ })()
128
+ }
129
+ ```
130
+
131
+ 让 `cordis.yml` 只指向该文件并运行:
132
+
133
+ ```
134
+ HELLO
135
+ ** BLOCKED **
136
+ ```
137
+
138
+ 按顺序看第二行如何产生:监听器 1 先运行并调用 `next()`,从而调用监听器 2;监听器 2 看到 `blocked` 后直接返回而不调用 `next()`,因此最内层默认逻辑(传给 `ctx.waterfall` 的函数)从未运行;返回途中,监听器 1 再把替换消息转换为大写。
139
+
140
+ 由此得到一项纪律:**只负责观察或标注的 waterfall 监听器必须调用 `next()`**;不调用就直接返回代表有意短路。如果日志监听器忘记调用 `next()`,会悄无声息地吞掉所有下游的默认行为。这是本仓库的常设规则([waterfall 语义](../../reference/cordis-primer.md#cordis-waterfall-semantics))。
141
+
142
+ harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/request`](../../reference/subsystems/core.md#agentrequest--waterfall) 允许插件替换模型调用配置,[`approval/request`](../../reference/subsystems/approval.md#approvalrequest--waterfall) 允许策略代替用户作答。
143
+
144
+ 下一章:[配置](./05-config.md):来自 `cordis.yml` 的插件选项。