dsh-vscode-mode 0.16.0 → 0.18.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.
Files changed (45) hide show
  1. package/README.md +15 -9
  2. package/lib/client.js +954 -133
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +5026 -4387
  5. package/lib/index.js.map +1 -1
  6. package/locale/en.json +4 -2
  7. package/locale/zh.json +4 -2
  8. package/package.json +4 -2
  9. package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
  10. package/src/capture.ts +2 -2
  11. package/src/client/compat.ts +2 -1
  12. package/src/client/index.ts +37 -7
  13. package/src/client/localeClient.ts +97 -0
  14. package/src/client/mcpSessionState.ts +108 -0
  15. package/src/client/monaco/lsp/providers.ts +32 -3
  16. package/src/client/officialSidebar.ts +4 -1
  17. package/src/client/state/modelWarmer.ts +198 -0
  18. package/src/client/state/recordsFeed.ts +241 -0
  19. package/src/client/styles/editor.css +5 -1
  20. package/src/client/ui/ConversationDiffDock.ts +10 -19
  21. package/src/client/ui/DiffBadge.ts +12 -19
  22. package/src/client/ui/EditorPane.ts +16 -4
  23. package/src/client/ui/EditorView.ts +114 -51
  24. package/src/client/ui/McpSessionStatus.ts +75 -0
  25. package/src/client/ui/McpSettings.ts +47 -13
  26. package/src/client/ui/PerfSizeBadge.ts +7 -4
  27. package/src/client/ui/PluginDetailSection.ts +82 -0
  28. package/src/dshVersion.ts +4 -2
  29. package/src/fileVersions.ts +2 -2
  30. package/src/lsp/rpc.ts +2 -2
  31. package/src/mcp.ts +1 -0
  32. package/src/mcpBridge.ts +163 -0
  33. package/src/mcpDiscover.ts +38 -0
  34. package/src/mcpPool.ts +245 -0
  35. package/src/mcpProject.ts +15 -12
  36. package/src/mcpRuntime.ts +301 -255
  37. package/src/registry.ts +15 -2
  38. package/src/rpc.ts +15 -3
  39. package/src/rules.ts +9 -4
  40. package/src/search/fallback.ts +2 -2
  41. package/src/search/ripgrep.ts +2 -2
  42. package/src/shared/mcp.ts +47 -0
  43. package/src/shared/rpc.ts +5 -1
  44. package/src/store.ts +26 -1
  45. package/src/svn.ts +2 -2
@@ -0,0 +1,82 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 官方插件管理器详情页的只读状态段。
4
+ * 挂在 DSH 0.2.1 新增的 `plugins.detail.section`(list / root)扩展位上,
5
+ * 只在「本插件自己」的详情页渲染,其余插件页返回 null。
6
+ * 作者 ddj 2026年10月19号
7
+ */
8
+ import React from 'react'
9
+ import { rpc } from '../rpc.js'
10
+
11
+ /** 本插件标识(与 package.json name / bundle 行 id 一致)。 */
12
+ export const SELF_ID = 'dsh-vscode-mode'
13
+
14
+ /**
15
+ * 判断详情页 subject 是否指向本插件。
16
+ * 官方有两种形状:官方内置项 `{kind:'item', id}`、安装包 `{kind:'bundle', pkg:{name}}`。
17
+ * @author ddj 2026年10月19号
18
+ * @param subject 详情页主题
19
+ * @returns 是否为本插件
20
+ */
21
+ export function isSelfSubject(subject) {
22
+ if (!subject || typeof subject !== 'object') return false
23
+ if (subject.kind === 'item') return subject.id === SELF_ID
24
+ if (subject.kind === 'bundle') return subject.pkg?.name === SELF_ID
25
+ return false
26
+ }
27
+
28
+ /**
29
+ * 把适配项列表压成「正常 / 未生效」计数文案。
30
+ * @author ddj 2026年10月19号
31
+ * @param list 适配项数组
32
+ * @returns 计数文案
33
+ */
34
+ function countText(list) {
35
+ const items = Array.isArray(list) ? list : []
36
+ const ok = items.filter((item) => item?.active === true).length
37
+ return ok + ' / ' + items.length + ' 项生效'
38
+ }
39
+
40
+ /**
41
+ * 插件详情页的只读状态段。
42
+ * @author ddj 2026年10月19号
43
+ * @param props DSH 注入的 { subject }
44
+ * @returns 状态段元素或 null(非本插件页)
45
+ */
46
+ export function PluginDetailSection(props) {
47
+ const subject = props?.subject
48
+ const isSelf = isSelfSubject(subject)
49
+ const [report, setReport] = React.useState(null)
50
+
51
+ React.useEffect(() => {
52
+ if (!isSelf) return undefined
53
+ let alive = true
54
+ rpc('compat', {}).then((result) => {
55
+ if (!alive || !result || !result.ok) return
56
+ setReport(result.report ?? null)
57
+ }).catch(() => { /* 状态段属锦上添花,取不到就不显示 */ })
58
+ return () => { alive = false }
59
+ }, [isSelf])
60
+
61
+ if (!isSelf) return null
62
+
63
+ const row = (name, note) => React.createElement('div', { key: name, className: 'vsm-compat-item ok' },
64
+ React.createElement('span', { className: 'vsm-compat-name' }, name),
65
+ React.createElement('span', { className: 'vsm-compat-note' }, note),
66
+ )
67
+
68
+ const versionText = report?.dshVersion ? 'DSH ' + report.dshVersion : '未探测到 DSH 版本'
69
+ const pluginText = report?.pluginVersion ? '插件 ' + report.pluginVersion : '插件版本未上报'
70
+ const warnCount = Array.isArray(report?.warnings) ? report.warnings.length : 0
71
+
72
+ return React.createElement('section', { className: 'vsm-panel', 'data-edrv-plugin-status': '1' },
73
+ React.createElement('h3', { className: 'vsm-panel-title' }, 'VSCodeMode 运行状态'),
74
+ React.createElement('div', { className: 'vsm-panel-body' },
75
+ row('版本', versionText + ' · ' + pluginText),
76
+ row('版本适配', countText(report?.adapters)),
77
+ row('外部插件与服务', countText(report?.external)),
78
+ row('护栏', countText(report?.guards)),
79
+ row('兼容性警告', warnCount ? warnCount + ' 条(设置页「兼容性」可查看详情)' : '未检测到'),
80
+ ),
81
+ )
82
+ }
package/src/dshVersion.ts CHANGED
@@ -84,14 +84,16 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
84
84
 
85
85
  /**
86
86
  * 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
87
- * 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2 精确标记。
88
- * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号
87
+ * 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2、0.2.1-rc.2 精确标记。
88
+ * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号 / 2026年10月19号
89
89
  * @param input 版本串(如 '0.1.6-alpha.2')
90
90
  * @returns 版本线标签
91
91
  */
92
92
  export function familyLabel(input: string): string {
93
93
  const version = parseDshVersion(input)
94
94
  if (!version) return '未知'
95
+ if (inDshRange(version, { from: '0.2.1-rc.2', to: '0.2.1-rc.2' })) return '0.2.1-rc.2(设置=profile Config;workingDirectory 投影跟随 cd;plugins.detail 扩展位;按能力探测)'
96
+ if (inDshRange(version, { from: '0.2.1-0', to: '0.2.1' })) return '0.2.1 线(rc.2 已逐版适配,其余按能力探测)'
95
97
  if (inDshRange(version, { from: '0.2.0-rc.2', to: '0.2.0-rc.2' })) return '0.2.0-rc.2(设置=profile Config;按能力探测)'
96
98
  if (inDshRange(version, { from: '0.2.0-rc.1', to: '0.2.0-rc.1' })) return '0.2.0-rc.1(设置=profile Config;按能力探测)'
97
99
  if (inDshRange(version, { from: '0.2.0-0' })) return '0.2.0 及更新(除 rc.1/rc.2 外未经逐版适配;按能力探测)'
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type { FileVersionItem } from './shared/rpc.js'
22
22
  import type { Ctx } from './store.js'
23
- import { cwdOf, sessionOf } from './registry.js'
23
+ import { cwdOfSession, sessionOf } from './registry.js'
24
24
  import { invalidateIndex } from './treeIndex.js'
25
25
  import { log } from './log.js'
26
26
 
@@ -150,7 +150,7 @@ export function createFileVersions(ctx: Ctx): FileVersions {
150
150
  .slice(0, MAX_PATHS)
151
151
  if (!fs || !paths.length) return { ok: true, items: [] }
152
152
  const session = sessionOf(ctx, args.sessionId)
153
- const cwd = cwdOf(session)
153
+ const cwd = cwdOfSession(ctx, session)
154
154
  const items: FileVersionItem[] = []
155
155
  for (const path of paths) items.push(await observe(fs, cwd, path))
156
156
  return { ok: true, items }
package/src/lsp/rpc.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { Ctx } from '../store.js'
9
9
  import { searchRoot } from '../search/ripgrep.js'
10
- import { sessionOf, cwdOf } from '../registry.js'
10
+ import { cwdOfSession, sessionOf } from '../registry.js'
11
11
  import { resolveProviderSpec, langOfPath, configFromPlugin, mergeConfig, sanitizeLang, LSP_LANGUAGES, type LspConfig, type LspLangConfig } from './config.js'
12
12
  import { clearProviderCache, candidatesFor } from './providers.js'
13
13
  import { capCompletions } from './completionCap.js'
@@ -154,7 +154,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
154
154
  const rootOf = async (sessionId: string | undefined): Promise<{ root: string; sessionId: string } | { err: string }> => {
155
155
  const session = sessionOf(ctx, sessionId)
156
156
  if (!session) return { err: SESSION_MISSING_ERROR }
157
- const cwd = cwdOf(session)
157
+ const cwd = cwdOfSession(ctx, session)
158
158
  if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
159
159
  try {
160
160
  const root = await searchRoot(ctx, session)
package/src/mcp.ts CHANGED
@@ -218,6 +218,7 @@ export async function saveMcp(ctx: Ctx, config: MpcConfig): Promise<MpcServer> {
218
218
 
219
219
  /** @private @author ddj 2026年10月08号 @param ctx Host services. @param config Configuration. @returns Saved status; validation failures do not write. */
220
220
  async function saveGlobal(ctx: Ctx, config: MpcConfig): Promise<MpcServer> {
221
+ if (config && 'vscodeMode' in config) throw new Error('vscodeMode 启动和共享策略仅适用于项目 MCP')
221
222
  validateConfig(config)
222
223
  await checkMcpName(ctx, config.serverName)
223
224
  const loader = ctx.get('loader')
@@ -0,0 +1,163 @@
1
+ /** Private official-MCP registrations, never a global provider. @author ddj 2026年10月20号 */
2
+ import { loadHostModule } from './hostImport.js'
3
+ import type { Ctx } from './store.js'
4
+ import type { McpPool, PoolEntry } from './mcpPool.js'
5
+
6
+ export interface BridgeScope {
7
+ ctx: any
8
+ dispose(): Promise<void>
9
+ }
10
+ export interface McpSection {
11
+ name: string
12
+ order: number
13
+ interpolate?: boolean
14
+ text: string | ((context?: any) => string)
15
+ }
16
+ export interface McpProvider {
17
+ request(request: any, execution: any): Promise<any>
18
+ }
19
+ export type ScopeMaker = (ctx: any, key: object) => BridgeScope
20
+
21
+ /** Per-agent public surface; original definitions still run inside the normal registry pipeline. */
22
+ export class McpBinding {
23
+ private readonly removals: Array<() => void> = []
24
+ private unsubscribe?: () => void
25
+ private closed = false
26
+
27
+ /** @public @author ddj 2026年10月20号 @param agent Public scope. @param pool Connection owner. @param entry Exact generation. @param valid Authorization recheck. */
28
+ constructor(private readonly agent: any, private readonly pool: McpPool, private readonly entry: PoolEntry, private readonly valid: () => boolean, private readonly called?: (elapsed: number) => void) {}
29
+
30
+ /** @private @author ddj 2026年10月20号 @param execution Registry caller. @throws Stale generation or foreign caller. */
31
+ private check(execution?: any): void {
32
+ if (this.closed || !this.valid() || !this.entry.live || (execution && execution.agent !== this.agent)) throw new Error('项目 MCP 作用域或配置已失效')
33
+ }
34
+
35
+ /** @public @author ddj 2026年10月20号 Reconciles exact definitions after official tool-list updates. */
36
+ sync(): void {
37
+ this.check()
38
+ const bridge = this.entry.bridge
39
+ if (!bridge) return
40
+ // Resources and instructions can change independently of tool names; replace the whole small scoped surface.
41
+ this.clear()
42
+ for (const [name, definition] of bridge.tools) {
43
+ /** @author ddj 2026年10月20号 @param args Registry-validated arguments. @param execution Unmodified caller context. @returns Official canonical output. */
44
+ const execute = async (args: unknown, execution: any): Promise<unknown> => {
45
+ this.check(execution)
46
+ return this.pool.run(this.entry, async () => {
47
+ this.check(execution)
48
+ if (bridge.tools.get(name) !== definition) throw new Error('MCP 工具定义已更新,请重新发现')
49
+ const startedAt = Date.now()
50
+ const result = await definition.execute(args, execution)
51
+ this.called?.(Date.now() - startedAt)
52
+ return result
53
+ }, execution.signal)
54
+ }
55
+ this.removals.push(this.agent.ctx.get('tools').register({ ...definition, execute }))
56
+ }
57
+ // Resource ownership stays with the runtime's single lazy provider, avoiding duplicate namespace registration.
58
+ for (const section of bridge.sections.values()) {
59
+ /** @author ddj 2026年10月20号 @param context Actual prompt assembly. @returns Only valid-generation instructions. */
60
+ const text = (context: any): string => {
61
+ if (!this.valid() || this.closed) return ''
62
+ if (typeof section.text === 'function') return section.text(context)
63
+ return section.text
64
+ }
65
+ this.removals.push(this.agent.ctx.get('systemPrompt').section({ ...section, text }))
66
+ }
67
+ /** @author ddj 2026年10月20号 Reconciles dynamic official registrations only for a live subscriber. */
68
+ const update = (): void => { if (!this.closed && this.valid() && this.entry.live) this.sync() }
69
+ if (!this.unsubscribe) this.unsubscribe = bridge.subscribe(update)
70
+ }
71
+
72
+ /** @private @author ddj 2026年10月20号 Removes only this binding's exact scoped registrations. */
73
+ private clear(): void {
74
+ for (const remove of this.removals.splice(0).reverse()) remove()
75
+ }
76
+
77
+ /** @public @author ddj 2026年10月20号 Revokes proxies synchronously before releasing a lease. */
78
+ dispose(): void {
79
+ this.closed = true
80
+ this.unsubscribe?.()
81
+ this.unsubscribe = undefined
82
+ this.clear()
83
+ }
84
+ }
85
+
86
+ /** A connection's private surface. Public consumers register scoped proxies, not this scope. */
87
+ export class McpBridge {
88
+ readonly tools = new Map<string, any>()
89
+ readonly resources = new Map<string, McpProvider>()
90
+ readonly sections = new Map<string, McpSection>()
91
+ private readonly listeners = new Set<() => void>()
92
+ private closed = false
93
+ private closing?: Promise<void>
94
+
95
+ /** @private @author ddj 2026年10月20号 @param scope Private lifetime and registration context. */
96
+ private constructor(readonly scope: BridgeScope) {}
97
+
98
+ /** @public @author ddj 2026年10月20号 @param ctx Host owner. @param make Optional real scope factory for installed-host tests. @returns Isolated registration adapter. */
99
+ static async create(ctx: Ctx, make?: ScopeMaker): Promise<McpBridge> {
100
+ const factory = make ?? (await loadHostModule('@deepseek-ai/dsh-scope') as { createScope: ScopeMaker }).createScope
101
+ const scope = factory(ctx, {})
102
+ // Providers must be installed after the backing scope's initial activation transaction.
103
+ await scope.ctx.fiber?.await?.()
104
+ const bridge = new McpBridge(scope)
105
+ bridge.install()
106
+ return bridge
107
+ }
108
+
109
+ /** @private @author ddj 2026年10月20号 Installs only private services; no project registration reaches the root. */
110
+ private install(): void {
111
+ const privateCtx = this.scope.ctx.isolate('tools').isolate('mcpResources').isolate('systemPrompt')
112
+ privateCtx.provide('tools', { register: (definition: any) => this.register(this.tools, definition.name, definition) })
113
+ privateCtx.provide('mcpResources', { register: (name: string, provider: McpProvider) => this.register(this.resources, name, provider) })
114
+ privateCtx.provide('systemPrompt', {
115
+ getSectionOrder: () => 3100,
116
+ section: (section: McpSection) => this.register(this.sections, section.name, section),
117
+ })
118
+ this.scope.ctx = privateCtx
119
+ }
120
+
121
+ /** @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. */
122
+ private register<T>(registry: Map<string, T>, name: string, entry: T): () => void {
123
+ if (this.closed) throw new Error('MCP bridge is disposed')
124
+ if (registry.has(name)) throw new Error('Duplicate private MCP registration: ' + name)
125
+ registry.set(name, entry)
126
+ this.changed()
127
+
128
+ return () => {
129
+ if (registry.get(name) !== entry) return
130
+ registry.delete(name)
131
+ this.changed()
132
+ }
133
+ }
134
+
135
+ /** @public @author ddj 2026年10月20号 @param listener Surface change subscriber. @returns Subscription disposer. */
136
+ subscribe(listener: () => void): () => void {
137
+ this.listeners.add(listener)
138
+ return () => { this.listeners.delete(listener) }
139
+ }
140
+
141
+ /** @private @author ddj 2026年10月20号 Notifies consumers; observer errors cannot corrupt the official sync transaction. */
142
+ private changed(): void {
143
+ for (const listener of this.listeners) {
144
+ try { listener() } catch { /* Runtime owns diagnostics for its subscriber. */ }
145
+ }
146
+ }
147
+
148
+ /** @public @author ddj 2026年10月20号 @returns Quiescent private-scope teardown. */
149
+ dispose(): Promise<void> {
150
+ this.closed = true
151
+ return this.closing ??= this.closeScope()
152
+ }
153
+
154
+ /** @private @author ddj 2026年10月20号 @returns Exact scope closure before forgetting registrations. */
155
+ private async closeScope(): Promise<void> {
156
+ await this.scope.dispose()
157
+ this.tools.clear()
158
+ this.resources.clear()
159
+ this.sections.clear()
160
+ this.changed()
161
+ this.listeners.clear()
162
+ }
163
+ }
@@ -0,0 +1,38 @@
1
+ /** Scoped capability discovery; never accepts commands, paths or caller identity. @author ddj 2026年10月20号 */
2
+ import type { McpRuntime } from './mcpRuntime.js'
3
+
4
+ /** @public @author ddj 2026年10月20号 @param runtime Authorized runtime. @param ctx Host registration context. @returns Global discovery cleanup; normal capability filters remain authoritative. */
5
+ export function installDiscover(runtime: McpRuntime, ctx: any): () => void {
6
+ const tools = ctx.get('tools')
7
+ /** @author ddj 2026年10月20号 @param args Validated operation. @param execution Registry-owned caller identity. @returns Safe capability catalogue or real connected schemas. */
8
+ const execute = async (args: any, execution: any): Promise<unknown> => {
9
+ const agent = execution.agent
10
+ if (!agent || !runtime.agents.has(agent)) throw new Error('会话未激活,不能使用项目 MCP 发现入口')
11
+ if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).some((key) => !['action', 'serverName'].includes(key))) throw new Error('发现入口仅接受 action 和 serverName')
12
+ if (!['list', 'connect'].includes(args.action)) throw new Error('action 必须为 list 或 connect')
13
+ if (args.action === 'list') return runtime.catalogue(agent)
14
+ if (typeof args.serverName !== 'string') throw new Error('请指定已配置的 serverName')
15
+ return runtime.connect(agent, args.serverName, execution.signal)
16
+ }
17
+ /** @author ddj 2026年10月20号 @param _args Unused. @param result Safe canonical JSON. @returns Native content without bypassing output validation. */
18
+ const render = (_args: unknown, result: unknown): any[] => [{ type: 'text', text: JSON.stringify(result) }]
19
+ const removeTool = tools.register({
20
+ name: 'discover_project_mcp', 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.',
21
+ parameters: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'connect'] }, serverName: { type: 'string' } }, required: ['action'], additionalProperties: false },
22
+ output: { schema: {}, render }, execute,
23
+ })
24
+ return removeTool
25
+ }
26
+
27
+ /** @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. */
28
+ export function discoverPrompt(runtime: McpRuntime, agent: any): () => void {
29
+ const prompt = agent.ctx.get('systemPrompt')
30
+ /** @author ddj 2026年10月20号 @returns Current enabled names only; no credential-bearing configuration. */
31
+ const text = (): string => {
32
+ const names = runtime.catalogue(agent).map((server) => server.serverName)
33
+ if (!names.length) return ''
34
+ 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.'
35
+ }
36
+ const removePrompt = prompt.section({ name: 'vscode-mode:project-mcp', order: 3090, text, interpolate: false })
37
+ return removePrompt
38
+ }
package/src/mcpPool.ts ADDED
@@ -0,0 +1,245 @@
1
+ /** Connection ownership, bounded cold starts and workspace leases. @author ddj 2026年10月20号 */
2
+ import { createHash } from 'node:crypto'
3
+ import { McpBridge } from './mcpBridge.js'
4
+ import type { McpPolicy, McpPhase, MpcConfig } from './shared/mcp.js'
5
+ import type { Ctx } from './store.js'
6
+
7
+ export interface PoolSpec {
8
+ workspace: string
9
+ owner: object
10
+ config: MpcConfig
11
+ policy: McpPolicy
12
+ }
13
+ export interface PoolEntry {
14
+ key: string
15
+ spec: PoolSpec
16
+ refs: number
17
+ phase: McpPhase
18
+ queuedAt: number
19
+ phaseAt: number
20
+ finishedAt?: number
21
+ startAt?: number
22
+ connectedAt?: number
23
+ error?: string
24
+ retryAt?: number
25
+ bridge?: McpBridge
26
+ fiber?: any
27
+ ready: Promise<void>
28
+ close?: Promise<void>
29
+ idle?: ReturnType<typeof setTimeout>
30
+ live: boolean
31
+ calls: Promise<unknown>
32
+ }
33
+ export interface PoolLease {
34
+ entry: PoolEntry
35
+ release(): void
36
+ }
37
+ export type BridgeMaker = () => Promise<McpBridge>
38
+
39
+ /** @private @author ddj 2026年10月20号 @param input Plain effective configuration. @returns Stable secret-bearing identity used only as digest input. */
40
+ function stableConfig(input: unknown): string {
41
+ if (Array.isArray(input)) return '[' + input.map(stableConfig).join(',') + ']'
42
+ if (input && typeof input === 'object') {
43
+ const config = input as Record<string, unknown>
44
+ return '{' + Object.keys(config).sort().map((key) => JSON.stringify(key) + ':' + stableConfig(config[key])).join(',') + '}'
45
+ }
46
+ return JSON.stringify(input) ?? 'null'
47
+ }
48
+
49
+ /** @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. */
50
+ export function waitMcp<T>(task: Promise<T>, signal?: AbortSignal): Promise<T> {
51
+ if (!signal) return task
52
+ return new Promise<T>((resolve, reject) => {
53
+ /** @author ddj 2026年10月20号 Rejects only this waiter. */
54
+ const abort = (): void => { reject(new Error('MCP 等待已取消')) }
55
+ if (signal.aborted) { abort(); return }
56
+ signal.addEventListener('abort', abort, { once: true })
57
+ task.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort))
58
+ })
59
+ }
60
+
61
+ /** The pool owns private scopes independently from any first subscriber. */
62
+ export class McpPool {
63
+ readonly entries = new Map<string, PoolEntry>()
64
+ private readonly owners = new WeakMap<object, number>()
65
+ private ownerSeq = 0
66
+ private running = 0
67
+ private readonly queue: Array<() => Promise<void>> = []
68
+ private readonly jobs = new Set<Promise<void>>()
69
+ private stopped = false
70
+
71
+ /** @public @author ddj 2026年10月20号 @param ctx Host lifecycle owner. @param load Official plugin loader. @param make Private scope factory. */
72
+ constructor(private readonly ctx: Ctx, private readonly load: () => Promise<any>, private readonly make: BridgeMaker = () => McpBridge.create(ctx)) {}
73
+
74
+ /** @private @author ddj 2026年10月20号 @param spec Effective request. @returns Private digest; never emit configuration or secrets. */
75
+ private keyOf(spec: PoolSpec): string {
76
+ let owner = 0
77
+ if (spec.policy.sharing === 'agent') {
78
+ if (!this.owners.has(spec.owner)) this.owners.set(spec.owner, ++this.ownerSeq)
79
+ owner = this.owners.get(spec.owner)!
80
+ }
81
+ const workspace = process.platform === 'win32' ? spec.workspace.replace(/\\/g, '/').toLowerCase() : spec.workspace
82
+ return createHash('sha256').update(stableConfig([workspace, owner, spec.config, spec.policy])).digest('hex')
83
+ }
84
+
85
+ /** @public @author ddj 2026年10月20号 @param spec Authorized connection request. @returns Reference-counted single-flight lease. */
86
+ acquire(spec: PoolSpec): PoolLease {
87
+ if (this.stopped) throw new Error('MCP runtime is disposed')
88
+ const key = this.keyOf(spec)
89
+ let entry = this.entries.get(key)
90
+ if (entry?.phase === 'error' && entry.retryAt !== undefined && Date.now() >= entry.retryAt && !entry.close) {
91
+ this.entries.delete(key)
92
+ entry = undefined
93
+ }
94
+ if (entry && !entry.live) throw new Error(entry.error ?? 'MCP 旧连接仍在释放,暂不能创建替代实例')
95
+ if (!entry) {
96
+ entry = { key, spec, refs: 0, phase: 'queued', queuedAt: Date.now(), phaseAt: Date.now(), ready: Promise.resolve(), live: true, calls: Promise.resolve() }
97
+ this.entries.set(key, entry)
98
+ entry.ready = this.schedule(entry)
99
+ void entry.ready.catch(() => undefined)
100
+ }
101
+ if (entry.idle) clearTimeout(entry.idle)
102
+ entry.idle = undefined
103
+ entry.refs += 1
104
+ let released = false
105
+ return { entry, release: () => {
106
+ if (released) return
107
+ released = true
108
+ this.release(entry!)
109
+ } }
110
+ }
111
+
112
+ /** @private @author ddj 2026年10月20号 @param entry New entry. @returns Queued initialization, bounded to two concurrent starts. */
113
+ private schedule(entry: PoolEntry): Promise<void> {
114
+ return new Promise<void>((resolve, reject) => {
115
+ /** @author ddj 2026年10月20号 Initializes a still-owned queue item. */
116
+ const run = async (): Promise<void> => {
117
+ try { await this.start(entry); resolve() } catch (error) { reject(error) }
118
+ }
119
+ this.queue.push(run)
120
+ this.pump()
121
+ })
122
+ }
123
+
124
+ /** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
125
+ private pump(): void {
126
+ while (this.running < 2 && this.queue.length) {
127
+ const run = this.queue.shift()!
128
+ this.running += 1
129
+ const job = run().finally(() => {
130
+ this.running -= 1
131
+ this.jobs.delete(job)
132
+ this.pump()
133
+ })
134
+ this.jobs.add(job)
135
+ }
136
+ }
137
+
138
+ /** @private @author ddj 2026年10月20号 @param entry Connection to initialize. @returns Initialization completion, or failure after owned cleanup. */
139
+ private async start(entry: PoolEntry): Promise<void> {
140
+ if (!entry.live || this.stopped) throw new Error('MCP 启动已取消')
141
+ entry.phase = 'loading'
142
+ entry.phaseAt = Date.now()
143
+ entry.startAt = entry.phaseAt
144
+ let timer: ReturnType<typeof setTimeout> | undefined
145
+ const work = this.mount(entry)
146
+ const deadline = new Promise<never>((_resolve, reject) => {
147
+ timer = setTimeout(() => reject(new Error('MCP 初始化超时')), entry.spec.policy.startupTimeoutMs)
148
+ timer.unref?.()
149
+ })
150
+ try {
151
+ await Promise.race([work, deadline])
152
+ if (!entry.live) throw new Error('MCP 启动已取消')
153
+ entry.phase = 'ready'
154
+ entry.finishedAt = Date.now()
155
+ entry.connectedAt = entry.finishedAt
156
+ } catch (error) {
157
+ entry.error = 'MCP 初始化失败或超时,请查看 Host 日志'
158
+ this.ctx.logger?.warn?.('MCP initialization failed: ' + String(error))
159
+ // Invalidation precedes disposal; neither late module load nor callbacks may publish.
160
+ entry.live = false
161
+ entry.phase = 'stopping'
162
+ const closing = this.closeBridge(entry)
163
+ await Promise.allSettled([closing, work])
164
+ entry.phase = 'error'
165
+ entry.finishedAt = Date.now()
166
+ entry.retryAt = Date.now() + 5000
167
+ // Failed generations stay invalid; acquire can replace them only after confirmed closure and cooldown.
168
+ throw new Error(entry.error)
169
+ } finally {
170
+ if (timer) clearTimeout(timer)
171
+ }
172
+ }
173
+
174
+ /** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
175
+ private async mount(entry: PoolEntry): Promise<void> {
176
+ const official = await this.load()
177
+ if (!entry.live || this.stopped) return
178
+ const bridge = await this.make()
179
+ if (!entry.live || this.stopped) { await bridge.dispose(); return }
180
+ entry.bridge = bridge
181
+ entry.phase = 'starting'
182
+ entry.phaseAt = Date.now()
183
+ entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config)
184
+ await entry.fiber
185
+ if (entry.fiber?.state !== undefined && entry.fiber.state !== 2) throw new Error('MCP 插件未完成激活')
186
+ }
187
+
188
+ /** @private @author ddj 2026年10月20号 @param entry Connection to close. @returns Shared closure barrier; rejection retains a fenced entry. */
189
+ private async closeBridge(entry: PoolEntry): Promise<void> {
190
+ if (entry.close) return entry.close
191
+ const close = entry.bridge?.dispose() ?? Promise.resolve()
192
+ entry.close = close
193
+ try {
194
+ await close
195
+ entry.bridge = undefined
196
+ entry.fiber = undefined
197
+ entry.close = undefined
198
+ } catch (error) {
199
+ entry.error = 'MCP 释放未确认,禁止重建,请查看 Host 日志'
200
+ this.ctx.logger?.warn?.('MCP cleanup failed: ' + String(error))
201
+ throw error
202
+ }
203
+ }
204
+
205
+ /** @private @author ddj 2026年10月20号 @param entry One lease's owner. Arms workspace-only warm retention. */
206
+ private release(entry: PoolEntry): void {
207
+ entry.refs = Math.max(0, entry.refs - 1)
208
+ if (entry.refs || !entry.live) return
209
+ if (entry.spec.policy.sharing === 'agent') { void this.retire(entry).catch(() => undefined); return }
210
+ entry.idle = setTimeout(() => { void this.retire(entry).catch(() => undefined) }, 60000)
211
+ entry.idle.unref?.()
212
+ }
213
+
214
+ /** @public @author ddj 2026年10月20号 @param entry Invalid generation. @returns Quiescent close; callers may await before starting a replacement. */
215
+ async retire(entry: PoolEntry): Promise<void> {
216
+ entry.live = false
217
+ entry.phase = 'stopping'
218
+ if (entry.idle) clearTimeout(entry.idle)
219
+ await this.closeBridge(entry)
220
+ await entry.ready.catch(() => undefined)
221
+ // A late mount checks live before assigning its bridge.
222
+ await entry.calls.catch(() => undefined)
223
+ if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key)
224
+ }
225
+
226
+ /** @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. */
227
+ run<T>(entry: PoolEntry, action: () => Promise<T>, signal?: AbortSignal): Promise<T> {
228
+ /** @author ddj 2026年10月20号 Checks cancellation and generation again at actual dispatch. */
229
+ const invoke = async (): Promise<T> => {
230
+ await waitMcp(entry.ready, signal)
231
+ if (signal?.aborted || !entry.live || entry.phase !== 'ready') throw new Error('MCP 连接不可用或调用已取消')
232
+ return action()
233
+ }
234
+ const result = entry.calls.then(invoke)
235
+ entry.calls = result.catch(() => undefined)
236
+ return result
237
+ }
238
+
239
+ /** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
240
+ async dispose(): Promise<void> {
241
+ this.stopped = true
242
+ await Promise.all([...this.entries.values()].map((entry) => this.retire(entry)))
243
+ await Promise.all([...this.jobs])
244
+ }
245
+ }