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/README.md +30 -3
- package/lib/client.js +169 -35
- package/lib/client.js.map +1 -1
- package/lib/index.js +550 -197
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +91 -0
- package/src/client/fileOpeners.ts +2 -1
- package/src/client/index.ts +18 -10
- package/src/client/openPathRouter.ts +5 -5
- package/src/client/settingsContext.ts +7 -17
- package/src/client/styles/mcp.css +17 -0
- package/src/client/ui/McpSettings.ts +68 -2
- package/src/compat.ts +147 -0
- package/src/devForm.ts +133 -0
- package/src/fileOpenSettings.ts +58 -6
- package/src/index.ts +33 -5
- package/src/mcp.ts +8 -9
- package/src/mcpIsolation.ts +6 -12
- package/src/routes.ts +26 -7
- package/src/rpc.ts +13 -0
- package/src/shared/compat.ts +27 -0
- package/src/shared/rpc.ts +7 -0
package/src/devForm.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 开发形态管理(安装形态切换)。
|
|
3
|
+
* 开发形态 = profile 中以 link: 依赖 + junction 指向工作区的安装(用于插件开发);
|
|
4
|
+
* 关闭 = 改回版本依赖 + 删 junction + pnpm install 装配正式版。
|
|
5
|
+
* 切换后需重启 DSH 生效(运行中的代码仍是旧形态)。
|
|
6
|
+
* 与 compat.ts 存在模块环(devForm 用 pluginVersionOf、compat 用 readDevForm),
|
|
7
|
+
* 双方仅在函数体内交叉使用,模块求值期无依赖,ESM 安全。
|
|
8
|
+
* 作者 ddj 2026-08-24
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import { PLUGIN_NAME, pluginVersionOf } from './compat.js'
|
|
14
|
+
import type { DevFormInfo } from './shared/compat.js'
|
|
15
|
+
import type { Ctx } from './store.js'
|
|
16
|
+
|
|
17
|
+
/** 扫描 $DSH_HOME/profiles 找到依赖本插件的 profile 目录(无 env 依赖,多 profile 安全)。 */
|
|
18
|
+
export function findProfileDir(): string | undefined {
|
|
19
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
20
|
+
try {
|
|
21
|
+
for (const name of readdirSync(join(home, 'profiles'))) {
|
|
22
|
+
const pkgFile = join(home, 'profiles', name, 'package.json')
|
|
23
|
+
if (!existsSync(pkgFile)) continue
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(pkgFile, 'utf8')) as { dependencies?: Record<string, unknown> } | undefined
|
|
26
|
+
if (pkg?.dependencies?.[PLUGIN_NAME]) return dirname(pkgFile)
|
|
27
|
+
} catch {
|
|
28
|
+
/* 坏 manifest 跳过该 profile */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
/* profiles 目录不存在时放弃 */
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 读取当前开发形态(manifest link: 判定)。
|
|
39
|
+
* @author ddj 2026年08月24号
|
|
40
|
+
* @returns 开发形态状态
|
|
41
|
+
*/
|
|
42
|
+
export function readDevForm(): DevFormInfo {
|
|
43
|
+
const profileDir = findProfileDir()
|
|
44
|
+
if (!profileDir) return { enabled: false }
|
|
45
|
+
try {
|
|
46
|
+
const text = readFileSync(join(profileDir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
|
|
47
|
+
const pkg = JSON.parse(text) as { dependencies?: Record<string, unknown> } | undefined
|
|
48
|
+
const spec = pkg?.dependencies?.[PLUGIN_NAME]
|
|
49
|
+
if (typeof spec === 'string' && spec.startsWith('link:')) {
|
|
50
|
+
return { enabled: true, path: spec.slice('link:'.length) }
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
/* manifest 不可读视为非开发形态 */
|
|
54
|
+
}
|
|
55
|
+
return { enabled: false }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 规划切换后的 manifest 文本(纯函数,可单测;非法 JSON 抛错)。
|
|
60
|
+
* @author ddj 2026年08月24号
|
|
61
|
+
* @param text 原 manifest 文本
|
|
62
|
+
* @param enabled 目标形态(true=link:,false=版本)
|
|
63
|
+
* @param path 工作区路径(开启时必填)
|
|
64
|
+
* @param version 版本号(关闭时使用,写入 ^<version>)
|
|
65
|
+
* @returns 新 manifest 文本(无 BOM,2 空格缩进)
|
|
66
|
+
*/
|
|
67
|
+
export function planManifest(text: string, enabled: boolean, path: string, version: string): string {
|
|
68
|
+
const data = JSON.parse(text.replace(/^\uFEFF/, '')) as { dependencies?: Record<string, unknown> }
|
|
69
|
+
if (!data.dependencies || typeof data.dependencies !== 'object') {
|
|
70
|
+
throw new Error('profile package.json 缺少 dependencies 字段')
|
|
71
|
+
}
|
|
72
|
+
data.dependencies[PLUGIN_NAME] = enabled ? 'link:' + path.replace(/\\/g, '/') : '^' + version
|
|
73
|
+
return JSON.stringify(data, null, 2) + '\n'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 建立或移除 node_modules 下的开发 junction(目标不存在时开启失败)。 */
|
|
77
|
+
function applyJunction(profileDir: string, enabled: boolean, target: string): void {
|
|
78
|
+
const linkDir = join(profileDir, 'node_modules', PLUGIN_NAME)
|
|
79
|
+
if (existsSync(linkDir)) rmSync(linkDir, { recursive: true, force: true })
|
|
80
|
+
if (!enabled) return
|
|
81
|
+
if (!existsSync(target)) throw new Error('工作区路径不存在:' + target)
|
|
82
|
+
symlinkSync(target, linkDir, 'junction')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 切换开发形态:改 manifest → 建/删 junction → pnpm install(subprocess)。
|
|
87
|
+
* @author ddj 2026年08月24号
|
|
88
|
+
* @param ctx DSH host 上下文(subprocess 服务)
|
|
89
|
+
* @param enabled 目标形态
|
|
90
|
+
* @param path 工作区绝对路径(开启时必填)
|
|
91
|
+
* @returns 切换结果(restart=true 表示需重启 DSH 生效)
|
|
92
|
+
*/
|
|
93
|
+
export async function setDevForm(ctx: Ctx, enabled: boolean, path?: string): Promise<{ ok: boolean; error?: string; restart: boolean }> {
|
|
94
|
+
const profileDir = findProfileDir()
|
|
95
|
+
if (!profileDir) return { ok: false, error: '未找到依赖本插件的 profile(检查 DSH_HOME/profiles)', restart: false }
|
|
96
|
+
const current = readDevForm()
|
|
97
|
+
const target = (path ?? current.path ?? '').replace(/\\/g, '/')
|
|
98
|
+
if (enabled && !target) return { ok: false, error: '开启开发形态需要提供工作区路径(path 参数)', restart: false }
|
|
99
|
+
if (enabled === current.enabled && (!enabled || target === current.path)) {
|
|
100
|
+
return { ok: true, restart: false }
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const pkgFile = join(profileDir, 'package.json')
|
|
104
|
+
const version = pluginVersionOf()
|
|
105
|
+
if (!version) return { ok: false, error: '读取插件版本失败,无法写入版本依赖', restart: false }
|
|
106
|
+
const next = planManifest(readFileSync(pkgFile, 'utf8').replace(/^\uFEFF/, ''), enabled, target, version)
|
|
107
|
+
writeFileSync(pkgFile, next, 'utf8')
|
|
108
|
+
applyJunction(profileDir, enabled, target)
|
|
109
|
+
const installError = await runPnpmInstall(ctx, profileDir)
|
|
110
|
+
return { ok: !installError, error: installError, restart: true }
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { ok: false, error: String(error), restart: false }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 在 profile 目录执行 pnpm install(subprocess 契约:spawn({argv,cwd,stdio,graceMs}))。 */
|
|
117
|
+
async function runPnpmInstall(ctx: Ctx, profileDir: string): Promise<string | undefined> {
|
|
118
|
+
const sub = ctx.get('subprocess')
|
|
119
|
+
if (!sub || typeof sub.spawn !== 'function') return 'manifest/junction 已更新,但缺少 subprocess:请手动在 profile 目录运行 pnpm install'
|
|
120
|
+
const handle = sub.spawn({
|
|
121
|
+
argv: ['pnpm', 'install'],
|
|
122
|
+
cwd: profileDir,
|
|
123
|
+
stdio: { stdout: { maxBytes: 1 << 16 }, stderr: { maxBytes: 1 << 16 }, stdin: 'ignore' },
|
|
124
|
+
graceMs: 120000,
|
|
125
|
+
})
|
|
126
|
+
const outcome = await handle.done
|
|
127
|
+
const code = outcome?.exitCode ?? outcome?.code
|
|
128
|
+
if (code !== 0) {
|
|
129
|
+
const stderr = handle.collected?.stderr?.readFrom(0).text ?? ''
|
|
130
|
+
return 'pnpm install 失败(exit ' + code + '):' + stderr.slice(0, 500)
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode host — 文件链接打开工具的持久化设置。
|
|
3
|
-
*
|
|
3
|
+
* 依赖守卫:@deepseek-ai/dsh-settings / schemastery 以动态加载引入,
|
|
4
|
+
* 缺失时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见)。
|
|
5
|
+
* 作者 ddj 2026年08月24号
|
|
4
6
|
*/
|
|
5
|
-
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
6
|
-
import z from 'schemastery'
|
|
7
7
|
import type { Ctx } from './store.js'
|
|
8
8
|
|
|
9
|
-
export const FILE_OPEN_SETTINGS_NS =
|
|
9
|
+
export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
|
|
10
10
|
export const FILE_OPEN_DEFAULT = 'auto'
|
|
11
11
|
export interface FileOpenSettings { fileOpenTool: string }
|
|
12
12
|
export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
|
|
@@ -17,7 +17,35 @@ type SettingsProvider = {
|
|
|
17
17
|
get?: (ns: string) => unknown
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/** 动态依赖的装载结果(宽松类型:dsh-settings 无本地类型声明)。 */
|
|
21
|
+
export interface SettingsDeps {
|
|
22
|
+
installSettingsSection: (ctx: Ctx, ns: string, schema: unknown, entry: object, hooks: object) => void
|
|
23
|
+
z: {
|
|
24
|
+
object: (shape: Record<string, unknown>) => unknown
|
|
25
|
+
string: () => { default: (value: string) => unknown }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SettingsDepsLoader = () => Promise<SettingsDeps | null>
|
|
30
|
+
|
|
31
|
+
let depsPromise: Promise<SettingsDeps | null> | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 动态加载设置依赖(模块级缓存;任一缺失返回 null 而非抛错)。
|
|
35
|
+
* @author ddj 2026年08月24号
|
|
36
|
+
* @returns 设置依赖或 null
|
|
37
|
+
*/
|
|
38
|
+
export function loadSettingsDeps(): Promise<SettingsDeps | null> {
|
|
39
|
+
if (!depsPromise) {
|
|
40
|
+
depsPromise = Promise.all([import('@deepseek-ai/dsh-settings'), import('schemastery')])
|
|
41
|
+
.then(([dshSettings, schemastery]) => ({
|
|
42
|
+
installSettingsSection: (dshSettings as { installSettingsSection: SettingsDeps['installSettingsSection'] }).installSettingsSection,
|
|
43
|
+
z: (schemastery as { default: SettingsDeps['z'] }).default,
|
|
44
|
+
}))
|
|
45
|
+
.catch(() => null)
|
|
46
|
+
}
|
|
47
|
+
return depsPromise
|
|
48
|
+
}
|
|
21
49
|
|
|
22
50
|
function normalizeValue(value: unknown): string {
|
|
23
51
|
if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
|
|
@@ -28,6 +56,30 @@ function configValue(config: unknown): string {
|
|
|
28
56
|
return normalizeValue((config as { fileOpenTool?: unknown } | undefined)?.fileOpenTool)
|
|
29
57
|
}
|
|
30
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 安装设置 section(动态依赖;缺失返回 false 不抛错)。
|
|
61
|
+
* @author ddj 2026年08月24号
|
|
62
|
+
* @param ctx DSH host 上下文
|
|
63
|
+
* @param ns 设置命名空间
|
|
64
|
+
* @param entry 初始设置值
|
|
65
|
+
* @param hooks 设置源绑定与变更回调
|
|
66
|
+
* @param loader 依赖装载器(测试注入)
|
|
67
|
+
* @returns 是否成功安装
|
|
68
|
+
*/
|
|
69
|
+
export async function installOpenSettingsSection(
|
|
70
|
+
ctx: Ctx,
|
|
71
|
+
ns: string,
|
|
72
|
+
entry: FileOpenSettings,
|
|
73
|
+
hooks: { setSource: (source: () => FileOpenSettings) => void; onChange: () => void },
|
|
74
|
+
loader: SettingsDepsLoader = loadSettingsDeps,
|
|
75
|
+
): Promise<boolean> {
|
|
76
|
+
const deps = await loader()
|
|
77
|
+
if (!deps) return false
|
|
78
|
+
const schema = deps.z.object({ fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT) })
|
|
79
|
+
deps.installSettingsSection(ctx, ns, schema, entry, hooks)
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
/**
|
|
32
84
|
* 注册设置命名空间,并提供 host 侧的读取与更新状态。
|
|
33
85
|
* @author ddj 2026年08月24号
|
|
@@ -48,7 +100,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
48
100
|
const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
|
|
49
101
|
const settingsChange = (): void => syncRevision()
|
|
50
102
|
|
|
51
|
-
|
|
103
|
+
void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current }, { setSource, onChange: settingsChange })
|
|
52
104
|
ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
|
|
53
105
|
provider = settingsCtx.get('settings')
|
|
54
106
|
syncRevision()
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
|
|
3
|
-
* 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(
|
|
4
|
-
* webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor
|
|
5
|
-
*
|
|
3
|
+
* 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
|
|
4
|
+
* webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
|
|
5
|
+
* 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
|
|
6
|
+
* 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
|
|
6
7
|
* 作者 ddj 2026-08-20
|
|
7
8
|
*/
|
|
8
9
|
import { registerRoutes } from './routes.js'
|
|
@@ -13,6 +14,7 @@ import { installIsolation } from './mcpIsolation.js'
|
|
|
13
14
|
import { dropFileIndex } from './workspace.js'
|
|
14
15
|
import { cwdOf } from './registry.js'
|
|
15
16
|
import { setupOpenSettings } from './fileOpenSettings.js'
|
|
17
|
+
import { PLUGIN_NAME, buildReport } from './compat.js'
|
|
16
18
|
import type { Registry } from './registry.js'
|
|
17
19
|
import type { Ctx } from './store.js'
|
|
18
20
|
|
|
@@ -20,7 +22,7 @@ export const name = "dsh-vscode-mode"
|
|
|
20
22
|
export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
*
|
|
25
|
+
* 装配插件:挂事件监听、注册路由、安装兼容层。
|
|
24
26
|
* @author ddj 2026年08月20号
|
|
25
27
|
* @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
|
|
26
28
|
* @param config 插件配置(可选 imageDir 覆盖图标目录)
|
|
@@ -28,6 +30,8 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
|
|
|
28
30
|
export function apply(ctx: Ctx, config?: unknown): void {
|
|
29
31
|
const registry: Registry = new Map()
|
|
30
32
|
const searcher = newSearcher(ctx)
|
|
33
|
+
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
34
|
+
const warnings: string[] = []
|
|
31
35
|
setupOpenSettings(ctx, config, () => {})
|
|
32
36
|
|
|
33
37
|
ctx.on('tools/result', (exec: unknown, result: unknown) => {
|
|
@@ -43,8 +47,32 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
43
47
|
}
|
|
44
48
|
})
|
|
45
49
|
|
|
46
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher))
|
|
50
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher), (warning) => warnings.push(warning))
|
|
47
51
|
installIsolation(ctx)
|
|
52
|
+
void logCompatSummary(ctx, warnings)
|
|
48
53
|
|
|
49
54
|
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)')
|
|
50
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
|
|
59
|
+
* @author ddj 2026年08月24号
|
|
60
|
+
* @param ctx DSH host 上下文
|
|
61
|
+
* @param warnings 装配期收集的兼容性警告
|
|
62
|
+
*/
|
|
63
|
+
async function logCompatSummary(ctx: Ctx, warnings: string[]): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
const report = await buildReport(ctx)
|
|
66
|
+
for (const warning of warnings) report.warnings.push(warning)
|
|
67
|
+
const logger = ctx.logger
|
|
68
|
+
if (!logger?.info) return
|
|
69
|
+
const head = '[' + PLUGIN_NAME + '] 兼容性:' + report.external.length + ' 项外部适配 / ' + report.guards.length + ' 项护栏'
|
|
70
|
+
if (report.warnings.length) {
|
|
71
|
+
logger.warn?.(head + ',警告 ' + report.warnings.length + ' 条:' + report.warnings.join(';'))
|
|
72
|
+
} else {
|
|
73
|
+
logger.info?.(head + ',无警告')
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
ctx.logger?.warn?.('[' + PLUGIN_NAME + '] 兼容性报告生成失败:' + String(error))
|
|
77
|
+
}
|
|
78
|
+
}
|
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
|
-
|
|
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[] {
|
package/src/mcpIsolation.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* tools.restrict() 负责模型可见性,tools.guard() 负责执行层兜底拒绝。
|
|
5
5
|
* 作者 ddj 2026年08月22号
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
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
|
|
37
|
-
return id.startsWith(PROJECT_ENTRY_PREFIX) || id.startsWith(LEGACY_PREFIX)
|
|
38
|
-
}
|
|
35
|
+
/** 根据 entry id 判断是否为新旧格式项目 MCP(兼容层统一判定)。 */
|
|
36
|
+
export const isProjectEntry = isProjectEntryId
|
|
39
37
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
105
|
+
}, '/edrv/rpc 精确路由'), 'edrv: routes')
|
|
87
106
|
|
|
88
107
|
for (const r of IMG_ROUTES) {
|
|
89
|
-
ctx.effect(() =>
|
|
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
|
-
}
|
|
123
|
+
}, '静态资源 ' + r.file), 'edrv: routes')
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
// Monaco Editor AMD 构建:/edrv/vendor/* → assets/vendor/*(路径穿越防护 + 按扩展名 MIME)
|
|
108
|
-
ctx.effect(() =>
|
|
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
|
-
}
|
|
156
|
+
}, '/edrv/vendor 前缀路由'), 'edrv: routes')
|
|
138
157
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -27,6 +27,8 @@ 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
29
|
import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
|
|
30
|
+
import { buildReport } from './compat.js'
|
|
31
|
+
import { readDevForm, setDevForm } from './devForm.js'
|
|
30
32
|
|
|
31
33
|
/** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
|
|
32
34
|
const debugWriteQueues = new Map<string, Promise<void>>()
|
|
@@ -322,6 +324,17 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
322
324
|
} catch (error) { return { ok: false, error: String(error) }
|
|
323
325
|
}
|
|
324
326
|
},
|
|
327
|
+
'compat': async () => ({ ok: true, report: await buildReport(ctx) }),
|
|
328
|
+
'vscode.devFormGet': async () => ({ ok: true, devForm: readDevForm() }),
|
|
329
|
+
'vscode.devFormSet': async (args) => {
|
|
330
|
+
try {
|
|
331
|
+
const result = await setDevForm(ctx, args.enabled === true, args.path)
|
|
332
|
+
if (!result.ok) return { ok: false, error: result.error ?? '切换开发形态失败' }
|
|
333
|
+
return { ok: true, devForm: readDevForm(), restart: result.restart }
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return { ok: false, error: String(error) }
|
|
336
|
+
}
|
|
337
|
+
},
|
|
325
338
|
}
|
|
326
339
|
}
|
|
327
340
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode 兼容层双面契约 — 与其他插件 / DSH 版本的探测与自诊断报告。
|
|
3
|
+
* Purity rule: no node/react imports (same as types.ts / rpc.ts)。
|
|
4
|
+
* 作者 ddj 2026-08-24
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 单项适配状态:外部插件/服务或护栏项,active 表示健康/生效。 */
|
|
8
|
+
export interface CompatAdapter {
|
|
9
|
+
name: string
|
|
10
|
+
active: boolean
|
|
11
|
+
note?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 开发形态信息:profile 中以 link: 依赖 + junction 指向工作区的安装形态。 */
|
|
15
|
+
export interface DevFormInfo {
|
|
16
|
+
enabled: boolean
|
|
17
|
+
path?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 兼容性报告:host 侧构建(RPC edrv.compat 返回),client 侧补充本地适配项后展示。 */
|
|
21
|
+
export interface CompatReport {
|
|
22
|
+
pluginVersion: string
|
|
23
|
+
external: CompatAdapter[]
|
|
24
|
+
guards: CompatAdapter[]
|
|
25
|
+
warnings: string[]
|
|
26
|
+
devForm?: DevFormInfo
|
|
27
|
+
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
StaleHunk,
|
|
14
14
|
} from './types.js'
|
|
15
15
|
import type { MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mcp.js'
|
|
16
|
+
import type { CompatReport, DevFormInfo } from './compat.js'
|
|
16
17
|
|
|
17
18
|
/** webServer 精确路由。 */
|
|
18
19
|
export const RPC_PATH = '/edrv/rpc'
|
|
@@ -45,6 +46,9 @@ export interface RpcRequestMap {
|
|
|
45
46
|
'mcp.projectRefresh': { workspacePath: string; serverName: string }
|
|
46
47
|
'vscode.fileOpenSettingsGet': {}
|
|
47
48
|
'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; expectedRevision?: number }
|
|
49
|
+
'vscode.devFormGet': {}
|
|
50
|
+
'vscode.devFormSet': { enabled: boolean; path?: string }
|
|
51
|
+
'compat': {}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export type RpcMethod = keyof RpcRequestMap
|
|
@@ -74,6 +78,9 @@ export interface RpcOkMap {
|
|
|
74
78
|
'mcp.projectRefresh': { project: MpcProject }
|
|
75
79
|
'vscode.fileOpenSettingsGet': { fileOpenTool: string; revision?: number }
|
|
76
80
|
'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; revision?: number }
|
|
81
|
+
'vscode.devFormGet': { devForm: DevFormInfo }
|
|
82
|
+
'vscode.devFormSet': { devForm: DevFormInfo; restart: boolean }
|
|
83
|
+
'compat': { report: CompatReport }
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/** 统一响应:{ok:true, ...payload} 或 {ok:false, error}。 */
|