@jayyuen66/dsh-ocr-review 0.1.4 → 0.1.6
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/README.md +86 -53
- package/README.zh-CN.md +84 -49
- package/THIRD_PARTY_NOTICES.md +10 -8
- package/client.js +693 -123
- package/cordis.patch.yml +6 -5
- package/host.js +985 -359
- package/package.json +49 -52
- package/scripts/get-cred.mjs +27 -14
package/README.zh-CN.md
CHANGED
|
@@ -5,21 +5,50 @@
|
|
|
5
5
|
## 它做什么
|
|
6
6
|
|
|
7
7
|
- 把外部 `ocr`(open-code-review)CLI 封装成 dsh 的 5 个模型工具 + 1 张 web 设置卡片;宿主对工具入参零校验,形状与类型闸全在本包内做。
|
|
8
|
-
- Host 半(`host.ts`)声明 6 个 `.volatile()`
|
|
9
|
-
-
|
|
8
|
+
- Host 半(`host.ts`)声明 6 个 `.volatile()` 设置项(命名空间是隐式的:宿主按 profile 条目 id `ocr-review` 投影表单,插件不调 `settings.register`)。
|
|
9
|
+
- 同时注册 5 个模型工具、一段 systemPrompt 路由(`ocr-review-routing`,order 1555)与 `ocrReview` 命名空间下的 5 个方法。
|
|
10
|
+
- Client 半(`src/client-entry.ts` → `client.js`)是设置卡片,provider/model 取自设置服务的 `llm-pi-ai` 命名空间;此外还贡献三处只读显示:插件详情页标题旁的徽标、页面内容之下的区块,以及启用本包那一刻的指引——三处说的都是「这台机器上的外部 CLI 是什么状态」。
|
|
10
11
|
- 卡片把 provider/model 落盘到外部 ocr CLI 自己的 `~/.opencodereview/config.json`,写进去的是 `api_key_cmd` 命令而非明文 key。
|
|
12
|
+
- 评审结果回给模型的是一份结构化摘要,字段分两套口径:OCR 审出了什么(`status`/`aggregation`/`topComments`/`warnings`),以及这一场有没有真把选中的文件审完(`coverage`/`budgetExceeded`/`toolCalls`)。`--audience agent` 下 CLI 把进度行写进 `io.Discard`,后一套口径只能从这些字段里读。
|
|
11
13
|
|
|
12
14
|
## 前置条件(外部 CLI)
|
|
13
15
|
|
|
14
16
|
- 本包不 import 任何 OCR 库,只起进程:`ocr review` / `ocr scan` / `ocr delegate preview` / `ocr delegate rule` / `ocr session` / `ocr llm test`。
|
|
15
|
-
- 命令按
|
|
17
|
+
- 命令按 **1.13.0** 源码逐一核对(`lib/cli.ts`、`lib/parse.ts` 头注释),形如 `ocr review --audience agent --format json --effort medium --output <临时文件>`。这一版号只写在 package.json 的 `ocrCliVersion` 一处,`lib/cli.ts` 里的 `EXPECTED_OCR_CLI_VERSION` 是它在本包代码里的唯一镜像:`test/publish-manifest.test.ts` 断言两处逐字相等,`test/readme-parity.test.ts` 断言这一句写的也是它。「README 教用户装 A、代码却按 B 核对」这种两边各自自洽的错,只有并排断言才拦得住。
|
|
16
18
|
- 多值 `--exclude`/`--path` 合并成一个逗号分隔 flag,`--format` 只认 json/text/sarif。
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
+
- **CLI 由你自己装。** 本包刻意不带 `optionalDependencies`:把 `@alibaba-group/open-code-review` 列成依赖,一是让「装一个插件」顺带改了用户的全局工具链,二是那条绝对路径解析得比 PATH 上用户自装那份更靠前——用户在终端升了级,插件仍在跑随包那一份旧版,卡片报的版本与他实际跑的永远对不上,而且没有任何线索指向「机器上有两份」。
|
|
20
|
+
- 于是命令词的解析顺序是:PATH 上第一个能命中的 `ocr`(取它的绝对路径),其次才是本包依赖树里那一份兜底,两处都没有候选时才是裸 `ocr`(也就是下面那条拒绝要处理的情形)。设置页状态行、五个工具的前置短路、真正下发的那条命令词都读这同一份判据,「用的是哪一份」不许有三套说法。Windows 上随包兜底整体跳过(`bin/ocr.js` 是 shebang 脚本,pwsh/cmd 无法直执行),命令词也保持裸 `ocr`——带反斜杠的绝对路径在 `cmd` 里不靠单引号成对。
|
|
21
|
+
- 装法两条任选,npm 那条带得上核对版本,brew 那条带不上:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
brew install open-code-review # formula 只跟上游最新版
|
|
25
|
+
npm i -g @alibaba-group/open-code-review@1.13.0 # 钉在本包核对的那一版
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`homebrew/core` 里 `open-code-review` 只有一份 stable formula,没有 `open-code-review@x.y` 这种版本化 formula,所以 brew 钉不住版本,装出来就是最新的那一版。它与上面那一版不一致时,工具与卡片都会说(见「外部 CLI 的状态与版本」)。没装时先推哪一条由部署项 `ocrInstallHint` 决定;机器上已经有一份候选时,文案按那一份自己的来源说,部署偏好让位。
|
|
29
|
+
- 两条路都没有候选时,工具在起进程之前就拒绝,并把带版本的装法一起交给模型;如果判据拿到候选、子进程那边却执行不了(exit 127),工具报「ocr 命令不存在」并指到卡片上外部 CLI 那一行。
|
|
19
30
|
- 平台:目标平台是 macOS / Linux。进程收割(`ocr_review` / `ocr_scan` 的 reaper 守护)依赖 POSIX 工具链——bash 的 trap 与作业控制,加上 `ps` / `pgrep` / `kill` 的进程组语义;自定义 `ocrConfigPath` 背后的 `HOME=` 赋值前缀同样是 POSIX 专属。Windows 上这层整体缺席:本包按 `process.platform` 直接放行裸命令(不收割但能跑),并跳过随包 launcher(Windows 需 PATH 上的全局 `ocr`);`api_key_cmd` 在 Windows 按双引号转义写出(ocr 在那边经 `cmd.exe` 跑凭据命令)。代价是宿主硬退出后 OCR 可能留下孤儿进程。
|
|
20
31
|
- provider 清单与 key 状态走宿主官方通道 `ctx.settings.describe()`(`llm-pi-ai` 那条的 `value.providers`)与 `ctx.credentials`。
|
|
21
32
|
- 两者缺席时卡片显示降级原因,工具与设置照常可用。
|
|
22
|
-
- 宿主版本要求 `>= 0.2.1-
|
|
33
|
+
- 宿主版本要求 `>= 0.2.1-rc.2`:这条写在 `engines.dsh`,属于文档性声明,装载器与安装器都不读它。真正拦安装的是那四枚 peer 的精确钉:宿主拿运行时版本逐条比对 peer 范围(预发布号参与比对),不合就以 `incompatible-version` 拒装。跑 dsh 0.2.1-rc.2 的机器要先升 dsh,或在 profile 的 `compatibility.json` 里给本包登记豁免(`dsh plugin --profile <profile> allow-version <name@version> --dsh-version <运行时版本> --accept-risk`)。本包也不从 `@deepseek-ai/dsh` 主包导入任何符号(服务面全部来自 `@deepseek-ai/dsh-*` 子包),所以它在任何一张依赖表里都不出现。
|
|
34
|
+
|
|
35
|
+
## 外部 CLI 的状态与版本
|
|
36
|
+
|
|
37
|
+
判据只有一处,`lib/cli.ts` 的 `resolveOcrAvailability`,回答「这台机器有没有 ocr、用的是哪一份、那一份是谁装上的」:先扫 PATH 上第一个命中的可执行体(Windows 按 `PATHEXT` 补后缀),其次才是本包依赖树里那份兜底。「怎么装上的」看的是 bin 里那条软链**真正指向**的文件(`realpathSync`):落在 `node_modules` 下就是 npm 一类的包管理器,落在 `Cellar` / `Homebrew` 下就是 formula,两者都不是就报「未识别」而不猜。两条判据的顺序是有讲究的:用 brew 装的 node 跑 `npm i -g` 时,那条路径同时含 homebrew 与 node_modules,先判 homebrew 就会把 npm 装法说成 brew。整件事纯读文件系统——`statSync(...).isFile()`,绝不起进程——因此它答的是**找没找到候选**,不是**跑不跑得起来**:文件在但执行不了(丢了执行位,或随包入口在而平台二进制缺失)仍会通过前置检查,那一路由真实 spawn 报出来,所以「命令不存在」那句兜底文案必须留着。
|
|
38
|
+
|
|
39
|
+
| 显示面 | 拿到这份判据后做什么 |
|
|
40
|
+
| ------ | -------------------- |
|
|
41
|
+
| 五个工具 | 起任何进程之前先现读一次。两条路都没有候选就直接拒绝,模型看到的是「两条路都没找到 + 这么装(命令里带着核对版本)」,而不是子进程 exit 127 之后反推出来的那句「命令不存在」。每次调用现读、不按进程缓存:装完 CLI 直接重试即可,不必重启 dsh |
|
|
42
|
+
| 设置卡片 | provider 表单**之外**的一行状态(「这台机器有没有 ocr」与「宿主能不能列出 provider」是两件事,缺 CLI 的机器必须连原因都看得见):用的是哪一份、什么装法、本插件按哪一版核对、最近一次实测是哪一版,没装时再附一句装法;外加一枚「检测版本」按钮 |
|
|
43
|
+
| 插件详情页 | 标题旁一只徽标 + 页面自身内容之下一个区块;页面说的不是本包、或状态还没回来时两处都渲染 null。实测过且与核对版本不一致时,徽标改口说「版本不一致」,来源那一档让位 |
|
|
44
|
+
| 启用之后 | 启用指引只在判据为 `none` 时说话,说的就是带版本的装法——那是第三方 bundle 能渲染的最外侧位置 |
|
|
45
|
+
|
|
46
|
+
- 插件**列表**卡片上那排标签不归插件管:它们是宿主对条目自身的判定(`running` / `off` / `problem`)。上面两处就是能显示的最外一侧,而它们都属于 client 半——插件被禁用时 client 半一并撤下,没有谁还能在那里提示「CLI 没装」。
|
|
47
|
+
- 三处显示面读的都是 `providers()` 里同一个 `ocrBinary` 字段,且都不在本地自己算可得性、判装法、写装法命令:判据算三份就会分叉,命令写两处就会漂成「卡片教的版本与插件核对的版本不是一版」。
|
|
48
|
+
- **版本号不是白来的**:取版本要 spawn `ocr version`。跑它的时机有两处——按下卡片上的「检测版本」,以及 `startupVerify` 开着时装载期那一次(管它的是同一条开关,因为那条设置的判据就是「装载期允不允许起进程」)。页面渲染时绝不起进程:`providers()` 只交最近一次实测到的那个值,没测过就是 `null`,卡片于是只说「本插件按 1.13.0 核对」,不替你编一个本机版本。
|
|
49
|
+
- **版本对不上既显示也说出口**:回执那一行把两个数字并列,徽标改口,`ocr_review` / `ocr_scan` 的摘要 `hint` 尾部多一句「这份结果不是按核对版本跑出来的」,让模型也读到。它从不当成硬失败——新版 CLI 通常照样能用——本包也不替你执行升级:给的那条命令按那份 CLI 自己的来源说(formula 说 `brew upgrade open-code-review`,全局包说 `npm i -g …@<核对版本>`),来源判不出时按部署偏好说,并且始终把「brew 钉不住版本」那句一并说了。
|
|
50
|
+
- 回执在版本行之下附上 CLI 自己的输出,并剥掉终端控制序列(上游 launcher 那句「有新版本」的颜色码是硬编码的,而且打在 JS 包装层,`--color never` 管不到它)——要给人复制的那行是 `npm i -g …`,复制时不该拖着转义字节。一次来回走完后卡片会重读载荷,让状态行与刚取到的版本描述的是同一时刻(页面开着的时候装上 CLI,那一行也不会再跟回执自相矛盾)。本包刻意不提供更新动作,也不接剪贴板 API(非安全上下文会静默失败,看上去像按钮没反应)。
|
|
51
|
+
- **上游会自己升级**:`ocr` 的 npm 包装层会在调用过程中自行跑 `scripts/update.js`(默认 18 分钟冷却 + pid 锁),而那个脚本执行的是 `npm i -g @alibaba-group/open-code-review@<latest>`。部署项 `ocrNoAutoUpdate` 置真时,本包起的每一条 `ocr` 命令都带 `OCR_NO_UPDATE=1`,于是一场评审不会中途改动你的全局 npm;默认为假,即保持上游行为。版本探测无论这个开关怎么设都会带上该变量——读状态不该有写侧副作用。launcher 那句「有新版本」提示写在 `OCR_NO_UPDATE` 那道闸**之前**,所以它照样会出现在回执文本里。
|
|
23
52
|
|
|
24
53
|
## 安装
|
|
25
54
|
|
|
@@ -27,37 +56,26 @@
|
|
|
27
56
|
dsh plugin --profile web add @jayyuen66/dsh-ocr-review
|
|
28
57
|
```
|
|
29
58
|
|
|
59
|
+
- **升级已装的那一份要「先 remove 再 add」。** profile 跑着 HMR(出厂那几个 profile 都跑)时,`add` 一个 profile 已经依赖着的包会在 **pnpm 动手之前**就被拒:`application: 'failed'` 带 `error.code: 'already-installed'`,profile 的文件一字不改;网页上那个安装框同理。所以先 `dsh plugin --profile web remove @jayyuen66/dsh-ocr-review`,再 `add` 目标版本——remove 会停掉旧 bundle 的插件并摘掉它的模块映射,新版就此起跑、不必重启。没跑 HMR 时同一个 `add` 照样走 pnpm,报 `restart-required`。
|
|
30
60
|
- 包在公共 npm 上,安装不需要凭据。
|
|
31
61
|
- 发布面只含 `host.js`、`client.js`、`cordis.patch.yml`、`scripts` 与 `THIRD_PARTY_NOTICES.md`(随包运行时依赖的第三方许可证;`prepack` 重建两个 bundle),源码仓见 package.json 的 `repository.url`。
|
|
32
62
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
本包把 `@alibaba-group/open-code-review` 放在 `optionalDependencies`,上游带一个 `postinstall`(`scripts/install.js`)。pnpm 10+ 默认不执行依赖的构建脚本,于是安装会以这个错误收尾:
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
Error: ERR_PNPM_IGNORED_BUILDS
|
|
39
|
-
× installing dependencies
|
|
40
|
-
╰─▶ Ignored build scripts: @alibaba-group/open-code-review@1.12.11
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**这只需要放行一次,且只有装本包时才会遇到**(其余插件无需要构建脚本的依赖)。
|
|
63
|
+
## 外部 CLI 的安装与升级
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
本包只 spawn `ocr`,不安装它。两条命令任选:
|
|
46
66
|
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
```sh
|
|
68
|
+
brew install open-code-review # formula 只跟上游最新版
|
|
69
|
+
npm i -g @alibaba-group/open-code-review@1.13.0 # 钉在本包核对的那一版
|
|
50
70
|
```
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
如果你更希望完全不执行安装期脚本,可以写 `'@alibaba-group/open-code-review': false`:`ocr` 同样可用(launcher 直接从平台包目录取二进制),但宿主 `readPendingBuilds()` 只认值为 `set this to true or false` 的条目,改成 `false` 后官方审批流程就不再能代为授权,只能手工维护。
|
|
72
|
+
- **brew 钉不住版本。** `homebrew/core` 里 `open-code-review` 只有一份 stable formula,没有 `open-code-review@1.12` 这种版本化 formula,`brew install` / `brew upgrade` 拿到的永远是上游最新。需要与本插件同版本时只能走 npm;卡片上凡是出现 brew 那条命令的地方都会把这句一并说了。
|
|
73
|
+
- **本包不执行任何一条。** 版本探测恒定带 `OCR_NO_UPDATE=1`,读状态不许顺带装东西;`ocrNoAutoUpdate` 再把这条覆盖到本包起的每一条命令。
|
|
74
|
+
- **换版命令按那份 CLI 自己的来源给**:formula 给 `brew upgrade open-code-review`,全局包给 `npm i -g @alibaba-group/open-code-review@<核对版本>`。来源判不出来(手工 `cp` 到 `~/bin`、`npx`、别的前缀)就按部署项 `ocrInstallHint` 说,不猜。
|
|
75
|
+
- **从随包分发过 CLI 的版本升上来**(`optionalDependencies` 那几版,0.1.5-rc 及更早):什么都不必做——现在 PATH 上的那份优先,那份旧的不再遮蔽它。想把 profile 依赖树里那份清掉,在该 profile 目录(`<dsh 数据目录>` 下的 `profiles/<profile>/`)执行 `pnpm remove @alibaba-group/open-code-review`,或重装这个 profile;当初为它写的 `allowBuilds: {'@alibaba-group/open-code-review': true}` 也可以一并删掉。
|
|
76
|
+
- **`ERR_PNPM_IGNORED_BUILDS` 不再来自本包。** 之前会撞上,是因为上游包装层带一个 `postinstall`(`scripts/install.js`),而 pnpm 10+ 默认不执行依赖的构建脚本。本包现在没有需要跑脚本的依赖,这条报错只可能来自别的包——那种情况下宿主的审批流程照旧可用(Web 插件页的「允许这些脚本并重试」,或在对话里明确同意后由 Agent 经 `install_bundle` 的 `approvedBuilds` 代为授权;宿主只校验待决定的包名,不核实对话里的批准,所以仍需你明确同意)。
|
|
59
77
|
- `dependencies` 里不放任何 `@deepseek-ai/*`,这不是风格偏好。本包的消费方是 dsh 插件,被装进 profile(`nodeLinker: hoisted`、`autoInstallPeers: false`),只要宿主包出现在 `dependencies`,pnpm 就会把它在那颗 profile 里落成**真实目录**,与宿主进程已经加载的那份并存。而 `@deepseek-ai/dsh-tools` 的 `TOOL_RUNTIME_SCHEDULER` 是普通 `Symbol`(不是 `Symbol.for`),符号身份按模块实例计算——宿主 `dsh-agent-loop` 拿自己那份的 Symbol 去读由副本挂载出来的 `ToolRuntime`,得到 `undefined`,于是**每一次原生工具调用都抛** `Cannot read properties of undefined (reading 'prepare')`,整个 profile 的工具一起失效。本包的构建、类型检查与测试在这之前全部照绿,所以判据只能由 `test/publish-manifest.test.ts` 落在 manifest 面。
|
|
60
|
-
- 产物里以**值**导入的四枚宿主包是非 optional 的 `peerDependencies`:`@deepseek-ai/schemastery`(宿主 fork 的 schemastery
|
|
78
|
+
- 产物里以**值**导入的四枚宿主包是非 optional 的 `peerDependencies`:`@deepseek-ai/schemastery`(宿主 fork 的 schemastery,`.volatile()` 的解析只在它有实现)、`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-home-paths`、`@deepseek-ai/dsh-atomic-write`。运行期由宿主在场提供,而 profile 不装 peer,所以不产生副本;`build-host.mjs` 对 `dependencies`、`peerDependencies` 与 `optionalDependencies` 三张表一律外部化,`host.js` 里的裸说明符因此一字不变。
|
|
61
79
|
- 这四枚同时另列一份进 `devDependencies`——那才是本包自己的 typecheck、build、test 解析得到它们的方式。其余 `@deepseek-ai/*` 全是 type-only(构建即擦除,`host.js` 里不留说明符),只留在 `devDependencies`。
|
|
62
80
|
- `dependencies` 剩下的确实是本包自己的运行期代码:`@jayyuen66/dsh-plugin-shared` 与 `js-yaml`(后者只被 `scripts/get-cred.mjs` 用)。
|
|
63
81
|
|
|
@@ -66,7 +84,8 @@ allowBuilds:
|
|
|
66
84
|
- 包内 `cordis.patch.yml` 声明 `- id: ocr-review` / `name: "@jayyuen66/dsh-ocr-review"`,由 `package.json` 的 `dsh.bundle.patch` 指向。
|
|
67
85
|
- `dsh plugin --profile web add/remove` 负责登记与摘除,改完重启 dsh。
|
|
68
86
|
- 卡片要 web profile(`dsh.client.platform: web`、`immediately: true`)。
|
|
69
|
-
-
|
|
87
|
+
- 五个 `ocrReview` 方法由 `installOcrRemote` 注册在 apply fiber 上(无条件):它们不依赖任何可选宿主服务,所以宿主没有 web 层(如 TUI)时这五个方法照样在,工具与设置面照常可用。
|
|
88
|
+
- 网关只把方法暴露给**本 profile 的客户端**,来源校验与鉴权是它自己的事,本包因此没有 CSRF token、没有 body 上限,也不再自带路由释放器。
|
|
70
89
|
- 部署默认可写在 profile 注册行的 `config:` 上(cordis 按导出的 `Config` schema 校验并填默认),优先级:设置卡运行时值 > 行 config > 内置默认。
|
|
71
90
|
|
|
72
91
|
## 提供给模型的工具
|
|
@@ -78,7 +97,7 @@ allowBuilds:
|
|
|
78
97
|
| `ocr_scan` | repo、path(最多 100 条)、exclude(50)、batch none/by-language/by-directory、provider/model(scan_cmd 原生共享 flags,与 review 对齐)、同一组数值参数、wait;无需 git diff |
|
|
79
98
|
| `ocr_delegate_preview` / `ocr_delegate_rule` | 前者:repo、scope/commit/from/to、exclude 50、background;后者:repo、paths 必填 1–200 条,经 `--` 分隔的位置参数逐条转义。OCR 端零 LLM 消耗,只回文件清单与规则分组 |
|
|
80
99
|
| `ocr_session` | repo、action list/show/comments(白名单,越界即报错)、id、limit(钳到 1–100,缺省 10);show/comments 缺 id 时回落 list |
|
|
81
|
-
| 后台与回收 | `wait: false`(后台启动、不挂宿主 deadline
|
|
100
|
+
| 后台与回收 | `wait: false`(后台启动、不挂宿主 deadline、默认不设时限、不写 `--output`,结果用 `ocr_session` 轮询)与宿主回收守护只作用于 review/scan 这两条分钟级命令。**那次子进程同时是一枚官方作业**(见下面的「后台作业面」一节),工具回执那份 JSON 在换装那次一字未改(输出契约后来另改为 canonical 对象直返,见下节) |
|
|
82
101
|
|
|
83
102
|
## 设置项
|
|
84
103
|
|
|
@@ -96,43 +115,59 @@ allowBuilds:
|
|
|
96
115
|
| 键 | 默认 | 用途 |
|
|
97
116
|
| -------------------- | --------- | ---------------------------------------------------------------------------------------- |
|
|
98
117
|
| `llmTestTimeoutMs` | `90000` | `ocr llm test` 的墙钟(autoVerify 与卡片「测试连接」共用);评审机慢就调大 |
|
|
99
|
-
| `ocrBackgroundMaxMs` | `
|
|
118
|
+
| `ocrBackgroundMaxMs` | `0` | 后台评审的兜底回收窗口(`jobs.wait` 等到 deadline 即经注册表 `kill`);0 = 不设窗口,只有宿主退出、会话停止、插件卸载或显式 `job_kill` 才结束 |
|
|
100
119
|
| `stdoutMaxBytes` | `400000` | shell 执行器的 stdout 缓冲上限,前台与后台共用同一字段 |
|
|
101
120
|
|
|
121
|
+
第四枚非 volatile 字段是**部署决策**而不是调优值,理由与上面同一类(它改变一场运行对这台机器能做什么,不该放在一张用户可以在审查中途随手翻动的卡片上):
|
|
122
|
+
|
|
123
|
+
| 键 | 默认 | 用途 |
|
|
124
|
+
| ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `ocrNoAutoUpdate` | `false` | 置真时本包起的每条 `ocr` 命令都带 `OCR_NO_UPDATE=1`,拦住上游 npm 包装层在评审中途执行 `npm i -g @alibaba-group/open-code-review@<latest>`;置假保持上游行为(18 分钟冷却 + pid 锁)。版本探测不受本开关影响,恒定带上该变量 |
|
|
126
|
+
|
|
127
|
+
第五枚管的是「给用户看哪条装法命令」,同一个理由让它也不上卡片(一个统一用 brew 或统一用 npm 的部署,不该让每台机器各自在页面上发现这件事):
|
|
128
|
+
|
|
129
|
+
| 键 | 默认 | 用途 |
|
|
130
|
+
| ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `ocrInstallHint` | `both` | 还没装时,工具拒绝、卡片状态行与启用指引先推哪条命令:`both`(两条都给)/ `brew` / `npm`。它只是兜底而非覆盖——机器上已有一份候选时按那份自己的来源说,因为要给的是「替换他手上那一份」的命令:对着 brew 装的用户端出 `npm i -g`,机器上会多出第二份 ocr,而 PATH 上那条还是旧的。npm 那条恒带核对版本,brew 那条恒带不了 |
|
|
132
|
+
|
|
102
133
|
## 凭据与配置文件
|
|
103
134
|
|
|
104
135
|
| 面 | 事实 |
|
|
105
136
|
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
137
|
| config 里的凭据形态 | 写进去的是命令不是明文:POSIX 上 `api_key_cmd` = `node '<本包 scripts/get-cred.mjs 的路径>' '<REF>' '<dsh 数据目录>'`(三段都过单引号转义);Windows 上按双引号转义(ocr 在那边经 `cmd.exe` 跑凭据命令,单引号是普通字符)。第三段是宿主此刻 `resolveDshHome()` 的解析值,HOME 重定向或非 env 指定 dsh home 时 get-cred 的兜底层才不会跑偏;REF 名必须匹配 `^[A-Z0-9_]+$`,否则拒绝构造 |
|
|
138
|
+
| 取值层序 | get-cred 三层的先后**照抄**官方 credentials-local:`$DSH_HOME/.credentials.yaml` 的 `refs.<REF>` > 继承来的进程环境 > `$DSH_HOME/.env`。排错一位不会报错:宿主认「托管文件里那一份才是有效的」,脚本若先交环境里那份,OCR 就打的是另一个 key,而卡片与 OCR 两边都显示成功。dsh 0.2.1-rc.2 把环境层从最高降到了托管文件之后,本包的脚本与 `test/get-cred.test.ts` 同步跟上 |
|
|
107
139
|
| 可达性闸 | `/select` 与 `/migrate` 会拒绝「OCR 运行时取不到」的 key:dsh 起 shell 会剥掉名字命中 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 或 `DSH_` 前缀的环境变量(`scrubbedParentEnv`),get-cred 也刻意不读被审查仓库的 `.env`——只存在于 dsh 进程环境或工作区 `.env` 的 key 在宿主侧解析得到、OCR 运行时却取不到,旧行为是「select 成功 → OCR 401」的假绿。`file`(`$DSH_HOME/.credentials.yaml`)与 `user-env`(`$DSH_HOME/.env`)两层放行;`env` 仅当 ref 名不被净化模式命中才放行;`project-env` 一律不放行。拒绝发生在写盘之前,错误文本写明补救方式(把 key 存入托管凭据或 `$DSH_HOME/.env`) |
|
|
108
140
|
| 写入方式 | 只走原子替换(交官方 `@deepseek-ai/dsh-atomic-write`):同目录随机后缀 `.<hex>.tmp` → rename,新文件权限 0600 由新 inode 携带、父目录 0700;备份 `.bak` 由本包在 rename **之前**复制并 chmod 0600;整段「读→判→写」由官方 `withFileLock` 串行化,RMW 期间同目录会短暂出现 `config.json.lock`(正常路径下 `finally` 删除,持锁进程崩溃时按 pid 探活接管);现有 config 是坏 JSON 或顶层非对象时中止写入,绝不「按空配置覆盖」。**已知不一致**:并发争用超过官方默认 2s 等待上限时,写会以 `atomic-write: timed out waiting for the writer lock at …` 抛出并回 400 JSON,这条文本来自官方件、不走 `lib/messages.ts`(本包其余用户可见错误都在消息表里);换装前 RMW 同处一个 tick、没有超时这条路,是异步化换来的新失败模式,映射它需要给锁等待时间加注入缝并为 2s 超时写测试,本包选择记录而不是静默扩面 |
|
|
109
|
-
| `
|
|
110
|
-
| `scripts/get-cred.mjs` | 由外部 ocr CLI 自己起进程调用,是本包唯一还在读宿主凭据文件的地方。数据目录按「命令行第三参数 > `$DSH_HOME` > `~/.dsh
|
|
141
|
+
| `migrate()` | 把 custom provider 条目里的明文 `api_key` 换成 `api_key_cmd` 并删除明文;key 所在层在 OCR 运行时不可达(与 `select` 同一道闸)或 settings 里查不到 `apiKeyEnv` 的条目原样不动、列进 `skippedUnknown`;一条都没迁移成功时不写盘 |
|
|
142
|
+
| `scripts/get-cred.mjs` | 由外部 ocr CLI 自己起进程调用,是本包唯一还在读宿主凭据文件的地方。数据目录按「命令行第三参数 > `$DSH_HOME` > `~/.dsh`」定位,取值层序就是上面「取值层序」那一行(托管文档 > 继承环境 > `.env`)——注意「继承环境」这一层对凭据 ref 实际常为空:dsh 起 shell 会剥掉名字含 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 或 `DSH_` 前缀的变量。刻意不读被审查仓库的项目 `.env`;三层全落空即非零退出,并把查过的位置一并报出 |
|
|
111
143
|
|
|
112
144
|
## 对外接口
|
|
113
145
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
146
|
+
设置卡的契约面是一枚官方 Typert Remote 命名空间 `ocrReview`(`lib/remote-shape.ts` 的 `OCR_REMOTE_NAMESPACE`),五个方法用程序化方式打标记(`host.ts` 的 `markRemote`)。不再有 `/_dsh/*` 路由:认证、Host / DNS 重绑定 / Origin 判据与请求体上限都由宿主 `/api` 网关承担,本包因此没有 CSRF token、没有自己的 body 上限、没有 405 那一档,也没有路由释放器。
|
|
147
|
+
|
|
148
|
+
| 方法 | 入参 | 成功回执 |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| `providers()` | — | `{providers(每条附 hasKey 布尔), current, getCredReady, source, endpointVerify, ocrBinary}`,不含任何明文 key。`ocrBinary` 是那份纯文件系统的可得性判定再补上版本核对所需的两样:`{source: "path" \| "bundled" \| "none", path?, channel?: "brew" \| "npm" \| "unknown", expectedVersion, measuredVersion: string \| null, installHint}`——`measuredVersion` 是最近一次 `ocr version` 落下的值(装载期自检或按下「检测」),还没测过就是 `null`,读这个方法**不起进程**;`installHint` 是那句装法原文,语言随偏好、命令随来源,都在 host 侧一次组好,卡片不再另写一份(命令写两处就会漂成「卡片教的版本 ≠ 插件核对的版本」)。`endpointVerify` 是 apply 期那次 `ocr llm test` 的结论,没跑过则为 null |
|
|
151
|
+
| `select(request)` | `{provider, model}`,两者必须命中 `describe()` 给的清单 | `{ok, applied, llmTest}`(`autoVerify` 关闭时 `llmTest` 为 null) |
|
|
152
|
+
| `migrate()` | — | `{ok, migrated, skippedUnknown}` |
|
|
153
|
+
| `test()` | — | `{ok, output}`(这里的 `ok` 是**测试结论**,不是这次调用成没成;output 先脱敏再截 8000 字符) |
|
|
154
|
+
| `probe()` | — | `{ok, version, expectedVersion, consistent, output, reason, changeText}`——起一次 `ocr version`(15 秒墙钟,恒定带 `OCR_NO_UPDATE=1`),从它的输出里读版本串,并把读到的值写进本次 apply 的槽位,下一次 `providers()` 与下一条 `ocr_review`/`ocr_scan` 回执说的就是同一份测量。命令没跑成功或读不出版本时 `version` 为 null;`consistent` 是**三态**(`true` / `false` / `null`,null 表示压根没读出版本,此时宣称「不一致」就是把没证据的事说成结论);`reason` 点名失败那一档(找不到命令词、非零退出、被墙钟收走、跑通了但没有版本号——未发布构建就是最后一档);`changeText` 只在真有一条该给的命令时非空——实测到别的版本,或文件系统那边根本没有候选;候选仍在、只是那条命令超时,它是空串;`output` 与 `test` 同样先脱敏再截断,并剥掉终端控制序列后原样保留,好让上游那句升级提示还能被干净地复制 |
|
|
155
|
+
| 被拒时的回执 | — | 写方法答 `{ok:false, error, status}`,其中 `status` 是**保留下来的语义码,不是 HTTP 状态**:应答由网关成框,客户端只渲染 `error`,那个数字的作用是让测试钉住是哪条判据拦的(选择为空或不在清单上是 400,写失败或取凭据脚本缺失是 500) |
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
服务在**构造时**就注册(这是 cordis `Service` 的契约,走 `ctx.reflect.provide`),并随它所属的 fiber 一起撤回——所以 `installOcrRemote` 交回的实例对调用方没有用处,热重载也不可能留下一枚没人撤的旧命名空间:旧路由那套「必须自己调用释放器」的账已经随路由一起没了。客户端那一侧(`src/remote.ts`)持五条 descriptor,`$mount` 只做一次,然后按派生键 `remote.ocrReview` 取;客户端上下文里没有 `remote` 服务时根本不构造读面,卡片退回它的 `remoteUnavailable` 那一行。这些调用**刻意不带客户端墙钟**——超时归网关管,卡片再设一枚就是「写得慢的那次被判成丢了,而它其实落了」。
|
|
123
158
|
|
|
124
159
|
## 后台作业面(官方 `ctx.jobs`)
|
|
125
160
|
|
|
126
|
-
`wait: false` 起的那只外部 `ocr`
|
|
161
|
+
`wait: false` 起的那只外部 `ocr` 子进程同时登记为宿主 `ctx.jobs` 里的一枚**未拥有**作业(`kind: "ocr-review"` ⇒ id `ocr-review-<16 位 hex>`,由注册表按 randomUUID 截短发号,不是递增序号;未拥有 ⇒ 官方那份 `job/registered` / `job/settled` 会话生命周期记录也不写,它只给有 owner 的作业记)。改的是"谁能看见并停掉它",不是"模型拿到什么":
|
|
127
162
|
|
|
128
163
|
| 面 | 事实 |
|
|
129
164
|
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
130
165
|
| 模型的观察面 | 多了三位:官方 `job_list` 列得出这次评审、`job_output` 读得到它的 stdout/stderr(注册表按自己的节奏从 `proc.observed` 拉,非消费式,与 `readOutput()` 游标互不争夺 — 官方 d.ts 明写 "without stealing bytes from `readOutput`")、`job_kill` 停得掉它。评审结果本身仍以 `ocr_session` 会话记录为准;工具回执的内容自换装起未变,其后随输出契约改造改为 canonical 对象直返(不再把回执 stringify 进 `{ text }` 双重编码) |
|
|
131
166
|
| 坏掉的执行器读者 | 官方泵对"源抛错"的答法是只往宿主日志写一行、此后这一路读作空(实测)⇒ 本包额外写**一条**"这段不完整"进环,只写一次、也不再去敲坏掉的读者 |
|
|
132
167
|
| controller 寿命 | 只挂在 `start` 那一瞬:官方件在"没有 controller 服务这个 owner"时拒绝 `start`,而 web 面宿主的 `tool-jobs` 在各 preset realm 内(宿主平面那一行被 disabled)⇒ 未拥有作业必须自己挂一枚。挂完立刻摘(实测:摘掉即恢复拒绝、后续 read/kill/落定都不依赖它)——常驻等于在"宿主故意没装 job 工具"的组成里替全宿主开着那道闸门 |
|
|
133
|
-
|
|
|
134
|
-
| 取消链里不许抛 |
|
|
135
|
-
|
|
|
168
|
+
| 三条回收路径 | 可选的兜底窗口、插件卸载、会话 dispose 都是 `jobs.kill(id, undefined, reason)` 而不是绕过它杀进程 ⇒ 记录推到 `stopping`、reason 落进 `detail`(实测绕过那一层,名册里这条会永远停在 `running`)。三条可以叠加(实测对 `stopping` 再 kill 仍回 `requested`、`proc.kill()` 幂等)。reason 是模型可见文本 ⇒ 取双语字典 |
|
|
169
|
+
| 取消链里不许抛 | 宿主事件处理器里抛出的错误由 Node 以 `process.nextTick` 重抛 ⇒ 未捕获异常 ⇒ **整个宿主进程退出**(本机实跑复现:`Error: unknown job ...` + `EXIT=1`)。官方件也不替生产者兜:`killJob` 裸调 `job.cancel()`(dsh-jobs-local 的 `killJob`,只有 teardown 那一臂逐条 try/catch),注册表被重载/关掉时对陌生 id 抛(同文件 `expect` 那一处 throw)。于是两层各兜一次:`cancel` 经 `killProc` 吞掉"进程早就不在了"那一类,`stop` 兜住注册表那一臂,兜不住时退化成直接杀我们自己的进程(各有一条用例,删掉任一层那条就红) |
|
|
170
|
+
| 可选兜底窗口 | 本包仍不给外部 CLI 套宿主 `timeoutMs`(长评审会被误砍),但"完全没有终点"在换装后有了新受害者:未拥有的 10 格桶是**全宿主共用**的(实测 `running` 与 `stopping` 都计数、满员直接拒绝新 `start`)⇒ 一条挂死的 CLI 会永久占一格。收法用注册表自己的 `wait(id, ms)` + `kill`,不用宿主 timer 也不裸 `setTimeout` |
|
|
136
171
|
| 未拥有的暴露面 | 官方件按 owner 隔离,未拥有作业任何 caller 都能 `list`/`read`/`kill`。这是本波已知并接受的暴露面(受有作业要 `dsh-agent` 注册表,实测不可得);回滚这次换装是唯一的收回方式 |
|
|
137
172
|
| 落定记录窗口 | 注册表不会自行回收已落定的记录,而它随宿主活着、不随本插件卸载而死 ⇒ 本包在每次登记前剪掉 `ocr-review` 这一 kind 最老的已落定记录,剪到 9 条再起新的 ⇒ 在册任何时刻都不超过 10 条(每条最多带一只 256 KiB 的环)。卸载时两件事都做:活着的经注册表 kill,已落定的 remove —— 用户直接禁用本包就没有"下一次 apply"来剪窗口了。在跑与正在收的一条都不被剪 —— 官方件对超额的答法是**拒绝新 start**(实测未拥有桶 10),不是替我们杀评审 |
|
|
138
173
|
| 没注册表没后台 | 宿主没装 `dsh-jobs-local` 时,`wait: false` 直接抛出点名缺件的错误,**不会**先把子进程起在没人看得见的地方。前台(`wait: true`)不受影响 |
|
|
@@ -141,16 +176,16 @@ allowBuilds:
|
|
|
141
176
|
|
|
142
177
|
| 面 | 事实 |
|
|
143
178
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
-
| 进程边界 | 被审查仓库的路径与 diff 内容进 `ocr` 子进程,OCR 再把它发给自己在 config.json 里配置的 LLM 端点;这条外发是审查功能本身,不经 dsh 宿主。明文 key
|
|
179
|
+
| 进程边界 | 被审查仓库的路径与 diff 内容进 `ocr` 子进程,OCR 再把它发给自己在 config.json 里配置的 LLM 端点;这条外发是审查功能本身,不经 dsh 宿主。明文 key 不进工具返回、也不进 `ocrReview` 的应答,宿主侧 `resolve()` 拿到的值只用于确认「现在解析得到」,当场丢弃 |
|
|
145
180
|
| 原始输出脱敏 | OCR 的原始 stdout/stderr(失败详情、llm test、解析失败回显、session 透传)出宿主前统一过 `redactKeyMaterial()`:Bearer 与赋值形态从严抹除,独立 key 形态额外要求随机串特征,以免把要交给模型的文件路径一起抹掉 |
|
|
146
|
-
| 写盘范围 | 本包写盘只有两处:外部 ocr CLI 的 `config.json`(含同目录随机后缀 `.tmp`、备份 `.bak`,以及 RMW 期间短暂存在、正常路径下即删的 `config.json.lock`)与系统临时目录下的 `ocr-review-*`/`ocr-scan-*`(读完即删)。迁移前的明文可能仍留在同一次写入产生的 `.bak` 里(权限 0600
|
|
181
|
+
| 写盘范围 | 本包写盘只有两处:外部 ocr CLI 的 `config.json`(含同目录随机后缀 `.tmp`、备份 `.bak`,以及 RMW 期间短暂存在、正常路径下即删的 `config.json.lock`)与系统临时目录下的 `ocr-review-*`/`ocr-scan-*`(读完即删)。迁移前的明文可能仍留在同一次写入产生的 `.bak` 里(权限 0600),要彻底清掉需自行删除该备份。这两处之外仍可能被改动的是 **CLI 自己那层 npm 包装**:`ocrNoAutoUpdate` 没置真时,一次 `ocr` 调用会触发上游把新版全局 `npm i -g` 装上。本包自己从不执行安装动作,版本探测那条恒定拦住这条路 |
|
|
147
182
|
|
|
148
183
|
## 常见问题
|
|
149
184
|
|
|
150
185
|
| 现象 | 结论 |
|
|
151
186
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
152
187
|
| exit 0 就以为评审成功 | exit 0 不等于评审成功:`status` 不在 success/complete/**partial**/completed_with_warnings/completed_with_errors/skipped 白名单内即按失败报(`{"error": …}` 属这类;partial 是 manifest 终态之一,部分文件失败但整体有覆盖,评论仍有效),解析失败时回显脱敏后的原文前 500 码元(UTF-16,一张 emoji 占 2 枚) |
|
|
153
|
-
| 摘要是否覆盖了全部产出 |
|
|
188
|
+
| 摘要是否覆盖了全部产出 | 两件不同的事,不能互相替代:`droppedCount` 与 `invalidCommentCount` 只说明**本摘要**有没有列全 OCR 产出的评论(否则调大 `maxComments` 或置 0 再复核)。**这一场**有没有把选中的文件审完看 `coverage`(`selected` 是分母,`completed`/`reused`/`failed`/`waived` 是它的互斥划分,`failedItems` 给出文件名与分类、原因)和 `budgetExceeded`(token 预算提前收工,`status` 里仍只写 `partial`),模型工具调用的失败数看 `toolCalls`。stdout 侧另有 400000 字节上限,所以评审结果统一走 `--output` 临时文件 |
|
|
154
189
|
| dsh 退出时的后台进程 | dsh 以任何方式退出(含 kill -9 后被 launchd 收养)时,review/scan 的 OCR 进程树由命令内守护 TERM→1s→KILL 收走;取消工具调用即终止对应的后台进程 |
|
|
155
190
|
|
|
156
191
|
## 开发
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
## 结论:随包分发的第三方内容只有一处
|
|
7
7
|
|
|
8
8
|
`host.ts`、`lib/`、`src/`、`scripts/` 下全部是本仓原创代码,不含任何第三方源码或文案。
|
|
9
|
-
`@alibaba-group/open-code-review`(外部 `ocr` CLI)与 `js-yaml`
|
|
10
|
-
|
|
9
|
+
`@alibaba-group/open-code-review`(外部 `ocr` CLI)与 `js-yaml` 都是**运行期才涉及的东西**——
|
|
10
|
+
本包通过它们的公开入口调用它们,许可文本随各自的包走,本文件不重复其文本;见下面的运行期依赖表。
|
|
11
|
+
外部 CLI 尤其如此:它**不在本包的依赖表里**,由用户自己装(brew formula 或 npm 全局包),
|
|
12
|
+
本包只 spawn 它的可执行体。
|
|
11
13
|
|
|
12
14
|
唯一随包分发的第三方内容是**图标**(`icon.svg`),见下节。
|
|
13
15
|
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
- 上游项目:`open-code-review`
|
|
29
31
|
- 上游仓库:https://github.com/alibaba/open-code-review
|
|
30
32
|
- 上游许可:Apache-2.0,Copyright 2026 Alibaba
|
|
31
|
-
- 验证版本:1.
|
|
33
|
+
- 验证版本:1.13.0(声明在 package.json 的 `ocrCliVersion`,代码里的镜像常量与两侧 README 由门禁对齐同一枚数字)
|
|
32
34
|
|
|
33
35
|
本包**使用**而非**包含**以下接口(`host.ts` / `lib/cli.ts` 的头注释按该版本源码逐条核对):
|
|
34
36
|
|
|
@@ -40,9 +42,9 @@
|
|
|
40
42
|
| `ocr llm test` | select 后的连通性验证 |
|
|
41
43
|
| `config.json` 的 `custom_providers.<name>.{url,protocol,model,api_key_cmd}` | provider/model 应用面;key 经 `api_key_cmd` 动态读取,绝不落明文 |
|
|
42
44
|
|
|
43
|
-
这些都是 CLI 的外层面,但不是任何文档承诺过的稳定 API
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
这些都是 CLI 的外层面,但不是任何文档承诺过的稳定 API——本包把契约钉在上面那一版正是为了
|
|
46
|
+
守住这一组接口;上游变更时以本包的契约闸测试(`test/cli.test.ts`、`test/parse.test.ts`)为
|
|
47
|
+
首道防线,版本对不上时卡片、徽标与工具回执都会说(见 README 的「External CLI status and version」)。
|
|
46
48
|
|
|
47
49
|
## 运行期依赖
|
|
48
50
|
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
|
|
51
53
|
| 依赖 | 版本 | 许可 | 引入方 |
|
|
52
54
|
| --- | --- | --- | --- |
|
|
53
|
-
| `@alibaba-group/open-code-review` |
|
|
55
|
+
| `@alibaba-group/open-code-review` | 1.13.0(核对版本,非依赖) | Apache-2.0(Alibaba) | **不在依赖表里**:用户自选 `brew install open-code-review` 或 `npm i -g @alibaba-group/open-code-review@<版本>`,本包只 spawn 它 |
|
|
54
56
|
| `js-yaml` | ^5.4.3 | MIT | `scripts/get-cred.mjs` 读 dsh 托管凭据文档 |
|
|
55
57
|
|
|
56
|
-
dsh
|
|
58
|
+
dsh 宿主侧的包分两档:产物里以**值**导入、且列在 `peerDependencies` 的四枚(`@deepseek-ai/schemastery` 宿主 fork、`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-home-paths`、`@deepseek-ai/dsh-atomic-write`)由宿主在场提供、不随本包分发;其余 `@deepseek-ai/dsh-*` 与 `@deepseek-ai/cordis` 只在 `devDependencies` 里,用途是把类型绑到官方声明上(type-only 绑定),产物里不留说明符。本包按 dsh `0.2.1-alpha.3` 逐条核对过(同版的 cordis `4.0.5-alpha.2` 与 schemastery `3.18.5-alpha.2` fork 一并核对)。
|