dsh-mcp-bridge
把 DeepSeek Harness 的会话能力变成 MCP Server:Claude Code、Cursor、Codex 等 Agent 可创建会话、发送消息、长轮询回合、回答提问,复用 DSH 的模型、工具与工作区。
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 22, 2026
- Updated
- Oct 23, 2026
Introduction
DSH MCP Bridge
把 DeepSeek Harness(DSH) 的会话能力包装成一个 MCP Server,让 Claude Code、Cursor、VS Code、Codex 或其他 Agent 通过 MCP 调用 DSH:
- 在已有工作区或指定目录新建会话(
dsh_list_workspaces先选工作区) - 查询会话状态:
dsh_get_status一行摘要 + 实时活动,dsh_get_session完整状态 - 查询最近一个回合的结果:
dsh_get_turn_result返回finish_reason、error(408 单独标出)、usage、tool_calls、duration_ms - 长轮询:
dsh_send_message { wait: false }拿到since,再用dsh_wait等完成/提问/错误,不用每 10 秒轮询 - 查询历史消息
- 选择会话模式(DSH agent preset),MCP 新建会话默认标准模式
- 切换模型、切换推理强度(reasoning effort)
- 调整权限等级(sandbox + approval preset),MCP 新建会话默认完全权限
- 发送对话并等待回复
- 终止正在执行的对话
- 归档会话
插件以 Host-only bundle 的形式安装在 DSH profile 中,MCP 端点复用 DSH Web GUI 的 HTTP 监听端口,不需要额外进程或端口。
端口会变。 DSH 每次启动可能分配到不同端口。插件启动时会把当前地址写到固定文件
~/.dsh/mcp-bridge-endpoint.json(可配置endpoint_file),脚本应优先读这个文件,不要写死端口或每次lsof。
0.8.0 本轮新增
- 客户端绑定与自动收尾:
initialize返回Mcp-Session-Id;客户端在后续请求带上它,插件就知道会话属于谁。断连 30 秒或心跳超过 120 秒没有请求时,自动清空队列并取消该客户端仍在跑的回合;也可以用dsh_list_client_sessions/dsh_stop_client_sessions手动查看和收尾。 - 不登记工作区 / 清理工作区:
dsh_create_session.register_workspace=false不再往工作区列表加项;dsh_archive_session.remove_workspace=true和dsh_remove_workspace可清理空工作区。 - 多目录
workspace-write:dsh_create_session.writable_roots把沙箱根设为 workspace 与这些目录的共同父目录(DSH 只支持一个 workspace root,因此不是精确白名单,见下文)。 - 稳定模型别名:
gpt/luna、gpt/sol、deepseek/flash等别名不再绑定 provider 名;旧的旧provider/gpt-6-luna也能在 provider 改名后解析。找不到时返回近似候选。 - 模型能力与提供方健康度:
dsh_list_models增加accepts_images、context_window、max_output_tokens、alias,以及最近一小时的provider_health(turns/failures/retries/error_rate)。 - 按消息调推理强度:
dsh_send_message.reasoning_effort只对本次回合生效,回合结束后恢复原选择;mode=queue在已有回合运行时会被拒绝,避免污染正在跑的回合。 - 归档后可读:
dsh_get_messages/dsh_get_turn_result/dsh_get_status支持已归档会话,并在消息结果里附last_turn(含 408 等错误)。
1. 可行性结论
已在本机 DSH Desktop 上端到端验证通过:
| 能力 | 使用的 DSH 服务 | 验证结果 |
|---|---|---|
| 指定工作区新建会话 | workspaceRegistry.list/create + sessionController.create | ✅ 可按 workspace_id 复用已有工作区,或按路径自动注册新目录 |
| 查询会话状态 | sessionController.list/projections/page、agents、sessionQuery | ✅ 运行状态、模式、模型、队列、最近消息;dsh_get_status 提供一行摘要 |
| 查询回合结果 | durable turn/start/turn/end/tool/* 事件 + sessionController.inspect | ✅ finish_reason、408 error.http_status、usage、工具数、真实耗时;dsh_get_turn_result 在 send 超时后仍可用 |
| 长轮询 | sessionController.follow + 提问通知 | ✅ turn_end / question / error / timeout;dsh_wait 不空转 |
| 固定端点发现 | webServer.getOrigin + 原子写文件 | ✅ ~/.dsh/mcp-bridge-endpoint.json,无需每次 lsof |
| 选择/切换会话模式 | agentPresets.remoteExportList/resolve/select | ✅ 新建默认 standard;首回合前可切换,之后锁定 |
| 切换模型 | sessionController.modelCatalog + selectModel | ✅ 跨 provider 切换 |
| 切换推理强度 | 同上,reasoningEffort | ✅ off/low/high/max 等按模型校验 |
| 调整权限等级 | permissionPresets.catalog/current/resolve/set + approval.setPolicy | ✅ sandbox + approval 一起切换;MCP 新建会话默认 danger-full-access |
| 发送对话 | sessionController.prompt + follow | ✅ 等待回合结束并返回最终回复 |
| 终止对话 | sessionController.cancel | ✅ 运行中的回合被取消,会话回到 idle |
| 归档会话 | workspaceRegistry.archiveSession | ✅ 从活动列表隐藏,日志保留 |
关键点:DSH 的 ctx.sessionController 已经提供了创建、prompt、取消、模型选择、历史读取和事件流的完整 Host API;插件只需要把这些 API 映射成 MCP 工具,并注册一个 HTTP 路由。不需要修改 DSH 核心,也不需要启动第二个服务。
权限默认值也已实测:MCP 新建会话的 permission 为 danger-full-access,Agent 在 runtime context 中回报 Current DSH file policy: danger-full-access,并提示审批已禁用。
模式默认值同样已实测:MCP 新建会话的 mode 为 standard;dsh_list_modes 返回当前 DSH 的完整模式列表,dsh_set_mode 可在首回合前切换,首回合后会返回 mode_locked。
2. 架构
Other Agent / MCP Client
│ Streamable HTTP (JSON-RPC 2.0)
▼
http://127.0.0.1:<port>/mcp ← 端口见 ~/.dsh/mcp-bridge-endpoint.json
│
▼
@local/dsh-mcp-bridge (Host plugin)
│
├─ sessionController.create / prompt / cancel / selectModel / page / follow
├─ agents.get / inbox
├─ sessionQuery.filterSessions
├─ workspaceRegistry.create / list / archiveSession
├─ agentPresets.remoteExportList / resolve / select
└─ permissionPresets.catalog/current/resolve/set + approval.setPolicy
│
▼
DeepSeek Harness session runtime
- MCP 传输:Streamable HTTP,
POST /mcp,JSON 响应;不提供 SSE 长连接。 - MCP 客户端身份:
initialize会返回Mcp-Session-Id;后续请求带上它就能绑定会话、做租约和自动收尾。不带这个头的客户端仍按无状态方式工作,只是没有归属能力。 - 工具结果统一放在
content[0].text的 JSON 文本中;工具级失败通过isError: true返回。 - 发送消息时通过
sessionController.follow订阅 durable 事件,识别本次rpcId对应的用户消息,然后等待该回合的turn/end,收集最后的 assistant 文本。 dsh_get_turn_result从 durable 日志重建最近一个turn/start … turn/end:finish_reason、error.http_status、usage、工具调用数、真实回合耗时。即使dsh_send_message已超时/断连,结果仍在。dsh_get_status的“最近事件”是最后一条 durable 事件的time(回合中途会变),并额外给出当前工具、本回合工具调用次数、队列消息来源和last_turn。dsh_wait同样走follow,但只在回合结束、出现提问、出错或超时时返回;完成与失败都以正常结果返回,不靠isError。- 插件把当前端点写入
endpoint_file(默认~/.dsh/mcp-bridge-endpoint.json),并在/mcp/info的endpoint_file字段中回显路径。 - MCP 新建会话默认使用
standard模式(DSH agent preset);调用方可用dsh_create_session.mode选择standard、ptc、minimal、cordis等已声明模式。 - MCP 新建会话默认使用
danger-full-access权限预设(完全文件访问、无审批);这是本桥接的显式默认,不改变 DSH GUI 新会话的默认预设。调用方可用dsh_create_session.permission或dsh_set_permission覆盖。
3. 安装
本 profile(已安装)
当前 desktop profile 已经安装了本 bundle,行 id 为 include:dsh-mcp-bridge,配置文件:
~/.dsh/profiles/desktop/cordis.patch.yml 中的 dsh-mcp-bridge 行。
其他 profile
把本目录通过 DSH 的插件管理器安装(推荐,不要手写 profile 的 package.json):
plugin_manager install_bundle target=/absolute/path/to/dsh-mcp-bridge
安装后重启或让 DSH 重新加载该 bundle。本插件是 Host-only,不需要构建步骤,没有 npm 依赖。
修改后重新加载
本插件通过软链接安装到 profile。Desktop profile 默认启用 @deepseek-ai/dsh-hmr(base 组合包里的 hmr 条目,root: []),改完源码后通常不需要重启 DSH:让 ~/.dsh/profiles/desktop/cordis.patch.yml 的内容变化一次(单纯 touch 无效),或在侧边栏插件页把 dsh-mcp-bridge 关闭再打开,HMR 会重新组合并重启文件已变化的插件。也可以配置 hmr.root 监听本工作区,之后保存源码即自动重载。
完整的触发方式、机制、实测记录、限制与排错见 不重启 DSH 热重载插件(HMR)。
如果 hmr 条目被禁用或不可用(非 Desktop / headless profile),只能完全重启 DSH Desktop。
4. 连接 MCP
端点与固定文件
MCP: http://127.0.0.1:<port>/mcp
Info: http://127.0.0.1:<port>/mcp/info
端口取自 DSH Web GUI 的实际监听端口,每次启动可能不同。插件启动后会原子写入:
~/.dsh/mcp-bridge-endpoint.json
示例内容(不含 token):
{
"schema_version": 1,
"server": "dsh-mcp-bridge",
"version": "0.8.0",
"url": "http://127.0.0.1:57685/mcp",
"endpoint": "http://127.0.0.1:57685/mcp",
"info_endpoint": "http://127.0.0.1:57685/mcp/info",
"origin": "http://127.0.0.1:57685",
"host": "127.0.0.1",
"port": 57685,
"path": "/mcp",
"auth": "loopback-only",
"token_required": false,
"pid": 12345,
"started_at": "2026-10-23T08:00:00.000Z",
"updated_at": "2026-10-23T08:00:00.000Z"
}
脚本读取:
ENDPOINT=$(jq -r .endpoint ~/.dsh/mcp-bridge-endpoint.json)
PORT=$(jq -r .port ~/.dsh/mcp-bridge-endpoint.json)
curl -s "$ENDPOINT/info"
-
文件在插件启动时写入,权限
0600;干净退出时删除。进程崩溃时可能留下旧文件,可先检查pid是否存活,或用/mcp/info验证。 -
auth为bearer时只表示需要 token,文件里不会写 token 本身。 -
也可以配置绝对路径:
- id: dsh-mcp-bridge name: '@local/dsh-mcp-bridge' config: path: /mcp endpoint_file: /Users/me/.dsh/dsh-endpoint.json -
如果希望端口本身固定,可在 profile 的
cordis.patch.yml里覆盖webserver.port(例如57685)。这会改变 DSH GUI 的监听端口;端口冲突时 DSH 启动失败,因此默认仍建议用 endpoint 文件。
Claude Code
ENDPOINT=$(jq -r .endpoint ~/.dsh/mcp-bridge-endpoint.json)
claude mcp add --transport http dsh "$ENDPOINT"
Cursor(.cursor/mcp.json)
把 url 换成 endpoint 文件里的 endpoint:
{
"mcpServers": {
"dsh": {
"url": "http://127.0.0.1:<port>/mcp"
}
}
}
VS Code(.vscode/mcp.json)
{
"servers": {
"dsh": {
"type": "http",
"url": "http://127.0.0.1:<port>/mcp"
}
}
}
通用 Streamable HTTP 配置
{
"mcpServers": {
"dsh": {
"type": "http",
"url": "http://127.0.0.1:<port>/mcp"
}
}
}
curl 自检
ENDPOINT=$(jq -r .endpoint ~/.dsh/mcp-bridge-endpoint.json)
curl -s "$ENDPOINT/info"
curl -s -X POST "$ENDPOINT" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"curl","version":"0"}}}'
5. 工具
| 工具 | 作用 | 主要参数 |
|---|---|---|
dsh_create_session | 在已有/指定工作区创建会话;可跳过工作区登记、放宽 workspace-write 根 | workspace(绝对路径)或 workspace_id(二选一,必需)、mode、model、provider、reasoning_effort、permission、register_workspace(默认 true)、writable_roots(需 workspace) |
dsh_get_session | 查询状态、模式、模型、权限、队列内容、最近消息、最近回合结果 | session_id、include_messages、message_limit |
dsh_get_status | 实时状态:一行摘要 + 最后事件时间、当前工具、本回合工具数、队列来源、last_turn | session_id |
dsh_get_turn_result | 取最近(或指定)回合的完整结果,失败也作为正常数据返回 | session_id、turn(可选) |
dsh_get_questions | 查询待回答的问题批次、选项和可回答性 | session_id |
dsh_answer_question | 提交结构化答案,继续前台工具或补答超时问题 | session_id、call_id、answers |
dsh_list_sessions | 列出活动会话 | workspace、limit、include_blank |
dsh_list_workspaces | 列出已注册工作区(id、路径、会话数) | 无 |
dsh_list_client_sessions | 列出当前 MCP 客户端创建/接管的会话,含 running/queued | only_running、include_archived、limit |
dsh_stop_client_sessions | 清空并停止当前客户端拥有的运行中/排队会话 | 无 |
dsh_list_models | 模型列表:effort、稳定别名、看图/上下文能力、提供方错误率 | 无 |
dsh_set_model | 切换模型和/或推理强度 | session_id、model、provider、reasoning_effort |
dsh_list_modes | 列出会话模式(agent preset)、默认模式 | 无 |
dsh_set_mode | 切换空白会话的模式 | session_id、mode |
dsh_list_permissions | 列出可选权限预设、sandbox/approval 与默认值 | 无 |
dsh_set_permission | 切换权限等级(sandbox + approval) | session_id、permission |
dsh_send_message | 发送消息,默认等待回复;wait: false 立即返回 since;可覆盖本回合 effort | session_id、message、mode、wait、timeout_seconds、reasoning_effort |
dsh_wait | 长轮询:完成、提问、出错、超时或空闲时返回 | session_id、since、request_id(队列消息用)、timeout_seconds |
dsh_stop_session | 终止当前回合 | session_id |
dsh_get_messages | 读取最近对话消息;归档会话可读,附 last_turn | session_id、limit、before_seq |
dsh_archive_session | 归档会话(日志保留、仍可读),可顺手删空工作区 | session_id、remove_workspace |
dsh_remove_workspace | 删除工作区登记(默认要求没有非归档会话) | workspace_id、only_if_empty |
典型流程
dsh_list_workspaces / dsh_list_models / dsh_list_modes / dsh_list_permissions
↓ 选工作区(workspace_id)、provider/model/effort、会话模式、权限预设
dsh_create_session { workspace_id: "workspace-...", mode: "standard", model: "deepseek-flash", reasoning_effort: "max", permission: "danger-full-access" }
↓ 得到 session_id(也可以传 workspace: "/abs/path" 自动注册新目录)
dsh_send_message { session_id, message: "帮我检查这个仓库", wait: false }
↓ 立即返回 accepted + since
dsh_wait { session_id, since, timeout_seconds: 300 }
↓ 回合结束 / 提问 / 错误 / 超时时返回;把 cursor 作为下一次 since
dsh_get_turn_result { session_id } ← 即使 send 早已超时,也能拿到最近回合结果
↓
dsh_get_status / dsh_get_session / dsh_get_messages(需要细节时)
↓
dsh_set_mode(首回合前)/ dsh_set_permission(运行中调整权限等级)
↓
dsh_stop_session(需要中止时)
↓
dsh_archive_session(清理,可选)
dsh_create_session
会话必须有一个工作目录。两种指定方式二选一:
{
"workspace_id": "workspace-...",
"mode": "standard",
"model": "deepseek-flash",
"provider": "deepseek-account",
"reasoning_effort": "max",
"permission": "danger-full-access"
}
workspace_id:来自dsh_list_workspaces,复用已注册的工作区,不创建/注册任何目录。workspace:绝对路径;目录不存在时递归创建,并注册为 DSH workspace。
两者都不传返回 workspace_required,都传返回 invalid_params,workspace_id 不存在返回 workspace_not_found。
model / provider / reasoning_effort 可省略:省略 model 时用 DSH 默认模型;同一个模型 id 存在于多个 provider 下时(例如 deepseek-flash),必须同时传 provider,否则返回 model_ambiguous;reasoning_effort 按所选模型的 reasoning.efforts 校验,省略时用模型默认值。provider 不能单独传。
mode 可省略,默认 standard。它是 DSH 的 agent preset:决定该会话挂载的工具、prompt 区块和 skill 组合。dsh_list_modes 返回当前部署可用的模式;agent_preset 是旧调用方的别名,二者只能传一个(或传相同值)。
permission 可省略,默认 danger-full-access。完整权限预设由 dsh_list_permissions 返回;read-only、workspace-write、danger-full-access 是否可用取决于当前 DSH 配置。
返回:
{
"session_id": "session-...",
"workspace_id": "...",
"workspace": "/Users/me/project",
"sandbox_root": "/Users/me/project",
"writable_roots": [],
"registered_workspace": true,
"sandbox_scope": "workspace",
"mode": "standard",
"agent_preset": "standard",
"model": {
"provider": "deepseek-account",
"model": "deepseek-flash",
"reasoning_effort": "max"
},
"permission": {
"preset": "danger-full-access",
"sandbox": "danger-full-access",
"approval": "never"
}
}
- 用
workspace时目录默认会注册为 DSH workspace;用workspace_id时会话 attach 到该工作区,不会新建目录。 mode会在创建前校验;非法/损坏的模式不会留下空会话。model/reasoning_effort会先在模型目录中校验,typo 不会留下空会话;model可写稳定别名(见dsh_list_models),例如gpt/luna。permission会在创建后、首条 prompt 前应用;非法预设会在创建前被拒绝。
不登记工作区(0.8.0)
dsh_create_session 每调用一次就把目录注册成工作区,会让 dsh_list_workspaces 越来越长。传 register_workspace: false:
{
"workspace": "/Users/me/project",
"register_workspace": false,
"mode": "standard"
}
- 目录仍会被创建并作为会话 cwd,但不会出现在工作区列表;返回
workspace_id: null、registered_workspace: false。 - 想清掉之前自动登记的空工作区:
dsh_remove_workspace { workspace_id, only_if_empty: true },或归档时用dsh_archive_session { remove_workspace: true }。
多目录 workspace-write(0.8.0)
DSH 的 workspace-write 只有一个 workspaceRoot,没有“额外白名单”设置。插件的做法是把沙箱根设为 workspace 与 writable_roots 的共同父目录:
{
"workspace": "/Users/me/Dev/repo-a",
"writable_roots": ["/Users/me/Dev/repo-b"],
"permission": "workspace-write",
"register_workspace": false
}
返回会带:
{
"workspace": "/Users/me/Dev/repo-a",
"sandbox_root": "/Users/me/Dev",
"writable_roots": ["/Users/me/Dev/repo-b"],
"sandbox_scope": "common_ancestor",
"registered_workspace": false
}
- 这两个仓库都能写,但同属
/Users/me/Dev的其他目录也会变成可写。这是共同父目录,不是精确的 per-root 白名单;要更严格,只能把仓库放到同一个专用父目录,或继续用danger-full-access自行约束。 - 会话的 cwd 会变成这个共同父目录,
workspace字段仍返回原来的工作目录。依赖仓库内AGENTS.md的任务,可能需要在 prompt 里明确写“先working_directory到<workspace>”,否则初始指令发现会从共同父目录开始。 writable_roots需要workspace路径,不能和workspace_id一起用;默认register_workspace为 false。- 共同父目录是
/(例如跨磁盘/跨卷)时返回common_root_unavailable。
dsh_list_workspaces
列出已注册的 DSH 工作区,供 dsh_create_session.workspace_id 选择:
{
"workspaces": [
{
"workspace_id": "workspace-...",
"path": "/Users/me/project",
"title": "project",
"session_count": 3
}
],
"total": 1
}
session_count不含已归档会话。path可以直接作为dsh_list_sessions.workspace的过滤条件。- 列表为空时,仍可给
dsh_create_session.workspace传一个路径来创建并注册第一个工作区。
dsh_list_modes
返回当前 DSH 声明的模式、profile 默认模式,以及本桥接的 MCP 默认模式:
{
"default_mode": "standard",
"mcp_default": "standard",
"modes": [
{
"id": "standard",
"name": "standard",
"description": "…",
"is_default": true,
"available": true
},
{
"id": "ptc",
"name": "ptc",
"is_default": false,
"available": true
}
],
"note": "…"
}
default_mode是 DSH profile 当前默认(用户可能在 GUI 改过)。mcp_default是dsh_create_session的默认值,固定为standard。available: false表示该 preset 当前挂载失败,broken会给出原因;不要用它创建会话。
dsh_set_mode
{ "session_id": "session-...", "mode": "ptc" }
返回:
{
"session_id": "session-...",
"previous": "standard",
"selected": "ptc",
"locked": false,
"note": "The mode applies to this blank session. DSH locks mode selection after the first turn."
}
- 只在会话首回合开始前可用。DSH 会在
turn/start后锁定模式;此时调用返回isError: true,code为mode_locked。 - 需要换了模式再对话时,请新建会话并在
dsh_create_session.mode中指定。 - 切换会重新挂载该 preset 的工具/prompt/skill 组合,空会话的所有后续回合都使用新模式。
dsh_send_message
默认 wait: true,等待回合结束后返回:
{
"accepted": true,
"pending": false,
"session_id": "session-...",
"request_id": "...",
"message_seen": true,
"turn": 1,
"finish_reason": "completed",
"stopped": false,
"error": null,
"reply": "最终回复文本",
"assistant_messages": 1,
"tool_calls": 2,
"duration_ms": 12345,
"waited_ms": 12400,
"since": 10,
"cursor": 42,
"last_seq": 42,
"usage": {
"input_tokens": 1234,
"output_tokens": 456,
"total_tokens": 1690
}
}
wait: false:立即返回accepted: true, pending: true, request_id, since, cursor。把since传给dsh_wait,不要自己 sleep 轮询;如果消息可能排在正在运行的回合后面,再传上request_id。timeout_seconds超时:返回pending: true, timed_out: true, message_seen, waited_seconds, since, cursor,回合继续运行;dsh_wait { since }可以在它结束时立刻拿到结果。若此时有前台提问,还会返回waiting_for_answer: true和pending_questions。mode: "queue":排队到下一个回合;mode: "steer":尝试注入当前运行中的回合。reasoning_effort(0.8.0):只对本次消息启动的回合生效,回合结束后自动恢复原选择。机械活可以传low/off;mode=queue在已有回合运行时会被拒绝(session_busy),避免改到正在跑的回合;mode=steer会影响该运行回合的下一次模型请求。同一会话同时只能有一个待恢复的 effort override。底层仍走selectModel,所以会短暂写入 DSH 的“新会话默认模型”,回合结束后恢复。- 如果回合以
error/blocked结束,dsh_send_message仍按 MCP 错误返回(isError: true),错误 JSON 的details中带finish_reason、error(含http_status)和旧的failure字段。轮询/长轮询路径请用dsh_get_turn_result或dsh_wait,它们把失败当正常数据返回。 - 支持长任务:
timeout_seconds最大 600;更长的任务用wait: false+dsh_wait(timeout_seconds同样可以到 600,客户端 HTTP 超时要更大)。
finish_reason 的完整取值(工具说明里也已列出):
| 值 | 含义 |
|---|---|
completed | 正常结束 |
error | 模型/传输错误。error.message、error.code、error.status、error.http_status;408 断流会得到 http_status: 408 |
aborted | 被取消(abort_reason.kind 给出 user/parent/hook/disposed/legacy) |
blocked | 在发出模型请求前被阻塞(例如会话已归档) |
max-tokens | 达到输出 token 上限 |
interrupted | 回合被中断 |
forked | 会话分叉导致回合结束 |
- 回合仍在运行、等待回答或等待超时时,没有
finish_reason;看pending、waiting_for_answer或dsh_wait.event。 finish_reason只表示回合怎么结束,不表示任务是否完成;任务完成与否仍要看回复/验证文件。
dsh_send_message 的返回怎么读:
pending | timed_out | waiting_for_answer | finish_reason | 含义 |
|---|---|---|---|---|
false | false | false | completed/error/… | 回合已结束,看 finish_reason |
true | true | true | 无 | 子代理在提问;先 dsh_get_questions + dsh_answer_question,再 dsh_wait |
true | true | false | 无 | 还在跑;用 dsh_wait { since }(可带 request_id) |
true | false | 任意 | 无 | 事件流结束但没有 turn/end;用 dsh_get_turn_result 兜底 |
dsh_get_turn_result
从 durable 日志重建最近一个回合(或 turn 指定的回合),不依赖 dsh_send_message 是否还在等。失败回合作为正常结果返回:
{
"session_id": "session-...",
"turn": 1,
"open": false,
"running": false,
"finish_reason": "error",
"ok": false,
"outcome": "failure",
"error": {
"message": "408: {\"message\":\"stream disconnected before completion\"}",
"code": "INVALID_REQUEST",
"status": 408,
"http_status": 408
},
"abort_reason": null,
"usage": {
"input_tokens": 65731,
"output_tokens": 4622,
"total_tokens": 919889,
"cache_read_tokens": 849536
},
"tool_calls": 49,
"tool_errors": 1,
"assistant_messages": 19,
"duration_ms": 364009,
"started_at": "2026-10-23T06:15:37.959Z",
"ended_at": "2026-10-23T06:21:41.968Z",
"last_event_at": "2026-10-23T06:21:41.968Z",
"last_seq": 205,
"start_seq": 8,
"end_seq": 205,
"request_id": "540b8d0d-...",
"reply": "最后一段 assistant 文本(若有)",
"interrupted": false,
"active_tools": [],
"waiting_for_answer": false,
"pending_question_count": 0,
"open_question_count": 0
}
ok:只有completed为true;error/blocked/max-tokens/aborted/interrupted/forked为false;运行中或没有回合为null。outcome:success/failure/stopped/incomplete/running/none。error.status是有效的 HTTP 状态:provider 直接给了就用它,否则从"408: …"这类消息头解析;error.http_status是同一值的显式别名。两者在无法判定时都为null。duration_ms是真实回合的turn/start→turn/end时间,不是 HTTP 等待时间。- 回合仍在运行时:
open: true, running: true, finish_reason: null,active_tools给出当前工具和已运行时长,usage/tool_calls是到目前为止的累计值。 turn可选;指定不存在的回合返回isError: true, code: "turn_not_found"。reply是最后一段非空 assistant 文本,最多 40000 字符;长回复用dsh_get_messages取全文。- 没有回合的空白会话返回
turn: null, outcome: "none",不报错。
dsh_wait
长轮询,不空转。since 用 dsh_send_message 返回的 since,或上一次 dsh_wait 返回的 cursor:
{
"session_id": "session-...",
"event": "turn_end",
"pending": false,
"timed_out": false,
"status": "idle",
"since": 10,
"cursor": 42,
"turn": 1,
"finish_reason": "completed",
"ok": true,
"outcome": "success",
"error": null,
"usage": { "input_tokens": 1234, "output_tokens": 456 },
"tool_calls": 2,
"duration_ms": 12345,
"last_turn": { "turn": 1, "finish_reason": "completed", "ok": true }
}
event 取值:
| 值 | 含义 | pending |
|---|---|---|
turn_end | 回合结束。顶层字段就是 dsh_get_turn_result 的结果(含失败时的 error.http_status: 408) | false |
question | 有前台问题等待回答;用 dsh_get_questions / dsh_answer_question,然后带 cursor 再 dsh_wait | true |
timeout | 到达 timeout_seconds 仍在运行;用返回的 cursor 重试 | true |
idle | Agent 空闲、队列为空,没有可等的事件 | false |
cold | 没有活 Agent;发一条消息才会激活 | false |
stream_end | 事件流意外结束,且没有正在等待回答的问题;用 dsh_get_turn_result 兜底 | true |
- 省略
since:从当前最后事件之后开始等;要取最近一个已完成回合的结果,用dsh_get_turn_result(since: -1会从日志中最早的turn/end开始匹配,适合新建会话的第一个回合)。 - 队列消息(
mode: "queue")可能排在正在运行的回合后面。 这时把dsh_send_message返回的request_id也传给dsh_wait;它只认这个 prompt 所在的回合,不会把前一个回合的结束误当成本次完成。 - 返回的
cursor永远是下次since的推荐值。 dsh_wait不会因为回合失败而返回isError;只有参数/session 错误才走 MCP 错误。timeout_seconds1–600,默认 120;客户端的 HTTP 超时要大于它。
dsh_get_status
实时状态接口:不读消息、不查模型目录,但会读 durable 事件以计算真实活动:
{
"session_id": "session-...",
"status": "running",
"summary": "运行中 · standard · deepseek-flash (max) · 队列 1(下一回合 1;来源 dsh_send_message×1) · 正在执行 bash(12 秒) · 本回合工具 3 次 · 最近事件 2 秒前",
"running": true,
"waiting_for_answer": false,
"pending_question_count": 0,
"open_question_count": 0,
"last_event_at": "2026-10-23T09:00:02.000Z",
"last_event_type": "tool/call",
"activity": {
"turn": 2,
"open": true,
"running": true,
"started_at": "2026-10-23T08:59:50.000Z",
"elapsed_ms": 12000,
"tool_calls": 3,
"tool_errors": 0,
"current_tool": "bash",
"active_tools": [
{ "call_id": "call_...", "name": "bash", "started_at": "2026-10-23T08:59:50.000Z", "seq": 123, "elapsed_ms": 12000 }
],
"last_event_at": "2026-10-23T09:00:02.000Z",
"last_event_type": "tool/call",
"last_event_age_ms": 2000
},
"queue": {
"next_turn": 1,
"next_step": 0,
"total": 1,
"messages": [
{
"target": "next_turn",
"message_id": "...",
"source": "user",
"request_id": "...",
"via": "dsh_send_message",
"sent_at": "2026-10-23T08:59:49.000Z",
"mode": "queue",
"preview": "排队消息的文本…"
}
]
},
"last_turn": {
"turn": 1,
"open": false,
"finish_reason": "error",
"ok": false,
"outcome": "failure",
"error": { "message": "408: ...", "code": "INVALID_REQUEST", "status": 408, "http_status": 408 },
"usage": { "input_tokens": 65731, "output_tokens": 4622 },
"tool_calls": 49,
"tool_errors": 1,
"duration_ms": 364009,
"ended_at": "2026-10-23T06:21:41.968Z",
"request_id": "..."
}
}
status:running/idle/cold(没有活 Agent,DSH 需要时再加载)/waiting_for_answer(前台问题正在等待回答)。status: "idle"不等于成功。 回合超时、408 断流、取消之后都会回到 idle;必须看last_turn.ok/last_turn.finish_reason/last_turn.error.http_status,或调用dsh_get_turn_result。last_event_at是最后一条 durable 事件的真实时间,回合中途会随工具调用/assistant 消息更新;不再是“本回合开始时间”。activity.current_tool只在回合打开时给出;active_tools支持并行工具,elapsed_ms是工具已运行时长。回合结束后active_tools为空,tool_calls仍是上一回合的总数。queue.messages给出排队消息的target(next_turn/next_step,即下一回合 / 本回合补话)、来源、request_id、preview,以及是否来自桥接的dsh_send_message;摘要里会压缩成来源 tool-jobs×1 / dsh_send_message×2。- 若问题已超时进入
continued,pending_question_count会包含它,但open_question_count/waiting_for_answer只统计仍在等回答的open;Agent 的真实运行状态不会被误报。 - 归档会话返回
archived: true,摘要追加“已归档”;仍可读last_turn,但不能再发消息。 - 需要 workspace、权限、turn boundary 或消息时,用
dsh_get_session。
客户端绑定、租约与自动收尾(0.8.0)
MCP 本身是无状态的,所以插件用 MCP 标准的 Mcp-Session-Id 识别客户端:
- 客户端
initialize后,响应头会带Mcp-Session-Id。后续每个请求都带上它,插件会把dsh_create_session创建的会话记到这个客户端名下,并在客户端最后一次变更操作时更新归属。 - 你现有的 Python 驱动(
dsh_mcp.py)已经会自动保存并回传这个头,不需要改代码。 - 不发送这个头的旧客户端仍然可用,只是没有归属/自动收尾能力,
dsh_list_client_sessions会返回client_identity_required。
自动收尾规则(默认值):
| 情况 | 默认行为 |
|---|---|
| 请求连接在响应完成前断开 | 等 30 秒宽限(client_disconnect_grace_seconds);期间没有新请求就收尾 |
| 客户端超过 120 秒没有任何请求 | 视为心跳超时(client_lease_seconds);有长请求在飞时不算超时 |
| 收尾动作 | 清空该会话的 next-turn/next-step 队列,然后对运行中的回合调用 cancel;不归档、不删除日志 |
| 客户端正常退出但不发 DELETE | 同上,等租约超时后收尾 |
dsh_list_client_sessions { only_running: true }
→ 当前客户端的会话、running/queued、last_turn
dsh_stop_client_sessions {}
→ 立即清空并停止当前客户端仍在跑/排队的会话
DELETE /mcp (Mcp-Session-Id: ...)
→ 立即关闭 client 并触发同样的收尾
- 想关闭自动收尾:
auto_stop_owned_sessions: false;想调长/调短心跳:client_lease_seconds(0 表示不限)。 - 会话归属是“最后一个对会话做变更操作的客户端”。只读工具不会抢归属;新进程要继续旧会话时,先调用
dsh_send_message或dsh_stop_session即接管。 - 自动收尾后会话仍在 DSH 列表里,可以用
dsh_get_turn_result/dsh_get_messages查它最后发生了什么。
提问状态、查询与回答(0.8.0)
dsh_get_status { session_id }
→ status: "waiting_for_answer"
→ open_question_count: 1
dsh_wait { session_id, since }
→ event: "question", waiting_for_answer: true
dsh_get_questions { session_id }
→ 见下面的返回示例
dsh_answer_question {
session_id,
call_id: "call_00_qSnAwky21Knthj6mc1aW5615",
answers: [{ id: "choice", selected: ["选项 A"] }]
}
→ accepted: true, delivery: "tool_result"
dsh_get_questions 的完整返回示例(pending_questions 与 batches 是同一个数组的别名):
{
"session_id": "session-...",
"waiting_for_answer": true,
"count": 1,
"pending_questions": [
{
"call_id": "call_00_qSnAwky21Knthj6mc1aW5615",
"state": "open",
"answerable": true,
"questions": [
{
"id": "add_status_tool",
"header": "确认需求",
"question": "要我加一个轻量状态工具吗?",
"detail": "可选的补充说明",
"options": [
{
"id": "新增 dsh_get_status(推荐)",
"index": 0,
"label": "新增 dsh_get_status(推荐)",
"description": "选项说明"
},
{
"id": "先不改",
"index": 1,
"label": "先不改"
}
],
"multi_select": false
}
]
}
],
"batches": [ "…与 pending_questions 相同…" ]
}
- 一组问题 = 一次
ask_user_question工具调用;pending_questions/batches的每个元素是一个批次。call_id是不透明字符串(通常是call_00_...;桥接合成的 legacy 问题可能是mcp-question-<uuid>),原样传给dsh_answer_question.call_id。 - 选项没有单独的 ID 字段:
options[].id是label的别名,selected里放label(也就是这个id)。index只用于展示。 selected可以为空的情况:- 显式跳过:
{ "id": "q1", "selected": [] }(不传custom)。 - 自由文本:
{ "id": "q1", "selected": [], "custom": "回答" }。 - 多选 + 自由文本:
selected可以同时有选项,并带custom。 - 单选:最多一个选项标签,或一个非空
custom,二者不能同时出现。
- 显式跳过:
state:open(仍在前台等待,MCP/GUI 先答者生效)或continued(限时问题已超时或由日志修复恢复,仍可补答,delivery: "steer")。answerable: false:该前台问题未被桥接捕获(例如桥接加载前的旧问题);只能在 DSH GUI 回答。归档会话不接受答案。- 每次必须完整提交这组所有题目,
id各出现一次。重复排队返回question_reply_queued;已失效问题返回question_not_found。 dsh_get_session也返回pending_questions;dsh_list_sessions返回pending_question_count和同样的等待状态。- 恢复边界:MCP 请求断开不会取消原提问;重新查询后可继续回答。普通 legacy 阻塞式提问只在 Host 内存中捕获,桥接加载前的旧问题、Host 重启后已取消的问题不能凭空恢复;只有 DSH 持久化并保留为
continued的问题支持冷会话加载后补答。重新发一条普通消息不等于回答旧问题。 - 不处理权限审批和计划审核:带
intent的问题不交给 MCP 回答器;审批仍走 DSH 原来的审批流程。调用方必须提交用户明确提供的答案,不能将等待或超时理解为许可。 - GUI/MCP 并行回答的取消机制依赖当前 DSH 的 Cordis waterfall 和 gateway 共享请求对象的行为;升级 DSH 后应回归验证。插件不修改 DSH 核心,不接管无 Agent 或子 Agent 的提问。
dsh_get_messages
返回的 messages 只包含真正的对话消息(user/assistant)。DSH 注入的运行时上下文、技能目录、时间提示等 user 消息会被省略,避免污染其他 Agent 的上下文。
0.8.0 起:
- 归档会话也能读:
archived: true,durable 日志保留;不能再dsh_send_message,但可以读消息和回合结果。 - 返回值增加
last_turn(与dsh_get_status.last_turn同形),包含finish_reason、ok、error.http_status、usage、tool_calls、duration_ms。408 的完整error.message就在这里,不用只靠界面上的一句半截回复。
{
"session_id": "session-...",
"messages": [
{ "role": "user", "seq": 10, "time": "2026-10-22T13:52:22.000Z", "text": "...", "request_id": "..." },
{ "role": "assistant", "seq": 20, "time": "2026-10-22T13:52:23.000Z", "text": "..." }
],
"has_more": false,
"next_before_seq": null
}
分页:把上一页的 next_before_seq 作为下一次的 before_seq。
dsh_list_models(0.8.0)
除了 provider/model/effort,现在还返回稳定别名、能力和最近的提供方健康度:
{
"default": { "provider": "deepseek-account", "model": "deepseek-flash", "reasoning_effort": "high" },
"providers": [
{
"provider": "cli-proxy",
"name": "本地 CLI Proxy",
"models": [
{
"id": "gpt-6-luna",
"name": "gpt-6-luna",
"alias": "gpt/luna",
"input_modalities": ["text"],
"accepts_images": false,
"context_window": 262144,
"max_output_tokens": 32768,
"capabilities_available": true,
"reasoning": { "efforts": [{ "id": "low", "name": "Low" }, { "id": "max", "name": "Max" }], "default_effort": null }
}
]
}
],
"provider_health": {
"cli-proxy": {
"window_seconds": 3600,
"turns": 12,
"failures": 4,
"retries": 2,
"error_rate": 0.333,
"retry_rate": 0.167,
"state": "degraded",
"last_error_at": "2026-10-23T09:00:00.000Z",
"last_error_code": "INVALID_REQUEST",
"last_error_http_status": 408,
"last_error_message": "408: stream disconnected…"
}
},
"health_scope": {
"observed_since": "2026-10-23T08:00:00.000Z",
"window_seconds": 3600,
"note": "Observed by this bridge process from durable request/header, llm/retry and turn/end events; not historical telemetry."
}
}
alias是去掉版本号后的稳定名:gpt-6.1-sol和gpt-6-sol都是gpt/sol,gpt-6-luna/gpt-5.6-luna都是gpt/luna。多个模型共享别名时,解析会优先当前会话 provider、再默认 provider,然后选版本更高的。accepts_images:true/false来自 provider 披露的inputModalities;null表示 provider 没有披露,不要把它当成“能看图”。context_window/max_output_tokens:provider 披露的上下文上限和默认输出上限;null表示未知。provider_health是本 bridge 进程加载后观察到的一小时滚动窗口:turns是带模型请求的回合数,failures是turn/end为error的次数,retries来自llm/retry事件;error_rate = failures / turns,state为ok/degraded/unstable,没有样本时为unknown/turns: 0。它不包含 DSH 启动前或其他实例的历史,只适合判断“最近是不是在反复 408”。capabilities_available:false表示 provider 的resolveModelInfo失败,capability_error给出原因;别名和 effort 仍然可用。
dsh_set_model
{ "session_id": "session-...", "model": "deepseek-v4-pro", "provider": "deepseek-official", "reasoning_effort": "low" }
规则:
model:模型 id、精确 name,或稳定别名(gpt/luna、gpt/sol、deepseek/flash);用dsh_list_models获取。旧的旧provider/gpt-6-luna也能在 provider 改名后解析:provider 前缀不存在时会被忽略,只按模型名匹配。provider:当同一个模型 id 存在于多个 provider 下时用于消歧;如果只切 effort,可省略。- 选不到时返回
model_not_found,details 里带available_models和suggestions(按共享模型族 token 排序的前几个候选);重名多 provider 时返回model_ambiguous和candidates。 - 只传
reasoning_effort:保持当前模型,只换强度。 - 只传
model:新模型使用其默认 effort。 - 同时传两者:一起切换。
- effort 会按该模型的
reasoning.efforts校验,非法值会返回可用列表。 - 注意:DSH 的
sessionController.selectModel会把选择同时保存为“新会话的默认模型”。这是 DSH GUI 的既有行为,不是本插件额外引入的。
dsh_list_permissions
返回当前部署可选的权限预设、它们的 sandbox/approval 组合以及默认预设:
{
"default_preset": "workspace-write",
"mcp_default": "danger-full-access",
"options": [
{ "value": "read-only", "name": "read-only" },
{ "value": "workspace-write", "name": "workspace-write" },
{ "value": "danger-full-access", "name": "danger-full-access" }
],
"default_options": [
{ "value": "read-only", "name": "read-only" },
{ "value": "workspace-write", "name": "workspace-write" },
{ "value": "danger-full-access", "name": "danger-full-access" }
]
}
default_options 是配置中可持久选择的预设;auto(若当前集成存在)只出现在 options 中,custom 是派生状态,二者不能通过 dsh_set_permission 选择。mcp_default 是 dsh_create_session 的默认权限(danger-full-access);default_preset 是 DSH GUI 新会话的全局默认,二者可能不同。
dsh_set_permission
{ "session_id": "session-...", "permission": "workspace-write" }
返回:
{
"session_id": "session-...",
"previous": {
"preset": "danger-full-access",
"sandbox": "danger-full-access",
"approval": "never"
},
"selected": {
"preset": "workspace-write",
"sandbox": "workspace-write",
"approval": "ask"
},
"notified": true,
"note": "Applies to subsequent tool calls..."
}
- 权限预设同时决定 sandbox mode 和 approval policy:
danger-full-access→ sandboxdanger-full-access,approvalneverworkspace-write→ sandboxworkspace-write,approvalaskread-only→ sandboxread-only(若部署配置了该预设)
- 切换立即写入 session log,影响后续工具调用。
- 如果会话已有回合且 approval policy 发生变化,正在运行的 Agent 会收到一条“approval policy changed”的用户消息(
notified: true);空白会话只记录 durable 变更,不额外注入消息。 - 只改当前会话,不改 DSH 全局默认权限;GUI 新建会话的默认权限仍由 DSH 的
defaultPreset决定。 - MCP 新建会话默认已经是
danger-full-access,通常只有在需要收紧权限时才调用本工具。
dsh_stop_session
- 会话未运行:返回
stopped: false, status: "idle"。 - 会话运行中:调用
sessionController.cancel,返回stopped: true, status: "cancelling"。 - 只取消当前回合,不会丢弃队列中的 pending 消息。
- 用
dsh_get_session确认running变为false。
dsh_archive_session / dsh_remove_workspace
归档会话,停止其运行中的工作,并从活动列表隐藏;durable 日志保留,可在 DSH GUI 中 unarchive。用于其他 Agent 完成任务后的清理。
- 0.8.0 起归档会话仍然可用
dsh_get_messages/dsh_get_turn_result/dsh_get_status读回,last_turn会保留 408 等错误;只是不能再发新消息。 remove_workspace: true:归档后如果该工作区没有其他非归档会话,顺手删除工作区登记。共享工作区还有别的会话时不会删,返回workspace_remaining_sessions。dsh_remove_workspace { workspace_id, only_if_empty: true }:单独清理空工作区;默认有活跃会话时返回workspace_not_empty,传only_if_empty: false可强制删除登记(不会删会话/日志)。
6. 安全
默认完全权限:MCP 调用
dsh_create_session创建的会话默认使用danger-full-access(完全文件访问、审批策略never)。这意味着被调用的 Agent 可以读写本机任意可访问路径,不需要审批。只应把 MCP 端点暴露给受信任的本机 Agent;不要把端口暴露到公网。
DSH 的 webServer 明确不提供 server-wide auth,路由 owner 自己负责请求策略。本插件因此实现了自己的准入规则:
-
默认只接受 loopback 请求(
127.0.0.1/::1/::ffff:127.0.0.1)。DSH 默认也只绑定 loopback。 -
如果配置了
token,则所有/mcp和/mcp/info请求必须携带:Authorization: Bearer <token> -
有
Origin头的浏览器请求必须是 DSH 自身的 origin;其他 origin 返回 403。没有 CORS 头,浏览器跨域请求会被拒绝。 -
请求体上限 8 MiB;非
application/json返回 415。 -
如果 DSH 被配置为绑定非 loopback 地址,而插件没有设置 token,非 loopback 请求会被拒绝(403)。
-
客户端租约/自动收尾只操作
Mcp-Session-Id对应的归属会话:清空其队列并 cancel 运行中的回合,不归档、不删除日志;Mcp-Session-Id不是认证凭据,真正的访问控制仍依赖 loopback/token。
需要更保守的默认时,可在 dsh_create_session 中显式传 permission: "workspace-write",或在创建后调用 dsh_set_permission。
设置 token
在 profile 的 cordis.patch.yml 中修改本 bundle 行:
- id: dsh-mcp-bridge
name: '@local/dsh-mcp-bridge'
config:
path: /mcp
token: 'replace-with-a-random-token-at-least-16-chars'
然后重新加载插件。MCP 客户端配置需要加 header,例如 Claude Code:
ENDPOINT=$(jq -r .endpoint ~/.dsh/mcp-bridge-endpoint.json)
claude mcp add --transport http dsh "$ENDPOINT" \
--header "Authorization: Bearer replace-with-a-random-token-at-least-16-chars"
不要把 DSH 的 Web 端口暴露到公网。本插件的 token 只保护
/mcp,不保护 DSH GUI 的其他路由。
7. 配置
- insert:
- id: dsh-mcp-bridge
name: '@local/dsh-mcp-bridge'
config:
path: /mcp
# token: 'optional, >=16 chars'
# endpoint_file: '~/.dsh/mcp-bridge-endpoint.json'
# client_lease_seconds: 120
# client_disconnect_grace_seconds: 30
# auto_stop_owned_sessions: true
| 字段 | 默认 | 说明 |
|---|---|---|
path | /mcp | MCP 路由前缀;info 路由为 ${path}/info。不能是 / |
token | 未设置 | Bearer token;设置后所有请求必须携带。至少 16 字符 |
endpoint_file | ~/.dsh/mcp-bridge-endpoint.json | 固定位置的端点文件;支持 ~、绝对路径和相对路径。文件权限 0600,不含 token |
client_lease_seconds | 120 | 客户端超过这么久没有任何请求视为心跳超时;0 表示不限时。长请求在飞时不计超时 |
client_disconnect_grace_seconds | 30 | 请求连接异常断开后的宽限;期间没有新请求就收尾 |
auto_stop_owned_sessions | true | 超时/断连时是否自动清队列并 cancel 该客户端拥有的运行中回合 |
修改 path 后,端点变为 http://127.0.0.1:<port>${path},endpoint 文件会同步更新。
8. 限制与注意事项
- 只支持 Streamable HTTP:没有 stdio、没有旧版 HTTP+SSE、没有
Mcp-Session-Id。Claude Code、Cursor、VS Code 等现代 MCP 客户端可用。 - 默认模式
standard:dsh_create_session显式使用standard,即使 DSH profile 的默认模式被改过。可用dsh_list_modes查看,dsh_set_mode在首回合前切换。 - 模式在首回合后锁定:DSH 的
agentPresets.select只允许空白会话;首回合开始后调用dsh_set_mode返回mode_locked。需要换模式请新建会话。 - 默认完全权限:MCP 新建会话默认
danger-full-access。这是显式的桥接默认,安全边界依赖 loopback/token;不要暴露到不可信网络。 - 需要 DSH Host 正在运行:端点随 DSH Web/Desktop 生命周期存在;DSH 退出后端点消失。
- 审批与用户提问:普通用户提问可通过
dsh_get_questions/dsh_answer_question查询和回答;权限审批和计划审核不在此能力范围。如果会话触发需要审批的工具,仍须通过 DSH 原来的审批渠道处理,不能通过提问答案绕过。 - 模型选择影响全局默认:见
dsh_set_model的说明。 - 权限切换只影响当前会话:
dsh_set_permission不改变 DSH GUI 的全局默认权限预设;它只写当前 session 的permission/preset、sandbox/mode、approval/policy。 auto/custom不可由 MCP 选择:auto是 DSH GUI 的可选集成,custom是派生状态;MCP 只接受配置中的持久预设(default_options)。- 客户端收尾依赖
Mcp-Session-Id:不发送这个头的客户端仍可用,但不会被归属、不会自动收尾,dsh_list_client_sessions/dsh_stop_client_sessions会报client_identity_required。 writable_roots是共同父目录,不是精确白名单:DSH 只支持一个workspaceRoot。要严格到“只允许这两个目录”,目前只能把它们放进同一个专用父目录,或使用danger-full-access并自行约束。dsh_list_models.provider_health只代表本 bridge 进程加载后观察到的一小时:它不是 DSH 全局历史,也不包含启动前的失败。- 归档会话只读:
dsh_get_messages/dsh_get_turn_result/dsh_get_status可读,dsh_send_message会返回session_archived;要在 DSH GUI 里 unarchive 后才能继续。 - 按消息 effort 会短暂改默认模型:底层
selectModel会把选择保存为“新会话默认”,回合结束后恢复;如果同一时间有别的会话新建,可能看到这个短暂默认值。 - 不要用
status: "idle"判断成功:超时、408 断流、取消后 Agent 都会回到 idle。用dsh_get_status.last_turn或dsh_get_turn_result的finish_reason/ok/error.http_status判断。 dsh_send_message仍是同步的:默认在 HTTP 请求内等待;客户端自身可能有更短的超时。长任务用wait: false+dsh_wait,timeout_seconds最大 600,客户端 HTTP 超时要大于它。超时后用dsh_wait { since }或dsh_get_turn_result收尾,不要自己 sleep 轮询。- 回合错误默认走两条路:
dsh_send_message把它作为 MCPisError返回;dsh_wait/dsh_get_turn_result把它作为正常数据返回(finish_reason: "error"、error.http_status: 408)。 - endpoint 文件可能过期:进程崩溃时不会删除;先用
pid或/mcp/info验证,重启 DSH 后文件会更新。 - 会话标题:本插件不主动设置标题;DSH 会根据首条 prompt 自动生成。
- 单用户:没有多租户或按调用方隔离;所有 MCP 客户端都以 DSH owner 身份操作。
- 归档不是删除:
dsh_archive_session保留日志,只从活动列表隐藏。
9. 开发与测试
npm test
node scripts/smoke.mjs
test/protocol.test.js:JSON-RPC / MCP 协议、工具分发、错误、批处理、模型选择解析、配置校验。test/bridge.test.js:用 fake Host context 覆盖创建、状态/活动/队列、回合结果、长轮询、发送/超时/408、模型别名/能力/健康度、客户端归属/收尾、归档读取、工作区清理、按消息 effort。test/turns.test.js:回合边界、usage、工具调用、408/503 状态解析、打开回合的当前工具、指定回合、prompt→turn 映射。test/models.test.js:稳定别名、版本排序、别名候选、model_not_found近似建议。test/endpoint.test.js:固定端点文件、原子写入、权限和所有权清理。test/http.test.js:真实node:http下的初始化、工具调用、状态码、Origin、Bearer token。test/questions.test.js:前台/补答生命周期、GUI/MCP 竞态与取消、长轮询通知、重复答案、输入校验、超时原因保留、跳过/自由文本/多选。scripts/smoke.mjs:对已安装的真实 DSH 端点做端到端 smoke test(会创建一个真实会话并发送 prompt,可加--mode、--permission与--archive)。默认从 endpoint 文件发现地址。
目录
index.js Host plugin 入口、路由注册、配置校验、endpoint 文件发布
lib/server.js MCP Streamable HTTP + JSON-RPC 协议层
lib/bridge.js DSH 会话工具实现
lib/turns.js 回合/活动重建、finish_reason 与 HTTP 状态解析
lib/models.js 稳定模型别名、版本排序、近似候选
lib/questions.js 用户提问适配
lib/endpoint.js 固定端点文件(原子写、所有权清理)
lib/util.js 参数校验、错误、文本/usage 辅助
test/ node:test 测试
scripts/smoke.mjs 真实端点 smoke test
docs/hot-reload.md HMR 热重载调研(触发方式、机制、实测、限制)
cordis.patch.yml bundle patch
locale/{en,zh}.json 插件显示名称/描述
icon.svg 插件图标
重新加载
修改源码后无需重启:见 不重启 DSH 热重载插件(HMR)。没有 HMR 时仍需完全重启 DSH Desktop。