feat: add DSH plugin marketplace and shared MCP

GoodBuddy could share Skills across runtimes, but custom MCP remained limited and DeepSeek Harness could not manage third-party extensions. The app now provides a default-off DSH npm marketplace with managed installation, configuration, failure isolation, and packaged npm support, while assigned custom MCP is available to managed OpenCode, Continue Agent, and DeepSeek Harness in Execute.

Third-party DSH install scripts, initialization, and tools run with the current user's permissions. Ask remains read-only at dispatch, and turning off the marketplace hides management without disabling installed plugins.

Release note: 新增默认关闭的 DSH 插件市场,并让自定义 MCP 可分配给 OpenCode、Continue 和 DeepSeek Harness;安装第三方插件前会明确提示当前用户权限边界。
This commit is contained in:
mesalogo
2026-08-16 11:47:11 +08:00
parent 9e6f664e06
commit ff61b5f81d
67 changed files with 9337 additions and 443 deletions
@@ -630,7 +630,7 @@ describe('CapabilityService', () => {
})
})
it('allows Harness MCP assignment and rejects unsupported Agent Runtimes', async () => {
it('allows MCP assignment to every supported runtime', async () => {
const { service } = await createService()
await expect(
@@ -661,16 +661,28 @@ describe('CapabilityService', () => {
description: '',
enabled: true,
allowDynamicTools: false,
assignments: ['opencode'],
assignments: ['opencode', 'continue'],
secret: { action: 'keep' },
transport: 'stdio',
command: 'node',
args: ['server.js']
})
).rejects.toThrow('只能分配给直连模型或 DeepSeek Harness')
).resolves.toMatchObject({
mcpServers: expect.arrayContaining([
expect.objectContaining({
assignments: ['opencode', 'continue']
})
])
})
await expect(
service.getResolvedMcpServers('opencode')
).resolves.toHaveLength(1)
await expect(
service.getResolvedMcpServers('continue')
).resolves.toHaveLength(1)
})
it('migrates legacy OpenCode MCP assignments to the direct model', async () => {
it('preserves stored OpenCode MCP assignments', async () => {
const { filePath, builtinRoot, importedRoot } = await createService()
await writeFile(
filePath,
@@ -701,14 +713,14 @@ describe('CapabilityService', () => {
await expect(service.getSnapshot()).resolves.toMatchObject({
mcpServers: [
expect.objectContaining({ assignments: ['model'] })
expect.objectContaining({ assignments: ['opencode'] })
]
})
expect(await readFile(filePath, 'utf8')).toContain(
'"assignments": [\n "model"'
'"assignments": [\n "opencode"'
)
await expect(service.getResolvedMcpServers('opencode')).resolves.toEqual([])
await expect(service.getResolvedMcpServers('model')).resolves.toHaveLength(1)
await expect(service.getResolvedMcpServers('opencode')).resolves.toHaveLength(1)
await expect(service.getResolvedMcpServers('model')).resolves.toEqual([])
})
it('migrates v1 to v3 without losing skills, MCP configuration, or encrypted secrets', async () => {
+2 -30
View File
@@ -789,23 +789,9 @@ export class CapabilityService {
shouldPersist = true
}
}
const migrateMcpAssignments = loaded.mcpServers.some((server) =>
server.assignments.includes('opencode')
)
const migrated = migrateMcpAssignments
? {
...loaded,
mcpServers: loaded.mcpServers.map((server) => ({
...server,
assignments: server.assignments.includes('opencode')
? (['model'] as CapabilityAssignments)
: server.assignments
}))
}
: loaded
this.state = storedCapabilitiesSchema.parse(migrated)
this.state = storedCapabilitiesSchema.parse(loaded)
await this.validateBrowserProfileReferences(this.state)
if (shouldPersist || migrateMcpAssignments) {
if (shouldPersist) {
await this.persist(this.state)
}
return this.state
@@ -1609,17 +1595,6 @@ export class CapabilityService {
): Promise<CapabilitySnapshot> {
return this.queue(async () => {
const value = mcpServerInputSchema.parse(input)
if (
value.assignments.some(
(assignment) =>
assignment !== 'model' &&
assignment !== 'deepseek-harness'
)
) {
throw new Error(
'当前版本的 MCP Server 只能分配给直连模型或 DeepSeek Harness'
)
}
const state = await this.load()
const id = serverId ? mcpServerIdSchema.parse(serverId) : randomUUID()
const existing = state.mcpServers.find((server) => server.id === id)
@@ -1818,9 +1793,6 @@ export class CapabilityService {
async getResolvedMcpServers(
target: RuntimeTarget
): Promise<ResolvedMcpServer[]> {
if (target !== 'model' && target !== 'deepseek-harness') {
return []
}
const state = await this.load()
const assigned = state.mcpServers.filter(
(server) => server.enabled && server.assignments.includes(target)