dsh-vscode-mode 0.12.0 → 0.13.0
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 +129 -103
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +12465 -11526
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +10 -4
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +13 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +6 -3
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/ShortcutSettings.ts +34 -54
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
package/src/mcpProject.ts
CHANGED
|
@@ -1,42 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-vscode-mode host — 项目级 MCP 管理。
|
|
3
|
-
* 配置以项目根目录 .mcp.json(mcpServers 格式,对齐 Claude Code/Cursor)存储;
|
|
4
|
-
* 工具经 loader 动态激活(entry id 带 PROJECT_ENTRY_PREFIX,全局生效)。
|
|
5
|
-
* 重启后由本项目从各 .mcp.json 恢复激活;.mcp.json 为持久真相,reconcile 以文件为准。
|
|
6
|
-
* 作者 ddj 2026年08月22号
|
|
7
|
-
*/
|
|
1
|
+
/** Project MCP configuration; .mcp.json is the source of truth. @author ddj 2026年09月28号 */
|
|
8
2
|
import { createHash } from 'node:crypto'
|
|
9
|
-
import { PROJECT_ENTRY_PREFIX, entriesOf,
|
|
3
|
+
import { PROJECT_ENTRY_PREFIX, entriesOf, mergeSecrets, validateConfig } from './mcp.js'
|
|
4
|
+
import { isProjectEntryId } from './compat.js'
|
|
5
|
+
import { clearLegacy, runtimeOf } from './mcpRuntime.js'
|
|
6
|
+
import type { ProjectDef } from './mcpRuntime.js'
|
|
10
7
|
import type { MpcConfig, MpcProject, MpcServer } from './shared/mcp.js'
|
|
11
8
|
import type { Ctx } from './store.js'
|
|
12
9
|
|
|
13
10
|
const FILE_NAME = '.mcp.json'
|
|
14
11
|
|
|
15
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Require an exact registered workspace before any project file mutation.
|
|
14
|
+
* @private @author ddj 2026年09月28号
|
|
15
|
+
* @param ctx Host services.
|
|
16
|
+
* @param workspacePath Requested workspace.
|
|
17
|
+
* @returns Registered workspace; throws for an unmanaged path.
|
|
18
|
+
*/
|
|
16
19
|
function requireWorkspace(ctx: Ctx, workspacePath: string): { path: string; title?: string } {
|
|
17
20
|
const workspace = (ctx.get('workspaceRegistry')?.list?.() ?? []).find((item: { path: string }) => item.path === workspacePath)
|
|
18
21
|
if (!workspace) throw new Error('项目未注册为 DSH workspace,不能管理项目 MCP')
|
|
19
22
|
return workspace
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the explicit settings UI write policy for a fixed project configuration path.
|
|
27
|
+
* @private @author ddj 2026年09月28号
|
|
28
|
+
* @param ctx Host services.
|
|
29
|
+
* @returns Resolved policy if the host supports it.
|
|
30
|
+
*/
|
|
23
31
|
function fullPolicy(ctx: Ctx): unknown {
|
|
24
|
-
|
|
25
|
-
if (!svc || typeof svc.resolve !== 'function') return undefined
|
|
26
|
-
return svc.resolve({ mode: 'danger-full-access' })
|
|
32
|
+
return ctx.get('sandboxPolicy')?.resolve?.({ mode: 'danger-full-access' })
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Hash a workspace path for stable project server identities.
|
|
37
|
+
* @public @author ddj 2026年09月28号
|
|
38
|
+
* @param path Registered workspace path.
|
|
39
|
+
* @returns Ten hexadecimal characters.
|
|
40
|
+
*/
|
|
30
41
|
export function hashWorkspace(path: string): string {
|
|
31
42
|
return createHash('sha1').update(path).digest('hex').slice(0, 10)
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Preserve the historical project entry identity for RPC and UI consumers.
|
|
47
|
+
* @public @author ddj 2026年09月28号
|
|
48
|
+
* @param workspacePath Owning workspace.
|
|
49
|
+
* @param serverName Configured name.
|
|
50
|
+
* @returns Stable identity; this no longer denotes a global loader entry.
|
|
51
|
+
*/
|
|
35
52
|
export function projectEntryId(workspacePath: string, serverName: string): string {
|
|
36
53
|
return PROJECT_ENTRY_PREFIX + hashWorkspace(workspacePath) + '.' + serverName
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Read the complete source document; only ENOENT denotes an absent configuration.
|
|
58
|
+
* @private @author ddj 2026年09月28号
|
|
59
|
+
* @param ctx Host filesystem.
|
|
60
|
+
* @param workspacePath Registered workspace path.
|
|
61
|
+
* @returns Parsed document; throws on unreadable or malformed input without changing runtime state.
|
|
62
|
+
*/
|
|
40
63
|
async function readProjectJson(ctx: Ctx, workspacePath: string): Promise<Record<string, unknown>> {
|
|
41
64
|
const fs = ctx.get('fs')
|
|
42
65
|
if (!fs) throw new Error('缺少 fs 服务')
|
|
@@ -45,7 +68,8 @@ async function readProjectJson(ctx: Ctx, workspacePath: string): Promise<Record<
|
|
|
45
68
|
try {
|
|
46
69
|
text = await fs.readText(target)
|
|
47
70
|
} catch (error) {
|
|
48
|
-
return {}
|
|
71
|
+
if ((error as { code?: string })?.code === 'ENOENT') return {}
|
|
72
|
+
throw new Error('.mcp.json 读取失败:' + String(error))
|
|
49
73
|
}
|
|
50
74
|
let data: unknown
|
|
51
75
|
try {
|
|
@@ -57,20 +81,31 @@ async function readProjectJson(ctx: Ctx, workspacePath: string): Promise<Record<
|
|
|
57
81
|
return data as Record<string, unknown>
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Validate the server dictionary while retaining all definition fields.
|
|
86
|
+
* @public @author ddj 2026年09月28号
|
|
87
|
+
* @param data Source document.
|
|
88
|
+
* @returns Server definitions; malformed definitions reject the entire snapshot.
|
|
89
|
+
*/
|
|
61
90
|
export function serversOf(data: Record<string, unknown>): Record<string, Record<string, unknown>> {
|
|
62
91
|
const raw = data.mcpServers
|
|
63
|
-
if (raw === undefined) return
|
|
92
|
+
if (raw === undefined) return Object.create(null)
|
|
64
93
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('.mcp.json 的 mcpServers 必须是对象')
|
|
65
|
-
const
|
|
94
|
+
const servers: Record<string, Record<string, unknown>> = Object.create(null)
|
|
66
95
|
for (const [name, value] of Object.entries(raw)) {
|
|
67
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
68
|
-
|
|
96
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('.mcp.json 的 MCP 定义必须是对象:' + name)
|
|
97
|
+
servers[name] = value as Record<string, unknown>
|
|
69
98
|
}
|
|
70
|
-
return
|
|
99
|
+
return servers
|
|
71
100
|
}
|
|
72
101
|
|
|
73
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Normalize a definition for the official plugin, retaining supported advanced options.
|
|
104
|
+
* @public @author ddj 2026年09月28号
|
|
105
|
+
* @param def Persisted server definition.
|
|
106
|
+
* @param serverName Dictionary key.
|
|
107
|
+
* @returns Plugin config, or null for a missing definition.
|
|
108
|
+
*/
|
|
74
109
|
export function configFromDef(def: Record<string, unknown> | undefined, serverName: string): MpcConfig | null {
|
|
75
110
|
if (!def) return null
|
|
76
111
|
const transport = typeof def.url === 'string' ? 'streamable-http' : 'stdio'
|
|
@@ -79,155 +114,207 @@ export function configFromDef(def: Record<string, unknown> | undefined, serverNa
|
|
|
79
114
|
config.command = typeof def.command === 'string' ? def.command : ''
|
|
80
115
|
config.args = Array.isArray(def.args) ? def.args.map(String) : []
|
|
81
116
|
if (typeof def.cwd === 'string') config.cwd = def.cwd
|
|
82
|
-
if (def.env && typeof def.env === 'object') config.env = Object.fromEntries(Object.entries(def.env
|
|
117
|
+
if (def.env && typeof def.env === 'object') config.env = Object.fromEntries(Object.entries(def.env).map(([k, v]) => [k, String(v)]))
|
|
83
118
|
} else {
|
|
84
119
|
config.url = def.url as string
|
|
85
|
-
if (def.headers && typeof def.headers === 'object') config.headers = Object.fromEntries(Object.entries(def.headers
|
|
120
|
+
if (def.headers && typeof def.headers === 'object') config.headers = Object.fromEntries(Object.entries(def.headers).map(([k, v]) => [k, String(v)]))
|
|
86
121
|
}
|
|
87
122
|
if (typeof def.toolCallTimeoutMs === 'number') config.toolCallTimeoutMs = def.toolCallTimeoutMs
|
|
123
|
+
if (typeof def.maxInstructionBytes === 'number') config.maxInstructionBytes = def.maxInstructionBytes
|
|
124
|
+
if (typeof def.failOnStartupError === 'boolean') config.failOnStartupError = def.failOnStartupError
|
|
125
|
+
if (def.reconnect && typeof def.reconnect === 'object' && !Array.isArray(def.reconnect)) config.reconnect = { ...def.reconnect }
|
|
88
126
|
return config
|
|
89
127
|
}
|
|
90
128
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Write a validated source document without dropping unrelated keys.
|
|
131
|
+
* @private @author ddj 2026年09月28号
|
|
132
|
+
* @param ctx Host filesystem.
|
|
133
|
+
* @param workspacePath Registered workspace path.
|
|
134
|
+
* @param data Complete next document.
|
|
135
|
+
* @returns Completion of the authorized settings write.
|
|
136
|
+
*/
|
|
137
|
+
async function writeProjectJson(ctx: Ctx, workspacePath: string, data: Record<string, unknown>): Promise<void> {
|
|
93
138
|
const fs = ctx.get('fs')
|
|
94
139
|
if (!fs) throw new Error('缺少 fs 服务')
|
|
95
140
|
const target = await fs.resolve(FILE_NAME, { cwd: workspacePath })
|
|
96
|
-
|
|
97
|
-
await fs.writeText(target, JSON.stringify(next, null, 2) + '\n', void 0, void 0, fullPolicy(ctx))
|
|
141
|
+
await fs.writeText(target, JSON.stringify(data, null, 2) + '\n', void 0, void 0, fullPolicy(ctx))
|
|
98
142
|
}
|
|
99
143
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Reserve names across global entries and configured project files, including idle projects.
|
|
146
|
+
* @public @author ddj 2026年09月28号
|
|
147
|
+
* @param ctx Host services.
|
|
148
|
+
* @param serverName Proposed name.
|
|
149
|
+
* @param workspacePath Owning project, omitted for a global save.
|
|
150
|
+
* @returns Completion; throws on collision or an unreadable competing source document.
|
|
151
|
+
*/
|
|
152
|
+
export async function checkMcpName(ctx: Ctx, serverName: string, workspacePath?: string): Promise<void> {
|
|
153
|
+
const conflict = 'serverName "' + serverName + '" 已被另一个 MCP 使用(全局或其他项目),请换一个名称'
|
|
103
154
|
for (const entry of entriesOf(ctx)) {
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
155
|
+
const project = isProjectEntryId(String(entry.id ?? entry.options?.id ?? ''))
|
|
156
|
+
if (project || workspacePath === undefined) continue
|
|
157
|
+
if (entry.options?.config?.serverName === serverName) throw new Error(conflict)
|
|
158
|
+
}
|
|
159
|
+
for (const ws of ctx.get('workspaceRegistry')?.list?.() ?? []) {
|
|
160
|
+
if (ws.path === workspacePath) continue
|
|
161
|
+
const servers = serversOf(await readProjectJson(ctx, ws.path))
|
|
162
|
+
if (Object.hasOwn(servers, serverName)) throw new Error(conflict)
|
|
108
163
|
}
|
|
109
|
-
return null
|
|
110
164
|
}
|
|
111
165
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Reconcile one complete, validated snapshot; invalid reads preserve all active connections.
|
|
168
|
+
* @public @author ddj 2026年09月28号
|
|
169
|
+
* @param ctx Host services.
|
|
170
|
+
* @param workspacePath Registered workspace.
|
|
171
|
+
* @param refresh Optional unchanged server to reconnect.
|
|
172
|
+
* @returns Completion after scoped fibers match the file, including disabled flags.
|
|
173
|
+
*/
|
|
174
|
+
export async function reconcileProject(ctx: Ctx, workspacePath: string, refresh?: string): Promise<void> {
|
|
175
|
+
const data = await readProjectJson(ctx, workspacePath)
|
|
176
|
+
const defs = new Map<string, ProjectDef>()
|
|
177
|
+
for (const [name, def] of Object.entries(serversOf(data))) {
|
|
178
|
+
const config = configFromDef(def, name)!
|
|
179
|
+
validateConfig(config)
|
|
180
|
+
await checkMcpName(ctx, name, workspacePath)
|
|
181
|
+
defs.set(name, { config, enabled: def.disabled !== true })
|
|
124
182
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!entry) throw new Error('MCP 已创建但未能找到 entry(id=' + id + ')')
|
|
128
|
-
return serverOf(ctx, entry)
|
|
183
|
+
await clearLegacy(ctx)
|
|
184
|
+
await runtimeOf(ctx).replace(workspacePath, defs, refresh)
|
|
129
185
|
}
|
|
130
186
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Preserve the historical activation entrypoint while using the source file and agent scopes.
|
|
189
|
+
* @public @author ddj 2026年09月28号
|
|
190
|
+
* @param ctx Host services.
|
|
191
|
+
* @param workspacePath Registered workspace.
|
|
192
|
+
* @param config Requested configuration to persist.
|
|
193
|
+
* @returns Masked runtime summary after saving and attaching matching agents.
|
|
194
|
+
*/
|
|
195
|
+
export async function activateProjectMcp(ctx: Ctx, workspacePath: string, config: MpcConfig): Promise<MpcServer> {
|
|
196
|
+
const project = await projectSave(ctx, workspacePath, config.serverName, config)
|
|
197
|
+
return project.servers.find((server) => server.serverName === config.serverName)!
|
|
138
198
|
}
|
|
139
199
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (config === null) continue
|
|
155
|
-
const id = projectEntryId(workspacePath, serverName)
|
|
156
|
-
if (entriesOf(ctx).some((entry) => String(entry.id ?? entry.options?.id ?? '') === id)) continue
|
|
157
|
-
await activateProjectMcp(ctx, workspacePath, { ...config, serverName })
|
|
200
|
+
/**
|
|
201
|
+
* Disable a project definition by its stable id, retaining its persisted configuration.
|
|
202
|
+
* @public @author ddj 2026年09月28号
|
|
203
|
+
* @param ctx Host services.
|
|
204
|
+
* @param entryId Stable project identity.
|
|
205
|
+
* @returns Completion of disabled persistence and connection teardown.
|
|
206
|
+
*/
|
|
207
|
+
export async function deactivateMcp(ctx: Ctx, entryId: string): Promise<void> {
|
|
208
|
+
for (const [path, defs] of runtimeOf(ctx).projects) {
|
|
209
|
+
for (const name of defs.keys()) {
|
|
210
|
+
if (projectEntryId(path, name) !== entryId) continue
|
|
211
|
+
await projectToggle(ctx, path, name, false)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
158
214
|
}
|
|
159
215
|
}
|
|
160
216
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
217
|
+
/**
|
|
218
|
+
* List one project, retaining the last good runtime view on file errors.
|
|
219
|
+
* @private @author ddj 2026年09月28号
|
|
220
|
+
* @param ctx Host services.
|
|
221
|
+
* @param workspacePath Registered path.
|
|
222
|
+
* @param title Workspace title.
|
|
223
|
+
* @returns Configuration and honest runtime status with any read error attached.
|
|
224
|
+
*/
|
|
168
225
|
async function projectOf(ctx: Ctx, workspacePath: string, title: string): Promise<MpcProject> {
|
|
169
|
-
const fs = ctx.get('fs')
|
|
170
|
-
if (!fs) return { workspacePath, title, servers: [], source: 'project' }
|
|
171
|
-
try {
|
|
172
|
-
const target = await fs.resolve(workspacePath)
|
|
173
|
-
const info = await fs.stat(target)
|
|
174
|
-
if (!info || info.type !== 'directory') return { workspacePath, title, servers: [], source: 'project', missingDir: true }
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return { workspacePath, title, servers: [], source: 'project', missingDir: true }
|
|
177
|
-
}
|
|
178
|
-
await reconcileProject(ctx, workspacePath)
|
|
179
|
-
let servers: MpcServer[] = []
|
|
180
226
|
let fileError: string | undefined
|
|
227
|
+
let missingDir: boolean | undefined
|
|
181
228
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
229
|
+
const fs = ctx.get('fs')
|
|
230
|
+
if (!fs) throw new Error('缺少 fs 服务')
|
|
231
|
+
const info = await fs.stat(await fs.resolve(workspacePath))
|
|
232
|
+
missingDir = !info || info.type !== 'directory'
|
|
233
|
+
if (!missingDir) await reconcileProject(ctx, workspacePath)
|
|
186
234
|
} catch (error) {
|
|
235
|
+
missingDir = (error as { code?: string })?.code === 'ENOENT' || undefined
|
|
187
236
|
fileError = String(error)
|
|
188
237
|
}
|
|
189
|
-
return { workspacePath, title, servers, source: 'project', fileError }
|
|
238
|
+
return { workspacePath, title, servers: runtimeOf(ctx).views(workspacePath), source: 'project', missingDir, fileError }
|
|
190
239
|
}
|
|
191
240
|
|
|
192
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* List registered projects and reconcile readable source documents.
|
|
243
|
+
* @public @author ddj 2026年09月28号
|
|
244
|
+
* @param ctx Host services.
|
|
245
|
+
* @returns Project list; individual malformed files are reported on their project.
|
|
246
|
+
*/
|
|
193
247
|
export async function listProjects(ctx: Ctx): Promise<{ projects: MpcProject[] }> {
|
|
194
|
-
const registry = ctx.get('workspaceRegistry')
|
|
195
|
-
const workspaces = registry?.list?.() ?? []
|
|
196
248
|
const projects: MpcProject[] = []
|
|
197
|
-
for (const ws of
|
|
249
|
+
for (const ws of ctx.get('workspaceRegistry')?.list?.() ?? []) projects.push(await projectOf(ctx, ws.path, ws.title ?? ''))
|
|
198
250
|
return { projects }
|
|
199
251
|
}
|
|
200
252
|
|
|
201
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Merge editable fields while retaining advanced fields and unchanged masked credentials.
|
|
255
|
+
* @private @author ddj 2026年09月28号
|
|
256
|
+
* @param previous Persisted server definition.
|
|
257
|
+
* @param config Submitted form config.
|
|
258
|
+
* @returns Enabled source definition without runtime-only keys or stale transport fields.
|
|
259
|
+
*/
|
|
260
|
+
function mergeDefinition(previous: Record<string, unknown>, config: MpcConfig): Record<string, unknown> {
|
|
261
|
+
const next = mergeSecrets({ ...previous, ...config }, previous)
|
|
262
|
+
delete next.serverName
|
|
263
|
+
delete next.transport
|
|
264
|
+
delete next.disabled
|
|
265
|
+
const stale = config.transport === 'stdio' ? ['url', 'headers'] : ['command', 'args', 'cwd', 'env']
|
|
266
|
+
for (const key of stale) delete next[key]
|
|
267
|
+
return next
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Save one definition, preserving unrelated fields and masked secrets, then reconcile.
|
|
272
|
+
* @public @author ddj 2026年09月28号
|
|
273
|
+
* @param ctx Host services.
|
|
274
|
+
* @param workspacePath Registered workspace.
|
|
275
|
+
* @param serverName Dictionary key.
|
|
276
|
+
* @param config Submitted configuration.
|
|
277
|
+
* @returns Updated project view; invalid sources or conflicts reject before writing.
|
|
278
|
+
*/
|
|
202
279
|
export async function projectSave(ctx: Ctx, workspacePath: string, serverName: string, config: MpcConfig): Promise<MpcProject> {
|
|
203
280
|
const workspace = requireWorkspace(ctx, workspacePath)
|
|
204
281
|
validateConfig({ ...config, serverName })
|
|
205
|
-
|
|
206
|
-
if (conflict) throw new Error(conflict)
|
|
282
|
+
await checkMcpName(ctx, serverName, workspacePath)
|
|
207
283
|
const data = await readProjectJson(ctx, workspacePath)
|
|
208
284
|
const servers = serversOf(data)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
delete next.disabled // disabled 由 projectToggle 管理,保存恒为启用
|
|
213
|
-
servers[serverName] = next
|
|
214
|
-
await writeProjectJson(ctx, workspacePath, data, servers)
|
|
215
|
-
await activateProjectMcp(ctx, workspacePath, { ...config, serverName })
|
|
285
|
+
servers[serverName] = mergeDefinition(servers[serverName] ?? {}, { ...config, serverName })
|
|
286
|
+
await writeProjectJson(ctx, workspacePath, { ...data, mcpServers: servers })
|
|
287
|
+
await reconcileProject(ctx, workspacePath)
|
|
216
288
|
return projectOf(ctx, workspacePath, workspace.title ?? '')
|
|
217
289
|
}
|
|
218
290
|
|
|
219
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Persist removal before shutting down its scoped fibers.
|
|
293
|
+
* @public @author ddj 2026年09月28号
|
|
294
|
+
* @param ctx Host services.
|
|
295
|
+
* @param workspacePath Registered workspace.
|
|
296
|
+
* @param serverName Target definition.
|
|
297
|
+
* @returns Updated project view; write failures leave active configuration intact.
|
|
298
|
+
*/
|
|
220
299
|
export async function projectRemove(ctx: Ctx, workspacePath: string, serverName: string): Promise<MpcProject> {
|
|
221
300
|
const workspace = requireWorkspace(ctx, workspacePath)
|
|
222
|
-
await deactivateMcp(ctx, projectEntryId(workspacePath, serverName))
|
|
223
301
|
const data = await readProjectJson(ctx, workspacePath)
|
|
224
302
|
const servers = serversOf(data)
|
|
225
303
|
delete servers[serverName]
|
|
226
|
-
await writeProjectJson(ctx, workspacePath, data, servers)
|
|
304
|
+
await writeProjectJson(ctx, workspacePath, { ...data, mcpServers: servers })
|
|
305
|
+
await reconcileProject(ctx, workspacePath)
|
|
227
306
|
return projectOf(ctx, workspacePath, workspace.title ?? '')
|
|
228
307
|
}
|
|
229
308
|
|
|
230
|
-
/**
|
|
309
|
+
/**
|
|
310
|
+
* Persist enabled state and reconcile all matching agents immediately.
|
|
311
|
+
* @public @author ddj 2026年09月28号
|
|
312
|
+
* @param ctx Host services.
|
|
313
|
+
* @param workspacePath Registered workspace.
|
|
314
|
+
* @param serverName Target definition.
|
|
315
|
+
* @param enabled Desired enabled state.
|
|
316
|
+
* @returns Updated project view, including retained disabled definitions.
|
|
317
|
+
*/
|
|
231
318
|
export async function projectToggle(ctx: Ctx, workspacePath: string, serverName: string, enabled: boolean): Promise<MpcProject> {
|
|
232
319
|
const workspace = requireWorkspace(ctx, workspacePath)
|
|
233
320
|
const data = await readProjectJson(ctx, workspacePath)
|
|
@@ -236,23 +323,23 @@ export async function projectToggle(ctx: Ctx, workspacePath: string, serverName:
|
|
|
236
323
|
if (!def) throw new Error('项目中不存在该 MCP')
|
|
237
324
|
if (enabled) delete def.disabled
|
|
238
325
|
else def.disabled = true
|
|
239
|
-
await writeProjectJson(ctx, workspacePath, data, servers)
|
|
240
|
-
|
|
241
|
-
if (loader) {
|
|
242
|
-
const entry = entriesOf(ctx).find((candidate) => String(candidate.id ?? candidate.options?.id ?? '') === projectEntryId(workspacePath, serverName))
|
|
243
|
-
if (entry) await loader.update(entry.id, { disabled: !enabled })
|
|
244
|
-
}
|
|
326
|
+
await writeProjectJson(ctx, workspacePath, { ...data, mcpServers: servers })
|
|
327
|
+
await reconcileProject(ctx, workspacePath)
|
|
245
328
|
return projectOf(ctx, workspacePath, workspace.title ?? '')
|
|
246
329
|
}
|
|
247
330
|
|
|
248
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* Reconnect one enabled source definition across its active agents.
|
|
333
|
+
* @public @author ddj 2026年09月28号
|
|
334
|
+
* @param ctx Host services.
|
|
335
|
+
* @param workspacePath Registered workspace.
|
|
336
|
+
* @param serverName Target definition.
|
|
337
|
+
* @returns Updated project view; disabled or idle definitions stay connection-free.
|
|
338
|
+
*/
|
|
249
339
|
export async function projectRefresh(ctx: Ctx, workspacePath: string, serverName: string): Promise<MpcProject> {
|
|
250
340
|
const workspace = requireWorkspace(ctx, workspacePath)
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
253
|
-
|
|
254
|
-
const entry = entriesOf(ctx).find((candidate) => String(candidate.id ?? candidate.options?.id ?? '') === id)
|
|
255
|
-
if (!entry) throw new Error('该项目 MCP 未激活')
|
|
256
|
-
await loader.update(entry.id, { config: { ...(entry.options?.config ?? {}) } })
|
|
341
|
+
const data = await readProjectJson(ctx, workspacePath)
|
|
342
|
+
if (!Object.hasOwn(serversOf(data), serverName)) throw new Error('项目中不存在该 MCP')
|
|
343
|
+
await reconcileProject(ctx, workspacePath, serverName)
|
|
257
344
|
return projectOf(ctx, workspacePath, workspace.title ?? '')
|
|
258
345
|
}
|