@jayyuen66/dsh-ocr-review 0.1.4-rc → 0.1.4

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 CHANGED
@@ -13,14 +13,14 @@ English · [中文](README.zh-CN.md)
13
13
  ## Prerequisites (external CLI)
14
14
 
15
15
  - This package imports no OCR library, it only spawns processes: `ocr review` / `ocr scan` / `ocr delegate preview` / `ocr delegate rule` / `ocr session` / `ocr llm test`.
16
- - Commands are built against the contract verified line-by-line on the v1.12.x source (header comments of `lib/cli.ts` and `lib/parse.ts`), shaped like `ocr review --audience agent --format json --effort medium --output <temp file>`.
16
+ - Commands are built against the contract verified line-by-line on the v1.12.13 source (header comments of `lib/cli.ts` and `lib/parse.ts`; the `optionalDependencies` pin is `^1.12.13`), shaped like `ocr review --audience agent --format json --effort medium --output <temp file>`.
17
17
  - Multi-value `--exclude`/`--path` are merged into one comma-separated flag, and `--format` only accepts json/text/sarif.
18
- - Command resolution order: the launcher bundled with the package first - `@alibaba-group/open-code-review` (an `optionalDependencies` entry, whose own optionalDependencies pick the platform binary) resolved to the absolute path of its `bin/ocr.js` - and only when that does not resolve does it fall back to a bare `ocr` on PATH. Both installation shapes keep working, so installing this plugin is enough to get a working tool without a separate brew / npm i -g step. Resolution is lazy and memoized once per process (the same shape dsh core uses to resolve `@vscode/ripgrep`); a failed resolve never throws at load time, which would take the whole plugin offline over one optional binary.
18
+ - Command resolution order: the launcher bundled with the package first - `@alibaba-group/open-code-review` (an `optionalDependencies` entry, whose own optionalDependencies pick the platform binary) resolved to the absolute path of its `bin/ocr.js` - and only when that does not resolve does it fall back to a bare `ocr` on PATH. Both installation shapes keep working, so installing this plugin is enough to get a working tool without a separate brew / npm i -g step. Resolution is lazy and memoized once per process (the same shape dsh core uses to resolve `@vscode/ripgrep`); a failed resolve never throws at load time, which would take the whole plugin offline over one optional binary. On Windows the bundled launcher is skipped outright (`bin/ocr.js` is a shebang script that pwsh/cmd cannot execute), so the bare `ocr` on PATH is the only supported shape there.
19
19
  - With neither path available the child exits 127 and the tool reports "ocr command not found", offering `brew install open-code-review` or `npm i -g @alibaba-group/open-code-review`.
20
- - Platforms: macOS and Linux are the targets. Process reaping (the reaper guard around `ocr_review` / `ocr_scan`) depends on a POSIX toolchain - bash traps and job control plus the process-group semantics of `ps` / `pgrep` / `kill`. None of that exists on Windows, so the package runs the bare command there (guarded by `process.platform`): no reaping, but it still works. The cost is that a hard host exit can leave an orphaned OCR process.
20
+ - Platforms: macOS and Linux are the targets. Process reaping (the reaper guard around `ocr_review` / `ocr_scan`) depends on a POSIX toolchain - bash traps and job control plus the process-group semantics of `ps` / `pgrep` / `kill` - and the `HOME=` assignment prefix behind a custom `ocrConfigPath` is POSIX-only too. None of that exists on Windows: the package runs the bare command there (guarded by `process.platform`), skips the bundled launcher (a bare `ocr` on PATH is required), and writes the `api_key_cmd` with double-quote escaping because ocr runs credential commands through `cmd.exe` on Windows. The cost is that a hard host exit can leave an orphaned OCR process on Windows.
21
21
  - The provider list and the key status come from the official host channels `ctx.settings.describe()` (the `value.providers` of the `llm-pi-ai` entry) and `ctx.credentials`.
22
22
  - When either is missing the card shows the degradation reason while tools and settings keep working.
23
- - The host must be `>= 0.2.1-alpha.1`: declared in `peerDependencies` (the host checks it on plugin install from 0.1.7-rc; alpha.1 has no such gate yet) and in `engines.dsh` (same value, read by nothing).
23
+ - The host must be `>= 0.2.1-alpha.2`: declared in `peerDependencies` and in `engines.dsh` (same value, read by nothing).
24
24
 
25
25
  ## Installation
26
26
 
@@ -29,9 +29,9 @@ dsh plugin --profile web add @jayyuen66/dsh-ocr-review
29
29
  ```
30
30
 
31
31
  - The packages are on the public npm registry, so installation needs no credentials.
32
- - The published files are only `host.js`, `client.js`, `cordis.patch.yml` and `scripts` (`prepack` rebuilds both bundles); source repository: see `repository.url` in package.json.
32
+ - The published files are only `host.js`, `client.js`, `cordis.patch.yml`, `scripts` and `THIRD_PARTY_NOTICES.md` (third-party licenses of the bundled runtime dependencies; `prepack` rebuilds both bundles), source repository: see `repository.url` in package.json.
33
33
  - No `@deepseek-ai/*` goes in `dependencies`, and that is not a style preference. Consumers of this package are dsh plugins installed into a profile (`nodeLinker: hoisted`, `autoInstallPeers: false`), so any host package listed under `dependencies` is written into that profile as a **real directory**, coexisting with the copy the host process already loaded. `@deepseek-ai/dsh-tools` keys its `TOOL_RUNTIME_SCHEDULER` with a plain `Symbol` (not `Symbol.for`), so symbol identity is per module instance: the host's `dsh-agent-loop` reads a `ToolRuntime` mounted from the duplicate using its own copy's symbol, gets `undefined`, and **every native tool call throws** `Cannot read properties of undefined (reading 'prepare')` — the whole profile loses its tools. This package's build, typecheck and tests all stay green right up to that point, so `test/publish-manifest.test.ts` pins the invariant in the manifest instead.
34
- - The four host packages this bundle imports as **values** are required (non-optional) `peerDependencies`: `@deepseek-ai/schemastery` (the host's fork of schemastery — only it implements the 0.1.7 `.volatile()` resolution), `@deepseek-ai/dsh-credentials`, `@deepseek-ai/dsh-home-paths` and `@deepseek-ai/dsh-atomic-write`. The host provides them at runtime, and a profile installs no peers, so no duplicate appears. `build-host.mjs` externalizes `dependencies` *and* `peerDependencies`, so `host.js` keeps the bare specifiers unchanged.
34
+ - The four host packages this bundle imports as **values** are required (non-optional) `peerDependencies`: `@deepseek-ai/schemastery` (the host's fork of schemastery — only it implements the 0.1.7 `.volatile()` resolution), `@deepseek-ai/dsh-credentials`, `@deepseek-ai/dsh-home-paths` and `@deepseek-ai/dsh-atomic-write`. The host provides them at runtime, and a profile installs no peers, so no duplicate appears. `build-host.mjs` externalizes `dependencies`, `peerDependencies` *and* `optionalDependencies`, so `host.js` keeps the bare specifiers unchanged.
35
35
  - Those four are repeated in `devDependencies` — that is what lets this repository's own typecheck, build and tests resolve them. Every other `@deepseek-ai/*` is type-only (erased at build, no specifier left in `host.js`) and stays in `devDependencies` alone.
36
36
  - What is left in `dependencies` is genuinely this package's own runtime code: `@jayyuen66/dsh-plugin-shared` and `js-yaml` (the last one used only by `scripts/get-cred.mjs`).
37
37
 
@@ -105,10 +105,11 @@ Three deployment-level tuning keys are deliberately **not** on the settings card
105
105
 
106
106
  | Aspect | Fact |
107
107
  | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108
- | Credential form in the config | What lands in the config is a command, not a secret: `api_key_cmd` = `node '<path of scripts/get-cred.mjs in this package>' '<REF>' '<dsh data dir>'` (all three parts single-quote escaped; the third is the host's current `resolveDshHome()` result, so get-cred's fallback layers stay correct when HOME is redirected or the dsh home is set outside the env), and the REF name must match `^[A-Z0-9_]+$` or the command is refused |
109
- | How writes happen | Atomic replacements via the official `@deepseek-ai/dsh-atomic-write`: a random-suffixed `.<hex>.tmp` sibling created with `wx`, then rename, with the new inode carrying 0600 (parent directories 0700). The `.bak` backup is copied by this package **before** the rename and chmod-ed to 0600 (the official helper has no backup semantics). The whole read→decide→write span is serialized by the official `withFileLock`, so a short-lived `config.json.lock` appears in that directory during a write and is removed in `finally`; a lock left by a crashed holder is taken over via a pid liveness probe. **Known inconsistency**: contention beyond the official default 2s wait makes the write throw `atomic-write: timed out waiting for the writer lock at …`, and the endpoint answers 400 with that English text verbatim — it comes from the official package and bypasses `lib/messages.ts`, unlike every other user-visible error here. Before this swap the read-modify-write shared one event-loop tick, so this failure mode did not exist; mapping it would need an injectable wait budget plus a timeout test, so this package documents it instead of silently widening the surface. An existing config that is invalid JSON or whose top level is not an object aborts the write instead of "treating it as empty and overwriting" |
110
- | `POST migrate` | Replaces the plaintext `api_key` of custom provider entries with `api_key_cmd` and deletes the plaintext; entries whose `apiKeyEnv` is not found in settings are left untouched and listed in `skippedUnknown`; nothing is written when nothing was migrated |
111
- | `scripts/get-cred.mjs` | Spawned by the external ocr CLI itself and the only place in this package still reading host credential files. The data directory is resolved as explicit third argument > `$DSH_HOME` > `~/.dsh`, and values are looked up in inherited process environment > `refs.<REF>` of `<dsh data dir>/.credentials.yaml` > `<dsh data dir>/.env`. The project `.env` of the reviewed repository is deliberately not read; when all three layers miss it exits non-zero and names every location it checked |
108
+ | Credential form in the config | What lands in the config is a command, not a secret: `api_key_cmd` = `node '<path of scripts/get-cred.mjs in this package>' '<REF>' '<dsh data dir>'` (POSIX, single-quote escaped) or `node "<path>" "<REF>" "<dsh data dir>"` (Windows, double-quote escaped - ocr runs credential commands through `cmd.exe` there); the third part is the host's current `resolveDshHome()` result, so get-cred's fallback layers stay correct when HOME is redirected or the dsh home is set outside the env, and the REF name must match `^[A-Z0-9_]+$` or the command is refused |
109
+ | Reachability gate | `/select` and `/migrate` refuse a key whose credential source OCR cannot reach at runtime: dsh strips variables named `KEY`/`PASSWORD`/`SECRET`/`TOKEN` or prefixed `DSH_` from spawned shells (`scrubbedParentEnv`), and get-cred deliberately does not read the reviewed repository's `.env`. Keys that only live in the dsh process environment or the workspace `.env` therefore resolve on the host side yet fail at OCR runtime - previously a silent "select succeeded → OCR 401". Sources `file` (`$DSH_HOME/.credentials.yaml`) and `user-env` (`$DSH_HOME/.env`) pass; `env` passes only when the ref name matches none of the scrubbed patterns; `project-env` never passes. The refusal happens before any write, with the fix named in the error text (store the key in the managed credentials file or `$DSH_HOME/.env`) |
110
+ | How writes happen | Atomic replacements via the official `@deepseek-ai/dsh-atomic-write`: a random-suffixed `.<hex>.tmp` sibling created with `wx`, then rename, with the new inode carrying 0600 (parent directories 0700). The `.bak` backup is copied by this package **before** the rename and chmod-ed to 0600 (the official helper has no backup semantics). The whole read→decide→write span is serialized by the official `withFileLock`, so a short-lived `config.json.lock` appears in that directory during a write and is removed in `finally`; a lock left by a crashed holder is taken over via a pid liveness probe. **Known inconsistency**: contention beyond the official default 2s wait makes the write throw `atomic-write: timed out waiting for the writer lock at …`, and the endpoint answers 400 with that English text verbatim — it comes from the official package and bypasses `lib/messages.ts`, unlike every other user-visible error here. Before this swap the read-modify-write shared one event-loop tick, so this failure mode did not exist; mapping it would need an injectable wait budget plus a timeout test, so this package documents it instead of silently widening the surface. An existing config that is invalid JSON or whose top level is not an object aborts the write instead of "treating it as empty and overwriting" |
111
+ | `POST migrate` | Replaces the plaintext `api_key` of custom provider entries with `api_key_cmd` and deletes the plaintext; entries whose key source is unreachable at OCR runtime (same gate as `/select`) or whose `apiKeyEnv` is not found in settings are left untouched and listed in `skippedUnknown`; nothing is written when nothing was migrated |
112
+ | `scripts/get-cred.mjs` | Spawned by the external ocr CLI itself and the only place in this package still reading host credential files. The data directory is resolved as explicit third argument > `$DSH_HOME` > `~/.dsh`, and values are looked up in inherited process environment > `refs.<REF>` of `<dsh data dir>/.credentials.yaml` > `<dsh data dir>/.env` - note the inherited layer is usually empty for credential refs, because dsh scrubs `KEY`/`PASSWORD`/`SECRET`/`TOKEN`- and `DSH_*`-named variables out of spawned shells. The project `.env` of the reviewed repository is deliberately not read; when all three layers miss it exits non-zero and names every location it checked |
112
113
 
113
114
  ## Public endpoints
114
115
 
package/README.zh-CN.md CHANGED
@@ -12,14 +12,14 @@
12
12
  ## 前置条件(外部 CLI)
13
13
 
14
14
  - 本包不 import 任何 OCR 库,只起进程:`ocr review` / `ocr scan` / `ocr delegate preview` / `ocr delegate rule` / `ocr session` / `ocr llm test`。
15
- - 命令按 v1.12.x 源码逐一核对(`lib/cli.ts`、`lib/parse.ts` 头注释),形如 `ocr review --audience agent --format json --effort medium --output <临时文件>`。
15
+ - 命令按 v1.12.13 源码逐一核对(`lib/cli.ts`、`lib/parse.ts` 头注释;`optionalDependencies` 钉 `^1.12.13`),形如 `ocr review --audience agent --format json --effort medium --output <临时文件>`。
16
16
  - 多值 `--exclude`/`--path` 合并成一个逗号分隔 flag,`--format` 只认 json/text/sarif。
17
- - 命令解析优先级:先解析**随包装上**的 `@alibaba-group/open-code-review`(`optionalDependencies`,平台二进制由它自己的 optionalDependencies 选一),取其 `bin/ocr.js` 绝对路径;解析不到才回落到 PATH 上的裸 `ocr`——两种装法都继续支持,装上本插件即工具可用,不必用户自己再 brew/npm i -g 一遍。解析是惰性的且每进程只做一次(与 dsh 核心解析 `@vscode/ripgrep` 同款),解析失败不在装载期抛,否则整包会因一个可选二进制而下线。
17
+ - 命令解析优先级:先解析**随包装上**的 `@alibaba-group/open-code-review`(`optionalDependencies`,平台二进制由它自己的 optionalDependencies 选一),取其 `bin/ocr.js` 绝对路径;解析不到才回落到 PATH 上的裸 `ocr`——两种装法都继续支持,装上本插件即工具可用,不必用户自己再 brew/npm i -g 一遍。解析是惰性的且每进程只做一次(与 dsh 核心解析 `@vscode/ripgrep` 同款),解析失败不在装载期抛,否则整包会因一个可选二进制而下线。Windows 上随包 launcher 整体跳过(`bin/ocr.js` 是 shebang 脚本,pwsh/cmd 无法直执行),PATH 上的全局 `ocr` 是 Windows 唯一受支持的装法。
18
18
  - 两条路径都没有时子进程 exit 127,工具报「ocr 命令不存在」并给出 `brew install open-code-review` 或 `npm i -g @alibaba-group/open-code-review`。
19
- - 平台:目标平台是 macOS / Linux。进程收割(`ocr_review` / `ocr_scan` 的 reaper 守护)依赖 POSIX 工具链——bash 的 trap 与作业控制,加上 `ps` / `pgrep` / `kill` 的进程组语义。Windows 上这层整体缺席,本包按 `process.platform` 直接放行裸命令(不收割但能跑);代价是宿主硬退出后 OCR 可能留下孤儿进程。
19
+ - 平台:目标平台是 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
20
  - provider 清单与 key 状态走宿主官方通道 `ctx.settings.describe()`(`llm-pi-ai` 那条的 `value.providers`)与 `ctx.credentials`。
21
21
  - 两者缺席时卡片显示降级原因,工具与设置照常可用。
22
- - 宿主版本要求 `>= 0.2.1-alpha.1`:写在 `peerDependencies`(0.1.7-rc 起宿主装插件时校验它;alpha.1 还没有这道门)与 `engines.dsh`(同值、无人读)。
22
+ - 宿主版本要求 `>= 0.2.1-alpha.2`:写在 `peerDependencies` 与 `engines.dsh`(同值、无人读)。
23
23
 
24
24
  ## 安装
25
25
 
@@ -28,7 +28,7 @@ dsh plugin --profile web add @jayyuen66/dsh-ocr-review
28
28
  ```
29
29
 
30
30
  - 包在公共 npm 上,安装不需要凭据。
31
- - 发布面只含 `host.js`、`client.js`、`cordis.patch.yml`、`scripts`(`prepack` 重建两个 bundle),源码仓见 package.json 的 `repository.url`。
31
+ - 发布面只含 `host.js`、`client.js`、`cordis.patch.yml`、`scripts` 与 `THIRD_PARTY_NOTICES.md`(随包运行时依赖的第三方许可证;`prepack` 重建两个 bundle),源码仓见 package.json 的 `repository.url`。
32
32
 
33
33
  ## 安装时 pnpm 拦下依赖脚本(ERR_PNPM_IGNORED_BUILDS)
34
34
 
@@ -57,7 +57,7 @@ allowBuilds:
57
57
 
58
58
  如果你更希望完全不执行安装期脚本,可以写 `'@alibaba-group/open-code-review': false`:`ocr` 同样可用(launcher 直接从平台包目录取二进制),但宿主 `readPendingBuilds()` 只认值为 `set this to true or false` 的条目,改成 `false` 后官方审批流程就不再能代为授权,只能手工维护。
59
59
  - `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,0.1.7 的 `.volatile()` 解析只在它有实现)、`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-home-paths`、`@deepseek-ai/dsh-atomic-write`。运行期由宿主在场提供,而 profile 不装 peer,所以不产生副本;`build-host.mjs` 对 `dependencies` 与 `peerDependencies` 两张表一律外部化,`host.js` 里的裸说明符因此一字不变。
60
+ - 产物里以**值**导入的四枚宿主包是非 optional 的 `peerDependencies`:`@deepseek-ai/schemastery`(宿主 fork 的 schemastery,0.1.7 的 `.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
61
  - 这四枚同时另列一份进 `devDependencies`——那才是本包自己的 typecheck、build、test 解析得到它们的方式。其余 `@deepseek-ai/*` 全是 type-only(构建即擦除,`host.js` 里不留说明符),只留在 `devDependencies`。
62
62
  - `dependencies` 剩下的确实是本包自己的运行期代码:`@jayyuen66/dsh-plugin-shared` 与 `js-yaml`(后者只被 `scripts/get-cred.mjs` 用)。
63
63
 
@@ -103,10 +103,11 @@ allowBuilds:
103
103
 
104
104
  | 面 | 事实 |
105
105
  | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
106
- | config 里的凭据形态 | 写进去的是命令不是明文:`api_key_cmd` = `node '<本包 scripts/get-cred.mjs 的路径>' '<REF>' '<dsh 数据目录>'`(三段都过单引号转义;第三段是宿主此刻 `resolveDshHome()` 的解析值,HOME 重定向或非 env 指定 dsh home 时 get-cred 的兜底层才不会跑偏),REF 名必须匹配 `^[A-Z0-9_]+$`,否则拒绝构造 |
106
+ | 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_]+$`,否则拒绝构造 |
107
+ | 可达性闸 | `/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`) |
107
108
  | 写入方式 | 只走原子替换(交官方 `@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 超时写测试,本包选择记录而不是静默扩面 |
108
- | `POST migrate` | 把 custom provider 条目里的明文 `api_key` 换成 `api_key_cmd` 并删除明文;settings 里查不到 `apiKeyEnv` 的条目原样不动、列进 `skippedUnknown`;一条都没迁移成功时不写盘 |
109
- | `scripts/get-cred.mjs` | 由外部 ocr CLI 自己起进程调用,是本包唯一还在读宿主凭据文件的地方。数据目录按「命令行第三参数 > `$DSH_HOME` > `~/.dsh`」定位,取值层序为继承来的进程环境变量 > `<dsh 数据目录>/.credentials.yaml` 的 `refs.<REF>` > `<dsh 数据目录>/.env`。刻意不读被审查仓库的项目 `.env`;三层全落空即非零退出,并把查过的位置一并报出 |
109
+ | `POST migrate` | 把 custom provider 条目里的明文 `api_key` 换成 `api_key_cmd` 并删除明文;key 所在层在 OCR 运行时不可达(与 `/select` 同一道闸)或 settings 里查不到 `apiKeyEnv` 的条目原样不动、列进 `skippedUnknown`;一条都没迁移成功时不写盘 |
110
+ | `scripts/get-cred.mjs` | 由外部 ocr CLI 自己起进程调用,是本包唯一还在读宿主凭据文件的地方。数据目录按「命令行第三参数 > `$DSH_HOME` > `~/.dsh`」定位,取值层序为继承来的进程环境变量 > `<dsh 数据目录>/.credentials.yaml` 的 `refs.<REF>` > `<dsh 数据目录>/.env`——注意「继承环境」这一层对凭据 ref 实际常为空:dsh 起 shell 会剥掉名字含 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 或 `DSH_` 前缀的变量。刻意不读被审查仓库的项目 `.env`;三层全落空即非零退出,并把查过的位置一并报出 |
110
111
 
111
112
  ## 对外接口
112
113
 
@@ -0,0 +1,56 @@
1
+ # Third-party notices
2
+
3
+ 本包自身编写的代码以 MIT 发布,见 [`LICENSE`](./LICENSE)。本文件记录**与第三方的边界**:
4
+ 哪一部分来自第三方、在什么意义上「来自」,以及本包自身代码与第三方代码的分界在哪里。
5
+
6
+ ## 结论:随包分发的第三方内容只有一处
7
+
8
+ `host.ts`、`lib/`、`src/`、`scripts/` 下全部是本仓原创代码,不含任何第三方源码或文案。
9
+ `@alibaba-group/open-code-review`(外部 `ocr` CLI)与 `js-yaml` 都是**运行期依赖**——本包
10
+ 通过它们的公开入口调用它们,许可文本随各自的包走,本文件不重复其文本;见下面的运行期依赖表。
11
+
12
+ 唯一随包分发的第三方内容是**图标**(`icon.svg`),见下节。
13
+
14
+ ## 图标:Tabler Icons
15
+
16
+ - 上游项目:`Tabler Icons`
17
+ - 上游仓库:https://github.com/tabler/tabler-icons
18
+ - 上游许可:MIT,Copyright (c) Paweł Kuna
19
+ - 取用图标:`code`(https://icones.js.org/icons/tabler/code)
20
+
21
+ `icon.svg` 的形状取自该图标的**两枚尖括号**:坐标整体平移(+1, −1)并去掉中间的斜杠笔画,
22
+ 另加一枚对勾段(形态对齐 Tabler `check` 的笔画风格)。改动都不引入新的第三方形状:把
23
+ `currentColor` 换成本包插件图标配色(`#658EFF` 主色 + `#54ECE7` 底衬),并把外层 `<g>` 与
24
+ 画布尺寸对齐本族插件统一的 `36×36` / `viewBox 0 0 24 24` 规格。
25
+
26
+ ## 机制与接口来源:open-code-review(ocr CLI)
27
+
28
+ - 上游项目:`open-code-review`
29
+ - 上游仓库:https://github.com/alibaba/open-code-review
30
+ - 上游许可:Apache-2.0,Copyright 2026 Alibaba
31
+ - 验证版本:1.12.13(`optionalDependencies` 钉 `^1.12.13`)
32
+
33
+ 本包**使用**而非**包含**以下接口(`host.ts` / `lib/cli.ts` 的头注释按该版本源码逐条核对):
34
+
35
+ | 接口 | 用途 |
36
+ | --- | --- |
37
+ | `ocr review` / `ocr scan` | diff 审查与全文件扫描(`--audience agent --format json --output` 机器可读面) |
38
+ | `ocr delegate preview` / `ocr delegate rule` | 委托模式脚手架(文件清单 + 规则分组,OCR 端零 LLM 消耗) |
39
+ | `ocr session list/show/comments` | 会话查询闭环(含 `--resume` 恢复) |
40
+ | `ocr llm test` | select 后的连通性验证 |
41
+ | `config.json` 的 `custom_providers.<name>.{url,protocol,model,api_key_cmd}` | provider/model 应用面;key 经 `api_key_cmd` 动态读取,绝不落明文 |
42
+
43
+ 这些都是 CLI 的外层面,但不是任何文档承诺过的稳定 API——本包把版本钉在 `^1.12.13` 正是为了
44
+ 守住这一组契约;上游变更时以本包的契约闸测试(`test/cli.test.ts`、`test/parse.test.ts`)为
45
+ 首道防线。
46
+
47
+ ## 运行期依赖
48
+
49
+ 运行期依赖的许可由各自包随包分发,本包不重复其文本:
50
+
51
+ | 依赖 | 版本 | 许可 | 引入方 |
52
+ | --- | --- | --- | --- |
53
+ | `@alibaba-group/open-code-review` | ^1.12.13 | Apache-2.0(Alibaba) | `optionalDependencies`;平台二进制由它自己的 `@alibaba-group/ocr-<os>-<arch>` 选一 |
54
+ | `js-yaml` | ^5.4.3 | MIT | `scripts/get-cred.mjs` 读 dsh 托管凭据文档 |
55
+
56
+ dsh 宿主包(`@deepseek-ai/dsh` 及其子包)是 peer 依赖,由宿主提供、不随本包分发。
package/client.js CHANGED
@@ -1,16 +1,17 @@
1
1
  // GENERATED by buildClient() in build-client.mjs — edit src/client-entry.ts, then run `node build-client.mjs`.
2
2
  window.__ModuleLoader__.load({
3
- id: '@jayyuen66/dsh-ocr-review',
3
+ id: "@jayyuen66/dsh-ocr-review",
4
4
  factory: (require) => {
5
- var module = { exports: {} };
6
- var exports = module.exports;
5
+ 'use strict';
6
+ const module = { exports: {} };
7
+ let exports = module.exports;
7
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
9
  let react = require("react");
9
10
  //#region src/ui-messages.ts
10
11
  const UI_MESSAGES = {
11
12
  zh: {
12
13
  cardTitle: "open-code-review 代码审查",
13
- cardDescription: "ocr 工具配置:选择 provider/model 应用到 OCR(key 走 api_key_cmd 动态读取,不落明文)+ 审查参数设置",
14
+ cardDescription: "ocr 工具配置:选择 provider/model 应用到 OCR(应用后 key 由 api_key_cmd 动态读取,不再写入明文)+ 审查参数设置",
14
15
  statusReadOnly: "当前作用域只读",
15
16
  statusDirty: "有未保存的修改,点「保存」生效",
16
17
  statusClean: "无未保存的修改",
@@ -18,6 +19,7 @@ const UI_MESSAGES = {
18
19
  saving: "保存中…",
19
20
  revert: "撤销",
20
21
  saveFailed: "保存失败:",
22
+ saveRefused: "未写入(宿主拒收或跳过):{fields}",
21
23
  loadProvidersFailed: "加载 providers 失败:{error}(端点未就绪?重启 dsh 后重试)",
22
24
  loadingProviders: "加载 providers…",
23
25
  noKeySuffix: "(未配置 key)",
@@ -68,7 +70,7 @@ const UI_MESSAGES = {
68
70
  },
69
71
  en: {
70
72
  cardTitle: "open-code-review code review",
71
- cardDescription: "ocr tool setup: pick a provider/model and apply it to OCR (keys are read dynamically via api_key_cmd, never stored in plain text) + review parameters",
73
+ cardDescription: "ocr tool setup: pick a provider/model and apply it to OCR (once applied, keys are read dynamically via api_key_cmd and no longer written in plain text) + review parameters",
72
74
  statusReadOnly: "This scope is read-only",
73
75
  statusDirty: "Unsaved changes — press Save to apply",
74
76
  statusClean: "No unsaved changes",
@@ -76,6 +78,7 @@ const UI_MESSAGES = {
76
78
  saving: "Saving…",
77
79
  revert: "Revert",
78
80
  saveFailed: "save failed: ",
81
+ saveRefused: "not written (host refused or skipped): {fields}",
79
82
  loadProvidersFailed: "failed to load providers: {error} (endpoint not ready? restart dsh and retry)",
80
83
  loadingProviders: "loading providers…",
81
84
  noKeySuffix: " (no key configured)",
@@ -126,7 +129,7 @@ const UI_MESSAGES = {
126
129
  }
127
130
  };
128
131
  //#endregion
129
- //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.3_@deepseek-ai+cordis@4.0.5-alpha.1_@deepseek-ai+dsh-c_6c028874c2202469b2088f67c885abe7/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
132
+ //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.4_@deepseek-ai+cordis@4.0.5-alpha.1_@deepseek-ai+dsh-c_472a88c28a1052d4f20b6206ef930fa9/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
130
133
  function e(e) {
131
134
  return typeof e == `object` && !!e && !Array.isArray(e);
132
135
  }
@@ -359,7 +362,7 @@ function OcrPluginCard(props) {
359
362
  }
360
363
  }, headRow, chevron);
361
364
  const body = open ? (0, react.createElement)("div", { className: "ocr-card-body" }, children) : null;
362
- return (0, react.createElement)("li", { className: `ocr-card${open ? " ocr-card-open" : ""}` }, header, body);
365
+ return (0, react.createElement)("div", { className: `ocr-card${open ? " ocr-card-open" : ""}` }, header, body);
363
366
  }
364
367
  /** 整数数字输入行(保存条模式:合法输入上报草稿,不直写)。 */
365
368
  function NumberRow(props) {
@@ -827,7 +830,10 @@ async function runTestConnection(deps) {
827
830
  deps.setBusy(false);
828
831
  }
829
832
  }
830
- /** 只写差异字段:touched 值为 undefined → 写 null(= 清回 schema 默认)。 */
833
+ /** 只写差异字段:touched 值为 undefined → 写 null(= 清回 schema 默认)。
834
+ * 受理位必须回来:`set` 回 `false` 表示宿主拒收或跳过了这次写入(值没落盘),
835
+ * 这时不能清 touched、也不能报成功——否则用户看到「已保存」而配置原封未动。
836
+ * 传输失败仍以 rejection 表达。 */
831
837
  async function saveSettingsDraft(deps) {
832
838
  const { t, value, touched } = deps;
833
839
  const keys = diffTouched(touched, value);
@@ -836,11 +842,16 @@ async function saveSettingsDraft(deps) {
836
842
  deps.setSaveError(null);
837
843
  const ops = keys.map(async (field) => {
838
844
  const draft = touched[field];
839
- await deps.set(field, draft === void 0 ? null : draft);
845
+ return deps.set(field, draft === void 0 ? null : draft);
840
846
  });
841
847
  try {
842
- await Promise.all(ops);
848
+ const settled = await Promise.all(ops);
843
849
  deps.setSettingsBusy(false);
850
+ const refused = keys.filter((_field, index) => settled[index] !== true);
851
+ if (refused.length > 0) {
852
+ deps.setSaveError(t("saveRefused", { fields: refused.join(", ") }));
853
+ return;
854
+ }
844
855
  deps.setTouched({});
845
856
  } catch (error) {
846
857
  deps.setSettingsBusy(false);
@@ -1012,13 +1023,7 @@ function apply(ctx) {
1012
1023
  inject: () => ({
1013
1024
  t,
1014
1025
  hooks: { card: store },
1015
- set: async (field, value) => {
1016
- try {
1017
- await scope.set(field, value);
1018
- } catch (error) {
1019
- console.error(`[ocr-review-card] set ${field} failed:`, error);
1020
- }
1021
- }
1026
+ set: (field, value) => scope.set(field, value)
1022
1027
  })
1023
1028
  }, OcrCard);
1024
1029
  });