@jayyuen66/dsh-ocr-review 0.1.7 → 0.1.8

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/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@jayyuen66/dsh-ocr-review",
3
- "version": "0.1.7",
4
- "description": "dsh 工作区代码审查插件:把 open-code-review(ocr CLI)封装为 5 个模型工具 + 设置卡片(从宿主设置服务的 provider 清单选 provider/model 应用到 OCR 配置)。",
3
+ "version": "0.1.8",
4
+ "description": "dsh 工作区代码审查插件:把 open-code-review(ocr CLI)封装为 7 个模型工具 + 设置卡片(从宿主设置服务的 provider 清单选 provider/model 应用到 OCR 配置)。",
5
5
  "keywords": [
6
- "AI code review",
7
- "DeepSeek Harness",
6
+ "ai-code-review",
8
7
  "code-review",
9
8
  "deepseek-harness",
10
9
  "dsh",
@@ -28,6 +27,7 @@
28
27
  "client.js",
29
28
  "cordis.patch.yml",
30
29
  "scripts",
30
+ "CHANGELOG.md",
31
31
  "THIRD_PARTY_NOTICES.md"
32
32
  ],
33
33
  "type": "module",
@@ -47,64 +47,64 @@
47
47
  "registry": "https://registry.npmjs.org"
48
48
  },
49
49
  "dependencies": {
50
- "@jayyuen66/dsh-plugin-shared": "^0.1.6",
50
+ "@jayyuen66/dsh-plugin-shared": "0.1.9",
51
51
  "js-yaml": "^5.4.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@deepseek-ai/cordis": "4.0.5",
55
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
56
- "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.2",
57
- "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.2",
58
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
59
- "@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
60
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.2",
61
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.2",
62
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
63
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.2",
64
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
65
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
66
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
67
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
68
- "@deepseek-ai/dsh-jobs": "0.2.1-rc.2",
69
- "@deepseek-ai/dsh-jobs-local": "0.2.1-rc.2",
70
- "@deepseek-ai/dsh-output-retention": "0.2.1-rc.2",
71
- "@deepseek-ai/dsh-package-manifest": "0.2.1-rc.2",
72
- "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.2",
73
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
74
- "@deepseek-ai/dsh-scope": "0.2.1-rc.2",
75
- "@deepseek-ai/dsh-session": "0.2.1-rc.2",
76
- "@deepseek-ai/dsh-session-projection": "0.2.1-rc.2",
77
- "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
78
- "@deepseek-ai/dsh-shell": "0.2.1-rc.2",
79
- "@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
80
- "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.2",
81
- "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
82
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
83
- "@deepseek-ai/dsh-user-approval": "0.2.1-rc.2",
55
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.3",
56
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.3",
57
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.3",
58
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.3",
59
+ "@deepseek-ai/dsh-client-store": "0.2.1-rc.3",
60
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.3",
61
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.3",
62
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.3",
63
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.3",
64
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
65
+ "@deepseek-ai/dsh-credentials": "0.2.1-rc.3",
66
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.3",
67
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.3",
68
+ "@deepseek-ai/dsh-jobs": "0.2.1-rc.3",
69
+ "@deepseek-ai/dsh-jobs-local": "0.2.1-rc.3",
70
+ "@deepseek-ai/dsh-output-retention": "0.2.1-rc.3",
71
+ "@deepseek-ai/dsh-package-manifest": "0.2.1-rc.3",
72
+ "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.3",
73
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.3",
74
+ "@deepseek-ai/dsh-scope": "0.2.1-rc.3",
75
+ "@deepseek-ai/dsh-session": "0.2.1-rc.3",
76
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
77
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.3",
78
+ "@deepseek-ai/dsh-shell": "0.2.1-rc.3",
79
+ "@deepseek-ai/dsh-subprocess": "0.2.1-rc.3",
80
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.3",
81
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.3",
82
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.3",
83
+ "@deepseek-ai/dsh-user-approval": "0.2.1-rc.3",
84
84
  "@deepseek-ai/schemastery": "3.18.5",
85
85
  "@types/node": "26.6.5",
86
86
  "@types/react": "19.3.0",
87
87
  "@vitest/coverage-v8": "5.0.5",
88
88
  "eslint-plugin-sonarjs": "4.2.2",
89
- "oxfmt": "0.73.0",
90
- "oxlint": "1.88.0",
91
- "oxlint-tsgolint": "7.0.2004",
89
+ "oxfmt": "0.74.0",
90
+ "oxlint": "1.89.0",
91
+ "oxlint-tsgolint": "7.0.2005",
92
92
  "react": "^19.3.0",
93
- "rolldown": "1.2.14",
93
+ "rolldown": "1.2.15",
94
94
  "typescript": "7.0.2",
95
- "vite": "8.3.4",
95
+ "vite": "8.3.5",
96
96
  "vitest": "5.0.5"
97
97
  },
98
98
  "peerDependencies": {
99
- "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.2",
100
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
101
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
102
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
99
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.3",
100
+ "@deepseek-ai/dsh-credentials": "0.2.1-rc.3",
101
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.3",
102
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.3",
103
103
  "@deepseek-ai/schemastery": "3.18.5"
104
104
  },
105
105
  "icon": "icon.svg",
106
106
  "engines": {
107
- "dsh": "^0.2.1-rc.2",
107
+ "dsh": "^0.2.1-rc.3",
108
108
  "node": ">=22.12.0"
109
109
  },
110
110
  "dsh": {
@@ -117,11 +117,11 @@
117
117
  "immediately": true
118
118
  }
119
119
  },
120
- "ocrCliVersion": "1.13.0",
120
+ "ocrCliVersion": "1.13.3",
121
121
  "scripts": {
122
122
  "build:client": "node build-client.mjs",
123
123
  "build": "node build-host.mjs",
124
- "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
124
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p src/tsconfig.json && tsc --noEmit -p test/client/tsconfig.json && tsc --noEmit -p tsconfig.build-scripts.json",
125
125
  "lint": "oxlint .",
126
126
  "test": "vitest run --coverage",
127
127
  "test:watch": "vitest",
@@ -62,6 +62,19 @@ const REF_PATTERN = /^[A-Z0-9_]+$/u;
62
62
  /** 托管凭据文档的固定文件名(credentials-local 的 CREDENTIALS_FILENAME)。 */
63
63
  const CREDENTIALS_FILENAME = ".credentials.yaml";
64
64
 
65
+ /**
66
+ * 失败出口:写 stderr 并置退出码,**不**硬杀进程。
67
+ *
68
+ * process.stderr.write 对管道是异步的,紧随其后的 process.exit(1) 会在缓冲未清空时把
69
+ * 诊断截成半句;置 exitCode 让事件循环自然排空后以非零收场,报错全文有保证。
70
+ * @param {string} message
71
+ * @returns {void}
72
+ */
73
+ function fail(message) {
74
+ process.stderr.write(message);
75
+ process.exitCode = 1;
76
+ }
77
+
65
78
  /**
66
79
  * 对象守卫(shared/lib/tool-events 同款防御):typeof 收窄为 Record 而不经断言。
67
80
  * @param {unknown} value
@@ -73,10 +86,8 @@ function isRecord(value) {
73
86
 
74
87
  /**
75
88
  * 数据目录:显式参数 > $DSH_HOME > homedir()/.dsh(空/全空白按未设置处理)。
76
- *
77
- * 相对路径在这里是硬错误而不是照用:本脚本的 cwd 是 OCR 当时所在的那个仓库,
78
- * 相对路径会被解析成 `<被审查仓库>/.dsh/…`——正是文件头声明要避免的「拿被审查
79
- * 仓库里的文件当用户凭据」。猜不出用户指的是哪个目录,就不猜。
89
+ * 只做**选择**;绝对性校验在调用点(main)做——那是一条要报错退出的硬门,与本函数
90
+ * 「挑出候选目录」的职责分开。
80
91
  * @param {string | undefined} fromArg
81
92
  * @returns {string}
82
93
  */
@@ -90,15 +101,13 @@ function dataDirFrom(fromArg) {
90
101
  if (arg !== "") {
91
102
  chosen = arg;
92
103
  }
93
- if (!path.isAbsolute(chosen)) {
94
- process.stderr.write(`[get-cred] dsh 数据目录必须是绝对路径,收到 ${JSON.stringify(chosen)}\n`);
95
- process.exit(1);
96
- }
97
104
  return chosen;
98
105
  }
99
106
 
100
- /** 取值过程中遇到的坏消息(文件损坏/权限);只在最终失败时一并报出,
101
- * 免得某一层的小毛病提前 exit 1 把后面的合法层也堵掉。 */
107
+ /** 取值过程中遇到的坏消息(文件损坏/权限/引号写坏);收集但不提前退出——某一层的
108
+ * 小毛病不能把后面的合法层堵掉。出口只有两处:最终取不到时随失败原因一并报出,
109
+ * 或取到了更低一层时以警告形式报出(用户必须知道生效的 key 不是最高优先级给的)。
110
+ * 全程脱敏:明文值不进这条列表,YAML 报错走 yamlErrorDetail。 */
102
111
  const problems = [];
103
112
 
104
113
  /**
@@ -195,22 +204,6 @@ function fromCredentialsDoc(credPath, refName) {
195
204
  return key !== "" && !/[\r\n]/u.test(key) ? key : undefined;
196
205
  }
197
206
 
198
- /**
199
- * `.env` 的单行取值:去首尾空白,长度 > 1 且以引号开头时剥掉包裹引号。
200
- * @param {string} raw
201
- * @returns {string}
202
- */
203
- function unquotedValue(raw) {
204
- const trimmed = raw.trim();
205
- const [first] = trimmed;
206
- // 只有首尾**配对**的引号才剥。单边引号是用户写坏了,硬砍首尾等于把凭据改短
207
- // 一位,拿一个错 key 去打上游(401),比报「未配置」难排查得多。
208
- if (trimmed.length > 1 && (first === '"' || first === "'") && trimmed.endsWith(first)) {
209
- return trimmed.slice(1, -1);
210
- }
211
- return trimmed;
212
- }
213
-
214
207
  /**
215
208
  * 继承来的进程环境层:去首尾空白,空白按「这一层没有值」处理;含内嵌换行的值整层作废。
216
209
  *
@@ -225,8 +218,87 @@ function fromInheritedEnv(raw) {
225
218
  return value === "" || /[\r\n]/u.test(value) ? undefined : value;
226
219
  }
227
220
 
221
+ /** 未加引号的取值:trim,含「 #」时从那里剥离行内注释;剥完为空白按没有值。 */
222
+ function unquotedValueOf(trimmed) {
223
+ const commentAt = trimmed.indexOf(" #");
224
+ const bare = commentAt === -1 ? trimmed : trimmed.slice(0, commentAt).trim();
225
+ return { value: bare === "" ? undefined : bare, problem: undefined };
226
+ }
227
+
228
+ /**
229
+ * 扫描带引号的值:交出引号内文本、是否闭合、闭合后的游标。双引号内 `\"` 与 `\\`
230
+ * 是转义(交出的文本已还原),单引号内全部字面——与 dotenv 的两套引号语义一致。
231
+ * @param {string} trimmed
232
+ * @param {string} quote
233
+ * @returns {{ value: string, closed: boolean, end: number }}
234
+ */
235
+ function scanQuoted(trimmed, quote) {
236
+ let value = "";
237
+ let index = 1;
238
+ while (index < trimmed.length) {
239
+ const ch = trimmed[index];
240
+ const escaped =
241
+ quote === '"' && ch === "\\" && (trimmed[index + 1] === '"' || trimmed[index + 1] === "\\");
242
+ if (escaped) {
243
+ value += trimmed[index + 1];
244
+ index += 2;
245
+ } else if (ch === quote) {
246
+ return { value, closed: true, end: index + 1 };
247
+ } else {
248
+ value += ch;
249
+ index += 1;
250
+ }
251
+ }
252
+ return { value, closed: false, end: index };
253
+ }
254
+
255
+ /**
256
+ * 带引号形态的取值:成对才剥;闭引号之后只允许空白或行内注释。单边引号、闭引号后跟
257
+ * 多余字符都记 problem——硬砍首尾等于把凭据改短一位,拿一个错 key 去打上游(401),
258
+ * 比报「未配置」难排查得多。剥引号后再 trim 并判空:`KEY=""` / `KEY=" "` 与另两层
259
+ * (托管文档、进程环境)的口径一致——纯空白不是可用凭据,按「该行没有值」处理。
260
+ * @param {string} trimmed
261
+ * @param {string} quote
262
+ * @returns {{ value: string | undefined, problem: string | undefined }}
263
+ */
264
+ function quotedValueOf(trimmed, quote) {
265
+ const scanned = scanQuoted(trimmed, quote);
266
+ if (!scanned.closed) {
267
+ return { value: undefined, problem: "引号未配对" };
268
+ }
269
+ const rest = trimmed.slice(scanned.end).trim();
270
+ if (rest !== "" && !rest.startsWith("#")) {
271
+ return { value: undefined, problem: "闭引号之后还有多余字符" };
272
+ }
273
+ const unquoted = scanned.value.trim();
274
+ return { value: unquoted === "" ? undefined : unquoted, problem: undefined };
275
+ }
276
+
277
+ /**
278
+ * .env 一行里「=」右侧的取值:按首字符分派到未加引号 / 带引号两条解析路径(判据各自
279
+ * 见下)。只实现与官方 dotenv 相交且本脚本用得到的语义;拿不准的形态 fail-closed
280
+ * (按该行未配置处理并记 problem),绝不猜一个值出去。
281
+ * @param {string} raw
282
+ * @returns {{ value: string | undefined, problem: string | undefined }}
283
+ */
284
+ function parseDotEnvValue(raw) {
285
+ const trimmed = raw.trim();
286
+ if (trimmed === "") {
287
+ return { value: undefined, problem: undefined };
288
+ }
289
+ const quote = trimmed.charAt(0);
290
+ if (quote === '"' || quote === "'") {
291
+ return quotedValueOf(trimmed, quote);
292
+ }
293
+ return unquotedValueOf(trimmed);
294
+ }
295
+
228
296
  /**
229
- * $DSH_HOME/.env 的 KEY=VALUE(去掉可选引号;文件不存在按「该层没有值」处理)。
297
+ * $DSH_HOME/.env 的 KEY=VALUE(文件不存在按「该层没有值」处理)。
298
+ *
299
+ * 同名 key 取**最后一行**的值(dotenv 语义:后者覆盖前者);空值行同样参与覆盖——
300
+ * 最后一行写 `KEY=` 就是把这层 unset。解析出 problem 的行不参与覆盖(该行整条作废,
301
+ * 不能拿一个坏行抹掉前面好好的值),但会记进 problems。
230
302
  * @param {string} envPath
231
303
  * @param {string} refName
232
304
  * @returns {string | undefined}
@@ -241,41 +313,65 @@ function fromUserDotEnv(envPath, refName) {
241
313
  }
242
314
  }
243
315
  const pattern = new RegExp(`^\\s*(?:export\\s+)?${refName}\\s*=\\s*(?<value>.*)$`, "u");
244
- let hit = "";
316
+ let hit;
245
317
  // 按 /\r?\n/ 拆而不是 "\n":上面那条正则的 `.*` 不匹配 \r、`$` 又要求串尾,
246
318
  // 而按 "\n" 拆出来的 CRLF 行每行都带尾随 \r,于是每一行都匹配不上——整个 .env
247
319
  // 层对所有 key 静默失配,用户只看到隔了一层的「取不到凭据」。
248
- for (const line of (text ?? "").split(/\r?\n/u)) {
249
- const value = unquotedValue(pattern.exec(line)?.groups?.value ?? "");
250
- if (value.length > 0) {
251
- hit = value;
252
- break;
320
+ const lines = (text ?? "").split(/\r?\n/u);
321
+ for (const [offset, line] of lines.entries()) {
322
+ const match = pattern.exec(line);
323
+ if (match === null) {
324
+ continue;
325
+ }
326
+ const parsed = parseDotEnvValue(match.groups?.value ?? "");
327
+ if (parsed.problem === undefined) {
328
+ hit = parsed.value;
329
+ } else {
330
+ problems.push(
331
+ `${envPath} 第 ${offset + 1} 行(${refName})${parsed.problem},该行按未配置处理`,
332
+ );
253
333
  }
254
334
  }
255
- return hit === "" ? undefined : hit;
335
+ return hit;
256
336
  }
257
337
 
258
- const refName = process.argv[2] ?? "";
259
- if (!REF_PATTERN.test(refName)) {
260
- process.stderr.write(`[get-cred] 非法 env key 名:${JSON.stringify(refName)}\n`);
261
- process.exit(1);
262
- }
263
-
264
- const home = dataDirFrom(process.argv[3]);
265
- const found =
266
- fromCredentialsDoc(path.join(home, CREDENTIALS_FILENAME), refName) ??
267
- fromInheritedEnv(process.env[refName]) ??
268
- fromUserDotEnv(path.join(home, ".env"), refName);
338
+ function main() {
339
+ const refName = process.argv[2] ?? "";
340
+ if (!REF_PATTERN.test(refName)) {
341
+ fail(`[get-cred] 非法 env key 名:${JSON.stringify(refName)}\n`);
342
+ return;
343
+ }
344
+ const home = dataDirFrom(process.argv[3]);
345
+ // 相对路径在这里是硬错误而不是照用:本脚本的 cwd 是 OCR 当时所在的那个仓库,
346
+ // 相对路径会被解析成 `<被审查仓库>/.dsh/…`——正是文件头声明要避免的「拿被审查
347
+ // 仓库里的文件当用户凭据」。猜不出用户指的是哪个目录,就不猜。
348
+ if (!path.isAbsolute(home)) {
349
+ fail(`[get-cred] dsh 数据目录必须是绝对路径,收到 ${JSON.stringify(home)}\n`);
350
+ return;
351
+ }
352
+ const found =
353
+ fromCredentialsDoc(path.join(home, CREDENTIALS_FILENAME), refName) ??
354
+ fromInheritedEnv(process.env[refName]) ??
355
+ fromUserDotEnv(path.join(home, ".env"), refName);
269
356
 
270
- if (found === undefined) {
271
- // 硬失败:OCR 契约里非零退出/空输出都不回退,所以原因必须写全(含查过的目录与
272
- // 各层的坏消息),否则用户只能看到「llm 请求 401」这种隔了一层的症状。
273
- const notes = problems.length === 0 ? "" : `;${problems.join(";")}`;
274
- process.stderr.write(
275
- `[get-cred] 取不到 ${refName}:查过 ${path.join(home, CREDENTIALS_FILENAME)}、进程环境与 ${path.join(home, ".env")}${notes}\n`,
276
- );
277
- process.exit(1);
278
- } else {
279
- // 成功路径与失败路径同为「一条语句」形态:exit(1) 之后的分支互斥,不落到 stdout。
357
+ if (found === undefined) {
358
+ // 硬失败:OCR 契约里非零退出/空输出都不回退,所以原因必须写全(含查过的目录与
359
+ // 各层的坏消息),否则用户只能看到「llm 请求 401」这种隔了一层的症状。
360
+ const notes = problems.length === 0 ? "" : `;${problems.join(";")}`;
361
+ fail(
362
+ `[get-cred] 取不到 ${refName}:查过 ${path.join(home, CREDENTIALS_FILENAME)}、进程环境与 ${path.join(home, ".env")}${notes}\n`,
363
+ );
364
+ return;
365
+ }
366
+ // 成功回退也要说话:托管文件损坏/无权限/ref 形状错被回退层盖过去时,用户必须知道
367
+ // 生效的 key 不是最高优先级那层给的。problems 在收集时已按脱敏口径拼装(YAML 报错走
368
+ // yamlErrorDetail,绝不带邻域原文),这里原样转述即可。
369
+ if (problems.length > 0) {
370
+ process.stderr.write(
371
+ `[get-cred] 注意:高优先级凭据层有坏消息(${problems.join(";")}),${refName} 实际取自更低的一层\n`,
372
+ );
373
+ }
280
374
  process.stdout.write(`${found}\n`);
281
375
  }
376
+
377
+ main();