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:
@@ -0,0 +1,178 @@
|
||||
import type { Context, Fiber, Plugin } from '@deepseek-ai/cordis'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const DEFAULT_ACTIVATION_TIMEOUT_MS = 5_000
|
||||
const DEFAULT_TOTAL_ACTIVATION_TIMEOUT_MS = 90_000
|
||||
const DISPOSAL_TIMEOUT_MS = 1_000
|
||||
|
||||
export type ControlledHarnessExtensionPackage = {
|
||||
id: string
|
||||
entrypoint: string
|
||||
configuration: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type ControlledHarnessExtensionLoadResult = {
|
||||
loadedIds: string[]
|
||||
failedIds: string[]
|
||||
failures: Array<{ id: string; message: string }>
|
||||
}
|
||||
|
||||
type ExtensionModule = {
|
||||
default?: unknown
|
||||
apply?: unknown
|
||||
}
|
||||
|
||||
// Keep the import native so Vite does not try to resolve userData file URLs
|
||||
// while bundling or running Vitest.
|
||||
const nativeImportModule = new Function(
|
||||
'specifier',
|
||||
'return import(specifier)'
|
||||
) as (specifier: string) => Promise<ExtensionModule>
|
||||
const requireExtension = createRequire(import.meta.url)
|
||||
|
||||
async function defaultImportModule(
|
||||
specifier: string
|
||||
): Promise<ExtensionModule> {
|
||||
try {
|
||||
return requireExtension(
|
||||
fileURLToPath(specifier)
|
||||
) as ExtensionModule
|
||||
} catch (error) {
|
||||
const code =
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
typeof error.code === 'string'
|
||||
? error.code
|
||||
: undefined
|
||||
if (
|
||||
code !== 'ERR_REQUIRE_ASYNC_MODULE' &&
|
||||
code !== 'ERR_REQUIRE_ESM'
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
return nativeImportModule(specifier)
|
||||
}
|
||||
}
|
||||
|
||||
function isPlugin(value: unknown): value is Plugin {
|
||||
return (
|
||||
typeof value === 'function' ||
|
||||
(value !== null &&
|
||||
typeof value === 'object' &&
|
||||
typeof (value as { apply?: unknown }).apply === 'function')
|
||||
)
|
||||
}
|
||||
|
||||
function resolvePlugin(module: ExtensionModule): Plugin {
|
||||
if (isPlugin(module)) {
|
||||
return module
|
||||
}
|
||||
if (isPlugin(module.default)) {
|
||||
return module.default
|
||||
}
|
||||
throw new Error(
|
||||
'DeepSeek Harness extension must export a Cordis plugin'
|
||||
)
|
||||
}
|
||||
|
||||
async function withTimeout<T>(
|
||||
operation: PromiseLike<T>,
|
||||
timeoutMs: number,
|
||||
message: string,
|
||||
onTimeout?: () => void
|
||||
): Promise<T> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
return await Promise.race([
|
||||
Promise.resolve(operation),
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(
|
||||
() => {
|
||||
onTimeout?.()
|
||||
reject(new Error(message))
|
||||
},
|
||||
timeoutMs
|
||||
)
|
||||
})
|
||||
])
|
||||
} finally {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadControlledHarnessExtensions(
|
||||
ctx: Context,
|
||||
extensions: readonly ControlledHarnessExtensionPackage[],
|
||||
options: {
|
||||
activationTimeoutMs?: number
|
||||
totalActivationTimeoutMs?: number
|
||||
importModule?: (url: string) => Promise<ExtensionModule>
|
||||
} = {}
|
||||
): Promise<ControlledHarnessExtensionLoadResult> {
|
||||
const loadedIds: string[] = []
|
||||
const failedIds: string[] = []
|
||||
const failures: Array<{ id: string; message: string }> = []
|
||||
const activationTimeoutMs =
|
||||
options.activationTimeoutMs ?? DEFAULT_ACTIVATION_TIMEOUT_MS
|
||||
const deadline =
|
||||
Date.now() +
|
||||
(options.totalActivationTimeoutMs ??
|
||||
DEFAULT_TOTAL_ACTIVATION_TIMEOUT_MS)
|
||||
const importModule = options.importModule ?? defaultImportModule
|
||||
|
||||
for (const extension of extensions) {
|
||||
let fiber: (Fiber & PromiseLike<Fiber>) | undefined
|
||||
let acceptActivation = true
|
||||
try {
|
||||
const remainingMs = deadline - Date.now()
|
||||
if (remainingMs <= 0) {
|
||||
throw new Error(
|
||||
'DeepSeek Harness extension startup deadline exceeded'
|
||||
)
|
||||
}
|
||||
await withTimeout(
|
||||
(async () => {
|
||||
const module = await importModule(
|
||||
pathToFileURL(extension.entrypoint).href
|
||||
)
|
||||
if (!acceptActivation) {
|
||||
throw new Error(
|
||||
'DeepSeek Harness extension activation timed out'
|
||||
)
|
||||
}
|
||||
const plugin = resolvePlugin(module)
|
||||
fiber = ctx.plugin(plugin, extension.configuration)
|
||||
await Promise.resolve(fiber)
|
||||
})(),
|
||||
Math.max(1, Math.min(activationTimeoutMs, remainingMs)),
|
||||
'DeepSeek Harness extension activation timed out',
|
||||
() => {
|
||||
acceptActivation = false
|
||||
}
|
||||
)
|
||||
loadedIds.push(extension.id)
|
||||
} catch (error) {
|
||||
if (fiber) {
|
||||
await withTimeout(
|
||||
fiber.dispose(),
|
||||
DISPOSAL_TIMEOUT_MS,
|
||||
'DeepSeek Harness extension disposal timed out'
|
||||
).catch(() => undefined)
|
||||
}
|
||||
failedIds.push(extension.id)
|
||||
failures.push({
|
||||
id: extension.id,
|
||||
message:
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message.slice(0, 1_000)
|
||||
: 'DeepSeek Harness extension failed to start'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { loadedIds, failedIds, failures }
|
||||
}
|
||||
Reference in New Issue
Block a user