← Back to home@ahhTou

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_turnsession_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/queuedonly_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;可覆盖本回合 effortsession_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_turnsession_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 的返回怎么读:

pendingtimed_outwaiting_for_answerfinish_reason含义
falsefalsefalsecompleted/error/…回合已结束,看 finish_reason
truetruetrue无子代理在提问;先 dsh_get_questions + dsh_answer_question,再 dsh_wait
truetruefalse无还在跑;用 dsh_wait { since }(可带 request_id)
truefalse任意无事件流结束但没有 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_waittrue
timeout到达 timeout_seconds 仍在运行;用返回的 cursor 重试true
idleAgent 空闲、队列为空,没有可等的事件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_seconds 1–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 → sandbox danger-full-access,approval never
    • workspace-write → sandbox workspace-write,approval ask
    • read-only → sandbox read-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 自己负责请求策略。本插件因此实现了自己的准入规则:

  1. 默认只接受 loopback 请求(127.0.0.1 / ::1 / ::ffff:127.0.0.1)。DSH 默认也只绑定 loopback。

  2. 如果配置了 token,则所有 /mcp 和 /mcp/info 请求必须携带:

    Authorization: Bearer <token>
    
  3. 有 Origin 头的浏览器请求必须是 DSH 自身的 origin;其他 origin 返回 403。没有 CORS 头,浏览器跨域请求会被拒绝。

  4. 请求体上限 8 MiB;非 application/json 返回 415。

  5. 如果 DSH 被配置为绑定非 loopback 地址,而插件没有设置 token,非 loopback 请求会被拒绝(403)。

  6. 客户端租约/自动收尾只操作 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/mcpMCP 路由前缀;info 路由为 ${path}/info。不能是 /
token未设置Bearer token;设置后所有请求必须携带。至少 16 字符
endpoint_file~/.dsh/mcp-bridge-endpoint.json固定位置的端点文件;支持 ~、绝对路径和相对路径。文件权限 0600,不含 token
client_lease_seconds120客户端超过这么久没有任何请求视为心跳超时;0 表示不限时。长请求在飞时不计超时
client_disconnect_grace_seconds30请求连接异常断开后的宽限;期间没有新请求就收尾
auto_stop_owned_sessionstrue超时/断连时是否自动清队列并 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 把它作为 MCP isError 返回;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。