Open Design + DeepSeek Harness 配置和补丁
更新时间:2026-10-23 15:19:00
版本信息:OpenDesign 0.26.1 + DeepSeek Harness 0.2.1-rc.2 ,Windows x64
概要:当前官方Open Design是不支持 DSH 0.2.x版本的,需要手动加个补丁才可以,AI可以完成这个任务,但是让AI从头探索一遍比较慢,这里记录一下过程,也可以让AI读这篇文章去执行。
补丁文件:放到DSH配置文件这个路径下.dsh\profiles\open-design\node_modules\@open-design\dsh-runtime\dist\index.js
// src/index.ts
import { randomUUID } from "node:crypto" ;
import { createInterface } from "node:readline" ;
import {
installModelSelection
} from "@deepseek-ai/dsh-agent" ;
import {
createUserMessage,
ReasoningEffortId
} from "@deepseek-ai/dsh-llm" ;
import { SessionId } from "@deepseek-ai/dsh-session" ;
// src/protocol.ts
var PROTOCOL_VERSION = 1;
var RUNTIME_NAME = "open-design" ;
var CAPABILITIES = {
session_resume: true ,
session_cancel: true ,
structured_events: true
};
function modelsFrame(models) {
return {
v: PROTOCOL_VERSION,
type: "models" ,
runtime: RUNTIME_NAME,
models
};
}
function record(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null ;
}
function nonEmpty(value) {
return typeof value === "string" && value.trim().length > 0;
}
function parseHostCommand(value) {
const input = record(value);
if (!input || input.v !== 1 || !nonEmpty(input.type) || !nonEmpty(input.request_id)) {
throw new Error( "invalid protocol envelope" );
}
if (input.type === "cancel" ) {
return { v: 1, type: "cancel" , request_id: input.request_id };
}
if (input.type !== "execute" || !nonEmpty(input.cwd) || !nonEmpty(input.prompt) || !Array.isArray(input.mcp_servers)) {
throw new Error( "invalid execute command" );
}
const model = record(input.model);
if (input.model !== void 0 && (!model || !nonEmpty(model.provider) || !nonEmpty(model.id))) {
throw new Error( "invalid model selection" );
}
if (input.resume_session_id !== void 0 && !nonEmpty(input.resume_session_id)) {
throw new Error( "invalid resume session id" );
}
if (input.reasoning_effort !== void 0 && !nonEmpty(input.reasoning_effort)) {
throw new Error( "invalid reasoning effort" );
}
return {
v: 1,
type: "execute" ,
request_id: input.request_id,
cwd: input.cwd,
prompt: input.prompt,
mcp_servers: input.mcp_servers,
...input.resume_session_id === void 0 ? {} : { resume_session_id: input.resume_session_id },
...model === null ? {} : { model: { provider: model.provider, id: model.id } },
...input.reasoning_effort === void 0 ? {} : { reasoning_effort: input.reasoning_effort }
};
}
function identityFrame(type, pluginVersion) {
return {
v: PROTOCOL_VERSION,
type,
runtime: RUNTIME_NAME,
protocol_version: PROTOCOL_VERSION,
plugin_version: pluginVersion,
capabilities: CAPABILITIES
};
}
// src/index.ts
var name = "open-design-runtime" ;
var inject = [
"openDesignStartup" ,
"agentDefaultModel" ,
"agents" ,
"llm" ,
"sessions" ,
"sessionPersistence"
];
var PLUGIN_VERSION = "0.1.0" ;
var PROCESS_EXIT_FALLBACK_MS = 1e3;
function writeFrame(output, frame) {
output.write(${JSON.stringify(frame)} );
}
function errorFacts(error, fallbackCode) {
const candidate = typeof error === "object" && error !== null ? error : void 0;
const code = typeof candidate?.code === "string" && candidate.code !== "" ? candidate.code : fallbackCode;
if (error instanceof Error && error.message !== "" ) return { code, message: error.message };
if ( typeof candidate?.message === "string" && candidate.message !== "" ) {
return { code, message: candidate.message };
}
if ( typeof error === "string" && error !== "" ) return { code, message: error };
return { code, message: "DeepSeek Harness reported an execution error." };
}
function contentText(content) {
const text = [];
const visit = (blocks) => {
for (const block of blocks) {
if (block.type === "text" || block.type === "reasoning" ) text.push(block.text);
if (block.type === "tool-result" ) visit(block.content);
}
};
visit(content);
return text.join( "" );
}
function resultStatus(reason) {
if (reason?.kind === "completed" || reason?.kind === "max-tokens" ) return "completed" ;
if (reason?.kind === "aborted" ) return "cancelled" ;
return "failed" ;
}
function resultError(reason) {
if (resultStatus(reason) !== "failed" ) return void 0;
if (!reason) {
return {
code: "DSH_PROFILE_MISSING_TURN_END" ,
message: "DeepSeek Harness became idle without reporting how the turn ended."
};
}
if (reason.kind === "error" ) return errorFacts(reason.error, "DSH_PROFILE_TURN_FAILED" );
if (reason.kind === "blocked" ) {
return {
code: "DSH_PROFILE_TURN_BLOCKED" ,
message: "DeepSeek Harness blocked the turn before it completed."
};
}
return {
code: "DSH_PROFILE_TURN_FAILED" ,
message: DeepSeek Harness ended the turn with reason "${reason.kind}" .
};
}
function terminalOutput(output) {
return output === "" ? {} : { output };
}
function writeCancelledResult(output, requestId, sessionId) {
writeFrame(output, {
v: 1,
type: "result" ,
request_id: requestId,
status: "cancelled" ,
session_id: sessionId,
resume_rejected: false
});
}
function requestProfileExit(exit, forceExit = () => process.exit(0), schedule = (callback, delayMs) => setTimeout(callback, delayMs)) {
exit(0);
schedule(forceExit, PROCESS_EXIT_FALLBACK_MS).unref();
}
function createCancellationLatch(cancelActiveAgent) {
const pendingRequestIds = /* @PURE */ new Set();
let activeRequest;
const cancelActive = () => {
if (!activeRequest || activeRequest.controller.signal.aborted) return ;
activeRequest.controller.abort();
cancelActiveAgent();
};
return {
activate(requestId, controller) {
activeRequest = { requestId, controller };
if (pendingRequestIds. delete (requestId)) cancelActive();
},
cancel(requestId) {
if (activeRequest?.requestId === requestId) {
cancelActive();
return ;
}
pendingRequestIds.add(requestId);
}
};
}
function usageFrame(requestId, provider, model, usage) {
return {
v: 1,
type: "usage" ,
request_id: requestId,
provider,
model,
input_tokens: usage.inputTokens,
output_tokens: usage.outputTokens,
...usage.cacheReadTokens === void 0 ? {} : { cache_read_tokens: usage.cacheReadTokens },
...usage.cacheWriteTokens === void 0 ? {} : { cache_write_tokens: usage.cacheWriteTokens }
};
}
async function listModelCatalog(ctx) {
const catalog = [];
for (const provider of ctx.llm.listProviders()) {
try {
for (const model of await ctx.llm.listModels(provider.id)) {
const resolved = await ctx.llm.resolveModelInfo(provider.id, model.id). catch (() => null );
catalog.push({
provider: provider.id,
provider_name: provider.name,
id: model.id,
name: model.name,
...resolved?.reasoning?.efforts.length ? {
reasoning_options: resolved.reasoning.efforts.map((effort) => ({
id: String(effort.id),
name: effort.name,
...resolved.reasoning?.defaultEffort === effort.id ? { default : true } : {}
}))
} : {}
});
}
} catch {
ctx.logger.warn(open-design-runtime: could not list models for provider "${provider.id}" );
}
}
return catalog;
}
function emitSessionEvent(output, request, provider, model, event) {
switch (event.type) {
case "assistant/chunk" : {
const chunk = event.data.chunk;
if (chunk.type === "text-delta" && chunk.text !== "" ) {
writeFrame(output, { v: 1, type: "text" , request_id: request.request_id, content: chunk.text });
} else if (chunk.type === "reasoning-delta" && chunk.text !== "" ) {
writeFrame(output, { v: 1, type: "thinking" , request_id: request.request_id, content: chunk.text });
}
return ;
}
case "tool/call" :
writeFrame(output, {
v: 1,
type: "tool_call" ,
request_id: request.request_id,
call_id: String(event.data.callId),
name: event.data.name,
arguments: event.data.arguments
});
return ;
case "tool/result" : {
const toolMessage = record(event.data?.message) ?? {};
const toolContent = Array.isArray(toolMessage.content) ? toolMessage.content : [];
const nestedBlock = toolContent[0]?.type === "tool-result" ? toolContent[0] : null ;
writeFrame(output, {
v: 1,
type: "tool_result" ,
request_id: request.request_id,
call_id: String(toolMessage.toolCallId ?? nestedBlock?.toolCallId ?? "" ),
name: event.data.error?.name ?? "tool" ,
output: nestedBlock ? contentText(nestedBlock.content) : contentText(toolContent),
is_error: nestedBlock ? nestedBlock.isError === true : toolMessage.isError === true
});
return ;
}
case "assistant/message" : {
if (event.data.usage) writeFrame(output, usageFrame(request.request_id, provider, model, event.data.usage));
const messageContent = Array.isArray(event.data.message?.content) ? event.data.message.content : [];
const text = messageContent.filter((block) => block.type === "text" ).map((block) => block.text).join( "" );
if (text.trim() !== "" ) writeFrame(output, { v: 1, type: "text" , request_id: request.request_id, content: text });
return ;
}
default :
return ;
}
}
async function execute(ctx, request, output, onHandle, signal) {
const defaultSelection = ctx.agentDefaultModel.currentSelection();
const baseSelection = request.model ? { provider: request.model.provider, model: request.model.id } : defaultSelection;
const selection = request.reasoning_effort ? { ...baseSelection, reasoningEffort: ReasoningEffortId(request.reasoning_effort) } : baseSelection;
const sessionId = SessionId(request.resume_session_id ?? od-${randomUUID()});
let handle;
let firstSeq = Number.POSITIVE_INFINITY;
let turnEnd;
let assistantOutput = "" ;
const setup = (agentCtx) => {
const selected = { current: selection, assembled: void 0 };
installModelSelection(agentCtx, selected);
};
let disposeEvent = () => {
};
const onSessionEvent = (session, event) => {
if (String(session.id) !== String(sessionId) || event.seq < firstSeq) return ;
emitSessionEvent(output, request, selection.provider, selection.model, event);
if (event.type === "assistant/chunk" && event.data.chunk.type === "text-delta" ) {
assistantOutput += event.data.chunk.text;
}
if (event.type === "assistant/message" ) {
const messageContent2 = Array.isArray(event.data.message?.content) ? event.data.message.content : [];
for (const block of messageContent2) {
if (block.type === "text" && typeof block.text === "string" ) assistantOutput += block.text;
}
}
if (event.type === "turn/end" ) turnEnd = event;
};
try {
try {
handle = request.resume_session_id ? await ctx.agents.resume({ resumeSessionId: sessionId, agentOptions: selection, setup, signal }) : await ctx.agents.create({
sessionId,
meta: { cwd: request.cwd },
agentOptions: selection,
setup,
signal
});
onHandle(handle);
} catch (error) {
if (signal.aborted) {
writeCancelledResult(output, request.request_id, String(sessionId));
return ;
}
const facts = errorFacts(error, request.resume_session_id ? "DSH_PROFILE_RESUME_REJECTED" : "DSH_PROFILE_SESSION_CREATE_FAILED" );
writeFrame(output, {
v: 1,
type: "result" ,
request_id: request.request_id,
status: "failed" ,
session_id: String(sessionId),
resume_rejected: Boolean(request.resume_session_id),
error: facts
});
return ;
}
writeFrame(output, {
v: 1,
type: "session" ,
request_id: request.request_id,
session_id: String(sessionId),
resumed: Boolean(request.resume_session_id)
});
await handle.agent.whenIdle();
if (signal.aborted) {
writeCancelledResult(output, request.request_id, String(sessionId));
return ;
}
firstSeq = handle.agent.session.seq + 1;
disposeEvent = ctx.on( "session/event" , onSessionEvent);
await handle.agent.followup(createUserMessage({
content: [{ type: "text" , text: request.prompt }],
source: { kind: "user" }
}));
await handle.agent.whenIdle();
await ctx.sessions.flush(handle.agent.session);
if (signal.aborted) {
writeCancelledResult(output, request.request_id, String(sessionId));
return ;
}
const reason = turnEnd?.data.reason;
const status = resultStatus(reason);
const failed = resultError(reason);
writeFrame(output, {
v: 1,
type: "result" ,
request_id: request.request_id,
status,
session_id: String(sessionId),
...terminalOutput(assistantOutput),
stop_reason: reason?.kind ?? "unknown" ,
resume_rejected: false ,
...failed === void 0 ? {} : { error: failed }
});
} catch (error) {
if (signal.aborted) {
writeCancelledResult(output, request.request_id, String(sessionId));
return ;
}
writeFrame(output, {
v: 1,
type: "result" ,
request_id: request.request_id,
status: "failed" ,
session_id: String(sessionId),
resume_rejected: false ,
error: errorFacts(error, "DSH_PROFILE_EXECUTION_FAILED" )
});
} finally {
disposeEvent();
await handle?.dispose(). catch (() => void 0);
onHandle(void 0);
}
}
async function serve(ctx, output, exit, input = process.stdin, finishProfile = requestProfileExit) {
writeFrame(output, identityFrame( "ready" , PLUGIN_VERSION));
const lines = createInterface({ input, crlfDelay: Infinity });
let requestId = null ;
let handle;
let task;
const cancellation = createCancellationLatch(() => {
handle?.agent.cancel({ kind: "user" });
});
await new Promise((resolve) => {
let settled = false ;
const settle = () => {
if (settled) return ;
settled = true ;
lines.close();
input.destroy();
resolve();
};
lines.on( "line" , (line) => {
if (line.trim() === "" ) return ;
let command;
try {
command = parseHostCommand(JSON.parse(line));
} catch {
writeFrame(output, {
v: 1,
type: "protocol_error" ,
...requestId ? { request_id: requestId } : {},
code: "DSH_PROFILE_INVALID_COMMAND" ,
message: "OpenDesign sent an invalid profile command."
});
return ;
}
if (command.type === "cancel" ) {
cancellation.cancel(command.request_id);
return ;
}
if (task) {
writeFrame(output, {
v: 1,
type: "protocol_error" ,
request_id: command.request_id,
code: "DSH_PROFILE_BUSY" ,
message: "This profile process accepts exactly one execute command."
});
return ;
}
requestId = command.request_id;
const taskAbort = new AbortController();
cancellation.activate(requestId, taskAbort);
task = execute(
ctx,
command,
output,
(nextHandle) => {
handle = nextHandle;
},
taskAbort.signal
);
void task.finally(settle);
});
lines.on( "close" , () => {
if (!task) settle();
});
});
finishProfile(exit);
}
function apply(ctx) {
const startup = ctx.openDesignStartup;
const exit = ctx.get( "appExit" );
if (!startup || !exit) throw new Error( "open-design-runtime requires startup and appExit services" );
if (startup.mode === "probe" ) {
writeFrame(process.stdout, identityFrame( "probe" , PLUGIN_VERSION));
exit(0);
return ;
}
void ctx.get( "loader" )?.await().then(async () => {
if (startup.mode === "models" ) {
writeFrame(process.stdout, modelsFrame(await listModelCatalog(ctx)));
exit(0);
return ;
}
await serve(ctx, process.stdout, exit);
}). catch ((error) => {
process.stderr.write(open-design-runtime: ${error instanceof Error ? error.message : String(error)} );
exit(1);
});
}
var internals = {
contentText,
createCancellationLatch,
errorFacts,
emitSessionEvent,
execute,
listModelCatalog,
requestProfileExit,
resultStatus,
resultError,
serve,
terminalOutput
};
export {
apply,
inject,
internals,
name
}
------以下为AI总结的执行过程,实测有效-----
# OpenDesign × DeepSeek Harness 配置手册
适用:Windows x64,OpenDesign 0.26.1 + DeepSeek Harness 0.2.1-rc.2
记录时间:2026-10-23 实测通过的路径,不是理论推演。
一句话结论:装 OD → 让dsh进 PATH → 把 OD 自带的 dsh 运行时装成open-designprofile → 打三段协议补丁。
前三步只是"接线",第四步(协议补丁)是 0.2.x 必须做的,不做就是测试失败: exit 0。脱敏说明:本文已移除个人机器信息——所有绝对路径改为
%TEMP%/%LOCALAPPDATA%/%USERPROFILE%等通用占位符或<DSH 安装目录>占位符,示例中的cwd与 API endpoint 也做了泛化。命令本身可直接照抄,只需把占位符换成你本机的真实值。
0. 先看症状,快速定位
| 你看到的 | 真正的原因 | 跳到 |
|---|---|---|
列表里根本找不到 DeepSeek Harness / not-on-path |
dsh 不在启动 OD 的那个进程的 PATH 里 |
§2 |
version-probe-failed,版本串是空的 |
设了 DSH_BIN 且指向 .cmd,OD 绕过了 shim 处理 |
§2 末尾 |
| 双击 / 从工具里启动 OD,进程秒退 | 环境变量残留 ELECTRON_RUN_AS_NODE=1,Electron 被当成 node 跑 |
§1 末尾 |
能识别、能选,但点"测试"→ 测试失败: exit 0 |
DSH 0.2.x 没有 assistant/chunk 事件,适配器发不出 text 帧 |
§5 |
有 text 帧但工具调用不显示结果 |
tool/result 载荷结构变了,适配器抛异常 |
§5 |
patch: entry "dsh-llm-pi-ai" not found |
patch 里插件 id 写错,应为 llm-pi-ai |
§4 |
| 模型跑的是 DeepSeek 官方账号,不是你的 key | 上面那条没修好,配置层整段没生效,回退到默认 provider | §4 |
1. 安装 OpenDesign
下载(国内网络必须注意)
GitHub Release 直连实测只有 ~11 KB/s(422MB 要 11 小时),别用。按速度排序:
| 源 | 实测速度 | 说明 |
|---|---|---|
https://gh-proxy.org/ + GitHub 原链 |
~2.1 MB/s | 首选,约 4 分钟 |
https://releases.open-design.ai/stable/versions/<ver>/... |
~220 KB/s | 官方 CDN,约 32 分钟,可作兜底 |
$dir = "$env:TEMP\od"; New-Item -ItemType Directory -Force $dir | Out-Null
$u = "https://gh-proxy.org/https://github.com/nexu-io/open-design/releases/download/open-design-v0.26.1/open-design-0.26.1-win-x64-setup.exe"
curl.exe -sS -L --no-progress-meter --retry 5 --retry-delay 3 -o "$dir\setup.exe" $u
校验(必做,镜像源可以换,哈希不能变)
(Get-FileHash "$dir\setup.exe" -Algorithm SHA256).Hash.ToLower()
# 期望:704a996e2a4f4a0a5a3dcac18d80e26ff163ce37cc9bda4e6004c0a8dc524f3c
静默安装
$p = Start-Process "$dir\setup.exe" -ArgumentList '/S' -Wait -PassThru
"exit=$($p.ExitCode)" # 0 即成功
# 安装目录:%LOCALAPPDATA%\Programs\Open Design
坑:装完启动不起来 / 进程秒退
如果当前环境有ELECTRON_RUN_AS_NODE=1(有些 agent 运行时会给子进程预置这个变量),Electron 会以 node 模式启动、无脚本可用、立刻退出——而且没有任何报错。启动前先清掉:Remove-Item Env:\ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue
2. 让 dsh 可被发现
OD 是 spawn("dsh", ...),靠 PATH 找二进制。用户的持久化 PATH 必须在新进程里生效(Explorer 启动的 OD 会读到)。
# 确认 dsh 的真实位置(典型:<DSH 安装目录>\resources\runtime\cli\bin\dsh.cmd)
$dshDir = Split-Path (Get-Command dsh).Source
# 写入用户 PATH(若尚未包含)
$user = [Environment]::GetEnvironmentVariable('Path','User')
if(($user -split ';') -notcontains $dshDir){
[Environment]::SetEnvironmentVariable('Path', ($user.TrimEnd(';') + ';' + $dshDir), 'User')
}
验证(模拟 Explorer 会给的 PATH):
$machine = [Environment]::GetEnvironmentVariable('Path','Machine')
$user = [Environment]::GetEnvironmentVariable('Path','User')
$env:PATH = "$machine;$user"
dsh --version # 应输出 0.2.1-rc.2 之类
坑:不要设
DSH_BIN
OD 文档说"二进制不在 PATH 上时才用DSH_BIN"。实测把它指向dsh.cmd会让 OD 报version-probe-failed(拿到空版本串),因为显式路径会绕过它对 Windows shim 的处理。清掉这个变量,只靠 PATH。[Environment]::SetEnvironmentVariable('DSH_BIN', $null, 'User')
3. 把 OD 自带的运行时装成 dsh profile
OD 安装包里已经内置了适配器组件,不需要联网找:
%LOCALAPPDATA%\Programs\Open Design\resources\open-design\agent-runtimes\deepseek-harness\
open-design-dsh-runtime-0.1.0.tgz
manifest.json ← 含 packageName / version / sha256
$a = "$env:LOCALAPPDATA\Programs\Open Design\resources\open-design\agent-runtimes\deepseek-harness"
$tgz = "$a\open-design-dsh-runtime-0.1.0.tgz"
# 校验(包内 manifest 声明的哈希)
(Get-FileHash $tgz -Algorithm SHA256).Hash.ToLower()
# 期望:5a86c62cd791600cd150ce2539f083f24059607500e8f23f628a3bce19465a83
# 装成 profile(会自动初始化 %USERPROFILE%\.dsh\profiles\open-design)
dsh plugin --profile open-design add "$tgz"
第一次装完立刻验证协议:
dsh --profile open-design --probe
# {"v":1,"type":"probe","runtime":"open-design","protocol_version":1,"plugin_version":"0.1.0",
# "capabilities":{"session_resume":true,"session_cancel":true,"structured_events":true}}
dsh --profile open-design --models
# {"v":1,"type":"models", ... 列出 provider/model ...}
--probe 通了只代表握手没问题,不代表能跑任务(§5 的坑就发生在这之后)。
4. 配置模型提供方
profile 的补丁层文件:%USERPROFILE%\.dsh\profiles\open-design\cordis.patch.yml
两个硬性要求:
- 插件 id 必须是
llm-pi-ai。写成dsh-llm-pi-ai会报patch: entry not found,整段配置静默失效,然后悄悄回退到 DeepSeek 官方账号额度——你会以为"配置没生效"。 providers下必须给全baseURL/apiKeyEnv/models,只写model是不够的。
以走百度千帆(BAIDU_API_KEY 放在 ~/.dsh/.credentials.yaml 里)为例:
- id: agent-default-model
config:
provider: baidu
model: deepseek-v4.1-flash
- id: llm-pi-ai
config:
providers:
baidu:
displayName: qianfan
apiKeyEnv: BAIDU_API_KEY
api: openai-completions
baseURL: https://qianfan.baidubce.com/v2
models:
- id: deepseek-v4.1-flash
- id: deepseek-v4-pro-0813
- id: glm-5.3
- id: qianfan-code-latest
baseURL填千帆控制台给你的 OpenAI 兼容 endpoint。不同产品/资源包可能给出带额外子路径的专用地址,以控制台显示的为准,不要照抄本文示例。
验证配置层干净 + 模型出现:
dsh --profile open-design --dump-config | Select-String 'agent-default-model' -Context 0,3
# 不能出现 "patch: entry ... not found"
dsh --profile open-design --models
# 输出里应能看到 "provider":"baidu","provider_name":"qianfan","id":"deepseek-v4.1-flash"
OD 界面的模型下拉只有一项
Default (CLI config)—— 这是正常的,它表示"用 DSH 自己的默认模型",也就是上面配的 baidu / deepseek-v4.1-flash。
4.1 为什么必须写两条 id(少一条都不行)
cordis.patch.yml 是按插件 id 定向打补丁的,每条 - id: 对应组合后插件树里的一个插件条目。这两条解决的是完全不同层面的问题,不能合并:
| id | 对应的插件 | 管什么 | 打个比方 |
|---|---|---|---|
llm-pi-ai |
@deepseek-ai/dsh-llm-pi-ai |
provider 的连接方式与能力清单:baseURL、apiKeyEnv、api 协议、models 列表 |
说明"哪些模型存在、怎么连" |
agent-default-model |
@deepseek-ai/dsh-agent-default-model |
agent 起手选哪一个 provider/model | 说明"默认用哪个" |
- 只写
llm-pi-ai:provider 定义好了,但没人选中它 → agent 仍走别家默认(deepseek-official官方账号)。 - 只写
agent-default-model指向baidu:指向一个不存在的 provider → 解析不出来。
这正好解释了"配了千帆却还在跑 DeepSeek 官方账号":id 写成 dsh-llm-pi-ai 时整条 patch 应用失败,provider 根本没建出来,agent-default-model 也就只能退回官方账号。
4.2 怎么确定现在到底在用哪个模型
证据分三层,越靠下越硬:
# ① 目录:能选什么(注意:这是"可选项",不是"当前项")
dsh --profile open-design --models
# [deepseek-account] deepseek-flash deepseek-v4-pro
# [baidu] deepseek-v4.1-flash deepseek-v4-pro-0813 glm-5.3 qianfan-code-latest
# ② 配置:默认选中了谁
dsh --profile open-design --dump-config | Select-String 'agent-default-model' -Context 0,4
# config:
# provider: baidu
# model: deepseek-v4.1-flash
# ③ 实际调用:最硬的证据(看 usage 帧)
$req='{"v":1,"type":"execute","request_id":"m1","cwd":"C:\\work","prompt":"Say OK only.","mcp_servers":[]}'
Set-Content "$env:TEMP\m.jsonl" $req -Encoding ascii -NoNewline
Get-Content "$env:TEMP\m.jsonl" -Raw | dsh --profile open-design --stdio | Select-String '"type":"usage"'
# {"v":1,"type":"usage","provider":"baidu","model":"deepseek-v4.1-flash", ...}
cwd换成你机器上真实存在的目录(JSON 里要写成双反斜杠,如"C:\\work")。
OD 界面看不出模型,这是设计如此:下拉固定显示 Default (CLI config),因为 supportsCustomModel: false。OD 也没有在 app 配置里钉模型(appConfig.agentModels 为空),所以决策权完全在这个 profile 里。
选择优先级(适配器源码逻辑):request.model(OD 主动传)> agent-default-model(profile 默认)> provider 自身默认。当前 OD 不传,所以第 ② 层定的就是实际用的。
换模型只需改
agent-default-model.model一行,取值必须出现在llm-pi-ai的models列表里;改完用上面 ③ 复查。
5. 关键补丁:适配 DSH 0.2.x 的协议变化
这是之前反复失败的真凶。
OD 内置的 @open-design/dsh-runtime@0.1.0 是按 DSH 0.1.x 写死的,而 0.2.x 的会话事件变了:
| 项 | 适配器期望(0.1.x) | 0.2.x 实际 |
|---|---|---|
| 助手文本 | assistant/chunk 事件的 text-delta 增量 |
该事件完全不存在,文本只出现在完整的 assistant/message 里 |
| 工具结果 | message.content[0] 是嵌套的 tool-result 块 |
content 是扁平的 [{type:"text"}];toolCallId / isError 在 message 顶层 |
后果:一个 text 帧都不发 → OD 的 smoke test 判定"退出码 0 但没文本" → 界面报 测试失败: exit 0;tool/result 处理器还会因取嵌套字段而抛异常,tool_result 帧直接消失。
5.1 最快做法:覆盖整份文件
$dst = "$env:USERPROFILE\.dsh\profiles\open-design\node_modules\@open-design\dsh-runtime\dist\index.js"
Copy-Item "$env:TEMP\od\dsh-runtime-index.patched.js" $dst -Force
($env:TEMP\od\dsh-runtime-index.patched.js 是你按 §5.2 改好的备份副本,路径随你放。)
5.2 手工改(重装后 node_modules 被还原时用)
文件:...\@open-design\dsh-runtime\dist\index.js
(a) 补发文本帧 — 找到 case "assistant/message":,把原来两行替换为:
case "assistant/message": {
if (event.data.usage) writeFrame(output, usageFrame(request.request_id, provider, model, event.data.usage));
const messageContent = Array.isArray(event.data.message?.content) ? event.data.message.content : [];
const text = messageContent.filter((block) => block.type === "text").map((block) => block.text).join("");
if (text.trim() !== "") writeFrame(output, { v: 1, type: "text", request_id: request.request_id, content: text });
return;
}
(b) 兼容工具结果结构 — 找到 case "tool/result":,整段替换为:
case "tool/result": {
const toolMessage = record(event.data?.message) ?? {};
const toolContent = Array.isArray(toolMessage.content) ? toolMessage.content : [];
const nestedBlock = toolContent[0]?.type === "tool-result" ? toolContent[0] : null;
writeFrame(output, {
v: 1,
type: "tool_result",
request_id: request.request_id,
call_id: String(toolMessage.toolCallId ?? nestedBlock?.toolCallId ?? ""),
name: event.data.error?.name ?? "tool",
output: nestedBlock ? contentText(nestedBlock.content) : contentText(toolContent),
is_error: nestedBlock ? nestedBlock.isError === true : toolMessage.isError === true
});
return;
}
(c) 让 result.output 有值 — 找到累积 assistantOutput 的那段,在其后追加:
if (event.type === "assistant/message") {
const messageContent2 = Array.isArray(event.data.message?.content) ? event.data.message.content : [];
for (const block of messageContent2) {
if (block.type === "text" && typeof block.text === "string") assistantOutput += block.text;
}
}
注意:这个补丁位于
node_modules内,OD 升级或重跑od agent setup deepseek-harness会被覆盖。升级后照 §5.1 重贴即可。
6. 验证(三层,逐层过)
第一层:CLI 协议直连
$env:DSH_PERMISSION_MODE = "danger-full-access" # 非交互环境别让它卡在审批
$req = '{"v":1,"type":"execute","request_id":"t1","cwd":"C:\\work","prompt":"Run: echo hi. Then say DONE.","mcp_servers":[]}'
$f = "$env:TEMP\req.jsonl"; Set-Content -LiteralPath $f -Value $req -Encoding ascii -NoNewline
Get-Content -LiteralPath $f -Raw | dsh --profile open-design --stdio
期望帧序(缺 text 就是 §5 没打):
{"v":1,"type":"ready",...}
{"v":1,"type":"session","request_id":"t1","session_id":"od-..."}
{"v":1,"type":"usage","provider":"baidu","model":"deepseek-v4.1-flash",...}
{"v":1,"type":"tool_call",...}
{"v":1,"type":"tool_result","output":"hi",...}
{"v":1,"type":"text","content":"DONE"}
{"v":1,"type":"result","status":"completed","output":"DONE",...}
第二层:OD daemon 的 agent 探测
GUI 启动后会有多个监听端口(Next.js 网页 + daemon + web sidecar),只有返回 JSON 的那个才是 daemon:
$pids = (Get-Process | Where-Object { $_.ProcessName -match 'Open.?Design' }).Id
$ports = (Get-NetTCPConnection -State Listen | Where-Object { $pids -contains $_.OwningProcess }).LocalPort | Sort-Object -Unique
foreach($pt in $ports){
$r = curl.exe -sS --max-time 90 -o "$env:TEMP\a.json" -w "%{http_code}" "http://127.0.0.1:$pt/api/agents"
$first = (Get-Content "$env:TEMP\a.json" -Raw).TrimStart().Substring(0,1)
"$pt => $r first=$first" # first={ 的那个就是 daemon
}
在 daemon 上确认:
# available 必须为 true
curl.exe -s "http://127.0.0.1:<port>/api/agents" | Select-String 'deepseek-harness'
diagnostics里出现untested-version(warning)是正常的,OD 只实测过 0.1.x;只要不是version-probe-failed/not-on-path就行。
第三层:OD 自己的连接测试(等价于点"测试"按钮)
Set-Content "$env:TEMP\body.json" '{"mode":"agent","agentId":"deepseek-harness"}' -Encoding ascii -NoNewline
curl.exe -sS -X POST -H "Content-Type: application/json" --data-binary "@$env:TEMP\body.json" `
"http://127.0.0.1:<port>/api/test/connection"
期望:
{"ok":true,"kind":"success","latencyMs":4101,"model":"default",
"agentName":"DeepSeek Harness","sample":"ok",
"diagnostics":{"phase":"connection_smoke_test","exitCode":0}}
ok:true 才算真的通了。
7. 无 GUI 调试 daemon(可选,排错很有用)
$env:ELECTRON_RUN_AS_NODE = "1" # 必须用 OD 自带 Electron,不能用系统 node
$exe = "$env:LOCALAPPDATA\Programs\Open Design\Open Design.exe"
$cli = "$env:LOCALAPPDATA\Programs\Open Design\resources\app\prebundled\daemon\daemon-cli.mjs"
Start-Process -FilePath $exe -ArgumentList "`"$cli`" --port 7801 --no-open" -PassThru
三个必须知道的限制:
- 必须用 app 的 Electron:
better_sqlite3.node是按 Electron 的 ABI 145 编的,系统 node v26 是 147,直接NODE_MODULE_VERSION不匹配崩掉。 - 路径要加引号:
Start-Process -ArgumentList会把Open Design按空格拆开,报Cannot find module '...\Programs\Open'。 - 别在会被回收的 job 里跑:父 shell 一收工 daemon 就死。要么
Start-Process独立起,要么"起→测→杀"写在同一条命令里。
日志:%APPDATA%\Open Design\namespaces\release-stable-win\logs\daemon\latest.log
8. 升级/复刻检查清单
9. 已知行为(不是故障)
- 版本
0.2.1-rc.2会带untested-version警告:OD 只白名单了0.1.0-rc.8 / 0.1.1-rc.2 / 0.1.2-rc.1。 - 模型下拉只有
Default (CLI config):supportsCustomModel: false,模型由 DSH profile 自己决定。 - 文本不是逐字流式的:0.2.x 没有增量事件,补丁是整条 assistant 消息一次性出
text帧。工具调用仍然是实时的。 - OD 遥测默认
send(telemetry.open-design.ai),要关在设置里调。 od命令本身没有装进 PATH,它等于"用 app 的 Electron 跑daemon-cli.mjs",需要时按 §7 包装成 shim。
附录:本文用到的产物(相对路径)
| 文件 | 说明 |
|---|---|
%TEMP%\od\setup.exe |
安装包(422 MB,装完可删) |
%TEMP%\od\dsh-runtime-index.patched.js |
打好补丁的适配器,重装后覆盖即恢复 |
%TEMP%\od\cordis.patch.yml.orig |
改坏之前的原始 patch(含错误 id,留作对照) |
%USERPROFILE%\.dsh\profiles\open-design\ |
profile 本体(cordis.patch.yml 是你要维护的那层) |

浙公网安备 33010602011771号