# dsh-xiaoyi
[English](./README.md) | **简体中文**
把「**小艺开放平台 · OpenClaw 模式**」的通道实现成 **DSH 插件**:手机上的小艺(语音/文字/图片)直接对话到你本机运行的 DSH 会话。
> **这不是一个独立的聊天机器人。** 它只是"入口",小艺连进来的是你本机那套 DSH 会话(同一套工具、同一份工作目录)。没有第二个 agent。
```
手机小艺(在哪都行)
│ 语音 / 文字 / 图片
▼
华为云 wss://hag.cloud.huawei.com/openclaw/v1/ws/link
│ 长连接(AK/SK 鉴权 + 30 秒心跳)
▼
本插件(跑在 DSH 宿主进程里)
│
▼
DSH 会话 xiaoyi-listen-*
```
---
## 前置条件(先看完再装)
这个东西**不是开箱即用的**,装之前请确认三件事:
| # | 条件 | 说明 |
| --- | --- | --- |
| 1 | **你有一个 DSH 宿主** | DSH = DeepSeek Harness。本插件是宿主侧插件,没有宿主它跑不起来 |
| 2 | **你有小艺开放平台的 OpenClaw 模式凭证** | 需要 `AK` / `SK` / `agentId` 三样。这是华为云侧的开发者凭证,需要你自己去小艺开放平台申请开通 |
| 3 | **Node.js ≥ 20** | 代码用了内置的 `fetch` / `FormData` / `Blob`。开发环境是 Node 24 |
> 如果你拿不到第 2 项,也可以把这个仓库当成**协议实现参考**来读——`index.js` 里把 OpenClaw 的握手、鉴权、心跳、JSON-RPC 收发都写全了。
### `AK` / `SK` / `agentId` 到底去哪儿搞?
**这三样是华为发的,本插件造不出来,也没法帮你判断你的账号有没有资格。** 得你自己去申请:
1. 注册**华为开发者账号**并完成**实名认证**——
2. 登录**小艺开放平台**,创建一个智能体——
创建时**必须选「OpenClaw 模式」**,别的模式跟本插件不兼容。
3. 在智能体详情页取三样东西:
- `agentId`——在 **AgentCard** 模块里;
- `AK` / `SK`——左侧**「凭证管理」→「新建凭证」**。**SK 只显示一次**,当场复制存好。
4. 把你自己的华为账号加进智能体的**「测试管理」→「测试白名单」**。上架之前只有白名单里的账号能访问,大约几分钟后生效。
5. 这个模式的官方文档——
> 菜单名、入口位置、审核流程**全部由华为决定,随时可能变**。上面哪一步跟你看的对不上,请以华为官方文档为准,别以这份文件为准。
> 本插件只**使用**这三样凭证,不会替你去跟华为要任何别的东西。
---
## 特性
- **零第三方依赖**:只用 Node 内置模块,**自己手写了 WebSocket 客户端**(RFC6455 客户端侧:文本帧、分片重组、ping/pong、握手校验)。
插件树里少一个包,就少一次「某个包加载失败 → 整个宿主起不来」的风险。
- **完整协议实现**:鉴权签名、初始化握手、心跳与假死检测、JSON-RPC 2.0 流式收发、控制类消息(clearContext / tasks/cancel)。
- **入站附件自动落地**:小艺发来的图片/文件自动下载到本地,再把路径交给会话里的 agent(可直接 `read_image` / `read`)。
- **出站在线文件(实验性)**:把本地文件发回小艺(协议只接受公网 https URL,支持本机隧道或第三方图床两种方式)。**这个方向不可靠**,见下面的方向表。
- **会话复用**:冷启动优先 resume 原来那条会话,不重复新开对话。
### 两个方向分别能传什么
| 方向 | 支持情况 |
| --- | --- |
| **手机 → 本插件**
(你发给小艺的) | **文字、语音(转成文字)、图片/文件都可以。** 附件会下载到 `~/.dsh/xiaoyi/media/`,再把本地路径交给会话里的 agent |
| **本插件 → 手机**
(小艺回给你的) | **文字可以**——回复正文,以及 `xiaoyi_send` 推的短消息。**图片/文件推回手机不可靠**:协议只接受公网 https URL,而且小艺客户端不保证把文件渲染出来。`xiaoyi_send_photo` 请当成实验性功能 |
> 一句话:**入站方向才是靠得住的。** 如果你的用法依赖"图片能回到手机上",别指望它。
---
## 安装
### 1. 放置源码
把本目录放到任意位置,推荐和其他插件放一起:
```
~/.dsh/plugin-sources/dsh-xiaoyi
```
### 2. 注册到 profile
**方式 A(推荐,一条命令):**
```bash
# 从 GitHub 仓库装
dsh plugin --profile web add git+https://github.com/HangZhouXi/dsh-xiaoyi
# 或从 npm 装(发布到 npm 之后)
dsh plugin --profile web add dsh-xiaoyi
```
**方式 B(手动编辑):**
编辑 `~/.dsh/profiles/web/package.json`:
```jsonc
{
"dsh": {
"profile": {
"bundles": [
// ...
"dsh-xiaoyi" // ← 加进 bundles
]
}
},
"dependencies": {
// ...
"dsh-xiaoyi": "git+file:///C:/Users/<你>/.dsh/plugin-sources/dsh-xiaoyi"
}
}
```
> 也可以直接用本地路径依赖:`"dsh-xiaoyi": "file:../../plugin-sources/dsh-xiaoyi"`。
### 3. 安装并冷启动
```bash
dsh plugin --profile web install
```
然后**冷启动 DSH 宿主**(不是热重载 —— 宿主端插件必须冷启动才生效)。
### 4. 填凭证
编辑 `~/.dsh/xiaoyi/state.json`:
```json
{
"enabled": true,
"ak": "小艺开放平台凭证 AK",
"sk": "小艺开放平台凭证 SK",
"agentId": "OpenClaw 模式智能体的 agentId",
"wsUrl": "wss://hag.cloud.huawei.com/openclaw/v1/ws/link",
"sessionId": "",
"cwd": "C:\\Users\\<你>\\Desktop",
"publicBaseUrl": "",
"imageHost": ""
}
```
| 字段 | 作用 |
| --- | --- |
| `enabled` | 总开关。`false` 会断开通道 |
| `ak` / `sk` / `agentId` | 小艺开放平台凭证,三个都必需 |
| `wsUrl` | WebSocket 地址,除非华为侧改动,保持默认即可 |
| `sessionId` | 要复用的 DSH 会话,通常由插件自己写入 |
| `cwd` | 会话工作目录,必须可写 |
| `publicBaseUrl` | 只用于出站文件的 `local` 模式:你那条隧道对外的公网 https 前缀 |
| `imageHost` | 出站文件的默认发布方式。填 `"catbox"` 就把 catbox.moe 设为默认;留空则只用本机 |
或者启动后让会话里的 agent 调工具 `xiaoyi_set` 直接改(改完立即生效,会重连)。
日志:`~/.dsh/xiaoyi/xiaoyi.log`
---
## 提供给 agent 的工具
| 工具 | 作用 |
| --- | --- |
| `xiaoyi_status` | 通道状态:凭证齐不齐、WebSocket 连没连上、监听会话 id、队列长度、最后错误、消息计数 |
| `xiaoyi_set` | 改凭证 / 开关(`ak`、`sk`、`agentId`、`wsUrl`、`enabled`),立即作用于活的连接 |
| `xiaoyi_send` | 主动往当前小艺会话推一句短消息(适合长任务收尾提醒) |
| `xiaoyi_send_photo` | 把本地图片/文件发给小艺(协议只收公网 https,见下方「出站文件」) |
| `xiaoyi_reconnect` | 强制断开重连(改完凭证、或连接发僵时用) |
## HTTP 路由
| 路径 | 作用 |
| --- | --- |
| `GET /xiaoyi/state` | JSON 状态 |
| `GET /xiaoyi/connect` | 启动通道(并确保监听会话就绪) |
| `GET /xiaoyi/disconnect` | 断开通道 |
| `GET /xiaoyi/public/<文件名>` | 出站文件的本机对外通道(文件名含随机串,不可猜) |
---
## 协议要点
> 照 `@ynhcj/xiaoyi` 的 dist 逐行核对过,实现上不依赖该包。
| 环节 | 内容 |
| --- | --- |
| 连接 | `wss://hag.cloud.huawei.com/openclaw/v1/ws/link` |
| 鉴权请求头 | `x-access-key`=AK、`x-sign`=`Base64(HMAC-SHA256(SK, ts))`、`x-ts`=毫秒、`x-agent-id`=agentId |
| 握手 | `{"msgType":"clawd_bot_init","agentId":"..."}` |
| 心跳 | 每 30s `{"msgType":"heartbeat","agentId":"...","timestamp":...}` + 协议层 ping |
| 入站 | JSON-RPC 2.0,`method="message/stream"`;`sessionId=params.sessionId`、`taskId=params.id`、`messageId`=顶层 `id`;文本在 `params.message.parts[kind=text].text`,附件在 `parts[kind=file].file{name,mimeType,uri}` |
| 出站 | `{"msgType":"agent_response", agentId, sessionId, taskId, msgDetail:""}` |
| 出站帧类型 | `result.kind="status-update"`(先回执 `state=working`)、`result.kind="artifact-update"`(正文,`lastChunk=true, final=true` 收尾) |
| 控制 | `clearContext` / `tasks/cancel` / `action="clear"` → 回对应 JSON-RPC response |
### 出站文件(`xiaoyi_send_photo`)
协议回给端侧的 Part 是 `{ url, filename, mediaType }` —— **只接受公网 https 地址,没有"上传字节"的接口**。所以有两种模式:
| mode | 行为 | 隐私 |
| --- | --- | --- |
| `local`(**默认**) | 复制到 `~/.dsh/xiaoyi/public/`,由本插件自己的路由对外提供,再配合一条隧道(如 `localhost.run`)把 `state.publicBaseUrl` 指过来 | 文件不出本机(除了隧道) |
| `catbox` | 上传到第三方图床 `catbox.moe` | **文件会被传到第三方**,必须显式指定才启用 |
配 `state.publicBaseUrl` 之后 `local` 模式才能用;没配就直接报错,**不会**偷偷回退到图床。
---
## 安全与隐私
- **凭证只存在本地** `~/.dsh/xiaoyi/state.json`,已被 `.gitignore` 排除,不进仓库、不进日志(日志只记录"有/无")。
- **入站附件有 SSRF 防护**:入站 `uri` 来自外部消息,属于不可信输入。只允许 `http/https`;重定向的**每一跳都在发请求之前**先检查(用 `redirect: 'manual'` 自己跟),而且每台主机要过**两道关**:结构检查 + **它实际解析到什么地址**。IPv6 是按字节解析的,不做字符串前缀比较,所以同义写法绕不过去。拦掉的段:环回、私网、链路本地、唯一本地、多播、站点本地、IPv4-mapped(`::ffff:a.b.c.d`)、NAT64、6to4、Teredo —— 含 `169.254.169.254` 这类云元数据地址。
- **附件有体积闸门**:单文件上限 50 MB,流式读取,对方少报 `content-length` 也不会把内存打爆。
- **出站公开链接不可猜**:文件名带 8 字节随机串。
- ⚠️ **HTTP 路由本身没有鉴权**。`/xiaoyi/state`、`/xiaoyi/public/*` 谁都能访问。请**不要**把 DSH 的端口裸奔到公网;要走隧道的话,用带鉴权/随机域名的方案。
---
## 已知限制
- **图片/文件推回手机不可靠**:协议只接受公网 https URL,而且文件发出去之后小艺客户端会不会渲染出来,不在本插件的控制范围内。文字回复不受影响。
- **出站附件必须走公网 URL**:这是协议限制,不是实现偷懒。
- **入站附件链接是预签名的、大约 30 分钟过期**:所以必须"一到就抓"。本插件就是这么做的;如果下载失败,基本就是链接已经过期。
- **`media/` 目录不会自动清理**:长期使用请自己定期清理 `~/.dsh/xiaoyi/media/`。
- **平台侧策略可能变**:OpenClaw 模式、地址、字段都可能随华为侧调整而失效,本插件不保证长期可用。
---
## 卸载
1. 从 `~/.dsh/profiles/web/package.json` 的 `bundles` 与 `dependencies` 里删掉 `dsh-xiaoyi`
2. `dsh plugin --profile web install`
3. 冷启动宿主
4. 想彻底清净的话,删掉 `~/.dsh/xiaoyi/`(里面有凭证和日志)
---
## 排错
| 现象 | 排查方向 |
| --- | --- |
| 装完没反应 | **冷启动**了吗?宿主端插件热重载不生效 |
| `status` 里 `ready:false` | AK / SK / agentId 有缺项 |
| 一直连不上、`lastError` 有握手错误 | `wsUrl` 是否被改错;网络是否能到 `hag.cloud.huawei.com:443` |
| 连上又断、反复重连 | 看日志里的"连接假死";超过 90 秒没有任何回音(含 pong)会主动重连 |
| 小艺发消息没回应 | 看 `~/.dsh/xiaoyi/xiaoyi.log`;确认宿主日志里插件是否加载成功 |
| 收到「DSH 会话未就绪」 | 宿主启动后第一次请求要现建会话,稍等再发一次 |
| 发的图片收不到 | 这个方向本来就是不可靠的,见「两个方向分别能传什么」。协议还要求公网 https,检查 `publicBaseUrl` 有没有配、隧道还活着吗 |
---
## 许可
[MIT](./LICENSE)
## 免责声明
**使用前请务必读 [`DISCLAIMER.zh-CN.md`](./DISCLAIMER.zh-CN.md)。**