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/client.js +551 -31
- package/lib/client.js.map +1 -1
- package/lib/index.js +561 -98
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/inline.ts +192 -0
- package/src/ai/llmTypes.ts +83 -0
- package/src/ai/rpc.ts +35 -0
- package/src/capture.ts +2 -1
- package/src/client/ai/inlineProvider.ts +190 -0
- package/src/client/compat.ts +3 -2
- package/src/client/externalOpen.ts +3 -2
- package/src/client/index.ts +7 -6
- package/src/client/log.ts +10 -0
- package/src/client/sidebarBridge.ts +6 -5
- package/src/client/state/records.ts +1 -1
- package/src/client/state/regions.ts +13 -6
- package/src/client/ui/AiSettings.ts +104 -0
- package/src/client/ui/EditorView.ts +65 -4
- package/src/client/ui/McpSettings.ts +3 -1
- package/src/debugLog.ts +84 -0
- package/src/fileOpenSettings.ts +70 -8
- package/src/host-deps.d.ts +3 -0
- package/src/index.ts +21 -14
- package/src/log.ts +19 -0
- package/src/mcpIsolation.ts +2 -1
- package/src/rpc.ts +10 -76
- package/src/shared/ai.ts +158 -0
- package/src/shared/diff.ts +18 -0
- package/src/shared/logger.ts +114 -0
- package/src/shared/rpc.ts +9 -0
- package/src/store.ts +8 -4
- package/src/workspace.ts +2 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
+
log.warn("settings.installSection 安装失败(" + String(error) + "),section " + ns + " 降级为配置值");
|
|
235
351
|
recordInstall("none", INSTALL_NONE);
|
|
236
352
|
}
|
|
237
353
|
});
|
|
238
354
|
} catch (error) {
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
10593
|
+
/** LSP 服务器管理器(语言智能:跳转/引用/大纲;诊断行带模块内前缀如 [lsp-manager])。 */
|
|
10594
|
+
const lspManager = createLspManager((line) => log.debug(line));
|
|
10135
10595
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
10136
10596
|
const warnings = [];
|
|
10137
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
10186
|
-
if (
|
|
10187
|
-
|
|
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
|
-
|
|
10654
|
+
log.warn("兼容性报告生成失败:" + String(error));
|
|
10192
10655
|
}
|
|
10193
10656
|
}
|
|
10194
10657
|
//#endregion
|