@jayyuen66/dsh-ocr-review 0.1.6 → 0.1.7
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 +6 -1
- package/README.zh-CN.md +6 -1
- package/client.js +4 -4
- package/host.js +154 -48
- package/package.json +1 -1
- package/scripts/get-cred.mjs +6 -2
package/README.md
CHANGED
|
@@ -32,6 +32,11 @@ English · [中文](README.zh-CN.md)
|
|
|
32
32
|
- When either is missing the card shows the degradation reason while tools and settings keep working.
|
|
33
33
|
- The host must be `>= 0.2.1-rc.2`. That floor sits in `engines.dsh`, which is documentation only — no loader or installer reads it. What actually gates installation is the exact spelling of the four peer pins: the host compares every peer range against its own runtime version (prereleases participate) and refuses with `incompatible-version` when one does not match. On a machine still running dsh 0.2.1-rc.2, upgrade dsh first, or record an exemption for this package in the profile's `compatibility.json` (`dsh plugin --profile <profile> allow-version <name@version> --dsh-version <runtime version> --accept-risk`). 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.
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
## Shared version floor
|
|
37
|
+
|
|
38
|
+
If your profile pins `@jayyuen66/dsh-plugin-shared` with a pnpm `overrides` entry, that pin must be at least the `@jayyuen66/dsh-plugin-shared` version this package declares. A lower pin installs a copy without the names this package imports at runtime, and the host then reports `N entries did not activate` with `does not provide an export named ...` when it boots -- the package installs fine and never loads, and no per-repo CI run can see it.
|
|
39
|
+
|
|
35
40
|
## External CLI status and version
|
|
36
41
|
|
|
37
42
|
One judgement, `resolveOcrAvailability` in `lib/cli.ts`, answers "is there an `ocr` here, which copy, and how was it installed": the first executable found on `PATH` (with `PATHEXT` extensions on Windows), otherwise a copy inside this package's own dependency tree. "How it was installed" is read off the file the symlink in `bin` actually points at (`realpathSync`): anything under a `node_modules` is an npm-style global install, anything under `Cellar`/`Homebrew` is a formula, and neither of those is reported as unrecognised rather than guessed — the brew-vs-npm order matters because a formula-installed Node puts its own global npm packages under the same `homebrew` prefix. 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.
|
|
@@ -191,7 +196,7 @@ The external `ocr` subprocess started by `wait: false` is also registered as an
|
|
|
191
196
|
## Development
|
|
192
197
|
|
|
193
198
|
- `npm run check` is the single gate to run before publishing: typecheck (both tsconfigs) + `oxlint` + rebuild both bundles + `npm test` (vitest with coverage) + `oxfmt --check`.
|
|
194
|
-
- `npm test` runs with `--coverage` under a **100%
|
|
199
|
+
- `npm test` runs with `--coverage` under a **100% threshold** (lines/statements/branches/functions) over the coverage surface this repo opts into (`coverageInclude` in `vitest.config.ts`: `host.ts` + `lib/**`). Anything inside that surface not pinned by a test turns the suite red by design; the client half (`src/**`) is deliberately outside it, since those modules are exercised through the bundle and the settings-card tests rather than as units.
|
|
195
200
|
- `npm run build` / `npm run build:client` produce the published `host.js` / `client.js`. After editing `host.ts`, `lib/*` or `src/*` they must be rebuilt (`test/build-host.test.ts` pins bundle freshness against the sources, so a stale bundle fails the suite). Never hand-edit the bundles.
|
|
196
201
|
- The plugin source is TypeScript loaded directly by the host's cordis loader (Node ≥ 22.18 type stripping); the bundles exist for the published npm surface.
|
|
197
202
|
|
package/README.zh-CN.md
CHANGED
|
@@ -60,6 +60,11 @@ dsh plugin --profile web add @jayyuen66/dsh-ocr-review
|
|
|
60
60
|
- 包在公共 npm 上,安装不需要凭据。
|
|
61
61
|
- 发布面只含 `host.js`、`client.js`、`cordis.patch.yml`、`scripts` 与 `THIRD_PARTY_NOTICES.md`(随包运行时依赖的第三方许可证;`prepack` 重建两个 bundle),源码仓见 package.json 的 `repository.url`。
|
|
62
62
|
|
|
63
|
+
|
|
64
|
+
## shared 的版本下限
|
|
65
|
+
|
|
66
|
+
如果你的 profile 用 pnpm `overrides` 把 `@jayyuen66/dsh-plugin-shared` 钉在某个版本,那个钉住的值必须不低于本包声明的 `@jayyuen66/dsh-plugin-shared` 版本。钉得更低就会装到一份缺少本包运行期所导入的名字的产物,宿主起来时报 `N entries did not activate` 加 `does not provide an export named ...`——包装得上、永远不生效,而各仓单仓 CI 看不见这一类。
|
|
67
|
+
|
|
63
68
|
## 外部 CLI 的安装与升级
|
|
64
69
|
|
|
65
70
|
本包只 spawn `ocr`,不安装它。两条命令任选:
|
|
@@ -191,7 +196,7 @@ npm i -g @alibaba-group/open-code-review@1.13.0 # 钉在本包核对的那
|
|
|
191
196
|
## 开发
|
|
192
197
|
|
|
193
198
|
- `npm run check` 是发布前的一条门:typecheck(两侧 tsconfig)+ `oxlint` + 重建两个 bundle + `npm test`(vitest 带 coverage)+ `oxfmt --check`。
|
|
194
|
-
- `npm test` 带 `--coverage
|
|
199
|
+
- `npm test` 带 `--coverage`,阈值 **100%**(lines/statements/branches/functions),但分母是本仓 `vitest.config.ts` 里 `coverageInclude` 选定的那一面(`host.ts` + `lib/**`)——那一面里任何一行或分支没被用例钉住就当场红。客户端半(`src/**`)**刻意在分母外**:那几份模块是经 bundle 与设置卡用例覆盖的,不是按单元测的。
|
|
195
200
|
- `npm run build` / `npm run build:client` 产出发布物 `host.js` / `client.js`;改动 `host.ts`、`lib/*` 或 `src/*` 后必须重建(`test/build-host.test.ts` 钉了产物与源码的新鲜度,过期产物会让套件变红)。发布物不手改。
|
|
196
201
|
- 插件源码是 TS,由宿主的 cordis Loader 直接加载(Node ≥ 22.18 类型剥离);bundle 只为 npm 发布面存在。
|
|
197
202
|
|
package/client.js
CHANGED
|
@@ -37,7 +37,7 @@ const UI_MESSAGES = {
|
|
|
37
37
|
noProvider: "当前未配置 provider",
|
|
38
38
|
noReason: "宿主未给出原因(重启 dsh 或重试可恢复)",
|
|
39
39
|
readProvidersFailed: "未能从 dsh 配置服务读取 provider 列表:{reason}",
|
|
40
|
-
endpointVerifyFailed: "
|
|
40
|
+
endpointVerifyFailed: "装载期端点自检未通过:{reason}(这条是端点给的回执,不是本插件的故障;配置照样能改,也可点「测试连接」重跑)",
|
|
41
41
|
applied: "已应用:{provider} / {model}\n{test}",
|
|
42
42
|
notVerified: "(未自动验证,可点「测试连接」手动验证)",
|
|
43
43
|
llmTestPassed: "✓ ocr llm test 通过",
|
|
@@ -63,7 +63,7 @@ const UI_MESSAGES = {
|
|
|
63
63
|
maxCommentsLabel: "摘要评论条数 (maxComments)",
|
|
64
64
|
maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认 12)",
|
|
65
65
|
timeoutLabel: "运行超时 (timeoutMinutes)",
|
|
66
|
-
timeoutHint: "前台墙钟超时(分钟);0 = 宿主上限(shell.maxTimeoutMs,当前 60 分钟);更长的评审用工具 wait=false 后台模式(不设宿主 deadline
|
|
66
|
+
timeoutHint: "前台墙钟超时(分钟);0 = 宿主上限(shell.maxTimeoutMs,当前 60 分钟);更长的评审用工具 wait=false 后台模式(不设宿主 deadline,兜底窗口看设置项 ocrBackgroundMaxMs,默认 0 = 不设窗口)(默认 0)",
|
|
67
67
|
configPathLabel: "OCR 配置文件 (ocrConfigPath)",
|
|
68
68
|
configPathHint: "外部 open-code-review CLI 自己的配置位置;留空 = ~/.opencodereview/config.json。自定义时必须是 <X>/.opencodereview/config.json 布局(OCR 只认该形状,插件经 HOME 重定向让它生效)",
|
|
69
69
|
settingsLoading: "设置加载中,稍候可改",
|
|
@@ -121,7 +121,7 @@ const UI_MESSAGES = {
|
|
|
121
121
|
noProvider: "No provider configured yet",
|
|
122
122
|
noReason: "the host gave no reason (restart dsh or retry to recover)",
|
|
123
123
|
readProvidersFailed: "Could not read the provider list from the dsh settings service: {reason}",
|
|
124
|
-
endpointVerifyFailed: "Startup endpoint check
|
|
124
|
+
endpointVerifyFailed: "Startup endpoint check did not pass: {reason} (this is the endpoint's reply, not a fault of this plugin; the config stays editable, and “Test connection” reruns it)",
|
|
125
125
|
applied: "Applied: {provider} / {model}\n{test}",
|
|
126
126
|
notVerified: "(not verified automatically — use “Test connection” to check manually)",
|
|
127
127
|
llmTestPassed: "✓ ocr llm test passed",
|
|
@@ -147,7 +147,7 @@ const UI_MESSAGES = {
|
|
|
147
147
|
maxCommentsLabel: "Summary comment cap (maxComments)",
|
|
148
148
|
maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default 12)",
|
|
149
149
|
timeoutLabel: "Run timeout (timeoutMinutes)",
|
|
150
|
-
timeoutHint: "Foreground wall-clock timeout in minutes; 0 = host cap (shell.maxTimeoutMs, currently 60 min); for longer reviews use the tool's wait=false background mode (no host deadline,
|
|
150
|
+
timeoutHint: "Foreground wall-clock timeout in minutes; 0 = host cap (shell.maxTimeoutMs, currently 60 min); for longer reviews use the tool's wait=false background mode (no host deadline; the backstop window is the ocrBackgroundMaxMs setting, default 0 = none) (default 0)",
|
|
151
151
|
configPathLabel: "OCR config file (ocrConfigPath)",
|
|
152
152
|
configPathHint: "Where the external open-code-review CLI keeps its own config; empty = ~/.opencodereview/config.json. A custom value must follow the <X>/.opencodereview/config.json layout (OCR only reads that shape; the plugin makes it effective via a HOME redirect)",
|
|
153
153
|
settingsLoading: "settings still loading — editable in a moment",
|
package/host.js
CHANGED
|
@@ -41,15 +41,19 @@ const MESSAGES = {
|
|
|
41
41
|
missingRequiredArgs: "ocr {name}: 缺少必填参数 {keys}(缺失即拒绝,不产生副作用)",
|
|
42
42
|
bodyMustBeObject: "body 必须是 JSON 对象",
|
|
43
43
|
outputFileMissing: "ocr 未写出 --output 文件({path}):{cause}——请确认 ocr 版本支持 --output,或用 ocr_session 查询会话结果",
|
|
44
|
+
outputFileTooLarge: "ocr 的 --output 文件过大({path},{size} 字节 > {limit} 字节上限):不整份读进内存。请缩小审查范围(exclude / --commit / 更小的目录)后重试,或用 ocr_session 查这一场的结果",
|
|
45
|
+
failureCoverage: "\n这一场未完成的文件(取自 OCR 已发布的 --output):{items};完整清单见 ocr_session(action=comments/show)",
|
|
44
46
|
settingsServiceGone: "{ns} 所在的设置服务在读取过程中已不可用",
|
|
45
47
|
repoRequired: "repo 必填,且为 Git 仓库绝对路径",
|
|
46
48
|
repoRequiredUnknownWorkspace: "repo 必填:当前会话工作区未知,请显式传入 Git 仓库绝对路径",
|
|
47
49
|
repoMustBeAbsolute: "repo 必须是绝对路径(以 / 开头):{root}",
|
|
48
50
|
noNulBytes: "{name} 不能包含 NUL 字节",
|
|
51
|
+
listTruncated: "{name} 超过 {limit} 条上限,已丢弃 {dropped} 条——审查范围会因此小于你给的清单",
|
|
49
52
|
mustBeString: "{name} 必须是字符串(收到 {received})",
|
|
50
53
|
listItemsMustBeString: "{name} 数组元素必须是字符串",
|
|
51
54
|
mustBeStringOrList: "{name} 必须是字符串或字符串数组",
|
|
52
55
|
intFlagMustBeInt: "{flag} 需要为 ≥{min} 的整数,收到 {received}",
|
|
56
|
+
formatNotJson: "本包只支持 --format json(收到 {format}):text/sarif 没有解析层,交下去只会把参数错报成「OCR 输出解析失败」",
|
|
53
57
|
scopeBranchCommitExclusive: "scope=branch/from/to 与 commit 互斥(一次只能选一种审查范围)",
|
|
54
58
|
branchNeedsFromAndTo: "scope=branch 需要同时提供 from 与 to",
|
|
55
59
|
commitNeedsValue: "scope=commit 需要 commit 参数",
|
|
@@ -114,13 +118,13 @@ const MESSAGES = {
|
|
|
114
118
|
paramBackground: "需求/业务上下文(官方 skill 推荐:调用前先用 git log/diff/提交信息快速分析变更意图,提取 3-5 句业务背景传入,是提升评审质量最有效的参数;从其它 agent 调用时始终携带 PR 描述/需求)",
|
|
115
119
|
paramBackgroundShort: "需求/业务上下文",
|
|
116
120
|
paramBackgroundPreview: "业务上下文(会出现在输出中供审查参考)",
|
|
117
|
-
paramExclude: "gitignore
|
|
118
|
-
paramExcludeShort: "
|
|
119
|
-
paramWait: "
|
|
121
|
+
paramExclude: "gitignore 风格排除模式(可多值;上限 2000 条,超出会直接报错而不是静默丢弃)",
|
|
122
|
+
paramExcludeShort: "排除模式(上限 2000 条,超出会报错)",
|
|
123
|
+
paramWait: "false(缺省)=后台启动、立即返回作业 id,用 ocr_session 轮询结果——一次 review 是分钟级长任务,阻塞等待会占住整轮;true=前台阻塞到完成并直接返回聚合摘要。后台模式的兜底窗口由设置项 ocrBackgroundMaxMs 给出(默认 0 = 不设窗口)。两种模式都带宿主回收守护:dsh 退出(含 kill -9)时 OCR 进程一并结束",
|
|
120
124
|
paramProvider: "本次运行临时指定已配置 provider",
|
|
121
125
|
paramModel: "本次运行临时覆盖 model",
|
|
122
126
|
paramResume: "从之前中断的会话 id 恢复(会话 id 在返回结果里)",
|
|
123
|
-
paramScanPath: "
|
|
127
|
+
paramScanPath: "仓库相对目录或文件(缺省整仓,可多值;上限 2000 条,超出会直接报错)",
|
|
124
128
|
paramBatch: "分批策略",
|
|
125
129
|
paramPaths: "待查询的仓库相对文件路径(至少 1 个,最多 200)",
|
|
126
130
|
paramSessionAction: "list:列最近会话(缺省)/ show:查看单个 / comments:取评论",
|
|
@@ -131,6 +135,7 @@ const MESSAGES = {
|
|
|
131
135
|
tuningConcurrency: "并发{unit}数(OCR --concurrency;缺省不传=OCR 原生 8)",
|
|
132
136
|
tuningTimeout: "每{unit}任务超时分钟(OCR --timeout;0=不限时;缺省不传=OCR 原生 15)",
|
|
133
137
|
tuningMaxTools: "每{unit}工具调用轮数上限(OCR --max-tools;0=模板默认;1-49 会被 OCR 抬到 50,帮助文本 min 50;缺省不传=模板默认)",
|
|
138
|
+
tuningMaxToolsScan: "每{unit}工具调用轮数上限(OCR --max-tools;0=模板默认;只在大于模板默认值时生效,不抬到 50;缺省不传=模板默认)",
|
|
134
139
|
tuningMaxTokens: "每{unit}提示词 token 上限(OCR --max-tokens;0=配置/模板默认;缺省不传=模板默认)",
|
|
135
140
|
tuningMaxTokensBudget: "本次运行 total token(input+output)预算闸门,超出即停止派发部分结果(OCR --max-tokens-budget;0=不限;缺省不传=不限)",
|
|
136
141
|
previewHint: "对每个 reviewable 文件:用 git diff <merge_base>..<to> -- <path>(branch)/git show <commit> -- <path>(commit)/ git diff HEAD -- <path> 或直接读文件(workspace 新文件)取 diff。",
|
|
@@ -185,15 +190,19 @@ const MESSAGES = {
|
|
|
185
190
|
missingRequiredArgs: "ocr {name}: missing required parameter(s) {keys} (rejected outright, no side effects)",
|
|
186
191
|
bodyMustBeObject: "body must be a JSON object",
|
|
187
192
|
outputFileMissing: "ocr did not write the --output file ({path}): {cause} — check that your ocr version supports --output, or query the session with ocr_session",
|
|
193
|
+
outputFileTooLarge: "the ocr --output file is too large ({path}, {size} bytes > {limit}-byte cap): refusing to read it whole. Narrow the review scope (exclude / --commit / smaller directory) and retry, or read this run's results with ocr_session",
|
|
194
|
+
failureCoverage: "\nfiles that did not finish in this run (from the --output OCR already published): {items}; full list via ocr_session(action=comments/show)",
|
|
188
195
|
settingsServiceGone: "the settings service holding {ns} became unavailable while being read",
|
|
189
196
|
repoRequired: "repo is required and must be the absolute path of a Git repository",
|
|
190
197
|
repoRequiredUnknownWorkspace: "repo is required: the current session workspace is unknown, pass the absolute path of a Git repository explicitly",
|
|
191
198
|
repoMustBeAbsolute: "repo must be an absolute path (starting with /): {root}",
|
|
199
|
+
listTruncated: "{name} exceeds the {limit}-item cap; {dropped} item(s) were dropped - the review will cover less than the list you passed",
|
|
192
200
|
noNulBytes: "{name} must not contain NUL bytes",
|
|
193
201
|
mustBeString: "{name} must be a string (got {received})",
|
|
194
202
|
listItemsMustBeString: "the items of {name} must be strings",
|
|
195
203
|
mustBeStringOrList: "{name} must be a string or an array of strings",
|
|
196
204
|
intFlagMustBeInt: "{flag} must be an integer ≥ {min}, got {received}",
|
|
205
|
+
formatNotJson: "this package supports --format json only (got {format}): text/sarif have no parser here, so passing them through reports a bad parameter as an OCR output failure",
|
|
197
206
|
scopeBranchCommitExclusive: "scope=branch/from/to and commit are mutually exclusive (only one review scope at a time)",
|
|
198
207
|
branchNeedsFromAndTo: "scope=branch requires both from and to",
|
|
199
208
|
commitNeedsValue: "scope=commit requires the commit parameter",
|
|
@@ -258,13 +267,13 @@ const MESSAGES = {
|
|
|
258
267
|
paramBackground: "Requirements/business context (official skill recommends: analyse the change intent via git log/diff/commit messages first and pass 3-5 sentences of context — the single most effective parameter for review quality; always carry the PR description/requirements when called from another agent)",
|
|
259
268
|
paramBackgroundShort: "Requirements/business context",
|
|
260
269
|
paramBackgroundPreview: "Business context (it shows up in the output for review reference)",
|
|
261
|
-
paramExclude: "gitignore-style exclusion patterns (multiple allowed)",
|
|
262
|
-
paramExcludeShort: "Exclusion patterns",
|
|
263
|
-
paramWait: "
|
|
270
|
+
paramExclude: "gitignore-style exclusion patterns (multiple allowed; cap 2000 - exceeding it is an error, never a silent drop)",
|
|
271
|
+
paramExcludeShort: "Exclusion patterns (cap 2000; exceeding it errors out)",
|
|
272
|
+
paramWait: "false (default) = start in the background, return the job id immediately and poll with ocr_session - a review is a minutes-long task and blocking the call ties up the whole turn; true = block until it finishes and return the aggregated summary directly. The background backstop window is the ocrBackgroundMaxMs setting (default 0 = none). Both modes carry the host reaper guard: when dsh exits (including kill -9) the OCR process is terminated too",
|
|
264
273
|
paramProvider: "Temporarily use a configured provider for this run",
|
|
265
274
|
paramModel: "Temporarily override the model for this run",
|
|
266
275
|
paramResume: "Resume from a previously interrupted session id (the id is in the result)",
|
|
267
|
-
paramScanPath: "Repo-relative directories or files (default: whole repo, multiple allowed)",
|
|
276
|
+
paramScanPath: "Repo-relative directories or files (default: whole repo, multiple allowed; cap 2000 - exceeding it is an error, never a silent drop)",
|
|
268
277
|
paramBatch: "Batching strategy",
|
|
269
278
|
paramPaths: "Repo-relative file paths to look up (at least 1, at most 200)",
|
|
270
279
|
paramSessionAction: "list: recent sessions (default) / show: one session / comments: comment list",
|
|
@@ -275,6 +284,7 @@ const MESSAGES = {
|
|
|
275
284
|
tuningConcurrency: "Concurrent {unit} count (OCR --concurrency; unset = OCR native 8)",
|
|
276
285
|
tuningTimeout: "Per-{unit} task timeout in minutes (OCR --timeout; 0 = unlimited; unset = OCR native 15)",
|
|
277
286
|
tuningMaxTools: "Per-{unit} tool-call round cap (OCR --max-tools; 0 = template default; 1-49 is raised to 50 by OCR, help text says min 50; unset = template default)",
|
|
287
|
+
tuningMaxToolsScan: "Per-{unit} tool-call round cap (OCR --max-tools; 0 = template default; only takes effect above the template default, never raised to 50; unset = template default)",
|
|
278
288
|
tuningMaxTokens: "Per-{unit} prompt token cap (OCR --max-tokens; 0 = config/template default; unset = template default)",
|
|
279
289
|
tuningMaxTokensBudget: "Total token (input+output) budget gate for this run; dispatch stops once exceeded (OCR --max-tokens-budget; 0 = unlimited; unset = unlimited)",
|
|
280
290
|
previewHint: "For each reviewable file, get its diff with git diff <merge_base>..<to> -- <path> (branch) / git show <commit> -- <path> (commit) / git diff HEAD -- <path>, or read the file directly (new workspace files).",
|
|
@@ -550,12 +560,18 @@ function listSourceItems(value, name, messages) {
|
|
|
550
560
|
function takeTrimmedItems(items, name, limit, messages) {
|
|
551
561
|
const cap = Number.isSafeInteger(limit) && limit > 0 ? limit : DEFAULT_LIST_LIMIT;
|
|
552
562
|
const out = [];
|
|
563
|
+
let dropped = 0;
|
|
553
564
|
for (const item of items) {
|
|
554
565
|
if (item.includes("\0")) throw new Error(format(messages.noNulBytes, { name }));
|
|
555
566
|
const trimmed = item.trim();
|
|
556
|
-
if (trimmed.length > 0
|
|
557
|
-
if (
|
|
567
|
+
if (trimmed.length > 0 && out.length >= cap) dropped += 1;
|
|
568
|
+
else if (trimmed.length > 0) out.push(trimmed);
|
|
558
569
|
}
|
|
570
|
+
if (dropped > 0) throw new Error(format(messages.listTruncated, {
|
|
571
|
+
name,
|
|
572
|
+
dropped: String(dropped),
|
|
573
|
+
limit: String(cap)
|
|
574
|
+
}));
|
|
559
575
|
return out;
|
|
560
576
|
}
|
|
561
577
|
/** 字符串数组 → 逗号拼接(--exclude/--path 契约)。
|
|
@@ -589,6 +605,11 @@ function resolveOutputFormat(value) {
|
|
|
589
605
|
//#region lib/cli.ts
|
|
590
606
|
/** `ocr review|scan|delegate preview` 的背景说明 flag(三处命令构造共用一条)。
|
|
591
607
|
* pushTextFlag 按 `flag.slice(2)` 反推报错用的参数名,故值必须带 `--` 前缀。 */
|
|
608
|
+
/** `--exclude` 的模式条数上限。**不是审查范围的上限**,只是防一条失控输入把命令串撑爆;
|
|
609
|
+
* 真实使用里几十条就到头,给到 2000 是为了"尽量不截断"——超了会明确告警,不静默丢。 */
|
|
610
|
+
const EXCLUDE_LIST_LIMIT = 2e3;
|
|
611
|
+
/** `--path` 的路径条数上限(同 EXCLUDE_LIST_LIMIT 的取向:够大 + 超限告警)。 */
|
|
612
|
+
const PATH_LIST_LIMIT = 2e3;
|
|
592
613
|
const BACKGROUND_FLAG = "--background";
|
|
593
614
|
/**
|
|
594
615
|
* 本包的契约是按外部 `ocr` CLI 的哪一版核对的(README 的前置条件、装法建议与工具回显里
|
|
@@ -629,29 +650,40 @@ function idValue(value, name, messages) {
|
|
|
629
650
|
if (trimmed.includes("\0")) throw new Error(format(messages.noNulBytes, { name }));
|
|
630
651
|
return trimmed;
|
|
631
652
|
}
|
|
653
|
+
/** 纯十进制整数的文本形态:数值槽位接受的唯一字符串写法。 */
|
|
654
|
+
const DECIMAL_INTEGER_TEXT = /^\d+$/u;
|
|
655
|
+
/**
|
|
656
|
+
* 数值槽位的**唯一**取值口:只接受 `number` 与纯十进制数字串,其余一律 null。
|
|
657
|
+
*
|
|
658
|
+
* 为什么单列一份:`Number()` 会把 true 当 1、把 [] 当 0、把 "0x10" 当 16、把 "1e2" 当 100,
|
|
659
|
+
* 于是错类型或怪写法的输入会被静默读成**另一个数**。数值 flag 与 session limit 两处槽位
|
|
660
|
+
* 共用这一条判据,包里就不会存在第二种数值口径(此前 session limit 用 `Number()`,
|
|
661
|
+
* `{limit: "0x10"}` 被读成 16)。
|
|
662
|
+
* 非安全整数(1e300、1e21)与非法形态同样 null,由调用点按各自的范围判据拒绝。
|
|
663
|
+
*/
|
|
664
|
+
function decimalInteger(value) {
|
|
665
|
+
const isDecimalText = typeof value === "string" && DECIMAL_INTEGER_TEXT.test(value.trim());
|
|
666
|
+
if (typeof value !== "number" && !isDecimalText) return null;
|
|
667
|
+
const num = Number(value);
|
|
668
|
+
return Number.isSafeInteger(num) ? num : null;
|
|
669
|
+
}
|
|
632
670
|
/**
|
|
633
671
|
* 数值 flag 忠实透传(桥接层不设钳制——OCR 原生语义:0 = 配置/模板默认或不限,
|
|
634
672
|
* 负数/非整数由 OCR 侧报错,但桥接层先做同源校验给出更早的错误)。
|
|
635
673
|
* value 为 undefined/null 时省略 flag(跟随 OCR flag 默认值)。
|
|
636
674
|
*
|
|
637
675
|
* 注入面:数值经 `String(num)` 落进命令串(不走 shq)。数值 token 结构上不可能带
|
|
638
|
-
* shell
|
|
676
|
+
* shell 元字符;安全整数经 String() 只产出 `-?[0-9]+`,而 min ≥ 0 挡掉负号。
|
|
639
677
|
*/
|
|
640
678
|
function pushIntFlag(parts, flag, value, min, messages) {
|
|
641
679
|
if (value === void 0 || value === null) return;
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const decimalString = typeof value === "string" && /^\d+$/u.test(value.trim());
|
|
650
|
-
if (typeof value !== "number" && !decimalString) reject();
|
|
651
|
-
const num = Number(value);
|
|
652
|
-
const text = String(num);
|
|
653
|
-
if (!Number.isSafeInteger(num) || num < min || !/^\d+$/u.test(text)) reject();
|
|
654
|
-
parts.push(flag, text);
|
|
680
|
+
const num = decimalInteger(value);
|
|
681
|
+
if (num === null || num < min) throw new Error(format(messages.intFlagMustBeInt, {
|
|
682
|
+
flag,
|
|
683
|
+
min: String(min),
|
|
684
|
+
received: describeValue(value)
|
|
685
|
+
}));
|
|
686
|
+
parts.push(flag, String(num));
|
|
655
687
|
}
|
|
656
688
|
/** 模式参数解析:workspace(默认)/commit/branch 互斥,非法组合与错类型抛错。
|
|
657
689
|
*
|
|
@@ -695,7 +727,7 @@ function pushTextFlag(parts, flag, value, messages) {
|
|
|
695
727
|
* buildReviewCommand 在拿到 scopeFlags 结果后前置校验。 */
|
|
696
728
|
function optionalFlags(args, messages) {
|
|
697
729
|
const parts = [];
|
|
698
|
-
const excludes = commaList(args.exclude, "exclude",
|
|
730
|
+
const excludes = commaList(args.exclude, "exclude", EXCLUDE_LIST_LIMIT, messages);
|
|
699
731
|
if (excludes.length > 0) parts.push("--exclude", shq(excludes.join(",")));
|
|
700
732
|
pushTextFlag(parts, BACKGROUND_FLAG, args.background, messages);
|
|
701
733
|
pushTextFlag(parts, "--provider", args.provider, messages);
|
|
@@ -944,6 +976,18 @@ function parseOcrVersion(text) {
|
|
|
944
976
|
return /open-code-review\s+v?(?<version>\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)/u.exec(text)?.groups?.["version"] ?? null;
|
|
945
977
|
}
|
|
946
978
|
/**
|
|
979
|
+
* `--format` 交给 OCR 的取值:本包只认 json——`lib/parse.ts` 的解析层只解 JSON 一份形状。
|
|
980
|
+
*
|
|
981
|
+
* 白名单(`resolveOutputFormat`)管的是注入面,它放行的 text/sarif 在这里被拒:
|
|
982
|
+
* 那两个值没有下游,交下去只会换回一条「OCR 输出解析失败」,把「参数不在本包支持面内」
|
|
983
|
+
* 说成 OCR 的错。`format` 不在任何工具的 parameters 里,走到这里的只有 PTC/直连调用方。
|
|
984
|
+
*/
|
|
985
|
+
function jsonOnlyFormat(value, messages) {
|
|
986
|
+
const outputFormat = resolveOutputFormat(value);
|
|
987
|
+
if (outputFormat !== "json") throw new Error(format(messages.formatNotJson, { format: outputFormat }));
|
|
988
|
+
return outputFormat;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
947
991
|
* 构造 `ocr review` 命令。--output 固定为调用方给定的临时文件路径(防截断),
|
|
948
992
|
* --audience agent --format json 是默认(结构化、无进度行)。
|
|
949
993
|
* outputPath 缺省(后台模式)省略 --output:结果以 OCR 会话记录为准。
|
|
@@ -951,13 +995,14 @@ function parseOcrVersion(text) {
|
|
|
951
995
|
*/
|
|
952
996
|
function buildReviewCommand(args, outputPath, messages) {
|
|
953
997
|
const repo = assertAbsoluteRoot(args.repo, messages);
|
|
998
|
+
const outputFormat = jsonOnlyFormat(args.format, messages);
|
|
954
999
|
const parts = [
|
|
955
1000
|
ocrCommand(),
|
|
956
1001
|
"review",
|
|
957
1002
|
"--audience",
|
|
958
1003
|
"agent",
|
|
959
1004
|
"--format",
|
|
960
|
-
|
|
1005
|
+
outputFormat
|
|
961
1006
|
];
|
|
962
1007
|
const effort = clampEffort(args.effort, "medium");
|
|
963
1008
|
parts.push("--effort", effort);
|
|
@@ -985,17 +1030,18 @@ function buildReviewCommand(args, outputPath, messages) {
|
|
|
985
1030
|
* outputPath 缺省(后台模式)省略 --output:结果以 OCR 会话记录为准。 */
|
|
986
1031
|
function buildScanCommand(args, outputPath, messages) {
|
|
987
1032
|
const repo = assertAbsoluteRoot(args.repo, messages);
|
|
1033
|
+
const outputFormat = jsonOnlyFormat(args.format, messages);
|
|
988
1034
|
const parts = [
|
|
989
1035
|
ocrCommand(),
|
|
990
1036
|
"scan",
|
|
991
1037
|
"--audience",
|
|
992
1038
|
"agent",
|
|
993
1039
|
"--format",
|
|
994
|
-
|
|
1040
|
+
outputFormat
|
|
995
1041
|
];
|
|
996
|
-
const paths = commaList(args.path, "path",
|
|
1042
|
+
const paths = commaList(args.path, "path", PATH_LIST_LIMIT, messages);
|
|
997
1043
|
if (paths.length > 0) parts.push("--path", shq(paths.join(",")));
|
|
998
|
-
const excludes = commaList(args.exclude, "exclude",
|
|
1044
|
+
const excludes = commaList(args.exclude, "exclude", EXCLUDE_LIST_LIMIT, messages);
|
|
999
1045
|
if (excludes.length > 0) parts.push("--exclude", shq(excludes.join(",")));
|
|
1000
1046
|
pushTextFlag(parts, "--batch", args.batch, messages);
|
|
1001
1047
|
pushTextFlag(parts, "--provider", args.provider, messages);
|
|
@@ -1027,7 +1073,7 @@ function buildDelegatePreviewCommand(args, messages) {
|
|
|
1027
1073
|
"json",
|
|
1028
1074
|
...scopeFlags(scopeArgs, messages)
|
|
1029
1075
|
];
|
|
1030
|
-
const excludes = commaList(args.exclude, "exclude",
|
|
1076
|
+
const excludes = commaList(args.exclude, "exclude", EXCLUDE_LIST_LIMIT, messages);
|
|
1031
1077
|
if (excludes.length > 0) parts.push("--exclude", shq(excludes.join(",")));
|
|
1032
1078
|
pushTextFlag(parts, BACKGROUND_FLAG, args.background, messages);
|
|
1033
1079
|
return {
|
|
@@ -1065,10 +1111,8 @@ const SESSION_ACTIONS = [
|
|
|
1065
1111
|
* 「只看一条」。数字字符串接受("3"),与 pushIntFlag 的数值槽位同一口径。 */
|
|
1066
1112
|
function sessionLimit(value, messages) {
|
|
1067
1113
|
if (value === void 0 || value === null) return 10;
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
1070
|
-
else if (typeof value === "string") num = Number(value);
|
|
1071
|
-
if (!Number.isInteger(num) || num < 1 || num > 100) throw new Error(format(messages.sessionLimitRange, { received: describeValue(value) }));
|
|
1114
|
+
const num = decimalInteger(value);
|
|
1115
|
+
if (num === null || num < 1 || num > 100) throw new Error(format(messages.sessionLimitRange, { received: describeValue(value) }));
|
|
1072
1116
|
return num;
|
|
1073
1117
|
}
|
|
1074
1118
|
/** 构造 `ocr session list/show/comments` 命令(历史评审会话闭环,Json 输出)。 */
|
|
@@ -1176,6 +1220,7 @@ const REVIEW_STATUSES = [
|
|
|
1176
1220
|
"success",
|
|
1177
1221
|
"complete",
|
|
1178
1222
|
"partial",
|
|
1223
|
+
"failed",
|
|
1179
1224
|
"completed_with_warnings",
|
|
1180
1225
|
"completed_with_errors",
|
|
1181
1226
|
"skipped"
|
|
@@ -1539,7 +1584,13 @@ function toDelegateFiles(raw) {
|
|
|
1539
1584
|
}).filter((fileEntry) => fileEntry !== null);
|
|
1540
1585
|
return raw.length > 0 && files.length === 0 ? null : files;
|
|
1541
1586
|
}
|
|
1542
|
-
/** excluded_files 同款门:条目形状与 reviewable 一致,缺 path 即不可用。
|
|
1587
|
+
/** excluded_files 同款门:条目形状与 reviewable 一致,缺 path 即不可用。
|
|
1588
|
+
*
|
|
1589
|
+
* 理由的键名以 CLI 侧 `delegatePreviewFileJSON` 为准,那是 `exclude_reason`
|
|
1590
|
+
* (`delegate_cmd.go` 的 `ExcludeReason string \`json:"exclude_reason,omitempty"\``)。
|
|
1591
|
+
* 读 `reason` 是照自己脑补的形状写的:真回执里那个键不存在,`str()` 把缺失读成空串,
|
|
1592
|
+
* 于是「被排除文件及原因」永远只有文件没有原因——schema 与工具描述都在承诺后者。
|
|
1593
|
+
* `reason` 作回退保留:形如 `{"reason":…}` 的旧形态/别家产物仍读得出来。 */
|
|
1543
1594
|
function toExcludedFiles(raw) {
|
|
1544
1595
|
if (!Array.isArray(raw)) return [];
|
|
1545
1596
|
const files = raw.map((entry) => {
|
|
@@ -1548,7 +1599,7 @@ function toExcludedFiles(raw) {
|
|
|
1548
1599
|
if (entryPath === "") return null;
|
|
1549
1600
|
return {
|
|
1550
1601
|
path: entryPath,
|
|
1551
|
-
reason: str(entry["reason"])
|
|
1602
|
+
reason: str(entry["exclude_reason"]) || str(entry["reason"])
|
|
1552
1603
|
};
|
|
1553
1604
|
}).filter((fileEntry) => fileEntry !== null);
|
|
1554
1605
|
return raw.length > 0 && files.length === 0 ? null : files;
|
|
@@ -2457,6 +2508,10 @@ const LLM_TEST_TIMEOUT_MS = 9e4;
|
|
|
2457
2508
|
* 人按下「检测」时跑,不进任何热路径,因此不做成设置项。 */
|
|
2458
2509
|
const VERSION_PROBE_TIMEOUT_MS = 15e3;
|
|
2459
2510
|
const STDOUT_MAX_BYTES = 4e5;
|
|
2511
|
+
/** `--output` 落盘结果的读取上限(字节)。**刻意不取 STDOUT_MAX_BYTES**:`--output` 存在的
|
|
2512
|
+
* 理由正是「结果比 stdout 缓冲大」,用 400 KB 卡它等于把正常的大仓评审判死。这一枚只是
|
|
2513
|
+
* 护栏,把「整份读进内存」的失败形态从 OOM 换成一条点名文件的错误。 */
|
|
2514
|
+
const OUT_FILE_MAX_BYTES = 67108864;
|
|
2460
2515
|
/** 解析失败时回显给模型的原始内容长度上限(UTF-16 码元;已脱敏)。
|
|
2461
2516
|
* 单位写码元而不是"字符":本常量喂给 `echoRaw`→`truncateEnd`,那一刀按码元数计
|
|
2462
2517
|
* (一张 emoji = 2 枚),写"字符"会把给模型的说明夸大约一倍。 */
|
|
@@ -3232,8 +3287,20 @@ function echoRaw(text, limit) {
|
|
|
3232
3287
|
return redactKeyMaterial(truncateEnd(text, limit));
|
|
3233
3288
|
}
|
|
3234
3289
|
/** 读 --output 落盘结果。OCR exit 0 却没写出文件(版本漂移/被中途 kill)时给
|
|
3235
|
-
* 可行动的错误,而不是把裸 ENOENT 栈丢给模型。
|
|
3290
|
+
* 可行动的错误,而不是把裸 ENOENT 栈丢给模型。
|
|
3291
|
+
* 超过 OUT_FILE_MAX_BYTES 的先拒掉:`readFileSync` + `JSON.parse` 是两倍内存的整份读,
|
|
3292
|
+
* 病理级的大回执只该换回一条点名文件的错误。大小探测失败(文件不在)不在这里报——
|
|
3293
|
+
* 交给下面那条 `outputFileMissing` 统一说话。 */
|
|
3236
3294
|
function readOutFile(outPath, messages) {
|
|
3295
|
+
let size = 0;
|
|
3296
|
+
try {
|
|
3297
|
+
({size} = statSync(outPath));
|
|
3298
|
+
} catch {}
|
|
3299
|
+
if (size > OUT_FILE_MAX_BYTES) throw new Error(format(messages.outputFileTooLarge, {
|
|
3300
|
+
path: outPath,
|
|
3301
|
+
size: String(size),
|
|
3302
|
+
limit: String(OUT_FILE_MAX_BYTES)
|
|
3303
|
+
}));
|
|
3237
3304
|
try {
|
|
3238
3305
|
return readFileSync(outPath, "utf8");
|
|
3239
3306
|
} catch (error) {
|
|
@@ -3244,11 +3311,35 @@ function readOutFile(outPath, messages) {
|
|
|
3244
3311
|
}
|
|
3245
3312
|
}
|
|
3246
3313
|
/**
|
|
3314
|
+
* 非零退出时把 `--output` 里已经发布的 coverage.failed 明细附到失败详情上。
|
|
3315
|
+
*
|
|
3316
|
+
* 上游的退出契约只对 run-level 失败、或「每个被选中的文件都失败」给非零,而那份 manifest
|
|
3317
|
+
* 照发(`review_cmd.go` 的 reviewResultError:“Partial results are published regardless”)
|
|
3318
|
+
* ——只回 stderr 的话模型读到「N of M selected item(s) failed」,却不知道是哪些文件、
|
|
3319
|
+
* 哪一类失败。读不出或没有失败明细时原样交回那个错误,不改变既有失败形态。
|
|
3320
|
+
*
|
|
3321
|
+
* 这里刻意不复用 `readOutFile`:那条路会给「文件缺失/过大」抛错,而失败详情里再加一层
|
|
3322
|
+
* 「读不到落盘结果」的错误只会盖住 OCR 自己的报错。
|
|
3323
|
+
*/
|
|
3324
|
+
function withCoverageDetail(error, outPath, messages) {
|
|
3325
|
+
let text;
|
|
3326
|
+
try {
|
|
3327
|
+
text = readFileSync(outPath, "utf8");
|
|
3328
|
+
} catch {
|
|
3329
|
+
return error;
|
|
3330
|
+
}
|
|
3331
|
+
const items = parseReviewOutput(text, messages).coverage?.failedItems ?? [];
|
|
3332
|
+
if (items.length === 0) return error;
|
|
3333
|
+
const note = format(messages.failureCoverage, { items: redactKeyMaterial(items.join("; ")) });
|
|
3334
|
+
return new Error(`${errorText(error)}${note}`, { cause: error });
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3247
3337
|
* OCR 原生数值参数的 tool schema 片段(忠实透传语义,桥接层不设钳制)。
|
|
3248
|
-
* unit 区分 review(每组)/ scan
|
|
3249
|
-
*
|
|
3338
|
+
* unit 区分 review(每组)/ scan(每文件)的措辞;maxToolsTemplate 也按命令分开,因为
|
|
3339
|
+
* 两侧的 --max-tools 语义本就不同:review 把 1-49 抬到 50,scan 只在大于模板默认时生效。
|
|
3340
|
+
* 语言随设置页偏好走;OCR 侧语义见 shared_flags.go 的 registerReviewFlags/registerScanFlags。
|
|
3250
3341
|
*/
|
|
3251
|
-
function tuningParams(messages, unit) {
|
|
3342
|
+
function tuningParams(messages, unit, maxToolsTemplate) {
|
|
3252
3343
|
const of = (template) => ({
|
|
3253
3344
|
type: "integer",
|
|
3254
3345
|
description: format(template, { unit })
|
|
@@ -3256,7 +3347,7 @@ function tuningParams(messages, unit) {
|
|
|
3256
3347
|
return {
|
|
3257
3348
|
concurrency: of(messages.tuningConcurrency),
|
|
3258
3349
|
timeoutMinutes: of(messages.tuningTimeout),
|
|
3259
|
-
maxTools: of(
|
|
3350
|
+
maxTools: of(maxToolsTemplate),
|
|
3260
3351
|
maxTokens: of(messages.tuningMaxTokens),
|
|
3261
3352
|
maxTokensBudget: {
|
|
3262
3353
|
type: "integer",
|
|
@@ -3485,10 +3576,17 @@ function effortOf(value, config, messages) {
|
|
|
3485
3576
|
}));
|
|
3486
3577
|
return clampEffort(value, "medium");
|
|
3487
3578
|
}
|
|
3488
|
-
/**
|
|
3489
|
-
*
|
|
3579
|
+
/**
|
|
3580
|
+
* wait 入参闸:undefined → **false(后台缺省)**;布尔透传;其余类型抛错——字符串
|
|
3581
|
+
* "false" 若被 `!== false` 当真值处理会静默从后台变前台,吞掉用户的等待语义。
|
|
3582
|
+
*
|
|
3583
|
+
* 为什么缺省改成后台:一次 review 是分钟级长任务(真跑过 8 分钟以上),前台缺省会让模型
|
|
3584
|
+
* 在一次工具调用里干等,既占住轮次又容易撞上宿主超时;后台立刻返回作业 id,模型用
|
|
3585
|
+
* `ocr_session` 轮询,等待因此发生在**模型自己的节奏里**而不是一次阻塞调用里。
|
|
3586
|
+
* 想要旧行为(一次拿到结果)显式传 `wait: true` 即可。
|
|
3587
|
+
*/
|
|
3490
3588
|
function waitOf(value, messages) {
|
|
3491
|
-
if (value === void 0 || value === null) return
|
|
3589
|
+
if (value === void 0 || value === null) return false;
|
|
3492
3590
|
if (typeof value !== "boolean") throw new TypeError(format(messages.waitMustBeBoolean, { received: describeValue(value) }));
|
|
3493
3591
|
return value;
|
|
3494
3592
|
}
|
|
@@ -3864,7 +3962,7 @@ function registerReviewTool(host, config, probe, versionSink) {
|
|
|
3864
3962
|
items: { type: "string" },
|
|
3865
3963
|
description: msgs.paramExclude
|
|
3866
3964
|
},
|
|
3867
|
-
...tuningParams(msgs, msgs.unitGroup),
|
|
3965
|
+
...tuningParams(msgs, msgs.unitGroup, msgs.tuningMaxTools),
|
|
3868
3966
|
wait: {
|
|
3869
3967
|
type: "boolean",
|
|
3870
3968
|
description: msgs.paramWait
|
|
@@ -3910,7 +4008,11 @@ function registerReviewTool(host, config, probe, versionSink) {
|
|
|
3910
4008
|
backgroundMaxMs: config.ocrBackgroundMaxMs,
|
|
3911
4009
|
env: ocrEnv(config)
|
|
3912
4010
|
});
|
|
3913
|
-
|
|
4011
|
+
try {
|
|
4012
|
+
await runForeground(host, guarded, workdir, tuningOf(config), exec.signal);
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
throw withCoverageDetail(error, outPath, runMsgs);
|
|
4015
|
+
}
|
|
3914
4016
|
const text = readOutFile(outPath, runMsgs);
|
|
3915
4017
|
const parsed = parseReviewOutput(text, runMsgs);
|
|
3916
4018
|
if (!parsed.ok) return format(runMsgs.parseFailedWithReason, {
|
|
@@ -3973,7 +4075,7 @@ function registerScanTool(host, config, probe, versionSink) {
|
|
|
3973
4075
|
type: "string",
|
|
3974
4076
|
description: msgs.paramModel
|
|
3975
4077
|
},
|
|
3976
|
-
...tuningParams(msgs, msgs.unitFile),
|
|
4078
|
+
...tuningParams(msgs, msgs.unitFile, msgs.tuningMaxToolsScan),
|
|
3977
4079
|
wait: {
|
|
3978
4080
|
type: "boolean",
|
|
3979
4081
|
description: msgs.paramWait
|
|
@@ -4005,7 +4107,11 @@ function registerScanTool(host, config, probe, versionSink) {
|
|
|
4005
4107
|
backgroundMaxMs: config.ocrBackgroundMaxMs,
|
|
4006
4108
|
env: ocrEnv(config)
|
|
4007
4109
|
});
|
|
4008
|
-
|
|
4110
|
+
try {
|
|
4111
|
+
await runForeground(host, guarded, workdir, tuningOf(config), exec.signal);
|
|
4112
|
+
} catch (error) {
|
|
4113
|
+
throw withCoverageDetail(error, outPath, runMsgs);
|
|
4114
|
+
}
|
|
4009
4115
|
const text = readOutFile(outPath, runMsgs);
|
|
4010
4116
|
const parsed = parseReviewOutput(text, runMsgs);
|
|
4011
4117
|
if (!parsed.ok) return format(runMsgs.parseFailedWithReason, {
|
package/package.json
CHANGED
package/scripts/get-cred.mjs
CHANGED
|
@@ -212,13 +212,17 @@ function unquotedValue(raw) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
*
|
|
215
|
+
* 继承来的进程环境层:去首尾空白,空白按「这一层没有值」处理;含内嵌换行的值整层作废。
|
|
216
|
+
*
|
|
217
|
+
* 单行是 OCR 的 api_key_cmd 契约(多行 stdout = 硬错误,见文件头),托管文档层已按同一条
|
|
218
|
+
* 判据丢弃——环境层不能是例外:把带内嵌换行的值原样吐出去,用户拿到的是隔了一层的
|
|
219
|
+
* 「resolve LLM endpoint」报错,而这一层本可以当没有值、继续往下回退。
|
|
216
220
|
* @param {string | undefined} raw
|
|
217
221
|
* @returns {string | undefined}
|
|
218
222
|
*/
|
|
219
223
|
function fromInheritedEnv(raw) {
|
|
220
224
|
const value = typeof raw === "string" ? raw.trim() : "";
|
|
221
|
-
return value === "" ? undefined : value;
|
|
225
|
+
return value === "" || /[\r\n]/u.test(value) ? undefined : value;
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
/**
|