DeepSeek Harness(命令 dsh)是 DeepSeek 开源的编程 agent,一条 npm 命令就能装好。有 DeepSeek 官方 key 可直接用;只有 OpenRouter key 时需多做两步。本文基于 0.1.7-rc.2 实测,它仍是开发者预览版。
官方 key 直连;OpenRouter key 要经本地垫片转一道,因为两边格式有两处对不上。
dsh→api.deepseek.comdsh→本地垫片 127.0.0.1:18080→openrouter.ainull 改成 0一条 npm 命令即可,前提是本机有 Node.js 和 npm(官方要求 Node ^22.19.0 或 >=24,见 GitHub 仓库根目录 package.json 的 engines;我们在 v24.14.1 上验证过)。
npm install -g @deepseek-ai/dsh@latest
dsh --version # 输出 0.1.7-rc.2 即成功
latest 标签目前指向候选版 0.1.7-rc.2,不是正式版。~/.dsh/,卸载用 npm rm -g @deepseek-ai/dsh,再删这个目录即可清干净。npx @deepseek-ai/dsh web。没有 key 什么都跑不了。DeepSeek 官方 key 在 platform.deepseek.com 的 API Keys 页创建,按用量付费;两种填法任选一种:
dsh web 后进 Settings → Models,粘贴保存,存进 dsh 自己的凭据库。DEEPSEEK_API_KEY,命令行和脚本用这种更方便。export DEEPSEEK_API_KEY=<你的 DeepSeek API Key> # 写进 ~/.zshrc 可长期生效
sk-or-v1- 开头)的,直接这样填会失败,走路线 B,见下面「用 OpenRouter key 接入」。日常用 Web 界面,批量或脚本用 headless。命令行模式以启动时所在目录为工作区,Web 界面在页面上选工作区;用命令行时先 cd 进项目再启动。
| 用法 | 命令 | 适合 |
|---|---|---|
| Web 界面 | dsh web(可加 --port 8080、--no-open) | 多轮对话、看 diff 和每一步轨迹 |
| 命令行一次性 | dsh headless "任务" | 脚本、CI、批量任务 |
| 程序对接 | dsh --profile sdk / dsh --profile acp | 嵌进自己的程序或编辑器 |
headless 的常用写法:
dsh headless "跑一下测试,总结失败原因"
echo "审查这段改动:$(git diff)" | dsh headless - # - 表示从 stdin 读任务,stdin 全文就是任务
dsh headless --json "跑一下测试" # 输出机器可读的事件流
dsh headless --session-id session-xxxx "接着上次的继续" # 把 session-xxxx 换成真实会话 id
会话 id 形如 session-<uuid>,在 --json 事件流开头的 session 事件里。续接时必须在原来的目录下执行,否则会报错退出。
答案输出到 stdout,诊断信息到 stderr。其他帮助:dsh --help、dsh web --help、dsh headless --help。
建议先在一个不怕改坏的小项目里试。我们用一个含 3 个失败测试的 Python 小项目跑过下面的流程。
cd ~/你的项目。dsh headless "用一句话介绍这个目录里有哪些文件"。dsh headless "运行 python3 -m pytest -q,修复导致测试失败的 bug,不要改测试文件,修完再跑一遍"。实测它自己跑测试、改代码、复跑,3 条测试全部通过。dsh web,关掉「Internal Testing Notice」提示,点「Choose workspace」选项目文件夹。单次实测两个任务合计花费约 $0.004(OpenRouter 账单接口读数,模型 deepseek-v4-flash)。
用 OpenRouter key 要多做两步:写一份配置覆盖文件,再跑一个本地垫片。原因是 dsh 按 DeepSeek 自家的 Anthropic 格式发请求,而 OpenRouter 的 Anthropic 兼容接口有两处对不上:
| 问题 | 报错 | 处理 |
|---|---|---|
dsh 开思考模式时不带 budget_tokens,OpenRouter 校验不过 | INVALID_REQUEST: Invalid Anthropic Messages API request | 配置里 thinking: disabled |
OpenRouter 流式返回里 cache_read_input_tokens 等字段为 null,dsh 要求是数字 | MALFORMED_RESPONSE: ... invalid cache_read_input_tokens | 本地垫片把 null 改成 0 |
~/.dsh/cordis.patch.yml对所有 profile 生效,删掉这个文件即恢复默认;没跑过 dsh 的先执行 mkdir -p ~/.dsh。
- id: llm-deepseek
config:
baseURL: http://127.0.0.1:18080/api # 指向本地垫片
thinking: disabled
models:
- id: deepseek/deepseek-v4-flash
name: DeepSeek-V4-Flash (OpenRouter)
contextWindow: 1048576
maxTokens: 131072
inputModalities: [text]
- id: deepseek/deepseek-v4-pro
name: DeepSeek-V4-Pro (OpenRouter)
contextWindow: 1024000 # OpenRouter 上 Pro 的上游上限
maxTokens: 384000
inputModalities: [text]
- id: agent-default-model
config:
provider: deepseek-official
model: deepseek/deepseek-v4-flash
~/.dsh/openrouter-shim.mjsimport http from 'node:http';
const PORT = Number(process.argv[2] ?? 18080);
const UPSTREAM = 'https://openrouter.ai';
const fixNulls = (s) => s.replace(/"(cache_creation_input_tokens|cache_read_input_tokens)":null/g, '"$1":0');
http.createServer(async (req, res) => {
try {
const chunks = [];
for await (const c of req) chunks.push(c);
const body = Buffer.concat(chunks);
const headers = { ...req.headers };
delete headers.host;
delete headers['content-length'];
delete headers['accept-encoding'];
const up = await fetch(UPSTREAM + req.url, { method: req.method, headers, body: body.length ? body : undefined });
res.writeHead(up.status, { 'content-type': up.headers.get('content-type') ?? 'application/json' });
if (!up.body) return res.end();
const decoder = new TextDecoder();
let pending = '';
for await (const chunk of up.body) {
pending += decoder.decode(chunk, { stream: true });
const cut = pending.lastIndexOf('\n') + 1; // 只改整行,避免字段被切开
if (cut > 0) {
res.write(fixNulls(pending.slice(0, cut)));
pending = pending.slice(cut);
}
}
res.end(fixNulls(pending + decoder.decode()));
} catch (err) {
console.error(new Date().toISOString(), req.url, err.message);
if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });
res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: `shim: ${err.message}` } }));
}
}).listen(PORT, '127.0.0.1', () => console.error(`openrouter-shim listening on 127.0.0.1:${PORT}`));
重启电脑后垫片要重新开:
node ~/.dsh/openrouter-shim.mjs & # 先起垫片
export DEEPSEEK_API_KEY=<你的 OpenRouter Key> # sk-or-v1- 开头
cd ~/你的项目 && dsh web # 或 dsh headless "任务"
成功标志:启动后 Web 界面模型栏显示 DeepSeek-V4-Flash (OpenRouter)。OpenRouter 上 deepseek-v4-flash 的标价是输入 $0.14、输出 $0.28 / 百万 token(2026-09-28 从 OpenRouter models 接口读取)。
| 现象 | 原因 | 怎么办 |
|---|---|---|
浏览器打开 127.0.0.1:3080 返回 401 | Web 界面需要访问 token | 用终端打印的完整地址(带 ?token=...)打开 |
dsh tui 报 profile "tui" does not exist | 0.1.7-rc.2 没有随附终端界面,网上教程常写错 | 终端里用 dsh headless |
| 点「Choose workspace」页面没反应 | 它弹的是 macOS 系统自带的选文件夹窗口,常被浏览器挡在后面 | 切到那个窗口选好再点 Choose;别反复点,每点一次弹一个 |
| 它改了别的目录里的文件,或找不到项目文件 | 命令行模式的工作区就是启动时所在目录 | 先 cd 进项目再启动 |
报 no API key for provider route | 没配 key | 见「配置 API Key」 |
用 OpenRouter key 报 INVALID_REQUEST 或 MALFORMED_RESPONSE | 格式不兼容 | 见「用 OpenRouter key 接入」,并确认垫片在跑 |
| 想接着上次的会话继续 | — | dsh headless --session-id session-xxxx "继续";id 在 --json 事件流开头的 session 事件里,必须在原目录下执行 |
~/.dsh,升级后本文的命令和配置可能失效。export DSH_TELEMETRY_DISABLED=1。