dsh-vscode-mode 0.1.14 → 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.
@@ -8,6 +8,8 @@ import React from 'react'
8
8
  import { rpc } from '../rpc.js'
9
9
  import type { MpcProject, MpcServer } from '../../shared/mcp.js'
10
10
  import '../styles/mcp.css'
11
+ import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
12
+ import { SettingsContext } from '../settingsContext.js'
11
13
 
12
14
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
13
15
 
@@ -117,14 +119,83 @@ function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
117
119
  return React.createElement('section', { className: 'vsm-project' }, head, body)
118
120
  }
119
121
 
122
+ /** 通用设置:选择当前对话文件链接的打开器。 */
123
+ function GeneralSettings({ registry }) {
124
+ const [tool, setTool] = React.useState(AUTO_OPEN_TOOL)
125
+ const [busy, setBusy] = React.useState(false)
126
+ const [error, setError] = React.useState('')
127
+ const settings = React.useContext(SettingsContext)
128
+ const snapshot = settings?.getSnapshot?.()
129
+ const loading = !snapshot || snapshot.status === 'loading'
130
+ const unavailable = snapshot?.status === 'unavailable' || !settings
131
+ const notReady = snapshot?.status !== 'ready'
132
+ React.useEffect(() => {
133
+ const onChange = () => {
134
+ const next = settings?.getSnapshot?.().value?.fileOpenTool
135
+ if (typeof next === 'string') setTool(next)
136
+ }
137
+ onChange()
138
+ return settings?.subscribe?.(onChange)
139
+ }, [settings])
140
+ const save = (value) => {
141
+ setTool(value); setBusy(true); setError('')
142
+ if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
143
+ settings.set('fileOpenTool', value).catch((e) => setError(String(e))).finally(() => setBusy(false))
144
+ }
145
+ const options = [{ id: AUTO_OPEN_TOOL, label: '自动选择', description: '按当前已注册工具自动选择' }, ...availableOpeners(registry).map((item) => ({ id: item.id, label: item.label, description: item.description }))]
146
+ const currentOption = options.some((item) => item.id === tool) ? tool : AUTO_OPEN_TOOL
147
+ return React.createElement('section', { className: 'vsm-general-page' },
148
+ React.createElement('h2', null, '通用'),
149
+ React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
150
+ unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用自动选择。'),
151
+ error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
152
+ 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, '读取中…')),
153
+ )
154
+ }
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
+
120
189
  /** MCP 管理主面板。 */
121
- export function McpSettings() {
190
+ export function McpSettings(props) {
191
+ const openerRegistry = props?.openerRegistry ?? { list: () => [] }
192
+ const compatSummary = props?.compatSummary ?? (() => [])
122
193
  const [servers, setServers] = React.useState<MpcServer[]>([])
123
194
  const [projects, setProjects] = React.useState<MpcProject[]>([])
124
195
  const [loading, setLoading] = React.useState(true)
125
196
  const [busy, setBusy] = React.useState('')
126
197
  const [error, setError] = React.useState('')
127
- const [tab, setTab] = React.useState('mine')
198
+ const [tab, setTab] = React.useState('general')
128
199
  const [draft, setDraft] = React.useState(EMPTY)
129
200
  const [showForm, setShowForm] = React.useState(false)
130
201
  const [projectForm, setProjectForm] = React.useState(null)
@@ -212,13 +283,15 @@ export function McpSettings() {
212
283
  ) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
213
284
  let body
214
285
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
286
+ else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
215
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'))
216
288
  else if (tab === 'projects') body = projectBody
289
+ else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
217
290
  else body = marketBody
218
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
219
292
  return React.createElement('section', { className: 'vsm-mcp-page' },
220
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')),
221
- React.createElement('nav', { className: 'vsm-mcp-tabs' }, 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') }, '兼容性')),
222
295
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
223
296
  body,
224
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
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * dsh-vscode-mode host — 文件链接打开工具的持久化设置。
3
+ * 依赖守卫:@deepseek-ai/dsh-settings / schemastery 以动态加载引入,
4
+ * 缺失时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见)。
5
+ * 作者 ddj 2026年08月24号
6
+ */
7
+ import type { Ctx } from './store.js'
8
+
9
+ export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
10
+ export const FILE_OPEN_DEFAULT = 'auto'
11
+ export interface FileOpenSettings { fileOpenTool: string }
12
+ export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
13
+
14
+ type SettingsProvider = {
15
+ update?: (ns: string, patch: object, expectedRevision?: number) => Promise<void>
16
+ describe?: (options?: unknown) => Array<{ ns?: string; value?: unknown; revision?: number }>
17
+ get?: (ns: string) => unknown
18
+ }
19
+
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
+ }
49
+
50
+ function normalizeValue(value: unknown): string {
51
+ if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
52
+ return value.trim()
53
+ }
54
+
55
+ function configValue(config: unknown): string {
56
+ return normalizeValue((config as { fileOpenTool?: unknown } | undefined)?.fileOpenTool)
57
+ }
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
+
83
+ /**
84
+ * 注册设置命名空间,并提供 host 侧的读取与更新状态。
85
+ * @author ddj 2026年08月24号
86
+ * @param ctx DSH host 上下文
87
+ * @param config 插件组合配置
88
+ * @param onChange 设置变化回调
89
+ * @returns 设置状态
90
+ */
91
+ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: string) => void): FileOpenSettingsState {
92
+ let current = configValue(config)
93
+ let revision: number | undefined
94
+ let provider: SettingsProvider | undefined
95
+ const notify = (value: unknown): void => { current = normalizeValue(value); onChange(current) }
96
+ const syncRevision = (): void => {
97
+ const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
98
+ revision = descriptor?.revision
99
+ }
100
+ const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
101
+ const settingsChange = (): void => syncRevision()
102
+
103
+ void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current }, { setSource, onChange: settingsChange })
104
+ ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
105
+ provider = settingsCtx.get('settings')
106
+ syncRevision()
107
+ })
108
+
109
+ return {
110
+ get value() { return current },
111
+ get revision() { return revision },
112
+ update: async (value: string, expectedRevision?: number): Promise<void> => {
113
+ const next = normalizeValue(value)
114
+ if (!provider?.update) { notify(next); return }
115
+ await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
116
+ const descriptor = provider.describe?.({ redactSecrets: true })?.find((item: { ns?: string; value?: unknown }) => item.ns === FILE_OPEN_SETTINGS_NS)
117
+ const stored = descriptor?.value as { fileOpenTool?: unknown } | undefined
118
+ notify(stored?.fileOpenTool ?? next)
119
+ syncRevision()
120
+ },
121
+ }
122
+ }
123
+
124
+ export function normalizeFileOpenTool(value: unknown): string { return normalizeValue(value) }
@@ -0,0 +1,14 @@
1
+ declare module 'schemastery' {
2
+ const schema: any
3
+ export default schema
4
+ }
5
+
6
+ declare module '@deepseek-ai/dsh-settings' {
7
+ export function installSettingsSection(...args: any[]): void
8
+ export function settingsNamespace(value: string): string
9
+ }
10
+
11
+ declare module '@deepseek-ai/schemastery' {
12
+ const schema: any
13
+ export default schema
14
+ }
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'
@@ -12,6 +13,8 @@ import { newSearcher } from './search/orchestrator.js'
12
13
  import { installIsolation } from './mcpIsolation.js'
13
14
  import { dropFileIndex } from './workspace.js'
14
15
  import { cwdOf } from './registry.js'
16
+ import { setupOpenSettings } from './fileOpenSettings.js'
17
+ import { PLUGIN_NAME, buildReport } from './compat.js'
15
18
  import type { Registry } from './registry.js'
16
19
  import type { Ctx } from './store.js'
17
20
 
@@ -19,7 +22,7 @@ export const name = "dsh-vscode-mode"
19
22
  export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
20
23
 
21
24
  /**
22
- * 装配插件:挂事件监听、注册路由。
25
+ * 装配插件:挂事件监听、注册路由、安装兼容层。
23
26
  * @author ddj 2026年08月20号
24
27
  * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
25
28
  * @param config 插件配置(可选 imageDir 覆盖图标目录)
@@ -27,6 +30,9 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
27
30
  export function apply(ctx: Ctx, config?: unknown): void {
28
31
  const registry: Registry = new Map()
29
32
  const searcher = newSearcher(ctx)
33
+ /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
34
+ const warnings: string[] = []
35
+ setupOpenSettings(ctx, config, () => {})
30
36
 
31
37
  ctx.on('tools/result', (exec: unknown, result: unknown) => {
32
38
  void captureToolResult(ctx, registry, exec, result)
@@ -41,8 +47,32 @@ export function apply(ctx: Ctx, config?: unknown): void {
41
47
  }
42
48
  })
43
49
 
44
- 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))
45
51
  installIsolation(ctx)
52
+ void logCompatSummary(ctx, warnings)
46
53
 
47
54
  ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)')
48
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
+ }
package/src/mcp.ts CHANGED
@@ -1,20 +1,19 @@
1
1
  /**
2
2
  * dsh-vscode-mode host — MCP 运行时管理适配层。
3
3
  * 只操作已装配的 @deepseek-ai/dsh-mcp-client loader entry,不在浏览器侧连接 MCP。
4
+ * 身份常量(包名/条目前缀/新旧格式判定)统一由 compat 层持有,本模块再导出保持既有 import 面。
4
5
  * 作者 ddj 2026年08月22号
5
6
  */
7
+ import {
8
+ LEGACY_PROJECT_PREFIX,
9
+ MCP_PACKAGE,
10
+ PROJECT_ENTRY_PREFIX,
11
+ isProjectEntryId,
12
+ } from './compat.js'
6
13
  import type { MpcConfig, MpcServer, MpcTool } from './shared/mcp.js'
7
14
  import type { Ctx } from './store.js'
8
15
 
9
- const MCP_PACKAGE = '@deepseek-ai/dsh-mcp-client'
10
- /** 项目级 MCP 的 loader entry id 固定前缀(区分 profile 全局 MCP;注意 loader id 不能用 ':' 分隔,故用 '.')。 */
11
- export const PROJECT_ENTRY_PREFIX = 'vsm-mcp.'
12
- const LEGACY_PROJECT_PREFIX = 'vsm-mcp:'
13
-
14
- /** 判断新旧格式项目 entry id,避免旧残留被显示为全局 MCP。 */
15
- export function isProjectEntryId(id: string): boolean {
16
- return id.startsWith(PROJECT_ENTRY_PREFIX) || id.startsWith(LEGACY_PROJECT_PREFIX)
17
- }
16
+ export { LEGACY_PROJECT_PREFIX, MCP_PACKAGE, PROJECT_ENTRY_PREFIX, isProjectEntryId }
18
17
 
19
18
  /** 列出当前 loader 中的 MCP entry。 */
20
19
  function entriesOf(ctx: Ctx): any[] {
@@ -4,11 +4,11 @@
4
4
  * tools.restrict() 负责模型可见性,tools.guard() 负责执行层兜底拒绝。
5
5
  * 作者 ddj 2026年08月22号
6
6
  */
7
- import { PROJECT_ENTRY_PREFIX, entriesOf } from './mcp.js'
7
+ import { LEGACY_PROJECT_PREFIX, entryHash, isProjectEntryId } from './compat.js'
8
+ import { entriesOf } from './mcp.js'
8
9
  import { hashWorkspace } from './mcpProject.js'
9
10
  import type { Ctx } from './store.js'
10
11
 
11
- const LEGACY_PREFIX = 'vsm-mcp:'
12
12
  const UNKNOWN_PROJECT = '__unknown_project__'
13
13
 
14
14
  type ToolProjects = Map<string, string>
@@ -32,17 +32,11 @@ export function matchWorkspace(cwd: string | undefined, paths: string[]): string
32
32
  return matches[0]
33
33
  }
34
34
 
35
- /** 根据 entry id 判断是否为新旧格式项目 MCP。 */
36
- export function isProjectEntry(id: string): boolean {
37
- return id.startsWith(PROJECT_ENTRY_PREFIX) || id.startsWith(LEGACY_PREFIX)
38
- }
35
+ /** 根据 entry id 判断是否为新旧格式项目 MCP(兼容层统一判定)。 */
36
+ export const isProjectEntry = isProjectEntryId
39
37
 
40
- /** 从项目 entry id 解析 workspace hash。 */
41
- function entryHash(id: string): string | undefined {
42
- if (id.startsWith(PROJECT_ENTRY_PREFIX)) return id.slice(PROJECT_ENTRY_PREFIX.length).split('.')[0]
43
- if (id.startsWith(LEGACY_PREFIX)) return id.slice(LEGACY_PREFIX.length).split(':')[0]
44
- return undefined
45
- }
38
+ /** 兼容:旧版前缀常量名(mcpIsolation 历史导出,语义不变)。 */
39
+ export const LEGACY_PREFIX = LEGACY_PROJECT_PREFIX
46
40
 
47
41
  /** 返回一个 agent 不应继承的项目工具名。 */
48
42
  export function denyTools(toolProjects: ToolProjects, currentPath: string | undefined): string[] {
package/src/routes.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { readFile } from 'node:fs/promises'
7
7
  import { dirname, extname, join, normalize, resolve, sep } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
+ import { ROUTE_PREFIX, noteOwnRoute, resetOwnRoutes, routeConflict } from './compat.js'
9
10
  import { RPC_PATH } from './shared/rpc.js'
10
11
  import type { RpcMethod, RpcRequestMap } from './shared/rpc.js'
11
12
  import type { Ctx } from './store.js'
@@ -45,22 +46,40 @@ function imageDirOf(config: unknown): string {
45
46
  const vendorDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'vendor')
46
47
 
47
48
  /**
48
- * 注册全部 webServer 路由。
49
+ * 注册全部 webServer 路由(带兼容护栏:前缀冲突预警 + 注册失败降级跳过)。
49
50
  * @author ddj 2026年08月20号
50
51
  * @param ctx DSH 上下文
51
52
  * @param config 插件配置(可选 imageDir 等)
52
53
  * @param handleRpc RPC 分发入口(method,args → RpcResult)
54
+ * @param onWarning 兼容性警告收集(可选)
53
55
  */
54
56
  export function registerRoutes(
55
57
  ctx: Ctx,
56
58
  config: unknown,
57
59
  handleRpc: <M extends RpcMethod>(method: M, args: RpcRequestMap[M]) => Promise<unknown>,
60
+ onWarning?: (warning: string) => void,
58
61
  ): void {
59
62
  const imgDir = imageDirOf(config)
60
63
  const web = ctx.get('webServer')
61
64
  if (!web) return
62
65
 
63
- ctx.effect(() => web.register({
66
+ resetOwnRoutes()
67
+ const conflict = routeConflict(web, ROUTE_PREFIX)
68
+ if (conflict) onWarning?.('兼容性:' + conflict)
69
+
70
+ /** 护栏注册:重复路由抛错时降级为警告并跳过,不让单条路由拖垮装配。 */
71
+ const guarded = (route: { kind: 'exact' | 'prefix'; path: string; handler?: unknown }, label: string): (() => void) | undefined => {
72
+ try {
73
+ const dispose = web.register(route)
74
+ noteOwnRoute(route.kind, route.path)
75
+ return dispose
76
+ } catch (error) {
77
+ onWarning?.('兼容性:' + label + ' 注册失败(' + String(error) + ')')
78
+ return undefined
79
+ }
80
+ }
81
+
82
+ ctx.effect(() => guarded({
64
83
  kind: 'exact',
65
84
  path: RPC_PATH,
66
85
  handler: async (req: unknown, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string) => void }) => {
@@ -83,10 +102,10 @@ export function registerRoutes(
83
102
  res.end(JSON.stringify({ ok: false, error: String(error) }))
84
103
  }
85
104
  },
86
- }), 'edrv: /edrv/rpc route')
105
+ }, '/edrv/rpc 精确路由'), 'edrv: routes')
87
106
 
88
107
  for (const r of IMG_ROUTES) {
89
- ctx.effect(() => web.register({
108
+ ctx.effect(() => guarded({
90
109
  kind: 'exact',
91
110
  path: r.url,
92
111
  handler: async (req: unknown, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
@@ -101,11 +120,11 @@ export function registerRoutes(
101
120
  res.end()
102
121
  }
103
122
  },
104
- }), 'edrv: static ' + r.file)
123
+ }, '静态资源 ' + r.file), 'edrv: routes')
105
124
  }
106
125
 
107
126
  // Monaco Editor AMD 构建:/edrv/vendor/* → assets/vendor/*(路径穿越防护 + 按扩展名 MIME)
108
- ctx.effect(() => web.register({
127
+ ctx.effect(() => guarded({
109
128
  kind: 'prefix',
110
129
  path: VENDOR_PREFIX,
111
130
  handler: async (req: { method?: string; url?: string }, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
@@ -134,5 +153,5 @@ export function registerRoutes(
134
153
  res.end()
135
154
  }
136
155
  },
137
- }), 'edrv: /edrv/vendor prefix')
156
+ }, '/edrv/vendor 前缀路由'), 'edrv: routes')
138
157
  }
package/src/rpc.ts CHANGED
@@ -26,6 +26,8 @@ import { newSearcher } from './search/orchestrator.js'
26
26
  import { restoreFile, revertCall, revertHunk } from './revert.js'
27
27
  import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
28
28
  import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
29
+ import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
30
+ import { buildReport } from './compat.js'
29
31
 
30
32
  /** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
31
33
  const debugWriteQueues = new Map<string, Promise<void>>()
@@ -304,6 +306,24 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
304
306
  try { return { ok: true, project: await projectRefresh(ctx, args.workspacePath, args.serverName) } }
305
307
  catch (error) { return { ok: false, error: String(error) } }
306
308
  },
309
+ 'vscode.fileOpenSettingsGet': async () => {
310
+ const settings = ctx.get('settings')
311
+ const descriptor = settings?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
312
+ const value = descriptor?.value as { fileOpenTool?: unknown } | undefined
313
+ return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? FILE_OPEN_DEFAULT), revision: descriptor?.revision }
314
+ },
315
+ 'vscode.fileOpenSettingsUpdate': async (args) => {
316
+ const settings = ctx.get('settings')
317
+ if (!settings?.update) return { ok: false, error: '设置服务不可用' }
318
+ try {
319
+ await settings.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) }, args.expectedRevision)
320
+ const descriptor = settings.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
321
+ const value = descriptor?.value as { fileOpenTool?: unknown } | undefined
322
+ return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool), revision: descriptor?.revision }
323
+ } catch (error) { return { ok: false, error: String(error) }
324
+ }
325
+ },
326
+ 'compat': async () => ({ ok: true, report: await buildReport(ctx) }),
307
327
  }
308
328
  }
309
329