Skip to content
dsh.fish
Bundle

dsh-xiaoyi

A DSH (DeepSeek Harness) host plugin that connects Huawei XiaoYi in OpenClaw mode to your local DSH session. Zero third-party dependencies, with a hand-written minimal RFC6455 WebSocket client.

Source
HangZhouXi
License
MIT
Updated
Updated 7 days ago

Readme

# dsh-xiaoyi

**English** | [简体中文](./README.zh-CN.md)

A **DSH (DeepSeek Harness) host plugin** that connects **Huawei XiaoYi in OpenClaw mode** to your local DSH session — voice, text and images from your phone reach the agent running on your own machine.

> **This is not a standalone chatbot.** XiaoYi is only the entry point; what it connects to is your local DSH session (same tools, same working directory). There is no second agent.

```
Huawei XiaoYi on your phone  (anywhere)
   │  voice / text / images
   ▼
Huawei Cloud  wss://hag.cloud.huawei.com/openclaw/v1/ws/link
   │  long-lived connection (AK/SK auth + 30s heartbeat)
   ▼
This plugin  (runs inside the DSH host process)
   │
   ▼
DSH session  xiaoyi-listen-*
```

---

## Prerequisites

This is **not** plug-and-play. Before installing, make sure you have all three:

| # | Requirement | Notes |
| --- | --- | --- |
| 1 | **A DSH host** | DSH = DeepSeek Harness. This is a host-side plugin; without a host it cannot run. |
| 2 | **XiaoYi Open Platform credentials (OpenClaw mode)** | You need `AK` / `SK` / `agentId`. These are Huawei Cloud developer credentials you must apply for yourself on the XiaoYi Open Platform. |
| 3 | **Node.js ≥ 20** | The code uses the built-in `fetch` / `FormData` / `Blob`. Developed on Node 24. |

> If you cannot obtain item 2, you can still read this repository as a **protocol reference implementation** — `index.js` implements the XiaoYi/OpenClaw handshake, auth, heartbeat and JSON-RPC message exchange in full.

### Where do `AK` / `SK` / `agentId` come from?

**They are issued by Huawei. This project cannot create them for you and cannot tell you whether your account is eligible.** You have to apply for them yourself:

1. Register a **Huawei developer account** and complete **real-name verification** — <https://developer.huawei.com/consumer/cn/register/>
2. Sign in to the **XiaoYi Open Platform** and create an agent — <https://developer.huawei.com/consumer/cn/hag/hagindex.html#/>
   When creating it, choose the **OpenClaw mode**. Other modes are not compatible with this plugin.
3. Take the values from the agent's detail page:
   - `agentId` — shown in the **AgentCard** block;
   - `AK` / `SK` — under **Credentials → New credential**. **The SK is displayed only once**, so copy it immediately.
4. Add your own Huawei account to the agent's **test whitelist**. Until the agent is published, only whitelisted accounts can reach it; it takes effect after a few minutes.
5. Official documentation for this mode — <https://developer.huawei.com/consumer/cn/doc/doccenter-celia/openclaw-0000002518410344>

> Menu names, entry points and the review process are decided entirely by Huawei and may change at any time. If a step above no longer matches what you see, follow Huawei's own documentation rather than this file.
> This plugin only **consumes** the three credentials. It never asks Huawei for anything else on your behalf.

---

## Features

- **Zero third-party dependencies.** Only Node built-ins are used, including a **hand-written minimal WebSocket client** (RFC6455 client side: text frames, fragmentation reassembly, ping/pong, and handshake verification). Fewer packages in the plugin tree means fewer chances that one failed package takes the whole host down.
- **Complete protocol implementation.** Auth signing, init handshake, heartbeat with stale-connection detection, JSON-RPC 2.0 streaming, and control messages (`clearContext` / `tasks/cancel`).
- **Inbound attachments.** Images and files sent from XiaoYi are downloaded to disk, and their local paths are handed to the agent in the session (ready for `read_image` / `read`).
- **Outbound online files (experimental).** Send local files back to XiaoYi (the protocol only accepts public https URLs; supports a local tunnel or a third-party image host). See the direction table below — this direction is **not** reliable.
- **Session reuse.** On a cold start the plugin resumes the existing session instead of opening a new conversation every time.

### What can travel in each direction

| Direction | Support |
| --- | --- |
| **Phone → this plugin**<br>(what you send to XiaoYi) | **Text, voice (transcribed) and images/files all work.** Attachments are downloaded to `~/.dsh/xiaoyi/media/` and their local paths are handed to the agent. |
| **This plugin → phone**<br>(what you get back in XiaoYi) | **Text works** — the reply body, plus anything sent with `xiaoyi_send`. **Pushing an image or file back to the phone is unreliable**: the protocol accepts only a public https URL, and the XiaoYi client is not guaranteed to render the file part. Treat `xiaoyi_send_photo` as experimental. |

> In short: **the inbound direction is the reliable one.** If your workflow depends on images or files arriving on your phone, do not rely on it.

---

## Installation

### 1. Place the source

Put this directory anywhere you keep plugins. The conventional location is:

```
~/.dsh/plugin-sources/dsh-xiaoyi
```

### 2. Register it in your profile

**Option A (recommended, one command):**

```bash
# install from the GitHub repository
dsh plugin --profile web add git+https://github.com/HangZhouXi/dsh-xiaoyi

# or from npm (after it is published there)
dsh plugin --profile web add dsh-xiaoyi
```

**Option B (edit by hand):**

Edit `~/.dsh/profiles/web/package.json`:

```jsonc
{
  "dsh": {
    "profile": {
      "bundles": [
        // ...
        "dsh-xiaoyi"          // ← add to bundles
      ]
    }
  },
  "dependencies": {
    // ...
    "dsh-xiaoyi": "git+file:///C:/Users/<you>/.dsh/plugin-sources/dsh-xiaoyi"
  }
}
```

> A plain local path dependency also works: `"dsh-xiaoyi": "file:../../plugin-sources/dsh-xiaoyi"`.

### 3. Install and cold-start

```bash
dsh plugin --profile web install
```

Then **cold-start the DSH host**. (Host-side plugins are not applied by hot reload — a cold start is required.)

### 4. Provide credentials

Edit `~/.dsh/xiaoyi/state.json`:

```json
{
  "enabled": true,
  "ak": "XiaoYi Open Platform AK",
  "sk": "XiaoYi Open Platform SK",
  "agentId": "agentId of your OpenClaw-mode agent",
  "wsUrl": "wss://hag.cloud.huawei.com/openclaw/v1/ws/link",
  "sessionId": "",
  "cwd": "C:\\Users\\<you>\\Desktop",
  "publicBaseUrl": "",
  "imageHost": ""
}
```

| Field | Purpose |
| --- | --- |
| `enabled` | Master switch. `false` disconnects the channel. |
| `ak` / `sk` / `agentId` | XiaoYi Open Platform credentials. All three are required. |
| `wsUrl` | WebSocket endpoint. Leave as the default unless Huawei changes it. |
| `sessionId` | The DSH session to reuse. Normally written by the plugin itself. |
| `cwd` | Working directory for the session. Must be writable. |
| `publicBaseUrl` | Only for outbound files in `local` mode: the public https base URL of your tunnel. |
| `imageHost` | Optional default publisher for outbound files. Set `"catbox"` to make catbox.moe the default; leave empty to stay local-only. |

You can also let the agent configure these at runtime with the `xiaoyi_set` tool (changes take effect immediately and reconnect).

Log file: `~/.dsh/xiaoyi/xiaoyi.log`

---

## Agent tools

| Tool | Purpose |
| --- | --- |
| `xiaoyi_status` | Channel status: whether credentials are complete, whether the WebSocket is connected, the listening session id, queue length, last error, message counters. |
| `xiaoyi_set` | Update credentials / switch (`ak`, `sk`, `agentId`, `wsUrl`, `enabled`). Takes effect immediately on the live connection. |
| `xiaoyi_send` | Push a short message to the current XiaoYi conversation (useful as a long-task completion notice). |
| `xiaoyi_send_photo` | Send a local image or file to XiaoYi (the protocol only accepts public https; see "Outbound files" below). |
| `xiaoyi_reconnect` | Force a disconnect and reconnect (after changing credentials, or when the link looks stale). |

## HTTP routes

| Path | Purpose |
| --- | --- |
| `GET /xiaoyi/state` | JSON status. |
| `GET /xiaoyi/connect` | Start the channel and make sure the listening session is ready. |
| `GET /xiaoyi/disconnect` | Stop the channel. |
| `GET /xiaoyi/public/<filename>` | Serves published outbound files. Filenames carry a random component and are not guessable. |

---

## Protocol summary

> Verified against the published artifacts of `@ynhcj/xiaoyi`. This project does not depend on that package.

| Stage | Detail |
| --- | --- |
| Endpoint | `wss://hag.cloud.huawei.com/openclaw/v1/ws/link` |
| Auth headers | `x-access-key`=AK, `x-sign`=`Base64(HMAC-SHA256(SK, ts))`, `x-ts`=milliseconds, `x-agent-id`=agentId |
| Handshake | `{"msgType":"clawd_bot_init","agentId":"..."}` |
| Heartbeat | Every 30s `{"msgType":"heartbeat","agentId":"...","timestamp":...}` plus a protocol-level ping |
| Inbound | JSON-RPC 2.0, `method="message/stream"`; `sessionId=params.sessionId`, `taskId=params.id`, `messageId`=top-level `id`; text in `params.message.parts[kind=text].text`, attachments in `parts[kind=file].file{name,mimeType,uri}` |
| Outbound | `{"msgType":"agent_response", agentId, sessionId, taskId, msgDetail:"<JSON-RPC string>"}` |
| Outbound frame kinds | `result.kind="status-update"` (acknowledge with `state=working`), `result.kind="artifact-update"` (the body, closed with `lastChunk=true, final=true`) |
| Control | `clearContext` / `tasks/cancel` / `action="clear"` → reply with the matching JSON-RPC response |

### Outbound files (`xiaoyi_send_photo`)

The Part sent back to the client is `{ url, filename, mediaType }` — **only public https URLs are accepted; there is no "upload bytes" interface**. Hence two modes:

| mode | Behaviour | Privacy |
| --- | --- | --- |
| `local` (**default**) | The file is copied to `~/.dsh/xiaoyi/public/` and served by this plugin's own route; pair it with a tunnel (e.g. `localhost.run`) and point `state.publicBaseUrl` at it. | The file never leaves your machine (except through your tunnel). |
| `catbox` | Uploads to the third-party host `catbox.moe`. | **The file is sent to a third party.** Must be requested explicitly. |

`local` mode requires `state.publicBaseUrl`; without it the tool returns an error and **never** silently falls back to an image host.

---

## Security and privacy

- **Credentials stay local.** They live in `~/.dsh/xiaoyi/state.json`, which is excluded by `.gitignore`. They are never committed and never logged (the log only records whether a value is present).
- **Inbound attachments are protected against SSRF.** The inbound `uri` comes from an external message and is treated as untrusted input. Only `http`/`https` is allowed. Every hop of a redirect chain is checked **before it is fetched** (`redirect: 'manual'`), and each host is judged two ways: structurally, and by **what it actually resolves to**. IPv6 addresses are parsed into bytes rather than compared as strings, so equivalent spellings cannot slip through. Blocked ranges: loopback, private, link-local, unique-local, multicast, site-local, IPv4-mapped (`::ffff:a.b.c.d`), NAT64, 6to4 and Teredo — including cloud metadata endpoints such as `169.254.169.254`.
- **Attachments are size-capped.** 50 MB per file, read as a stream, so a lying `content-length` cannot exhaust memory.
- **Published URLs are unguessable.** Filenames include 8 random bytes.
- ⚠️ **The HTTP routes themselves have no authentication.** `/xiaoyi/state` and `/xiaoyi/public/*` are reachable by anyone who can reach the port. Do **not** expose the DSH port directly to the internet; if you use a tunnel, prefer one with authentication or an unguessable hostname.

---

## Known limitations

- **A file or image cannot be pushed back to the phone reliably.** The protocol only accepts a public https URL for outbound files, and whether the XiaoYi client renders the resulting file part is outside this project's control. Text replies are unaffected.
- **Outbound attachments must use a public URL.** This is a protocol constraint, not a shortcut in the implementation.
- **Inbound attachment URLs are pre-signed and expire in roughly 30 minutes**, so they must be fetched immediately. This plugin does exactly that; if a download fails, the link has most likely expired.
- **The `media/` directory is never cleaned automatically.** Prune `~/.dsh/xiaoyi/media/` yourself periodically.
- **Platform-side policies may change.** OpenClaw mode, endpoints and field names can be adjusted by Huawei at any time; long-term availability is not guaranteed.

---

## Uninstall

1. Remove `dsh-xiaoyi` from `bundles` and `dependencies` in `~/.dsh/profiles/web/package.json`.
2. `dsh plugin --profile web install`
3. Cold-start the host.
4. For a complete cleanup, delete `~/.dsh/xiaoyi/` (it contains credentials and logs).

---

## Troubleshooting

| Symptom | What to check |
| --- | --- |
| Nothing happens after installing | Did you **cold-start**? Host-side plugins are not applied by hot reload. |
| `status` reports `ready:false` | One of AK / SK / agentId is missing. |
| Cannot connect, `lastError` mentions a handshake failure | Is `wsUrl` correct? Can the machine reach `hag.cloud.huawei.com:443`? |
| Connects then keeps reconnecting | Look for "stale connection" in the log; if nothing (including pong) arrives for over 90 seconds the plugin reconnects on purpose. |
| XiaoYi messages get no reply | Check `~/.dsh/xiaoyi/xiaoyi.log` and confirm in the host log that the plugin loaded. |
| Reply says the DSH session is not ready | The very first request after a host start has to create the session; send the message again shortly after. |
| Sent images do not arrive | This direction is unreliable by design — see "What can travel in each direction". The protocol also requires a public https URL, so check `publicBaseUrl` and whether your tunnel is still alive. |

---

## License

[MIT](./LICENSE)

## Disclaimer

**Please read [`DISCLAIMER.md`](./DISCLAIMER.md) before use.**

Install

dsh plugin --profile web add github:HangZhouXi/dsh-xiaoyi

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source