@mhfire/dsh-im-bridge 0.3.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,174 +1,220 @@
1
- [中文](./README.md) | English
2
-
3
- # @mhfire/dsh-im-bridge
4
-
5
- WeCom AI Bot ⇄ DeepSeek Harness Agent bridge — a **DSH plugin**.
6
-
7
- Creates Agents **in-process** inside a dsh profile (no child-process spawn): one durable DSH session **per WeCom chat window** (1:1 = that user; everyone in the same group shares one session — a person’s DM and group chats stay separate), sessions registered with the Web GUI (live view and continue-chat), plus a Settings → Plugins card (`botId` / `secret`, allow-list, timeouts, copy, and model overrides, written to `settings.yaml`).
8
-
9
- The Host half registers the `im-bridge` namespace through `installSettingsSection`. The browser half contributes a card into `settings.plugin.item` under `key: im-bridge`. The card can set `botId` / `secret` on the same user layer as the profile patch. Live fields such as `startHint` apply on the next message; changing credentials still requires a process restart to open the WebSocket.
10
-
11
- ## Session granularity
12
-
13
- One DSH session maps to one WeCom chat window, not “every window of the same userid”:
14
-
15
- - **1:1**: `single:<userid>` — one Agent for that user
16
- - **Group**: `group:<chatid>` — all members share one Agent and one serial queue (two people speaking at once still cannot concurrent-`followup`)
17
- - `allowFrom` still filters by **sender** userid; rejected senders are not enqueued
18
- - Leading `@nickname` mentions are stripped on arrival, so both the model input and the title read `测试一下` instead of `@MediaAgent 测试一下`. Mentions later in the text stay, and a message that is nothing but mentions goes to the model unchanged
19
- - Process restart continues the same durable session via a stable `wecom-` + short hash of the key: adopt a live Agent if the process already has one (for example the browser already opened that row), `resume` from persistence otherwise, and `create` only when neither exists. Resume cwd / preset follow the archive, not the current config; a cwd mismatch logs a warning and is not rewritten.
20
- - The GUI title is `企微·私聊` / `企微·群` plus the first user prompt (the same automatic title as other sessions), not a userid / chatid; two windows of the same kind with similar first lines stay two sidebar rows
21
- - Archiving a WeCom session in the GUI ends that context: the next message silently starts a new session at `wecom-<hash>-2` (`-3` after another archive), with no extra WeCom notice. DSH has no unarchive API, so the archived session is merely no longer written to — it neither becomes visible again nor is deleted
22
- - Every window still shares the same `workspace` (files / rag). That is environment isolation, separate from chat-context windows
23
-
24
- ## Compatible DeepSeek Harness versions
25
-
26
- DeepSeek Harness is still a developer preview and makes **no semver compatibility promise** to out-of-tree plugins. Package 0.3.0 is aligned to published tags by the APIs it actually calls:
27
-
28
- | DSH | This package |
29
- |---|---|
30
- | [0.1.0-rc.8](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.0-rc.8), [0.1.1-rc.1](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.1-rc.1), [0.1.1-rc.2](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.1-rc.2) | Compatible (developed against the 0.1.1-rc.2 line) |
31
- | [0.1.0-rc.7](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.0-rc.7) and earlier | Not compatible. rc.7 already has the plugin settings-card slot and `dsh plugin add`, but the browser has no `settingsScope.describe()`, so the card and the credential “Configured” badges fail |
32
- | Newer RCs / untagged HEAD | Not guaranteed. After upgrading dsh, re-check the Settings card and the WeCom connection |
33
-
34
- Pin `dsh` to `0.1.0-rc.8` or later, for example `npx @deepseek-ai/dsh@0.1.1-rc.2 web`. Do not rely on a floating `latest`.
35
-
36
- ## Install
37
-
38
- ### Recommended: npm package
39
-
40
- ```powershell
41
- dsh plugin --profile web add @mhfire/dsh-im-bridge
42
- # Or pin a version:
43
- # dsh plugin --profile web add @mhfire/dsh-im-bridge@0.3.0
44
- ```
45
-
46
- In `$DSH_HOME/profiles/web/cordis.patch.yml` (or your profile), supply credentials only (other fields ship as bundle defaults and can be overridden):
47
-
48
- ```yaml
49
- - id: im-bridge
50
- config:
51
- botId: "<your BotID>"
52
- secret: "<your Secret>"
53
- # optional: workspace / personaFile / … — see Configuration below
54
- ```
55
-
56
- Restart dsh to apply (e.g. `dsh web` / `pnpm dsh web`).
57
-
58
- ### Optional: local development
59
-
60
- Install from this repo’s `plugin/` directory or a `file:` path:
61
-
62
- ```powershell
63
- dsh plugin --profile web add <package-path>
64
- ```
65
-
66
- If `botId` / `secret` are missing, the plugin still loads (does not block `dsh web`); logs warn that WeCom connect is skipped. You can also fill them on the Settings plugin card (see the next section).
67
-
68
- ## Settings plugin card
69
-
70
- After the plugin is installed and `dsh web` is running, open **Settings → Plugins → Plugin configuration** and expand **WeCom Bridge** (same card chrome as Shell / Agent loop / Web search). **Save** writes the user layer of `settings.yaml`, the same layer as the profile `cordis.patch.yml`. **Discard** drops unsaved drafts. Fields marked **Overridden** can be **Reset** to the bundle default.
71
-
72
- Fields, top to bottom:
73
-
74
- | Card control | Config key | After save |
75
- |---|---|---|
76
- | Bot ID / Secret | `botId` / `secret` | Badge becomes “Configured”; a **process restart** is required to open the WebSocket. Inputs are password fields; stored literals never ride the wire. A **blank save does not clear** a stored credential |
77
- | Allowed sender userids | `allowFrom` | Applies on the next message; comma-separated, empty = allow everyone |
78
- | Task timeout (seconds) | `agentTimeoutSec` | Applies on the next message |
79
- | Placeholder while thinking | `startHint` | Applies on the next message |
80
- | Denied-sender reply | `deniedMessage` | Applies on the next message |
81
- | Welcome message | `welcomeMessage` | Applies on the next message |
82
- | WeCom-only provider / model | `provider` / `model` | Applies only to **later new** WeCom-window sessions; both must be set to override, otherwise the GUI default model is used |
83
-
84
- `workspace`, `agentPreset`, `persona` / `personaFile`, `thinking`, `maxReplyBytes`, and `reasoningEffort` are not on the card; set them in the profile patch or the table below. This release does not hot-reconnect credentials.
85
-
86
- ## Configuration
87
-
88
- The bundle `cordis.patch.yml` supplies defaults for every field except `botId` / `secret`. Full field list:
89
-
90
- | Field | Description |
91
- |---|---|
92
- | `botId` / `secret` | WeCom AI Bot credentials (`role('secret')`, redacted in UI); when empty, WeCom side is skipped and the host keeps running |
93
- | `workspace` | Agent working directory (session cwd) |
94
- | `allowFrom` | Allowed sender userids; empty = allow everyone |
95
- | `agentTimeoutSec` | Max seconds per task; also drives progress / ETA |
96
- | `startHint` | Placeholder text when processing starts |
97
- | `agentPreset` | Agent preset to mount (default `standard`) |
98
- | `provider` / `model` | WeCom-only model; **both must be non-empty** to override, otherwise follow the GUI `agent-default-model`. Filling only one warns and falls back. Editable in Settings; applies to later new WeCom-window sessions only |
99
- | `reasoningEffort` | Optional effort when the WeCom override is in effect; ignored otherwise |
100
- | `persona` / `personaFile` | Bot persona; precedence: `personaFile` → `persona` → packaged default (zh/en via Host `locale.preference`); overrides do not follow language; do not commit secrets |
101
- | `maxReplyBytes` | Reply size cap in bytes (default 20000) |
102
- | `deniedMessage` | Reply when the sender is not on `allowFrom` (editable in Settings) |
103
- | `welcomeMessage` | Welcome text on `enter_chat` (editable in Settings) |
104
- | `thinking` | Streaming animation. Precedence: tool activity (`toolLabels`) > model stream phase (`reasoningStatus` / `outputStatus` from `assistant/chunk`) > timed `phases` fallback; also `spin` / `reasoningSpin` / `outputSpin` / `eggs` |
105
-
106
- To give WeCom a different model from the GUI, set both in the profile `cordis.patch.yml`:
107
-
108
- ```yaml
109
- - id: im-bridge
110
- config:
111
- provider: deepseek-official
112
- model: deepseek-reasoner
113
- ```
114
-
115
- Behavior:
116
-
117
- 1. `reasoning-delta` → rotating “model thinking” copy + `reasoningSpin`
118
- 2. `text-delta` → rotating “writing reply” copy + `outputSpin`
119
- 3. `tool/call` → `activityPrefix` + friendly label; brief done/fail after `tool/result`
120
- 4. No chunks yet → timed `phases` (unrelated to whether the model is reasoning)
121
-
122
- ```yaml
123
- thinking:
124
- intervalMs: 1500
125
- reasoningStatus:
126
- - '💭 Model thinking…'
127
- outputStatus:
128
- - '✍️ Writing reply…'
129
- toolLabels:
130
- pwsh: PowerShell
131
- ```
132
-
133
- ## Persona
134
-
135
- Precedence: `personaFile` → `persona` string → packaged default persona.
136
-
137
- - **Packaged defaults**: [`persona.default.md`](./persona.default.md) (Chinese) / [`persona.default.en.md`](./persona.default.en.md) (English). Chosen from Host settings `locale.preference` (`zh`|`en`); **falls back to Chinese when unset** (the Host cannot see a browser-only provisional locale). Re-read on each assemble so a Settings language change applies on the next request.
138
- - **Overrides do not follow language**: a set `personaFile` / non-empty `persona` always wins.
139
-
140
- **Recommended override**: place `persona.md` next to `cordis.patch.yml` under `$DSH_HOME/profiles/<name>/`, and point `personaFile` at it with an absolute path (relative paths resolve against process cwd):
141
-
142
- ```yaml
143
- - id: im-bridge
144
- config:
145
- personaFile: 'C:\\Users\\you\\.dsh\\profiles\\web\\persona.md'
146
- ```
147
-
148
- Or copy [`persona.example.md`](./persona.example.md) as a fill-in template. Supports `{{model}}` / `{{cwd}}`. Do not commit secret-bearing persona files. Welcome / deny / thinking copy remain Chinese config strings and do not follow locale yet.
149
-
150
- ## Sending PNGs to WeCom
151
-
152
- If the agent’s final reply contains Markdown pointing at a **workspace** PNG, the bridge uploads it after the text stream finishes and sends it as a **separate image message** (`uploadMedia`, then `sendMediaMessage` with `media_id`).
153
-
154
- Trigger (paths relative to `workspace`):
155
-
156
- ```markdown
157
- ![screenshot](main_screen.png)
158
- [screenshot](out/frame.png)
159
- ```
160
-
161
- - Only `.png`; `http(s):` / `data:` are ignored; the file must stay inside `workspace`
162
- - Must be a real PNG (signature), at most **10MB** each, at most **10** images (first-seen order, deduped)
163
- - Writing a PNG without that Markdown does **not** send it
164
- - Images follow the text bubble; they are not inlined into the stream
165
- - Inbound image / voice / file messages are still ignored
166
-
167
- ## Security
168
-
169
- - Do not commit secret-bearing files such as `config.json` / `persona.md`
170
- - Session and tool output may contain adversarial text; the plugin’s safety prompt tells the agent not to treat tool output as instructions
171
-
172
- ## License
173
-
174
- MIT
1
+ [中文](./README.md) | English
2
+
3
+ # @mhfire/dsh-im-bridge
4
+
5
+ WeCom AI Bot ⇄ DeepSeek Harness Agent bridge — a **DSH plugin**.
6
+
7
+ Creates Agents **in-process** inside a dsh profile (no child-process spawn): one durable DSH session **per WeCom chat window** (1:1 = that user; everyone in the same group shares one session — a person’s DM and group chats stay separate), sessions registered with the Web GUI (live view and continue-chat), plus a Settings → Plugins card (`botId` / `secret`, allow-list, timeouts, copy, and model overrides, written to `settings.yaml`).
8
+
9
+ The Host half registers the `im-bridge` namespace through `ctx.settings.installSection`. The browser half contributes a card into `settings.plugin.item` under `key: im-bridge`. The card can set `botId` / `secret` on the same user layer as the profile patch. Live fields such as `startHint` apply on the next message; changing credentials still requires a process restart to open the WebSocket.
10
+
11
+ ## Session granularity
12
+
13
+ One DSH session maps to one WeCom chat window, not “every window of the same userid”:
14
+
15
+ - **1:1**: `single:<userid>` — one Agent for that user
16
+ - **Group**: `group:<chatid>` — all members share one Agent and one serial queue (two people speaking at once still cannot concurrent-`followup`)
17
+ - `allowFrom` filters **who may chat** by sender userid; empty = enqueue everyone. Office commands use `wecomCli.allowFrom` separately
18
+ - Leading `@nickname` mentions are stripped on arrival, so both the model input and the title read `测试一下` instead of `@MediaAgent 测试一下`. Mentions later in the text stay, and a message that is nothing but mentions goes to the model unchanged
19
+ - Process restart continues the same durable session via a stable `wecom-` + short hash of the key: adopt a live Agent if the process already has one (for example the browser already opened that row), `resume` from persistence otherwise, and `create` only when neither exists. Resume cwd / preset follow the archive, not the current config; a cwd mismatch logs a warning and is not rewritten.
20
+ - The GUI title is `企微·私聊` / `企微·群` plus the first user prompt (the same automatic title as other sessions), not a userid / chatid; two windows of the same kind with similar first lines stay two sidebar rows
21
+ - Archiving a WeCom session in the GUI ends that context: the next message silently starts a new session at `wecom-<hash>-2` (`-3` after another archive), with no extra WeCom notice. DSH has no unarchive API, so the archived session is merely no longer written to — it neither becomes visible again nor is deleted
22
+ - Every window still shares the same `workspace` (files / rag). That is environment isolation, separate from chat-context windows
23
+
24
+ ## Compatible DeepSeek Harness versions
25
+
26
+ DeepSeek Harness is still a developer preview and makes **no semver compatibility promise** to out-of-tree plugins. Package 0.4.3 is aligned to published tags by the APIs it actually calls:
27
+
28
+ | DSH | This package |
29
+ |---|---|
30
+ | [0.1.2-rc.1](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.2-rc.1) | Compatible (the line this package is developed and verified against) |
31
+ | 0.1.2-alpha.4 / 0.1.2-alpha.5 | All three required APIs are in place; not verified individually |
32
+ | [0.1.1-rc.2](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.1-rc.2) and earlier | Not compatible; pin `@mhfire/dsh-im-bridge@0.4.2` instead. These versions still ship `@deepseek-ai/dsh-client-runtime` (removed in 0.1.2-alpha.1, so the browser half reports `missed the module table`), have no `ctx.settings.installSection` (0.1.2-alpha.2), and still expose `Session.events` rather than `snapshotEvents()` (0.1.2-alpha.4) |
33
+ | 0.1.3-alpha.x / untagged HEAD | Not guaranteed. After upgrading dsh, re-check the Settings card and the WeCom connection |
34
+
35
+ Pin `dsh` to `0.1.2-rc.1`, for example `npx @deepseek-ai/dsh@0.1.2-rc.1 web`. Do not rely on a floating `latest`.
36
+
37
+ ## Install
38
+
39
+ ### Recommended: npm package
40
+
41
+ ```powershell
42
+ dsh plugin --profile web add @mhfire/dsh-im-bridge
43
+ # Or pin a version:
44
+ # dsh plugin --profile web add @mhfire/dsh-im-bridge@0.4.3
45
+ ```
46
+
47
+ In `$DSH_HOME/profiles/web/cordis.patch.yml` (or your profile), supply credentials only (other fields ship as bundle defaults and can be overridden):
48
+
49
+ ```yaml
50
+ - id: im-bridge
51
+ config:
52
+ botId: "<your BotID>"
53
+ secret: "<your Secret>"
54
+ # optional: workspace / personaFile / … — see Configuration below
55
+ ```
56
+
57
+ Restart dsh to apply (e.g. `dsh web` / `pnpm dsh web`).
58
+
59
+ ### Optional: local development
60
+
61
+ Install from this repo’s `plugin/` directory or a `file:` path:
62
+
63
+ ```powershell
64
+ dsh plugin --profile web add <package-path>
65
+ ```
66
+
67
+ If `botId` / `secret` are missing, the plugin still loads (does not block `dsh web`); logs warn that WeCom connect is skipped. You can also fill them on the Settings plugin card (see the next section).
68
+
69
+ `npm test` runs `npm run typecheck` first. That check resolves `@deepseek-ai/*` through the `paths` in [tsconfig.typecheck.json](tsconfig.typecheck.json), pointing at the **built** `lib/types` of a sibling `../deepseek-harness` checkout, so a harness change to a service or event contract fails here instead of at boot. Local development therefore needs that checkout next to this repository, built with `pnpm run build`. Neither `npm run build` nor `npm pack` depends on it.
70
+
71
+ ## Settings plugin card
72
+
73
+ After the plugin is installed and `dsh web` is running, open **Settings → Plugins → Plugin configuration** and expand **WeCom Bridge** (same card chrome as Shell / Agent loop / Web search). **Save** writes the user layer of `settings.yaml`, the same layer as the profile `cordis.patch.yml`. **Discard** drops unsaved drafts. Fields marked **Overridden** can be **Reset** to the bundle default.
74
+
75
+ Fields, top to bottom:
76
+
77
+ | Card control | Config key | After save |
78
+ |---|---|---|
79
+ | Bot ID / Secret | `botId` / `secret` | Badge becomes “Configured”; a **process restart** is required to open the WebSocket. Inputs are password fields; stored literals never ride the wire. A **blank save does not clear** a stored credential |
80
+ | Allowed sender userids | `allowFrom` | Applies on the next message; comma-separated, empty = allow everyone |
81
+ | Task timeout (seconds) | `agentTimeoutSec` | Applies on the next message |
82
+ | Placeholder while thinking | `startHint` | Applies on the next message |
83
+ | Denied-sender reply | `deniedMessage` | Applies on the next message |
84
+ | Welcome message | `welcomeMessage` | Applies on the next message |
85
+ | WeCom-only provider / model | `provider` / `model` | Applies only to **later new** WeCom-window sessions; both must be set to override, otherwise the GUI default model is used |
86
+
87
+ `workspace`, `agentPreset`, `persona` / `personaFile`, `thinking`, `maxReplyBytes`, `reasoningEffort`, and `wecomCli` are not on the card; set them in the profile patch or the table below. This release does not hot-reconnect credentials; changing `wecomCli` also requires a process restart.
88
+
89
+ ## Configuration
90
+
91
+ The bundle `cordis.patch.yml` supplies defaults for every field except `botId` / `secret`. Full field list:
92
+
93
+ | Field | Description |
94
+ |---|---|
95
+ | `botId` / `secret` | WeCom AI Bot credentials (`role('secret')`, redacted in UI); when empty, WeCom side is skipped and the host keeps running |
96
+ | `workspace` | Agent working directory (session cwd) |
97
+ | `allowFrom` | Chat allow-list; empty = everyone may ask. Does not gate wecom-cli |
98
+ | `agentTimeoutSec` | Max seconds per task; also drives progress / ETA |
99
+ | `startHint` | Placeholder text when processing starts |
100
+ | `agentPreset` | Agent preset to mount (default `standard`) |
101
+ | `provider` / `model` | WeCom-only model; **both must be non-empty** to override, otherwise follow the GUI `agent-default-model`. Filling only one warns and falls back. Editable in Settings; applies to later new WeCom-window sessions only |
102
+ | `reasoningEffort` | Optional effort when the WeCom override is in effect; ignored otherwise |
103
+ | `persona` / `personaFile` | Bot persona; precedence: `personaFile` → `persona` → packaged default (zh/en via Host `locale.preference`); overrides do not follow language; do not commit secrets |
104
+ | `maxReplyBytes` | Reply size cap in bytes (default 20000) |
105
+ | `deniedMessage` | Reply when the sender is not on `allowFrom` (editable in Settings) |
106
+ | `welcomeMessage` | Welcome text on `enter_chat` (editable in Settings) |
107
+ | `wecomCli` | Optional WeCom office skills (off by default). See the next section |
108
+ | `thinking` | Streaming animation. Precedence: tool activity (`toolLabels`) > model stream phase (`reasoningStatus` / `outputStatus` from `assistant/chunk`) > timed `phases` fallback; also `spin` / `reasoningSpin` / `outputSpin` / `eggs` |
109
+
110
+ To give WeCom a different model from the GUI, set both in the profile `cordis.patch.yml`:
111
+
112
+ ```yaml
113
+ - id: im-bridge
114
+ config:
115
+ provider: deepseek-official
116
+ model: deepseek-reasoner
117
+ ```
118
+
119
+ Behavior:
120
+
121
+ 1. `reasoning-delta` → rotating “model thinking” copy + `reasoningSpin`
122
+ 2. `text-delta` → rotating “writing reply” copy + `outputSpin`
123
+ 3. `tool/call` → `activityPrefix` + friendly label; brief done/fail after `tool/result`
124
+ 4. No chunks yet → timed `phases` (unrelated to whether the model is reasoning)
125
+
126
+ ```yaml
127
+ thinking:
128
+ intervalMs: 1500
129
+ reasoningStatus:
130
+ - '💭 Model thinking…'
131
+ outputStatus:
132
+ - '✍️ Writing reply…'
133
+ toolLabels:
134
+ pwsh: PowerShell
135
+ ```
136
+
137
+ ## WeCom office skills (wecom-cli)
138
+
139
+ The plugin depends on the official [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) binary. Install `wecomcli-*` under **`$DSH_HOME/wecom-cli-skills`** (not workspace `.dsh/skills` / `.agents/skills`, and not `$DSH_HOME/skills`). Only the **office userid’s 1:1** agent gets the office layer: `skills.register()` for the catalog and `tools.register()` for the gated `wecom_cli` tool. Both go through that agent’s own ctx, so group chats and the GUI never see them. Other workspace skills stay on `skill-filesystem`.
140
+
141
+ Office commands run **only through the `wecom_cli` tool**: the model passes `argv` (the arguments after `wecom-cli`), the plugin spawns the official binary directly, and any `auth init` is refused. The `wecom-cli` on PATH is a shim that prints a refusal and exits 1, so group chats, the GUI, and any `pwsh wecom-cli` fail; the shim’s message points back at `wecom_cli`. The credential directory is never exported to the process environment — it is injected per spawn.
142
+
143
+ `wecomCli.enabled` is off by default. Turning it on requires a non-empty **`wecomCli.allowFrom`** (office userids). Root `allowFrom` only gates who may chat; empty means everyone may ask. An empty office list logs a warning and skips the shim / auth.
144
+
145
+ One-time setup:
146
+
147
+ 1. Leave root `allowFrom` empty (everyone may ask) and put **office** userids in `wecomCli.allowFrom` (do not leave that empty)
148
+ 2. In **Settings → Plugins → WeCom Bridge**, click **Install official skills** (the Host downloads the official repo zip into `$DSH_HOME/wecom-cli-skills`). **Do not** use `npx skills add -g` (it leaks into the GUI). The skills CLI has **no `--dir`**, so that flag does not write into the plugin directory. If `wecomcli-*` folders already sit in the workspace, move them here and delete the workspace copies. You can also copy official `skills/wecomcli-*` into `$DSH_HOME/wecom-cli-skills` by hand.
149
+
150
+ 3. Enable it in the profile `cordis.patch.yml` (the plugin then writes wecom-cli credentials from the existing `botId` / `secret` via hidden `auth init --bot-id/--secret`; no QR scan and no `npm install -g @wecom/cli`):
151
+
152
+ ```yaml
153
+ - id: im-bridge
154
+ config:
155
+ allowFrom: []
156
+ wecomCli:
157
+ enabled: true
158
+ allowFrom: ["<office userid>"]
159
+ # skillsDir: '' # empty = $DSH_HOME/wecom-cli-skills
160
+ # configDir: '' # empty = <workspace>/.dsh/wecom-cli; gitignore this directory
161
+ ```
162
+
163
+ | Field | Description |
164
+ |---|---|
165
+ | `wecomCli.enabled` | Install the PATH deny shim, run the auth check, and wire the prompt plus the `wecom_cli` tool; default `false` |
166
+ | `wecomCli.allowFrom` | Userids that receive the `wecom_cli` tool; empty skips the shim / auth. Independent of the chat list |
167
+ | `wecomCli.skillsDir` | Override skills root; empty = `$DSH_HOME/wecom-cli-skills` |
168
+ | `wecomCli.configDir` | Override the credential directory; empty = `<workspace>/.dsh/wecom-cli`. Gitignore that directory. `WECOM_CLI_CONFIG_DIR` is injected only when the plugin spawns the CLI, never into the process environment, so `~/.config/wecom` stays unused. |
169
+
170
+ Changing `wecomCli` requires a process restart. Messages still arrive when unauthorized; at boot the plugin seeds credentials with hidden `--bot-id/--secret` (stderr is not a TTY). If automatic seeding fails, the log prints a manual command that sets `WECOM_CLI_CONFIG_DIR` — keep that prefix, or `npx --yes @wecom/cli auth init --manual` writes to `~/.config/wecom`, which the plugin never reads. Do not run `auth init` from the agent (that creates a new bot).
171
+
172
+ ## Persona
173
+
174
+ Precedence: `personaFile` → `persona` string → packaged default persona.
175
+
176
+ - **Packaged defaults**: [`persona.default.md`](./persona.default.md) (Chinese) / [`persona.default.en.md`](./persona.default.en.md) (English). Chosen from Host settings `locale.preference` (`zh`|`en`); **falls back to Chinese when unset** (the Host cannot see a browser-only provisional locale). Re-read on each assemble so a Settings language change applies on the next request.
177
+ - **Overrides do not follow language**: a set `personaFile` / non-empty `persona` always wins.
178
+
179
+ **Recommended override**: place `persona.md` next to `cordis.patch.yml` under `$DSH_HOME/profiles/<name>/`, and point `personaFile` at it with an absolute path (relative paths resolve against process cwd):
180
+
181
+ ```yaml
182
+ - id: im-bridge
183
+ config:
184
+ personaFile: 'C:\\Users\\you\\.dsh\\profiles\\web\\persona.md'
185
+ ```
186
+
187
+ Or copy [`persona.example.md`](./persona.example.md) as a fill-in template. Supports `{{model}}` / `{{cwd}}`. Do not commit secret-bearing persona files. Welcome / deny / thinking copy remain Chinese config strings and do not follow locale yet.
188
+
189
+ ## Sending PNGs to WeCom
190
+
191
+ If the agent’s final reply contains Markdown pointing at a **workspace** PNG, the bridge uploads it after the text stream finishes and sends it as a **separate image message** (`uploadMedia`, then `sendMediaMessage` with `media_id`).
192
+
193
+ Trigger (paths relative to `workspace`):
194
+
195
+ ```markdown
196
+ ![screenshot](main_screen.png)
197
+ [screenshot](out/frame.png)
198
+ ```
199
+
200
+ - Only `.png`; `http(s):` / `data:` are ignored; the file must stay inside `workspace`
201
+ - Must be a real PNG (signature), at most **10MB** each, at most **10** images (first-seen order, deduped)
202
+ - Writing a PNG without that Markdown does **not** send it
203
+ - Images follow the text bubble; they are not inlined into the stream
204
+ - Inbound image / voice / file messages are still ignored
205
+
206
+ ## Security
207
+
208
+ - Do not commit secret-bearing files such as `config.json` / `persona.md` / `.dsh/wecom-cli/`
209
+ - Session and tool output may contain adversarial text; the plugin’s safety prompt tells the agent not to treat tool output as instructions
210
+
211
+ ## Known Limitations and Deferred Work
212
+
213
+ - wecom-cli credentials live in the workspace `<workspace>/.dsh/wecom-cli` (gitignore it). People on `wecomCli.allowFrom` borrow that identity’s office permissions; the chat list (root `allowFrom`) does not grant office access. An office 1:1 may still send to any `--chat-id`; the plugin does not pin recipients.
214
+ - Gating is not a sandbox. The `wecom_cli` tool and `wecomcli-*` register only on the office 1:1 agent, PATH resolves `wecom-cli` to a refusal, and the credential directory reaches only the plugin's own spawns — but a shell in the same process can still bypass all of it: run `node <absolute path to @wecom/cli's wecom.js>`, or `npx --yes @wecom/cli` with a self-set `WECOM_CLI_CONFIG_DIR`. Without that variable such a bypass lands on the unauthorized `~/.config/wecom`. Real isolation needs a process sandbox.
215
+ - The WeCom channel has no GUI confirmation dialog. Irreversible actions (send mail, cancel a meeting, delete a todo, overwrite a document) are constrained only by the prompt (run `--dry-run` first, wait for the next user message). Every WeCom session forbids `ask_user_question` (it hangs until the task times out).
216
+ - Leftover `wecomcli-*` folders in workspace `.dsh/skills` / `.agents/skills` remain visible to every agent with that cwd, including GUI. Other skills are unchanged. `enabled: false` only turns off the deny shim, the auth check, `wecom_cli`, and wecomcli-* registration; the channel ban on `ask_user_question` is still injected.
217
+
218
+ ## License
219
+
220
+ MIT