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

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
@@ -5,10 +5,11 @@ English · [中文](README.zh-CN.md)
5
5
  ## What it does
6
6
 
7
7
  - Wraps the external `ocr` (open-code-review) CLI into 5 model tools plus 1 web settings card for dsh; the host validates tool arguments not at all, so every shape and type gate lives in this package.
8
- - The host half (`host.ts`) declares 6 `.volatile()` settings fields — as of 0.1.7 the namespace is implicit: the host projects the form from the profile entry id `ocr-review` and the plugin calls no `settings.register`.
9
- - It also registers 5 tools, one systemPrompt routing section (`ocr-review-routing`, order 1555) and 4 `/_dsh/ocr-review/*` endpoints.
10
- - The client half (`src/client-entry.ts` → `client.js`) is the settings card, taking provider/model from the `llm-pi-ai` namespace of the settings service.
8
+ - The host half (`host.ts`) declares 6 `.volatile()` settings fields — the namespace is implicit: the host projects the form from the profile entry id `ocr-review` and the plugin calls no `settings.register`.
9
+ - It also registers 5 tools, one systemPrompt routing section (`ocr-review-routing`, order 1555) and 5 `/_dsh/ocr-review/*` endpoints.
10
+ - The client half (`src/client-entry.ts` → `client.js`) is the settings card, taking provider/model from the `llm-pi-ai` namespace of the settings service, plus three read-only contributions about the external CLI: a badge and a section on the plugin's detail page and the guidance shown right after the bundle is enabled.
11
11
  - It writes provider/model into the external ocr CLI's own `~/.opencodereview/config.json` — as an `api_key_cmd` command, never as a plaintext key.
12
+ - Review results come back as a structured summary whose fields split into two notions: what OCR found (`status`, `aggregation`, `topComments`, `warnings`) and whether the run actually covered the selected files (`coverage`, `budgetExceeded`, `toolCalls`). Under `--audience agent` the CLI drops its progress lines to `io.Discard`, so the second notion is only reachable through those fields.
12
13
 
13
14
  ## Prerequisites (external CLI)
14
15
 
@@ -20,7 +21,23 @@ English · [中文](README.zh-CN.md)
20
21
  - 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
22
  - 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
23
  - 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.2`: declared in `peerDependencies` and in `engines.dsh` (same value, read by nothing).
24
+ - The host must be `>= 0.2.1-alpha.2`: declared in `engines.dsh`, which is documentation only — no loader or installer reads it. This bundle imports no symbol from the `@deepseek-ai/dsh` package itself (every service face comes from the `@deepseek-ai/dsh-*` sub-packages), so it is not declared as a dependency of any kind.
25
+
26
+ ## External CLI status and version
27
+
28
+ One judgement, `resolveOcrAvailability` in `lib/cli.ts`, answers "is there an `ocr` here, and where does it come from": the bundled launcher first, otherwise a PATH scan (`PATHEXT` extensions on Windows). It is pure filesystem — `statSync(...).isFile()`, never a spawn — and it answers **candidate**, not **runnable**: a file that exists but cannot execute (no executable bit, or the bundled entry present while its platform binary is missing) passes the probe and is reported by the real spawn instead, which is why the "command not found" wording is kept as a separate fallback.
29
+
30
+ | Surface | What it does with that answer |
31
+ | ------- | ----------------------------- |
32
+ | The five tools | The probe runs before anything spawns. With no candidate on either path the model gets "neither path found, install it like this" instead of an exit-127 reverse-derivation. Re-read per call, never cached per process: installing the CLI takes effect on the next call without restarting dsh |
33
+ | Settings card | A status line **outside** the provider panel (whether this machine has `ocr` and whether the host can list providers are two different facts, and a machine missing the CLI must still see the reason), plus one `Detect version` button |
34
+ | Detail page | A badge beside the title and a section under the page's own content, both rendering null for a subject that is not this package and while the read has not come back |
35
+ | After enabling | The activation guidance slot says the install recipe only when the answer is `none` — the outermost position a third-party bundle may render in |
36
+
37
+ - The plugin **list** card's tag row is not plugin-extensible: those tags are the host's own verdicts on the entry (`running` / `off` / `problem`). The two places above are as far out as a bundle can reach, and they are client-half surfaces, so a disabled plugin cannot warn that its CLI is missing — disabling it removes the thing that would have said so.
38
+ - All three client surfaces read the same `ocrBinary` field of `GET providers`, and none of them computes availability locally: three separate judgements is how a status display drifts.
39
+ - **Version is not free**: reading it spawns `ocr version`, so it runs only when you press `Detect version` — never at apply and never while a page renders. The receipt shows the CLI's own output underneath the version line, with terminal control sequences stripped (upstream's launcher hard-codes the colour codes around its "new version available" nudge, and that nudge is printed by the JS wrapper, so `--color never` cannot reach it) — the line you are meant to select and copy is `npm i -g …`, and copying it must not drag escape bytes into your terminal. One completed round trip re-reads the payload, so the status line and the version you just got describe the same moment (install the CLI while the page is open and the row no longer contradicts the receipt). This package deliberately provides no update action and no clipboard API (a non-secure context would fail silently, which looks like the button doing nothing).
40
+ - **Upstream self-update**: the `ocr` npm wrapper runs `scripts/update.js` on its own during invocations (18-minute cooldown, pid lock), and that script executes `npm i -g @alibaba-group/open-code-review@<latest>`. The `ocrNoAutoUpdate` deployment switch sets `OCR_NO_UPDATE=1` on every command this package spawns, so a review never changes global packages under you; it defaults to off, i.e. upstream behaviour. The version probe always sets it, switch or not — probing is a read and must not install anything. The launcher prints its "new version available" nudge to stderr *before* checking that variable, so the nudge still arrives in the receipt text.
24
41
 
25
42
  ## Installation
26
43
 
@@ -31,7 +48,7 @@ dsh plugin --profile web add @jayyuen66/dsh-ocr-review
31
48
  - The packages are on the public npm registry, so installation needs no credentials.
32
49
  - 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
50
  - 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`, `peerDependencies` *and* `optionalDependencies`, so `host.js` keeps the bare specifiers unchanged.
51
+ - 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 `.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
52
  - 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
53
  - 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
54
 
@@ -67,7 +84,7 @@ If you would rather run nothing at install time, write `'@alibaba-group/open-cod
67
84
  - The in-package `cordis.patch.yml` declares `- id: ocr-review` / `name: "@jayyuen66/dsh-ocr-review"` and is pointed at by `dsh.bundle.patch` in `package.json`.
68
85
  - `dsh plugin --profile web add/remove` registers and removes it, then restart dsh.
69
86
  - The card needs a web profile (`dsh.client.platform: web`, `immediately: true`).
70
- - The four `/_dsh/ocr-review/*` endpoints hang off an `inject(["webServer"])` child fiber: with no webServer (a TUI host) the child never activates, so the endpoints simply do not exist while tools and settings stay available.
87
+ - The five `/_dsh/ocr-review/*` endpoints hang off an `inject(["webServer"])` child fiber: with no webServer (a TUI host) the child never activates, so the endpoints simply do not exist while tools and settings stay available.
71
88
  - On the real host webServer arrives about a second after this entry, which is why it has to be a dependency rather than a one-off `ctx.get` in apply.
72
89
  - Deployment defaults can go on the profile's `config:` line (cordis validates and fills defaults through the exported `Config` schema); precedence: runtime settings value > line config > built-in default.
73
90
 
@@ -80,7 +97,7 @@ If you would rather run nothing at install time, write `'@alibaba-group/open-cod
80
97
  | `ocr_scan` | repo, path (at most 100 entries), exclude (50), batch none/by-language/by-directory, provider/model (native shared flags of scan_cmd, matching review), the same numeric group, wait; no git diff required |
81
98
  | `ocr_delegate_preview` / `ocr_delegate_rule` | The former: repo, scope/commit/from/to, exclude 50, background. The latter: repo, paths required, 1–200 entries, positional arguments after `--`, each escaped. Zero LLM cost on the OCR side, they return only the file list and the rule groups |
82
99
  | `ocr_session` | repo, action list/show/comments (whitelist, anything else errors), id, limit (clamped to 1–100, default 10); show/comments without an id falls back to list |
83
- | Background and reaper | `wait: false` (background start, no host deadline, reclaimed by a 2-hour backstop, no `--output`, poll results with `ocr_session`) and the host reaper guard apply only to these two minute-scale commands, review and scan - that subprocess is also an official job (see the job-surface section below); that change left the tool's JSON untouched (a later output-contract rework switched it to the direct canonical object, see below) |
100
+ | Background and reaper | `wait: false` (background start, no host deadline, no time limit by default, no `--output`, poll results with `ocr_session`) and the host reaper guard apply only to these two minute-scale commands, review and scan - that subprocess is also an official job (see the job-surface section below); that change left the tool's JSON untouched (a later output-contract rework switched it to the direct canonical object, see below) |
84
101
 
85
102
  ## Settings
86
103
 
@@ -98,9 +115,15 @@ Three deployment-level tuning keys are deliberately **not** on the settings card
98
115
  | Key | Default | Use |
99
116
  | -------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
100
117
  | `llmTestTimeoutMs` | `90000` | Wall clock for `ocr llm test` (shared by autoVerify and the card's "test connection"); raise it on slow machines |
101
- | `ocrBackgroundMaxMs` | `7200000` | The 2-hour backstop window for background reviews (`jobs.wait` to the deadline, then `kill`) |
118
+ | `ocrBackgroundMaxMs` | `0` | Backstop window for background reviews (`jobs.wait` to the deadline, then `kill`); 0 = none, so a run ends only when the host exits, the session stops, the plugin unloads or the job is killed |
102
119
  | `stdoutMaxBytes` | `400000` | Executor stdout buffer cap, shared by foreground and background runs |
103
120
 
121
+ A fourth non-volatile field is a deployment decision rather than a tuning value, for the same reason (it changes what a run may do to the machine, so it does not belong on a card a user can flip mid-review):
122
+
123
+ | Key | Default | Use |
124
+ | ------------------ | ------- | ------------------------------------------------------------------------------------------------------ |
125
+ | `ocrNoAutoUpdate` | `false` | When true, every `ocr` command this package spawns carries `OCR_NO_UPDATE=1`, which stops upstream's wrapper from running `npm i -g @alibaba-group/open-code-review@<latest>` in the middle of a review. `false` keeps upstream behaviour (18-minute cooldown, pid lock). The version probe sets the variable regardless of this switch |
126
+
104
127
  ## Credentials and config files
105
128
 
106
129
  | Aspect | Fact |
@@ -115,13 +138,14 @@ Three deployment-level tuning keys are deliberately **not** on the settings card
115
138
 
116
139
  | Route | Method | Input | Success payload |
117
140
  | ---------------------------------------- | ------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
118
- | `/_dsh/ocr-review/providers` | GET | — | `{providers (each with a hasKey boolean), current, getCredReady, source, csrf}`, never containing a key |
141
+ | `/_dsh/ocr-review/providers` | GET | — | `{providers (each with a hasKey boolean), current, getCredReady, source, csrf, endpointVerify, ocrBinary}`, never containing a key. `ocrBinary` is the filesystem-only availability verdict (`{source: "bundled" \| "path" \| "none", path?}`), read fresh per request; `endpointVerify` is the apply-time `ocr llm test` result or null when it has not run |
119
142
  | `/_dsh/ocr-review/select` | POST | body `{provider, model}`, both must hit the list returned by `describe()` | `{ok, applied, llmTest}` (`llmTest` is null when `autoVerify` is off) |
120
143
  | `/_dsh/ocr-review/migrate` | POST | — | `{ok, migrated, skippedUnknown}` |
121
144
  | `/_dsh/ocr-review/test` | POST | — | `{ok, output}` (output is redacted first, then cut to 8000 characters) |
122
- | Front guard of the three write endpoints | — | — | The first statement of each handler is `guardTrust` (Host authority -> the `sec-fetch-site` allowlist -> a verbatim `origin` compare; failing it gives 403 with `{ok:false,error:"untrusted host authority" \| "cross-origin request rejected"}`); 405 for non-POST carrying `Allow: POST` and `{ok:false,error:"POST only"}`; 403 for a missing or wrong `x-ocr-csrf` header; 413 above the 8KB body cap; 400 for an aborted stream. The token is regenerated on every plugin apply and the old one stops working |
145
+ | `/_dsh/ocr-review/probe` | POST | — | `{ok, version, output, reason}` — spawns `ocr version` with a 15s wall clock and `OCR_NO_UPDATE=1`, then reads the version out of its output. `version` is null unless the command succeeded and printed a parseable version; `reason` names the failure arm (`command not found`, non-zero exit, timeout, or exit 0 with nothing parseable, which is what an unreleased build looks like); `output` is redacted and cut the same way as `/test`, with terminal control sequences stripped, so an upstream upgrade notice stays copyable |
146
+ | Front guard of the four write endpoints | — | — | The first statement of each handler is `guardTrust` (Host authority -> the `sec-fetch-site` allowlist -> a verbatim `origin` compare; failing it gives 403 with `{ok:false,error:"untrusted host authority" \| "cross-origin request rejected"}`); 405 for non-POST carrying `Allow: POST` and `{ok:false,error:"POST only"}`; 403 for a missing or wrong `x-ocr-csrf` header; 413 above the 8KB body cap; 400 for an aborted stream. The token is regenerated on every plugin apply and the old one stops working |
123
147
 
124
- These four routes live and die with the plugin's injected child fiber. The official `webServer.register` hands back a disposer the **caller must invoke**, and it throws on a duplicate path outright (installed dsh-host-webserver/lib/index.js:177-184), while the route table belongs to the host-provided webServer instance and does **not** die when this plugin's fiber does. This package attaches all four disposers to the effect of the `inject(["webServer"])` child fiber, so a hot reload (old fiber disposes, then a new apply runs) cannot collide on the path and kill the whole settings card, and disabling or uninstalling the plugin makes these endpoints unreachable again.
148
+ These five routes live and die with the plugin's injected child fiber. The official `webServer.register` hands back a disposer the **caller must invoke**, and it throws on a duplicate path outright (installed dsh-host-webserver/lib/index.js:177-184), while the route table belongs to the host-provided webServer instance and does **not** die when this plugin's fiber does. This package attaches all five disposers to the effect of the `inject(["webServer"])` child fiber, so a hot reload (old fiber disposes, then a new apply runs) cannot collide on the path and kill the whole settings card, and disabling or uninstalling the plugin makes these endpoints unreachable again.
125
149
 
126
150
  ## The background job surface (official `ctx.jobs`)
127
151
 
@@ -132,9 +156,9 @@ The external `ocr` subprocess started by `wait: false` is now also registered as
132
156
  | Model observation surface | Three more tools: the official `job_list` shows the review, `job_output` reads its stdout/stderr (the registry pulls from `proc.observed` on its own cadence - non-consuming, so it never steals bytes from the `readOutput()` cursor), and `job_kill` stops it. Results still come from the `ocr_session` record; the payload content is unchanged since the job-surface swap, and a later output-contract rework returns the canonical object directly (no more stringify-ing the receipt into a `{ text }` wrapper) |
133
157
  | Broken executor reader | When the executor's reader throws, the official pump only writes one line to the host log and afterwards reads that channel as empty (measured) ⇒ this package writes **one** "this section is incomplete" note into the ring, exactly once, and stops knocking on the broken reader |
134
158
  | Controller lifetime | Attached only for the instant of `start`: the registry refuses `start` when no controller serves the owner, and on the web surface the host's own `tool-jobs` row is disabled at the host plane (each preset realm mounts its own), so an unowned job needs a controller of our own. We attach it and detach it immediately (measured: detaching restores the refusal, and read/kill/settlement never need it again) - a resident token would pry that gate open for a composition that deliberately has no job tools |
135
- | Three cancellation paths | An aborted tool call, a plugin unload, and the 2-hour backstop all call `jobs.kill(id, undefined, reason)` instead of killing the process around it, so the record moves to `stopping` and the reason lands in `detail` (measured: bypassing it left the row stuck at `running` forever). They may overlap - killing a `stopping` job still returns `requested` and `proc.kill()` is idempotent. Reasons are model-facing, so they come from the bilingual catalog |
136
- | Nothing there may throw | An error raised inside an `abort` listener is rethrown by Node via `process.nextTick` ⇒ uncaught ⇒ **the whole host process exits** (reproduced locally: `Error: unknown job ...` followed by `EXIT=1`). The official package does not cover for producers either - `killJob` calls `job.cancel()` bare (installed dsh-jobs-local:611-622, and only the teardown arm wraps each call in try/catch), and a reloaded registry throws for an unfamiliar id (same file `:554-558`). So two layers each catch once: `cancel` swallows the "process is already gone" family through `killProc`, `stop` catches the registry arm and falls back to killing our own process. One test per layer - deleting either layer turns it red |
137
- | The 2-hour backstop | We still give the external CLI no host `timeoutMs` (a long review would be cut off), but "no end at all" gained a victim with this adoption - the 10-slot unowned bucket is **shared host-wide** (measured: `running` and `stopping` both count, overflow refuses new `start`s), so one hung CLI parks a slot forever. The backstop uses the registry's own `wait(id, ms)` + `kill`, not a host timer and not a bare `setTimeout` |
159
+ | Three reap paths | The optional backstop window, a plugin unload, and a session dispose all call `jobs.kill(id, undefined, reason)` instead of killing the process around it, so the record moves to `stopping` and the reason lands in `detail` (measured: bypassing it left the row stuck at `running` forever). They may overlap - killing a `stopping` job still returns `requested` and `proc.kill()` is idempotent. Reasons are model-facing, so they come from the bilingual catalog |
160
+ | Nothing there may throw | An error raised inside a host event handler is rethrown by Node via `process.nextTick` ⇒ uncaught ⇒ **the whole host process exits** (reproduced locally: `Error: unknown job ...` followed by `EXIT=1`). The official package does not cover for producers either - `killJob` calls `job.cancel()` bare (installed dsh-jobs-local:611-622, and only the teardown arm wraps each call in try/catch), and a reloaded registry throws for an unfamiliar id (same file `:554-558`). So two layers each catch once: `cancel` swallows the "process is already gone" family through `killProc`, `stop` catches the registry arm and falls back to killing our own process. One test per layer - deleting either layer turns it red |
161
+ | The optional backstop | We still give the external CLI no host `timeoutMs` (a long review would be cut off), but "no end at all" gained a victim with this adoption - the 10-slot unowned bucket is **shared host-wide** (measured: `running` and `stopping` both count, overflow refuses new `start`s), so one hung CLI parks a slot forever. The backstop uses the registry's own `wait(id, ms)` + `kill`, not a host timer and not a bare `setTimeout` |
138
162
  | Unowned exposure | Unowned means visible to every session - any caller can `list`/`read`/`kill` it. Known and accepted for this wave (owned jobs would need the `dsh-agent` registry, measured unavailable); rolling the adoption back is the only way to close it |
139
163
  | Settled-record window | The registry never recycles settled records on its own, and it outlives this plugin - so before each registration this package prunes the oldest settled `ocr-review` records down to 9 and only then starts, so the roster never holds more than 10 (each ring is capped at 256 KiB). At unload both jobs are done: live ones are killed through the registry, settled ones are removed - disabling this plugin means there is no "next apply" left to prune the window. Live and stopping records are never pruned: the registry answers overflow by **refusing `start`** (measured: the unowned bucket holds 10), not by killing somebody's review |
140
164
  | No registry, no background | When the host has not loaded `dsh-jobs-local`, `wait: false` throws an error naming the missing piece instead of starting a subprocess nobody can observe. Foreground runs (`wait: true`) are unaffected |
@@ -145,14 +169,14 @@ The external `ocr` subprocess started by `wait: false` is now also registered as
145
169
  | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
146
170
  | Process boundary | The reviewed repository's paths and diff content go into the `ocr` child process, and OCR sends them to the LLM endpoint it has in its own config.json; that egress is the review feature itself and does not pass through the dsh host. Plaintext keys never appear in tool results or in `/_dsh/*`, and the value the host gets from `resolve()` is used only to confirm "it resolves right now" and is discarded on the spot |
147
171
  | Redaction of raw output | Raw OCR stdout/stderr (failure details, llm test, parse-failure echoes, session passthrough) all go through `redactKeyMaterial()` before leaving the host: bearer and assignment forms are redacted strictly, while the standalone-key form additionally demands random-string features so that file paths handed to the model are not erased as well |
148
- | Places written | This package writes to only two places: the external ocr CLI's `config.json` (plus its random-suffixed `.tmp`, the `.bak` backup, and the short-lived `config.json.lock` held only for the duration of one read-modify-write) and `ocr-review-*`/`ocr-scan-*` in the system temp directory (removed right after reading). Plaintext that predates a migration may still sit in the `.bak` produced by that same write (mode 0600); removing it for good means deleting that backup yourself |
172
+ | Places written | This package writes to only two places: the external ocr CLI's `config.json` (plus its random-suffixed `.tmp`, the `.bak` backup, and the short-lived `config.json.lock` held only for the duration of one read-modify-write) and `ocr-review-*`/`ocr-scan-*` in the system temp directory (removed right after reading). Plaintext that predates a migration may still sit in the `.bak` produced by that same write (mode 0600); removing it for good means deleting that backup yourself. Writes outside those two places can still happen **inside the CLI's own wrapper**: unless `ocrNoAutoUpdate` is on, an `ocr` invocation may trigger upstream's global `npm i -g` of a newer release. This package never runs an installer itself, and the version probe always suppresses that path |
149
173
 
150
174
  ## FAQ
151
175
 
152
176
  | Symptom | Verdict |
153
177
  | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
154
178
  | Exit 0 taken for a successful review | Exit 0 is not a successful review: any `status` outside success/complete/**partial**/completed_with_warnings/completed_with_errors/skipped is reported as failure (`{"error": …}` is of that kind; partial is one of the manifest terminal states — some files failed but coverage remains, and the comments still stand), and a parse failure echoes the first 500 redacted UTF-16 code units of the raw output (an emoji takes 2) |
155
- | Whether the summary covered everything | It covers everything OCR produced only when `droppedCount` and `invalidCommentCount` are both 0, otherwise raise `maxComments` (or set 0) and re-check; stdout has its own 400000-byte cap, which is why review results always go through the `--output` temp file |
179
+ | Whether the summary covered everything | Two different notions, neither replaces the other: `droppedCount` and `invalidCommentCount` only say whether *this summary* listed every comment OCR produced (raise `maxComments`, or set 0, and re-check). Whether *the run* covered everything is in `coverage` (`selected` is the denominator, `completed`/`reused`/`failed`/`waived` partition it, `failedItems` names the files with their classification and reason) plus `budgetExceeded` (the token budget stopped it early — `status` still only says `partial`); `toolCalls` reports how many of the model's tool calls failed. stdout has its own 400000-byte cap, which is why review results always go through the `--output` temp file |
156
180
  | Background processes when dsh exits | When dsh exits in any way (including kill -9 followed by launchd adoption), the OCR process tree of review/scan is taken down by the reaper guard inside the command with TERM→1s→KILL; cancelling the tool call terminates the corresponding background process too |
157
181
 
158
182
  ## Development
package/README.zh-CN.md CHANGED
@@ -5,9 +5,11 @@
5
5
  ## 它做什么
6
6
 
7
7
  - 把外部 `ocr`(open-code-review)CLI 封装成 dsh 的 5 个模型工具 + 1 张 web 设置卡片;宿主对工具入参零校验,形状与类型闸全在本包内做。
8
- - Host 半(`host.ts`)声明 6 个 `.volatile()` 设置项(0.1.7 起命名空间是隐式的:宿主按 profile 条目 id `ocr-review` 投影表单,插件不再 `settings.register`)、注册 5 个模型工具、一段 systemPrompt 路由(`ocr-review-routing`,order 1555)与 4 个 `/_dsh/ocr-review/*` 端点。
9
- - Client 半(`src/client-entry.ts` → `client.js`)是设置卡片,provider/model 取自设置服务的 `llm-pi-ai` 命名空间。
8
+ - Host 半(`host.ts`)声明 6 个 `.volatile()` 设置项(命名空间是隐式的:宿主按 profile 条目 id `ocr-review` 投影表单,插件不调 `settings.register`)。
9
+ - 同时注册 5 个模型工具、一段 systemPrompt 路由(`ocr-review-routing`,order 1555)与 5 个 `/_dsh/ocr-review/*` 端点。
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
 
@@ -19,7 +21,23 @@
19
21
  - 平台:目标平台是 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
22
  - provider 清单与 key 状态走宿主官方通道 `ctx.settings.describe()`(`llm-pi-ai` 那条的 `value.providers`)与 `ctx.credentials`。
21
23
  - 两者缺席时卡片显示降级原因,工具与设置照常可用。
22
- - 宿主版本要求 `>= 0.2.1-alpha.2`:写在 `peerDependencies` 与 `engines.dsh`(同值、无人读)。
24
+ - 宿主版本要求 `>= 0.2.1-alpha.2`:只写在 `engines.dsh`,属于文档性声明,装载器与安装器都不读它。本包也不从 `@deepseek-ai/dsh` 主包导入任何符号(服务面全部来自 `@deepseek-ai/dsh-*` 子包),所以它在任何一张依赖表里都不出现。
25
+
26
+ ## 外部 CLI 的状态与版本
27
+
28
+ 判据只有一处,`lib/cli.ts` 的 `resolveOcrAvailability`,回答「这台机器有没有 ocr、从哪条路来」:先看随包 launcher,再看 PATH 扫描(Windows 按 `PATHEXT` 补后缀)。它纯读文件系统——`statSync(...).isFile()`,绝不起进程——因此它答的是**找没找到候选**,不是**跑不跑得起来**:文件在但执行不了(丢了执行位,或随包入口在而平台二进制缺失)仍会通过前置检查,那一路由真实 spawn 报出来,所以「命令不存在」那句兜底文案必须留着。
29
+
30
+ | 显示面 | 拿到这份判据后做什么 |
31
+ | ------ | -------------------- |
32
+ | 五个工具 | 起任何进程之前先现读一次。两条路都没有候选就直接拒绝,模型看到的是「两条路都没找到 + 这么装」,而不是子进程 exit 127 之后反推出来的那句「命令不存在」。每次调用现读、不按进程缓存:装完 CLI 直接重试即可,不必重启 dsh |
33
+ | 设置卡片 | provider 表单**之外**的一行状态(「这台机器有没有 ocr」与「宿主能不能列出 provider」是两件事,缺 CLI 的机器必须连原因都看得见),外加一枚「检测版本」按钮 |
34
+ | 插件详情页 | 标题旁一只徽标 + 页面自身内容之下一个区块;页面说的不是本包、或状态还没回来时两处都渲染 null |
35
+ | 启用之后 | 启用指引只在判据为 `none` 时说话——那是第三方 bundle 能渲染的最外侧位置 |
36
+
37
+ - 插件**列表**卡片上那排标签不归插件管:它们是宿主对条目自身的判定(`running` / `off` / `problem`)。上面两处就是能显示的最外一侧,而它们都属于 client 半——插件被禁用时 client 半一并撤下,没有谁还能在那里提示「CLI 没装」。
38
+ - 三处显示面读的都是 `GET providers` 里同一个 `ocrBinary` 字段,且都不在本地自己算一遍可得性:一处判据算三份,就是状态显示分叉的开始。
39
+ - **版本号不是白来的**:取版本要 spawn `ocr version`,所以只有按下卡片上的「检测版本」才跑——不在 apply 时跑,也不在页面渲染时跑。回执在版本行之下附上 CLI 自己的输出,并剥掉终端控制序列(上游 launcher 那句「有新版本」的颜色码是硬编码的,而且打在 JS 包装层,`--color never` 管不到它)——要给人复制的那行是 `npm i -g …`,复制时不该拖着转义字节。一次来回走完后卡片会重读载荷,让状态行与刚取到的版本描述的是同一时刻(页面开着的时候装上 CLI,那一行也不会再跟回执自相矛盾)。本包刻意不提供更新动作,也不接剪贴板 API(非安全上下文会静默失败,看上去像按钮没反应)。
40
+ - **上游会自己升级**:`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
41
 
24
42
  ## 安装
25
43
 
@@ -57,7 +75,7 @@ allowBuilds:
57
75
 
58
76
  如果你更希望完全不执行安装期脚本,可以写 `'@alibaba-group/open-code-review': false`:`ocr` 同样可用(launcher 直接从平台包目录取二进制),但宿主 `readPendingBuilds()` 只认值为 `set this to true or false` 的条目,改成 `false` 后官方审批流程就不再能代为授权,只能手工维护。
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,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` 里的裸说明符因此一字不变。
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,7 @@ 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
- - 4 个 `/_dsh/ocr-review/*` 端点挂在 `inject(["webServer"])` 的子 fiber 上:宿主没有 webServer(如 TUI)时子 fiber 不激活、端点不存在,工具与设置照常。真实宿主上 webServer 比本条目晚到位约 1 秒,所以它必须是依赖而不是在 apply 里 `ctx.get` 读一次。
87
+ - 5 个 `/_dsh/ocr-review/*` 端点挂在 `inject(["webServer"])` 的子 fiber 上:宿主没有 webServer(如 TUI)时子 fiber 不激活、端点不存在,工具与设置照常。真实宿主上 webServer 比本条目晚到位约 1 秒,所以它必须是依赖而不是在 apply 里 `ctx.get` 读一次。
70
88
  - 部署默认可写在 profile 注册行的 `config:` 上(cordis 按导出的 `Config` schema 校验并填默认),优先级:设置卡运行时值 > 行 config > 内置默认。
71
89
 
72
90
  ## 提供给模型的工具
@@ -78,7 +96,7 @@ allowBuilds:
78
96
  | `ocr_scan` | repo、path(最多 100 条)、exclude(50)、batch none/by-language/by-directory、provider/model(scan_cmd 原生共享 flags,与 review 对齐)、同一组数值参数、wait;无需 git diff |
79
97
  | `ocr_delegate_preview` / `ocr_delegate_rule` | 前者:repo、scope/commit/from/to、exclude 50、background;后者:repo、paths 必填 1–200 条,经 `--` 分隔的位置参数逐条转义。OCR 端零 LLM 消耗,只回文件清单与规则分组 |
80
98
  | `ocr_session` | repo、action list/show/comments(白名单,越界即报错)、id、limit(钳到 1–100,缺省 10);show/comments 缺 id 时回落 list |
81
- | 后台与回收 | `wait: false`(后台启动、不挂宿主 deadline、2 小时后兜底回收、不写 `--output`,结果用 `ocr_session` 轮询)与宿主回收守护只作用于 review/scan 这两条分钟级命令。**那次子进程同时是一枚官方作业**(见下面的「后台作业面」一节),工具回执那份 JSON 在换装那次一字未改(输出契约后来另改为 canonical 对象直返,见下节) |
99
+ | 后台与回收 | `wait: false`(后台启动、不挂宿主 deadline、默认不设时限、不写 `--output`,结果用 `ocr_session` 轮询)与宿主回收守护只作用于 review/scan 这两条分钟级命令。**那次子进程同时是一枚官方作业**(见下面的「后台作业面」一节),工具回执那份 JSON 在换装那次一字未改(输出契约后来另改为 canonical 对象直返,见下节) |
82
100
 
83
101
  ## 设置项
84
102
 
@@ -96,9 +114,15 @@ allowBuilds:
96
114
  | 键 | 默认 | 用途 |
97
115
  | -------------------- | --------- | ---------------------------------------------------------------------------------------- |
98
116
  | `llmTestTimeoutMs` | `90000` | `ocr llm test` 的墙钟(autoVerify 与卡片「测试连接」共用);评审机慢就调大 |
99
- | `ocrBackgroundMaxMs` | `7200000` | 后台评审的兜底回收窗口(`jobs.wait` 等到 deadline 即经注册表 `kill`,即 2 小时) |
117
+ | `ocrBackgroundMaxMs` | `0` | 后台评审的兜底回收窗口(`jobs.wait` 等到 deadline 即经注册表 `kill`);0 = 不设窗口,只有宿主退出、会话停止、插件卸载或显式 `job_kill` 才结束 |
100
118
  | `stdoutMaxBytes` | `400000` | shell 执行器的 stdout 缓冲上限,前台与后台共用同一字段 |
101
119
 
120
+ 第四枚非 volatile 字段是**部署决策**而不是调优值,理由与上面同一类(它改变一场运行对这台机器能做什么,不该放在一张用户可以在审查中途随手翻动的卡片上):
121
+
122
+ | 键 | 默认 | 用途 |
123
+ | ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
124
+ | `ocrNoAutoUpdate` | `false` | 置真时本包起的每条 `ocr` 命令都带 `OCR_NO_UPDATE=1`,拦住上游 npm 包装层在评审中途执行 `npm i -g @alibaba-group/open-code-review@<latest>`;置假保持上游行为(18 分钟冷却 + pid 锁)。版本探测不受本开关影响,恒定带上该变量 |
125
+
102
126
  ## 凭据与配置文件
103
127
 
104
128
  | 面 | 事实 |
@@ -113,13 +137,14 @@ allowBuilds:
113
137
 
114
138
  | 路由 | 方法 | 入参 | 成功回执 |
115
139
  | ---------------------------- | ---- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
116
- | `/_dsh/ocr-review/providers` | GET | — | `{providers(每条附 hasKey 布尔), current, getCredReady, source, csrf}`,不含任何 key 明文 |
140
+ | `/_dsh/ocr-review/providers` | GET | — | `{providers(每条附 hasKey 布尔), current, getCredReady, source, csrf, endpointVerify, ocrBinary}`,不含任何 key 明文。`ocrBinary` 是那份纯文件系统的可得性判定(`{source: "bundled" \| "path" \| "none", path?}`),每次请求现读;`endpointVerify` 是 apply 期那次 `ocr llm test` 的结论,没跑过则为 null |
117
141
  | `/_dsh/ocr-review/select` | POST | body `{provider, model}`,两者必须命中 `describe()` 给的清单 | `{ok, applied, llmTest}`(`autoVerify` 关闭时 `llmTest` 为 null) |
118
142
  | `/_dsh/ocr-review/migrate` | POST | — | `{ok, migrated, skippedUnknown}` |
119
143
  | `/_dsh/ocr-review/test` | POST | — | `{ok, output}`(output 先脱敏再截 8000 字符) |
120
- | 三个写端点的统一前置 | — | — | handler 第一句是 `guardTrust`(Host 权威 → `sec-fetch-site` 白名单 → `origin` 逐字比对,不过则 403 + `{ok:false,error:"untrusted host authority" \| "cross-origin request rejected"}`);非 POST 405 带 `Allow: POST` 与 `{ok:false,error:"POST only"}`;缺或错 `x-ocr-csrf` 头 403、body 超 8KB 413、流中断 400;token 每次插件 apply 重新生成,旧 token 随即失效 |
144
+ | `/_dsh/ocr-review/probe` | POST | — | `{ok, version, output, reason}`——起一次 `ocr version`(15 秒墙钟,恒定带 `OCR_NO_UPDATE=1`),再从它的输出里读版本串。命令没跑成功或读不出版本时 `version` 为 null;`reason` 点名失败那一档(找不到命令词、被墙钟收走、非零退出、跑通了但没有版本号——未发布构建就是最后一档);`output` 与 `/test` 同样先脱敏再截断,并剥掉终端控制序列后原样保留,好让上游那句升级提示还能被干净地复制 |
145
+ | 四个写端点的统一前置 | — | — | handler 第一句是 `guardTrust`(Host 权威 → `sec-fetch-site` 白名单 → `origin` 逐字比对,不过则 403 + `{ok:false,error:"untrusted host authority" \| "cross-origin request rejected"}`);非 POST 405 带 `Allow: POST` 与 `{ok:false,error:"POST only"}`;缺或错 `x-ocr-csrf` 头 403、body 超 8KB 413、流中断 400;token 每次插件 apply 重新生成,旧 token 随即失效 |
121
146
 
122
- 这四条路由随插件的注入子 fiber 生灭。官方 `webServer.register` 交回的是**必须自己调用**的释放器,且对重复路径当场抛错(installed dsh-host-webserver/lib/index.js:177-184),而路由表挂在宿主提供的 webServer 实例上、**不**随本插件卸载而死。本包把四枚释放器挂在 `inject(["webServer"])` 子 fiber 的效应上,于是:热重载(旧 fiber dispose ⇒ 新 apply)不会撞名而挂掉整张设置卡,禁用或卸载插件后这四条端点也不再可达。
147
+ 这五条路由随插件的注入子 fiber 生灭。官方 `webServer.register` 交回的是**必须自己调用**的释放器,且对重复路径当场抛错(installed dsh-host-webserver/lib/index.js:177-184),而路由表挂在宿主提供的 webServer 实例上、**不**随本插件卸载而死。本包把五枚释放器挂在 `inject(["webServer"])` 子 fiber 的效应上,于是:热重载(旧 fiber dispose ⇒ 新 apply)不会撞名而挂掉整张设置卡,禁用或卸载插件后这五条端点也不再可达。
123
148
 
124
149
  ## 后台作业面(官方 `ctx.jobs`)
125
150
 
@@ -130,9 +155,9 @@ allowBuilds:
130
155
  | 模型的观察面 | 多了三位:官方 `job_list` 列得出这次评审、`job_output` 读得到它的 stdout/stderr(注册表按自己的节奏从 `proc.observed` 拉,非消费式,与 `readOutput()` 游标互不争夺 — 官方 d.ts 明写 "without stealing bytes from `readOutput`")、`job_kill` 停得掉它。评审结果本身仍以 `ocr_session` 会话记录为准;工具回执的内容自换装起未变,其后随输出契约改造改为 canonical 对象直返(不再把回执 stringify 进 `{ text }` 双重编码) |
131
156
  | 坏掉的执行器读者 | 官方泵对"源抛错"的答法是只往宿主日志写一行、此后这一路读作空(实测)⇒ 本包额外写**一条**"这段不完整"进环,只写一次、也不再去敲坏掉的读者 |
132
157
  | controller 寿命 | 只挂在 `start` 那一瞬:官方件在"没有 controller 服务这个 owner"时拒绝 `start`,而 web 面宿主的 `tool-jobs` 在各 preset realm 内(宿主平面那一行被 disabled)⇒ 未拥有作业必须自己挂一枚。挂完立刻摘(实测:摘掉即恢复拒绝、后续 read/kill/落定都不依赖它)——常驻等于在"宿主故意没装 job 工具"的组成里替全宿主开着那道闸门 |
133
- | 三条取消路径 | 工具调用被中止、插件卸载、2 小时兜底 deadline 都是 `jobs.kill(id, undefined, reason)` 而不是绕过它杀进程 ⇒ 记录推到 `stopping`、reason 落进 `detail`(实测绕过那一层,名册里这条会永远停在 `running`)。三条可以叠加(实测对 `stopping` 再 kill 仍回 `requested`、`proc.kill()` 幂等)。reason 是模型可见文本 ⇒ 取双语字典 |
134
- | 取消链里不许抛 | `abort` 监听器里抛出的错误由 Node 以 `process.nextTick` 重抛 ⇒ 未捕获异常 ⇒ **整个宿主进程退出**(本机实跑复现:`Error: unknown job ...` + `EXIT=1`)。官方件也不替生产者兜:`killJob` 裸调 `job.cancel()`(installed dsh-jobs-local:611-622,只有 teardown 那一臂逐条 try/catch),注册表被重载/关掉时对陌生 id 抛(同文件 `:554-558`)。于是两层各兜一次:`cancel` 经 `killProc` 吞掉"进程早就不在了"那一类,`stop` 兜住注册表那一臂,兜不住时退化成直接杀我们自己的进程(各有一条用例,删掉任一层那条就红) |
135
- | 2 小时兜底 | 本包仍不给外部 CLI 套宿主 `timeoutMs`(长评审会被误砍),但"完全没有终点"在换装后有了新受害者:未拥有的 10 格桶是**全宿主共用**的(实测 `running` 与 `stopping` 都计数、满员直接拒绝新 `start`)⇒ 一条挂死的 CLI 会永久占一格。收法用注册表自己的 `wait(id, ms)` + `kill`,不用宿主 timer 也不裸 `setTimeout` |
158
+ | 三条回收路径 | 可选的兜底窗口、插件卸载、会话 dispose 都是 `jobs.kill(id, undefined, reason)` 而不是绕过它杀进程 ⇒ 记录推到 `stopping`、reason 落进 `detail`(实测绕过那一层,名册里这条会永远停在 `running`)。三条可以叠加(实测对 `stopping` 再 kill 仍回 `requested`、`proc.kill()` 幂等)。reason 是模型可见文本 ⇒ 取双语字典 |
159
+ | 取消链里不许抛 | 宿主事件处理器里抛出的错误由 Node 以 `process.nextTick` 重抛 ⇒ 未捕获异常 ⇒ **整个宿主进程退出**(本机实跑复现:`Error: unknown job ...` + `EXIT=1`)。官方件也不替生产者兜:`killJob` 裸调 `job.cancel()`(installed dsh-jobs-local:611-622,只有 teardown 那一臂逐条 try/catch),注册表被重载/关掉时对陌生 id 抛(同文件 `:554-558`)。于是两层各兜一次:`cancel` 经 `killProc` 吞掉"进程早就不在了"那一类,`stop` 兜住注册表那一臂,兜不住时退化成直接杀我们自己的进程(各有一条用例,删掉任一层那条就红) |
160
+ | 可选兜底窗口 | 本包仍不给外部 CLI 套宿主 `timeoutMs`(长评审会被误砍),但"完全没有终点"在换装后有了新受害者:未拥有的 10 格桶是**全宿主共用**的(实测 `running` 与 `stopping` 都计数、满员直接拒绝新 `start`)⇒ 一条挂死的 CLI 会永久占一格。收法用注册表自己的 `wait(id, ms)` + `kill`,不用宿主 timer 也不裸 `setTimeout` |
136
161
  | 未拥有的暴露面 | 官方件按 owner 隔离,未拥有作业任何 caller 都能 `list`/`read`/`kill`。这是本波已知并接受的暴露面(受有作业要 `dsh-agent` 注册表,实测不可得);回滚这次换装是唯一的收回方式 |
137
162
  | 落定记录窗口 | 注册表不会自行回收已落定的记录,而它随宿主活着、不随本插件卸载而死 ⇒ 本包在每次登记前剪掉 `ocr-review` 这一 kind 最老的已落定记录,剪到 9 条再起新的 ⇒ 在册任何时刻都不超过 10 条(每条最多带一只 256 KiB 的环)。卸载时两件事都做:活着的经注册表 kill,已落定的 remove —— 用户直接禁用本包就没有"下一次 apply"来剪窗口了。在跑与正在收的一条都不被剪 —— 官方件对超额的答法是**拒绝新 start**(实测未拥有桶 10),不是替我们杀评审 |
138
163
  | 没注册表没后台 | 宿主没装 `dsh-jobs-local` 时,`wait: false` 直接抛出点名缺件的错误,**不会**先把子进程起在没人看得见的地方。前台(`wait: true`)不受影响 |
@@ -143,14 +168,14 @@ allowBuilds:
143
168
  | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
144
169
  | 进程边界 | 被审查仓库的路径与 diff 内容进 `ocr` 子进程,OCR 再把它发给自己在 config.json 里配置的 LLM 端点;这条外发是审查功能本身,不经 dsh 宿主。明文 key 不进工具返回、不进 `/_dsh/*`,宿主侧 `resolve()` 拿到的值只用于确认「现在解析得到」,当场丢弃 |
145
170
  | 原始输出脱敏 | 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),要彻底清掉需自行删除该备份 |
171
+ | 写盘范围 | 本包写盘只有两处:外部 ocr CLI 的 `config.json`(含同目录随机后缀 `.tmp`、备份 `.bak`,以及 RMW 期间短暂存在、正常路径下即删的 `config.json.lock`)与系统临时目录下的 `ocr-review-*`/`ocr-scan-*`(读完即删)。迁移前的明文可能仍留在同一次写入产生的 `.bak` 里(权限 0600),要彻底清掉需自行删除该备份。这两处之外仍可能被改动的是 **CLI 自己那层 npm 包装**:`ocrNoAutoUpdate` 没置真时,一次 `ocr` 调用会触发上游把新版全局 `npm i -g` 装上。本包自己从不执行安装动作,版本探测那条恒定拦住这条路 |
147
172
 
148
173
  ## 常见问题
149
174
 
150
175
  | 现象 | 结论 |
151
176
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
152
177
  | exit 0 就以为评审成功 | exit 0 不等于评审成功:`status` 不在 success/complete/**partial**/completed_with_warnings/completed_with_errors/skipped 白名单内即按失败报(`{"error": …}` 属这类;partial 是 manifest 终态之一,部分文件失败但整体有覆盖,评论仍有效),解析失败时回显脱敏后的原文前 500 码元(UTF-16,一张 emoji 占 2 枚) |
153
- | 摘要是否覆盖了全部产出 | `droppedCount` 与 `invalidCommentCount` 同时为 0 才覆盖 OCR 全部产出,否则调大 `maxComments`(或置 0)再复核;stdout 侧另有 400000 字节上限,所以评审结果统一走 `--output` 临时文件 |
178
+ | 摘要是否覆盖了全部产出 | 两件不同的事,不能互相替代:`droppedCount` 与 `invalidCommentCount` 只说明**本摘要**有没有列全 OCR 产出的评论(否则调大 `maxComments` 或置 0 再复核)。**这一场**有没有把选中的文件审完看 `coverage`(`selected` 是分母,`completed`/`reused`/`failed`/`waived` 是它的互斥划分,`failedItems` 给出文件名与分类、原因)和 `budgetExceeded`(token 预算提前收工,`status` 里仍只写 `partial`),模型工具调用的失败数看 `toolCalls`。stdout 侧另有 400000 字节上限,所以评审结果统一走 `--output` 临时文件 |
154
179
  | dsh 退出时的后台进程 | dsh 以任何方式退出(含 kill -9 后被 launchd 收养)时,review/scan 的 OCR 进程树由命令内守护 TERM→1s→KILL 收走;取消工具调用即终止对应的后台进程 |
155
180
 
156
181
  ## 开发