dsh-vscode-mode 0.17.0 → 0.19.0
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/README.md +8 -4
- package/lib/client.debug.js +934 -0
- package/lib/client.debug.js.map +1 -0
- package/lib/client.js +1425 -1724
- package/lib/client.js.map +1 -1
- package/lib/client.rules.js +793 -0
- package/lib/client.rules.js.map +1 -0
- package/lib/index.js +1078 -286
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/client/index.ts +24 -0
- package/src/client/lazyPanel.ts +68 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/sidebar/PanelBoundary.ts +57 -0
- package/src/client/sidebar/SidebarView.ts +58 -21
- package/src/client/sidebar/panels/FileExplorer.ts +80 -27
- package/src/client/sidebar/panels/SvnPanel.ts +24 -8
- package/src/client/sidebar/panels/index.ts +3 -4
- package/src/client/sidebar/panels/treeWindow.ts +106 -0
- package/src/client/sidebar/panels/useExplorerReveal.ts +5 -2
- package/src/client/sidebar/registry.ts +4 -1
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/styles/editor.css +13 -1
- package/src/client/textPages.ts +135 -0
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +147 -29
- package/src/client/ui/LargeTextView.ts +94 -0
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/mcp.ts +1 -0
- package/src/mcpBridge.ts +163 -0
- package/src/mcpDiscover.ts +38 -0
- package/src/mcpPool.ts +245 -0
- package/src/mcpProject.ts +15 -12
- package/src/mcpRuntime.ts +299 -254
- package/src/rpc.ts +42 -8
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +8 -2
- package/src/shared/textPages.ts +28 -0
- package/src/textPages.ts +149 -0
- package/src/treeIndex.ts +62 -37
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, realpath, r
|
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
7
|
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { DatabaseSync } from "node:sqlite";
|
|
9
9
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
@@ -295,6 +295,469 @@ function installIsolation(ctx) {
|
|
|
295
295
|
syncAll();
|
|
296
296
|
}
|
|
297
297
|
//#endregion
|
|
298
|
+
//#region src/shared/mcp.ts
|
|
299
|
+
/** @public @author ddj 2026年10月20号 @param input Project-only policy. @returns Validated defaults. @throws Invalid explicit policy. */
|
|
300
|
+
function parseMcpPolicy(input) {
|
|
301
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) throw new Error("vscodeMode 必须是对象");
|
|
302
|
+
const policy = input ?? {};
|
|
303
|
+
const startup = policy.startup ?? "on-demand";
|
|
304
|
+
const sharing = policy.sharing ?? "agent";
|
|
305
|
+
const startupTimeoutMs = policy.startupTimeoutMs ?? 6e4;
|
|
306
|
+
if (!["on-demand", "background"].includes(startup)) throw new Error("不支持的 MCP startup");
|
|
307
|
+
if (!["agent", "workspace"].includes(sharing)) throw new Error("不支持的 MCP sharing");
|
|
308
|
+
if (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs < 1e3 || startupTimeoutMs > 3e5) throw new Error("MCP startupTimeoutMs 必须为 1000–300000 的整数");
|
|
309
|
+
return {
|
|
310
|
+
startup,
|
|
311
|
+
sharing,
|
|
312
|
+
startupTimeoutMs
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/mcpBridge.ts
|
|
317
|
+
/** Private official-MCP registrations, never a global provider. @author ddj 2026年10月20号 */
|
|
318
|
+
/** Per-agent public surface; original definitions still run inside the normal registry pipeline. */
|
|
319
|
+
var McpBinding = class {
|
|
320
|
+
agent;
|
|
321
|
+
pool;
|
|
322
|
+
entry;
|
|
323
|
+
valid;
|
|
324
|
+
called;
|
|
325
|
+
removals = [];
|
|
326
|
+
unsubscribe;
|
|
327
|
+
closed = false;
|
|
328
|
+
/** @public @author ddj 2026年10月20号 @param agent Public scope. @param pool Connection owner. @param entry Exact generation. @param valid Authorization recheck. */
|
|
329
|
+
constructor(agent, pool, entry, valid, called) {
|
|
330
|
+
this.agent = agent;
|
|
331
|
+
this.pool = pool;
|
|
332
|
+
this.entry = entry;
|
|
333
|
+
this.valid = valid;
|
|
334
|
+
this.called = called;
|
|
335
|
+
}
|
|
336
|
+
/** @private @author ddj 2026年10月20号 @param execution Registry caller. @throws Stale generation or foreign caller. */
|
|
337
|
+
check(execution) {
|
|
338
|
+
if (this.closed || !this.valid() || !this.entry.live || execution && execution.agent !== this.agent) throw new Error("项目 MCP 作用域或配置已失效");
|
|
339
|
+
}
|
|
340
|
+
/** @public @author ddj 2026年10月20号 Reconciles exact definitions after official tool-list updates. */
|
|
341
|
+
sync() {
|
|
342
|
+
this.check();
|
|
343
|
+
const bridge = this.entry.bridge;
|
|
344
|
+
if (!bridge) return;
|
|
345
|
+
this.clear();
|
|
346
|
+
for (const [name, definition] of bridge.tools) {
|
|
347
|
+
/** @author ddj 2026年10月20号 @param args Registry-validated arguments. @param execution Unmodified caller context. @returns Official canonical output. */
|
|
348
|
+
const execute = async (args, execution) => {
|
|
349
|
+
this.check(execution);
|
|
350
|
+
return this.pool.run(this.entry, async () => {
|
|
351
|
+
this.check(execution);
|
|
352
|
+
if (bridge.tools.get(name) !== definition) throw new Error("MCP 工具定义已更新,请重新发现");
|
|
353
|
+
const startedAt = Date.now();
|
|
354
|
+
const result = await definition.execute(args, execution);
|
|
355
|
+
this.called?.(Date.now() - startedAt);
|
|
356
|
+
return result;
|
|
357
|
+
}, execution.signal);
|
|
358
|
+
};
|
|
359
|
+
this.removals.push(this.agent.ctx.get("tools").register({
|
|
360
|
+
...definition,
|
|
361
|
+
execute
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
for (const section of bridge.sections.values()) {
|
|
365
|
+
/** @author ddj 2026年10月20号 @param context Actual prompt assembly. @returns Only valid-generation instructions. */
|
|
366
|
+
const text = (context) => {
|
|
367
|
+
if (!this.valid() || this.closed) return "";
|
|
368
|
+
if (typeof section.text === "function") return section.text(context);
|
|
369
|
+
return section.text;
|
|
370
|
+
};
|
|
371
|
+
this.removals.push(this.agent.ctx.get("systemPrompt").section({
|
|
372
|
+
...section,
|
|
373
|
+
text
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
/** @author ddj 2026年10月20号 Reconciles dynamic official registrations only for a live subscriber. */
|
|
377
|
+
const update = () => {
|
|
378
|
+
if (!this.closed && this.valid() && this.entry.live) this.sync();
|
|
379
|
+
};
|
|
380
|
+
if (!this.unsubscribe) this.unsubscribe = bridge.subscribe(update);
|
|
381
|
+
}
|
|
382
|
+
/** @private @author ddj 2026年10月20号 Removes only this binding's exact scoped registrations. */
|
|
383
|
+
clear() {
|
|
384
|
+
for (const remove of this.removals.splice(0).reverse()) remove();
|
|
385
|
+
}
|
|
386
|
+
/** @public @author ddj 2026年10月20号 Revokes proxies synchronously before releasing a lease. */
|
|
387
|
+
dispose() {
|
|
388
|
+
this.closed = true;
|
|
389
|
+
this.unsubscribe?.();
|
|
390
|
+
this.unsubscribe = void 0;
|
|
391
|
+
this.clear();
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
/** A connection's private surface. Public consumers register scoped proxies, not this scope. */
|
|
395
|
+
var McpBridge = class McpBridge {
|
|
396
|
+
scope;
|
|
397
|
+
tools = /* @__PURE__ */ new Map();
|
|
398
|
+
resources = /* @__PURE__ */ new Map();
|
|
399
|
+
sections = /* @__PURE__ */ new Map();
|
|
400
|
+
listeners = /* @__PURE__ */ new Set();
|
|
401
|
+
closed = false;
|
|
402
|
+
closing;
|
|
403
|
+
/** @private @author ddj 2026年10月20号 @param scope Private lifetime and registration context. */
|
|
404
|
+
constructor(scope) {
|
|
405
|
+
this.scope = scope;
|
|
406
|
+
}
|
|
407
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host owner. @param make Optional real scope factory for installed-host tests. @returns Isolated registration adapter. */
|
|
408
|
+
static async create(ctx, make) {
|
|
409
|
+
const scope = (make ?? (await loadHostModule("@deepseek-ai/dsh-scope")).createScope)(ctx, {});
|
|
410
|
+
await scope.ctx.fiber?.await?.();
|
|
411
|
+
const bridge = new McpBridge(scope);
|
|
412
|
+
bridge.install();
|
|
413
|
+
return bridge;
|
|
414
|
+
}
|
|
415
|
+
/** @private @author ddj 2026年10月20号 Installs only private services; no project registration reaches the root. */
|
|
416
|
+
install() {
|
|
417
|
+
const privateCtx = this.scope.ctx.isolate("tools").isolate("mcpResources").isolate("systemPrompt");
|
|
418
|
+
privateCtx.provide("tools", { register: (definition) => this.register(this.tools, definition.name, definition) });
|
|
419
|
+
privateCtx.provide("mcpResources", { register: (name, provider) => this.register(this.resources, name, provider) });
|
|
420
|
+
privateCtx.provide("systemPrompt", {
|
|
421
|
+
getSectionOrder: () => 3100,
|
|
422
|
+
section: (section) => this.register(this.sections, section.name, section)
|
|
423
|
+
});
|
|
424
|
+
this.scope.ctx = privateCtx;
|
|
425
|
+
}
|
|
426
|
+
/** @private @author ddj 2026年10月20号 @param registry Private generation map. @param name Stable registration name. @param entry Exact value. @returns Exact-entry removal, safe after replacement. */
|
|
427
|
+
register(registry, name, entry) {
|
|
428
|
+
if (this.closed) throw new Error("MCP bridge is disposed");
|
|
429
|
+
if (registry.has(name)) throw new Error("Duplicate private MCP registration: " + name);
|
|
430
|
+
registry.set(name, entry);
|
|
431
|
+
this.changed();
|
|
432
|
+
return () => {
|
|
433
|
+
if (registry.get(name) !== entry) return;
|
|
434
|
+
registry.delete(name);
|
|
435
|
+
this.changed();
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** @public @author ddj 2026年10月20号 @param listener Surface change subscriber. @returns Subscription disposer. */
|
|
439
|
+
subscribe(listener) {
|
|
440
|
+
this.listeners.add(listener);
|
|
441
|
+
return () => {
|
|
442
|
+
this.listeners.delete(listener);
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/** @private @author ddj 2026年10月20号 Notifies consumers; observer errors cannot corrupt the official sync transaction. */
|
|
446
|
+
changed() {
|
|
447
|
+
for (const listener of this.listeners) try {
|
|
448
|
+
listener();
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
/** @public @author ddj 2026年10月20号 @returns Quiescent private-scope teardown. */
|
|
452
|
+
dispose() {
|
|
453
|
+
this.closed = true;
|
|
454
|
+
return this.closing ??= this.closeScope();
|
|
455
|
+
}
|
|
456
|
+
/** @private @author ddj 2026年10月20号 @returns Exact scope closure before forgetting registrations. */
|
|
457
|
+
async closeScope() {
|
|
458
|
+
await this.scope.dispose();
|
|
459
|
+
this.tools.clear();
|
|
460
|
+
this.resources.clear();
|
|
461
|
+
this.sections.clear();
|
|
462
|
+
this.changed();
|
|
463
|
+
this.listeners.clear();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/mcpPool.ts
|
|
468
|
+
/** Connection ownership, bounded cold starts and workspace leases. @author ddj 2026年10月20号 */
|
|
469
|
+
/** @private @author ddj 2026年10月20号 @param input Plain effective configuration. @returns Stable secret-bearing identity used only as digest input. */
|
|
470
|
+
function stableConfig(input) {
|
|
471
|
+
if (Array.isArray(input)) return "[" + input.map(stableConfig).join(",") + "]";
|
|
472
|
+
if (input && typeof input === "object") {
|
|
473
|
+
const config = input;
|
|
474
|
+
return "{" + Object.keys(config).sort().map((key) => JSON.stringify(key) + ":" + stableConfig(config[key])).join(",") + "}";
|
|
475
|
+
}
|
|
476
|
+
return JSON.stringify(input) ?? "null";
|
|
477
|
+
}
|
|
478
|
+
/** @public @author ddj 2026年10月20号 @param task Shared work that must remain alive for other waiters. @param signal One waiter's cancellation. @returns Caller-local wait without abandoning owned pool work. */
|
|
479
|
+
function waitMcp(task, signal) {
|
|
480
|
+
if (!signal) return task;
|
|
481
|
+
return new Promise((resolve, reject) => {
|
|
482
|
+
/** @author ddj 2026年10月20号 Rejects only this waiter. */
|
|
483
|
+
const abort = () => {
|
|
484
|
+
reject(/* @__PURE__ */ new Error("MCP 等待已取消"));
|
|
485
|
+
};
|
|
486
|
+
if (signal.aborted) {
|
|
487
|
+
abort();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
491
|
+
task.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/** The pool owns private scopes independently from any first subscriber. */
|
|
495
|
+
var McpPool = class {
|
|
496
|
+
ctx;
|
|
497
|
+
load;
|
|
498
|
+
make;
|
|
499
|
+
entries = /* @__PURE__ */ new Map();
|
|
500
|
+
owners = /* @__PURE__ */ new WeakMap();
|
|
501
|
+
ownerSeq = 0;
|
|
502
|
+
running = 0;
|
|
503
|
+
queue = [];
|
|
504
|
+
jobs = /* @__PURE__ */ new Set();
|
|
505
|
+
stopped = false;
|
|
506
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host lifecycle owner. @param load Official plugin loader. @param make Private scope factory. */
|
|
507
|
+
constructor(ctx, load, make = () => McpBridge.create(ctx)) {
|
|
508
|
+
this.ctx = ctx;
|
|
509
|
+
this.load = load;
|
|
510
|
+
this.make = make;
|
|
511
|
+
}
|
|
512
|
+
/** @private @author ddj 2026年10月20号 @param spec Effective request. @returns Private digest; never emit configuration or secrets. */
|
|
513
|
+
keyOf(spec) {
|
|
514
|
+
let owner = 0;
|
|
515
|
+
if (spec.policy.sharing === "agent") {
|
|
516
|
+
if (!this.owners.has(spec.owner)) this.owners.set(spec.owner, ++this.ownerSeq);
|
|
517
|
+
owner = this.owners.get(spec.owner);
|
|
518
|
+
}
|
|
519
|
+
const workspace = process.platform === "win32" ? spec.workspace.replace(/\\/g, "/").toLowerCase() : spec.workspace;
|
|
520
|
+
return createHash("sha256").update(stableConfig([
|
|
521
|
+
workspace,
|
|
522
|
+
owner,
|
|
523
|
+
spec.config,
|
|
524
|
+
spec.policy
|
|
525
|
+
])).digest("hex");
|
|
526
|
+
}
|
|
527
|
+
/** @public @author ddj 2026年10月20号 @param spec Authorized connection request. @returns Reference-counted single-flight lease. */
|
|
528
|
+
acquire(spec) {
|
|
529
|
+
if (this.stopped) throw new Error("MCP runtime is disposed");
|
|
530
|
+
const key = this.keyOf(spec);
|
|
531
|
+
let entry = this.entries.get(key);
|
|
532
|
+
if (entry?.phase === "error" && entry.retryAt !== void 0 && Date.now() >= entry.retryAt && !entry.close) {
|
|
533
|
+
this.entries.delete(key);
|
|
534
|
+
entry = void 0;
|
|
535
|
+
}
|
|
536
|
+
if (entry && !entry.live) throw new Error(entry.error ?? "MCP 旧连接仍在释放,暂不能创建替代实例");
|
|
537
|
+
if (!entry) {
|
|
538
|
+
entry = {
|
|
539
|
+
key,
|
|
540
|
+
spec,
|
|
541
|
+
refs: 0,
|
|
542
|
+
phase: "queued",
|
|
543
|
+
queuedAt: Date.now(),
|
|
544
|
+
phaseAt: Date.now(),
|
|
545
|
+
ready: Promise.resolve(),
|
|
546
|
+
live: true,
|
|
547
|
+
calls: Promise.resolve()
|
|
548
|
+
};
|
|
549
|
+
this.entries.set(key, entry);
|
|
550
|
+
entry.ready = this.schedule(entry);
|
|
551
|
+
entry.ready.catch(() => void 0);
|
|
552
|
+
}
|
|
553
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
554
|
+
entry.idle = void 0;
|
|
555
|
+
entry.refs += 1;
|
|
556
|
+
let released = false;
|
|
557
|
+
return {
|
|
558
|
+
entry,
|
|
559
|
+
release: () => {
|
|
560
|
+
if (released) return;
|
|
561
|
+
released = true;
|
|
562
|
+
this.release(entry);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/** @private @author ddj 2026年10月20号 @param entry New entry. @returns Queued initialization, bounded to two concurrent starts. */
|
|
567
|
+
schedule(entry) {
|
|
568
|
+
return new Promise((resolve, reject) => {
|
|
569
|
+
/** @author ddj 2026年10月20号 Initializes a still-owned queue item. */
|
|
570
|
+
const run = async () => {
|
|
571
|
+
try {
|
|
572
|
+
await this.start(entry);
|
|
573
|
+
resolve();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
reject(error);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
this.queue.push(run);
|
|
579
|
+
this.pump();
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
|
|
583
|
+
pump() {
|
|
584
|
+
while (this.running < 2 && this.queue.length) {
|
|
585
|
+
const run = this.queue.shift();
|
|
586
|
+
this.running += 1;
|
|
587
|
+
const job = run().finally(() => {
|
|
588
|
+
this.running -= 1;
|
|
589
|
+
this.jobs.delete(job);
|
|
590
|
+
this.pump();
|
|
591
|
+
});
|
|
592
|
+
this.jobs.add(job);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** @private @author ddj 2026年10月20号 @param entry Connection to initialize. @returns Initialization completion, or failure after owned cleanup. */
|
|
596
|
+
async start(entry) {
|
|
597
|
+
if (!entry.live || this.stopped) throw new Error("MCP 启动已取消");
|
|
598
|
+
entry.phase = "loading";
|
|
599
|
+
entry.phaseAt = Date.now();
|
|
600
|
+
entry.startAt = entry.phaseAt;
|
|
601
|
+
let timer;
|
|
602
|
+
const work = this.mount(entry);
|
|
603
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
604
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("MCP 初始化超时")), entry.spec.policy.startupTimeoutMs);
|
|
605
|
+
timer.unref?.();
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
await Promise.race([work, deadline]);
|
|
609
|
+
if (!entry.live) throw new Error("MCP 启动已取消");
|
|
610
|
+
entry.phase = "ready";
|
|
611
|
+
entry.finishedAt = Date.now();
|
|
612
|
+
entry.connectedAt = entry.finishedAt;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
entry.error = "MCP 初始化失败或超时,请查看 Host 日志";
|
|
615
|
+
this.ctx.logger?.warn?.("MCP initialization failed: " + String(error));
|
|
616
|
+
entry.live = false;
|
|
617
|
+
entry.phase = "stopping";
|
|
618
|
+
const closing = this.closeBridge(entry);
|
|
619
|
+
await Promise.allSettled([closing, work]);
|
|
620
|
+
entry.phase = "error";
|
|
621
|
+
entry.finishedAt = Date.now();
|
|
622
|
+
entry.retryAt = Date.now() + 5e3;
|
|
623
|
+
throw new Error(entry.error);
|
|
624
|
+
} finally {
|
|
625
|
+
if (timer) clearTimeout(timer);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
|
|
629
|
+
async mount(entry) {
|
|
630
|
+
const official = await this.load();
|
|
631
|
+
if (!entry.live || this.stopped) return;
|
|
632
|
+
const bridge = await this.make();
|
|
633
|
+
if (!entry.live || this.stopped) {
|
|
634
|
+
await bridge.dispose();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
entry.bridge = bridge;
|
|
638
|
+
entry.phase = "starting";
|
|
639
|
+
entry.phaseAt = Date.now();
|
|
640
|
+
entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config);
|
|
641
|
+
await entry.fiber;
|
|
642
|
+
if (entry.fiber?.state !== void 0 && entry.fiber.state !== 2) throw new Error("MCP 插件未完成激活");
|
|
643
|
+
}
|
|
644
|
+
/** @private @author ddj 2026年10月20号 @param entry Connection to close. @returns Shared closure barrier; rejection retains a fenced entry. */
|
|
645
|
+
async closeBridge(entry) {
|
|
646
|
+
if (entry.close) return entry.close;
|
|
647
|
+
const close = entry.bridge?.dispose() ?? Promise.resolve();
|
|
648
|
+
entry.close = close;
|
|
649
|
+
try {
|
|
650
|
+
await close;
|
|
651
|
+
entry.bridge = void 0;
|
|
652
|
+
entry.fiber = void 0;
|
|
653
|
+
entry.close = void 0;
|
|
654
|
+
} catch (error) {
|
|
655
|
+
entry.error = "MCP 释放未确认,禁止重建,请查看 Host 日志";
|
|
656
|
+
this.ctx.logger?.warn?.("MCP cleanup failed: " + String(error));
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/** @private @author ddj 2026年10月20号 @param entry One lease's owner. Arms workspace-only warm retention. */
|
|
661
|
+
release(entry) {
|
|
662
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
663
|
+
if (entry.refs || !entry.live) return;
|
|
664
|
+
if (entry.spec.policy.sharing === "agent") {
|
|
665
|
+
this.retire(entry).catch(() => void 0);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
entry.idle = setTimeout(() => {
|
|
669
|
+
this.retire(entry).catch(() => void 0);
|
|
670
|
+
}, 6e4);
|
|
671
|
+
entry.idle.unref?.();
|
|
672
|
+
}
|
|
673
|
+
/** @public @author ddj 2026年10月20号 @param entry Invalid generation. @returns Quiescent close; callers may await before starting a replacement. */
|
|
674
|
+
async retire(entry) {
|
|
675
|
+
entry.live = false;
|
|
676
|
+
entry.phase = "stopping";
|
|
677
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
678
|
+
await this.closeBridge(entry);
|
|
679
|
+
await entry.ready.catch(() => void 0);
|
|
680
|
+
await entry.calls.catch(() => void 0);
|
|
681
|
+
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
682
|
+
}
|
|
683
|
+
/** @public @author ddj 2026年10月20号 @param entry Authorized connection. @param action Per-call work retaining original execution identity. @param signal Caller cancellation. @returns Serialized shared result; no direct registry bypass is introduced here. */
|
|
684
|
+
run(entry, action, signal) {
|
|
685
|
+
/** @author ddj 2026年10月20号 Checks cancellation and generation again at actual dispatch. */
|
|
686
|
+
const invoke = async () => {
|
|
687
|
+
await waitMcp(entry.ready, signal);
|
|
688
|
+
if (signal?.aborted || !entry.live || entry.phase !== "ready") throw new Error("MCP 连接不可用或调用已取消");
|
|
689
|
+
return action();
|
|
690
|
+
};
|
|
691
|
+
const result = entry.calls.then(invoke);
|
|
692
|
+
entry.calls = result.catch(() => void 0);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
/** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
|
|
696
|
+
async dispose() {
|
|
697
|
+
this.stopped = true;
|
|
698
|
+
await Promise.all([...this.entries.values()].map((entry) => this.retire(entry)));
|
|
699
|
+
await Promise.all([...this.jobs]);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/mcpDiscover.ts
|
|
704
|
+
/** @public @author ddj 2026年10月20号 @param runtime Authorized runtime. @param ctx Host registration context. @returns Global discovery cleanup; normal capability filters remain authoritative. */
|
|
705
|
+
function installDiscover(runtime, ctx) {
|
|
706
|
+
const tools = ctx.get("tools");
|
|
707
|
+
/** @author ddj 2026年10月20号 @param args Validated operation. @param execution Registry-owned caller identity. @returns Safe capability catalogue or real connected schemas. */
|
|
708
|
+
const execute = async (args, execution) => {
|
|
709
|
+
const agent = execution.agent;
|
|
710
|
+
if (!agent || !runtime.agents.has(agent)) throw new Error("会话未激活,不能使用项目 MCP 发现入口");
|
|
711
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).some((key) => !["action", "serverName"].includes(key))) throw new Error("发现入口仅接受 action 和 serverName");
|
|
712
|
+
if (!["list", "connect"].includes(args.action)) throw new Error("action 必须为 list 或 connect");
|
|
713
|
+
if (args.action === "list") return runtime.catalogue(agent);
|
|
714
|
+
if (typeof args.serverName !== "string") throw new Error("请指定已配置的 serverName");
|
|
715
|
+
return runtime.connect(agent, args.serverName, execution.signal);
|
|
716
|
+
};
|
|
717
|
+
/** @author ddj 2026年10月20号 @param _args Unused. @param result Safe canonical JSON. @returns Native content without bypassing output validation. */
|
|
718
|
+
const render = (_args, result) => [{
|
|
719
|
+
type: "text",
|
|
720
|
+
text: JSON.stringify(result)
|
|
721
|
+
}];
|
|
722
|
+
return tools.register({
|
|
723
|
+
name: "discover_project_mcp",
|
|
724
|
+
description: "List this workspace’s configured project MCP services without starting them, or connect one to discover its real tools. After connecting, call discovered tools in the NEXT request/program; never guess schemas. Global/preset MCPs are not included.",
|
|
725
|
+
parameters: {
|
|
726
|
+
type: "object",
|
|
727
|
+
properties: {
|
|
728
|
+
action: {
|
|
729
|
+
type: "string",
|
|
730
|
+
enum: ["list", "connect"]
|
|
731
|
+
},
|
|
732
|
+
serverName: { type: "string" }
|
|
733
|
+
},
|
|
734
|
+
required: ["action"],
|
|
735
|
+
additionalProperties: false
|
|
736
|
+
},
|
|
737
|
+
output: {
|
|
738
|
+
schema: {},
|
|
739
|
+
render
|
|
740
|
+
},
|
|
741
|
+
execute
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/** @public @author ddj 2026年10月20号 @param runtime Active owners. @param agent Scoped catalogue owner. @returns Scoped instruction cleanup; does not register a tool override. */
|
|
745
|
+
function discoverPrompt(runtime, agent) {
|
|
746
|
+
const prompt = agent.ctx.get("systemPrompt");
|
|
747
|
+
/** @author ddj 2026年10月20号 @returns Current enabled names only; no credential-bearing configuration. */
|
|
748
|
+
const text = () => {
|
|
749
|
+
const names = runtime.catalogue(agent).map((server) => server.serverName);
|
|
750
|
+
if (!names.length) return "";
|
|
751
|
+
return "Project MCP capabilities (may not be started): " + JSON.stringify(names) + ". Use discover_project_mcp action=list, then action=connect with serverName when needed. Only use returned real tool schemas. Native tools appear on the next model request; PTC must finish the current program and start a new program to refresh its SDK. Do not eval or bypass tool approval.";
|
|
752
|
+
};
|
|
753
|
+
return prompt.section({
|
|
754
|
+
name: "vscode-mode:project-mcp",
|
|
755
|
+
order: 3090,
|
|
756
|
+
text,
|
|
757
|
+
interpolate: false
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
//#endregion
|
|
298
761
|
//#region src/shared/diff.ts
|
|
299
762
|
/** 统一取对应 hunk;callHunk 只用于旧记录缺失 hunks 时兼容。 */
|
|
300
763
|
function preciseHunk(record, idx) {
|
|
@@ -2245,22 +2708,13 @@ async function archiveRecords(ctx, session, cwd, bucket, recs, reason, opts) {
|
|
|
2245
2708
|
}
|
|
2246
2709
|
//#endregion
|
|
2247
2710
|
//#region src/mcpRuntime.ts
|
|
2248
|
-
/**
|
|
2711
|
+
/** Project-owned MCP connections and scoped on-demand capabilities. @author ddj 2026年10月20号 */
|
|
2249
2712
|
const runtimes = /* @__PURE__ */ new WeakMap();
|
|
2250
|
-
/**
|
|
2251
|
-
* Load the official plugin from the running host's installation tree.
|
|
2252
|
-
* @private @author ddj 2026年09月28号
|
|
2253
|
-
* @returns Official MCP plugin namespace; rejects if the host package is absent.
|
|
2254
|
-
*/
|
|
2713
|
+
/** @private @author ddj 2026年09月28号 @returns Official namespace from the running Host installation. */
|
|
2255
2714
|
async function loadMcp() {
|
|
2256
2715
|
return loadHostModule(MCP_PACKAGE);
|
|
2257
2716
|
}
|
|
2258
|
-
/**
|
|
2259
|
-
* Resolve one runtime per host, shared by lifecycle and RPC contexts.
|
|
2260
|
-
* @public @author ddj 2026年09月28号
|
|
2261
|
-
* @param ctx Host context.
|
|
2262
|
-
* @returns Lazy runtime; allocating it does not start connections.
|
|
2263
|
-
*/
|
|
2717
|
+
/** @public @author ddj 2026年09月28号 @param ctx Host context. @returns Lazy per-host runtime, without opening connections. */
|
|
2264
2718
|
function runtimeOf(ctx) {
|
|
2265
2719
|
const owner = ctx.root ?? ctx;
|
|
2266
2720
|
let runtime = runtimes.get(owner);
|
|
@@ -2270,178 +2724,273 @@ function runtimeOf(ctx) {
|
|
|
2270
2724
|
}
|
|
2271
2725
|
return runtime;
|
|
2272
2726
|
}
|
|
2273
|
-
/**
|
|
2727
|
+
/** The pool owns transports; each agent owns only revocable public capabilities and leases. */
|
|
2274
2728
|
var McpRuntime = class {
|
|
2275
2729
|
ctx;
|
|
2276
2730
|
load;
|
|
2277
2731
|
projects = /* @__PURE__ */ new Map();
|
|
2278
2732
|
agents = /* @__PURE__ */ new Map();
|
|
2279
|
-
|
|
2733
|
+
pool;
|
|
2280
2734
|
stopped = false;
|
|
2735
|
+
offDiscover;
|
|
2281
2736
|
retired = /* @__PURE__ */ new WeakSet();
|
|
2282
2737
|
module;
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
* @param load Official module resolver, replaceable in targeted tests.
|
|
2288
|
-
*/
|
|
2289
|
-
constructor(ctx, load = loadMcp) {
|
|
2738
|
+
pending = /* @__PURE__ */ new Set();
|
|
2739
|
+
barriers = /* @__PURE__ */ new Map();
|
|
2740
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host services. @param load Official module resolver. @param make Optional controlled private-scope factory for tests. */
|
|
2741
|
+
constructor(ctx, load = loadMcp, make) {
|
|
2290
2742
|
this.ctx = ctx;
|
|
2291
2743
|
this.load = load;
|
|
2744
|
+
this.pool = new McpPool(ctx, () => this.getModule(), make);
|
|
2292
2745
|
}
|
|
2293
|
-
/**
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
* @returns Completion of this mutation, including official startup readiness.
|
|
2298
|
-
*/
|
|
2299
|
-
enqueue(action) {
|
|
2300
|
-
const run = async () => {
|
|
2301
|
-
if (!this.stopped) await action();
|
|
2302
|
-
};
|
|
2303
|
-
const next = this.tail.then(run);
|
|
2304
|
-
this.tail = next.catch(() => void 0);
|
|
2305
|
-
return next;
|
|
2746
|
+
/** @private @author ddj 2026年10月20号 @param task Owned background operation. Tracks failures without abandoning work. */
|
|
2747
|
+
track(task) {
|
|
2748
|
+
this.pending.add(task);
|
|
2749
|
+
task.catch((error) => this.ctx.logger?.warn?.("Project MCP task failed: " + String(error))).finally(() => this.pending.delete(task));
|
|
2306
2750
|
}
|
|
2307
|
-
/**
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2751
|
+
/** @private @author ddj 2026年10月20号 @param agent Scope owner. @returns Live record; retired agents cannot be resurrected. */
|
|
2752
|
+
recordOf(agent) {
|
|
2753
|
+
if (this.stopped || this.retired.has(agent)) return void 0;
|
|
2754
|
+
let record = this.agents.get(agent);
|
|
2755
|
+
if (!record) {
|
|
2756
|
+
record = {
|
|
2757
|
+
instances: /* @__PURE__ */ new Map(),
|
|
2758
|
+
resources: /* @__PURE__ */ new Map(),
|
|
2759
|
+
requested: /* @__PURE__ */ new Set(),
|
|
2760
|
+
live: true,
|
|
2761
|
+
tail: Promise.resolve(),
|
|
2762
|
+
startedAt: Date.now()
|
|
2763
|
+
};
|
|
2764
|
+
this.agents.set(agent, record);
|
|
2765
|
+
}
|
|
2766
|
+
return record;
|
|
2767
|
+
}
|
|
2768
|
+
/** @public @author ddj 2026年10月20号 @param agent Newly created scope. Starts only configuration timing. */
|
|
2769
|
+
prepare(agent) {
|
|
2770
|
+
this.recordOf(agent);
|
|
2771
|
+
}
|
|
2772
|
+
/** @public @author ddj 2026年10月20号 @param agent Owner. @param error Configuration failure, never sent raw to clients. */
|
|
2773
|
+
configError(agent, error) {
|
|
2774
|
+
const record = this.recordOf(agent);
|
|
2775
|
+
if (record) record.error = error === void 0 ? void 0 : "项目 MCP 配置读取或校验失败,保留最后有效配置;请查看 Host 日志";
|
|
2776
|
+
}
|
|
2777
|
+
/** @public @author ddj 2026年09月28号 @param agent Agent identity. @returns Longest registered workspace containing its cwd. */
|
|
2313
2778
|
workspace(agent) {
|
|
2314
2779
|
const paths = (this.ctx.get("workspaceRegistry")?.list?.() ?? []).map((ws) => ws.path);
|
|
2315
2780
|
return matchWorkspace(cwdOfSession(this.ctx, agent?.session) ?? void 0, paths);
|
|
2316
2781
|
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Read configured server ownership, including disabled definitions.
|
|
2319
|
-
* @public @author ddj 2026年09月28号
|
|
2320
|
-
* @returns Server name to workspace mapping.
|
|
2321
|
-
*/
|
|
2782
|
+
/** @public @author ddj 2026年09月28号 @returns Configured namespaces including disabled definitions. */
|
|
2322
2783
|
owners() {
|
|
2323
2784
|
const names = /* @__PURE__ */ new Map();
|
|
2324
2785
|
for (const record of this.agents.values()) for (const [name, instance] of record.instances) names.set(name, instance.path);
|
|
2325
2786
|
for (const [path, defs] of this.projects) for (const name of defs.keys()) names.set(name, path);
|
|
2326
2787
|
return names;
|
|
2327
2788
|
}
|
|
2328
|
-
/**
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
*/
|
|
2789
|
+
/** @private @author ddj 2026年10月20号 @param def Effective definition. @returns Config and policy generation identity. */
|
|
2790
|
+
defKey(def) {
|
|
2791
|
+
return JSON.stringify([def.config, parseMcpPolicy(def.policy)]);
|
|
2792
|
+
}
|
|
2793
|
+
/** @private @author ddj 2026年09月28号 Refreshes existing isolation restrictions synchronously. */
|
|
2333
2794
|
changed() {
|
|
2334
2795
|
this.ctx.emit?.("vscode-mode/mcp-change");
|
|
2335
2796
|
}
|
|
2336
|
-
/**
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2797
|
+
/** @private @author ddj 2026年10月20号 @param instance Public lease to revoke. Revokes synchronously before lease release. */
|
|
2798
|
+
revoke(instance) {
|
|
2799
|
+
instance.binding.dispose();
|
|
2800
|
+
instance.lease.release();
|
|
2801
|
+
}
|
|
2802
|
+
/** @public @author ddj 2026年10月20号 @param path Owner workspace. @param defs Validated snapshot. @param refresh Optional explicitly invalidated service. @param wait Await configuration cleanup, never connection readiness. @returns Publication and optionally cleanup completion. */
|
|
2803
|
+
async replace(path, defs, refresh, wait = true) {
|
|
2804
|
+
if (this.stopped) return;
|
|
2805
|
+
const previous = this.projects.get(path);
|
|
2806
|
+
if (!refresh && previous && JSON.stringify([...previous]) === JSON.stringify([...defs])) return;
|
|
2807
|
+
this.projects.set(path, defs);
|
|
2808
|
+
const closing = /* @__PURE__ */ new Set();
|
|
2809
|
+
for (const [agent, record] of this.agents) for (const [name, instance] of record.instances) {
|
|
2810
|
+
const def = defs.get(name);
|
|
2811
|
+
if (instance.path !== path) continue;
|
|
2812
|
+
if (this.workspace(agent) === path && def?.enabled && this.defKey(def) === instance.key && name !== refresh) continue;
|
|
2813
|
+
record.instances.delete(name);
|
|
2814
|
+
this.revoke(instance);
|
|
2815
|
+
closing.add(instance.lease.entry);
|
|
2816
|
+
}
|
|
2817
|
+
for (const entry of this.pool.entries.values()) {
|
|
2818
|
+
if (entry.spec.workspace !== path) continue;
|
|
2819
|
+
const def = defs.get(entry.spec.config.serverName);
|
|
2820
|
+
const config = def && this.options(def.config, path);
|
|
2821
|
+
if (!def?.enabled || JSON.stringify(config) !== JSON.stringify(entry.spec.config) || JSON.stringify(parseMcpPolicy(def.policy)) !== JSON.stringify(entry.spec.policy) || entry.spec.config.serverName === refresh) closing.add(entry);
|
|
2822
|
+
}
|
|
2823
|
+
const prior = this.barriers.get(path) ?? Promise.resolve();
|
|
2824
|
+
const cleanup = Promise.all([...closing].map((entry) => this.pool.retire(entry))).then(() => void 0);
|
|
2825
|
+
const barrier = Promise.all([prior, cleanup]).then(() => void 0);
|
|
2826
|
+
this.barriers.set(path, barrier);
|
|
2827
|
+
this.track(barrier);
|
|
2828
|
+
this.changed();
|
|
2829
|
+
const updates = [...this.agents].map(([agent, record]) => this.enqueue(agent, record));
|
|
2830
|
+
if (wait) await Promise.all([barrier, ...updates]);
|
|
2831
|
+
}
|
|
2832
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Lightweight queue. @returns Scoped catalogue/resource update, not transport initialization. */
|
|
2833
|
+
enqueue(agent, record) {
|
|
2345
2834
|
const update = async () => {
|
|
2346
|
-
this.
|
|
2347
|
-
this.
|
|
2348
|
-
for (const [agent, record] of this.agents) await this.syncAgent(agent, record, this.workspace(agent) === path ? refresh : void 0);
|
|
2349
|
-
this.changed();
|
|
2835
|
+
if (!record.live || this.stopped) return;
|
|
2836
|
+
this.syncAgent(agent, record);
|
|
2350
2837
|
};
|
|
2351
|
-
|
|
2838
|
+
const task = record.tail.then(update);
|
|
2839
|
+
record.tail = task.catch(() => {
|
|
2840
|
+
record.error = "MCP 清理或入口更新失败,请查看 Host 日志";
|
|
2841
|
+
});
|
|
2842
|
+
this.track(task);
|
|
2843
|
+
return task;
|
|
2352
2844
|
}
|
|
2353
|
-
/**
|
|
2354
|
-
* Attach an agent once and await its project plugin startups.
|
|
2355
|
-
* @public @author ddj 2026年09月28号
|
|
2356
|
-
* @param agent Created or already-active agent.
|
|
2357
|
-
* @returns Completion suitable for the serial agent/created listener.
|
|
2358
|
-
*/
|
|
2845
|
+
/** @public @author ddj 2026年10月20号 @param agent Live agent. @returns Lightweight capability installation; never waits for MCP startup. */
|
|
2359
2846
|
attach(agent) {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2847
|
+
const record = this.recordOf(agent);
|
|
2848
|
+
if (!record) return Promise.resolve();
|
|
2849
|
+
this.offDiscover ??= installDiscover(this, this.ctx);
|
|
2850
|
+
if (!record.remove) record.remove = discoverPrompt(this, agent);
|
|
2851
|
+
return this.enqueue(agent, record);
|
|
2852
|
+
}
|
|
2853
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Scope record. Updates lazy resources and schedules explicit background services. */
|
|
2854
|
+
syncAgent(agent, record) {
|
|
2855
|
+
const path = this.workspace(agent);
|
|
2856
|
+
const defs = this.projects.get(path ?? "") ?? /* @__PURE__ */ new Map();
|
|
2857
|
+
for (const [name, instance] of record.instances) {
|
|
2858
|
+
const def = defs.get(name);
|
|
2859
|
+
if (instance.path === path && def?.enabled && this.defKey(def) === instance.key) continue;
|
|
2860
|
+
record.instances.delete(name);
|
|
2861
|
+
this.revoke(instance);
|
|
2368
2862
|
}
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2863
|
+
for (const remove of record.resources.values()) remove();
|
|
2864
|
+
record.resources.clear();
|
|
2865
|
+
for (const [name, def] of defs) {
|
|
2866
|
+
if (!def.enabled) continue;
|
|
2867
|
+
this.lazyResource(agent, record, name);
|
|
2868
|
+
if (!record.instances.has(name) && (parseMcpPolicy(def.policy).startup === "background" || record.requested.has(name))) this.track(this.connect(agent, name));
|
|
2869
|
+
}
|
|
2870
|
+
record.finishedAt ??= Date.now();
|
|
2871
|
+
this.changed();
|
|
2872
|
+
}
|
|
2873
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param name Enabled namespace. Installs a resource-only on-demand entry without starting a connection. */
|
|
2874
|
+
lazyResource(agent, record, name) {
|
|
2875
|
+
const resources = agent.ctx.get("mcpResources");
|
|
2876
|
+
/** @author ddj 2026年10月20号 @param request Resource operation. @param execution Actual registry caller. @returns Official provider result after demand connection. */
|
|
2877
|
+
const request = async (request, execution) => {
|
|
2878
|
+
if (execution?.agent !== agent) throw new Error("不能借用其他会话的 MCP 资源");
|
|
2879
|
+
await this.connect(agent, name, execution.signal);
|
|
2880
|
+
const instance = record.instances.get(name);
|
|
2881
|
+
if (!instance || !this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2882
|
+
return this.pool.run(instance.lease.entry, async () => {
|
|
2883
|
+
if (!this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2884
|
+
const provider = instance.lease.entry.bridge?.resources.get(name);
|
|
2885
|
+
if (!provider) throw new Error("MCP 未提供资源入口");
|
|
2886
|
+
const startedAt = Date.now();
|
|
2887
|
+
const result = await provider.request(request, execution);
|
|
2888
|
+
instance.firstCallMs ??= Date.now() - startedAt;
|
|
2889
|
+
return result;
|
|
2890
|
+
}, execution.signal);
|
|
2373
2891
|
};
|
|
2374
|
-
|
|
2892
|
+
record.resources.set(name, resources.register(name, { request }));
|
|
2375
2893
|
}
|
|
2376
|
-
/**
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
*/
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2894
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param name Namespace. @param instance Exact lease. @returns Whether a previously issued proxy can still execute. */
|
|
2895
|
+
valid(agent, record, name, instance) {
|
|
2896
|
+
const path = this.workspace(agent);
|
|
2897
|
+
const def = this.projects.get(path ?? "")?.get(name);
|
|
2898
|
+
return !this.stopped && record.live && record.instances.get(name) === instance && instance.path === path && def?.enabled === true && this.defKey(def) === instance.key;
|
|
2899
|
+
}
|
|
2900
|
+
/** @private @author ddj 2026年10月20号 @param config Official config. @param path Default working directory. @returns Detached effective config; project strategy is never forwarded. */
|
|
2901
|
+
options(config, path) {
|
|
2902
|
+
return config.transport === "stdio" ? {
|
|
2903
|
+
...config,
|
|
2904
|
+
cwd: config.cwd || path
|
|
2905
|
+
} : { ...config };
|
|
2906
|
+
}
|
|
2907
|
+
/** @public @author ddj 2026年10月20号 @param agent Actual execution owner. @returns Safe enabled catalogue without opening connections. */
|
|
2908
|
+
catalogue(agent) {
|
|
2909
|
+
const record = this.agents.get(agent);
|
|
2910
|
+
if (!record?.live || this.stopped) return [];
|
|
2911
|
+
return [...this.projects.get(this.workspace(agent) ?? "") ?? []].filter(([, def]) => def.enabled).map(([serverName]) => {
|
|
2912
|
+
const entry = record.instances.get(serverName)?.lease.entry;
|
|
2913
|
+
return {
|
|
2914
|
+
serverName,
|
|
2915
|
+
phase: entry?.fiber?.state === 3 ? "error" : entry?.phase ?? "idle"
|
|
2916
|
+
};
|
|
2917
|
+
});
|
|
2387
2918
|
}
|
|
2388
|
-
/**
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2919
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param path Workspace. @param name Namespace. @param def Validated definition. @returns Synchronously reserved lease and public binding. */
|
|
2920
|
+
reserve(agent, record, path, name, def) {
|
|
2921
|
+
const lease = this.pool.acquire({
|
|
2922
|
+
workspace: path,
|
|
2923
|
+
owner: agent,
|
|
2924
|
+
config: this.options(def.config, path),
|
|
2925
|
+
policy: parseMcpPolicy(def.policy)
|
|
2926
|
+
});
|
|
2927
|
+
const instance = {
|
|
2928
|
+
key: this.defKey(def),
|
|
2929
|
+
path,
|
|
2930
|
+
lease,
|
|
2931
|
+
startedAt: Date.now(),
|
|
2932
|
+
reused: lease.entry.refs > 1 || lease.entry.phase === "ready"
|
|
2399
2933
|
};
|
|
2400
|
-
|
|
2934
|
+
const valid = () => this.valid(agent, record, name, instance);
|
|
2935
|
+
instance.binding = new McpBinding(agent, this.pool, lease.entry, valid, (elapsed) => {
|
|
2936
|
+
instance.firstCallMs ??= elapsed;
|
|
2937
|
+
});
|
|
2938
|
+
record.instances.set(name, instance);
|
|
2939
|
+
instance.ready = lease.entry.ready.then(() => {
|
|
2940
|
+
if (!valid()) throw new Error("项目 MCP 配置已失效");
|
|
2941
|
+
instance.binding.sync();
|
|
2942
|
+
this.changed();
|
|
2943
|
+
});
|
|
2944
|
+
this.track(instance.ready);
|
|
2945
|
+
return instance;
|
|
2401
2946
|
}
|
|
2402
|
-
/**
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2947
|
+
/** @public @author ddj 2026年10月20号 @param agent Actual active caller. @param name Enabled configured server. @param signal Caller-local cancellation. @returns Real tool schemas after private initial sync; no guessed tools. */
|
|
2948
|
+
async connect(agent, name, signal) {
|
|
2949
|
+
const record = this.agents.get(agent);
|
|
2950
|
+
if (!record?.live || this.stopped) throw new Error("会话未激活,不能连接项目 MCP");
|
|
2951
|
+
await waitMcp(record.tail, signal);
|
|
2952
|
+
const path = this.workspace(agent);
|
|
2953
|
+
await waitMcp(this.barriers.get(path ?? "") ?? Promise.resolve(), signal);
|
|
2954
|
+
if (!record.live || this.workspace(agent) !== path) throw new Error("项目 MCP 作用域已失效");
|
|
2955
|
+
const def = this.projects.get(path ?? "")?.get(name);
|
|
2956
|
+
if (!def?.enabled || !path) throw new Error("当前工作区未启用该项目 MCP");
|
|
2957
|
+
if (entriesOf(this.ctx).some((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === name)) throw new Error("serverName 已被全局 MCP 使用");
|
|
2958
|
+
let instance = record.instances.get(name);
|
|
2959
|
+
if (instance?.lease.entry.phase === "error" && (instance.lease.entry.retryAt ?? Infinity) <= Date.now()) {
|
|
2960
|
+
record.instances.delete(name);
|
|
2961
|
+
this.revoke(instance);
|
|
2962
|
+
instance = void 0;
|
|
2963
|
+
}
|
|
2964
|
+
record.requested.add(name);
|
|
2965
|
+
instance ??= this.reserve(agent, record, path, name, def);
|
|
2966
|
+
await waitMcp(instance.ready, signal);
|
|
2967
|
+
if (!this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2968
|
+
const visible = agent.ctx.get("tools")?.view?.(agent)?.visible;
|
|
2969
|
+
return {
|
|
2970
|
+
serverName: name,
|
|
2971
|
+
tools: [...instance.lease.entry.bridge.tools.values()].filter((tool) => visible?.has(tool.name)).map((tool) => ({
|
|
2972
|
+
name: tool.name,
|
|
2973
|
+
description: tool.description,
|
|
2974
|
+
parameters: tool.parameters
|
|
2975
|
+
})),
|
|
2976
|
+
resources: instance.lease.entry.bridge.resources.has(name),
|
|
2977
|
+
nextRequest: "Use these tools in the next model request; PTC must start a new program to refresh its SDK."
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
/** @public @author ddj 2026年10月20号 @param agent Disposed owner. @returns Scoped teardown; shared transports remain owned by other leases. */
|
|
2408
2981
|
async detach(agent) {
|
|
2409
2982
|
this.retired.add(agent);
|
|
2410
2983
|
const record = this.agents.get(agent);
|
|
2411
2984
|
if (!record) return;
|
|
2412
2985
|
record.live = false;
|
|
2413
|
-
|
|
2986
|
+
record.remove?.();
|
|
2987
|
+
for (const remove of record.resources.values()) remove();
|
|
2988
|
+
for (const instance of record.instances.values()) this.revoke(instance);
|
|
2989
|
+
record.instances.clear();
|
|
2414
2990
|
this.agents.delete(agent);
|
|
2415
2991
|
this.changed();
|
|
2416
2992
|
}
|
|
2417
|
-
/**
|
|
2418
|
-
* Match the agent ledger to the current enabled definitions.
|
|
2419
|
-
* @private @author ddj 2026年09月28号
|
|
2420
|
-
* @param agent Scoped plugin owner.
|
|
2421
|
-
* @param record Agent ledger.
|
|
2422
|
-
* @param refresh Optional server to reconnect.
|
|
2423
|
-
* @returns Completion of removals and scoped mounts.
|
|
2424
|
-
*/
|
|
2425
|
-
async syncAgent(agent, record, refresh) {
|
|
2426
|
-
if (!record.live || this.stopped) return;
|
|
2427
|
-
const path = this.workspace(agent);
|
|
2428
|
-
const defs = this.projects.get(path ?? "") ?? /* @__PURE__ */ new Map();
|
|
2429
|
-
for (const [name, instance] of record.instances) {
|
|
2430
|
-
const def = defs.get(name);
|
|
2431
|
-
if (def?.enabled && instance.path === path && instance.key === JSON.stringify(def.config) && name !== refresh) continue;
|
|
2432
|
-
await this.stopOne(record, name);
|
|
2433
|
-
}
|
|
2434
|
-
for (const [name, def] of defs) {
|
|
2435
|
-
if (!record.live || this.stopped) break;
|
|
2436
|
-
if (!def.enabled || record.instances.has(name)) continue;
|
|
2437
|
-
await this.mount(agent, record, def.config, path);
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
/**
|
|
2441
|
-
* Cache successful module loads while allowing explicit refresh after transient failures.
|
|
2442
|
-
* @private @author ddj 2026年10月08号
|
|
2443
|
-
* @returns Official plugin module; load failures propagate to the instance diagnostic.
|
|
2444
|
-
*/
|
|
2993
|
+
/** @private @author ddj 2026年10月08号 @returns Cached official module; failures are retryable. */
|
|
2445
2994
|
async getModule() {
|
|
2446
2995
|
const loading = this.module ??= this.load();
|
|
2447
2996
|
try {
|
|
@@ -2451,100 +3000,113 @@ var McpRuntime = class {
|
|
|
2451
3000
|
throw error;
|
|
2452
3001
|
}
|
|
2453
3002
|
}
|
|
2454
|
-
/**
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
* @param agent Scoped context owner.
|
|
2458
|
-
* @param record Agent ledger.
|
|
2459
|
-
* @param config Validated source config.
|
|
2460
|
-
* @param path Default stdio working directory.
|
|
2461
|
-
* @returns Settled startup; errors are retained for the project status view.
|
|
2462
|
-
*/
|
|
2463
|
-
async mount(agent, record, config, path) {
|
|
2464
|
-
const instance = {
|
|
2465
|
-
key: JSON.stringify(config),
|
|
2466
|
-
path
|
|
2467
|
-
};
|
|
2468
|
-
record.instances.set(config.serverName, instance);
|
|
2469
|
-
try {
|
|
2470
|
-
const officialMcp = await this.getModule();
|
|
2471
|
-
if (!record.live || this.stopped) return;
|
|
2472
|
-
if (entriesOf(this.ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === config.serverName)) throw new Error("serverName 已被全局 MCP 使用:" + config.serverName);
|
|
2473
|
-
const options = config.transport === "stdio" ? {
|
|
2474
|
-
...config,
|
|
2475
|
-
cwd: config.cwd || path
|
|
2476
|
-
} : { ...config };
|
|
2477
|
-
instance.fiber = agent.ctx.plugin(officialMcp, options);
|
|
2478
|
-
await instance.fiber;
|
|
2479
|
-
} catch (error) {
|
|
2480
|
-
instance.error = String(error);
|
|
2481
|
-
await this.stopFiber(instance);
|
|
2482
|
-
instance.fiber = void 0;
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
/**
|
|
2486
|
-
* Build project status from exact agent scopes without assuming ACTIVE means connected.
|
|
2487
|
-
* @public @author ddj 2026年09月28号
|
|
2488
|
-
* @param path Workspace to summarize.
|
|
2489
|
-
* @returns Masked server views, including configured-only and disabled definitions.
|
|
2490
|
-
*/
|
|
2491
|
-
views(path) {
|
|
2492
|
-
return [...this.projects.get(path) ?? /* @__PURE__ */ new Map()].map(([name, def]) => this.serverView(path, name, def));
|
|
2493
|
-
}
|
|
2494
|
-
/**
|
|
2495
|
-
* Summarize one definition and its connection instances across matching agents.
|
|
2496
|
-
* @private @author ddj 2026年09月28号
|
|
2497
|
-
* @param path Owning workspace.
|
|
2498
|
-
* @param name Server name.
|
|
2499
|
-
* @param def Persisted definition.
|
|
2500
|
-
* @returns Honest status and union of tools visible to its agents.
|
|
2501
|
-
*/
|
|
2502
|
-
serverView(path, name, def) {
|
|
2503
|
-
const tools = /* @__PURE__ */ new Map();
|
|
2504
|
-
const instances = [];
|
|
3003
|
+
/** @public @author ddj 2026年10月20号 @returns Detached read-only active session statuses, without IO or activation. */
|
|
3004
|
+
sessionSnapshot() {
|
|
3005
|
+
const sessions = [];
|
|
2505
3006
|
for (const [agent, record] of this.agents) {
|
|
2506
|
-
if (
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
3007
|
+
if (!record.live || typeof agent?.session?.id !== "string") continue;
|
|
3008
|
+
const servers = this.sessionServers(agent, record);
|
|
3009
|
+
const active = servers.find((server) => [
|
|
3010
|
+
"stopping",
|
|
3011
|
+
"starting",
|
|
3012
|
+
"loading",
|
|
3013
|
+
"queued"
|
|
3014
|
+
].includes(server.phase));
|
|
3015
|
+
let phase = "ready";
|
|
3016
|
+
if (record.error || servers.some((server) => server.phase === "error")) phase = "error";
|
|
3017
|
+
else if (active) phase = active.phase;
|
|
3018
|
+
else if (!record.finishedAt) phase = "preparing";
|
|
3019
|
+
else if (!servers.length) phase = "none";
|
|
3020
|
+
else if (servers.some((server) => server.phase === "idle")) phase = "idle";
|
|
3021
|
+
const attempts = servers.filter((server) => server.phase !== "idle");
|
|
3022
|
+
const startedAt = attempts.length ? Math.min(...attempts.map((server) => server.startedAt)) : record.startedAt;
|
|
3023
|
+
sessions.push({
|
|
3024
|
+
configMs: Math.max(0, (record.finishedAt ?? Date.now()) - record.startedAt),
|
|
3025
|
+
sessionId: agent.session.id,
|
|
3026
|
+
phase,
|
|
3027
|
+
startedAt,
|
|
3028
|
+
phaseAt: active?.phaseAt ?? startedAt,
|
|
3029
|
+
finishedAt: active ? void 0 : Math.max(record.finishedAt ?? record.startedAt, ...servers.map((server) => server.finishedAt ?? 0)),
|
|
3030
|
+
error: record.error,
|
|
3031
|
+
servers
|
|
3032
|
+
});
|
|
2511
3033
|
}
|
|
2512
|
-
const error = instances.find((item) => item.error)?.error;
|
|
2513
|
-
const fibers = instances.filter((item) => item.fiber);
|
|
2514
|
-
const failed = error || fibers.some((item) => item.fiber.state === 3);
|
|
2515
|
-
const status = !def.enabled ? "disabled" : failed ? "error" : fibers.length === 0 ? "configured" : fibers.some((item) => item.fiber.state === 1 || item.fiber.state === 0) ? "connecting" : "unverified";
|
|
2516
3034
|
return {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
enabled: def.enabled,
|
|
2520
|
-
transport: def.config.transport,
|
|
2521
|
-
config: publicConfig(def.config),
|
|
2522
|
-
status,
|
|
2523
|
-
instanceCount: fibers.length,
|
|
2524
|
-
toolCount: tools.size,
|
|
2525
|
-
tools: [...tools.values()],
|
|
2526
|
-
error: error || (failed ? "MCP 插件未正常运行" : void 0)
|
|
3035
|
+
sessions,
|
|
3036
|
+
sampledAt: Date.now()
|
|
2527
3037
|
};
|
|
2528
3038
|
}
|
|
2529
|
-
/**
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
3039
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @returns Server phases with caller-local timing, not pooled connection age. */
|
|
3040
|
+
sessionServers(agent, record) {
|
|
3041
|
+
return this.catalogue(agent).map(({ serverName, phase }) => {
|
|
3042
|
+
const instance = record.instances.get(serverName);
|
|
3043
|
+
if (!instance) return {
|
|
3044
|
+
serverName,
|
|
3045
|
+
phase: "idle",
|
|
3046
|
+
startedAt: record.finishedAt ?? record.startedAt,
|
|
3047
|
+
phaseAt: record.finishedAt ?? record.startedAt
|
|
3048
|
+
};
|
|
3049
|
+
const entry = instance.lease.entry;
|
|
3050
|
+
return {
|
|
3051
|
+
serverName,
|
|
3052
|
+
phase,
|
|
3053
|
+
startedAt: instance.startedAt,
|
|
3054
|
+
phaseAt: Math.max(instance.startedAt, entry.phaseAt),
|
|
3055
|
+
finishedAt: entry.finishedAt && Math.max(instance.startedAt, entry.finishedAt),
|
|
3056
|
+
error: entry.error,
|
|
3057
|
+
reused: instance.reused,
|
|
3058
|
+
queuedMs: Math.max(0, (entry.startAt ?? Date.now()) - instance.startedAt),
|
|
3059
|
+
connectMs: entry.connectedAt === void 0 ? void 0 : Math.max(0, entry.connectedAt - Math.max(instance.startedAt, entry.startAt ?? instance.startedAt)),
|
|
3060
|
+
firstCallMs: instance.firstCallMs
|
|
3061
|
+
};
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
/** @public @author ddj 2026年09月28号 @param path Workspace. @returns Masked configuration and pool/lease counts; initialization is not health. */
|
|
3065
|
+
views(path) {
|
|
3066
|
+
return [...this.projects.get(path) ?? []].map(([name, def]) => {
|
|
3067
|
+
const entries = [...this.pool.entries.values()].filter((entry) => entry.spec.workspace === path && entry.spec.config.serverName === name);
|
|
3068
|
+
const tools = /* @__PURE__ */ new Map();
|
|
3069
|
+
for (const [agent] of this.agents) if (this.workspace(agent) === path) for (const tool of toolsOf(this.ctx, name, agent)) tools.set(tool.name, tool);
|
|
3070
|
+
const failed = entries.some((entry) => entry.phase === "error" || entry.error);
|
|
3071
|
+
const busy = entries.some((entry) => [
|
|
3072
|
+
"queued",
|
|
3073
|
+
"loading",
|
|
3074
|
+
"starting",
|
|
3075
|
+
"stopping"
|
|
3076
|
+
].includes(entry.phase));
|
|
3077
|
+
let status = "configured";
|
|
3078
|
+
if (!def.enabled) status = "disabled";
|
|
3079
|
+
else if (failed) status = "error";
|
|
3080
|
+
else if (busy) status = "connecting";
|
|
3081
|
+
else if (entries.some((entry) => entry.phase === "ready")) status = "unverified";
|
|
3082
|
+
return {
|
|
3083
|
+
id: projectEntryId(path, name),
|
|
3084
|
+
serverName: name,
|
|
3085
|
+
enabled: def.enabled,
|
|
3086
|
+
transport: def.config.transport,
|
|
3087
|
+
config: publicConfig(def.config),
|
|
3088
|
+
policy: parseMcpPolicy(def.policy),
|
|
3089
|
+
status,
|
|
3090
|
+
instanceCount: entries.filter((entry) => entry.fiber).length,
|
|
3091
|
+
sessionCount: entries.reduce((total, entry) => total + entry.refs, 0),
|
|
3092
|
+
tools: [...tools.values()],
|
|
3093
|
+
toolCount: tools.size,
|
|
3094
|
+
error: failed ? "MCP 初始化或清理异常,请查看 Host 日志" : void 0
|
|
3095
|
+
};
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
/** @public @author ddj 2026年10月20号 @returns Complete owned transport and capability cleanup. */
|
|
2534
3099
|
async dispose() {
|
|
2535
3100
|
this.stopped = true;
|
|
3101
|
+
this.offDiscover?.();
|
|
2536
3102
|
await Promise.all([...this.agents.keys()].map((agent) => this.detach(agent)));
|
|
2537
|
-
await this.
|
|
3103
|
+
await this.pool.dispose();
|
|
3104
|
+
await Promise.allSettled([...this.pending]);
|
|
2538
3105
|
this.projects.clear();
|
|
2539
3106
|
this.module = void 0;
|
|
2540
3107
|
}
|
|
2541
3108
|
};
|
|
2542
|
-
/**
|
|
2543
|
-
* Remove every old global project entry before any agent-scoped mount is attempted.
|
|
2544
|
-
* @public @author ddj 2026年09月28号
|
|
2545
|
-
* @param ctx Host loader context.
|
|
2546
|
-
* @returns Completion of loader removal and connection teardown.
|
|
2547
|
-
*/
|
|
3109
|
+
/** @public @author ddj 2026年09月28号 @param ctx Host loader. @returns Removal of legacy global project entries before any scoped entry is installed. */
|
|
2548
3110
|
async function clearLegacy(ctx) {
|
|
2549
3111
|
const loader = ctx.get("loader");
|
|
2550
3112
|
for (const entry of entriesOf(ctx)) {
|
|
@@ -2552,35 +3114,41 @@ async function clearLegacy(ctx) {
|
|
|
2552
3114
|
await loader.remove(entry.id);
|
|
2553
3115
|
}
|
|
2554
3116
|
}
|
|
2555
|
-
/**
|
|
2556
|
-
* Install awaited creation hooks, migrate global entries, and attach existing agents.
|
|
2557
|
-
* @public @author ddj 2026年09月28号
|
|
2558
|
-
* @param ctx Host lifecycle context; installIsolation must already be installed.
|
|
2559
|
-
* @returns Readiness for index.apply to await before advertising startup completion.
|
|
2560
|
-
*/
|
|
3117
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host lifecycle. @returns Config/catalogue readiness, independent from all project MCP handshakes. */
|
|
2561
3118
|
async function installMcpRuntime(ctx) {
|
|
2562
3119
|
const runtime = runtimeOf(ctx);
|
|
2563
3120
|
let ready;
|
|
3121
|
+
/** @author ddj 2026年10月20号 Loads validated snapshots before exposing lazy capabilities. */
|
|
2564
3122
|
const initialize = async () => {
|
|
2565
3123
|
await clearLegacy(ctx);
|
|
2566
|
-
await listProjects(ctx);
|
|
2567
|
-
for (const agent of ctx.get("agents")?.list?.() ?? [])
|
|
3124
|
+
const { projects } = await listProjects(ctx, false);
|
|
3125
|
+
for (const agent of ctx.get("agents")?.list?.() ?? []) {
|
|
3126
|
+
runtime.prepare(agent);
|
|
3127
|
+
const project = projects.find((item) => item.workspacePath === runtime.workspace(agent));
|
|
3128
|
+
if (project?.fileError) runtime.configError(agent, project.fileError);
|
|
3129
|
+
}
|
|
2568
3130
|
};
|
|
3131
|
+
/** @author ddj 2026年10月20号 @param event Actual created agent. Reads configuration but never waits for connection startup. */
|
|
2569
3132
|
const created = async ({ agent }) => {
|
|
3133
|
+
runtime.prepare(agent);
|
|
2570
3134
|
await ready;
|
|
2571
3135
|
const path = runtime.workspace(agent);
|
|
2572
3136
|
if (path) try {
|
|
2573
|
-
await reconcileProject(ctx, path);
|
|
3137
|
+
await reconcileProject(ctx, path, void 0, false);
|
|
3138
|
+
runtime.configError(agent);
|
|
2574
3139
|
} catch (error) {
|
|
3140
|
+
runtime.configError(agent, error);
|
|
2575
3141
|
ctx.logger?.warn?.("MCP configuration retained: " + String(error));
|
|
2576
3142
|
}
|
|
2577
3143
|
await runtime.attach(agent);
|
|
2578
3144
|
};
|
|
3145
|
+
/** @author ddj 2026年10月20号 @param event Actual disposed agent. Revokes public access synchronously. */
|
|
2579
3146
|
const disposed = ({ agent }) => {
|
|
2580
3147
|
runtime.detach(agent).catch((error) => ctx.logger?.warn?.("MCP disposal failed: " + String(error)));
|
|
2581
3148
|
};
|
|
2582
3149
|
const offCreate = ctx.on("agent/created", created);
|
|
2583
3150
|
const offDispose = ctx.on("agent/disposed", disposed);
|
|
3151
|
+
/** @author ddj 2026年10月20号 @returns Full lifecycle teardown. */
|
|
2584
3152
|
const cleanup = async () => {
|
|
2585
3153
|
offCreate?.();
|
|
2586
3154
|
offDispose?.();
|
|
@@ -2590,6 +3158,7 @@ async function installMcpRuntime(ctx) {
|
|
|
2590
3158
|
ctx.effect(() => cleanup, "vscode-mode:mcp-runtime");
|
|
2591
3159
|
ready = initialize();
|
|
2592
3160
|
await ready;
|
|
3161
|
+
await Promise.all((ctx.get("agents")?.list?.() ?? []).map((agent) => runtime.attach(agent)));
|
|
2593
3162
|
}
|
|
2594
3163
|
//#endregion
|
|
2595
3164
|
//#region src/mcpProject.ts
|
|
@@ -2773,9 +3342,10 @@ async function checkMcpName(ctx, serverName, workspacePath) {
|
|
|
2773
3342
|
* @param ctx Host services.
|
|
2774
3343
|
* @param workspacePath Registered workspace.
|
|
2775
3344
|
* @param refresh Optional unchanged server to reconnect.
|
|
2776
|
-
* @
|
|
3345
|
+
* @param wait Await connection synchronization for management, or only publication/scheduling for creation.
|
|
3346
|
+
* @returns Configuration publication, optionally including scoped fiber synchronization.
|
|
2777
3347
|
*/
|
|
2778
|
-
async function reconcileProject(ctx, workspacePath, refresh) {
|
|
3348
|
+
async function reconcileProject(ctx, workspacePath, refresh, wait = true) {
|
|
2779
3349
|
const data = await readProjectJson(ctx, workspacePath);
|
|
2780
3350
|
const defs = /* @__PURE__ */ new Map();
|
|
2781
3351
|
for (const [name, def] of Object.entries(serversOf(data))) {
|
|
@@ -2784,11 +3354,12 @@ async function reconcileProject(ctx, workspacePath, refresh) {
|
|
|
2784
3354
|
await checkMcpName(ctx, name, workspacePath);
|
|
2785
3355
|
defs.set(name, {
|
|
2786
3356
|
config,
|
|
2787
|
-
enabled: def.disabled !== true
|
|
3357
|
+
enabled: def.disabled !== true,
|
|
3358
|
+
policy: parseMcpPolicy(def.vscodeMode)
|
|
2788
3359
|
});
|
|
2789
3360
|
}
|
|
2790
3361
|
await clearLegacy(ctx);
|
|
2791
|
-
await runtimeOf(ctx).replace(workspacePath, defs, refresh);
|
|
3362
|
+
await runtimeOf(ctx).replace(workspacePath, defs, refresh, wait);
|
|
2792
3363
|
}
|
|
2793
3364
|
/**
|
|
2794
3365
|
* List one project, retaining the last good runtime view on file errors.
|
|
@@ -2798,7 +3369,7 @@ async function reconcileProject(ctx, workspacePath, refresh) {
|
|
|
2798
3369
|
* @param title Workspace title.
|
|
2799
3370
|
* @returns Configuration and honest runtime status with any read error attached.
|
|
2800
3371
|
*/
|
|
2801
|
-
async function projectOf(ctx, workspacePath, title) {
|
|
3372
|
+
async function projectOf(ctx, workspacePath, title, wait = true) {
|
|
2802
3373
|
let fileError;
|
|
2803
3374
|
let missingDir;
|
|
2804
3375
|
try {
|
|
@@ -2806,7 +3377,7 @@ async function projectOf(ctx, workspacePath, title) {
|
|
|
2806
3377
|
if (!fs) throw new Error("缺少 fs 服务");
|
|
2807
3378
|
const info = await fs.stat(await fs.resolve(workspacePath));
|
|
2808
3379
|
missingDir = !info || info.type !== "directory";
|
|
2809
|
-
if (!missingDir) await reconcileProject(ctx, workspacePath);
|
|
3380
|
+
if (!missingDir) await reconcileProject(ctx, workspacePath, void 0, wait);
|
|
2810
3381
|
} catch (error) {
|
|
2811
3382
|
missingDir = isMissingFile(error) || void 0;
|
|
2812
3383
|
fileError = String(error);
|
|
@@ -2833,9 +3404,9 @@ async function projectOf(ctx, workspacePath, title) {
|
|
|
2833
3404
|
* @param ctx Host services.
|
|
2834
3405
|
* @returns Project list; individual malformed files are reported on their project.
|
|
2835
3406
|
*/
|
|
2836
|
-
async function listProjects(ctx) {
|
|
3407
|
+
async function listProjects(ctx, wait = true) {
|
|
2837
3408
|
const projects = [];
|
|
2838
|
-
for (const ws of ctx.get("workspaceRegistry")?.list?.() ?? []) projects.push(await projectOf(ctx, ws.path, ws.title ?? ""));
|
|
3409
|
+
for (const ws of ctx.get("workspaceRegistry")?.list?.() ?? []) projects.push(await projectOf(ctx, ws.path, ws.title ?? "", wait));
|
|
2839
3410
|
return { projects };
|
|
2840
3411
|
}
|
|
2841
3412
|
/**
|
|
@@ -2881,11 +3452,12 @@ function mergeDefinition(previous, config) {
|
|
|
2881
3452
|
* @param config Submitted configuration.
|
|
2882
3453
|
* @returns Updated project view; invalid sources or conflicts reject before writing.
|
|
2883
3454
|
*/
|
|
2884
|
-
async function projectSave(ctx, workspacePath, serverName, config) {
|
|
2885
|
-
|
|
3455
|
+
async function projectSave(ctx, workspacePath, serverName, config, policy) {
|
|
3456
|
+
if (policy !== void 0) parseMcpPolicy(policy);
|
|
3457
|
+
return mcpEdit(ctx, () => saveProject(ctx, workspacePath, serverName, config, policy));
|
|
2886
3458
|
}
|
|
2887
3459
|
/** @private @author ddj 2026年10月08号 @param ctx Host services. @param workspacePath Owner. @param serverName Namespace. @param config Editable fields. @returns Persisted project status. */
|
|
2888
|
-
async function saveProject(ctx, workspacePath, serverName, config) {
|
|
3460
|
+
async function saveProject(ctx, workspacePath, serverName, config, policy) {
|
|
2889
3461
|
const workspace = requireWorkspace$2(ctx, workspacePath);
|
|
2890
3462
|
validateConfig({
|
|
2891
3463
|
...config,
|
|
@@ -2898,6 +3470,7 @@ async function saveProject(ctx, workspacePath, serverName, config) {
|
|
|
2898
3470
|
...config,
|
|
2899
3471
|
serverName
|
|
2900
3472
|
});
|
|
3473
|
+
if (policy !== void 0) servers[serverName].vscodeMode = { ...parseMcpPolicy(policy) };
|
|
2901
3474
|
await writeProjectJson(ctx, workspacePath, {
|
|
2902
3475
|
...data,
|
|
2903
3476
|
mcpServers: servers
|
|
@@ -3180,6 +3753,7 @@ async function saveMcp(ctx, config) {
|
|
|
3180
3753
|
}
|
|
3181
3754
|
/** @private @author ddj 2026年10月08号 @param ctx Host services. @param config Configuration. @returns Saved status; validation failures do not write. */
|
|
3182
3755
|
async function saveGlobal(ctx, config) {
|
|
3756
|
+
if (config && "vscodeMode" in config) throw new Error("vscodeMode 启动和共享策略仅适用于项目 MCP");
|
|
3183
3757
|
validateConfig(config);
|
|
3184
3758
|
await checkMcpName(ctx, config.serverName);
|
|
3185
3759
|
const loader = ctx.get("loader");
|
|
@@ -5977,13 +6551,6 @@ const PERSIST_DEBOUNCE_MS = 1e4;
|
|
|
5977
6551
|
const HEAL_DEBOUNCE_MS = 3e3;
|
|
5978
6552
|
/** cwd → 索引状态(随会话销毁清理)。 */
|
|
5979
6553
|
const states = /* @__PURE__ */ new Map();
|
|
5980
|
-
/** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出/裁剪顺序可预期)。 */
|
|
5981
|
-
let clock = 0;
|
|
5982
|
-
/** 取下一时间戳(不小于真实时间且严格递增)。 */
|
|
5983
|
-
function nextTs() {
|
|
5984
|
-
clock = Math.max(Date.now(), clock + 1);
|
|
5985
|
-
return clock;
|
|
5986
|
-
}
|
|
5987
6554
|
/** 条目原始值 → 合法条目(非法返回 null)。 */
|
|
5988
6555
|
function entryOf(raw) {
|
|
5989
6556
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -6054,10 +6621,10 @@ function indexParse(text) {
|
|
|
6054
6621
|
type: e.type
|
|
6055
6622
|
});
|
|
6056
6623
|
}
|
|
6057
|
-
|
|
6624
|
+
const full = dir.full === true && entries.length === dir.entries.length;
|
|
6058
6625
|
dirs[rel] = {
|
|
6059
6626
|
ts: typeof dir.ts === "number" ? dir.ts : 0,
|
|
6060
|
-
full
|
|
6627
|
+
full,
|
|
6061
6628
|
entries
|
|
6062
6629
|
};
|
|
6063
6630
|
}
|
|
@@ -6076,11 +6643,12 @@ function trimPersist(data, maxDirs = 400, maxEntries = PERSIST_DIR_ENTRIES) {
|
|
|
6076
6643
|
const sorted = Object.entries(data.dirs).sort((a, b) => b[1].ts - a[1].ts);
|
|
6077
6644
|
for (const [rel, dir] of sorted) {
|
|
6078
6645
|
if (Object.keys(dirs).length >= maxDirs) break;
|
|
6079
|
-
const
|
|
6646
|
+
const truncated = dir.entries.length > maxEntries;
|
|
6647
|
+
const full = dir.full && !truncated;
|
|
6080
6648
|
dirs[rel] = {
|
|
6081
6649
|
ts: dir.ts,
|
|
6082
6650
|
full,
|
|
6083
|
-
entries:
|
|
6651
|
+
entries: truncated ? dir.entries.slice(0, maxEntries) : dir.entries
|
|
6084
6652
|
};
|
|
6085
6653
|
}
|
|
6086
6654
|
return {
|
|
@@ -6089,7 +6657,7 @@ function trimPersist(data, maxDirs = 400, maxEntries = PERSIST_DIR_ENTRIES) {
|
|
|
6089
6657
|
dirs
|
|
6090
6658
|
};
|
|
6091
6659
|
}
|
|
6092
|
-
/** 取(或建)某 cwd
|
|
6660
|
+
/** 取(或建)某 cwd 的索引状态并惰性加载快照;加载期间真实列取优先,不被旧快照覆盖。 */
|
|
6093
6661
|
function ensureState(ctx, cwd) {
|
|
6094
6662
|
const existing = states.get(cwd);
|
|
6095
6663
|
if (existing) return existing;
|
|
@@ -6102,6 +6670,7 @@ function ensureState(ctx, cwd) {
|
|
|
6102
6670
|
dirs: {}
|
|
6103
6671
|
},
|
|
6104
6672
|
stale: /* @__PURE__ */ new Set(),
|
|
6673
|
+
lru: /* @__PURE__ */ new Map(),
|
|
6105
6674
|
dirty: false,
|
|
6106
6675
|
persistTimer: null,
|
|
6107
6676
|
healTimer: null,
|
|
@@ -6129,44 +6698,68 @@ async function loadIndex(state) {
|
|
|
6129
6698
|
try {
|
|
6130
6699
|
const parsed = indexParse(await readFile(treeCacheFile(state.cwd), "utf8"));
|
|
6131
6700
|
if (!parsed) return;
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
if (
|
|
6701
|
+
const snapshots = Object.entries(parsed.dirs).sort((a, b) => a[1].ts - b[1].ts);
|
|
6702
|
+
for (const [rel, dir] of snapshots) {
|
|
6703
|
+
if (state.data.dirs[rel]) continue;
|
|
6704
|
+
state.data.dirs[rel] = dir;
|
|
6705
|
+
touchDir(state, rel);
|
|
6135
6706
|
}
|
|
6136
6707
|
if (parsed.root && !state.data.root) state.data.root = parsed.root;
|
|
6137
6708
|
} catch (error) {} finally {
|
|
6138
6709
|
state.loading = false;
|
|
6139
6710
|
}
|
|
6140
6711
|
}
|
|
6141
|
-
/**
|
|
6712
|
+
/**
|
|
6713
|
+
* @private
|
|
6714
|
+
* @author ddj 2026年10月22号
|
|
6715
|
+
* @description 更新运行时访问顺序,超限时同步清理目录及失效标记。
|
|
6716
|
+
* @param state 工作区索引状态
|
|
6717
|
+
* @param rel 本次访问的相对目录
|
|
6718
|
+
*/
|
|
6719
|
+
function touchDir(state, rel) {
|
|
6720
|
+
state.lru.delete(rel);
|
|
6721
|
+
state.lru.set(rel, true);
|
|
6722
|
+
if (state.lru.size <= 2e3) return;
|
|
6723
|
+
const oldest = state.lru.keys().next().value;
|
|
6724
|
+
if (oldest === void 0) return;
|
|
6725
|
+
state.lru.delete(oldest);
|
|
6726
|
+
state.stale.delete(oldest);
|
|
6727
|
+
delete state.data.dirs[oldest];
|
|
6728
|
+
}
|
|
6729
|
+
/**
|
|
6730
|
+
* @public
|
|
6731
|
+
* @author ddj 2026年10月22号
|
|
6732
|
+
* @description 完整、新鲜且未失效才命中;访问仅更新 LRU,不续期硬 TTL。
|
|
6733
|
+
* @param cwd 工作区根
|
|
6734
|
+
* @param rel 相对目录
|
|
6735
|
+
* @returns 完整条目,或未命中时的 null
|
|
6736
|
+
*/
|
|
6142
6737
|
function hitIndex(cwd, rel) {
|
|
6143
6738
|
const state = states.get(cwd);
|
|
6144
6739
|
if (!state) return null;
|
|
6145
6740
|
const dir = state.data.dirs[rel];
|
|
6146
|
-
if (!dir || state.stale.has(rel)) return null;
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
dir.ts = now;
|
|
6741
|
+
if (!dir || !dir.full || state.stale.has(rel)) return null;
|
|
6742
|
+
if (Date.now() - dir.ts >= 6e4) return null;
|
|
6743
|
+
touchDir(state, rel);
|
|
6150
6744
|
return dir.entries;
|
|
6151
6745
|
}
|
|
6152
|
-
/**
|
|
6746
|
+
/**
|
|
6747
|
+
* @public
|
|
6748
|
+
* @author ddj 2026年10月22号
|
|
6749
|
+
* @description 保存完整实列结果与确认时间,更新 LRU、撤销 stale 并防抖落盘。
|
|
6750
|
+
* @param ctx 工作区上下文
|
|
6751
|
+
* @param cwd 工作区根
|
|
6752
|
+
* @param rel 相对目录
|
|
6753
|
+
* @param entries 完整实列条目
|
|
6754
|
+
*/
|
|
6153
6755
|
function putIndex(ctx, cwd, rel, entries) {
|
|
6154
6756
|
const state = ensureState(ctx, cwd);
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
let oldestTs = Infinity;
|
|
6159
|
-
for (const [k, v] of Object.entries(dirs)) if (v.ts < oldestTs) {
|
|
6160
|
-
oldestTs = v.ts;
|
|
6161
|
-
oldest = k;
|
|
6162
|
-
}
|
|
6163
|
-
delete dirs[oldest];
|
|
6164
|
-
}
|
|
6165
|
-
dirs[rel] = {
|
|
6166
|
-
ts: nextTs(),
|
|
6167
|
-
full: entries.length <= PERSIST_DIR_ENTRIES,
|
|
6757
|
+
state.data.dirs[rel] = {
|
|
6758
|
+
ts: Date.now(),
|
|
6759
|
+
full: true,
|
|
6168
6760
|
entries
|
|
6169
6761
|
};
|
|
6762
|
+
touchDir(state, rel);
|
|
6170
6763
|
state.stale.delete(rel);
|
|
6171
6764
|
state.dirty = true;
|
|
6172
6765
|
schedulePersist(state);
|
|
@@ -6231,6 +6824,7 @@ function disposeIndex(cwd) {
|
|
|
6231
6824
|
state.healTimer = null;
|
|
6232
6825
|
}
|
|
6233
6826
|
if (state.dirty) persistNow(state);
|
|
6827
|
+
state.lru.clear();
|
|
6234
6828
|
states.delete(cwd);
|
|
6235
6829
|
}
|
|
6236
6830
|
/** 错误 → 用户文案(DirListError 用自身 message,其余带上下文)。 */
|
|
@@ -6283,6 +6877,7 @@ async function listDirCached(ctx, cwd, rel, force) {
|
|
|
6283
6877
|
if (error instanceof DirListError && error.code === "not-found") {
|
|
6284
6878
|
if (state.data.dirs[rel]) {
|
|
6285
6879
|
delete state.data.dirs[rel];
|
|
6880
|
+
state.lru.delete(rel);
|
|
6286
6881
|
state.dirty = true;
|
|
6287
6882
|
schedulePersist(state);
|
|
6288
6883
|
}
|
|
@@ -11385,7 +11980,140 @@ function settingsDocPath(configuredPath, requestedPath, platform = process.platf
|
|
|
11385
11980
|
}
|
|
11386
11981
|
}
|
|
11387
11982
|
//#endregion
|
|
11983
|
+
//#region src/shared/textPages.ts
|
|
11984
|
+
/** Bounded, read-only text paging protocol. @author ddj 2026年10月22号 */
|
|
11985
|
+
const TEXT_PAGE_BYTES = 262144;
|
|
11986
|
+
//#endregion
|
|
11987
|
+
//#region src/textPages.ts
|
|
11988
|
+
/**
|
|
11989
|
+
* Create a Host-owned pager with process-local authenticated cursors.
|
|
11990
|
+
* @public @author ddj 2026年10月22号
|
|
11991
|
+
* @returns A bounded reader requiring an independently authorized range capability.
|
|
11992
|
+
*/
|
|
11993
|
+
function createTextPages() {
|
|
11994
|
+
const secret = randomBytes(32);
|
|
11995
|
+
/** @private @author ddj 2026年10月22号 @param payload Authenticated state. @returns Signed cursor. */
|
|
11996
|
+
function signCursor(payload) {
|
|
11997
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
11998
|
+
return body + "." + createHmac("sha256", secret).update(body).digest("base64url");
|
|
11999
|
+
}
|
|
12000
|
+
/** @private @author ddj 2026年10月22号 @param token Untrusted cursor. @returns Verified state or null. */
|
|
12001
|
+
function parseCursor(token) {
|
|
12002
|
+
if (typeof token !== "string" || token.length > 8192) return null;
|
|
12003
|
+
const parts = token.split(".");
|
|
12004
|
+
if (parts.length !== 2 || !/^[A-Za-z0-9_-]+$/.test(parts[0])) return null;
|
|
12005
|
+
const signature = Buffer.from(parts[1], "base64url");
|
|
12006
|
+
const expected = createHmac("sha256", secret).update(parts[0]).digest();
|
|
12007
|
+
if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) return null;
|
|
12008
|
+
try {
|
|
12009
|
+
const payload = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
|
|
12010
|
+
if (typeof payload.identity !== "string" || typeof payload.version !== "string" || typeof payload.sessionId !== "string" || typeof payload.path !== "string" || typeof payload.pendingCR !== "boolean" || !Number.isSafeInteger(payload.offset) || payload.offset <= 0) return null;
|
|
12011
|
+
return payload;
|
|
12012
|
+
} catch {
|
|
12013
|
+
return null;
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
/**
|
|
12017
|
+
* Read at most 256 KiB, validating freshness before and after IO.
|
|
12018
|
+
* @public @author ddj 2026年10月22号
|
|
12019
|
+
* @param source Authorized range capability; absent means unsupported.
|
|
12020
|
+
* @param args Request identity and optional signed cursor.
|
|
12021
|
+
* @returns Strict UTF-8 text or an explicit fail-closed result; never partial success on mutation.
|
|
12022
|
+
*/
|
|
12023
|
+
async function readTextPage(source, args) {
|
|
12024
|
+
if (!source) return failure("unsupported", "Host does not expose authorized range reads");
|
|
12025
|
+
const cursor = args.cursor === void 0 ? void 0 : parseCursor(args.cursor);
|
|
12026
|
+
if (cursor === null || cursor && (cursor.identity !== source.identity || cursor.sessionId !== (args.sessionId ?? "") || cursor.path !== args.path)) return failure("invalid-cursor", "Cursor does not belong to this file and session");
|
|
12027
|
+
try {
|
|
12028
|
+
const before = await source.stat();
|
|
12029
|
+
if (!before || before.type !== "file" || typeof before.version !== "string" || !Number.isSafeInteger(before.size) || before.size < 0) return failure("unsupported", "Regular file size and opaque version are required");
|
|
12030
|
+
if (cursor && (cursor.version !== before.version || cursor.offset >= before.size)) return failure("stale", "File changed; restart text preview");
|
|
12031
|
+
const offset = cursor?.offset ?? 0;
|
|
12032
|
+
const length = Math.min(TEXT_PAGE_BYTES, before.size - offset);
|
|
12033
|
+
const bytes = await source.readRange(offset, length);
|
|
12034
|
+
const after = await source.stat();
|
|
12035
|
+
if (!after || after.version !== before.version || after.size !== before.size || after.type !== "file") return failure("stale", "File changed during text read");
|
|
12036
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== length) return failure("read-failed", "Range reader returned an incomplete or oversized page");
|
|
12037
|
+
return decodePage(bytes, before, offset, cursor?.pendingCR ?? false, args, source.identity, signCursor);
|
|
12038
|
+
} catch (error) {
|
|
12039
|
+
return failure("read-failed", String(error));
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
return readTextPage;
|
|
12043
|
+
}
|
|
12044
|
+
/** @private @author ddj 2026年10月22号 @param reason Failure discriminator. @param error User-visible error. @returns Failure result. */
|
|
12045
|
+
function failure(reason, error) {
|
|
12046
|
+
return {
|
|
12047
|
+
ok: false,
|
|
12048
|
+
reason,
|
|
12049
|
+
error
|
|
12050
|
+
};
|
|
12051
|
+
}
|
|
12052
|
+
/**
|
|
12053
|
+
* Keep an incomplete final UTF-8 sequence for a bounded re-read on the next page.
|
|
12054
|
+
* @private @author ddj 2026年10月22号
|
|
12055
|
+
* @param bytes Raw page. @returns Complete prefix length; invalid sequences remain for fatal decoding.
|
|
12056
|
+
*/
|
|
12057
|
+
function utf8Prefix(bytes) {
|
|
12058
|
+
let start = bytes.length - 1;
|
|
12059
|
+
while (start >= 0 && (bytes[start] & 192) === 128) start--;
|
|
12060
|
+
if (start < 0) return bytes.length;
|
|
12061
|
+
const lead = bytes[start];
|
|
12062
|
+
let width = 1;
|
|
12063
|
+
if (lead >= 194 && lead <= 223) width = 2;
|
|
12064
|
+
else if (lead >= 224 && lead <= 239) width = 3;
|
|
12065
|
+
else if (lead >= 240 && lead <= 244) width = 4;
|
|
12066
|
+
return bytes.length - start < width ? start : bytes.length;
|
|
12067
|
+
}
|
|
12068
|
+
/**
|
|
12069
|
+
* Decode one page preserving original line endings without buffering a long line.
|
|
12070
|
+
* @private @author ddj 2026年10月22号
|
|
12071
|
+
* @param bytes Raw page. @param info File snapshot. @param offset Current offset.
|
|
12072
|
+
* @param pendingCR Previous trailing CR. @param args Request identity. @param identity Authorized target identity.
|
|
12073
|
+
* @param signCursor Host cursor signer. @returns Page or strict decoding failure.
|
|
12074
|
+
*/
|
|
12075
|
+
function decodePage(bytes, info, offset, pendingCR, args, identity, signCursor) {
|
|
12076
|
+
const consumed = offset + bytes.length === info.size ? bytes.length : utf8Prefix(bytes);
|
|
12077
|
+
let text;
|
|
12078
|
+
try {
|
|
12079
|
+
text = new TextDecoder("utf-8", {
|
|
12080
|
+
fatal: true,
|
|
12081
|
+
ignoreBOM: true
|
|
12082
|
+
}).decode(bytes.subarray(0, consumed));
|
|
12083
|
+
} catch {
|
|
12084
|
+
return failure("not-text", "File is not valid UTF-8");
|
|
12085
|
+
}
|
|
12086
|
+
if (text.includes("\0")) return failure("not-text", "Binary text preview is unsupported");
|
|
12087
|
+
if (offset === 0 && text.startsWith("")) text = text.slice(1);
|
|
12088
|
+
if (pendingCR) text = "\r" + text;
|
|
12089
|
+
const nextOffset = offset + consumed;
|
|
12090
|
+
const eof = nextOffset === info.size;
|
|
12091
|
+
const trailingCR = !eof && text.endsWith("\r");
|
|
12092
|
+
if (trailingCR) text = text.slice(0, -1);
|
|
12093
|
+
const nextCursor = eof ? null : signCursor({
|
|
12094
|
+
identity,
|
|
12095
|
+
sessionId: args.sessionId ?? "",
|
|
12096
|
+
path: args.path,
|
|
12097
|
+
version: info.version,
|
|
12098
|
+
offset: nextOffset,
|
|
12099
|
+
pendingCR: trailingCR
|
|
12100
|
+
});
|
|
12101
|
+
return {
|
|
12102
|
+
ok: true,
|
|
12103
|
+
text,
|
|
12104
|
+
version: info.version,
|
|
12105
|
+
offset: nextOffset,
|
|
12106
|
+
size: info.size,
|
|
12107
|
+
eof,
|
|
12108
|
+
nextCursor,
|
|
12109
|
+
encoding: "utf-8",
|
|
12110
|
+
partialLine: !eof && !/[\r\n]$/.test(text)
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
//#endregion
|
|
11388
12114
|
//#region src/rpc.ts
|
|
12115
|
+
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
12116
|
+
const readTextPage = createTextPages();
|
|
11389
12117
|
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
11390
12118
|
const staleCheckedAt = /* @__PURE__ */ new Map();
|
|
11391
12119
|
/** stale 自动清理最小间隔。 */
|
|
@@ -11475,7 +12203,10 @@ async function readTargetOf(ctx, sc, path) {
|
|
|
11475
12203
|
err: "文件不存在",
|
|
11476
12204
|
resolvedPath: fs.processPath(target)
|
|
11477
12205
|
};
|
|
11478
|
-
if ((info.size ?? 0) > 33554432) return {
|
|
12206
|
+
if ((info.size ?? 0) > 33554432) return {
|
|
12207
|
+
err: "文件过大(>32MB),不支持整文件预览",
|
|
12208
|
+
code: "file-too-large"
|
|
12209
|
+
};
|
|
11479
12210
|
return {
|
|
11480
12211
|
target,
|
|
11481
12212
|
fs,
|
|
@@ -11492,13 +12223,11 @@ async function readTargetOf(ctx, sc, path) {
|
|
|
11492
12223
|
* @returns { ok:false } 形态载荷
|
|
11493
12224
|
*/
|
|
11494
12225
|
function targetErrOf(prep) {
|
|
11495
|
-
return
|
|
12226
|
+
return {
|
|
11496
12227
|
ok: false,
|
|
11497
12228
|
error: prep.err,
|
|
11498
|
-
resolvedPath: prep.resolvedPath
|
|
11499
|
-
|
|
11500
|
-
ok: false,
|
|
11501
|
-
error: prep.err
|
|
12229
|
+
...prep.resolvedPath !== void 0 ? { resolvedPath: prep.resolvedPath } : {},
|
|
12230
|
+
...prep.code ? { code: prep.code } : {}
|
|
11502
12231
|
};
|
|
11503
12232
|
}
|
|
11504
12233
|
/**
|
|
@@ -12027,6 +12756,44 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12027
12756
|
results: await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
|
|
12028
12757
|
};
|
|
12029
12758
|
},
|
|
12759
|
+
/** @author ddj 2026年10月22号 @param args Read-only request. @returns Bounded official range read, or unsupported on older providers; never bypasses Host IO. */
|
|
12760
|
+
"edrv.textPage": async (args) => {
|
|
12761
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
12762
|
+
if ("err" in sc) return {
|
|
12763
|
+
ok: false,
|
|
12764
|
+
error: sc.err,
|
|
12765
|
+
reason: "read-failed"
|
|
12766
|
+
};
|
|
12767
|
+
try {
|
|
12768
|
+
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
12769
|
+
const fs = ctx.get("fs");
|
|
12770
|
+
if (typeof fs?.readByteRange !== "function" || typeof fs.stat !== "function") return readTextPage(void 0, args);
|
|
12771
|
+
const targetKey = typeof target === "string" ? target : target.targetKey;
|
|
12772
|
+
if (typeof targetKey !== "string") return readTextPage(void 0, args);
|
|
12773
|
+
/** @author ddj 2026年10月22号 @returns Provider metadata used for before/after freshness checks. */
|
|
12774
|
+
function statPage() {
|
|
12775
|
+
return fs.stat(target);
|
|
12776
|
+
}
|
|
12777
|
+
/** @author ddj 2026年10月22号 @param offset Verified cursor position. @param length Bounded byte count. @returns Bytes through the official provider only. */
|
|
12778
|
+
function readRange(offset, length) {
|
|
12779
|
+
return fs.readByteRange(target, {
|
|
12780
|
+
offset,
|
|
12781
|
+
length
|
|
12782
|
+
});
|
|
12783
|
+
}
|
|
12784
|
+
return readTextPage({
|
|
12785
|
+
identity: JSON.stringify([sc.cwd, targetKey]),
|
|
12786
|
+
stat: statPage,
|
|
12787
|
+
readRange
|
|
12788
|
+
}, args);
|
|
12789
|
+
} catch (error) {
|
|
12790
|
+
return {
|
|
12791
|
+
ok: false,
|
|
12792
|
+
error: "读取失败:" + String(error),
|
|
12793
|
+
reason: "read-failed"
|
|
12794
|
+
};
|
|
12795
|
+
}
|
|
12796
|
+
},
|
|
12030
12797
|
"edrv.read": async (args) => {
|
|
12031
12798
|
const settingsTarget = settingsTargetOf(ctx, args.path);
|
|
12032
12799
|
if (settingsTarget && args.encoding !== "base64") try {
|
|
@@ -12091,7 +12858,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12091
12858
|
}
|
|
12092
12859
|
if ((prep.info.size ?? 0) > 8388608) return {
|
|
12093
12860
|
ok: false,
|
|
12094
|
-
error: "文件过大(>8MB),不支持整文件预览"
|
|
12861
|
+
error: "文件过大(>8MB),不支持整文件预览",
|
|
12862
|
+
code: "file-too-large"
|
|
12095
12863
|
};
|
|
12096
12864
|
const content = await prep.fs.readText(prep.target);
|
|
12097
12865
|
return {
|
|
@@ -12850,6 +13618,10 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12850
13618
|
ok: true,
|
|
12851
13619
|
...mcpSnapshot(ctx)
|
|
12852
13620
|
}),
|
|
13621
|
+
"mcp.sessionSnapshot": async () => ({
|
|
13622
|
+
ok: true,
|
|
13623
|
+
...runtimeOf(ctx).sessionSnapshot()
|
|
13624
|
+
}),
|
|
12853
13625
|
"mcp.save": async (args) => {
|
|
12854
13626
|
try {
|
|
12855
13627
|
return {
|
|
@@ -12913,11 +13685,31 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12913
13685
|
};
|
|
12914
13686
|
}
|
|
12915
13687
|
},
|
|
13688
|
+
"mcp.projectConnect": async (args) => {
|
|
13689
|
+
const runtime = runtimeOf(ctx);
|
|
13690
|
+
const agent = [...runtime.agents.keys()].find((owner) => owner.session?.id === args.sessionId);
|
|
13691
|
+
if (!agent || runtime.workspace(agent) !== args.workspacePath) return {
|
|
13692
|
+
ok: false,
|
|
13693
|
+
error: "请先打开此项目的活跃会话;不会激活历史会话"
|
|
13694
|
+
};
|
|
13695
|
+
try {
|
|
13696
|
+
await runtime.connect(agent, args.serverName);
|
|
13697
|
+
return {
|
|
13698
|
+
ok: true,
|
|
13699
|
+
project: mcpSnapshot(ctx).projects.find((item) => item.workspacePath === args.workspacePath)
|
|
13700
|
+
};
|
|
13701
|
+
} catch {
|
|
13702
|
+
return {
|
|
13703
|
+
ok: false,
|
|
13704
|
+
error: "项目 MCP 连接失败,请查看会话状态和 Host 日志"
|
|
13705
|
+
};
|
|
13706
|
+
}
|
|
13707
|
+
},
|
|
12916
13708
|
"mcp.projectSave": async (args) => {
|
|
12917
13709
|
try {
|
|
12918
13710
|
return {
|
|
12919
13711
|
ok: true,
|
|
12920
|
-
project: await projectSave(ctx, args.workspacePath, args.serverName, args.config)
|
|
13712
|
+
project: await projectSave(ctx, args.workspacePath, args.serverName, args.config, args.policy)
|
|
12921
13713
|
};
|
|
12922
13714
|
} catch (error) {
|
|
12923
13715
|
return {
|