dsh-vscode-mode 0.1.15 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 中央编辑器(文件页签/QuickOpen/状态栏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -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 = 'dsh-better-sidebar'
10
+ export const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN
10
11
 
11
12
  export interface FileOpenContext {
12
13
  sessionId?: string
@@ -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, getSettingsScope } from './settingsContext.js'
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 settings = getSettingsScope(ctx)
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.slots.inject('conversation.view', () => ctx.slots.register({
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.slots.inject('conversation.session.header.utilities', () => ctx.slots.register(
89
- { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '差异' },
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.slots.inject('settings.section', () => ctx.slots.register({
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
- * @author ddj 2026年08月24号
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 original = options.workspaces.openPath
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.openPath = open
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
- * @author ddj 2026年08月24号
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 interface SettingsScopeLike {
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;未安装设置服务时返回 undefined
17
+ * @returns 设置 scope 或 undefined
26
18
  */
27
19
  export function getSettingsScope(ctx: { get: (name: string) => unknown }): SettingsScopeLike | undefined {
28
- const service = ctx.get('webUiSettings') ?? ctx.get('settingsScope')
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,20 @@
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}
104
+
105
+ /* 开发形态开关 */
106
+ .vsm-devform-note{display:block;margin-top:4px;font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);word-break:break-all}
107
+ .vsm-devform-actions{display:flex;flex-direction:column;align-items:flex-end;gap:6px;min-width:0}
108
+ .vsm-devform-msg{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px;text-align:right}
109
+ .vsm-general-row button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:6px 12px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}
110
+ .vsm-general-row button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
111
+ .vsm-general-row button:disabled{opacity:.55;cursor:default}
112
+ .vsm-general-row button.vsm-devform-close{color:var(--dsw-alias-state-error-primary,#f07878);border-color:var(--dsw-alias-state-error-secondary,#d9534f55)}
@@ -119,11 +119,14 @@ function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
119
119
  return React.createElement('section', { className: 'vsm-project' }, head, body)
120
120
  }
121
121
 
122
- /** 通用设置:选择当前对话文件链接的打开器。 */
122
+ /** 通用设置:选择当前对话文件链接的打开器 + 开发形态开关。 */
123
123
  function GeneralSettings({ registry }) {
124
124
  const [tool, setTool] = React.useState(AUTO_OPEN_TOOL)
125
125
  const [busy, setBusy] = React.useState(false)
126
126
  const [error, setError] = React.useState('')
127
+ const [devForm, setDevForm] = React.useState(null)
128
+ const [devBusy, setDevBusy] = React.useState(false)
129
+ const [devMessage, setDevMessage] = React.useState('')
127
130
  const settings = React.useContext(SettingsContext)
128
131
  const snapshot = settings?.getSnapshot?.()
129
132
  const loading = !snapshot || snapshot.status === 'loading'
@@ -137,25 +140,87 @@ function GeneralSettings({ registry }) {
137
140
  onChange()
138
141
  return settings?.subscribe?.(onChange)
139
142
  }, [settings])
143
+ React.useEffect(() => {
144
+ rpc('vscode.devFormGet', {}).then((result) => {
145
+ if (result.ok) setDevForm(result.devForm)
146
+ }).catch(() => { /* 开发形态查询失败静默 */ })
147
+ }, [])
140
148
  const save = (value) => {
141
149
  setTool(value); setBusy(true); setError('')
142
150
  if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
143
151
  settings.set('fileOpenTool', value).catch((e) => setError(String(e))).finally(() => setBusy(false))
144
152
  }
153
+ const closeDevForm = () => {
154
+ if (!window.confirm('关闭开发形态:插件将切换为正式版安装(版本依赖 + 删除工作区链接),pnpm 装配后需重启 DSH 生效。确认关闭?')) return
155
+ setDevBusy(true)
156
+ setDevMessage('')
157
+ rpc('vscode.devFormSet', { enabled: false }).then((result) => {
158
+ if (!result.ok) { setDevMessage('关闭失败:' + String(result.error)); return }
159
+ setDevForm(result.devForm)
160
+ setDevMessage(result.restart ? '已切换为正式版,重启 DSH 后生效。' : '已关闭开发形态。')
161
+ }).catch((e) => setDevMessage('关闭失败:' + String(e))).finally(() => setDevBusy(false))
162
+ }
145
163
  const options = [{ id: AUTO_OPEN_TOOL, label: '自动选择', description: '按当前已注册工具自动选择' }, ...availableOpeners(registry).map((item) => ({ id: item.id, label: item.label, description: item.description }))]
146
164
  const currentOption = options.some((item) => item.id === tool) ? tool : AUTO_OPEN_TOOL
165
+ const devFormRow = devForm?.enabled ? React.createElement('div', { className: 'vsm-general-row' },
166
+ React.createElement('span', null, '开发形态', React.createElement('small', { className: 'vsm-devform-note' }, '工作区链接安装:' + devForm.path)),
167
+ React.createElement('div', { className: 'vsm-devform-actions' },
168
+ devMessage && React.createElement('small', { className: 'vsm-devform-msg' }, devMessage),
169
+ React.createElement('button', { className: 'vsm-devform-close', disabled: devBusy, onClick: closeDevForm }, devBusy ? '切换中…' : '关闭开发形态'),
170
+ ),
171
+ ) : null
147
172
  return React.createElement('section', { className: 'vsm-general-page' },
148
173
  React.createElement('h2', null, '通用'),
149
174
  React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
150
175
  unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用自动选择。'),
151
176
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
152
177
  React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
178
+ devFormRow,
179
+ )
180
+ }
181
+
182
+ /** 兼容性小节:host 报告(RPC compat)+ 客户端本地适配项。 */
183
+ function CompatSection({ getSummary }) {
184
+ const [report, setReport] = React.useState(null)
185
+ const [error, setError] = React.useState('')
186
+ React.useEffect(() => {
187
+ let alive = true
188
+ rpc('compat', {}).then((result) => {
189
+ if (!alive) return
190
+ if (!result.ok) setError(String(result.error))
191
+ else setReport(result.report)
192
+ }).catch((e) => { if (alive) setError(String(e)) })
193
+ return () => { alive = false }
194
+ }, [])
195
+ const renderItems = (list) => (list ?? []).map((item) => React.createElement('div', { key: item.name, className: 'vsm-compat-item ' + (item.active ? 'ok' : 'warn') },
196
+ React.createElement('span', { className: 'vsm-compat-name' }, item.name),
197
+ React.createElement('span', { className: 'vsm-compat-note' }, item.note ?? (item.active ? '正常' : '未生效')),
198
+ ))
199
+ const warnings = report?.warnings ?? []
200
+ const devForm = report?.devForm
201
+ return React.createElement('section', { className: 'vsm-general-page' },
202
+ React.createElement('h2', null, '兼容性'),
203
+ React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' + (report?.pluginVersion ? '(插件版本 ' + report.pluginVersion + ')' : '')),
204
+ error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
205
+ devForm?.enabled && React.createElement(React.Fragment, null,
206
+ React.createElement('h3', { className: 'vsm-compat-h3' }, '开发形态'),
207
+ React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '开发形态开启(工作区链接安装)'), React.createElement('span', { className: 'vsm-compat-note' }, devForm.path)),
208
+ ),
209
+ React.createElement('h3', { className: 'vsm-compat-h3' }, '外部插件与服务'),
210
+ renderItems(report?.external),
211
+ React.createElement('h3', { className: 'vsm-compat-h3' }, '客户端适配'),
212
+ renderItems(getSummary?.() ?? []),
213
+ React.createElement('h3', { className: 'vsm-compat-h3' }, '护栏'),
214
+ renderItems(report?.guards),
215
+ React.createElement('h3', { className: 'vsm-compat-h3' }, '警告'),
216
+ 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' }, '')),
153
217
  )
154
218
  }
155
219
 
156
220
  /** MCP 管理主面板。 */
157
221
  export function McpSettings(props) {
158
222
  const openerRegistry = props?.openerRegistry ?? { list: () => [] }
223
+ const compatSummary = props?.compatSummary ?? (() => [])
159
224
  const [servers, setServers] = React.useState<MpcServer[]>([])
160
225
  const [projects, setProjects] = React.useState<MpcProject[]>([])
161
226
  const [loading, setLoading] = React.useState(true)
@@ -252,11 +317,12 @@ export function McpSettings(props) {
252
317
  else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
253
318
  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
319
  else if (tab === 'projects') body = projectBody
320
+ else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
255
321
  else body = marketBody
256
322
  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
323
  return React.createElement('section', { className: 'vsm-mcp-page' },
258
324
  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 市场')),
325
+ 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
326
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
261
327
  body,
262
328
  form,
package/src/compat.ts ADDED
@@ -0,0 +1,147 @@
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 { readDevForm } from './devForm.js'
15
+ import type { CompatAdapter, CompatReport } from './shared/compat.js'
16
+ import type { Ctx } from './store.js'
17
+
18
+ export const PLUGIN_NAME = 'dsh-vscode-mode'
19
+ export const MCP_PACKAGE = '@deepseek-ai/dsh-mcp-client'
20
+ /** 项目级 MCP 的 loader entry id 固定前缀(区分 profile 全局 MCP;loader id 不能用 ':',故用 '.')。 */
21
+ export const PROJECT_ENTRY_PREFIX = 'vsm-mcp.'
22
+ /** 旧版项目 MCP 条目前缀(升级残留识别,不显示为全局 MCP)。 */
23
+ export const LEGACY_PROJECT_PREFIX = 'vsm-mcp:'
24
+ /** webServer 路由前缀(RPC 精确路由 + 静态资源前缀共用)。 */
25
+ export const ROUTE_PREFIX = '/edrv'
26
+
27
+ /** 判断新旧格式项目 MCP 条目 id(旧残留不被视为全局 MCP)。 */
28
+ export function isProjectEntryId(id: string): boolean {
29
+ return id.startsWith(PROJECT_ENTRY_PREFIX) || id.startsWith(LEGACY_PROJECT_PREFIX)
30
+ }
31
+
32
+ /** 从项目条目 id 解析 workspace hash(新旧格式通用)。 */
33
+ export function entryHash(id: string): string | undefined {
34
+ if (id.startsWith(PROJECT_ENTRY_PREFIX)) return id.slice(PROJECT_ENTRY_PREFIX.length).split('.')[0]
35
+ if (id.startsWith(LEGACY_PROJECT_PREFIX)) return id.slice(LEGACY_PROJECT_PREFIX.length).split(':')[0]
36
+ return undefined
37
+ }
38
+
39
+ /** 读取随包 package.json 版本号(缺失/解析失败降级为空串)。 */
40
+ export function pluginVersionOf(): string {
41
+ try {
42
+ const file = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
43
+ const pkg = JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }
44
+ return typeof pkg.version === 'string' ? pkg.version : ''
45
+ } catch {
46
+ return ''
47
+ }
48
+ }
49
+
50
+ /** 探测外部插件/服务适配状态(depsAvailable 由调用方注入,便于测试)。 */
51
+ export function detectExternal(ctx: Ctx, depsAvailable: boolean): CompatAdapter[] {
52
+ const mcpCount = entriesOf(ctx).length
53
+ const settings = ctx.get('settings') as { describe?: unknown; update?: unknown } | undefined
54
+ const hasSettings = Boolean(settings?.describe || settings?.update)
55
+ return [
56
+ { name: MCP_PACKAGE, active: mcpCount > 0, note: mcpCount > 0 ? mcpCount + ' 个 MCP 服务条目' : '未检测到 MCP 条目(MCP 管理页显示为空)' },
57
+ { name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section 已安装' : '未安装:fileOpenTool 持久化降级为配置值' },
58
+ { name: 'settings 服务', active: hasSettings, note: hasSettings ? '可读写设置' : '不可用(设置读写走配置回退)' },
59
+ ]
60
+ }
61
+
62
+ /** 本插件已注册的路由(kind → path 集合),冲突检测时排除自身,避免自误报。 */
63
+ const ownRoutes = { exact: new Set<string>(), prefixes: new Set<string>() }
64
+
65
+ /** 清空本插件路由记录(registerRoutes 装配开始时调用,热重载不残留)。 */
66
+ export function resetOwnRoutes(): void {
67
+ ownRoutes.exact.clear()
68
+ ownRoutes.prefixes.clear()
69
+ }
70
+
71
+ /** 记录本插件路由(routes.ts 注册成功时调用)。 */
72
+ export function noteOwnRoute(kind: 'exact' | 'prefix', path: string): void {
73
+ const table = kind === 'exact' ? ownRoutes.exact : ownRoutes.prefixes
74
+ table.add(path)
75
+ }
76
+
77
+ /** 检测路由前缀占用与重叠(排除本插件自有路由;内部表不可读视为无冲突)。 */
78
+ export function routeConflict(web: unknown, prefix: string): string | null {
79
+ try {
80
+ const w = web as { exact?: Map<unknown, unknown>; prefixes?: Map<unknown, unknown> } | undefined
81
+ const exact = w?.exact
82
+ const prefixes = w?.prefixes
83
+ if (exact instanceof Map) {
84
+ for (const path of exact.keys()) {
85
+ const other = String(path)
86
+ if (other.startsWith(prefix + '/') && !ownRoutes.exact.has(other)) {
87
+ return '精确路由 ' + other + ' 与 ' + prefix + ' 前缀重叠(可能来自热重载残留或其他插件)'
88
+ }
89
+ }
90
+ }
91
+ if (prefixes instanceof Map) {
92
+ for (const path of prefixes.keys()) {
93
+ const other = String(path)
94
+ if (other !== prefix && (other.startsWith(prefix + '/') || prefix.startsWith(other + '/')) && !ownRoutes.prefixes.has(other)) {
95
+ return '路由前缀 ' + prefix + ' 与 ' + other + ' 重叠'
96
+ }
97
+ }
98
+ }
99
+ } catch {
100
+ /* 内部表不可读时放弃检查 */
101
+ }
102
+ return null
103
+ }
104
+
105
+ /** 检测本插件是否被重复装配(bundle 层 + 用户层重复 insert 的自诊断)。 */
106
+ export function duplicateEntries(ctx: Ctx): string[] {
107
+ const loader = ctx.get('loader') as { entries?: () => Iterable<{ id?: unknown; options?: { name?: unknown } }> } | undefined
108
+ if (!loader || typeof loader.entries !== 'function') return []
109
+ const own = [...loader.entries()].filter((entry) => String(entry.options?.name ?? entry.id ?? '') === PLUGIN_NAME)
110
+ if (own.length > 1) {
111
+ return ['检测到 ' + own.length + ' 个 ' + PLUGIN_NAME + ' loader 条目(重复装配)。请删除 profile 用户层 cordis.patch.yml 的重复 insert(或运行 dev_fix_patch 清理)']
112
+ }
113
+ return []
114
+ }
115
+
116
+ /** 护栏适配状态:路由前缀唯一性 + 插件装配唯一性。 */
117
+ export function detectGuards(ctx: Ctx): CompatAdapter[] {
118
+ const web = ctx.get('webServer')
119
+ const conflict = web ? routeConflict(web, ROUTE_PREFIX) : null
120
+ const duplicates = duplicateEntries(ctx)
121
+ return [
122
+ { name: '路由前缀 ' + ROUTE_PREFIX, active: !conflict, note: conflict ?? '无冲突' },
123
+ { name: '插件装配唯一性', active: duplicates.length === 0, note: duplicates[0] ?? '唯一装配' },
124
+ ]
125
+ }
126
+
127
+ /**
128
+ * 构建完整兼容性报告(RPC 与启动日志共用)。
129
+ * @author ddj 2026年08月24号
130
+ * @param ctx DSH host 上下文
131
+ * @param options 测试注入:depsAvailable 跳过动态导入、version 固定版本号
132
+ * @returns 兼容性报告
133
+ */
134
+ export async function buildReport(
135
+ ctx: Ctx,
136
+ options?: { depsAvailable?: boolean; version?: string },
137
+ ): Promise<CompatReport> {
138
+ const deps = options?.depsAvailable ?? (await loadSettingsDeps()) !== null
139
+ const external = detectExternal(ctx, deps)
140
+ const guards = detectGuards(ctx)
141
+ const warnings: string[] = []
142
+ if (!deps) warnings.push('未安装 @deepseek-ai/dsh-settings:文件打开工具设置持久化不可用(降级为配置值)')
143
+ for (const guard of guards) {
144
+ if (!guard.active && guard.note) warnings.push(guard.note)
145
+ }
146
+ return { pluginVersion: options?.version ?? pluginVersionOf(), external, guards, warnings, devForm: readDevForm() }
147
+ }