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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
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,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
+ }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * dsh-vscode-mode host — 文件链接打开工具的持久化设置。
3
- * @author ddj 2026年08月24号
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 = settingsNamespace('dsh-vscode-mode')
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
- const FILE_OPEN_SCHEMA = z.object({ fileOpenTool: z.string().default(FILE_OPEN_DEFAULT) })
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
- installSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, FILE_OPEN_SCHEMA, { fileOpenTool: current }, { setSource, onChange: settingsChange })
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 分发(11 个 edrv.* 方法)、
4
- * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化。
5
- * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes(host 模块)。
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
+ }