@jayyuen66/dsh-ocr-review 0.1.9 → 0.1.11

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@jayyuen66/dsh-ocr-review",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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": "JayYuen",
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,42 +54,49 @@
51
54
  "registry": "https://registry.npmjs.org"
52
55
  },
53
56
  "dependencies": {
54
- "@jayyuen66/dsh-plugin-shared": "0.2.1",
55
- "js-yaml": "^5.4.3",
57
+ "@jayyuen66/dsh-plugin-shared": "0.2.3",
58
+ "js-yaml": "5.4.3",
56
59
  "zod": "4.6.7"
57
60
  },
58
61
  "devDependencies": {
59
- "@deepseek-ai/cordis": "4.0.6-alpha.1",
60
- "@deepseek-ai/dsh-agent": "0.2.2-alpha.2",
61
- "@deepseek-ai/dsh-api-remotes": "0.2.2-alpha.2",
62
- "@deepseek-ai/dsh-atomic-write": "0.2.2-alpha.2",
63
- "@deepseek-ai/dsh-client-locale": "0.2.2-alpha.2",
64
- "@deepseek-ai/dsh-client-store": "0.2.2-alpha.2",
65
- "@deepseek-ai/dsh-client-ui-layout": "0.2.2-alpha.2",
66
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.2-alpha.2",
67
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.2-alpha.2",
68
- "@deepseek-ai/dsh-client-ui-settings": "0.2.2-alpha.2",
69
- "@deepseek-ai/dsh-client-ui-slots": "0.2.2-alpha.2",
70
- "@deepseek-ai/dsh-credentials": "0.2.2-alpha.2",
71
- "@deepseek-ai/dsh-home-paths": "0.2.2-alpha.2",
72
- "@deepseek-ai/dsh-host-webserver": "0.2.2-alpha.2",
73
- "@deepseek-ai/dsh-jobs": "0.2.2-alpha.2",
74
- "@deepseek-ai/dsh-jobs-local": "0.2.2-alpha.2",
75
- "@deepseek-ai/dsh-output-retention": "0.2.2-alpha.2",
76
- "@deepseek-ai/dsh-package-manifest": "0.2.2-alpha.2",
77
- "@deepseek-ai/dsh-ptc-runtime": "0.2.2-alpha.2",
78
- "@deepseek-ai/dsh-sandbox": "0.2.2-alpha.2",
79
- "@deepseek-ai/dsh-scope": "0.2.2-alpha.2",
80
- "@deepseek-ai/dsh-session": "0.2.2-alpha.2",
81
- "@deepseek-ai/dsh-session-projection": "0.2.2-alpha.2",
82
- "@deepseek-ai/dsh-settings": "0.2.2-alpha.2",
83
- "@deepseek-ai/dsh-shell": "0.2.2-alpha.2",
84
- "@deepseek-ai/dsh-subprocess": "0.2.2-alpha.2",
85
- "@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.2",
86
- "@deepseek-ai/dsh-tools": "0.2.2-alpha.2",
87
- "@deepseek-ai/dsh-typert-protocol": "0.2.2-alpha.2",
88
- "@deepseek-ai/dsh-user-approval": "0.2.2-alpha.2",
89
- "@deepseek-ai/schemastery": "3.18.6-alpha.1",
62
+ "@deepseek-ai/cordis": "4.0.6-alpha.2",
63
+ "@deepseek-ai/cordis-plugin-include": "1.0.11-alpha.2",
64
+ "@deepseek-ai/cordis-plugin-loader": "1.0.7-alpha.2",
65
+ "@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
66
+ "@deepseek-ai/dsh-api-remotes": "0.2.2-alpha.3",
67
+ "@deepseek-ai/dsh-atomic-write": "0.2.2-alpha.3",
68
+ "@deepseek-ai/dsh-bash-local": "0.2.2-alpha.3",
69
+ "@deepseek-ai/dsh-client-locale": "0.2.2-alpha.3",
70
+ "@deepseek-ai/dsh-client-store": "0.2.2-alpha.3",
71
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.2-alpha.3",
72
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.2-alpha.3",
73
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.2-alpha.3",
74
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.2-alpha.3",
75
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.2-alpha.3",
76
+ "@deepseek-ai/dsh-credentials": "0.2.2-alpha.3",
77
+ "@deepseek-ai/dsh-home-paths": "0.2.2-alpha.3",
78
+ "@deepseek-ai/dsh-host-webserver": "0.2.2-alpha.3",
79
+ "@deepseek-ai/dsh-http-proxy": "0.2.2-alpha.3",
80
+ "@deepseek-ai/dsh-jobs": "0.2.2-alpha.3",
81
+ "@deepseek-ai/dsh-jobs-local": "0.2.2-alpha.3",
82
+ "@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
83
+ "@deepseek-ai/dsh-output-retention": "0.2.2-alpha.3",
84
+ "@deepseek-ai/dsh-package-manifest": "0.2.2-alpha.3",
85
+ "@deepseek-ai/dsh-ptc-runtime": "0.2.2-alpha.3",
86
+ "@deepseek-ai/dsh-sandbox": "0.2.2-alpha.3",
87
+ "@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
88
+ "@deepseek-ai/dsh-session": "0.2.2-alpha.3",
89
+ "@deepseek-ai/dsh-session-projection": "0.2.2-alpha.3",
90
+ "@deepseek-ai/dsh-settings": "0.2.2-alpha.3",
91
+ "@deepseek-ai/dsh-shell": "0.2.2-alpha.3",
92
+ "@deepseek-ai/dsh-subprocess": "0.2.2-alpha.3",
93
+ "@deepseek-ai/dsh-subprocess-local": "0.2.2-alpha.3",
94
+ "@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.3",
95
+ "@deepseek-ai/dsh-timeout": "0.2.2-alpha.3",
96
+ "@deepseek-ai/dsh-tools": "0.2.2-alpha.3",
97
+ "@deepseek-ai/dsh-typert-protocol": "0.2.2-alpha.3",
98
+ "@deepseek-ai/dsh-user-approval": "0.2.2-alpha.3",
99
+ "@deepseek-ai/schemastery": "3.18.6-alpha.2",
90
100
  "@e18e/eslint-plugin": "0.8.1",
91
101
  "@types/node": "26.6.6",
92
102
  "@types/react": "19.3.0",
@@ -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",
@@ -104,15 +115,15 @@
104
115
  "vitest": "5.0.5"
105
116
  },
106
117
  "peerDependencies": {
107
- "@deepseek-ai/dsh-atomic-write": "^0.2.1-rc.3",
108
- "@deepseek-ai/dsh-credentials": "^0.2.1-rc.3",
109
- "@deepseek-ai/dsh-home-paths": "^0.2.1-rc.3",
110
- "@deepseek-ai/dsh-typert-protocol": "^0.2.1-rc.3",
111
- "@deepseek-ai/schemastery": "3.18.6-alpha.1"
118
+ "@deepseek-ai/dsh-atomic-write": "^0.2.1-rc.3 || ^0.2.2-alpha.2",
119
+ "@deepseek-ai/dsh-credentials": "^0.2.1-rc.3 || ^0.2.2-alpha.2",
120
+ "@deepseek-ai/dsh-home-paths": "^0.2.1-rc.3 || ^0.2.2-alpha.2",
121
+ "@deepseek-ai/dsh-typert-protocol": "^0.2.1-rc.3 || ^0.2.2-alpha.2",
122
+ "@deepseek-ai/schemastery": "^3.18.6-alpha.1"
112
123
  },
113
124
  "icon": "icon.svg",
114
125
  "engines": {
115
- "dsh": "^0.2.1-rc.3",
126
+ "dsh": "^0.2.1-rc.3 || ^0.2.2-alpha.2",
116
127
  "node": ">=22.12.0"
117
128
  },
118
129
  "dsh": {
@@ -125,7 +136,7 @@
125
136
  "immediately": true
126
137
  }
127
138
  },
128
- "ocrCliVersion": "1.13.3",
139
+ "ocrCliVersion": "1.13.4",
129
140
  "scripts": {
130
141
  "build:client": "node build-client.mjs",
131
142
  "build": "node build-host.mjs",
@@ -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 fromEnv = typeof process.env.DSH_HOME === "string" ? process.env.DSH_HOME.trim() : "";
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
- * 或取到了更低一层时以警告形式报出(用户必须知道生效的 key 不是最高优先级给的)。
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
- return isRecord(error) && error.code === "ENOENT";
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 有两处会把 mark.snippet(出错点邻域的
126
- * **原文**)附在文本后面——构造函数里的 `this.message = formatError(this, false)`,以及
127
- * `toString()` 在 compact 为假时的分支。而 .credentials.yaml 的 refs.<REF> 里放的就是
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
- * 实测 `String(err)` 与 `err.message` 两条路都会带出 snippet,只有 toString(true)
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
- // js-yaml 的 YAMLException 覆盖了 toString(compact):传 true 走紧凑分支,
139
- // 只有这一条路不带 snippet。别的 Error 会忽略这个参数,返回普通的
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
- // 到这里 error 不是 Error 实例:String(error) 会隐式走 Object.prototype.toString
159
- // 产出 "[object Object]" 这种毫无信息量的文本,直接给空串。
160
- return "";
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
- * 托管 YAML 文档里的 refs.<REF>(形状不合一律回 undefined,不抛)。
165
- * 解析失败走 `yamlErrorDetail`,绝不把邻域原文带进 stderr。
164
+ * 读取并解析托管文档;读不到或解析失败都留一条 problem,交回 undefined。
166
165
  * @param {string} credPath
167
- * @param {string} refName
168
- * @returns {string | undefined}
166
+ * @returns {unknown}
169
167
  */
170
- function fromCredentialsDoc(credPath, refName) {
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
- const refs = isRecord(doc) ? doc.refs : undefined;
190
- let value = isRecord(refs) ? refs[refName] : undefined;
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
- value = undefined;
254
+ } else {
255
+ key = singleLineKeyOf(value, refName, credPath);
199
256
  }
200
- // OCR 的 api_key_cmd 契约只认**单行** stdout(空/多行都是硬错误)。托管文档里
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
- /** 未加引号的取值:trim,含「 #」时从那里剥离行内注释;剥完为空白按没有值。 */
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 parsed = parseDotEnvValue(match.groups?.value ?? "");
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 found =
353
- fromCredentialsDoc(path.join(home, CREDENTIALS_FILENAME), refName) ??
354
- fromInheritedEnv(process.env[refName]) ??
355
- fromUserDotEnv(path.join(home, ".env"), refName);
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
- // 成功回退也要说话:托管文件损坏/无权限/ref 形状错被回退层盖过去时,用户必须知道
367
- // 生效的 key 不是最高优先级那层给的。problems 在收集时已按脱敏口径拼装(YAML 报错走
368
- // yamlErrorDetail,绝不带邻域原文),这里原样转述即可。
430
+ // 成功回退也要说话:查过的层里有坏消息时,用户必须知道生效的 key 得自哪一层。
431
+ // problems 在收集时已按脱敏口径拼装(YAML 报错走 yamlErrorDetail,绝不带邻域
432
+ // 原文),这里原样转述。只报实际来源,不推断「更低的一层」——坏消息可能来自
433
+ // .env 自己的坏行,而值也正是同一层给的。
369
434
  if (problems.length > 0) {
370
435
  process.stderr.write(
371
- `[get-cred] 注意:高优先级凭据层有坏消息(${problems.join(";")}),${refName} 实际取自更低的一层\n`,
436
+ `[get-cred] 注意:查凭据时遇到坏消息(${problems.join(";")}),${refName} 实际取自 ${source}\n`,
372
437
  );
373
438
  }
374
439
  process.stdout.write(`${found}\n`);