编写插件、工具与模型适配器
本页说明 DeepSeek Harness CLI 如何把插件包管理、工具插件与模型相关插件组合到 profile 中,并通过显式版本兼容审批、文件锁和安装器上下文控制扩展生命周期。
Purpose and Scope
本页聚焦 apps/cli/src/plugin.ts 暴露的插件命令执行路径,以及 apps/cli/composition.md 所描述的基础插件组合关系。覆盖内容包括:profile 定位、插件命令分流、pnpm 操作委托、Desktop profile 的特殊处理、兼容版本豁免、构建脚本提示、输出与失败码传播,以及插件/工具/LLM 相关模块在 dsh-base bundle 中的组织方式。
本页不展开各个外部插件包的内部实现:这些包通过 @deepseek-ai/* 依赖注入,当前仓库中没有它们的源码实现。有关具体工具行为、模型 API 协议或单个插件的业务逻辑,应查看对应插件包或 sibling catalog 页面;本页只记录 Harness CLI 对它们的编排边界。
Overview
插件、工具和模型适配器在本项目中不是由 CLI 逐个实现,而是以 profile 为安装边界,由 runPlugin() 创建 profile 上下文并委托给 @deepseek-ai/dsh-plugin-manager。这种设计将两类职责分开:
- CLI 负责用户可见的命令语法、profile 前置检查、锁定、输出转发和退出码。
dsh-plugin-manager负责实际的包管理操作及不兼容版本检测;其内部算法未在当前仓库中提供。composition.md展示 dsh-base 通过 patch 配置聚合插件 ID 与 npm module,包括tool-plugin-manager、plugin-manager、llm、agent、agent-default-model、tools、llm-deepseek等。- Desktop profile 使用应用已初始化的 package tree;其他 profile 可以由 CLI 按模板初始化。
因此,适配器的扩展点是“可安装的 profile package/module”,而不是在 plugin.ts 中增加一个模型分支。CLI 只在版本豁免命令处解析 DSH 自有参数,其余参数原样留给 pnpm/插件管理器。
Architecture
Source: plugin.ts
Source: composition.md
架构中的实线关系对应 CLI 源码中的真实调用;bundle 内的插件关系表示 composition.md 中同一 dsh-base patch 对插件模块的聚合,而不是这些外部模块的内部调用关系。runPlugin() 依据 profile 是否为 desktop 在两个外部管理器操作之间选择:Desktop 走 runProfilePnpm 并加锁,其他 profile 走 runPluginCommand。
Profile 与插件安装边界
Profile 初始化
runPlugin() 首先对 Desktop 做快速检查:profile 目录必须已经存在 package.json。缺失时不会自动创建,而是返回失败并提示先启动并完全退出 Desktop 应用。这一限制避免 CLI 在应用专属目录中创建不完整的 package tree。
对于非 Desktop profile,版本命令进入锁区后会在缺少 package.json 时调用 initProfile(),并选择 PROFILE_TEMPLATES[profile]?.bundles,找不到模板时回退到 DEFAULT_PROFILE_BUNDLES。普通插件命令本身不在 CLI 中初始化 profile,而是把现有上下文交给 runPluginCommand;初始化策略因此只在版本豁免处理路径中明确出现,完整安装行为由外部管理器控制。
Desktop 与普通 profile 的差异
| 场景 | package.json 检查 | 执行器 | 文件锁 |
|---|---|---|---|
desktop | 必须预先存在 | runProfilePnpm | package.json,等待 120000 ms |
| 非 Desktop | 版本豁免路径可按模板初始化 | runPluginCommand | 由外部管理器承担普通命令的操作语义 |
源码还会将 profile、profile 目录、INSTALL_ANCHOR 和当前工作目录组成 context。这让安装器获得稳定的 profile 边界,同时保留调用方的 cwd,从而可以处理相对于调用目录的 pnpm 参数。
版本兼容审批流程
versionCommand() 只拦截三个 DSH 自有命令:allow-version、revoke-version 和 version-exemptions。任何其他命令返回 undefined,由 runPlugin() 继续普通插件操作;这避免 CLI 重新实现 pnpm 参数解析。
解析规则是严格的:
- 第一个非 option 参数作为
packageVersion。 --dsh-version value或--dsh-version=value设置精确的 DSH runtime 版本。allow-version只有在出现--accept-risk后才允许确认风险。- 未知参数、重复关键参数或缺少 package/runtime 版本会抛出 usage 错误。
version-exemptions不接受额外参数。
通过校验后,操作在 package.json 上执行 withFileLock(..., { waitMs: 120000 })。读取命令调用 readProfileCompatibility() 并把 warnings 写到 stderr,再把 exemptions 以格式化 JSON 写到 stdout;写入命令调用 setProfileVersionExemption(),随后输出 allowed 或 revoked 结果。整个函数捕获异常并返回 1,成功返回 0。
1const [command, ...rest] = args
2if (command !== 'allow-version' && command !== 'revoke-version' && command !== 'version-exemptions') return undefined
3try {
4 let packageVersion: string | undefined
5 let runtimeVersion: string | undefined
6 let acceptRisk = false
7 const argumentsIterator = rest.values()
8 for (const argument of argumentsIterator) {
9 if (argument === '--accept-risk' && command === 'allow-version' && !acceptRisk) acceptRisk = true
10 else if (argument === '--dsh-version' && runtimeVersion === undefined) runtimeVersion = argumentsIterator.next().value
11 else if (argument.startsWith('--dsh-version=') && runtimeVersion === undefined) runtimeVersion = argument.slice('--dsh-version='.length)
12 else if (!argument.startsWith('-') && packageVersion === undefined) packageVersion = argument
13 else throw new Error(`unexpected argument ${JSON.stringify(argument)}`)
14 }Source: plugin.ts
这里的设计意图是把“兼容性风险接受”变成精确的 package version + exact DSH version 记录,而不是一个永久的全局开关。源码在 allow-version 前明确向 stderr 输出风险警告,并把 acceptRisk 传给外部 setProfileVersionExemption()。
Core Flow
Source: plugin.ts
普通命令的关键顺序是:先确认 Desktop profile 可用,随后尝试处理版本命令;若未命中,再解析 profile 目录和兼容性 warnings,创建操作选项,最后选择外部执行器。这个顺序保证版本审批不会误落入普通 pnpm 参数,同时普通命令仍能看到已有兼容性警告。
插件、工具与模型适配器的组合
composition.md 的 dsh-base patch 将插件 ID 映射到外部包。例如基础组合中同时存在:
- 包管理基础设施:
tool-plugin-manager与plugin-manager。 - 模型/LLM:
llm、llm-retry、llm-deepseek、agent-default-model。 - 工具入口:
tools、tool-bash、tool-pwsh、tool-fs、tool-web、tool-workflow。 - Agent 与执行编排:
agent、agent-loop、subagent、workflow-ptc。
这些条目证明工具与模型适配器采用 bundle 配置组合,而不是硬编码在 CLI 的安装函数中。当前仓库没有提供这些包的类、方法签名或运行时协议,因此无法从本仓库进一步推导它们如何调用模型服务、注册工具或处理请求;相关实现细节应以对应 package 源码为准。
Usage Examples
普通 profile 插件命令
下面的调用路径是仓库中导出的 CLI service function。args 不由该函数自行限制为某一套插件参数,未被 DSH 版本命令识别的参数会继续交给插件管理器。
1export async function runPlugin(profile: string, args: readonly string[], packageManager?: ProfileContext['packageManager']): Promise<number> {
2 if (profile === 'desktop') {
3 try { requireDesktopProfile(resolveProfileDir(profile)) } catch (error) {
4 process.stderr.write(`dsh: ${String(error)}\n`)
5 return 1
6 }
7 }
8 const versionResult = await versionCommand(profile, args)
9 if (versionResult !== undefined) return versionResult
10 const dir = resolveProfileDir(profile)
11 if (existsSync(join(dir, 'package.json'))) {
12 for (const warning of readProfileCompatibility(dir).warnings) process.stderr.write(`dsh: warning: ${warning}\n`)
13 }Source: plugin.ts
通过 profile manager 执行安装
CLI 为外部管理器统一设置 CLI 执行模式、输出上限、锁等待和 registry lookup 超时,并把子进程输出转发到相同名称的 process stream。
1const options: PackageOperationOptions = {
2 ...packageManager,
3 execution: 'cli',
4 outputBytes: 16384,
5 lockWaitMs: 120000,
6 lookupTimeoutMs: 120000,
7 onOutput: (text, stream) => { process[stream].write(text) },
8}
9const result = profile === 'desktop'
10 ? await withFileLock(join(dir, 'package.json'), async () => {
11 requireDesktopProfile(dir)
12 return runProfilePnpm(context, args, options)
13 }, { waitMs: 120000 })
14 : await runPluginCommand(context, args, options)Source: plugin.ts
dsh-base 中的工具与模型模块声明
以下摘录来自自动生成的组合文档,展示配置层如何把 bundle patch 连接到工具管理器、LLM 和模型适配器模块。
1# composition.md 中生成的关系(展示为配置图节点)
2packages/bundle/base/cordis.patch.yml -> @deepseek-ai/dsh-plugin-manager/tools
3packages/bundle/base/cordis.patch.yml -> @deepseek-ai/dsh-plugin-manager
4packages/bundle/base/cordis.patch.yml -> @deepseek-ai/dsh-llm
5packages/bundle/base/cordis.patch.yml -> @deepseek-ai/dsh-agent-default-model
6packages/bundle/base/cordis.patch.yml -> @deepseek-ai/dsh-tools
7packages/bundle/base/cordis.patch.yml -> @deepseek-ai/dsh-llm-deepseek-api-keySource: composition.md
上例是文档生成器输出的关系图内容,不是可直接执行的配置文件;真实 patch 文件未在本次有限源代码读取范围内读取。
Configuration Options
| 选项/上下文 | 类型 | 默认/固定值 | 说明 |
|---|---|---|---|
profile | string | 调用方提供 | 决定 profile 目录、初始化模板和 Desktop 分支。 |
args | readonly string[] | 调用方提供 | DSH 只解析三个版本命令;其他参数交给 pnpm/插件管理器。 |
packageManager | ProfileContext['packageManager'] | 可选 | 覆盖或补充 PackageOperationOptions 中的安装器配置。 |
execution | 字面量 | 'cli' | CLI 固定使用的外部操作执行模式。 |
outputBytes | number | 16384 | 传给外部管理器的输出字节限制。 |
lockWaitMs | number | 120000 | 包操作上下文中的等待时长。 |
lookupTimeoutMs | number | 120000 | 包查找操作的超时。 |
| 文件锁等待 | number | 120000 | 版本豁免与 Desktop pnpm 操作都以 120 秒等待 package.json 锁。 |
resolveProfileDir()、PROFILE_TEMPLATES、DEFAULT_PROFILE_BUNDLES 和 INSTALL_ANCHOR 来自外部包或另一个已读取但未展开的本地模块;它们的具体目录、模板内容和 anchor 值在当前证据中不可见,不能进一步假设。
API Reference
runPlugin(profile: string, args: readonly string[], packageManager?: ProfileContext['packageManager']): Promise<number>
公开导出的 CLI 插件执行入口。它返回外部插件管理器的 exitCode,或在 Desktop profile 前置检查失败时返回 1。
profile:profile 名称;desktop触发预初始化检查和加锁的runProfilePnpm路径。args:版本豁免命令或 pnpm 相对参数。packageManager:可选的安装器上下文,展开到操作选项。- 返回值:
0或外部管理器返回的退出码表示成功/失败;Desktop profile 不存在时为1。
versionCommand(profile: string, args: readonly string[]): Promise<number | undefined>
内部命令分流器。识别 DSH 自有版本命令时返回 0 或 1;对于其他参数返回 undefined,让调用方继续普通插件流程。源码没有将其导出,因此它是实现细节而非稳定 CLI API。
Failure Modes、边界与并发
- Desktop 未初始化:
requireDesktopProfile()检查package.json;不存在时抛错,runPlugin()捕获并返回1。 - 参数非法:未知 option、缺少 package/runtime version、
version-exemptions带参数都会抛出错误;错误字符串写入 stderr。 - pnpm 不存在:当结果码为
127时,CLI 明确提示安装 pnpm 并确保它位于 PATH。 - 不兼容插件版本:外部结果中的
incompatible项会转换成可复制的allow-version ... --accept-risk命令,但 CLI 不会自动批准。 - 非零执行结果:写出插件管理器提供的
logPath,便于定位诊断日志。 - Git hosted plugin 构建限制:参数包含 git URL 形态时,失败后提示将精确的包名加入 profile 的
pnpm-workspace.yamlallowBuilds,再重试。 - 并发写入:版本豁免和 Desktop pnpm 操作都锁定 profile 的
package.json,等待时间为 120 秒,避免并发修改同一 profile 元数据。 - 输出流:外部管理器的
stdout/stderr由onOutput直接写回对应 process stream;大输出受outputBytes: 16384约束。外部管理器如何截断或记录日志,当前仓库没有实现证据。
Performance、运维与扩展点
性能相关的明确参数只有两个 120 秒超时(锁等待与 package lookup)以及 16384 字节输出限制。CLI 不实现缓存、重试或并行安装;这些能力若存在,应在 dsh-plugin-manager 中查找。运维时应优先检查 stderr 中的兼容性 warning、pnpm was not found 提示和失败结果的 logPath。
扩展插件时,优先通过 profile bundle 增加 package/module 映射,并让 runPluginCommand 处理安装;不要在 runPlugin() 中为每一个工具或模型新增硬编码分支。若确实需要新的 DSH-owned 参数,应扩展 versionCommand() 的严格解析规则,同时维持未知参数交给 pnpm 的约定。版本豁免必须保持 package version 与 exact DSH version 成对记录,避免把兼容风险扩大为 profile 全局开关。
Tests 与证据边界
在本次限定读取范围内未发现与 runPlugin() 直接对应的测试文件,因此具体测试覆盖率和断言集合无法从已读取源码确认。已确认的行为只来自 plugin.ts 的实现和自动生成的 composition.md;外部插件包的 API、模型适配器协议、工具注册机制和 profile 模板内容属于当前仓库证据之外的实现细节。