dsh-vscode-mode 0.1.57 → 0.1.59

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/lib/index.js CHANGED
@@ -140,9 +140,125 @@ const SHELL_MENU_LEAF = "DSHEditor";
140
140
  /** 深链基址默认值(DSH Web GUI 默认端口,dsh-web.cmd 实证)。 */
141
141
  const INTEGRATION_BASE_DEFAULT = "http://127.0.0.1:3080";
142
142
  //#endregion
143
+ //#region src/shared/logger.ts
144
+ /**
145
+ * dsh-vscode-mode — 统一日志核心(双面共享,平台中立)。
146
+ * 输出规范:
147
+ * - 格式:`[dsh-vscode-mode][:scope] 消息`;前缀只在模块内部拼接,业务代码禁止手写前缀
148
+ * - 级别语义:debug=诊断明细 / info=装配与路由里程碑 / warn=降级与兼容回退 / error=失败
149
+ * - 出口:host 面经 bindHostLog(ctx) 绑定 ctx.logger(缺失回退 console);client 面固定 console
150
+ * - 诊断文件通道(edrv.debug → ~/.dsh/dsh-vscode-mode/logs/)见 debugLog.ts,与本模块前缀共用
151
+ * 新增日志一律 `import { log } from '../log.js'`(client 为 './log.js')后调 log.debug/info/warn/error;
152
+ * 子域日志用 log.child('scope'),禁止另起 console。
153
+ * 作者 ddj 2026-09-08
154
+ */
155
+ /** 插件统一日志前缀(与包安装名一致)。 */
156
+ const LOG_PREFIX = "[dsh-vscode-mode]";
157
+ /** 默认级别到 console 方法名的映射。 */
158
+ const CONSOLE_METHODS = {
159
+ debug: "debug",
160
+ info: "info",
161
+ warn: "warn",
162
+ error: "error"
163
+ };
164
+ /**
165
+ * 默认出口:console(级别直映方法;方法缺失时静默丢弃,不抛错)。
166
+ * @author ddj 2026年09月08号
167
+ * @returns console 出口
168
+ */
169
+ function consoleSink() {
170
+ return (level, line) => {
171
+ const emit = console[CONSOLE_METHODS[level]];
172
+ if (typeof emit === "function") emit.call(console, line);
173
+ };
174
+ }
175
+ /**
176
+ * DSH host 出口工厂:转接 ctx.logger(方法级可选调用,缺服务/缺方法回退 console)。
177
+ * @author ddj 2026年09月08号
178
+ * @param ctx DSH host 上下文
179
+ * @returns 日志出口
180
+ */
181
+ function ctxLogSink(ctx) {
182
+ const base = consoleSink();
183
+ return (level, line) => {
184
+ const logger = ctx?.logger;
185
+ const emit = logger?.[level];
186
+ if (typeof emit === "function") emit.call(logger, line);
187
+ else base(level, line);
188
+ };
189
+ }
190
+ /**
191
+ * 拼一行日志:`[dsh-vscode-mode][:scope] 消息`(scope 为空省略冒号段)。
192
+ * @author ddj 2026年09月08号
193
+ * @param scope 子域(可空)
194
+ * @param message 消息文本
195
+ * @returns 整行文本
196
+ */
197
+ function formatLine(scope, message) {
198
+ const text = String(message);
199
+ return scope ? LOG_PREFIX + ":" + scope + " " + text : LOG_PREFIX + " " + text;
200
+ }
201
+ /**
202
+ * 日志器工厂(内部):emit 时读共享出口状态,保证父 bind 后子实例跟随。
203
+ * @author ddj 2026年09月08号
204
+ * @param state 共享出口状态
205
+ * @param scope 子域文本(可空)
206
+ * @returns 日志器
207
+ */
208
+ function makeLogger(state, scope) {
209
+ const emit = (level, message) => state.sink(level, formatLine(scope, message));
210
+ return {
211
+ debug: (message) => emit("debug", message),
212
+ info: (message) => emit("info", message),
213
+ warn: (message) => emit("warn", message),
214
+ error: (message) => emit("error", message),
215
+ bind(next) {
216
+ state.sink = next;
217
+ },
218
+ child(next) {
219
+ return makeLogger(state, scope ? scope + "." + next : next);
220
+ }
221
+ };
222
+ }
223
+ /**
224
+ * 创建插件日志器(默认 console 出口;host 面单例由 bindHostLog 换到 ctx.logger)。
225
+ * @author ddj 2026年09月08号
226
+ * @param sink 初始出口(缺省 console)
227
+ * @param scope 初始子域(业务方一般经 child 派生,不直接传)
228
+ * @returns 日志器
229
+ */
230
+ function createLogger(sink = consoleSink(), scope = "") {
231
+ return makeLogger({ sink }, scope);
232
+ }
233
+ //#endregion
234
+ //#region src/log.ts
235
+ /**
236
+ * dsh-vscode-mode host — 日志单例(全 host 面唯一日志入口)。
237
+ * 默认 console 出口;apply() 首行调 bindHostLog(ctx) 切到 ctx.logger(缺失回退 console)。
238
+ * 用法:`import { log } from './log.js'` → `log.debug/info/warn/error(msg)`;子域用 `log.child('scope')`。
239
+ * 作者 ddj 2026-09-08
240
+ */
241
+ /** host 面插件日志单例(格式/级别语义见 shared/logger.ts 头注释)。 */
242
+ const log = createLogger();
243
+ /**
244
+ * 装配期绑定 host 日志出口:ctx.logger 可用时全部日志转经 DSH 日志系统。
245
+ * @author ddj 2026年09月08号
246
+ * @param ctx DSH host 上下文(logger 服务缺失时保持 console 出口)
247
+ */
248
+ function bindHostLog(ctx) {
249
+ log.bind(ctxLogSink(ctx));
250
+ }
251
+ //#endregion
143
252
  //#region src/fileOpenSettings.ts
144
253
  const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
145
254
  const FILE_OPEN_DEFAULT = "auto";
255
+ /** AI 内联补全配置默认值(默认关闭;路由空 = 自动;档位空 = 跟随模型默认)。 */
256
+ const AI_CONFIG_DEFAULT = {
257
+ enabled: false,
258
+ provider: "",
259
+ model: "",
260
+ effort: ""
261
+ };
146
262
  let depsPromise;
147
263
  /**
148
264
  * 动态加载设置依赖(模块级缓存;任一缺失/失败返回 null 而非抛错)。
@@ -205,7 +321,7 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
205
321
  deps = await loader();
206
322
  } catch {}
207
323
  if (!deps) {
208
- console.warn("[dsh-vscode-mode] 设置依赖不可用,section " + ns + " 未安装(配置回退)");
324
+ log.warn("设置依赖不可用,section " + ns + " 未安装(配置回退)");
209
325
  return recordInstall("none", INSTALL_NONE);
210
326
  }
211
327
  const legacy = deps.installSettingsSection;
@@ -213,7 +329,7 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
213
329
  legacy(ctx, ns, schema, entry, hooks);
214
330
  return recordInstall("legacy", INSTALL_LEGACY);
215
331
  } catch (error) {
216
- console.warn("[dsh-vscode-mode] legacy 设置安装失败,尝试服务路由:" + String(error));
332
+ log.warn("legacy 设置安装失败,尝试服务路由:" + String(error));
217
333
  }
218
334
  const ctxInject = ctx.inject;
219
335
  if (typeof ctxInject !== "function") return recordInstall("none", INSTALL_NONE);
@@ -223,7 +339,7 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
223
339
  const provider = typeof sc.get === "function" ? sc.get("settings") : sc.settings;
224
340
  const install = provider?.installSection;
225
341
  if (typeof install !== "function") {
226
- console.warn("[dsh-vscode-mode] settings 服务无 installSection(DSH 版本 API 变化),section " + ns + " 降级为配置值");
342
+ log.warn("settings 服务无 installSection(DSH 版本 API 变化),section " + ns + " 降级为配置值");
227
343
  recordInstall("none", INSTALL_NONE);
228
344
  return;
229
345
  }
@@ -231,12 +347,12 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
231
347
  install.call(provider, ctx, ns, schema, entry, hooks);
232
348
  recordInstall("service", INSTALL_SERVICE);
233
349
  } catch (error) {
234
- console.warn("[dsh-vscode-mode] settings.installSection 安装失败(" + String(error) + "),section " + ns + " 降级为配置值");
350
+ log.warn("settings.installSection 安装失败(" + String(error) + "),section " + ns + " 降级为配置值");
235
351
  recordInstall("none", INSTALL_NONE);
236
352
  }
237
353
  });
238
354
  } catch (error) {
239
- console.warn("[dsh-vscode-mode] settings 服务路由不可用(" + String(error) + ")");
355
+ log.warn("settings 服务路由不可用(" + String(error) + ")");
240
356
  return recordInstall("none", INSTALL_NONE);
241
357
  }
242
358
  if (observedInstall.strategy !== "unknown") return observedInstall.strategy;
@@ -285,20 +401,34 @@ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSe
285
401
  fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
286
402
  keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
287
403
  sidebarMinWidth: deps.z.number().default(300),
288
- integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT)
404
+ integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
405
+ aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
406
+ aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
407
+ aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
408
+ aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort)
289
409
  }), entry, {
290
410
  setSource: (source) => hooks.setSource(source),
291
411
  onChange: hooks.onChange
292
412
  }, loader);
293
413
  return strategy === "legacy" || strategy === "service";
294
414
  }
415
+ /** AI 配置脏值读取(settings 未就绪时回退默认)。 */
416
+ function aiValueOf(stored) {
417
+ const raw = stored ?? {};
418
+ return {
419
+ enabled: raw.aiInline === true,
420
+ provider: typeof raw.aiProvider === "string" ? raw.aiProvider : AI_CONFIG_DEFAULT.provider,
421
+ model: typeof raw.aiModel === "string" ? raw.aiModel : AI_CONFIG_DEFAULT.model,
422
+ effort: typeof raw.aiEffort === "string" ? raw.aiEffort : AI_CONFIG_DEFAULT.effort
423
+ };
424
+ }
295
425
  /**
296
426
  * 注册设置命名空间,并提供 host 侧的读取与更新状态。
297
427
  * @author ddj 2026年08月24号
298
428
  * @param ctx DSH host 上下文
299
429
  * @param config 插件组合配置
300
430
  * @param onChange 设置变化回调
301
- * @returns 设置状态
431
+ * @returns 设置状态(fileOpenTool 值 + AI 补全配置读写)
302
432
  */
303
433
  function setupOpenSettings(ctx, config, onChange) {
304
434
  let current = configValue(config);
@@ -311,17 +441,27 @@ function setupOpenSettings(ctx, config, onChange) {
311
441
  const syncRevision = () => {
312
442
  revision = (provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.revision;
313
443
  };
314
- const setSource = (source) => notify(source().fileOpenTool);
315
444
  const settingsChange = () => syncRevision();
445
+ /** AI 配置当前值(settings 未就绪回退默认)。 */
446
+ let aiCurrent = { ...AI_CONFIG_DEFAULT };
447
+ const aiSync = () => {
448
+ const stored = (provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
449
+ aiCurrent = stored !== void 0 ? aiValueOf(stored) : aiCurrent;
450
+ };
316
451
  installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
317
452
  fileOpenTool: current,
318
453
  integrationBaseUrl: baseValueOf(config)
319
454
  }, {
320
- setSource,
455
+ setSource: (source) => {
456
+ notify(source().fileOpenTool);
457
+ aiCurrent = aiValueOf(source());
458
+ syncRevision();
459
+ },
321
460
  onChange: settingsChange
322
461
  });
323
462
  ctx.inject?.(["settings"], (settingsCtx) => {
324
463
  provider = settingsCtx.get("settings");
464
+ aiSync();
325
465
  syncRevision();
326
466
  });
327
467
  return {
@@ -341,6 +481,37 @@ function setupOpenSettings(ctx, config, onChange) {
341
481
  const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
342
482
  notify(stored?.fileOpenTool ?? next);
343
483
  syncRevision();
484
+ },
485
+ ai: () => aiCurrent,
486
+ aiUpdate: async (patch, expectedRevision) => {
487
+ if (!provider?.update) {
488
+ if (patch.enabled !== void 0) aiCurrent.enabled = patch.enabled;
489
+ if (patch.provider !== void 0) aiCurrent.provider = patch.provider;
490
+ if (patch.model !== void 0) aiCurrent.model = patch.model;
491
+ if (patch.effort !== void 0) aiCurrent.effort = patch.effort;
492
+ return aiCurrent;
493
+ }
494
+ const stored = { ...aiCurrent };
495
+ const body = {};
496
+ if (patch.enabled !== void 0) {
497
+ stored.enabled = patch.enabled;
498
+ body.aiInline = patch.enabled;
499
+ }
500
+ if (patch.provider !== void 0) {
501
+ stored.provider = patch.provider;
502
+ body.aiProvider = patch.provider;
503
+ }
504
+ if (patch.model !== void 0) {
505
+ stored.model = patch.model;
506
+ body.aiModel = patch.model;
507
+ }
508
+ if (patch.effort !== void 0) {
509
+ stored.effort = patch.effort;
510
+ body.aiEffort = patch.effort;
511
+ }
512
+ await provider.update(FILE_OPEN_SETTINGS_NS, body, expectedRevision);
513
+ aiSync();
514
+ return aiCurrent;
344
515
  }
345
516
  };
346
517
  }
@@ -1246,6 +1417,22 @@ function hunkOf(record, idx) {
1246
1417
  function isNoopHunk(hunk) {
1247
1418
  return !!hunk && hunk.oldText !== null && hunk.oldText === hunk.newText;
1248
1419
  }
1420
+ /**
1421
+ * 差异比较口径归一化:剥离 UTF-8 BOM、统一 CRLF 为 LF。
1422
+ * 背景:外部工具(pwsh/编辑器)可能改变 BOM/行尾,导致 after 指纹与 hunk
1423
+ * 定位在内容语义未变的情况下误报 conflict,把待确认修改从 UI 排除。
1424
+ * @author ddj 2026年09月09号
1425
+ */
1426
+ function normalizeForCompare(text) {
1427
+ return text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
1428
+ }
1429
+ /** 归一化一个 hunk 的两侧文本(与 normalizeForCompare 同口径)。 */
1430
+ function normalizeHunk(hunk) {
1431
+ return {
1432
+ oldText: hunk.oldText === null ? null : normalizeForCompare(hunk.oldText),
1433
+ newText: normalizeForCompare(hunk.newText)
1434
+ };
1435
+ }
1249
1436
  /** 按文件顺序为多 hunk 写入不重复的 after 坐标。 */
1250
1437
  function annotateHunks(hunks, before, after) {
1251
1438
  if (typeof after !== "string") return hunks;
@@ -1698,7 +1885,7 @@ async function saveBucket(ctx, cwd, recsMap, session) {
1698
1885
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1699
1886
  await fs.writeText(target, JSON.stringify(existing), void 0, void 0, policyOf(ctx, session));
1700
1887
  } catch (error) {
1701
- console.error("edrv saveBucket failed", error);
1888
+ log.error("saveBucket failed: " + String(error));
1702
1889
  }
1703
1890
  }
1704
1891
  /**
@@ -1763,7 +1950,12 @@ async function recordIsStale(ctx, session, cache, rec) {
1763
1950
  rec.conflict = false;
1764
1951
  return false;
1765
1952
  }
1766
- if (locateHunks(state.content, pending.map((item) => item.hunk)).some((location) => location.matched)) {
1953
+ const normalizedContent = normalizeForCompare(state.content);
1954
+ if (rec.after != null && fingerprint(normalizeForCompare(rec.after)) === fingerprint(normalizedContent)) {
1955
+ rec.conflict = false;
1956
+ return false;
1957
+ }
1958
+ if (locateHunks(normalizedContent, pending.map((item) => normalizeHunk(item.hunk))).some((location) => location.matched)) {
1767
1959
  rec.conflict = false;
1768
1960
  return false;
1769
1961
  }
@@ -1838,7 +2030,7 @@ async function appendArchiveEntries(ctx, cwd, entries, session) {
1838
2030
  };
1839
2031
  await fs.writeText(target, JSON.stringify(data), void 0, void 0, policyOf(ctx, session));
1840
2032
  } catch (error) {
1841
- console.error("edrv appendArchiveEntries failed", error);
2033
+ log.error("appendArchiveEntries failed: " + String(error));
1842
2034
  }
1843
2035
  }
1844
2036
  /**
@@ -2432,7 +2624,7 @@ async function captureToolResult(ctx, registry, exec, result) {
2432
2624
  await saveBucket(ctx, cwd, bucket, session);
2433
2625
  invalidateIndex(ctx, cwd, path);
2434
2626
  } catch (error) {
2435
- console.error("edrv capture failed", error);
2627
+ log.error("capture failed: " + String(error));
2436
2628
  }
2437
2629
  }
2438
2630
  //#endregion
@@ -5366,6 +5558,83 @@ async function revealInExplorer(ctx, absPath, isDir) {
5366
5558
  }
5367
5559
  }
5368
5560
  //#endregion
5561
+ //#region src/debugLog.ts
5562
+ /**
5563
+ * dsh-vscode-mode host — 诊断文件通道(client 上报 → 缓冲批量落盘)。
5564
+ * 通道:edrv.debug RPC → 内存缓冲(满/空闲触发)→ 串行读改写
5565
+ * `~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log`(512KB 上限截断保留尾部)。
5566
+ * 设计:console 不一定落盘,文件可靠;写失败静默忽略(调试日志不阻塞业务)。
5567
+ * 迁移自 src/rpc.ts(enqueueDebug/flushDebug),逻辑一字不改,仅终端回显改走统一日志器。
5568
+ * 作者 ddj 2026-09-08
5569
+ */
5570
+ /** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
5571
+ const debugBuffers = /* @__PURE__ */ new Map();
5572
+ /** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
5573
+ const debugWriteQueues = /* @__PURE__ */ new Map();
5574
+ /** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
5575
+ const DEBUG_LOG_CAP = 524288;
5576
+ /** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
5577
+ const DEBUG_BUF_CAP = 32768;
5578
+ /** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
5579
+ const DEBUG_FLUSH_IDLE_MS = 1e3;
5580
+ /** cwd → 已清理旧工作区 debug 日志标记(一次性)。 */
5581
+ const debugLegacyCleaned = /* @__PURE__ */ new Set();
5582
+ /** 诊断回显日志器(终端回显统一带 `[dsh-vscode-mode:debug]` 前缀)。 */
5583
+ const debugLog = log.child("debug");
5584
+ /**
5585
+ * 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
5586
+ * 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
5587
+ * @author ddj 2026年09月08号
5588
+ * @param cwd 工作区(日志落 ~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log)
5589
+ * @param batch 本批日志行
5590
+ */
5591
+ function flushDebug(cwd, batch) {
5592
+ const task = (debugWriteQueues.get(cwd) ?? Promise.resolve()).then(async () => {
5593
+ try {
5594
+ await mkdir(pluginLogRoot(), { recursive: true });
5595
+ const target = debugLogFile(cwd);
5596
+ const appended = await readFile(target, "utf8").catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
5597
+ const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended;
5598
+ await writeFile(target, next, "utf8");
5599
+ if (!debugLegacyCleaned.has(cwd)) {
5600
+ debugLegacyCleaned.add(cwd);
5601
+ await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {});
5602
+ }
5603
+ } catch (e) {}
5604
+ });
5605
+ debugWriteQueues.set(cwd, task);
5606
+ return task;
5607
+ }
5608
+ /**
5609
+ * 记录一条 client 上报的诊断日志:入队缓冲批量落盘 + 终端回显(`[dsh-vscode-mode:debug]` 前缀)。
5610
+ * @author ddj 2026年09月08号
5611
+ * @param _ctx DSH 上下文(入参保留:诊断通道按 cwd 隔离,当前不需要 ctx,未来过滤用)
5612
+ * @param cwd 工作区(日志文件按 cwd hash 隔离)
5613
+ * @param text 单条日志文本(不含换行)
5614
+ */
5615
+ function debugRecord(_ctx, cwd, text) {
5616
+ const st = debugBuffers.get(cwd) ?? {
5617
+ lines: [],
5618
+ len: 0,
5619
+ timer: null
5620
+ };
5621
+ st.lines.push(text);
5622
+ st.len += text.length;
5623
+ const flush = () => {
5624
+ st.timer = null;
5625
+ const batch = st.lines;
5626
+ st.lines = [];
5627
+ st.len = 0;
5628
+ flushDebug(cwd, batch);
5629
+ };
5630
+ if (st.len >= DEBUG_BUF_CAP) {
5631
+ if (st.timer) clearTimeout(st.timer);
5632
+ flush();
5633
+ } else if (!st.timer) st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS);
5634
+ debugBuffers.set(cwd, st);
5635
+ debugLog.error(text);
5636
+ }
5637
+ //#endregion
5369
5638
  //#region src/perf.ts
5370
5639
  /**
5371
5640
  * dsh-vscode-mode host — DSH 会话性能管理(会话卫生 + 体积 stat + 侧车摘要)。
@@ -5823,75 +6092,10 @@ function patchInsertPerfConfig(text) {
5823
6092
  }
5824
6093
  //#endregion
5825
6094
  //#region src/rpc.ts
5826
- /** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
5827
- const debugBuffers = /* @__PURE__ */ new Map();
5828
- /** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
5829
- const debugWriteQueues = /* @__PURE__ */ new Map();
5830
- /** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
5831
- const DEBUG_LOG_CAP = 524288;
5832
- /** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
5833
- const DEBUG_BUF_CAP = 32768;
5834
- /** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
5835
- const DEBUG_FLUSH_IDLE_MS = 1e3;
5836
6095
  /** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
5837
6096
  const staleCheckedAt = /* @__PURE__ */ new Map();
5838
6097
  /** stale 自动清理最小间隔。 */
5839
6098
  const STALE_CHECK_MIN_MS = 1e4;
5840
- /**
5841
- * 调试日志入队:先攒内存缓冲(行 + 字节数),满 DEBUG_BUF_CAP 立即落盘,
5842
- * 否则空闲 DEBUG_FLUSH_IDLE_MS 后落盘;落盘 = 读旧文件 + 追加整批 + 超限截断 + 一次写入。
5843
- * @author ddj 2026年08月26号 / 2026年09月01号
5844
- * @param ctx DSH 上下文
5845
- * @param cwd 工作区(日志按 cwd hash 存 ~/.dsh/dsh-vscode-mode/logs/)
5846
- * @param line 单条日志文本(不含换行)
5847
- */
5848
- function enqueueDebug(ctx, cwd, line) {
5849
- const st = debugBuffers.get(cwd) ?? {
5850
- lines: [],
5851
- len: 0,
5852
- timer: null
5853
- };
5854
- st.lines.push(line);
5855
- st.len += line.length;
5856
- const flush = () => {
5857
- st.timer = null;
5858
- const batch = st.lines;
5859
- st.lines = [];
5860
- st.len = 0;
5861
- flushDebug(cwd, batch);
5862
- };
5863
- if (st.len >= DEBUG_BUF_CAP) {
5864
- if (st.timer) clearTimeout(st.timer);
5865
- flush();
5866
- } else if (!st.timer) st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS);
5867
- debugBuffers.set(cwd, st);
5868
- }
5869
- /** cwd → 已清理旧工作区 debug 日志标记(一次性)。 */
5870
- const debugLegacyCleaned = /* @__PURE__ */ new Set();
5871
- /**
5872
- * 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
5873
- * 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
5874
- * @author ddj 2026年08月26号 / 2026年09月01号
5875
- * @param cwd 工作区(日志落 ~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log)
5876
- * @param batch 本批日志行
5877
- */
5878
- function flushDebug(cwd, batch) {
5879
- const task = (debugWriteQueues.get(cwd) ?? Promise.resolve()).then(async () => {
5880
- try {
5881
- await mkdir(pluginLogRoot(), { recursive: true });
5882
- const target = debugLogFile(cwd);
5883
- const appended = await readFile(target, "utf8").catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
5884
- const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended;
5885
- await writeFile(target, next, "utf8");
5886
- if (!debugLegacyCleaned.has(cwd)) {
5887
- debugLegacyCleaned.add(cwd);
5888
- await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {});
5889
- }
5890
- } catch (e) {}
5891
- });
5892
- debugWriteQueues.set(cwd, task);
5893
- return task;
5894
- }
5895
6099
  /** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
5896
6100
  function recView(record) {
5897
6101
  return {
@@ -6023,9 +6227,10 @@ async function integrationBaseUrlOf(ctx) {
6023
6227
  return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
6024
6228
  }
6025
6229
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
6026
- function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
6230
+ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers) {
6027
6231
  return {
6028
6232
  ...lspHandlers ?? {},
6233
+ ...aiHandlers ?? {},
6029
6234
  "edrv.list": async (args) => {
6030
6235
  const sc = await requireSession(ctx, args.sessionId);
6031
6236
  if ("err" in sc) return {
@@ -6395,9 +6600,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
6395
6600
  ok: false,
6396
6601
  error: sc.err
6397
6602
  };
6398
- const text = String(args.text ?? "");
6399
- enqueueDebug(ctx, sc.cwd, text);
6400
- console.error("[edrv-debug] " + text);
6603
+ debugRecord(ctx, sc.cwd, String(args.text ?? ""));
6401
6604
  return { ok: true };
6402
6605
  },
6403
6606
  "edrv.searchFiles": async (args) => {
@@ -7094,8 +7297,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7094
7297
  * 统一入口:按方法分发到 handler 表。
7095
7298
  * @author ddj 2026年08月20号 / 2026年08月26号
7096
7299
  */
7097
- async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
7098
- const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers)[method];
7300
+ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers) {
7301
+ const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers)[method];
7099
7302
  if (!handler) return {
7100
7303
  ok: false,
7101
7304
  error: "未知方法: " + String(method)
@@ -7166,7 +7369,7 @@ function syncAgent(ctx, state, agent, tools) {
7166
7369
  if (denied.length > 0) try {
7167
7370
  state.dispose = agent.ctx.tools.restrict({ deny: denied });
7168
7371
  } catch (error) {
7169
- ctx.logger?.warn?.("[dsh-vscode-mode] MCP agent restriction failed: " + String(error));
7372
+ log.warn("MCP agent restriction failed: " + String(error));
7170
7373
  return;
7171
7374
  }
7172
7375
  state.denyKey = nextKey;
@@ -10100,6 +10303,261 @@ function createLspRpc(deps) {
10100
10303
  disposeSession
10101
10304
  };
10102
10305
  }
10306
+ /** 补全请求超时(毫秒):codebuddy CLI 网关实测 TTFT 3.5~8.4s 抖动(debug 实证),
10307
+ * 12s 内无任何 token 即放弃;token 一到仍按行数/字符早停。 */
10308
+ const AI_TIMEOUT_MS = 12e3;
10309
+ /**
10310
+ * 拼装补全 user 消息文本:语言/文件名 + 前缀 + 补全点标记 + 后缀。
10311
+ * host 侧与单测共用,保证请求形状一致。
10312
+ * @author ddj
10313
+ * @param req 补全请求(已裁剪)
10314
+ * @returns 用户消息文本
10315
+ */
10316
+ function buildInlinePrompt(req) {
10317
+ return [
10318
+ "文件:" + (String(req.path || "").split(/[\\/]/).pop() || "untitled"),
10319
+ "现有代码(光标位于 <CURSOR> 标记处):",
10320
+ req.prefix + "<CURSOR>" + req.suffix
10321
+ ].join("\n");
10322
+ }
10323
+ /** 补全 system 提示词:硬约束输出形态(只给插入文本)。 */
10324
+ const AI_INLINE_SYSTEM = "你是代码自动补全引擎。只输出应插入 <CURSOR> 处的代码文本:禁止任何解释、注释性语句、markdown 代码围栏或对原文的复述;最多 3 行,从光标处自然衔接前文,不要重复前缀已有内容;无法给出高置信度补全时输出空文本。";
10325
+ /**
10326
+ * 清理模型输出:剥 markdown 围栏、剥对 <CURSOR> 标记的复述、按行数截断、去首尾空行。
10327
+ * 产出空文本的判定也在这里(纯函数,单测覆盖)。
10328
+ * @author ddj
10329
+ * @param raw 模型原始输出
10330
+ * @param prefix 请求时的前缀(复述检测)
10331
+ * @returns 清理后的插入文本(可能为空串)
10332
+ */
10333
+ function cleanInlineText(raw, prefix) {
10334
+ let text = typeof raw === "string" ? raw : "";
10335
+ if (!text) return "";
10336
+ const fenced = /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(text.trim());
10337
+ if (fenced) text = fenced[1] ?? "";
10338
+ const cursorAt = text.indexOf("<CURSOR>");
10339
+ if (cursorAt >= 0) text = text.slice(cursorAt + 8);
10340
+ const tailPrefix = prefix.length > 0 ? prefix.slice(-40) : "";
10341
+ if (tailPrefix && text.startsWith(tailPrefix)) text = text.slice(tailPrefix.length);
10342
+ const lines = text.split("\n").slice(0, 3).map((l) => l.replace(/\s+$/, ""));
10343
+ while (lines.length > 0 && lines[0] === "") lines.shift();
10344
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
10345
+ return lines.join("\n");
10346
+ }
10347
+ //#endregion
10348
+ //#region src/ai/inline.ts
10349
+ /** 模型目录缓存(进程级:5 分钟 TTL,避免每次打开设置页都逐模型 resolve)。 */
10350
+ const DIR_TTL_MS = 3e5;
10351
+ let dirCache = null;
10352
+ /**
10353
+ * 取 llm 运行时(惰性;缺失/未装配返回 null 并给诊断)。
10354
+ * @author ddj
10355
+ * @param ctx DSH 上下文
10356
+ * @returns llm 运行时或 null
10357
+ */
10358
+ function llmOf(ctx) {
10359
+ try {
10360
+ const llm = ctx.get("llm");
10361
+ return llm && typeof llm.stream === "function" ? llm : null;
10362
+ } catch {
10363
+ return null;
10364
+ }
10365
+ }
10366
+ /**
10367
+ * 解析请求路由:用户配置优先(provider/model 均非空),否则取目录首个 provider+model。
10368
+ * @author ddj
10369
+ * @param llm llm 运行时
10370
+ * @param cfg AI 配置
10371
+ * @returns 路由(provider/model);不可路由返回 null
10372
+ */
10373
+ async function routeOf(llm, cfg) {
10374
+ if (cfg.provider && cfg.model) return {
10375
+ provider: cfg.provider,
10376
+ model: cfg.model
10377
+ };
10378
+ const providers = llm.listProviders();
10379
+ if (!providers.length) return null;
10380
+ const provider = cfg.provider || providers[0].id;
10381
+ const models = await llm.listModels(provider).catch(() => []);
10382
+ if (!models.length) return null;
10383
+ return {
10384
+ provider,
10385
+ model: models[0].id
10386
+ };
10387
+ }
10388
+ /**
10389
+ * 单次 AI 内联补全。
10390
+ * @author ddj
10391
+ * @param deps 服务依赖
10392
+ * @param req 补全请求(client 已裁剪窗口)
10393
+ * @returns 补全结果(text 空 = 无建议)
10394
+ */
10395
+ async function aiInlineComplete(deps, req) {
10396
+ const cfg = deps.config.get();
10397
+ if (!cfg.enabled) return { text: "" };
10398
+ if (!req || typeof req.prefix !== "string" || !req.prefix) return { text: "" };
10399
+ const llm = llmOf(deps.ctx);
10400
+ if (!llm) return {
10401
+ text: "",
10402
+ note: "llm 服务不可用"
10403
+ };
10404
+ const route = await routeOf(llm, cfg);
10405
+ if (!route) return {
10406
+ text: "",
10407
+ note: "未找到可用模型(请检查 DSH 模型配置)"
10408
+ };
10409
+ const controller = new AbortController();
10410
+ const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("timeout")), AI_TIMEOUT_MS);
10411
+ try {
10412
+ const stream = llm.stream({
10413
+ provider: route.provider,
10414
+ model: route.model,
10415
+ ...cfg.effort ? { reasoningEffort: cfg.effort } : {},
10416
+ messages: [{
10417
+ role: "user",
10418
+ content: [{
10419
+ type: "text",
10420
+ text: buildInlinePrompt(req)
10421
+ }],
10422
+ source: { kind: "user" }
10423
+ }],
10424
+ system: AI_INLINE_SYSTEM,
10425
+ temperature: .2,
10426
+ maxTokens: 120,
10427
+ signal: controller.signal
10428
+ });
10429
+ let text = "";
10430
+ let aborted = false;
10431
+ let failNote = "";
10432
+ let reasoningChars = 0;
10433
+ for await (const chunk of stream) {
10434
+ const c = chunk;
10435
+ if (c.type === "text-delta") {
10436
+ text += c.text;
10437
+ if (text.split("\n").length > 3 || text.length >= 300) {
10438
+ controller.abort(/* @__PURE__ */ new Error("early-stop"));
10439
+ aborted = true;
10440
+ }
10441
+ } else if (c.type === "reasoning-delta") reasoningChars += c.text.length;
10442
+ else if (c.type === "finish") {
10443
+ const kind = c.reason?.kind;
10444
+ if (kind === "error" || kind === "aborted") {
10445
+ const failure = c.reason.failure;
10446
+ failNote = (failure?.code ?? kind) + ": " + (failure?.message ?? "模型调用失败");
10447
+ log.debug("[ai-inline] 流失败:" + failNote);
10448
+ }
10449
+ break;
10450
+ } else if (c.type === "usage" || c.type === "block-start" || c.type === "block-end") {} else if (c.type === "tool-call-delta") {}
10451
+ if (aborted) break;
10452
+ }
10453
+ if (failNote) return {
10454
+ text: "",
10455
+ note: "AI 补全失败:" + failNote
10456
+ };
10457
+ const clean = cleanInlineText(text, req.prefix);
10458
+ if (!clean) {
10459
+ if (text.length === 0 && reasoningChars > 0) {
10460
+ log.debug("[ai-inline] 仅思考无正文(reasoning " + reasoningChars + " 字符)");
10461
+ return {
10462
+ text: "",
10463
+ note: "该模型只返回了思考没有正文,请在设置页换非思考模型"
10464
+ };
10465
+ }
10466
+ log.debug("[ai-inline] 空输出(原始 " + text.length + " 字符)");
10467
+ return { text: "" };
10468
+ }
10469
+ return { text: clean };
10470
+ } catch (error) {
10471
+ const msg = error instanceof Error ? error.message : String(error);
10472
+ log.debug("[ai-inline] 失败:" + msg);
10473
+ return {
10474
+ text: "",
10475
+ note: "AI 补全失败:" + msg
10476
+ };
10477
+ } finally {
10478
+ clearTimeout(timer);
10479
+ }
10480
+ }
10481
+ /**
10482
+ * 模型目录视图(edrv.ai.models):DSH 已配置 provider 全量 + 逐模型思考档位。
10483
+ * @author ddj
10484
+ * @param ctx DSH 上下文
10485
+ * @param force 跳过缓存强制刷新
10486
+ * @returns 目录视图(llm 缺失时 note 说明 + 空目录)
10487
+ */
10488
+ async function aiModels(ctx, force = false) {
10489
+ if (!force && dirCache && Date.now() - dirCache.at < DIR_TTL_MS) return dirCache.view;
10490
+ const llm = llmOf(ctx);
10491
+ if (!llm) return {
10492
+ providers: [],
10493
+ note: "llm 服务不可用(无法枚举模型)"
10494
+ };
10495
+ const out = { providers: [] };
10496
+ for (const p of llm.listProviders()) {
10497
+ const models = await llm.listModels(p.id).catch(() => []);
10498
+ const entries = [];
10499
+ for (const m of models) {
10500
+ let efforts = [];
10501
+ let def = "";
10502
+ try {
10503
+ const info = await llm.resolveModelInfo(p.id, m.id);
10504
+ efforts = (info.reasoning?.efforts ?? []).map((e) => ({
10505
+ id: e.id,
10506
+ name: e.name
10507
+ }));
10508
+ def = info.reasoning?.defaultEffort ?? "";
10509
+ } catch {}
10510
+ entries.push({
10511
+ provider: p.id,
10512
+ model: m.id,
10513
+ name: m.name || m.id,
10514
+ efforts,
10515
+ defaultEffort: def
10516
+ });
10517
+ }
10518
+ out.providers.push({
10519
+ id: p.id,
10520
+ name: p.name || p.id,
10521
+ models: entries
10522
+ });
10523
+ }
10524
+ dirCache = {
10525
+ view: out,
10526
+ at: Date.now()
10527
+ };
10528
+ return out;
10529
+ }
10530
+ //#endregion
10531
+ //#region src/ai/rpc.ts
10532
+ /**
10533
+ * 构造 edrv.ai.* handlers(一次性创建,无跨请求状态——目录缓存在 inline.ts 模块级)。
10534
+ * @author ddj
10535
+ * @param deps 依赖(ctx/settings)
10536
+ * @returns handlers
10537
+ */
10538
+ function createAiRpc(deps) {
10539
+ return { handlers: {
10540
+ "edrv.ai.inline": async (args) => ({
10541
+ ok: true,
10542
+ ...await aiInlineComplete({
10543
+ ctx: deps.ctx,
10544
+ config: { get: () => deps.settings.ai() }
10545
+ }, args)
10546
+ }),
10547
+ "edrv.ai.models": async (args) => ({
10548
+ ok: true,
10549
+ ...await aiModels(deps.ctx, args?.force === true)
10550
+ }),
10551
+ "edrv.ai.configGet": async () => ({
10552
+ ok: true,
10553
+ ...deps.settings.ai()
10554
+ }),
10555
+ "edrv.ai.configUpdate": async (args) => ({
10556
+ ok: true,
10557
+ ...await deps.settings.aiUpdate(args)
10558
+ })
10559
+ } };
10560
+ }
10103
10561
  //#endregion
10104
10562
  //#region src/index.ts
10105
10563
  /**
@@ -10107,7 +10565,8 @@ function createLspRpc(deps) {
10107
10565
  * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
10108
10566
  * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
10109
10567
  * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
10110
- * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
10568
+ * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)
10569
+ * + log/debugLog(统一日志:bindHostLog 绑定 ctx.logger,edrv.debug 诊断文件通道)。
10111
10570
  * 作者 ddj 2026-08-20
10112
10571
  */
10113
10572
  const name = "dsh-vscode-mode";
@@ -10127,18 +10586,20 @@ const inject = [
10127
10586
  * @param config 插件配置(可选 imageDir 覆盖图标目录)
10128
10587
  */
10129
10588
  function apply(ctx, config) {
10589
+ bindHostLog(ctx);
10130
10590
  const registry = /* @__PURE__ */ new Map();
10131
10591
  const searcher = newSearcher(ctx);
10132
10592
  const contentSearcher = newContentSearcher(ctx);
10133
- /** LSP 服务器管理器(语言智能:跳转/引用/大纲)。 */
10134
- const lspManager = createLspManager((line) => ctx.logger?.debug?.("[" + name + "] " + line));
10593
+ /** LSP 服务器管理器(语言智能:跳转/引用/大纲;诊断行带模块内前缀如 [lsp-manager])。 */
10594
+ const lspManager = createLspManager((line) => log.debug(line));
10135
10595
  /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
10136
10596
  const warnings = [];
10137
- setupOpenSettings(ctx, config, () => {});
10597
+ /** 设置状态(fileOpenTool + AI 补全配置读写;settings 不可用时内存态降级)。 */
10598
+ const openSettings = setupOpenSettings(ctx, config, () => {});
10138
10599
  /** LSP 配置为配置值模式:插件组合配置 + 会话内运行时覆盖(原 settings section 命名空间不合法,见 lsp/config.ts)。 */
10139
10600
  /** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
10140
10601
  const rulesInstalled = installRulesSection(ctx);
10141
- if (!rulesInstalled) ctx.logger?.warn?.("[" + name + "] 未检测到 systemPrompt 服务,规则仅可管理不注入");
10602
+ if (!rulesInstalled) log.warn("未检测到 systemPrompt 服务,规则仅可管理不注入");
10142
10603
  /** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
10143
10604
  const lspRpc = createLspRpc({
10144
10605
  ctx,
@@ -10146,6 +10607,10 @@ function apply(ctx, config) {
10146
10607
  manager: lspManager
10147
10608
  });
10148
10609
  const lspHandlers = lspRpc.handlers;
10610
+ const aiHandlers = createAiRpc({
10611
+ ctx,
10612
+ settings: openSettings
10613
+ }).handlers;
10149
10614
  ctx.on("tools/result", (exec, result) => {
10150
10615
  captureToolResult(ctx, registry, exec, result);
10151
10616
  });
@@ -10161,7 +10626,7 @@ function apply(ctx, config) {
10161
10626
  const sid = session?.id;
10162
10627
  if (typeof sid === "string") lspRpc.disposeSession(sid);
10163
10628
  });
10164
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning));
10629
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers), (warning) => warnings.push(warning));
10165
10630
  installIsolation(ctx);
10166
10631
  ctx.effect(() => shellMenuLifecycle(ctx));
10167
10632
  sweepTreeCache();
@@ -10170,7 +10635,7 @@ function apply(ctx, config) {
10170
10635
  lspManager.disposeAll().catch(() => {});
10171
10636
  disposeAllServers();
10172
10637
  });
10173
- ctx.logger?.info?.("[" + name + "] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ")");
10638
+ log.info("编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ")");
10174
10639
  }
10175
10640
  /**
10176
10641
  * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
@@ -10182,13 +10647,11 @@ async function logCompatSummary(ctx, warnings) {
10182
10647
  try {
10183
10648
  const report = await buildReport(ctx);
10184
10649
  for (const warning of warnings) report.warnings.push(warning);
10185
- const logger = ctx.logger;
10186
- if (!logger?.info) return;
10187
- const head = "[dsh-vscode-mode] 兼容性:" + (report.dshVersion ? "DSH " + report.dshVersion : "DSH 未探测") + " · " + report.external.length + " 项外部适配 / " + report.guards.length + " 项护栏 / " + (report.adapters?.length ?? 0) + " 项版本适配";
10188
- if (report.warnings.length) logger.warn?.(head + ",警告 " + report.warnings.length + " 条:" + report.warnings.join(";"));
10189
- else logger.info?.(head + ",无警告");
10650
+ const head = "兼容性:" + (report.dshVersion ? "DSH " + report.dshVersion : "DSH 未探测") + " · " + report.external.length + " 项外部适配 / " + report.guards.length + " 项护栏 / " + (report.adapters?.length ?? 0) + " 项版本适配";
10651
+ if (report.warnings.length) log.warn(head + ",警告 " + report.warnings.length + " 条:" + report.warnings.join(";"));
10652
+ else log.info(head + ",无警告");
10190
10653
  } catch (error) {
10191
- ctx.logger?.warn?.("[dsh-vscode-mode] 兼容性报告生成失败:" + String(error));
10654
+ log.warn("兼容性报告生成失败:" + String(error));
10192
10655
  }
10193
10656
  }
10194
10657
  //#endregion