定制 DeepSeek Harness — customize-dsh
DSH 是"万物皆可插件"的宿主:配置严格校验,格式错误会导致启动失败(fail loud)。以下是常用定制面,但这是摘要,非权威来源。
权威参考
定制前先查权威资料,不要凭记忆猜字段:
- 配置目录(所有包的可配字段与默认值):https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/config-catalog.md(按
@deepseek-ai/dsh-*包分节) - profile / patch / bundle 分层:
packages/boot/app-boot/README.md#Profiles、apps/cli/reference/README.zh.md - 子系统文档(中文):https://github.com/deepseek-ai/deepseek-harness/tree/master/docs/subsystems(settings、skills、tools、permission-presets、system-prompt、approval、sandbox、agent-lifecycle)
- 组合与 HMR:
docs/cordis-tutorial/06-composition-and-hmr.zh.md - 用户开发指南:
docs/user/develop/basic/(config、tool、publish) - cookbook:
docs/cookbook/(adding-a-tool、adding-a-package、extension-cookbook) - agent preset:
packages/preset/agent-presets/README.zh.md
若字段未在此 skill 说明或需确认精确形状,抓取上述文档阅读原文,不要猜测。
使配置生效(热重载 vs 重启)
- cordis.yml / patch 层:HMR。
cordis-plugin-hmr监视文件,保存时按 entryid对比、事务式重应用(卸载旧实例→加载新实例),!!js表达式在仍运行的服务基础上重算。 - settings.yaml:
watch热发布(默认开启)。liveowner 立即生效;restartowner 只在构造期读一次。改动后通常无需重启。 - 技能:文件系统 watcher 监听技能目录,
skills/change事件使目录失效;模型侧目录在 digest 变化时自动替换,改完下一轮即生效。 - agent preset 默认值:热设置只影响此后创建的会话;已运行会话停在原 preset。
- 需重启:修改 profile
package.json的dsh.profile.bundles列表(用dsh plugin走重调和安装)、改 bundle 的 patch 声明。 - 运行时通道:动态 cordis 插件(
cordis_define/cordis_run/cordis_stop/cordis_undefine)与本机 dsh-super-injector(dev_inject_plugin/dev_install_package/dev_reload_package/dev_uninject_plugin等 dev_* 工具)可免重启注入/热重载/卸载即净。
文件存放位置
| 范围 | 路径 |
|---|---|
| Harness home | $DSH_HOME,缺省 ~/.dsh |
| 用户设置 | $DSH_HOME/settings.yaml(也可 .yml/.json) |
| home 级 patch(所有 profile 共享,优先于 profile 层) | $DSH_HOME/cordis.patch.yml |
| 用户凭据 | $DSH_HOME/.credentials.yaml |
| 用户环境层 | $DSH_HOME/.env(优先级低于继承环境与调用目录 .env) |
| profile 目录 | $DSH_HOME/profiles/<name>/(package.json、cordis.patch.yml、node_modules/、pnpm-workspace.yaml) |
| 安装后备扁平目录 | $DSH_HOME/profiles/node_modules/(符号链接,每次启动修复) |
| 项目技能 | <projectRoot>/.dsh/skills(rank 100) |
| 项目 agents 技能 | <projectRoot>/.agents/skills(rank 200) |
| 自定义技能目录 | Config.customSkillDirs(rank 300) |
| 用户技能 | $DSH_HOME/skills(rank 400) |
| 用户 agents 技能 | <agentsHome>/skills(rank 500) |
| 随包技能 | Config.bundledSkillDir(rank 600) |
| agent preset | $DSH_HOME/.agent-presets/<id>/(agent.cordis.yml + 可选 preset.yml + plugins/) |
项目根目录 = 最近的含 .git 的祖先目录;找不到时用 cwd。技能本地提供方跳过 $DSH_HOME 的 .system 子目录。
settings.yaml
顶级键 = 已注册 namespace 名。每个 namespace 解析为 schema 默认值 → 注册方组合 base → 用户分节。本机实例的真实形状:
agent-presets:
default: mcp-opt # 默认 agent preset 名(影响此后创建的会话)
permission:
defaultPreset: danger-full-access # 权限预设:workspace-write | danger-full-access
ui-theme:
preference: dark
ui-conversation:
busyEnter: steer
agent-default-model:
provider: opencode-go
model: deepseek-v4-flash
reasoningEffort: max
llm-pi-ai:
providers:
<provider-id>:
models: [{ id, name, contextWindow, maxTokens }]
apiKeyEnv: <环境变量名>
# 自定义 provider 可加:displayName / api: openai-completions / baseURL
- 外部编辑经
ctx.settings热发布;更新在写锁下先重读再原子写回,保留注释与未加载插件拥有的分节 - 启动时存在但非法的文档使插件加载失败;运行中不可读/不可解析的编辑只告警并保留最后可用分节
- provider 的
apiKeyEnv指向环境变量而非明文密钥;凭据解析顺序:继承环境 →.credentials.yaml→ 调用目录 .env →$DSH_HOME/.env
Profiles 与 patch 层
$DSH_HOME/profiles/<name>/ 是 profile 根。配置树层叠(后应用者胜):
空根节点
→ [package.json 的 dsh.profile.bundles 顺序] 每个 bundle 的 patch
→ profiles/<name>/cordis.patch.yml(profile 层)
→ $DSH_HOME/cordis.patch.yml(home 层,各 profile 共享)
→ 按 argv 顺序的 --patch <path> overlay
- profile 根文件
cordis.yml默认是空数组[]——编辑 cordis.patch.yml,不要直接改 cordis.yml package.json的 profile manifest:"dsh": { "profile": { "bundles": [...] } }(有序 bundle 层列表,如@deepseek-ai/dsh-base、@deepseek-ai/dsh-web-app);bundle 包自身用"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }声明其 patch 层- 内置模板:
web(base + web-app)、headless(base + headless)首次使用自动初始化;其他 profile 名缺省会 fail loud,需dsh plugin --profile <name> add <package> - 改 bundle 列表用
dsh plugin --profile <name> add/remove <pkg>(自动重调dsh.profile.bundles并安装),不要手改依赖后重启
cordis.patch.yml 是顶层 YAML 数组,每个元素是 patch 条目。三种语义:
# 1) insert:追加 entry
- insert:
- id: mcp-fastctx # 行 id(全局唯一;重复会冲突)
name: '@deepseek-ai/dsh-mcp-client' # 插件包名/相对路径
config: # 插件配置(随插件 schema 而异)
serverName: fastctx
transport: stdio
command: C:/path/to/binary
args: ['serve']
env: { KEY: '1' }
cwd: !!js process.cwd() # !!js 表达式在挂载时求值
toolCallTimeoutMs: 300000
# 2) id-targeted:命中 id 即【替换该 entry 的整份 config】——无深度合并,
# 必须重述要保留的所有字段
- id: some-plugin-id
name: '@deepseek-ai/dsh-xxx'
config: { ... }
# 3) disable:卸载 entry 但不删除
- disable: { id: some-plugin-id }
校验与失败规则:
- patch 命中组合树中不存在的 entry id → 仅 stderr 警告
- 空文件或仅注释文件 → throws(解析成 nothing 不是列表);用
[]禁用一个层 - 缺失/不可读/不可解析/非数组文件 → throws;
--patch指定的文件缺失也 throws - entry 不带
id→ 每次读取生成新 id → 任何配置编辑都被视为"删了再加"并重新挂载 - 启动失败(fail loud):boot/loader rejection → 一行标签 stderr +
exit(1)
动手前先验证:dsh --profile <name> --dump-config(离线渲染,不启动;--dump-default-config 只 dump 组合包各层)→ 确认行与覆盖无误再启动。
Skills(技能创作)
技能加载器扫描技能目录的直接条目:目录包 <name>/SKILL.md 或扁平文件 <name>.md(**/SKILL.md 递归不支持)。
---
name: my-skill
description: 一句话说明功能与触发条件。前置用户可能说的关键词或文件名。
disable-model-invocation: false # 可选,true 则模型目录不显示
user-invocable: true # 可选,默认 true
---
# My Skill
(正文:说明、示例、参考)
name必填,kebab-case(^[a-z0-9]+(?:-[a-z0-9]+)*$),与目录名一致description几乎必填:无 description 的 skill 被过滤。目录只展示 name+description(正文不进目录),description 默认上限 500 字符- 重名裁决:rank 低者赢,再按提供方顺序、本地顺序;最近 scope 层条目直接压过全局层
skill工具加载后返回<skill_content>、<skill_resources>、<skill_instructions>;资源(scripts/references/assets)按需解析,不枚举目录- 本环境技能库在
~/.agents/skills/(user-agents 层),与customize-opencode、skill-creator等并列
Agent presets
$DSH_HOME/.agent-presets/<id>/ 下存放 preset:
agent.cordis.yml # 插件行顶层列表(必填,preset 的组合)
preset.yml # 可选展示元信息:只有 name/description;id=目录名、trust 来自根,不可写
plugins/ # preset 自带插件(相对路径从 preset 目录解析)
- 目录名必须是合法 preset id(
[a-z0-9][a-z0-9-]*),否则跳过;损坏(YAML 无法解析/非具名插件行列表)以broken原因列出而非跳过 - 常驻挂载(standing mount)每个进程只一次;加入的会话共享一份工具/提示词/投影,插件按 Session/Agent 键控状态
- 视角解析顺序
agent → preset → global(近者遮蔽远者) settings.yaml的agent-presets.default选择默认 preset;roots/includeUserRoot可配置(缺省追加$DSH_HOME/.agent-presets为 user 根)- 切换限制:preset 只能在"尚未产生任何内容的空白会话"上切换(
recompose);默认值只影响此后创建的会话;preset 文件是输入而非持久化目标(loader 不写回) - 创作是 copy-only:
ctx.agentPresets.copy()从现有 preset 整目录复制;remove()只删 user 根下的 preset - preset 的权限 = 它引用的插件的权限(trust 只用于展示 system/user 差异,不强制隔离)
插件(cordis package)与自定义工具
- 最小插件形态(cookbook adding-a-tool):
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read a file from disk.',
parameters: {
path: { type: 'string', required: true, description: 'Absolute path' },
limit: { type: 'number' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
},
}))
}
defineTool在 execute 前按ParameterSchemaSpec校验参数(类型/必填/字面量/oneOf);output.schema声明规范 JSON 值;抛异常即isError;必须遵守exec.signal;dispose 插件 fiber 即注销工具- Code Mode:
tools插件 config 的mode: 'native' | 'code' | 'both'(both下模型可写 TS/Python 程序经run_code触达全部工具;run_code是保留名);工具错误类型ToolArgsError/ToolOutputError/ToolNotFoundError - 插件可配 Config 用
@deepseek-ai/schemastery的Schema.object(...)导出Config(不要导出普通对象);无效配置在加载时失败并给出明确错误;约定:不同部署可能需要不同值的参数都必须定义为配置字段 - 挂载通道:①
dsh.profile.bundles(随 profile 启动)②cordis.patch.yml的 insert(HMR 热生效)③ 运行时注入(super-injector dev_* 工具,免重启)④ 动态 cordis(cordis_define/cordis_run,会话级) - 工具注册表分层:宿主行与 repository 插件落 GLOBAL 层,preset 挂载落该 preset 层;MCP 桥接工具以
mcp__<serverName>__<tool>出现在 GLOBAL 层
MCP servers
通过 cordis.patch.yml 插入 @deepseek-ai/dsh-mcp-client 行(见"Profiles 与 patch 层"示例)。关键配置键:
serverName:桥接名,工具前缀mcp__<serverName>__<工具原始名>;须匹配[A-Za-z0-9_-]{1,32}且在存活 mcp-client 实例间唯一transport:stdio|streamable-http(官方只有这两种,没有remote)command+args:stdio 模式的可执行与参数(用绝对路径,避免 .cmd shim/npx 网络检查)env:传给服务器的环境变量;cwd:工作目录(常用!!js process.cwd())toolCallTimeoutMs:工具调用超时(慢服务器如 LSP/代码索引建议 300000)- 远程(streamable-http):
url+ 可选headers failOnStartupError:初始连接或工具同步失败时插件激活失败(默认行为是失败)
权限与沙箱
权限预设把两个 knob 捆绑成具名预设:
| 预设 | sandbox/mode | approval/policy |
|---|---|---|
workspace-write |
workspace-write |
ask |
danger-full-access |
danger-full-access |
never |
custom是保留名(派生的"非预设"状态,不可切换)SandboxMode:read-only(拒绝写入)|workspace-write(工作区根+后端临时区)|danger-full-access(绕过隔离,直接 spawn 原始 argv)ApprovalPolicy:ask(委托应答者链,无应答 fail-closed 为 unavailable)|never(确定性rejected,不分发应答者)settings.yaml的permission.defaultPreset设置默认;新会话进程后备默认workspace-write(DSH_PERMISSION_MODE可改)- 会话被拒操作报告
[sandbox: ...]标记——属策略拒绝而非命令失败,不要绕道重试;如本会话允许且真实被拒,可用sandbox_permissions一次性升级精确命令
命令与系统提示
- slash 命令是插件级 API(非文件):插件用
ctx.commands.register(CommandDefinition)注册(name小写无斜杠/description/input?.hint/handler),执行结果直接呈现给 UI 不经模型;生命周期事件command/run、command/done,注册变更commands/change - 系统提示词是插件级 API:
ctx.systemPrompt.section({ name, order, text|complete })注册提示词段落(惯例:-100宿主身份、0部署 persona、工具指引 100–199;complete: true段落独占整份提示词,多于一个失败);context()/variable()注册动态上下文与插值变量 - 用户级 persona 定制:
@deepseek-ai/dsh-persona插件(patch 挂一行即可),config 含text/complete/includeRuntimeContext,渲染为deployment:persona段,支持{{variable}}插值;系统提示词包@deepseek-ai/dsh-system-prompt另有persona/toolOrder/includeHarnessIdentity/includeRuntimeContext配置键(改 compose 内置配置前先用--dump-config查确切条目 id) - 想在 UI/提示词层面定制 persona,优先走
@deepseek-ai/dsh-persona插件或 agent preset,而非改文件
逃生舱 / 自愈
- 先验证不启动:
dsh --profile <name> --dump-config(离线渲染 patch 层);[]可禁用一个 patch 层;disabled: true卸载 entry 不删 - patch 重复崩溃:
dev_fix_patch修复~/.dsh/profiles/*/cordis.patch.yml重复 loader entry id(备份原文件) - link 依赖悬空:
dev_heal_links重建缺失的 node_modules junction - 路由残留:
dev_clear_routes清理 webserver 路由表残留(插件热重载残留) - 卸载即净:
dev_uninject_plugin卸载注入插件并写 disabled 条目防 include.refresh 加回 - 全部自检:
dev_self_test一键回归注入器全链路
编辑建议
- 写入前对照 config-catalog / 子系统文档验证;不确定就抓原文
- profile 定制改
cordis.patch.yml,不要改cordis.yml;id-targeted patch 替换整份 config,必须重述要保留的所有字段 - 技能/插件/preset 定义优先用文件;保留用户未要求修改的已有字段与注释
- 改动后按"使配置生效"一节告知用户:patch/settings/技能多可热生效,bundle 列表改动需
dsh plugin重调后重启