dsh-vscode-mode 0.17.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 +6 -4
- package/lib/client.js +578 -49
- package/lib/client.js.map +1 -1
- package/lib/index.js +842 -244
- 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 +15 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/styles/editor.css +5 -1
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +76 -18
- 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 +13 -1
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +5 -1
|
@@ -68,6 +68,7 @@ import { mdPreviewLoad, mdPreviewSave } from '../state/mdPreviewCache.js'
|
|
|
68
68
|
import { onUiStateReady, reportUiStateEvent, uiStateSettled } from '../state/uiStatePersist.js'
|
|
69
69
|
import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
|
|
70
70
|
import { modelsForScope, rememberModel, retainModels } from '../state/modelCache.js'
|
|
71
|
+
import { clearModelWarmer, setModelWarmer } from '../state/modelWarmer.js'
|
|
71
72
|
import { navFlashRangeOf } from '../navHighlight.js'
|
|
72
73
|
import { chordOf, useKeybindingsVersion } from '../keybindings.js'
|
|
73
74
|
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
@@ -260,6 +261,8 @@ export function EditorView(props) {
|
|
|
260
261
|
const planActiveRef = React.useRef(null)
|
|
261
262
|
planActiveRef.current = planActive
|
|
262
263
|
const planHostRef = React.useRef(null)
|
|
264
|
+
// 进入只读计划预览前的磁盘选中页签,关闭该计划页签时恢复,避免跳到第一个页签。
|
|
265
|
+
const planReturnRef = React.useRef(null)
|
|
263
266
|
React.useEffect(() => { setPlanTabs([]); setPlanActive(null) }, [sessionId, cwd])
|
|
264
267
|
/** @private @author ddj 2026年10月08号 @param path Disk tab selection. Leave read-only plan mode before any disk navigation. */
|
|
265
268
|
function setActive(path) {
|
|
@@ -271,6 +274,10 @@ export function EditorView(props) {
|
|
|
271
274
|
const [layoutScope, setLayoutScope] = React.useState(null)
|
|
272
275
|
const benchRef = React.useRef(workbench)
|
|
273
276
|
benchRef.current = workbench
|
|
277
|
+
// 页签行归属的分组:焦点落在浮窗时行仍留在页面主组上(浮窗自带标题栏),与 renderGroup 同一判据。
|
|
278
|
+
// 行节点会随焦点在分组间迁移,绑定滚轮/滚入视野的 effect 必须跟着这个归属重跑。
|
|
279
|
+
const rowOnFloat = workbench.floats.some((item) => item.group === workbench.focused)
|
|
280
|
+
const rowOwner = rowOnFloat ? MAIN_GROUP : workbench.focused
|
|
274
281
|
const [dragPreview, setDragPreview] = React.useState(null)
|
|
275
282
|
const dragRef = React.useRef(null)
|
|
276
283
|
const benchOps = React.useRef(null)
|
|
@@ -1853,11 +1860,11 @@ export function EditorView(props) {
|
|
|
1853
1860
|
const right = left + el.offsetWidth
|
|
1854
1861
|
if (left < host.scrollLeft) host.scrollLeft = left
|
|
1855
1862
|
else if (right > host.scrollLeft + host.clientWidth) host.scrollLeft = right - host.clientWidth
|
|
1856
|
-
}, [active, tabs.length])
|
|
1863
|
+
}, [active, planActive, planTabs.length, tabs.length, rowOwner])
|
|
1857
1864
|
|
|
1858
1865
|
// 页签栏滚轮横向滚动(需求 3):页签栏溢出时,鼠标滚轮(含触控板纵向手势)横滚页签栏,
|
|
1859
1866
|
// 不溢出时不接管(保持页面垂直滚动);Shift+滚轮交由浏览器原生横滚。详见 horizontalWheel.ts。
|
|
1860
|
-
React.useEffect(() => attachHorizontalWheel(tabsHostRef.current), [])
|
|
1867
|
+
React.useEffect(() => attachHorizontalWheel(tabsHostRef.current), [rowOwner])
|
|
1861
1868
|
|
|
1862
1869
|
// 鼠标侧键后退/前进:Logitech 官方默认「后退/前进」= XButton 鼠标事件(button 3/4),不产生键盘事件。
|
|
1863
1870
|
// pointerdown 触发导航(preventDefault 后兼容 mousedown 可能不再触发,避免双触发);
|
|
@@ -2065,6 +2072,42 @@ export function EditorView(props) {
|
|
|
2065
2072
|
return model
|
|
2066
2073
|
}
|
|
2067
2074
|
|
|
2075
|
+
const warmRef = React.useRef(null)
|
|
2076
|
+
/**
|
|
2077
|
+
* 让目标文件的 Monaco model 存在(预热入口,供 Ctrl+hover 定义链接预览)。
|
|
2078
|
+
*
|
|
2079
|
+
* 为什么需要:Monaco 在渲染定义链接的源码预览时对目标 uri 调 `createModelReference`
|
|
2080
|
+
* 后只挂 then、没有 catch;本插件的定义目标是自造 `edrv://` uri,目标文件若从未打开过
|
|
2081
|
+
* 就没有已注册 model,会 reject("Model not found") 成为未捕获异常(见 state/modelWarmer.ts)。
|
|
2082
|
+
* 已缓存的路径直接返回成功:模型已在 Monaco 注册表里,无需再读盘。
|
|
2083
|
+
* 预热出的 model 走 modelsRef 常规 LRU(40 条、未附着超限被 dispose);正式打开该文件时
|
|
2084
|
+
* 由 loadContent 的 cached-model 分支 + checkCached 后台校验接管,不会显示陈旧内容。
|
|
2085
|
+
* @private
|
|
2086
|
+
* @author ddj 2026年10月20号
|
|
2087
|
+
* @param path 工作区相对路径
|
|
2088
|
+
* @returns 模型是否已存在/成功建出(失败一律 false,绝不抛)
|
|
2089
|
+
*/
|
|
2090
|
+
warmRef.current = (path) => {
|
|
2091
|
+
if (!path) return Promise.resolve(false)
|
|
2092
|
+
if (modelsRef.current.get(path)) return Promise.resolve(true)
|
|
2093
|
+
// 图片/PDF 不建文本 model(各自走预览控件),预热对它们没有意义
|
|
2094
|
+
if (isImagePath(path) || isPdfPath(path)) return Promise.resolve(false)
|
|
2095
|
+
const sid = sessionId
|
|
2096
|
+
if (!sid) return Promise.resolve(false)
|
|
2097
|
+
|
|
2098
|
+
return rpc('edrv.read', { sessionId: sid, path }).then((res) => {
|
|
2099
|
+
if (!res?.ok || typeof res.content !== 'string') return false
|
|
2100
|
+
getModel(path, res.content)
|
|
2101
|
+
return true
|
|
2102
|
+
}).catch(() => false)
|
|
2103
|
+
}
|
|
2104
|
+
// 预热实现随挂载安装、卸载移除:编辑器未挂载时 warmModelFor 直接返回 false(退化为修复前行为)
|
|
2105
|
+
React.useEffect(() => {
|
|
2106
|
+
const warmImpl = (path) => warmRef.current?.(path) ?? Promise.resolve(false)
|
|
2107
|
+
setModelWarmer(warmImpl)
|
|
2108
|
+
return () => clearModelWarmer(warmImpl)
|
|
2109
|
+
}, [])
|
|
2110
|
+
|
|
2068
2111
|
/**
|
|
2069
2112
|
* 提交待执行的防抖保存(**真正执行保存**,不是取消)。
|
|
2070
2113
|
* 语义与缺陷背景见 saveDebounce.ts:`schedule` 的返回值是只 clearTimeout 的 disposer,
|
|
@@ -3595,6 +3638,7 @@ export function EditorView(props) {
|
|
|
3595
3638
|
if (!document) return
|
|
3596
3639
|
flushSave()
|
|
3597
3640
|
saveViewState(active)
|
|
3641
|
+
if (!planActiveRef.current) planReturnRef.current = active
|
|
3598
3642
|
setDiskActive(null)
|
|
3599
3643
|
setPlanTabs((prev) => [...prev.filter((item) => item.address !== document.address), document].slice(-32))
|
|
3600
3644
|
setPlanActive(document.address)
|
|
@@ -3605,7 +3649,10 @@ export function EditorView(props) {
|
|
|
3605
3649
|
function closePlan(address, event) {
|
|
3606
3650
|
event?.stopPropagation()
|
|
3607
3651
|
setPlanTabs((prev) => prev.filter((item) => item.address !== address))
|
|
3608
|
-
if (planActiveRef.current
|
|
3652
|
+
if (planActiveRef.current !== address) return
|
|
3653
|
+
const previous = planReturnRef.current
|
|
3654
|
+
planReturnRef.current = null
|
|
3655
|
+
setActive(tabsRef.current.some((tab) => tab.path === previous) ? previous : (tabsRef.current[0]?.path ?? null))
|
|
3609
3656
|
}
|
|
3610
3657
|
/** @private @author ddj 2026年10月08号 @param request Claimed disk or virtual open. Flushes saves without replacing dirty models. */
|
|
3611
3658
|
function openReceiptFile(request) {
|
|
@@ -3916,11 +3963,17 @@ export function EditorView(props) {
|
|
|
3916
3963
|
setWorkbench((current) => ({ ...current, tree: replace(current.tree) }))
|
|
3917
3964
|
}
|
|
3918
3965
|
/** @private @author ddj 2026年10月14号 @param id Leaf id. @returns Tabs and either focused full-feature editor or independent passive view. */
|
|
3919
|
-
function renderGroup(id) {
|
|
3966
|
+
function renderGroup(id, floating) {
|
|
3920
3967
|
const group = workbench.groups.find((item) => item.id === id)
|
|
3921
3968
|
if (!group) return null
|
|
3922
3969
|
const focused = id === workbench.focused
|
|
3923
|
-
|
|
3970
|
+
// 同一行:计划预览页签并入该组自己的页签行(extras),与磁盘页签共用一条滚动区,
|
|
3971
|
+
// 页面工具列固定在行尾(滚动区之外,横滚不带走)。焦点落在浮窗时,这一行仍留在页面分组上,
|
|
3972
|
+
// 避免整条工具栏跟着搬进浮窗;页签缓存 ref 只认页面上的这条行。
|
|
3973
|
+
const chrome = !floating && id === rowOwner
|
|
3974
|
+
// 计划预览占用的正文只会在焦点组里;承载页签行的组同时收起磁盘选中态,保证整屏只有计划页签一条下划线。
|
|
3975
|
+
const hideDiskActive = Boolean(planActive) && (chrome || focused)
|
|
3976
|
+
return React.createElement(EditorPane, { key: id, group, focused, dirty: dirtyMap, suppressActive: hideDiskActive, tabsRef: chrome ? tabsHostRef : undefined, extras: chrome ? planTabExtras : undefined, tools: chrome ? rowTools : undefined, onSelect: selectGroup, onClose: closeTab, onMenu: paneMenu, onDrag: dragTab, consumeClick: consumeDrag },
|
|
3924
3977
|
focused ? React.createElement('div', { className: 'edrv-full-slot', ref: mountFull }) : React.createElement(React.Fragment, null,
|
|
3925
3978
|
React.createElement(GroupPathbar, { path: group.active, onFocus: () => selectGroup(id) }),
|
|
3926
3979
|
React.createElement(PassiveEditor, { key: String(scope) + ':' + group.active, path: group.active, groupId: id, mount: passiveMount })))
|
|
@@ -3932,15 +3985,18 @@ export function EditorView(props) {
|
|
|
3932
3985
|
}
|
|
3933
3986
|
// --endregion
|
|
3934
3987
|
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3988
|
+
// 计划预览页签不再是独立一行的工具条:它作为焦点分组页签行的尾部内容(EditorPane 的 extras)渲染,
|
|
3989
|
+
// 与磁盘页签同处一条滚动区,因此整块工作台只有一行页签。
|
|
3990
|
+
const planTabExtras = planTabs.length
|
|
3991
|
+
? planTabs.map((plan) => React.createElement('div', {
|
|
3992
|
+
key: plan.address, className: 'edrv-tab edrv-tab-plan' + (plan.address === planActive ? ' edrv-tab-active' : ''),
|
|
3938
3993
|
title: plan.title + '(计划预览 · 只读)', onClick: () => openPlanDoc(plan),
|
|
3939
|
-
}, React.createElement('span', {
|
|
3940
|
-
React.createElement('span', { className: 'edrv-tab-x', title: '关闭', onClick: (event) => closePlan(plan.address, event) }, '×')))
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3994
|
+
}, React.createElement('span', { className: 'edrv-group-tab-name' }, plan.title),
|
|
3995
|
+
React.createElement('span', { className: 'edrv-tab-x', title: '关闭', onClick: (event) => closePlan(plan.address, event) }, '×')))
|
|
3996
|
+
: null
|
|
3997
|
+
const openControl = openInput
|
|
3998
|
+
? React.createElement('input', { className: 'edrv-path-input', autoFocus: true, placeholder: '输入工作区相对/绝对路径,回车打开', value: pathDraft, onChange: (e) => setPathDraft(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') openPath(); if (e.key === 'Escape') setOpenInput(false) } })
|
|
3999
|
+
: React.createElement('button', { className: 'edrv-tab-add', title: '打开文件(输入路径)', onClick: () => setOpenInput(true) }, '+')
|
|
3944
4000
|
|
|
3945
4001
|
const activePlan = planTabs.find((plan) => plan.address === planActive)
|
|
3946
4002
|
const pathBar = activePlan ? React.createElement('div', { className: 'edrv-pathbar', title: activePlan.address },
|
|
@@ -4022,8 +4078,9 @@ export function EditorView(props) {
|
|
|
4022
4078
|
const navBackTarget = navRef.current.peekBack()
|
|
4023
4079
|
const navForwardTarget = navRef.current.peekForward()
|
|
4024
4080
|
const navNameOf = (entry) => (entry && entry.path ? String(entry.path).split(/[\\/]/).pop() : '')
|
|
4025
|
-
|
|
4026
|
-
|
|
4081
|
+
// 行尾固定工具列:位于焦点分组页签行的滚动区右侧,横滚页签时不跟着滚走。
|
|
4082
|
+
const rowTools = React.createElement('div', { className: 'edrv-tabs-tools' },
|
|
4083
|
+
openControl,
|
|
4027
4084
|
(sum.totalFiles > 0
|
|
4028
4085
|
? React.createElement('button', { className: 'edrv-diffchip', title: '打开差异总览/归档', onClick: () => setLauncherOpen((o) => !o) }, '⚠ 差异 ' + sum.totalFiles + ' 文件')
|
|
4029
4086
|
: null),
|
|
@@ -4441,7 +4498,8 @@ export function EditorView(props) {
|
|
|
4441
4498
|
'data-edrv-diff-dock-shell': '1',
|
|
4442
4499
|
}, React.createElement(DiffBox, { ...dockSnapshot, dock: true }))
|
|
4443
4500
|
: null
|
|
4444
|
-
//
|
|
4501
|
+
// 编辑区容器:pathBar 和页签行现在都由焦点分组自己渲染,这里只放编辑正文与各种浮层。
|
|
4502
|
+
|
|
4445
4503
|
const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
|
|
4446
4504
|
dapBar,
|
|
4447
4505
|
addCfgBtn,
|
|
@@ -4466,11 +4524,11 @@ export function EditorView(props) {
|
|
|
4466
4524
|
: null
|
|
4467
4525
|
// 底部状态栏共用一整条:挂在 mainCol 层级(所有分组之下),不随焦点分组搬家(ddj 2026-10-16)
|
|
4468
4526
|
const fullView = createPortal(React.createElement(React.Fragment, null, pathBar, sideHintEl, diskBanner(), editorArea), fullHost, 'edrv-full-editor')
|
|
4527
|
+
// 页签行已并入焦点分组自身(EditorPane 内一行),不再有独立的顶部工具条。
|
|
4469
4528
|
const mainCol = React.createElement('div', { className: 'edrv-main-col edrv-workbench', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
4470
|
-
React.createElement('div', { className: 'edrv-workbench-tools' }, tabRow),
|
|
4471
4529
|
renderTree(workbench.tree), fullView,
|
|
4472
4530
|
statusBar,
|
|
4473
|
-
workbench.floats.map((item, layer) => React.createElement(FloatingEditor, { key: item.group, owner: String(scope), group: item.group, rect: item, layer, focused: item.group === workbench.focused, onBounds: floatBounds, onFocus: selectGroup, onReturn: returnPane }, renderGroup(item.group))),
|
|
4531
|
+
workbench.floats.map((item, layer) => React.createElement(FloatingEditor, { key: item.group, owner: String(scope), group: item.group, rect: item, layer, focused: item.group === workbench.focused, onBounds: floatBounds, onFocus: selectGroup, onReturn: returnPane }, renderGroup(item.group, true))),
|
|
4474
4532
|
dragPreview ? createPortal(React.createElement(React.Fragment, null,
|
|
4475
4533
|
dragPreview.rect ? React.createElement('div', { className: 'edrv-drag-overlay', style: dragPreview.rect }, React.createElement('span', { className: 'edrv-drop-label' }, dragPreview.label)) : null,
|
|
4476
4534
|
React.createElement('div', { className: 'edrv-drag-ghost', style: { left: dragPreview.point.x + 12, top: dragPreview.point.y + 12 } }, baseNameOf(dragPreview.path) + ' · ' + dragPreview.label)), document.body) : null)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Official session-row slots; no DOM patching or session activation. @author ddj 2026年10月20号 */
|
|
2
|
+
import React, { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
3
|
+
import type { McpPhase } from '../../shared/mcp.js'
|
|
4
|
+
import { McpSessionState, type SessionMcpView } from '../mcpSessionState.js'
|
|
5
|
+
|
|
6
|
+
const labels: Record<McpPhase, string> = {
|
|
7
|
+
idle: '待使用,尚未启动', preparing: '读取项目配置', queued: '等待 MCP 启动名额', loading: '加载 MCP 插件模块',
|
|
8
|
+
starting: '等待 MCP 插件启动', stopping: '释放旧 MCP 实例', ready: '初始化完成,连接健康未验证',
|
|
9
|
+
error: '初始化异常', none: '无需加载项目 MCP',
|
|
10
|
+
}
|
|
11
|
+
const busy = new Set<McpPhase>(['preparing', 'queued', 'loading', 'starting', 'stopping'])
|
|
12
|
+
export interface McpRowProps { sessionId: string; store: McpSessionState }
|
|
13
|
+
|
|
14
|
+
/** @private @author ddj 2026年10月20号 @param props Root row identity and shared poller. @returns Stable per-session observation. */
|
|
15
|
+
function useMcpView({ sessionId, store }: McpRowProps): SessionMcpView {
|
|
16
|
+
const subscribe = useCallback((notify: () => void) => store.subscribe(sessionId, notify), [store, sessionId])
|
|
17
|
+
const snapshot = useCallback(() => store.get(sessionId), [store, sessionId])
|
|
18
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @public @author ddj 2026年10月20号 @param view Current observation. @returns Accessible row label; steady states deliberately do not occupy the leading slot. */
|
|
22
|
+
export function mcpRowLabel(view: SessionMcpView): string | undefined {
|
|
23
|
+
if (view.stale) return '项目 MCP 状态暂不可用(上次状态已过期)'
|
|
24
|
+
if (!view.status || (!busy.has(view.status.phase) && view.status.phase !== 'error')) return undefined
|
|
25
|
+
return '项目 MCP:' + labels[view.status.phase]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @public @author ddj 2026年10月20号 @param props Official row sessionId and poller. @returns 16px loading/error marker; official running/approval states retain precedence. */
|
|
29
|
+
export function McpSessionIcon(props: McpRowProps): React.ReactElement | null {
|
|
30
|
+
const view = useMcpView(props)
|
|
31
|
+
const label = mcpRowLabel(view)
|
|
32
|
+
if (!label) return null
|
|
33
|
+
const warning = view.stale || view.status?.phase === 'error'
|
|
34
|
+
return React.createElement('span', {
|
|
35
|
+
role: 'img', 'aria-label': label, title: label,
|
|
36
|
+
style: { display: 'inline-flex', width: 16, height: 16, alignItems: 'center', justifyContent: 'center',
|
|
37
|
+
color: warning ? 'var(--color-warning, #b7791f)' : 'var(--color-text-secondary, currentColor)', fontSize: 13 },
|
|
38
|
+
}, warning ? '⚠' : '◌')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @private @author ddj 2026年10月20号 @param start Stage start. @param end Stage finish or local clock. @returns Human-readable elapsed time, without claiming a timeout. */
|
|
42
|
+
function elapsed(start: number, end: number): string { return Math.max(0, (end - start) / 1000).toFixed(1) + ' 秒' }
|
|
43
|
+
|
|
44
|
+
/** @public @author ddj 2026年10月20号 @param props Official hover sessionId and poller. @returns Project-only diagnostic detail, never transport credentials. */
|
|
45
|
+
export function McpSessionDetail(props: McpRowProps): React.ReactElement {
|
|
46
|
+
const view = useMcpView(props)
|
|
47
|
+
const [now, setNow] = useState(Date.now)
|
|
48
|
+
const status = view.status
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!status || view.stale || !busy.has(status.phase)) return
|
|
51
|
+
const tick = setInterval(() => setNow(Date.now()), 1000)
|
|
52
|
+
return () => clearInterval(tick)
|
|
53
|
+
}, [status, view.stale])
|
|
54
|
+
|
|
55
|
+
const children: React.ReactNode[] = [React.createElement('strong', { key: 'title' }, '项目 MCP')]
|
|
56
|
+
if (view.stale) children.push(React.createElement('div', { key: 'stale', role: 'status' }, '状态暂不可用;以下如有记录,为上次观测结果'))
|
|
57
|
+
if (!status) children.push(React.createElement('div', { key: 'empty' }, view.stale ? '等待状态查询' : '当前无活跃 MCP 实例'))
|
|
58
|
+
else {
|
|
59
|
+
const end = status.finishedAt ?? now
|
|
60
|
+
const completed = status.servers.filter((server) => server.phase === 'ready').length
|
|
61
|
+
const slow = !view.stale && busy.has(status.phase) && now - status.phaseAt > 10000
|
|
62
|
+
children.push(React.createElement('div', { key: 'phase' }, labels[status.phase], slow ? '(耗时较长,不代表失败)' : ''))
|
|
63
|
+
children.push(React.createElement('div', { key: 'time' }, `${completed}/${status.servers.length} 已初始化 · 总耗时 ${elapsed(status.startedAt, end)} · 当前阶段 ${elapsed(status.phaseAt, end)}`))
|
|
64
|
+
if (status.configMs !== undefined) children.push(React.createElement('div', { key: 'config' }, `配置准备 ${elapsed(0, status.configMs)}`))
|
|
65
|
+
if (status.error) children.push(React.createElement('div', { key: 'error' }, status.error))
|
|
66
|
+
for (const server of status.servers) children.push(React.createElement('div', { key: 'server:' + server.serverName },
|
|
67
|
+
`${server.serverName}:${labels[server.phase]}${server.reused ? ' · 复用已有连接' : ''}${server.phase === 'idle' ? '' : ' · ' + elapsed(server.startedAt, server.finishedAt ?? now)}`,
|
|
68
|
+
server.queuedMs === undefined ? '' : ' · 排队 ' + elapsed(0, server.queuedMs),
|
|
69
|
+
server.connectMs === undefined ? '' : ' · 初始化 ' + elapsed(0, server.connectMs),
|
|
70
|
+
server.firstCallMs === undefined ? '' : ' · 首次工具执行 ' + elapsed(0, server.firstCallMs),
|
|
71
|
+
server.error ? ' · ' + server.error : ''))
|
|
72
|
+
}
|
|
73
|
+
children.push(React.createElement('small', { key: 'scope' }, '仅 VSCodeMode 管理的项目 MCP;不含全局或 preset MCP。'))
|
|
74
|
+
return React.createElement('section', { 'aria-label': '项目 MCP 状态', style: { display: 'grid', gap: 4, maxWidth: 320, overflowWrap: 'anywhere', fontSize: 12 } }, children)
|
|
75
|
+
}
|
|
@@ -10,6 +10,7 @@ import React from 'react'
|
|
|
10
10
|
import { rpc } from '../rpc.js'
|
|
11
11
|
import type { MpcProject, MpcServer } from '../../shared/mcp.js'
|
|
12
12
|
import { configOf } from '../mcpForm.js'
|
|
13
|
+
import { parseMcpPolicy } from '../../shared/mcp.js'
|
|
13
14
|
import { McpState } from '../mcpState.js'
|
|
14
15
|
import { statusOf, statusTone } from '../../shared/mcpStatus.js'
|
|
15
16
|
import '../styles/mcp.css'
|
|
@@ -25,10 +26,10 @@ import { PerfSettings } from './PerfSettings.js'
|
|
|
25
26
|
import { IntegrationSettings } from './IntegrationSettings.js'
|
|
26
27
|
import { AiSettings } from './AiSettings.js'
|
|
27
28
|
|
|
28
|
-
const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
|
|
29
|
+
const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '', startup: 'on-demand', sharing: 'agent', startupTimeoutMs: 60000 }
|
|
29
30
|
|
|
30
31
|
/** @private @author ddj 2026年10月08号 @param props Server snapshot, page-wide busy state and actions. @returns Lifecycle-evidence card; enabled does not imply online. */
|
|
31
|
-
function ServerCard({ server, busy, onRefresh, onToggle, onRemove }) {
|
|
32
|
+
function ServerCard({ server, busy, onRefresh, onToggle, onRemove, onConnect, onPolicy }) {
|
|
32
33
|
return React.createElement('article', { className: 'vsm-mcp-card' },
|
|
33
34
|
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
34
35
|
React.createElement('div', { className: 'vsm-mcp-title' }, React.createElement('span', { className: 'vsm-mcp-dot ' + statusTone(server) }), server.serverName),
|
|
@@ -39,12 +40,33 @@ function ServerCard({ server, busy, onRefresh, onToggle, onRemove }) {
|
|
|
39
40
|
),
|
|
40
41
|
),
|
|
41
42
|
React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport,
|
|
42
|
-
server.instanceCount !== undefined ? ' · ' + server.instanceCount + '
|
|
43
|
+
server.instanceCount !== undefined ? ' · ' + server.instanceCount + ' 个连接实例' : '',
|
|
44
|
+
server.sessionCount !== undefined ? ' · ' + server.sessionCount + ' 个会话使用' : ''),
|
|
45
|
+
server.policy && React.createElement(ProjectPolicy, { server, busy, onConnect, onPolicy }),
|
|
43
46
|
server.error && React.createElement('div', { className: 'vsm-mcp-error' }, server.error),
|
|
44
47
|
React.createElement('div', { className: 'vsm-mcp-tools' }, server.tools.map((tool) => React.createElement('span', { key: tool.name, className: 'vsm-mcp-chip', title: tool.description || tool.name }, tool.name))),
|
|
45
48
|
)
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** @private @author ddj 2026年10月20号 @param props Project policy and locked actions. @returns Explicit strategy controls and sharing risk notice. */
|
|
52
|
+
function ProjectPolicy({ server, busy, onConnect, onPolicy }) {
|
|
53
|
+
/** @author ddj 2026年10月20号 @param event Strategy selection. Saves only an explicit user change. */
|
|
54
|
+
const startup = (event) => onPolicy?.(server, { ...server.policy, startup: event.target.value })
|
|
55
|
+
/** @author ddj 2026年10月20号 @param event Sharing selection. Requires acknowledgement before state sharing. */
|
|
56
|
+
const sharing = (event) => {
|
|
57
|
+
const mode = event.target.value
|
|
58
|
+
if (mode === 'workspace' && !window.confirm('同一工作区的会话将共享此 MCP 的连接和内部状态;工具串行不等于业务状态隔离。确认服务适合共享?')) return
|
|
59
|
+
onPolicy?.(server, { ...server.policy, sharing: mode })
|
|
60
|
+
}
|
|
61
|
+
return React.createElement('div', { className: 'vsm-mcp-meta' },
|
|
62
|
+
React.createElement('label', null, '启动 ', React.createElement('select', { disabled: !!busy, value: server.policy.startup, onChange: startup },
|
|
63
|
+
React.createElement('option', { value: 'on-demand' }, '按需启动'), React.createElement('option', { value: 'background' }, '后台预热'))),
|
|
64
|
+
React.createElement('label', null, ' 连接 ', React.createElement('select', { disabled: !!busy, value: server.policy.sharing, onChange: sharing },
|
|
65
|
+
React.createElement('option', { value: 'agent' }, '逐会话隔离'), React.createElement('option', { value: 'workspace' }, '工作区共享(共享状态)'))),
|
|
66
|
+
React.createElement('button', { disabled: !!busy || !server.enabled, onClick: onConnect }, '连接当前活跃会话'),
|
|
67
|
+
React.createElement('small', null, '默认按需启动,不阻塞新对话;首次使用仍需等待。共享仅显式启用。'))
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
/** 按项目标题或路径过滤,大小写不敏感。 */
|
|
49
71
|
export function filterProjects(projects, query) {
|
|
50
72
|
const needle = String(query ?? '').trim().toLowerCase()
|
|
@@ -94,7 +116,7 @@ function ProjectPicker({ projects, value, onChange }) {
|
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
/** @private @author ddj 2026年10月08号 @param props Project view, shared busy lock and actions. @returns Project cards with actions disabled for any active mutation. */
|
|
97
|
-
function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
|
|
119
|
+
function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove, onConnect, onPolicy }) {
|
|
98
120
|
const head = React.createElement('div', { className: 'vsm-project-head' },
|
|
99
121
|
React.createElement('div', { className: 'vsm-project-title' }, React.createElement('span', { className: 'vsm-project-icon' }, '▣'), React.createElement('div', null, React.createElement('span', { className: 'vsm-project-name' }, project.title), React.createElement('span', { className: 'vsm-project-path' }, project.workspacePath))),
|
|
100
122
|
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: !!busy, onClick: () => onAdd(project) }, '+ 添加 MCP'),
|
|
@@ -103,7 +125,7 @@ function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
|
|
|
103
125
|
if (project.missingDir) body = React.createElement('div', { className: 'vsm-project-empty' }, '项目目录已不存在,无法管理 MCP')
|
|
104
126
|
else if (project.fileError) body = React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, project.fileError)
|
|
105
127
|
else if (!project.servers.length) body = React.createElement('div', { className: 'vsm-project-empty' }, '此项目未配置 MCP')
|
|
106
|
-
else body = project.servers.map((server) => React.createElement(ServerCard, { key: server.serverName, server, busy, onRefresh: (id) => onRefresh(project, server, id), onToggle: () => onToggle(project, server), onRemove: (id) => onRemove(project, server, id) }))
|
|
128
|
+
else body = project.servers.map((server) => React.createElement(ServerCard, { key: server.serverName, server, busy, onConnect: () => onConnect(project, server), onPolicy: (server, policy) => onPolicy(project, server, policy), onRefresh: (id) => onRefresh(project, server, id), onToggle: () => onToggle(project, server), onRemove: (id) => onRemove(project, server, id) }))
|
|
107
129
|
return React.createElement('section', { className: 'vsm-project' }, head, body)
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -513,7 +535,7 @@ function McpManagePanel(props) {
|
|
|
513
535
|
const form = showForm
|
|
514
536
|
? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: () => saveGlobal(closeGlobalForm), close: closeGlobalForm })
|
|
515
537
|
: projectForm
|
|
516
|
-
? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: () => saveProject(projectForm.workspacePath, closeProjectForm), close: closeProjectForm })
|
|
538
|
+
? React.createElement(McpForm, { project: true, title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: () => saveProject(projectForm.workspacePath, closeProjectForm), close: closeProjectForm })
|
|
517
539
|
: null
|
|
518
540
|
return React.createElement(React.Fragment, null,
|
|
519
541
|
React.createElement('div', { className: 'vsm-mcp-subhead' },
|
|
@@ -538,7 +560,7 @@ function manageBody(props, tab, path, select, add) {
|
|
|
538
560
|
const project = projects.find((item) => item.workspacePath === path)
|
|
539
561
|
return React.createElement(React.Fragment, null,
|
|
540
562
|
React.createElement(ProjectPicker, { projects, value: path, onChange: select }),
|
|
541
|
-
project ? React.createElement(ProjectGroup, { project, busy, onAdd: add, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
|
|
563
|
+
project ? React.createElement(ProjectGroup, { project, busy, onAdd: add, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject, onConnect: props.connectProject, onPolicy: props.policyProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
|
|
542
564
|
)
|
|
543
565
|
}
|
|
544
566
|
|
|
@@ -593,7 +615,7 @@ function projectAction(state, path, name, method, extra = {}) {
|
|
|
593
615
|
}
|
|
594
616
|
|
|
595
617
|
/** @private @author ddj 2026年10月08号 @param ref Mounted controller reference. @returns Global/project card actions sharing a synchronous lock. */
|
|
596
|
-
function cardActions(ref) {
|
|
618
|
+
function cardActions(ref, getSession) {
|
|
597
619
|
/** @author ddj 2026年10月08号 @param id Global identity. @returns Locked refresh. */
|
|
598
620
|
const refreshGlobal = (id) => globalAction(ref.current, id, 'mcp.refresh', { id })
|
|
599
621
|
/** @author ddj 2026年10月08号 @param server Current configuration. @returns Locked enable change. */
|
|
@@ -606,7 +628,15 @@ function cardActions(ref) {
|
|
|
606
628
|
const toggleProject = (project, server) => projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectToggle', { enabled: !server.enabled })
|
|
607
629
|
/** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Confirmed locked deletion. */
|
|
608
630
|
const removeProject = (project, server) => !ref.current?.view.busy && window.confirm('确认删除此项目的 MCP「' + server.serverName + '」?') && projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectRemove')
|
|
609
|
-
|
|
631
|
+
/** @author ddj 2026年10月20号 @param project Owner workspace. @param server Enabled service. @returns Explicit connection for an already-active current session. */
|
|
632
|
+
const connectProject = (project, server) => {
|
|
633
|
+
const sessionId = getSession?.()
|
|
634
|
+
if (!sessionId) return ref.current?.mutate('connect', async () => ({ ok: false, error: '请先打开此项目的活跃会话' }), () => undefined)
|
|
635
|
+
return projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectConnect', { sessionId })
|
|
636
|
+
}
|
|
637
|
+
/** @author ddj 2026年10月20号 @param project Owner. @param server Masked editable config. @param policy Explicit user strategy. @returns Serialized strategy persistence. */
|
|
638
|
+
const policyProject = (project, server, policy) => projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectSave', { config: server.config, policy })
|
|
639
|
+
return { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject, connectProject, policyProject }
|
|
610
640
|
}
|
|
611
641
|
|
|
612
642
|
/** @private @author ddj 2026年10月08号 @param state Controller. @param draft Editable fields. @param setDraft Draft setter. @param path Optional project target. @param close Success-only close callback. @returns Locked save, validation included. */
|
|
@@ -614,7 +644,7 @@ function saveDraft(state, draft, setDraft, path, close) {
|
|
|
614
644
|
/** @author ddj 2026年10月08号 @returns Validated save result. @throws Malformed arguments/headers. */
|
|
615
645
|
const request = () => {
|
|
616
646
|
const config = configOf(draft)
|
|
617
|
-
return path === null ? rpc('mcp.save', { config }) : rpc('mcp.projectSave', { workspacePath: path, serverName: config.serverName, config })
|
|
647
|
+
return path === null ? rpc('mcp.save', { config }) : rpc('mcp.projectSave', { workspacePath: path, serverName: config.serverName, config, policy: parseMcpPolicy({ startup: draft.startup, sharing: draft.sharing, startupTimeoutMs: Number(draft.startupTimeoutMs) }) })
|
|
618
648
|
}
|
|
619
649
|
/** @author ddj 2026年10月08号 @param result Save result. @description Update current view and only then reset/close the form. */
|
|
620
650
|
const commit = (result) => {
|
|
@@ -649,7 +679,7 @@ export function McpSettings(props) {
|
|
|
649
679
|
return cleanup
|
|
650
680
|
}
|
|
651
681
|
React.useEffect(selectTab, [tab])
|
|
652
|
-
const { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject } = cardActions(ref)
|
|
682
|
+
const { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject, connectProject, policyProject } = cardActions(ref, props?.getMcpSession)
|
|
653
683
|
/** @author ddj 2026年10月08号 @param close Success-only close callback. @returns Global save completion. */
|
|
654
684
|
const saveGlobal = (close) => saveDraft(ref.current, draft, setDraft, null, close)
|
|
655
685
|
/** @author ddj 2026年10月08号 @param path Target project. @param close Success-only close callback. @returns Project save completion. */
|
|
@@ -662,7 +692,7 @@ export function McpSettings(props) {
|
|
|
662
692
|
if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
|
|
663
693
|
else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry, getShortcuts })
|
|
664
694
|
else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
|
|
665
|
-
saveGlobal, saveProject,
|
|
695
|
+
saveGlobal, saveProject, connectProject, policyProject,
|
|
666
696
|
refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
|
|
667
697
|
else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
|
|
668
698
|
else if (tab === 'lsp') body = React.createElement(LspSettings, null)
|
|
@@ -677,7 +707,7 @@ export function McpSettings(props) {
|
|
|
677
707
|
}
|
|
678
708
|
|
|
679
709
|
/** @private @author ddj 2026年10月08号 @param props Draft, shared busy lock, edit/save/close callbacks. @returns Lossless argument/header form; failed save stays open. */
|
|
680
|
-
function McpForm({ title, draft, busy, edit, save, close }) {
|
|
710
|
+
function McpForm({ title, draft, busy, edit, save, close, project }) {
|
|
681
711
|
/** @author ddj 2026年10月08号 @param key Draft field. @returns Short input change callback. */
|
|
682
712
|
const change = (key) => (event) => edit(key, event.target.value)
|
|
683
713
|
/** @author ddj 2026年10月08号 @param key Draft field. @param placeholder Input hint. @returns Disabled-while-busy input. */
|
|
@@ -697,6 +727,10 @@ function McpForm({ title, draft, busy, edit, save, close }) {
|
|
|
697
727
|
React.createElement('label', null, '名称', input('serverName', '例如 codegraph')),
|
|
698
728
|
React.createElement('label', null, '传输方式', React.createElement('select', { disabled: !!busy, value: draft.transport, onChange: change('transport') }, React.createElement('option', { value: 'stdio' }, 'stdio'), React.createElement('option', { value: 'streamable-http' }, 'streamable-http'))),
|
|
699
729
|
fields,
|
|
730
|
+
project && React.createElement('label', null, '启动策略', React.createElement('select', { disabled: !!busy, value: draft.startup, onChange: change('startup') }, React.createElement('option', { value: 'on-demand' }, '按需启动(默认)'), React.createElement('option', { value: 'background' }, '后台预热'))),
|
|
731
|
+
project && React.createElement('label', null, '连接模式', React.createElement('select', { disabled: !!busy, value: draft.sharing, onChange: change('sharing') }, React.createElement('option', { value: 'agent' }, '逐会话隔离(默认)'), React.createElement('option', { value: 'workspace' }, '工作区共享(共享服务内部状态)'))),
|
|
732
|
+
project && React.createElement('label', null, '初始化预算(毫秒,1000–300000)', input('startupTimeoutMs', '60000')),
|
|
733
|
+
project && React.createElement('small', null, '新对话不等待连接。共享必须确认服务适合共享状态;不为 CodeGraph 自动启用。'),
|
|
700
734
|
React.createElement('div', { className: 'vsm-mcp-dialog-actions' }, React.createElement('button', { disabled: !!busy, onClick: close }, '取消'), React.createElement('button', { className: 'vsm-primary', disabled: !!busy, onClick: save }, '保存配置')),
|
|
701
735
|
))
|
|
702
736
|
}
|
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
|
+
}
|