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.
- package/README.md +15 -9
- package/lib/client.js +954 -133
- package/lib/client.js.map +1 -1
- package/lib/index.js +5026 -4387
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -2
- package/locale/zh.json +4 -2
- package/package.json +4 -2
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/capture.ts +2 -2
- package/src/client/compat.ts +2 -1
- package/src/client/index.ts +37 -7
- package/src/client/localeClient.ts +97 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/officialSidebar.ts +4 -1
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/state/recordsFeed.ts +241 -0
- package/src/client/styles/editor.css +5 -1
- package/src/client/ui/ConversationDiffDock.ts +10 -19
- package/src/client/ui/DiffBadge.ts +12 -19
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +114 -51
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/client/ui/PerfSizeBadge.ts +7 -4
- package/src/client/ui/PluginDetailSection.ts +82 -0
- package/src/dshVersion.ts +4 -2
- package/src/fileVersions.ts +2 -2
- package/src/lsp/rpc.ts +2 -2
- 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 +301 -255
- package/src/registry.ts +15 -2
- package/src/rpc.ts +15 -3
- package/src/rules.ts +9 -4
- package/src/search/fallback.ts +2 -2
- package/src/search/ripgrep.ts +2 -2
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +5 -1
- package/src/store.ts +26 -1
- 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 外未经逐版适配;按能力探测)'
|
package/src/fileVersions.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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')
|
package/src/mcpBridge.ts
ADDED
|
@@ -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
|
+
}
|