@mhfire/dsh-im-bridge 0.2.0 → 0.4.2
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 +109 -6
- package/README.md +109 -6
- package/cordis.patch.yml +6 -0
- package/lib/client.js +126 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +8579 -70
- package/package.json +26 -10
- package/persona.default.en.md +1 -0
- package/persona.default.md +2 -1
- package/persona.example.md +2 -1
- package/src/client/WecomCard.tsx +75 -0
- package/src/client/card-controller.ts +74 -1
- package/src/client/card-form.ts +5 -0
- package/src/client/fields.module.css +32 -0
- package/src/client/fields.tsx +3 -1
- package/src/client/index.ts +3 -1
- package/src/client/locales.ts +23 -2
- package/src/index.ts +578 -62
- package/src/reply-images.ts +228 -0
- package/src/session-key.ts +198 -0
- package/src/wecom-cli.ts +1015 -0
- package/tsconfig.json +1 -1
- package/tsdown.host.ts +1 -0
package/README.en.md
CHANGED
|
@@ -4,10 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
WeCom AI Bot ⇄ DeepSeek Harness Agent bridge — a **DSH plugin**.
|
|
6
6
|
|
|
7
|
-
Creates Agents **in-process** inside a dsh profile (no child-process spawn):
|
|
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
8
|
|
|
9
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
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.2 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
|
+
|
|
11
36
|
## Install
|
|
12
37
|
|
|
13
38
|
### Recommended: npm package
|
|
@@ -15,7 +40,7 @@ The Host half registers the `im-bridge` namespace through `installSettingsSectio
|
|
|
15
40
|
```powershell
|
|
16
41
|
dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
17
42
|
# Or pin a version:
|
|
18
|
-
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.2
|
|
43
|
+
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.4.2
|
|
19
44
|
```
|
|
20
45
|
|
|
21
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):
|
|
@@ -38,7 +63,25 @@ Install from this repo’s `plugin/` directory or a `file:` path:
|
|
|
38
63
|
dsh plugin --profile web add <package-path>
|
|
39
64
|
```
|
|
40
65
|
|
|
41
|
-
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
|
|
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`, `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.
|
|
42
85
|
|
|
43
86
|
## Configuration
|
|
44
87
|
|
|
@@ -48,16 +91,17 @@ The bundle `cordis.patch.yml` supplies defaults for every field except `botId` /
|
|
|
48
91
|
|---|---|
|
|
49
92
|
| `botId` / `secret` | WeCom AI Bot credentials (`role('secret')`, redacted in UI); when empty, WeCom side is skipped and the host keeps running |
|
|
50
93
|
| `workspace` | Agent working directory (session cwd) |
|
|
51
|
-
| `allowFrom` |
|
|
94
|
+
| `allowFrom` | Chat allow-list; empty = everyone may ask. Does not gate wecom-cli |
|
|
52
95
|
| `agentTimeoutSec` | Max seconds per task; also drives progress / ETA |
|
|
53
96
|
| `startHint` | Placeholder text when processing starts |
|
|
54
97
|
| `agentPreset` | Agent preset to mount (default `standard`) |
|
|
55
|
-
| `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
|
|
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 |
|
|
56
99
|
| `reasoningEffort` | Optional effort when the WeCom override is in effect; ignored otherwise |
|
|
57
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 |
|
|
58
101
|
| `maxReplyBytes` | Reply size cap in bytes (default 20000) |
|
|
59
102
|
| `deniedMessage` | Reply when the sender is not on `allowFrom` (editable in Settings) |
|
|
60
103
|
| `welcomeMessage` | Welcome text on `enter_chat` (editable in Settings) |
|
|
104
|
+
| `wecomCli` | Optional WeCom office skills (off by default). See the next section |
|
|
61
105
|
| `thinking` | Streaming animation. Precedence: tool activity (`toolLabels`) > model stream phase (`reasoningStatus` / `outputStatus` from `assistant/chunk`) > timed `phases` fallback; also `spin` / `reasoningSpin` / `outputSpin` / `eggs` |
|
|
62
106
|
|
|
63
107
|
To give WeCom a different model from the GUI, set both in the profile `cordis.patch.yml`:
|
|
@@ -87,6 +131,41 @@ thinking:
|
|
|
87
131
|
pwsh: PowerShell
|
|
88
132
|
```
|
|
89
133
|
|
|
134
|
+
## WeCom office skills (wecom-cli)
|
|
135
|
+
|
|
136
|
+
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`.
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
`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.
|
|
141
|
+
|
|
142
|
+
One-time setup:
|
|
143
|
+
|
|
144
|
+
1. Leave root `allowFrom` empty (everyone may ask) and put **office** userids in `wecomCli.allowFrom` (do not leave that empty)
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
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`):
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- id: im-bridge
|
|
151
|
+
config:
|
|
152
|
+
allowFrom: []
|
|
153
|
+
wecomCli:
|
|
154
|
+
enabled: true
|
|
155
|
+
allowFrom: ["<office userid>"]
|
|
156
|
+
# skillsDir: '' # empty = $DSH_HOME/wecom-cli-skills
|
|
157
|
+
# configDir: '' # empty = <workspace>/.dsh/wecom-cli; gitignore this directory
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Field | Description |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `wecomCli.enabled` | Install the PATH deny shim, run the auth check, and wire the prompt plus the `wecom_cli` tool; default `false` |
|
|
163
|
+
| `wecomCli.allowFrom` | Userids that receive the `wecom_cli` tool; empty skips the shim / auth. Independent of the chat list |
|
|
164
|
+
| `wecomCli.skillsDir` | Override skills root; empty = `$DSH_HOME/wecom-cli-skills` |
|
|
165
|
+
| `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. |
|
|
166
|
+
|
|
167
|
+
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).
|
|
168
|
+
|
|
90
169
|
## Persona
|
|
91
170
|
|
|
92
171
|
Precedence: `personaFile` → `persona` string → packaged default persona.
|
|
@@ -104,11 +183,35 @@ Precedence: `personaFile` → `persona` string → packaged default persona.
|
|
|
104
183
|
|
|
105
184
|
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.
|
|
106
185
|
|
|
186
|
+
## Sending PNGs to WeCom
|
|
187
|
+
|
|
188
|
+
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`).
|
|
189
|
+
|
|
190
|
+
Trigger (paths relative to `workspace`):
|
|
191
|
+
|
|
192
|
+
```markdown
|
|
193
|
+

|
|
194
|
+
[screenshot](out/frame.png)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- Only `.png`; `http(s):` / `data:` are ignored; the file must stay inside `workspace`
|
|
198
|
+
- Must be a real PNG (signature), at most **10MB** each, at most **10** images (first-seen order, deduped)
|
|
199
|
+
- Writing a PNG without that Markdown does **not** send it
|
|
200
|
+
- Images follow the text bubble; they are not inlined into the stream
|
|
201
|
+
- Inbound image / voice / file messages are still ignored
|
|
202
|
+
|
|
107
203
|
## Security
|
|
108
204
|
|
|
109
|
-
- Do not commit secret-bearing files such as `config.json` / `persona.md`
|
|
205
|
+
- Do not commit secret-bearing files such as `config.json` / `persona.md` / `.dsh/wecom-cli/`
|
|
110
206
|
- Session and tool output may contain adversarial text; the plugin’s safety prompt tells the agent not to treat tool output as instructions
|
|
111
207
|
|
|
208
|
+
## Known Limitations and Deferred Work
|
|
209
|
+
|
|
210
|
+
- 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.
|
|
211
|
+
- 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.
|
|
212
|
+
- 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).
|
|
213
|
+
- 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.
|
|
214
|
+
|
|
112
215
|
## License
|
|
113
216
|
|
|
114
217
|
MIT
|
package/README.md
CHANGED
|
@@ -4,10 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接 **DSH 插件**。
|
|
6
6
|
|
|
7
|
-
在 dsh profile 内**进程内**创建 Agent(不再 spawn
|
|
7
|
+
在 dsh profile 内**进程内**创建 Agent(不再 spawn 子进程):按**企微窗口**拆分持久会话(单聊 = 该用户一条;同一群里所有人共用一条;同一个人的私聊和群聊互不串上下文),会话与 Web GUI 同进程注册(实时可见、可续聊),并在 Settings → 插件配置页提供配置卡片(`botId` / `secret`、白名单、超时、提示语、模型覆盖,写入 `settings.yaml`)。
|
|
8
8
|
|
|
9
9
|
Host 通过 `installSettingsSection` 注册 `im-bridge` 命名空间;浏览器半包以 `key: im-bridge` 挂进 `settings.plugin.item`。卡片可填 `botId` / `secret`,与 profile patch 写入同一用户层;改 `startHint` 等热字段后下一轮消息即生效,改凭证仍需重启进程才会连 WebSocket。
|
|
10
10
|
|
|
11
|
+
## 会话粒度
|
|
12
|
+
|
|
13
|
+
一条 DSH 会话对应一个企微聊天窗口,而不是「同一个 userid 的所有窗口」:
|
|
14
|
+
|
|
15
|
+
- **单聊**:`single:<userid>`,该用户一条 Agent
|
|
16
|
+
- **群聊**:`group:<chatid>`,群内所有人共用一条 Agent 和同一条串行队列(两人同时发也不会并发 `followup`)
|
|
17
|
+
- `allowFrom` 按**发送者** userid 拦截谁能聊天;空 = 所有人可进队。办公命令另用 `wecomCli.allowFrom`
|
|
18
|
+
- 群里 @机器人 的消息,开头的 `@昵称` 在入站就去掉:模型看到的和标题用的都是「测试一下」而不是「@MediaAgent 测试一下」;正文中间的 @某人 保留,整条只有 @ 时按原文交给模型
|
|
19
|
+
- 进程重启后用稳定 id `wecom-` + key 的短 hash 续上同一条会话:进程里已有活 Agent 就直接采用(例如浏览器已打开该行),存档里有就 `resume`,都没有才 `create`。resume 的 cwd / preset 跟存档,不跟当前配置;cwd 不一致时打警告,不改写。
|
|
20
|
+
- GUI 标题为「企微·私聊/群」+ 第一句用户话(与其它会话一样由 DSH 生成),不再露出 userid / chatid;同类型窗口若第一句话相近,侧栏仍是两行
|
|
21
|
+
- 在 GUI 里归档某条企微会话 = 结束那段上下文:下一条消息用 `wecom-<hash>-2`(再归档就 `-3`)静默开一条新会话,企微侧不额外提示。DSH 没有取消归档的接口,所以旧会话只是不再被本插件写入,既不会恢复可见也不会被删除
|
|
22
|
+
- 所有窗口仍共用同一个 `workspace`(文件 / rag 环境),与聊天上下文分窗是两件事
|
|
23
|
+
|
|
24
|
+
## 兼容的 DeepSeek Harness 版本
|
|
25
|
+
|
|
26
|
+
DeepSeek Harness 仍是 developer preview,对外置插件**没有 semver 兼容承诺**。本包 0.4.2 按实际调用的 API 对齐已发布 tag:
|
|
27
|
+
|
|
28
|
+
| DSH | 本包 |
|
|
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) | 可适配(对照开发的是 0.1.1-rc.2 一线) |
|
|
31
|
+
| [0.1.0-rc.7](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.0-rc.7) 及更早 | 不可适配。rc.7 已有插件配置卡槽位和 `dsh plugin add`,但浏览器没有 `settingsScope.describe()`,配置卡加载和凭证「已配置」徽章会失败 |
|
|
32
|
+
| 更新的 RC / 未打 tag 的 HEAD | 未保证。升级 dsh 后请再验 Settings 卡和企微连线 |
|
|
33
|
+
|
|
34
|
+
建议把 `dsh` 钉在 `0.1.0-rc.8` 及以上,例如 `npx @deepseek-ai/dsh@0.1.1-rc.2 web`,不要只跑浮动的 `latest`。
|
|
35
|
+
|
|
11
36
|
## 安装
|
|
12
37
|
|
|
13
38
|
### 推荐:从 npm 安装
|
|
@@ -15,7 +40,7 @@ Host 通过 `installSettingsSection` 注册 `im-bridge` 命名空间;浏览器
|
|
|
15
40
|
```powershell
|
|
16
41
|
dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
17
42
|
# 或钉版本:
|
|
18
|
-
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.2
|
|
43
|
+
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.4.2
|
|
19
44
|
```
|
|
20
45
|
|
|
21
46
|
在 `$DSH_HOME/profiles/web/cordis.patch.yml`(或对应 profile)中补密钥即可(其余项已有 bundle 默认,可按需覆盖):
|
|
@@ -38,7 +63,25 @@ dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
|
38
63
|
dsh plugin --profile web add <本包路径>
|
|
39
64
|
```
|
|
40
65
|
|
|
41
|
-
未配置 `botId` / `secret` 时插件仍会加载(不阻塞 `dsh web`),日志会提示跳过企微连线;也可在 Settings
|
|
66
|
+
未配置 `botId` / `secret` 时插件仍会加载(不阻塞 `dsh web`),日志会提示跳过企微连线;也可在 Settings 插件配置卡填写(见下节)。
|
|
67
|
+
|
|
68
|
+
## Settings 插件配置卡
|
|
69
|
+
|
|
70
|
+
安装插件并启动 `dsh web` 后,打开 **设置 → 插件 → 插件配置**,展开 **企业微信桥接**(与「终端 / Agent 循环 / 网页搜索」同一组卡片)。改完点右下角 **保存** 写入 `settings.yaml` 用户层,与 profile `cordis.patch.yml` 同一层;**放弃** 丢掉未保存草稿。带「已覆盖」的字段可 **重置** 回 bundle 默认。
|
|
71
|
+
|
|
72
|
+
卡片字段自上而下:
|
|
73
|
+
|
|
74
|
+
| 卡片项 | 对应配置 | 保存后 |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| Bot ID / Secret | `botId` / `secret` | 徽章变为「已配置」;**须重启进程** 才会连 WebSocket。输入框为密码框,线上看不到已存字面值;**留空再保存不会清空**已存凭证 |
|
|
77
|
+
| 允许的发送者 userid | `allowFrom` | 下一轮消息生效;逗号分隔,空 = 允许所有人 |
|
|
78
|
+
| 单任务超时(秒) | `agentTimeoutSec` | 下一轮消息生效 |
|
|
79
|
+
| 开始处理时的占位提示 | `startHint` | 下一轮消息生效 |
|
|
80
|
+
| 非白名单拒绝文案 | `deniedMessage` | 下一轮消息生效 |
|
|
81
|
+
| 进入会话欢迎语 | `welcomeMessage` | 下一轮消息生效 |
|
|
82
|
+
| 企微专用 provider / model | `provider` / `model` | 只影响之后**新建**的企微窗口会话;须两项都填才覆盖,否则跟随 GUI 默认模型 |
|
|
83
|
+
|
|
84
|
+
`workspace`、`agentPreset`、`persona` / `personaFile`、`thinking`、`maxReplyBytes`、`reasoningEffort`、`wecomCli` 不在卡片上,仍在 profile patch 或下表中配置。本版本不做凭证热重连;改 `wecomCli` 也须重启进程。
|
|
42
85
|
|
|
43
86
|
## 配置项
|
|
44
87
|
|
|
@@ -48,16 +91,17 @@ bundle 的 `cordis.patch.yml` 已为除 `botId` / `secret` 外的字段提供默
|
|
|
48
91
|
|---|---|
|
|
49
92
|
| `botId` / `secret` | 企业微信智能机器人凭证(`role('secret')`,UI 自动脱敏);缺省时跳过企微侧,不阻塞主进程 |
|
|
50
93
|
| `workspace` | Agent 工作目录(会话 cwd) |
|
|
51
|
-
| `allowFrom` |
|
|
94
|
+
| `allowFrom` | 聊天白名单;空 = 允许所有人问诊断。不控制 wecom-cli |
|
|
52
95
|
| `agentTimeoutSec` | 单任务最长执行时间(秒),动画进度条/剩余估算的基准 |
|
|
53
96
|
| `startHint` | 开始处理时的占位提示语 |
|
|
54
97
|
| `agentPreset` | Agent 加入的 preset(默认 `standard`) |
|
|
55
|
-
| `provider` / `model` | 企微专用模型;**两者都非空**才覆盖,否则跟随 GUI 的 `agent-default-model`;只填一项会告警并回退。Settings
|
|
98
|
+
| `provider` / `model` | 企微专用模型;**两者都非空**才覆盖,否则跟随 GUI 的 `agent-default-model`;只填一项会告警并回退。Settings 可编,只影响之后新建的企微窗口会话 |
|
|
56
99
|
| `reasoningEffort` | 覆盖生效时可选的推理强度;未覆盖模型时忽略 |
|
|
57
100
|
| `persona` / `personaFile` | 机器人「人设」;优先级:`personaFile` → `persona` → 包内默认(按 Host `locale.preference` 选中/英);覆盖不跟语言切换;含敏感信息请勿入库 |
|
|
58
101
|
| `maxReplyBytes` | 回复上限(字节,默认 20000) |
|
|
59
102
|
| `deniedMessage` | 非白名单用户的拒绝文案(Settings 可编) |
|
|
60
103
|
| `welcomeMessage` | 进入会话欢迎语(Settings 可编) |
|
|
104
|
+
| `wecomCli` | 可选的企业微信办公能力(默认关闭)。见下一节 |
|
|
61
105
|
| `thinking` | 流式动画。优先级:工具活动(`toolLabels`)> 模型流式阶段(`reasoningStatus` / `outputStatus`,来自 `assistant/chunk`)> 时间轴 `phases` 兜底;另有 `spin` / `reasoningSpin` / `outputSpin` / `eggs` 等 |
|
|
62
106
|
|
|
63
107
|
企微与 GUI 使用不同模型时,在 profile `cordis.patch.yml` 同时填写:
|
|
@@ -87,6 +131,41 @@ thinking:
|
|
|
87
131
|
pwsh: PowerShell
|
|
88
132
|
```
|
|
89
133
|
|
|
134
|
+
## 企业微信办公能力(wecom-cli)
|
|
135
|
+
|
|
136
|
+
插件依赖官方 [`@wecom/cli`](https://www.npmjs.com/package/@wecom/cli) 二进制。`wecomcli-*` 装在 **`$DSH_HOME/wecom-cli-skills`**(不要装进工作区 `.dsh/skills` / `.agents/skills`,也不要装进 `$DSH_HOME/skills`)。插件只在**办公 userid 的单聊** Agent 上注入:`skills.register()` 装 catalog,`tools.register()` 装门控工具 `wecom_cli`。两者都走该 Agent 自己的 ctx,群聊与 GUI 看不到。工作区里其它 skill 仍由 `skill-filesystem` 发现,不受影响。
|
|
137
|
+
|
|
138
|
+
办公命令**只经 `wecom_cli` 工具执行**:模型传 `argv`(`wecom-cli` 之后的参数数组),插件直接 spawn 官方二进制,并拒绝任何 `auth init`。PATH 上的 `wecom-cli` 是一个只打印拒绝信息并 `exit 1` 的 shim,所以群聊、GUI 以及任何 `pwsh wecom-cli` 都跑不通;shim 的文案会指回 `wecom_cli`。凭证目录不进程级导出,只在插件自己 spawn 时注入。
|
|
139
|
+
|
|
140
|
+
`wecomCli.enabled` 默认关闭。开启须同时配置非空 **`wecomCli.allowFrom`**(办公 userid);根级 `allowFrom` 只管谁能聊天,空名单表示所有人可问诊断。办公名单为空时插件会告警并跳过 shim / 授权 / `wecom_cli`。
|
|
141
|
+
|
|
142
|
+
一次性准备:
|
|
143
|
+
|
|
144
|
+
1. 根级 `allowFrom` 留空(所有人可问诊断),把 **办公** userid 写进 `wecomCli.allowFrom`(不要留空)
|
|
145
|
+
2. 在 **Settings → 插件配置 → 企业微信桥接** 点「安装官方 skills」(Host 下载官方仓库 zip,解到 `$DSH_HOME/wecom-cli-skills`)。**不要用** `npx skills add -g`(会泄漏给 GUI);skills CLI **没有 `--dir`**,加了也不会写到程序目录。若工作区里已有 `wecomcli-*`,先挪到该目录再删工作区副本。也可手动把官方仓库 `skills/wecomcli-*` 拷进 `$DSH_HOME/wecom-cli-skills`。
|
|
146
|
+
|
|
147
|
+
3. 在 profile `cordis.patch.yml` 打开(启用后插件会用已有 `botId` / `secret` 走 `auth init --bot-id/--secret` 写入 wecom-cli 凭据,不必扫码、不必 `npm install -g @wecom/cli`):
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- id: im-bridge
|
|
151
|
+
config:
|
|
152
|
+
allowFrom: []
|
|
153
|
+
wecomCli:
|
|
154
|
+
enabled: true
|
|
155
|
+
allowFrom: ["<办公 userid>"]
|
|
156
|
+
# skillsDir: '' # 空 = $DSH_HOME/wecom-cli-skills
|
|
157
|
+
# configDir: '' # 空 = <workspace>/.dsh/wecom-cli;请 gitignore
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| 字段 | 说明 |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `wecomCli.enabled` | 装 PATH 拒绝 shim、跑授权检查、给企微 Agent 接上 prompt 与 `wecom_cli` 工具;默认 `false` |
|
|
163
|
+
| `wecomCli.allowFrom` | 能拿到 `wecom_cli` 工具的 userid;空则跳过 shim / 授权。与根级聊天名单独立 |
|
|
164
|
+
| `wecomCli.skillsDir` | 覆盖 skills 根目录;空 = `$DSH_HOME/wecom-cli-skills` |
|
|
165
|
+
| `wecomCli.configDir` | 覆盖凭证目录;空 = `<workspace>/.dsh/wecom-cli`。请把该目录加入 gitignore。`WECOM_CLI_CONFIG_DIR` 只在插件 spawn CLI 时注入,不写进程环境,因此不会使用 `~/.config/wecom`。 |
|
|
166
|
+
|
|
167
|
+
改 `wecomCli` 后须重启进程。未授权时插件仍收消息;启动时用隐藏的 `--bot-id/--secret`(stderr 非 TTY)写入凭据。若自动写入失败,日志会打印一条带 `WECOM_CLI_CONFIG_DIR` 的手动命令——必须带上它,否则 `npx --yes @wecom/cli auth init --manual` 会把凭证写到 `~/.config/wecom`,插件读不到。禁止在 Agent 里扫码 `auth init`(会新建机器人)。
|
|
168
|
+
|
|
90
169
|
## 人设(persona)
|
|
91
170
|
|
|
92
171
|
优先级:`personaFile` → `persona` 字符串 → 包内默认人设。
|
|
@@ -104,11 +183,35 @@ thinking:
|
|
|
104
183
|
|
|
105
184
|
也可复制 [`persona.example.md`](./persona.example.md) 为模板后按环境填写。支持 `{{model}}` / `{{cwd}}` 占位符。含环境凭据的人设文件请勿提交。欢迎语 / 拒绝文案 / 思考动画文案目前仍为中文配置项,不随语言切换。
|
|
106
185
|
|
|
186
|
+
## 把 PNG 发到企业微信
|
|
187
|
+
|
|
188
|
+
Agent 的最终回复若包含指向**工作区内** PNG 的 Markdown,桥会在文字流结束之后,把图作为**独立图片消息**发出(先 `uploadMedia`,再用 `media_id` 调用 `sendMediaMessage`)。
|
|
189
|
+
|
|
190
|
+
触发写法(相对 `workspace`):
|
|
191
|
+
|
|
192
|
+
```markdown
|
|
193
|
+

|
|
194
|
+
[屏幕截图](out/frame.png)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- 只认 `.png`;跳过 `http(s):` / `data:`;路径必须落在 `workspace` 内
|
|
198
|
+
- 文件须为真实 PNG(文件头),单张不超过 **10MB**,最多 **10** 张(按出现顺序去重)
|
|
199
|
+
- 只把 PNG 写到磁盘、回复里没有上述 Markdown,**不会**发图
|
|
200
|
+
- 图在文字气泡之后另发,不会嵌进同一条流式消息
|
|
201
|
+
- 入站图片/语音/文件消息仍忽略
|
|
202
|
+
|
|
107
203
|
## 安全
|
|
108
204
|
|
|
109
|
-
- `config.json` / `persona.md` 等含密钥文件不入库;
|
|
205
|
+
- `config.json` / `persona.md` / `.dsh/wecom-cli/` 等含密钥文件不入库;
|
|
110
206
|
- 会话与工具输出可能含对抗性文本,插件内置安全提示词约束 agent 不把工具输出当指令。
|
|
111
207
|
|
|
208
|
+
## Known Limitations and Deferred Work
|
|
209
|
+
|
|
210
|
+
- wecom-cli 凭证在工作区 `<workspace>/.dsh/wecom-cli`(请 gitignore)。`wecomCli.allowFrom` 里的人借用这份凭据的办公权限;聊天名单(根级 `allowFrom`)不授予办公。办公单聊仍可向任意 `--chat-id` 发信,插件不锁定收件人。
|
|
211
|
+
- 门控不是沙箱。`wecom_cli` 工具与 `wecomcli-*` 只注册到办公单聊 Agent,PATH 上的 `wecom-cli` 一律拒绝,凭证目录也只在插件自己 spawn 时注入;但同进程的 shell 仍可绕过:直接 `node <@wecom/cli 的 wecom.js 绝对路径>`,或 `npx --yes @wecom/cli` 并自行设置 `WECOM_CLI_CONFIG_DIR`。不设该变量时这类旁路会落到未授权的 `~/.config/wecom`。真正的隔离需要进程级沙箱。
|
|
212
|
+
- 企微通道没有 GUI 审批框:发信、取消会议、删待办、覆盖文档等不可逆操作只靠 prompt 约束(先 `--dry-run`,等用户下一条确认)。所有企微会话都禁止 `ask_user_question`(会挂到超时)。
|
|
213
|
+
- 工作区 `.dsh/skills` / `.agents/skills` 里残留的 `wecomcli-*` 仍会被同 cwd 的 GUI 和群聊发现。其它 skill 不受影响。`enabled: false` 只关拒绝 shim、授权检查、`wecom_cli` 与 wecomcli-* 注册;通道上的 `ask_user_question` 禁令仍会注入。
|
|
214
|
+
|
|
112
215
|
## License
|
|
113
216
|
|
|
114
217
|
MIT
|
package/cordis.patch.yml
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
maxReplyBytes: 20000
|
|
19
19
|
deniedMessage: '无权访问本服务'
|
|
20
20
|
welcomeMessage: '👋 办公助手已就绪。直接发消息即可,例如查文件、整理文档、查资料或处理日常事务。'
|
|
21
|
+
wecomCli:
|
|
22
|
+
enabled: false
|
|
23
|
+
skillsDir: ''
|
|
24
|
+
allowFrom: []
|
|
25
|
+
# skillsDir 空 = $DSH_HOME/wecom-cli-skills(不要用工作区 .dsh/skills)
|
|
26
|
+
# configDir: '' # 空 = <workspace>/.dsh/wecom-cli
|
|
21
27
|
thinking:
|
|
22
28
|
eggAfterSec: 240
|
|
23
29
|
intervalMs: 1500
|
package/lib/client.js
CHANGED
|
@@ -115,7 +115,7 @@ window.__ModuleLoader__.load({
|
|
|
115
115
|
}
|
|
116
116
|
//#endregion
|
|
117
117
|
//#region \0dsh-css:C:\Users\user\Desktop\dsh-im-bridge\plugin\src\client\fields.module.css.mjs
|
|
118
|
-
const css = ".OMD7cq_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.OMD7cq_field+.OMD7cq_field{border-top:1px solid var(--dsw-alias-border-l2)}.OMD7cq_head{align-items:center;gap:8px;display:flex}.OMD7cq_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.OMD7cq_badges{align-items:center;gap:8px;display:inline-flex}.OMD7cq_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.OMD7cq_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}.OMD7cq_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.OMD7cq_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.OMD7cq_reset:disabled{cursor:default}.OMD7cq_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.OMD7cq_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.OMD7cq_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.OMD7cq_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.OMD7cq_inputInvalid:focus-visible{outline:none}.OMD7cq_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.OMD7cq_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}";
|
|
118
|
+
const css = ".OMD7cq_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.OMD7cq_field+.OMD7cq_field{border-top:1px solid var(--dsw-alias-border-l2)}.OMD7cq_head{align-items:center;gap:8px;display:flex}.OMD7cq_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.OMD7cq_badges{align-items:center;gap:8px;display:inline-flex}.OMD7cq_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.OMD7cq_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}.OMD7cq_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.OMD7cq_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.OMD7cq_reset:disabled{cursor:default}.OMD7cq_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.OMD7cq_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.OMD7cq_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.OMD7cq_input::placeholder{color:var(--dsw-alias-label-tertiary)}.OMD7cq_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.OMD7cq_inputInvalid:focus-visible{outline:none}.OMD7cq_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.OMD7cq_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.OMD7cq_install{appearance:none;font:inherit;cursor:pointer;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border:1px solid #0000;border-radius:8px;align-self:flex-start;padding:5px 14px;font-size:13px;line-height:1.5}.OMD7cq_install:hover:not(:disabled){opacity:.92}.OMD7cq_install:disabled{opacity:.4;cursor:default}.OMD7cq_install:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
|
|
119
119
|
const tagId = "@mhfire/dsh-im-bridge/fields.module.css";
|
|
120
120
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
121
121
|
const tag = document.createElement("style");
|
|
@@ -133,6 +133,7 @@ window.__ModuleLoader__.load({
|
|
|
133
133
|
"hint": "OMD7cq_hint",
|
|
134
134
|
"input": "OMD7cq_input",
|
|
135
135
|
"inputInvalid": "OMD7cq_inputInvalid",
|
|
136
|
+
"install": "OMD7cq_install",
|
|
136
137
|
"invalid": "OMD7cq_invalid",
|
|
137
138
|
"label": "OMD7cq_label",
|
|
138
139
|
"reset": "OMD7cq_reset"
|
|
@@ -189,7 +190,8 @@ window.__ModuleLoader__.load({
|
|
|
189
190
|
}
|
|
190
191
|
/**
|
|
191
192
|
* Write-only credential control. The literal never rides a response, so the
|
|
192
|
-
* control starts blank and reports only whether one is configured.
|
|
193
|
+
* control starts blank and reports only whether one is configured. A configured
|
|
194
|
+
* empty draft shows a dots placeholder so the box does not look unset.
|
|
193
195
|
* @param props - the field's copy, staged text, and configured state.
|
|
194
196
|
* @returns the labelled control.
|
|
195
197
|
*/
|
|
@@ -217,6 +219,7 @@ window.__ModuleLoader__.load({
|
|
|
217
219
|
type: "password",
|
|
218
220
|
autoComplete: "off",
|
|
219
221
|
value: props.text,
|
|
222
|
+
placeholder: props.configured && props.text === "" ? "••••••••" : void 0,
|
|
220
223
|
disabled: props.disabled,
|
|
221
224
|
onChange: (event) => {
|
|
222
225
|
props.onEdit(event.target.value);
|
|
@@ -278,6 +281,16 @@ window.__ModuleLoader__.load({
|
|
|
278
281
|
props.edit("secret", text);
|
|
279
282
|
}
|
|
280
283
|
}),
|
|
284
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillsInstall, {
|
|
285
|
+
t,
|
|
286
|
+
disabled,
|
|
287
|
+
available: state.skillsInstallAvailable,
|
|
288
|
+
status: state.skillsInstallStatus,
|
|
289
|
+
dest: state.skillsDest,
|
|
290
|
+
count: state.skillsCount,
|
|
291
|
+
error: state.skillsError,
|
|
292
|
+
onInstall: props.installSkills
|
|
293
|
+
}),
|
|
281
294
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
282
295
|
id: "im-bridge-allowFrom",
|
|
283
296
|
label: t("allowFrom"),
|
|
@@ -373,6 +386,44 @@ window.__ModuleLoader__.load({
|
|
|
373
386
|
]
|
|
374
387
|
});
|
|
375
388
|
}
|
|
389
|
+
function skillsStatusText(t, status, dest, count, error, available) {
|
|
390
|
+
if (!available) return t("skillsUnavailable");
|
|
391
|
+
if (status === "ok") return t("skillsInstalled").replace("{count}", String(count)).replace("{dest}", dest);
|
|
392
|
+
if (status === "error") return error === "" ? t("skillsFailed") : `${t("skillsFailed")} ${error}`;
|
|
393
|
+
return t("skillsHint");
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Host-side install of official wecomcli-* (browser never chooses the path).
|
|
397
|
+
* @param props - copy, status, and the install action.
|
|
398
|
+
* @returns the labelled control.
|
|
399
|
+
*/
|
|
400
|
+
function SkillsInstall(props) {
|
|
401
|
+
const installing = props.status === "installing";
|
|
402
|
+
const status = skillsStatusText(props.t, props.status, props.dest, props.count, props.error, props.available);
|
|
403
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
404
|
+
className: fields_module_css_default.field,
|
|
405
|
+
children: [
|
|
406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
407
|
+
className: fields_module_css_default.head,
|
|
408
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
409
|
+
className: fields_module_css_default.label,
|
|
410
|
+
children: props.t("skillsTitle")
|
|
411
|
+
})
|
|
412
|
+
}),
|
|
413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
414
|
+
type: "button",
|
|
415
|
+
className: fields_module_css_default.install,
|
|
416
|
+
disabled: props.disabled || !props.available || installing,
|
|
417
|
+
onClick: props.onInstall,
|
|
418
|
+
children: props.t(installing ? "skillsInstalling" : "skillsInstall")
|
|
419
|
+
}),
|
|
420
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
421
|
+
className: props.status === "error" || !props.available ? fields_module_css_default.invalid : fields_module_css_default.hint,
|
|
422
|
+
children: status
|
|
423
|
+
})
|
|
424
|
+
]
|
|
425
|
+
});
|
|
426
|
+
}
|
|
376
427
|
//#endregion
|
|
377
428
|
//#region src/client/card-form.ts
|
|
378
429
|
/**
|
|
@@ -577,6 +628,10 @@ window.__ModuleLoader__.load({
|
|
|
577
628
|
this.failed = false;
|
|
578
629
|
this.publish();
|
|
579
630
|
}
|
|
631
|
+
/** Rebuild bound projections for card-owned state outside the settings draft. */
|
|
632
|
+
notify() {
|
|
633
|
+
this.publish();
|
|
634
|
+
}
|
|
580
635
|
spec(field) {
|
|
581
636
|
const spec = this.specs.get(field);
|
|
582
637
|
if (spec === void 0) throw new Error(`im-bridge card has no field ${field}`);
|
|
@@ -610,15 +665,22 @@ window.__ModuleLoader__.load({
|
|
|
610
665
|
var WecomCardController = class {
|
|
611
666
|
scope;
|
|
612
667
|
describe;
|
|
668
|
+
rpc;
|
|
613
669
|
form;
|
|
614
670
|
store;
|
|
671
|
+
skillsInstallStatus = "idle";
|
|
672
|
+
skillsDest = "";
|
|
673
|
+
skillsCount = 0;
|
|
674
|
+
skillsError = "";
|
|
615
675
|
/**
|
|
616
676
|
* @param scope - bound settings scope for the `im-bridge` namespace.
|
|
617
677
|
* @param describe - Host describe face; secret literals never ride it.
|
|
678
|
+
* @param rpc - optional Connection RPC for the skills install button.
|
|
618
679
|
*/
|
|
619
|
-
constructor(scope, describe) {
|
|
680
|
+
constructor(scope, describe, rpc) {
|
|
620
681
|
this.scope = scope;
|
|
621
682
|
this.describe = describe;
|
|
683
|
+
this.rpc = rpc;
|
|
622
684
|
this.form = new CardForm(scope, [
|
|
623
685
|
csvField("allowFrom"),
|
|
624
686
|
numberField("agentTimeoutSec"),
|
|
@@ -649,7 +711,12 @@ window.__ModuleLoader__.load({
|
|
|
649
711
|
deniedMessage: this.form.field("deniedMessage"),
|
|
650
712
|
welcomeMessage: this.form.field("welcomeMessage"),
|
|
651
713
|
provider: this.form.field("provider"),
|
|
652
|
-
model: this.form.field("model")
|
|
714
|
+
model: this.form.field("model"),
|
|
715
|
+
skillsInstallAvailable: this.rpc !== void 0,
|
|
716
|
+
skillsInstallStatus: this.skillsInstallStatus,
|
|
717
|
+
skillsDest: this.skillsDest,
|
|
718
|
+
skillsCount: this.skillsCount,
|
|
719
|
+
skillsError: this.skillsError
|
|
653
720
|
};
|
|
654
721
|
}
|
|
655
722
|
/**
|
|
@@ -670,11 +737,45 @@ window.__ModuleLoader__.load({
|
|
|
670
737
|
await this.scope.set(field, text);
|
|
671
738
|
return this.secretConfigured(field);
|
|
672
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Download official wecomcli-* into the Host-resolved directory.
|
|
742
|
+
* The browser does not choose the path.
|
|
743
|
+
*/
|
|
744
|
+
installSkills() {
|
|
745
|
+
this.runInstall();
|
|
746
|
+
}
|
|
747
|
+
async runInstall() {
|
|
748
|
+
if (this.rpc === void 0 || this.skillsInstallStatus === "installing") return;
|
|
749
|
+
this.skillsInstallStatus = "installing";
|
|
750
|
+
this.skillsError = "";
|
|
751
|
+
this.form.notify();
|
|
752
|
+
try {
|
|
753
|
+
const result = await this.rpc.call("/im-bridge", "wecomcli.installSkills", {});
|
|
754
|
+
if (!result.ok) {
|
|
755
|
+
this.skillsInstallStatus = "error";
|
|
756
|
+
this.skillsError = result.error.message;
|
|
757
|
+
this.form.notify();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const value = result.value;
|
|
761
|
+
this.skillsInstallStatus = "ok";
|
|
762
|
+
this.skillsDest = typeof value.dest === "string" ? value.dest : "";
|
|
763
|
+
this.skillsCount = typeof value.count === "number" ? value.count : 0;
|
|
764
|
+
this.skillsError = "";
|
|
765
|
+
} catch (error) {
|
|
766
|
+
this.skillsInstallStatus = "error";
|
|
767
|
+
this.skillsError = error instanceof Error ? error.message : String(error);
|
|
768
|
+
}
|
|
769
|
+
this.form.notify();
|
|
770
|
+
}
|
|
673
771
|
/** Face the slot registration injects. */
|
|
674
772
|
inject() {
|
|
675
773
|
return {
|
|
676
774
|
hooks: { wecomCard: this.store },
|
|
677
|
-
...this.form.actions()
|
|
775
|
+
...this.form.actions(),
|
|
776
|
+
installSkills: () => {
|
|
777
|
+
this.installSkills();
|
|
778
|
+
}
|
|
678
779
|
};
|
|
679
780
|
}
|
|
680
781
|
};
|
|
@@ -697,7 +798,7 @@ window.__ModuleLoader__.load({
|
|
|
697
798
|
invalidNumber: "Enter a finite number.",
|
|
698
799
|
botId: "Bot ID",
|
|
699
800
|
secret: "Secret",
|
|
700
|
-
secretHint: "
|
|
801
|
+
secretHint: "The box stays empty on purpose (the stored value never rides the wire). A Configured badge means it is saved; type a new value to replace it. Save, then restart the process to open the WebSocket.",
|
|
701
802
|
secretConfigured: "Configured",
|
|
702
803
|
secretUnset: "Not configured",
|
|
703
804
|
allowFrom: "Allowed sender userids",
|
|
@@ -713,7 +814,14 @@ window.__ModuleLoader__.load({
|
|
|
713
814
|
provider: "WeCom-only provider",
|
|
714
815
|
providerHint: "Empty follows the GUI default model. Both provider and model must be set to override.",
|
|
715
816
|
model: "WeCom-only model",
|
|
716
|
-
modelHint: "Takes effect only together with provider."
|
|
817
|
+
modelHint: "Takes effect only together with provider.",
|
|
818
|
+
skillsTitle: "WeCom office skills",
|
|
819
|
+
skillsHint: "Installs wecomcli-* into the plugin directory ($DSH_HOME/wecom-cli-skills). Do not use npx skills add -g; that CLI has no --dir. An empty Bot ID / Secret box is normal.",
|
|
820
|
+
skillsInstall: "Install official skills",
|
|
821
|
+
skillsInstalling: "Installing…",
|
|
822
|
+
skillsInstalled: "Installed {count} skills into {dest}",
|
|
823
|
+
skillsFailed: "Install failed.",
|
|
824
|
+
skillsUnavailable: "Install needs the Web host Connection."
|
|
717
825
|
};
|
|
718
826
|
/** Chinese copy for the im-bridge card. */
|
|
719
827
|
const zh = {
|
|
@@ -732,7 +840,7 @@ window.__ModuleLoader__.load({
|
|
|
732
840
|
invalidNumber: "请输入有效数字。",
|
|
733
841
|
botId: "Bot ID",
|
|
734
842
|
secret: "Secret",
|
|
735
|
-
secretHint: "
|
|
843
|
+
secretHint: "框空是正常的(已存值不会传到浏览器)。徽章「已配置」即已保存;重新输入可覆盖。保存后需重启进程才会连 WebSocket。",
|
|
736
844
|
secretConfigured: "已配置",
|
|
737
845
|
secretUnset: "未配置",
|
|
738
846
|
allowFrom: "允许的发送者 userid",
|
|
@@ -748,7 +856,14 @@ window.__ModuleLoader__.load({
|
|
|
748
856
|
provider: "企微专用 provider",
|
|
749
857
|
providerHint: "空 = 跟随 GUI 默认模型。须与 model 同时填写才覆盖。",
|
|
750
858
|
model: "企微专用 model",
|
|
751
|
-
modelHint: "仅在同时填写 provider 时生效。"
|
|
859
|
+
modelHint: "仅在同时填写 provider 时生效。",
|
|
860
|
+
skillsTitle: "企微办公 skills",
|
|
861
|
+
skillsHint: "装到程序目录 $DSH_HOME/wecom-cli-skills。不要用 npx skills add -g;CLI 没有 --dir。Bot ID / Secret 框空是正常的。",
|
|
862
|
+
skillsInstall: "安装官方 skills",
|
|
863
|
+
skillsInstalling: "正在安装…",
|
|
864
|
+
skillsInstalled: "已装 {count} 个到 {dest}",
|
|
865
|
+
skillsFailed: "安装失败。",
|
|
866
|
+
skillsUnavailable: "安装需要 Web Host 的 Connection。"
|
|
752
867
|
};
|
|
753
868
|
//#endregion
|
|
754
869
|
//#region src/client/index.ts
|
|
@@ -767,7 +882,8 @@ window.__ModuleLoader__.load({
|
|
|
767
882
|
* @param ctx - browser plugin context.
|
|
768
883
|
*/
|
|
769
884
|
function apply(ctx) {
|
|
770
|
-
const
|
|
885
|
+
const connection = ctx.get("connection");
|
|
886
|
+
const card = new WecomCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.settingsScope.describe(), connection?.rpc);
|
|
771
887
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
772
888
|
zh,
|
|
773
889
|
en
|