# dsh-xiaozhi · 小智语音接入 DSH 把小智(Xiaozhi)语音助手接入 **DeepSeek Harness(DSH)Web**:DSH 作为 **MCP 工具提供方**, 通过 WebSocket 上的 JSON-RPC 2.0 把工作区、会话、模型、设置、文件等能力封装成工具,供小智语音调用。 **中文** · [**English**](./README.md) · [安装与验证](./INSTALL.md) · [工具清单](./docs/TOOLS.md) > 把小智(Xiaozhi)语音助手接入 DSH Web:DSH 作为 MCP 工具提供方,把 35 个 DSH Web 接口封装成 16 个 > 语音友好工具,自带 DSH Web 设置页。 > Connect the Xiaozhi voice assistant to DSH Web as an MCP tool provider — 35 endpoints, 16 voice-friendly tools. > 实施前的设计提案(架构 ADR、v2 复核、设置页 UX 走查)归档在 [docs/design/](./docs/design/),其中与实际交付不一致的地方已逐条注明。 --- ## 1. 它解决什么问题 DSH Web 的能力都在 HTTP REST 接口上,而小智只认 **MCP**(Model Context Protocol)。 本插件坐在两者中间: ``` 你说一句话 │ ▼ ┌─────────────┐ MCP(JSON-RPC 2.0 / WebSocket) ┌──────────────────────────┐ │ 小智助手 │ ◄───────────────────────────────► │ dsh-xiaozhi (Host 半边) │ │ (App/硬件) │ initialize / tools/list / call │ ├ MCP 会话与工具注册表 │ └─────────────┘ │ ├ 能力→REST 路由映射 │ │ └ LocalInvoker(进程内调用)│ └───────────┬──────────────┘ │ 不走网络,直接调用 ▼ ┌──────────────────────────┐ │ DSH Web REST 路由(内置副本)│ └──────────────────────────┘ ``` 三个关键设计决定: 1. **DSH 永远是 MCP 的“服务端/工具提供方”。** 两种传输方式下都由 DSH 应答 `initialize` / `ping` / `tools/list` / `tools/call`,从不主动发起这些请求。 2. **默认主动外连(`endpoint` 模式)。** DSH 作为 WebSocket 客户端连到小智官方 MCP 接入点,所以**不需要**公网 IP、端口映射或反向代理。 3. **进程内调用,而不是回环 HTTP。** 工具调用通过 `LocalInvoker` 直接打到内置的 DSH REST 路由,无需猜测 DSH 自身的 host/port/鉴权,也不依赖外部服务。 --- ## 2. 快速开始(3 步) **前提**:DSH Web 已经在跑(`dsh web`,默认 `http://127.0.0.1:3080`);你有小智账号并能打开它的 「MCP 接入点」页面。 1. **安装插件**(在本仓库目录下执行): ```bash dsh plugin add /Users/tsbj/feyanggit/DHS-test/dsh-xiaozhi ``` 也可以在 DSH Web 的「设置 → 插件」里用「安装本地目录」选择该目录。 2. **拿到接入点地址并粘贴**:打开 DSH Web →「设置 → **小智接入**」→「接入配置」, 把小智后台的 MCP 接入点 WebSocket 地址(形如 `wss://api.xiaozhi.me/mcp/?token=…`)填进「小智 MCP 接入点地址」,点「保存并重载」。 3. **看状态**:回到「状态」页签,连接状态应为 `connected`,并显示已连接客户端数。 点一次「测试连接」可以看到真实握手结果。 然后在手机上对小智说:**“用 dsh 看一下我的会话列表”** 或 **“让 DSH 汇报一下运行状态”**。 > 接入点地址含 token,属于敏感信息。设置页读回时只显示 `token=***`, > 保存时也会被识别为「未修改」,不会把掩码写进配置。详见 §7。 --- ## 3. 两种传输方式 | | `endpoint`(默认,推荐) | `server`(自建服务端) | | --- | --- | --- | | 谁发起连接 | DSH 主动外连小智接入点 | 小智服务端连到 DSH | | 需要公网可达吗 | 不需要 | 需要(或反向代理 / 内网同段) | | 主要配置 | `endpointUrl`、`endpointHeaders` | `serverPath`、`serverPort`、`serverToken` | | 适用场景 | 小智官方 MCP 接入点、家用/办公本机 | 自建 `xiaozhi-esp32-server`、内网统一网关 | 两种方式可以同时开启:`mode` 决定**主**通道,`serverPort > 0` 时会额外在 `0.0.0.0` 上 监听一个独立端口。 **断线重连**:`endpoint` 模式带指数退避(`reconnectMinMs` → `reconnectMaxMs`,含 ±20% 抖动) 和 `heartbeatMs` 心跳;「状态」页的「重连次数」和日志可以看到全部过程。 --- ## 4. 工具暴露方式:grouped(默认)还是 flat 小智的工具名会被清洗成 `[A-Za-z0-9_\-中文]`,本插件的所有工具名都是该规则的**不动点** (例如 `dsh_session_history`),因此不会在平台侧被改名。 | 模式 | 工具数 | 说明 | | --- | --- | --- | | `grouped`(默认) | **16**(关闭工具组后更少) | 按能力域合并,用 `action` 参数选择具体动作 | | `flat` | **35** | 每个接口一个工具,名字与接口一一对应 | **默认选 grouped 的原因**:语音模型在 35 个工具里挑一个的准确率明显低于在 16 个里挑。 超过 24 个工具时设置页会给出提示。完整对照表见 [docs/TOOLS.md](./docs/TOOLS.md)。 **工具组开关**:可在「接入配置 → 工具组开关」里关掉暂时不用的域(例如 `docs`、`files`)。 关闭整组会让对应工具(或分组工具里的对应动作)直接不可用。 **写入开关**:`allowWriteTools = false` 时,创建/修改/删除/发送类动作会被拒绝并返回一句 可直接朗读的中文说明,**只读动作照常可用**(即使它们和写动作合并在同一个 grouped 工具里)。 --- ## 5. 能力覆盖 **35 个接口全部可达**,两种工具模式下都覆盖 35/35: | 能力域 | 能力数 | 对应接口 | | --- | --- | --- | | 系统状态 | 1 | `GET /system/status` | | 工作区 | 6 | `/workspaces`、`/workspaces/:id`、`/workspaces/:id/sessions` | | 会话 | 13 | `/sessions`、`/sessions/:id`、`history`、`stats`、`todos`、`skills`、`questions`、`answers`、`cancel`、`events` | | 文件 | 3 | `/sessions/:id/files`、`/files/download` | | 对话 | 3 | `/sessions/:id/prompt`、`/prompt-stream`、`/chat/completions` | | 模型与预设 | 5 | `/models`、`/models/default`、`/providers`、`/presets` | | 系统设置 | 2 | `/settings`、`/settings/:namespace` | | 接口文档 | 2 | `/docs`、`/openapi.json` | 其中 4 个能力在 MCP 语义下做了**降级**(不是缺失,但仍需你知情),详见下一节。 --- ## 6. MCP 语义降级(务必阅读) MCP 的 `tools/call` 是**一问一答**的,没有增量流式通道,而原 REST 接口里有几个是流式的。 本插件选择「尽量保住语义、并如实告知」而不是假装支持: | 能力 | 原本形态 | 在 MCP 上的行为 | 你需要知道 | | --- | --- | --- | --- | | `conversation.promptStream`
(`dsh_say` / `dsh_conversation_promptstream`) | `text/event-stream`,增量推送 | DSH 在服务端**收集完整个流**后一次性返回结果文本 | 语音端不会逐步流式;`promptTimeoutMs` 决定等待上限,超时返回「已提交、仍在运行」而不是错误 | | `sessions.events`
(`dsh_session_watch`) | 常驻 SSE 事件流 | 只在**有限时间窗**内(1–30 秒)收集事件后返回 | 只能当“看一眼最近的动静”,不能当实时监听;需要持续监听请用 `sessions.stats` 轮询 | | `files.download` | 二进制文件流 | 文本文件回传正文(截断到 `maxVoiceChars`);**二进制只回传摘要**(大小、类型、路径) | 语音播报二进制内容本来也没有意义;需要真文件请走 DSH Web 界面或内置 REST 层 | | `docs.openapi` | 完整 OpenAPI JSON | 返回**结构摘要**(`openapi` 版本、`title`、路径数、最多 100 条路径、字节数、原始 URL) | 完整规范请直接访问 `apiBase/openapi.json` | 另外两点: * `dsh_say(wait=false)` 用于「把话转给会话、不等结果」:它在约 1.5 秒预算内提交 `prompt-stream`,超时就静默返回「已提交」并附上会话当前状态,不会让你干等。 * 所有工具结果都会按 `maxVoiceChars` 截断成**单个 text 块**,避免语音播报冗长。 --- ## 7. 安全模型(请按自己的部署范围核对) | 面 | 默认 | 保护 | | --- | --- | --- | | DSH Web 设置页 API `/dsh-xiaozhi/admin` | 仅本机可访问(DSH 默认绑定 `127.0.0.1`) | ①跨站 `Origin` 拒绝 ②`sec-fetch-site: cross-site` 拒绝 ③**每个请求**(含读取)都必须带 `x-dsh-xiaozhi-admin: 1` 自定义头;跨站表单/图片无法设置自定义头,跨域 `fetch` 会触发预检而本路由 `cors: false` 从不放行 ④若宿主存在 `connection` 服务,先由它做浏览器 cookie + Host/Origin 判定(401/403) | | 内置 DSH REST 层 `/dsh-xiaozhi/api/v1` | 默认开启 | 设置 `apiKey` 后需 `Authorization: Bearer …` 或 `X-API-Key`;**未设置密钥时会给出警告** | | MCP 工具(对外) | 默认开启,默认允许写 | `allowWriteTools=false` 关闭全部写操作;`disabledGroups` 缩小攻击面 | | `server` 模式独立端口 | 默认关闭(`serverPort=0`) | 填端口会在 `0.0.0.0` 监听,**必须**设置 `serverToken`,否则设置页会警告 | **密钥掩码**:设置页读回配置时,`apiKey`、`serverToken`、接入点 URL 里的 `token=` 以及 `endpointHeaders` 的所有**值**都被替换成掩码(`••••••` / `***`),但会保留 header **名字**。 保存时掩码会被识别为“未修改”并丢弃,**不会**用掩码覆盖真实密钥。 **`endpointHeaders` 只能新增/覆盖,不能通过设置页删除**(底层是合并写入)。 要删掉某个请求头,手工编辑 `settings.json`。 --- ## 8. 配置项 配置分三层,优先级从低到高: 1. 代码默认值(`src/config.ts` 的 `DEFAULTS`) 2. 插件行的 `config`(profile 的 `cordis.patch.yml`) 3. 设置页保存的覆盖(`/settings.json`) | 配置 | 默认 | 说明 | | --- | --- | --- | | `enabled` | `true` | 关闭后小智无法调用任何工具 | | `mode` | `endpoint` | `endpoint` / `server` | | `endpointUrl` | `''` | 小智 MCP 接入点(`ws://`/`wss://`,须含 `/mcp/`) | | `endpointHeaders` | `{}` | 接入点附加请求头(合并写入) | | `serverPath` | `/mcp/xiaozhi` | `server` 模式的路径(须含 `/mcp/`) | | `serverPort` | `0` | `0` 复用 DSH Web 服务器;`>0` 额外监听 `0.0.0.0` | | `serverToken` | `''` | `serverPort>0` 时强烈建议设置 | | `toolMode` | `grouped` | `grouped` / `flat` | | `disabledGroups` | `[]` | 关闭的能力域 | | `allowWriteTools` | `true` | 是否允许写操作 | | `promptTimeoutMs` | `120000` | 语音指令等待上限(须小于内置 REST 层的 180000) | | `maxVoiceChars` | `700` | 单条回复截断长度 | | `listLimit` | `10` | 列表类结果条数 | | `heartbeatMs` | `30000` | 心跳间隔 | | `reconnectMinMs` / `reconnectMaxMs` | `1000` / `30000` | 重连退避区间 | | `apiPathPrefix` | `/dsh-xiaozhi/api` | **内置 REST 层**前缀(设置页 API 固定在 `/dsh-xiaozhi/admin`) | | `exposeDshApi` | `true` | 是否挂载内置 DSH REST 层 | | `apiKey` | `''` | 内置 REST 层鉴权密钥 | | `cors` | `false` | 内置 REST 层是否允许跨域 | | `defaultCwd` | `''` | 创建会话的默认目录 | | `maxUploadBytes` | `104857600` | 上传上限 | | `homeDir` | `''` | **只能**在插件行 `config` 里设置(见下) | | `logToolCalls` | `true` | 记录每次工具调用 | | `sendInitializedNotification` | `true` | MCP 握手后发送 `notifications/initialized` | | `serverName` | `DSH` | 对外声明的服务名 | > **为什么 `homeDir` 不在设置页里?** 它决定设定文件本身放在哪,如果允许从该文件里读, > 就会出现“设置页显示新目录、覆盖却仍写旧目录”的自相矛盾。因此 `homeDir` 固定只从插件行 > `config` 读取,设置页只做只读展示。环境变量 `DSH_XIAOZHI_HOME` 亦可指定。 --- ## 9. 设置页 DSH Web →「设置 → 小智接入」,共 5 个页签: * **状态**:连接状态徽标、传输方式、接入点(已掩码)、已连接客户端数、重连次数、最近错误、 需要注意的警告、对外地址、工具/能力计数、各工具组开关状态;可「测试连接」「立即重连」「刷新」。 * **接入配置**:基础项 + 工具组开关 + 折叠的高级项;「保存并重载」会写覆盖文件并重启运行时, 「恢复默认」清空全部覆盖。 * **工具清单**:当前实际暴露的工具、读写属性、覆盖的能力数。 * **能力清单**:35 个能力按域列出,含方法与路径。 * **日志**:插件环形日志(默认 300 条),可打开 5 秒自动刷新。 页面只用 DSH 主题 token(`--dsw-alias-*`)着色,**不依赖** `dsh-client-ui-primitives`, 因此明暗主题都跟随宿主,样式不会和宿主冲突。 --- ## 10. 开发与验证 ```bash cd dsh-xiaozhi bash scripts/build.sh # 需要 DSH 源码 checkout 提供 tsc(自动探测) node --test --test-timeout=30000 "test/*.test.mjs" ``` 测试覆盖(**107 个用例**): | 文件 | 覆盖内容 | | --- | --- | | `test/protocol.test.mjs` | MCP 报文、工具名清洗不动点、信封解析 | | `test/ws.test.mjs` | RFC 6455 分帧、掩码方向、分片、关闭握手 | | `test/config.test.mjs` | 三层配置合并、密钥掩码、`homeDir` 不可覆盖 | | `test/coverage.test.mjs` | **35 个接口逐条钉住**;两种工具编织都全覆盖;工具名是清洗不动点 | | `test/dispatcher.test.mjs` | 进程内调用:JSON、查询串、请求体、流式响应、404、超时 504 | | `test/mcp-session.test.mjs` | 真实 socket 上的握手 → `tools/list` → `tools/call`,含并发与协议错误 | | `test/routes.test.mjs` | 用**真实**路由表验证 35 个能力都命中已注册路由;分组工具端到端 | | `test/client.test.mjs` | 浏览器半边的常量一致性、双语字典完整性、helpers、`react-dom/server` 渲染 | | `test/admin.test.mjs` | 设置页 API:页面调用的每个路由都必须以正确方法可达;三层守卫;密钥掩码剥离 | | `test/docs.test.mjs` | 文档与代码一致性:工具名/能力/计数不得漂移 | `src/dshapi/` 是上游 `@dsh-external/dsh-web-service` v0.1.11 的**逐字拷贝**(BSD-3-Clause), 唯一的新文件是把它组装成单个路由的 `src/dshapi/service.ts`,这样上游更新时仍是干净的三方 diff。 详见 [NOTICE](./NOTICE)。 --- ## 11. 常见问题 | 现象 | 原因与处理 | | --- | --- | | 状态一直是 `disconnected` | 接入点地址没填或填错(必须是 `ws://`/`wss://` 且含 `/mcp/`,且**不能**含 `key`/`call` 字样);看「日志」页首个错误 | | 小智能看到工具但调用失败 | 检查 `allowWriteTools`;写入类动作被关闭时会返回明确的中文说明 | | 小智看不到任何工具 | `enabled=false`,或所有工具组都被关闭 | | 语音里会话/工作区 id 说不清 | grouped 工具返回的 id 都做了缩短(如 `sess-123`);也可以用名称调用 | | 局域网里的自建小智连不上 | `server` 模式且 `serverPort=0` 时只监听 DSH 服务器(默认仅本机);填端口并设置 `serverToken` | | 修改 `apiPathPrefix` 后设置页没变 | 符合预期:设置页 API 固定在 `/dsh-xiaozhi/admin`,`apiPathPrefix` 只影响内置 REST 层 | --- ## 12. 许可 BSD-3-Clause。派生自 `@dsh-external/dsh-web-service` v0.1.11(Copyright © 2026 toddpan 潘祖继), 同一许可。见 [LICENSE](./LICENSE) 与 [NOTICE](./NOTICE)。