dsh-vscode-mode 0.1.15 → 0.1.16
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 +13 -3
- package/lib/client.js +140 -33
- package/lib/client.js.map +1 -1
- package/lib/index.js +386 -198
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +91 -0
- package/src/client/fileOpeners.ts +2 -1
- package/src/client/index.ts +18 -10
- package/src/client/openPathRouter.ts +5 -5
- package/src/client/settingsContext.ts +7 -17
- package/src/client/styles/mcp.css +8 -0
- package/src/client/ui/McpSettings.ts +36 -1
- package/src/compat.ts +146 -0
- package/src/fileOpenSettings.ts +58 -6
- package/src/index.ts +33 -5
- package/src/mcp.ts +8 -9
- package/src/mcpIsolation.ts +6 -12
- package/src/routes.ts +26 -7
- package/src/rpc.ts +2 -0
- package/src/shared/compat.ts +20 -0
- package/src/shared/rpc.ts +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 兼容层:设置桥选择 / slot 安全注册 / openPath 链式补丁 / 外部插件常量。
|
|
3
|
+
* 纯逻辑与薄 ctx 胶水,node 环境可单测(不依赖 DOM)。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 本插件包名(与 host compat.PLUGIN_NAME 对应)。 */
|
|
8
|
+
export const PLUGIN_NAME = 'dsh-vscode-mode'
|
|
9
|
+
/** 外部侧栏插件名(dsh-web-ui-all 家族的文件打开能力提供方)。 */
|
|
10
|
+
export const SIDEBAR_PLUGIN = 'dsh-better-sidebar'
|
|
11
|
+
|
|
12
|
+
/** 设置 scope 的跨组件上下文形状(与官方 settingsScope 绑定的最小面)。 */
|
|
13
|
+
export interface SettingsScopeLike {
|
|
14
|
+
getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown }; writable?: boolean }
|
|
15
|
+
subscribe: (listener: () => void) => () => void
|
|
16
|
+
set: (field: string, value: unknown) => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 设置桥三级优先:webUiSettings(linxin666 兼容桥)→ settingsScope(官方)→ 无。
|
|
21
|
+
* 桥绑定抛错时降级到下一级,不让单个桥异常拖垮设置装配。
|
|
22
|
+
* @author ddj 2026年08月24号
|
|
23
|
+
* @param ctx 客户端服务上下文
|
|
24
|
+
* @returns 激活的桥名称与绑定 scope
|
|
25
|
+
*/
|
|
26
|
+
export function pickSettingsBinder(ctx: { get: (name: string) => unknown }): { service: string; scope: SettingsScopeLike | undefined } {
|
|
27
|
+
for (const service of ['webUiSettings', 'settingsScope']) {
|
|
28
|
+
try {
|
|
29
|
+
const binder = ctx.get(service) as { bind?: (spec: { namespace: string }) => SettingsScopeLike } | undefined
|
|
30
|
+
const scope = binder?.bind?.({ namespace: PLUGIN_NAME })
|
|
31
|
+
if (scope) return { service, scope }
|
|
32
|
+
} catch {
|
|
33
|
+
/* 桥异常时降级到下一级 */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { service: 'none', scope: undefined }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** slot 注册描述(对齐 ctx.slots.register 的形状)。 */
|
|
40
|
+
export interface SlotSpec {
|
|
41
|
+
name: string
|
|
42
|
+
id: string
|
|
43
|
+
order?: number
|
|
44
|
+
label?: string
|
|
45
|
+
inject?: (sessionId: string) => object
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 跨版本 slot 安全注册:封装 ctx.slots.inject 等待声明模式(新老 DSH 通用),
|
|
50
|
+
* 服务缺失或注册抛错时降级为警告,不让单条 slot 拖垮装配。
|
|
51
|
+
* 注意:方法必须挂在服务对象上调用(slots.register 是依赖 this.records 的类方法,
|
|
52
|
+
* 解构后 this 丢失会静默失败),故禁止解构。
|
|
53
|
+
* @author ddj 2026年08月24号
|
|
54
|
+
* @param ctx 客户端上下文(slots 服务)
|
|
55
|
+
* @param spec slot 描述
|
|
56
|
+
* @param render 渲染函数
|
|
57
|
+
*/
|
|
58
|
+
export function registerSlotSafely(ctx: { slots?: { inject?: (name: string, register: () => unknown) => unknown; register?: (...args: unknown[]) => unknown } }, spec: SlotSpec, render: (props: unknown) => unknown): void {
|
|
59
|
+
const slots = ctx?.slots as { inject: (name: string, register: () => unknown) => unknown; register: (...args: unknown[]) => unknown } | undefined
|
|
60
|
+
if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|
|
61
|
+
console.warn('[' + PLUGIN_NAME + '] slots 服务不可用,跳过 slot ' + spec.name)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
slots.inject(spec.name, () => slots.register(spec, render))
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn('[' + PLUGIN_NAME + '] slot ' + spec.name + ' 注册失败(' + String(error) + '),已跳过')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 链式补丁对象方法:保存原实现、装入包装实现,dispose 恢复。
|
|
73
|
+
* 恢复带归属校验(仅当当前实现仍是本包装时还原),多插件补丁按栈序互不踩踏。
|
|
74
|
+
* @author ddj 2026年08月24号
|
|
75
|
+
* @param owner 目标对象
|
|
76
|
+
* @param key 方法名
|
|
77
|
+
* @param wrapper 包装实现(original 为补丁前的实现)
|
|
78
|
+
* @returns 恢复函数(幂等)
|
|
79
|
+
*/
|
|
80
|
+
export function patchMethod<T extends object, K extends keyof T>(
|
|
81
|
+
owner: T,
|
|
82
|
+
key: K,
|
|
83
|
+
wrapper: (original: T[K], ...args: never[]) => unknown,
|
|
84
|
+
): () => void {
|
|
85
|
+
const original = owner[key]
|
|
86
|
+
const patched = ((...args: never[]) => wrapper(original, ...args)) as T[K]
|
|
87
|
+
owner[key] = patched
|
|
88
|
+
return () => {
|
|
89
|
+
if (owner[key] === patched) owner[key] = original
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* dsh-vscode-mode client — 文件打开器注册表与自动选择。
|
|
3
3
|
* @author ddj 2026年08月24号
|
|
4
4
|
*/
|
|
5
|
+
import { SIDEBAR_PLUGIN } from './compat.js'
|
|
5
6
|
|
|
6
7
|
export const AUTO_OPEN_TOOL = 'auto'
|
|
7
8
|
export const SYSTEM_OPEN_TOOL = 'system'
|
|
8
9
|
export const VSCODE_OPEN_TOOL = 'dsh-vscode-mode'
|
|
9
|
-
export const SIDEBAR_OPEN_TOOL =
|
|
10
|
+
export const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN
|
|
10
11
|
|
|
11
12
|
export interface FileOpenContext {
|
|
12
13
|
sessionId?: string
|
package/src/client/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ import { rpc } from './rpc.js'
|
|
|
20
20
|
import { createFileOpenerRegistry, scanSidebar, type FileOpenContext } from './fileOpeners.js'
|
|
21
21
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
22
22
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
23
|
-
import { SettingsContext
|
|
23
|
+
import { SettingsContext } from './settingsContext.js'
|
|
24
|
+
import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
|
|
25
|
+
import type { CompatAdapter } from '../shared/compat.js'
|
|
24
26
|
|
|
25
27
|
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'settingsScope', 'webUiSettings']
|
|
26
28
|
|
|
@@ -36,9 +38,17 @@ export function apply(ctx: any): void {
|
|
|
36
38
|
const workspaces = ctx.get('workspaces')
|
|
37
39
|
const sessions = ctx.get('sessions')
|
|
38
40
|
const originalOpenPath = workspaces?.openPath
|
|
39
|
-
const
|
|
41
|
+
const binder = pickSettingsBinder(ctx)
|
|
42
|
+
const settings = binder.scope
|
|
40
43
|
let selected = autoValue('auto')
|
|
41
44
|
|
|
45
|
+
/** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
|
|
46
|
+
const compatSummary = (): CompatAdapter[] => [
|
|
47
|
+
{ name: '设置桥', active: settings !== undefined, note: settings !== undefined ? '使用 ' + binder.service + ' 桥' : '未绑定设置服务(fileOpenTool 持久化不可用)' },
|
|
48
|
+
{ name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开' },
|
|
49
|
+
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
50
|
+
]
|
|
51
|
+
|
|
42
52
|
ctx.provide('fileOpeners', registry)
|
|
43
53
|
ctx.effect(() => registry.register(vscodeOpener()), 'vscode-mode: file opener')
|
|
44
54
|
ctx.effect(() => registry.register({
|
|
@@ -76,25 +86,23 @@ export function apply(ctx: any): void {
|
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
// 中央「文件编辑」页签:类 VSCode 编辑器(顶部=文件页签+搜索框,差异 UI=文件底部圆角悬浮框)
|
|
79
|
-
ctx
|
|
89
|
+
registerSlotSafely(ctx, {
|
|
80
90
|
name: 'conversation.view',
|
|
81
91
|
id: 'edrv-editor',
|
|
82
92
|
order: 5,
|
|
83
93
|
label: '文件编辑',
|
|
84
94
|
inject: (sessionId: string) => ({ sessionId }),
|
|
85
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule })))
|
|
95
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule })))
|
|
86
96
|
|
|
87
97
|
// header 差异角标:仅当前工作区存在差异时渲染
|
|
88
|
-
ctx
|
|
89
|
-
|
|
90
|
-
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)),
|
|
91
|
-
))
|
|
98
|
+
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '差异' },
|
|
99
|
+
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)))
|
|
92
100
|
|
|
93
101
|
// 设置页中的 VSCodeMode 专属页签,MCP 管理作为该页签的内部子 Tab。
|
|
94
|
-
ctx
|
|
102
|
+
registerSlotSafely(ctx, {
|
|
95
103
|
name: 'settings.section',
|
|
96
104
|
id: 'vscode-mode',
|
|
97
105
|
order: 30,
|
|
98
106
|
label: 'VSCodeMode',
|
|
99
|
-
}, () => React.createElement(SettingsContext.Provider, { value: settings }, React.createElement(McpSettings, { openerRegistry: registry })))
|
|
107
|
+
}, () => React.createElement(SettingsContext.Provider, { value: settings }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
|
|
100
108
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — DSH 对话文件链接的统一打开路由。
|
|
3
|
-
*
|
|
3
|
+
* openPath 补丁经 compat.patchMethod 安装(链式恢复,多插件补丁互不踩踏)。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
4
5
|
*/
|
|
6
|
+
import { patchMethod } from './compat.js'
|
|
5
7
|
import { openEditorView } from './events.js'
|
|
6
8
|
import { AUTO_OPEN_TOOL, VSCODE_OPEN_TOOL, type FileOpenContext, type FileOpenerRegistry, isAvailable, selectOpener } from './fileOpeners.js'
|
|
7
9
|
|
|
@@ -20,8 +22,7 @@ export interface OpenPathRouterOptions {
|
|
|
20
22
|
* @returns 恢复原方法的 disposer
|
|
21
23
|
*/
|
|
22
24
|
export function installOpenPathRouter(options: OpenPathRouterOptions): () => void {
|
|
23
|
-
const
|
|
24
|
-
const open = async (path: string): Promise<void> => {
|
|
25
|
+
const open = async (original: (path: string) => Promise<void>, path: string): Promise<void> => {
|
|
25
26
|
const context = options.context()
|
|
26
27
|
const opener = selectOpener(options.registry, options.selected())
|
|
27
28
|
if (!opener || !isAvailable(opener)) return original.call(options.workspaces, path)
|
|
@@ -33,8 +34,7 @@ export function installOpenPathRouter(options: OpenPathRouterOptions): () => voi
|
|
|
33
34
|
else throw error
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
options.workspaces
|
|
37
|
-
return () => { options.workspaces.openPath = original }
|
|
37
|
+
return patchMethod(options.workspaces, 'openPath', open as never)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -1,31 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 设置 scope 的跨组件上下文。
|
|
3
|
-
*
|
|
3
|
+
* 绑定逻辑(webUiSettings 兼容桥 → 官方 settingsScope 降级)统一由 compat 层提供。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
4
5
|
*/
|
|
5
6
|
import React from 'react'
|
|
7
|
+
import { pickSettingsBinder, type SettingsScopeLike } from './compat.js'
|
|
6
8
|
|
|
7
|
-
export
|
|
8
|
-
getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown }; writable?: boolean }
|
|
9
|
-
subscribe: (listener: () => void) => () => void
|
|
10
|
-
set: (field: string, value: unknown) => Promise<void>
|
|
11
|
-
}
|
|
9
|
+
export type { SettingsScopeLike }
|
|
12
10
|
|
|
13
11
|
export const SettingsContext = React.createContext<SettingsScopeLike | undefined>(undefined)
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
|
-
* 从 Cordis 服务绑定 VSCodeMode 设置 scope
|
|
17
|
-
* @author ddj 2026年08月24号
|
|
18
|
-
* @param ctx 客户端上下文
|
|
19
|
-
* @returns 设置 scope 或 undefined
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* 绑定官方设置 scope,优先使用 web-ui 兼容桥。
|
|
14
|
+
* 从 Cordis 服务绑定 VSCodeMode 设置 scope(兼容桥优先,官方次之)。
|
|
23
15
|
* @author ddj 2026年08月24号
|
|
24
16
|
* @param ctx 客户端服务上下文
|
|
25
|
-
* @returns 设置 scope
|
|
17
|
+
* @returns 设置 scope 或 undefined
|
|
26
18
|
*/
|
|
27
19
|
export function getSettingsScope(ctx: { get: (name: string) => unknown }): SettingsScopeLike | undefined {
|
|
28
|
-
|
|
29
|
-
const binder = service as { bind?: (spec: { namespace: string }) => SettingsScopeLike } | undefined
|
|
30
|
-
return binder?.bind?.({ namespace: 'dsh-vscode-mode' })
|
|
20
|
+
return pickSettingsBinder(ctx).scope
|
|
31
21
|
}
|
|
@@ -93,3 +93,11 @@
|
|
|
93
93
|
.vsm-mcp-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
|
|
94
94
|
.vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}
|
|
95
95
|
.vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
96
|
+
|
|
97
|
+
/* 兼容性小节 */
|
|
98
|
+
.vsm-compat-h3{margin:16px 0 2px;font-size:14px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
99
|
+
.vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}
|
|
100
|
+
.vsm-compat-item.ok .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-success-primary,#38bd75)}
|
|
101
|
+
.vsm-compat-item.warn .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
102
|
+
.vsm-compat-name{font-weight:500;min-width:0}
|
|
103
|
+
.vsm-compat-note{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:right;min-width:0}
|
|
@@ -153,9 +153,43 @@ function GeneralSettings({ registry }) {
|
|
|
153
153
|
)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/** 兼容性小节:host 报告(RPC compat)+ 客户端本地适配项。 */
|
|
157
|
+
function CompatSection({ getSummary }) {
|
|
158
|
+
const [report, setReport] = React.useState(null)
|
|
159
|
+
const [error, setError] = React.useState('')
|
|
160
|
+
React.useEffect(() => {
|
|
161
|
+
let alive = true
|
|
162
|
+
rpc('compat', {}).then((result) => {
|
|
163
|
+
if (!alive) return
|
|
164
|
+
if (!result.ok) setError(String(result.error))
|
|
165
|
+
else setReport(result.report)
|
|
166
|
+
}).catch((e) => { if (alive) setError(String(e)) })
|
|
167
|
+
return () => { alive = false }
|
|
168
|
+
}, [])
|
|
169
|
+
const renderItems = (list) => (list ?? []).map((item) => React.createElement('div', { key: item.name, className: 'vsm-compat-item ' + (item.active ? 'ok' : 'warn') },
|
|
170
|
+
React.createElement('span', { className: 'vsm-compat-name' }, item.name),
|
|
171
|
+
React.createElement('span', { className: 'vsm-compat-note' }, item.note ?? (item.active ? '正常' : '未生效')),
|
|
172
|
+
))
|
|
173
|
+
const warnings = report?.warnings ?? []
|
|
174
|
+
return React.createElement('section', { className: 'vsm-general-page' },
|
|
175
|
+
React.createElement('h2', null, '兼容性'),
|
|
176
|
+
React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' + (report?.pluginVersion ? '(插件版本 ' + report.pluginVersion + ')' : '')),
|
|
177
|
+
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
178
|
+
React.createElement('h3', { className: 'vsm-compat-h3' }, '外部插件与服务'),
|
|
179
|
+
renderItems(report?.external),
|
|
180
|
+
React.createElement('h3', { className: 'vsm-compat-h3' }, '客户端适配'),
|
|
181
|
+
renderItems(getSummary?.() ?? []),
|
|
182
|
+
React.createElement('h3', { className: 'vsm-compat-h3' }, '护栏'),
|
|
183
|
+
renderItems(report?.guards),
|
|
184
|
+
React.createElement('h3', { className: 'vsm-compat-h3' }, '警告'),
|
|
185
|
+
warnings.length ? warnings.map((w, index) => React.createElement('div', { key: index, className: 'vsm-mcp-error vsm-mcp-banner' }, w)) : React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '未检测到兼容性问题'), React.createElement('span', { className: 'vsm-compat-note' }, '')),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
/** MCP 管理主面板。 */
|
|
157
190
|
export function McpSettings(props) {
|
|
158
191
|
const openerRegistry = props?.openerRegistry ?? { list: () => [] }
|
|
192
|
+
const compatSummary = props?.compatSummary ?? (() => [])
|
|
159
193
|
const [servers, setServers] = React.useState<MpcServer[]>([])
|
|
160
194
|
const [projects, setProjects] = React.useState<MpcProject[]>([])
|
|
161
195
|
const [loading, setLoading] = React.useState(true)
|
|
@@ -252,11 +286,12 @@ export function McpSettings(props) {
|
|
|
252
286
|
else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
|
|
253
287
|
else if (tab === 'mine') body = React.createElement('div', null, servers.length ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal })) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP'))
|
|
254
288
|
else if (tab === 'projects') body = projectBody
|
|
289
|
+
else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
|
|
255
290
|
else body = marketBody
|
|
256
291
|
const form = showForm ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: saveGlobal, close: () => setShowForm(false) }) : projectForm ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: saveProject, close: () => setProjectForm(null) }) : null
|
|
257
292
|
return React.createElement('section', { className: 'vsm-mcp-page' },
|
|
258
293
|
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。')), React.createElement('button', { className: 'vsm-primary', onClick: () => { setDraft(EMPTY); setShowForm(true) } }, '+ 添加全局 MCP')),
|
|
259
|
-
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场')),
|
|
294
|
+
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
|
|
260
295
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
261
296
|
body,
|
|
262
297
|
form,
|
package/src/compat.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 兼容层:与其他插件 / DSH 版本的统一探测、适配、护栏与自诊断。
|
|
3
|
+
* 职责:身份常量(包名 / MCP 条目前缀 / 路由前缀)、外部插件探测、路由冲突与重复装配检测、
|
|
4
|
+
* 兼容性报告构建(RPC edrv.compat 与启动日志共用)。
|
|
5
|
+
* 注意:与 mcp.ts 存在模块环(compat 提供身份常量、mcp 提供 entriesOf),
|
|
6
|
+
* 双方仅在函数体内交叉使用导出,模块求值期无依赖,ESM 安全。
|
|
7
|
+
* 作者 ddj 2026-08-24
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { entriesOf } from './mcp.js'
|
|
13
|
+
import { loadSettingsDeps } from './fileOpenSettings.js'
|
|
14
|
+
import type { CompatAdapter, CompatReport } from './shared/compat.js'
|
|
15
|
+
import type { Ctx } from './store.js'
|
|
16
|
+
|
|
17
|
+
export const PLUGIN_NAME = 'dsh-vscode-mode'
|
|
18
|
+
export const MCP_PACKAGE = '@deepseek-ai/dsh-mcp-client'
|
|
19
|
+
/** 项目级 MCP 的 loader entry id 固定前缀(区分 profile 全局 MCP;loader id 不能用 ':',故用 '.')。 */
|
|
20
|
+
export const PROJECT_ENTRY_PREFIX = 'vsm-mcp.'
|
|
21
|
+
/** 旧版项目 MCP 条目前缀(升级残留识别,不显示为全局 MCP)。 */
|
|
22
|
+
export const LEGACY_PROJECT_PREFIX = 'vsm-mcp:'
|
|
23
|
+
/** webServer 路由前缀(RPC 精确路由 + 静态资源前缀共用)。 */
|
|
24
|
+
export const ROUTE_PREFIX = '/edrv'
|
|
25
|
+
|
|
26
|
+
/** 判断新旧格式项目 MCP 条目 id(旧残留不被视为全局 MCP)。 */
|
|
27
|
+
export function isProjectEntryId(id: string): boolean {
|
|
28
|
+
return id.startsWith(PROJECT_ENTRY_PREFIX) || id.startsWith(LEGACY_PROJECT_PREFIX)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 从项目条目 id 解析 workspace hash(新旧格式通用)。 */
|
|
32
|
+
export function entryHash(id: string): string | undefined {
|
|
33
|
+
if (id.startsWith(PROJECT_ENTRY_PREFIX)) return id.slice(PROJECT_ENTRY_PREFIX.length).split('.')[0]
|
|
34
|
+
if (id.startsWith(LEGACY_PROJECT_PREFIX)) return id.slice(LEGACY_PROJECT_PREFIX.length).split(':')[0]
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 读取随包 package.json 版本号(缺失/解析失败降级为空串)。 */
|
|
39
|
+
function pluginVersionOf(): string {
|
|
40
|
+
try {
|
|
41
|
+
const file = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
42
|
+
const pkg = JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }
|
|
43
|
+
return typeof pkg.version === 'string' ? pkg.version : ''
|
|
44
|
+
} catch {
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 探测外部插件/服务适配状态(depsAvailable 由调用方注入,便于测试)。 */
|
|
50
|
+
export function detectExternal(ctx: Ctx, depsAvailable: boolean): CompatAdapter[] {
|
|
51
|
+
const mcpCount = entriesOf(ctx).length
|
|
52
|
+
const settings = ctx.get('settings') as { describe?: unknown; update?: unknown } | undefined
|
|
53
|
+
const hasSettings = Boolean(settings?.describe || settings?.update)
|
|
54
|
+
return [
|
|
55
|
+
{ name: MCP_PACKAGE, active: mcpCount > 0, note: mcpCount > 0 ? mcpCount + ' 个 MCP 服务条目' : '未检测到 MCP 条目(MCP 管理页显示为空)' },
|
|
56
|
+
{ name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section 已安装' : '未安装:fileOpenTool 持久化降级为配置值' },
|
|
57
|
+
{ name: 'settings 服务', active: hasSettings, note: hasSettings ? '可读写设置' : '不可用(设置读写走配置回退)' },
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 本插件已注册的路由(kind → path 集合),冲突检测时排除自身,避免自误报。 */
|
|
62
|
+
const ownRoutes = { exact: new Set<string>(), prefixes: new Set<string>() }
|
|
63
|
+
|
|
64
|
+
/** 清空本插件路由记录(registerRoutes 装配开始时调用,热重载不残留)。 */
|
|
65
|
+
export function resetOwnRoutes(): void {
|
|
66
|
+
ownRoutes.exact.clear()
|
|
67
|
+
ownRoutes.prefixes.clear()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 记录本插件路由(routes.ts 注册成功时调用)。 */
|
|
71
|
+
export function noteOwnRoute(kind: 'exact' | 'prefix', path: string): void {
|
|
72
|
+
const table = kind === 'exact' ? ownRoutes.exact : ownRoutes.prefixes
|
|
73
|
+
table.add(path)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 检测路由前缀占用与重叠(排除本插件自有路由;内部表不可读视为无冲突)。 */
|
|
77
|
+
export function routeConflict(web: unknown, prefix: string): string | null {
|
|
78
|
+
try {
|
|
79
|
+
const w = web as { exact?: Map<unknown, unknown>; prefixes?: Map<unknown, unknown> } | undefined
|
|
80
|
+
const exact = w?.exact
|
|
81
|
+
const prefixes = w?.prefixes
|
|
82
|
+
if (exact instanceof Map) {
|
|
83
|
+
for (const path of exact.keys()) {
|
|
84
|
+
const other = String(path)
|
|
85
|
+
if (other.startsWith(prefix + '/') && !ownRoutes.exact.has(other)) {
|
|
86
|
+
return '精确路由 ' + other + ' 与 ' + prefix + ' 前缀重叠(可能来自热重载残留或其他插件)'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (prefixes instanceof Map) {
|
|
91
|
+
for (const path of prefixes.keys()) {
|
|
92
|
+
const other = String(path)
|
|
93
|
+
if (other !== prefix && (other.startsWith(prefix + '/') || prefix.startsWith(other + '/')) && !ownRoutes.prefixes.has(other)) {
|
|
94
|
+
return '路由前缀 ' + prefix + ' 与 ' + other + ' 重叠'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
/* 内部表不可读时放弃检查 */
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 检测本插件是否被重复装配(bundle 层 + 用户层重复 insert 的自诊断)。 */
|
|
105
|
+
export function duplicateEntries(ctx: Ctx): string[] {
|
|
106
|
+
const loader = ctx.get('loader') as { entries?: () => Iterable<{ id?: unknown; options?: { name?: unknown } }> } | undefined
|
|
107
|
+
if (!loader || typeof loader.entries !== 'function') return []
|
|
108
|
+
const own = [...loader.entries()].filter((entry) => String(entry.options?.name ?? entry.id ?? '') === PLUGIN_NAME)
|
|
109
|
+
if (own.length > 1) {
|
|
110
|
+
return ['检测到 ' + own.length + ' 个 ' + PLUGIN_NAME + ' loader 条目(重复装配)。请删除 profile 用户层 cordis.patch.yml 的重复 insert(或运行 dev_fix_patch 清理)']
|
|
111
|
+
}
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 护栏适配状态:路由前缀唯一性 + 插件装配唯一性。 */
|
|
116
|
+
export function detectGuards(ctx: Ctx): CompatAdapter[] {
|
|
117
|
+
const web = ctx.get('webServer')
|
|
118
|
+
const conflict = web ? routeConflict(web, ROUTE_PREFIX) : null
|
|
119
|
+
const duplicates = duplicateEntries(ctx)
|
|
120
|
+
return [
|
|
121
|
+
{ name: '路由前缀 ' + ROUTE_PREFIX, active: !conflict, note: conflict ?? '无冲突' },
|
|
122
|
+
{ name: '插件装配唯一性', active: duplicates.length === 0, note: duplicates[0] ?? '唯一装配' },
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 构建完整兼容性报告(RPC 与启动日志共用)。
|
|
128
|
+
* @author ddj 2026年08月24号
|
|
129
|
+
* @param ctx DSH host 上下文
|
|
130
|
+
* @param options 测试注入:depsAvailable 跳过动态导入、version 固定版本号
|
|
131
|
+
* @returns 兼容性报告
|
|
132
|
+
*/
|
|
133
|
+
export async function buildReport(
|
|
134
|
+
ctx: Ctx,
|
|
135
|
+
options?: { depsAvailable?: boolean; version?: string },
|
|
136
|
+
): Promise<CompatReport> {
|
|
137
|
+
const deps = options?.depsAvailable ?? (await loadSettingsDeps()) !== null
|
|
138
|
+
const external = detectExternal(ctx, deps)
|
|
139
|
+
const guards = detectGuards(ctx)
|
|
140
|
+
const warnings: string[] = []
|
|
141
|
+
if (!deps) warnings.push('未安装 @deepseek-ai/dsh-settings:文件打开工具设置持久化不可用(降级为配置值)')
|
|
142
|
+
for (const guard of guards) {
|
|
143
|
+
if (!guard.active && guard.note) warnings.push(guard.note)
|
|
144
|
+
}
|
|
145
|
+
return { pluginVersion: options?.version ?? pluginVersionOf(), external, guards, warnings }
|
|
146
|
+
}
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode host — 文件链接打开工具的持久化设置。
|
|
3
|
-
*
|
|
3
|
+
* 依赖守卫:@deepseek-ai/dsh-settings / schemastery 以动态加载引入,
|
|
4
|
+
* 缺失时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见)。
|
|
5
|
+
* 作者 ddj 2026年08月24号
|
|
4
6
|
*/
|
|
5
|
-
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
6
|
-
import z from 'schemastery'
|
|
7
7
|
import type { Ctx } from './store.js'
|
|
8
8
|
|
|
9
|
-
export const FILE_OPEN_SETTINGS_NS =
|
|
9
|
+
export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
|
|
10
10
|
export const FILE_OPEN_DEFAULT = 'auto'
|
|
11
11
|
export interface FileOpenSettings { fileOpenTool: string }
|
|
12
12
|
export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
|
|
@@ -17,7 +17,35 @@ type SettingsProvider = {
|
|
|
17
17
|
get?: (ns: string) => unknown
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/** 动态依赖的装载结果(宽松类型:dsh-settings 无本地类型声明)。 */
|
|
21
|
+
export interface SettingsDeps {
|
|
22
|
+
installSettingsSection: (ctx: Ctx, ns: string, schema: unknown, entry: object, hooks: object) => void
|
|
23
|
+
z: {
|
|
24
|
+
object: (shape: Record<string, unknown>) => unknown
|
|
25
|
+
string: () => { default: (value: string) => unknown }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SettingsDepsLoader = () => Promise<SettingsDeps | null>
|
|
30
|
+
|
|
31
|
+
let depsPromise: Promise<SettingsDeps | null> | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 动态加载设置依赖(模块级缓存;任一缺失返回 null 而非抛错)。
|
|
35
|
+
* @author ddj 2026年08月24号
|
|
36
|
+
* @returns 设置依赖或 null
|
|
37
|
+
*/
|
|
38
|
+
export function loadSettingsDeps(): Promise<SettingsDeps | null> {
|
|
39
|
+
if (!depsPromise) {
|
|
40
|
+
depsPromise = Promise.all([import('@deepseek-ai/dsh-settings'), import('schemastery')])
|
|
41
|
+
.then(([dshSettings, schemastery]) => ({
|
|
42
|
+
installSettingsSection: (dshSettings as { installSettingsSection: SettingsDeps['installSettingsSection'] }).installSettingsSection,
|
|
43
|
+
z: (schemastery as { default: SettingsDeps['z'] }).default,
|
|
44
|
+
}))
|
|
45
|
+
.catch(() => null)
|
|
46
|
+
}
|
|
47
|
+
return depsPromise
|
|
48
|
+
}
|
|
21
49
|
|
|
22
50
|
function normalizeValue(value: unknown): string {
|
|
23
51
|
if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
|
|
@@ -28,6 +56,30 @@ function configValue(config: unknown): string {
|
|
|
28
56
|
return normalizeValue((config as { fileOpenTool?: unknown } | undefined)?.fileOpenTool)
|
|
29
57
|
}
|
|
30
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 安装设置 section(动态依赖;缺失返回 false 不抛错)。
|
|
61
|
+
* @author ddj 2026年08月24号
|
|
62
|
+
* @param ctx DSH host 上下文
|
|
63
|
+
* @param ns 设置命名空间
|
|
64
|
+
* @param entry 初始设置值
|
|
65
|
+
* @param hooks 设置源绑定与变更回调
|
|
66
|
+
* @param loader 依赖装载器(测试注入)
|
|
67
|
+
* @returns 是否成功安装
|
|
68
|
+
*/
|
|
69
|
+
export async function installOpenSettingsSection(
|
|
70
|
+
ctx: Ctx,
|
|
71
|
+
ns: string,
|
|
72
|
+
entry: FileOpenSettings,
|
|
73
|
+
hooks: { setSource: (source: () => FileOpenSettings) => void; onChange: () => void },
|
|
74
|
+
loader: SettingsDepsLoader = loadSettingsDeps,
|
|
75
|
+
): Promise<boolean> {
|
|
76
|
+
const deps = await loader()
|
|
77
|
+
if (!deps) return false
|
|
78
|
+
const schema = deps.z.object({ fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT) })
|
|
79
|
+
deps.installSettingsSection(ctx, ns, schema, entry, hooks)
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
/**
|
|
32
84
|
* 注册设置命名空间,并提供 host 侧的读取与更新状态。
|
|
33
85
|
* @author ddj 2026年08月24号
|
|
@@ -48,7 +100,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
48
100
|
const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
|
|
49
101
|
const settingsChange = (): void => syncRevision()
|
|
50
102
|
|
|
51
|
-
|
|
103
|
+
void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current }, { setSource, onChange: settingsChange })
|
|
52
104
|
ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
|
|
53
105
|
provider = settingsCtx.get('settings')
|
|
54
106
|
syncRevision()
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
|
|
3
|
-
* 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(
|
|
4
|
-
* webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor
|
|
5
|
-
*
|
|
3
|
+
* 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
|
|
4
|
+
* webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
|
|
5
|
+
* 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
|
|
6
|
+
* 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
|
|
6
7
|
* 作者 ddj 2026-08-20
|
|
7
8
|
*/
|
|
8
9
|
import { registerRoutes } from './routes.js'
|
|
@@ -13,6 +14,7 @@ import { installIsolation } from './mcpIsolation.js'
|
|
|
13
14
|
import { dropFileIndex } from './workspace.js'
|
|
14
15
|
import { cwdOf } from './registry.js'
|
|
15
16
|
import { setupOpenSettings } from './fileOpenSettings.js'
|
|
17
|
+
import { PLUGIN_NAME, buildReport } from './compat.js'
|
|
16
18
|
import type { Registry } from './registry.js'
|
|
17
19
|
import type { Ctx } from './store.js'
|
|
18
20
|
|
|
@@ -20,7 +22,7 @@ export const name = "dsh-vscode-mode"
|
|
|
20
22
|
export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
*
|
|
25
|
+
* 装配插件:挂事件监听、注册路由、安装兼容层。
|
|
24
26
|
* @author ddj 2026年08月20号
|
|
25
27
|
* @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
|
|
26
28
|
* @param config 插件配置(可选 imageDir 覆盖图标目录)
|
|
@@ -28,6 +30,8 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
|
|
|
28
30
|
export function apply(ctx: Ctx, config?: unknown): void {
|
|
29
31
|
const registry: Registry = new Map()
|
|
30
32
|
const searcher = newSearcher(ctx)
|
|
33
|
+
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
34
|
+
const warnings: string[] = []
|
|
31
35
|
setupOpenSettings(ctx, config, () => {})
|
|
32
36
|
|
|
33
37
|
ctx.on('tools/result', (exec: unknown, result: unknown) => {
|
|
@@ -43,8 +47,32 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
43
47
|
}
|
|
44
48
|
})
|
|
45
49
|
|
|
46
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher))
|
|
50
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher), (warning) => warnings.push(warning))
|
|
47
51
|
installIsolation(ctx)
|
|
52
|
+
void logCompatSummary(ctx, warnings)
|
|
48
53
|
|
|
49
54
|
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)')
|
|
50
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
|
|
59
|
+
* @author ddj 2026年08月24号
|
|
60
|
+
* @param ctx DSH host 上下文
|
|
61
|
+
* @param warnings 装配期收集的兼容性警告
|
|
62
|
+
*/
|
|
63
|
+
async function logCompatSummary(ctx: Ctx, warnings: string[]): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
const report = await buildReport(ctx)
|
|
66
|
+
for (const warning of warnings) report.warnings.push(warning)
|
|
67
|
+
const logger = ctx.logger
|
|
68
|
+
if (!logger?.info) return
|
|
69
|
+
const head = '[' + PLUGIN_NAME + '] 兼容性:' + report.external.length + ' 项外部适配 / ' + report.guards.length + ' 项护栏'
|
|
70
|
+
if (report.warnings.length) {
|
|
71
|
+
logger.warn?.(head + ',警告 ' + report.warnings.length + ' 条:' + report.warnings.join(';'))
|
|
72
|
+
} else {
|
|
73
|
+
logger.info?.(head + ',无警告')
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
ctx.logger?.warn?.('[' + PLUGIN_NAME + '] 兼容性报告生成失败:' + String(error))
|
|
77
|
+
}
|
|
78
|
+
}
|