@jayyuen66/dsh-ocr-review 0.1.9 → 0.1.10
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/CHANGELOG.md +29 -2
- package/LICENSE +1 -1
- package/README.md +10 -10
- package/README.zh-CN.md +9 -9
- package/THIRD_PARTY_NOTICES.md +28 -1
- package/client.js +101 -47
- package/docs/DETAIL.md +4 -4
- package/docs/DETAIL.zh.md +5 -5
- package/host.js +654 -308
- package/package.json +16 -5
- package/scripts/get-cred.mjs +124 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jayyuen66/dsh-ocr-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "dsh 工作区代码审查插件:把 open-code-review(ocr CLI)封装为 7 个模型工具 + 设置卡片(从宿主设置服务的 provider 清单选 provider/model 应用到 OCR 配置)。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-code-review",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"url": "https://github.com/JayYuen666/dsh-ocr-review/issues"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
|
-
"author":
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "JayYuen",
|
|
21
|
+
"url": "https://github.com/JayYuen666"
|
|
22
|
+
},
|
|
20
23
|
"repository": {
|
|
21
24
|
"type": "git",
|
|
22
25
|
"url": "git+https://github.com/JayYuen666/dsh-ocr-review.git"
|
|
@@ -51,15 +54,18 @@
|
|
|
51
54
|
"registry": "https://registry.npmjs.org"
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|
|
54
|
-
"@jayyuen66/dsh-plugin-shared": "0.2.
|
|
55
|
-
"js-yaml": "
|
|
57
|
+
"@jayyuen66/dsh-plugin-shared": "0.2.2",
|
|
58
|
+
"js-yaml": "5.4.3",
|
|
56
59
|
"zod": "4.6.7"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
62
|
"@deepseek-ai/cordis": "4.0.6-alpha.1",
|
|
63
|
+
"@deepseek-ai/cordis-plugin-include": "1.0.11-alpha.1",
|
|
64
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.7-alpha.1",
|
|
60
65
|
"@deepseek-ai/dsh-agent": "0.2.2-alpha.2",
|
|
61
66
|
"@deepseek-ai/dsh-api-remotes": "0.2.2-alpha.2",
|
|
62
67
|
"@deepseek-ai/dsh-atomic-write": "0.2.2-alpha.2",
|
|
68
|
+
"@deepseek-ai/dsh-bash-local": "0.2.2-alpha.2",
|
|
63
69
|
"@deepseek-ai/dsh-client-locale": "0.2.2-alpha.2",
|
|
64
70
|
"@deepseek-ai/dsh-client-store": "0.2.2-alpha.2",
|
|
65
71
|
"@deepseek-ai/dsh-client-ui-layout": "0.2.2-alpha.2",
|
|
@@ -70,8 +76,10 @@
|
|
|
70
76
|
"@deepseek-ai/dsh-credentials": "0.2.2-alpha.2",
|
|
71
77
|
"@deepseek-ai/dsh-home-paths": "0.2.2-alpha.2",
|
|
72
78
|
"@deepseek-ai/dsh-host-webserver": "0.2.2-alpha.2",
|
|
79
|
+
"@deepseek-ai/dsh-http-proxy": "0.2.2-alpha.2",
|
|
73
80
|
"@deepseek-ai/dsh-jobs": "0.2.2-alpha.2",
|
|
74
81
|
"@deepseek-ai/dsh-jobs-local": "0.2.2-alpha.2",
|
|
82
|
+
"@deepseek-ai/dsh-llm": "0.2.2-alpha.2",
|
|
75
83
|
"@deepseek-ai/dsh-output-retention": "0.2.2-alpha.2",
|
|
76
84
|
"@deepseek-ai/dsh-package-manifest": "0.2.2-alpha.2",
|
|
77
85
|
"@deepseek-ai/dsh-ptc-runtime": "0.2.2-alpha.2",
|
|
@@ -82,7 +90,9 @@
|
|
|
82
90
|
"@deepseek-ai/dsh-settings": "0.2.2-alpha.2",
|
|
83
91
|
"@deepseek-ai/dsh-shell": "0.2.2-alpha.2",
|
|
84
92
|
"@deepseek-ai/dsh-subprocess": "0.2.2-alpha.2",
|
|
93
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.2-alpha.2",
|
|
85
94
|
"@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.2",
|
|
95
|
+
"@deepseek-ai/dsh-timeout": "0.2.2-alpha.2",
|
|
86
96
|
"@deepseek-ai/dsh-tools": "0.2.2-alpha.2",
|
|
87
97
|
"@deepseek-ai/dsh-typert-protocol": "0.2.2-alpha.2",
|
|
88
98
|
"@deepseek-ai/dsh-user-approval": "0.2.2-alpha.2",
|
|
@@ -94,6 +104,7 @@
|
|
|
94
104
|
"eslint-plugin-regexp": "3.3.2",
|
|
95
105
|
"eslint-plugin-sonarjs": "4.2.2",
|
|
96
106
|
"eslint-plugin-zod": "5.2.2",
|
|
107
|
+
"fast-check": "4.10.2",
|
|
97
108
|
"oxfmt": "0.75.0",
|
|
98
109
|
"oxlint": "1.90.0",
|
|
99
110
|
"oxlint-tsgolint": "7.0.2005",
|
|
@@ -108,7 +119,7 @@
|
|
|
108
119
|
"@deepseek-ai/dsh-credentials": "^0.2.1-rc.3",
|
|
109
120
|
"@deepseek-ai/dsh-home-paths": "^0.2.1-rc.3",
|
|
110
121
|
"@deepseek-ai/dsh-typert-protocol": "^0.2.1-rc.3",
|
|
111
|
-
"@deepseek-ai/schemastery": "3.18.6-alpha.1"
|
|
122
|
+
"@deepseek-ai/schemastery": "^3.18.6-alpha.1"
|
|
112
123
|
},
|
|
113
124
|
"icon": "icon.svg",
|
|
114
125
|
"engines": {
|
package/scripts/get-cred.mjs
CHANGED
|
@@ -93,7 +93,8 @@ function isRecord(value) {
|
|
|
93
93
|
*/
|
|
94
94
|
function dataDirFrom(fromArg) {
|
|
95
95
|
const arg = typeof fromArg === "string" ? fromArg.trim() : "";
|
|
96
|
-
const
|
|
96
|
+
const { DSH_HOME: rawHome } = process.env;
|
|
97
|
+
const fromEnv = typeof rawHome === "string" ? rawHome.trim() : "";
|
|
97
98
|
let chosen = path.join(homedir(), ".dsh");
|
|
98
99
|
if (fromEnv !== "") {
|
|
99
100
|
chosen = fromEnv;
|
|
@@ -106,68 +107,65 @@ function dataDirFrom(fromArg) {
|
|
|
106
107
|
|
|
107
108
|
/** 取值过程中遇到的坏消息(文件损坏/权限/引号写坏);收集但不提前退出——某一层的
|
|
108
109
|
* 小毛病不能把后面的合法层堵掉。出口只有两处:最终取不到时随失败原因一并报出,
|
|
109
|
-
*
|
|
110
|
-
* 全程脱敏:明文值不进这条列表,YAML 报错走 yamlErrorDetail。
|
|
110
|
+
* 或取到了值时以警告形式报出(用户必须知道查过的层里有坏消息)。
|
|
111
|
+
* 全程脱敏:明文值不进这条列表,YAML 报错走 yamlErrorDetail。
|
|
112
|
+
* @type {string[]} */
|
|
111
113
|
const problems = [];
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 缺字段时的空映射:解构取值同时满足 tsc 的 `noPropertyAccessFromIndexSignature`
|
|
117
|
+
* 与 lint 的点号规则,不必二选一。
|
|
118
|
+
* @type {Record<string, unknown>}
|
|
119
|
+
*/
|
|
120
|
+
const EMPTY_RECORD = {};
|
|
121
|
+
|
|
113
122
|
/**
|
|
114
123
|
* ENOENT 判定(文件不存在是正常态,别把它当损坏报出去)。
|
|
115
124
|
* @param {unknown} error
|
|
116
125
|
* @returns {boolean}
|
|
117
126
|
*/
|
|
118
127
|
function isNotFound(error) {
|
|
119
|
-
|
|
128
|
+
if (!isRecord(error)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const { code } = error;
|
|
132
|
+
return code === "ENOENT";
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
/**
|
|
123
136
|
* YAML 解析错误的**脱敏**描述:只留 reason + 行列定位,不带出错位置前后若干行的原文。
|
|
124
137
|
*
|
|
125
|
-
* 为什么必须自己拼:js-yaml 的 YAMLException
|
|
126
|
-
*
|
|
127
|
-
* `toString()`
|
|
138
|
+
* 为什么必须自己拼:js-yaml 的 YAMLException 会把 mark.snippet(出错点邻域的**原文**)
|
|
139
|
+
* 附在 message 上——构造函数里的 `this.message = formatError(this, false)` 与
|
|
140
|
+
* `toString()` 的非紧凑分支都是这样。而 .credentials.yaml 的 refs.<REF> 里放的就是
|
|
128
141
|
* 明文 key,于是一处缩进/引号笔误就会把邻近行的真实 key 写进 stderr(OCR 会记录
|
|
129
142
|
* api_key_cmd 的 stderr,也可能进宿主日志),直接违反「明文只在取用时出现一次」。
|
|
130
|
-
*
|
|
131
|
-
*
|
|
143
|
+
* 因此这里既不读 message 也不调 toString:只取 YAMLException 的 reason 与 mark
|
|
144
|
+
* 两个数据字段。非 YAMLException 的 Error 没有这两个字段,退回 name——同样不碰
|
|
145
|
+
* message。
|
|
132
146
|
*
|
|
133
147
|
* @param {unknown} error
|
|
134
148
|
* @returns {string}
|
|
135
149
|
*/
|
|
136
150
|
function yamlErrorDetail(error) {
|
|
137
|
-
if (error instanceof Error) {
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
// "Name: message",同样不含出错点邻域的原文。
|
|
141
|
-
const compact = error.toString(true);
|
|
142
|
-
if (typeof compact === "string" && compact !== "") {
|
|
143
|
-
return compact;
|
|
144
|
-
}
|
|
145
|
-
// 拿不到紧凑形态时**不能**退回 error.message:js-yaml 构造器里赋的就是
|
|
146
|
-
// this.message = formatError(this, false),那份 message 正是带 snippet 的
|
|
147
|
-
// 那一条。改用 reason + 行列定位自行拼装,两者都不含原文。
|
|
148
|
-
const reason = typeof error.reason === "string" ? error.reason : "";
|
|
149
|
-
const { mark } = error;
|
|
150
|
-
const line = isRecord(mark) ? mark.line : undefined;
|
|
151
|
-
const column = isRecord(mark) ? mark.column : undefined;
|
|
152
|
-
const located =
|
|
153
|
-
typeof line === "number" && typeof column === "number"
|
|
154
|
-
? ` (line:${line + 1}, column:${column + 1})`
|
|
155
|
-
: "";
|
|
156
|
-
return `${reason}${located}`;
|
|
151
|
+
if (!(error instanceof Error)) {
|
|
152
|
+
// 不是 Error 实例:String(error) 只会得到 "[object Object]" 一类无信息文本。
|
|
153
|
+
return "";
|
|
157
154
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
const { reason: rawReason, mark } = isRecord(error) ? error : EMPTY_RECORD;
|
|
156
|
+
const reason = typeof rawReason === "string" && rawReason !== "" ? rawReason : error.name;
|
|
157
|
+
const { line, column } = isRecord(mark) ? mark : EMPTY_RECORD;
|
|
158
|
+
const located =
|
|
159
|
+
typeof line === "number" && typeof column === "number" ? ` (${line + 1}:${column + 1})` : "";
|
|
160
|
+
return `${reason}${located}`;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
*
|
|
165
|
-
* 解析失败走 `yamlErrorDetail`,绝不把邻域原文带进 stderr。
|
|
164
|
+
* 读取并解析托管文档;读不到或解析失败都留一条 problem,交回 undefined。
|
|
166
165
|
* @param {string} credPath
|
|
167
|
-
* @
|
|
168
|
-
* @returns {string | undefined}
|
|
166
|
+
* @returns {unknown}
|
|
169
167
|
*/
|
|
170
|
-
function
|
|
168
|
+
function parseCredentialsDocument(credPath) {
|
|
171
169
|
let text;
|
|
172
170
|
try {
|
|
173
171
|
text = readFileSync(credPath, "utf8");
|
|
@@ -176,18 +174,76 @@ function fromCredentialsDoc(credPath, refName) {
|
|
|
176
174
|
if (!isNotFound(error)) {
|
|
177
175
|
problems.push(`读取凭证文件失败:${credPath} ${String(error)}`);
|
|
178
176
|
}
|
|
177
|
+
text = undefined;
|
|
179
178
|
}
|
|
180
|
-
/** @type {unknown} */
|
|
181
179
|
let doc;
|
|
182
180
|
if (text !== undefined) {
|
|
183
181
|
try {
|
|
184
182
|
doc = load(text);
|
|
185
183
|
} catch (error) {
|
|
186
184
|
problems.push(`凭证文件不是合法 YAML:${credPath} ${yamlErrorDetail(error)}`);
|
|
185
|
+
doc = undefined;
|
|
187
186
|
}
|
|
188
187
|
}
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
return doc;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 取文档里的 refs 映射;顶层不是映射、或 refs 存在却不是映射时记 problem。
|
|
193
|
+
* 文件能解析但形状不对时,静默回退到环境层会让用户以为托管文档仍生效,实际拿的
|
|
194
|
+
* 是另一份旧 key;这里记一条不含任何值的 problem,成功回退时一并转述。
|
|
195
|
+
* refs 键缺席不算坏形状:没有要取的 ref 时它本来就是可选的。
|
|
196
|
+
* @param {unknown} doc
|
|
197
|
+
* @param {string} credPath
|
|
198
|
+
* @returns {Record<string, unknown> | undefined}
|
|
199
|
+
*/
|
|
200
|
+
function refsMapOf(doc, credPath) {
|
|
201
|
+
const { refs } = isRecord(doc) ? doc : EMPTY_RECORD;
|
|
202
|
+
if (doc !== undefined && (!isRecord(doc) || (refs !== undefined && !isRecord(refs)))) {
|
|
203
|
+
problems.push(`凭证文件顶层不是 { refs: { … } } 映射:${credPath}`);
|
|
204
|
+
}
|
|
205
|
+
return isRecord(refs) ? refs : undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* OCR 的 api_key_cmd 契约只认**单行** stdout(空/多行都是硬错误)。托管文档里
|
|
210
|
+
* 引号内的空白与块标量(| / >)分别产出空白值与多行值:交给 OCR 只会换来隔了
|
|
211
|
+
* 一层的「resolve LLM endpoint」报错,而这一层本可以当没有值、继续往下回退。
|
|
212
|
+
* @param {unknown} value
|
|
213
|
+
* @param {string} refName
|
|
214
|
+
* @param {string} credPath
|
|
215
|
+
* @returns {string | undefined}
|
|
216
|
+
*/
|
|
217
|
+
function singleLineKeyOf(value, refName, credPath) {
|
|
218
|
+
let key;
|
|
219
|
+
if (typeof value === "string") {
|
|
220
|
+
const trimmed = value.trim();
|
|
221
|
+
if (trimmed === "") {
|
|
222
|
+
problems.push(`凭证文件 refs.${refName} 为空白值,按未配置处理:${credPath}`);
|
|
223
|
+
} else if (/[\r\n]/u.test(trimmed)) {
|
|
224
|
+
problems.push(`凭证文件 refs.${refName} 为多行值,按未配置处理:${credPath}`);
|
|
225
|
+
} else {
|
|
226
|
+
key = trimmed;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return key;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 托管 YAML 文档里的 refs.<REF>(形状不合一律回 undefined,不抛)。
|
|
234
|
+
* 解析失败走 `yamlErrorDetail`,绝不把邻域原文带进 stderr。
|
|
235
|
+
* @param {string} credPath
|
|
236
|
+
* @param {string} refName
|
|
237
|
+
* @returns {string | undefined}
|
|
238
|
+
*/
|
|
239
|
+
function fromCredentialsDoc(credPath, refName) {
|
|
240
|
+
const refs = refsMapOf(parseCredentialsDocument(credPath), credPath);
|
|
241
|
+
let value;
|
|
242
|
+
if (refs !== undefined) {
|
|
243
|
+
const { [refName]: selected } = refs;
|
|
244
|
+
value = selected;
|
|
245
|
+
}
|
|
246
|
+
let key;
|
|
191
247
|
if (value !== undefined && typeof value !== "string") {
|
|
192
248
|
// 只报形状,绝不把值本身写进 problems(明文不落日志)。refs.<REF> 被写成
|
|
193
249
|
// 数字/布尔/映射时若静默当「没有值」,用户改了托管文档却拿到下一层的旧值,
|
|
@@ -195,13 +251,10 @@ function fromCredentialsDoc(credPath, refName) {
|
|
|
195
251
|
problems.push(
|
|
196
252
|
`凭证文件 refs.${refName} 不是字符串(是 ${Array.isArray(value) ? "数组" : typeof value}):${credPath}`,
|
|
197
253
|
);
|
|
198
|
-
|
|
254
|
+
} else {
|
|
255
|
+
key = singleLineKeyOf(value, refName, credPath);
|
|
199
256
|
}
|
|
200
|
-
|
|
201
|
-
// 引号内的空白与块标量(| / >)分别产出空白值与多行值:交给 OCR 只会换来隔了
|
|
202
|
-
// 一层的「resolve LLM endpoint」报错,而这一层本可以当没有值、继续往下回退。
|
|
203
|
-
const key = typeof value === "string" ? value.trim() : "";
|
|
204
|
-
return key !== "" && !/[\r\n]/u.test(key) ? key : undefined;
|
|
257
|
+
return key;
|
|
205
258
|
}
|
|
206
259
|
|
|
207
260
|
/**
|
|
@@ -218,7 +271,11 @@ function fromInheritedEnv(raw) {
|
|
|
218
271
|
return value === "" || /[\r\n]/u.test(value) ? undefined : value;
|
|
219
272
|
}
|
|
220
273
|
|
|
221
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* 未加引号的取值:trim,含「 #」时从那里剥离行内注释;剥完为空白按没有值。
|
|
276
|
+
* @param {string} trimmed
|
|
277
|
+
* @returns {{ value: string | undefined, problem: string | undefined }}
|
|
278
|
+
*/
|
|
222
279
|
function unquotedValueOf(trimmed) {
|
|
223
280
|
const commentAt = trimmed.indexOf(" #");
|
|
224
281
|
const bare = commentAt === -1 ? trimmed : trimmed.slice(0, commentAt).trim();
|
|
@@ -323,7 +380,8 @@ function fromUserDotEnv(envPath, refName) {
|
|
|
323
380
|
if (match === null) {
|
|
324
381
|
continue;
|
|
325
382
|
}
|
|
326
|
-
const
|
|
383
|
+
const { value: rawValue } = match.groups ?? EMPTY_RECORD;
|
|
384
|
+
const parsed = parseDotEnvValue(typeof rawValue === "string" ? rawValue : "");
|
|
327
385
|
if (parsed.problem === undefined) {
|
|
328
386
|
hit = parsed.value;
|
|
329
387
|
} else {
|
|
@@ -349,26 +407,33 @@ function main() {
|
|
|
349
407
|
fail(`[get-cred] dsh 数据目录必须是绝对路径,收到 ${JSON.stringify(home)}\n`);
|
|
350
408
|
return;
|
|
351
409
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
410
|
+
const credPath = path.join(home, CREDENTIALS_FILENAME);
|
|
411
|
+
const envPath = path.join(home, ".env");
|
|
412
|
+
let found = fromCredentialsDoc(credPath, refName);
|
|
413
|
+
let source = credPath;
|
|
414
|
+
if (found === undefined) {
|
|
415
|
+
found = fromInheritedEnv(process.env[refName]);
|
|
416
|
+
source = "进程环境";
|
|
417
|
+
}
|
|
418
|
+
if (found === undefined) {
|
|
419
|
+
found = fromUserDotEnv(envPath, refName);
|
|
420
|
+
source = envPath;
|
|
421
|
+
}
|
|
356
422
|
|
|
357
423
|
if (found === undefined) {
|
|
358
424
|
// 硬失败:OCR 契约里非零退出/空输出都不回退,所以原因必须写全(含查过的目录与
|
|
359
425
|
// 各层的坏消息),否则用户只能看到「llm 请求 401」这种隔了一层的症状。
|
|
360
426
|
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
|
-
);
|
|
427
|
+
fail(`[get-cred] 取不到 ${refName}:查过 ${credPath}、进程环境与 ${envPath}${notes}\n`);
|
|
364
428
|
return;
|
|
365
429
|
}
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
430
|
+
// 成功回退也要说话:查过的层里有坏消息时,用户必须知道生效的 key 得自哪一层。
|
|
431
|
+
// problems 在收集时已按脱敏口径拼装(YAML 报错走 yamlErrorDetail,绝不带邻域
|
|
432
|
+
// 原文),这里原样转述。只报实际来源,不推断「更低的一层」——坏消息可能来自
|
|
433
|
+
// .env 自己的坏行,而值也正是同一层给的。
|
|
369
434
|
if (problems.length > 0) {
|
|
370
435
|
process.stderr.write(
|
|
371
|
-
`[get-cred]
|
|
436
|
+
`[get-cred] 注意:查凭据时遇到坏消息(${problems.join(";")}),${refName} 实际取自 ${source}\n`,
|
|
372
437
|
);
|
|
373
438
|
}
|
|
374
439
|
process.stdout.write(`${found}\n`);
|