@mhfire/dsh-im-bridge 0.3.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 +49 -6
- package/README.md +49 -6
- package/cordis.patch.yml +6 -0
- package/lib/client.js +126 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +8061 -12
- package/package.json +24 -9
- 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 +281 -10
- package/src/session-key.ts +1 -0
- package/src/wecom-cli.ts +1015 -0
- package/tsdown.host.ts +1 -0
package/README.en.md
CHANGED
|
@@ -14,7 +14,7 @@ One DSH session maps to one WeCom chat window, not “every window of the same u
|
|
|
14
14
|
|
|
15
15
|
- **1:1**: `single:<userid>` — one Agent for that user
|
|
16
16
|
- **Group**: `group:<chatid>` — all members share one Agent and one serial queue (two people speaking at once still cannot concurrent-`followup`)
|
|
17
|
-
- `allowFrom`
|
|
17
|
+
- `allowFrom` filters **who may chat** by sender userid; empty = enqueue everyone. Office commands use `wecomCli.allowFrom` separately
|
|
18
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
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
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
|
|
@@ -23,7 +23,7 @@ One DSH session maps to one WeCom chat window, not “every window of the same u
|
|
|
23
23
|
|
|
24
24
|
## Compatible DeepSeek Harness versions
|
|
25
25
|
|
|
26
|
-
DeepSeek Harness is still a developer preview and makes **no semver compatibility promise** to out-of-tree plugins. Package 0.
|
|
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
27
|
|
|
28
28
|
| DSH | This package |
|
|
29
29
|
|---|---|
|
|
@@ -40,7 +40,7 @@ Pin `dsh` to `0.1.0-rc.8` or later, for example `npx @deepseek-ai/dsh@0.1.1-rc.2
|
|
|
40
40
|
```powershell
|
|
41
41
|
dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
42
42
|
# Or pin a version:
|
|
43
|
-
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.
|
|
43
|
+
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.4.2
|
|
44
44
|
```
|
|
45
45
|
|
|
46
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):
|
|
@@ -81,7 +81,7 @@ Fields, top to bottom:
|
|
|
81
81
|
| Welcome message | `welcomeMessage` | Applies on the next message |
|
|
82
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
83
|
|
|
84
|
-
`workspace`, `agentPreset`, `persona` / `personaFile`, `thinking`, `maxReplyBytes`, and `
|
|
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.
|
|
85
85
|
|
|
86
86
|
## Configuration
|
|
87
87
|
|
|
@@ -91,7 +91,7 @@ The bundle `cordis.patch.yml` supplies defaults for every field except `botId` /
|
|
|
91
91
|
|---|---|
|
|
92
92
|
| `botId` / `secret` | WeCom AI Bot credentials (`role('secret')`, redacted in UI); when empty, WeCom side is skipped and the host keeps running |
|
|
93
93
|
| `workspace` | Agent working directory (session cwd) |
|
|
94
|
-
| `allowFrom` |
|
|
94
|
+
| `allowFrom` | Chat allow-list; empty = everyone may ask. Does not gate wecom-cli |
|
|
95
95
|
| `agentTimeoutSec` | Max seconds per task; also drives progress / ETA |
|
|
96
96
|
| `startHint` | Placeholder text when processing starts |
|
|
97
97
|
| `agentPreset` | Agent preset to mount (default `standard`) |
|
|
@@ -101,6 +101,7 @@ The bundle `cordis.patch.yml` supplies defaults for every field except `botId` /
|
|
|
101
101
|
| `maxReplyBytes` | Reply size cap in bytes (default 20000) |
|
|
102
102
|
| `deniedMessage` | Reply when the sender is not on `allowFrom` (editable in Settings) |
|
|
103
103
|
| `welcomeMessage` | Welcome text on `enter_chat` (editable in Settings) |
|
|
104
|
+
| `wecomCli` | Optional WeCom office skills (off by default). See the next section |
|
|
104
105
|
| `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
|
|
|
106
107
|
To give WeCom a different model from the GUI, set both in the profile `cordis.patch.yml`:
|
|
@@ -130,6 +131,41 @@ thinking:
|
|
|
130
131
|
pwsh: PowerShell
|
|
131
132
|
```
|
|
132
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
|
+
|
|
133
169
|
## Persona
|
|
134
170
|
|
|
135
171
|
Precedence: `personaFile` → `persona` string → packaged default persona.
|
|
@@ -166,9 +202,16 @@ Trigger (paths relative to `workspace`):
|
|
|
166
202
|
|
|
167
203
|
## Security
|
|
168
204
|
|
|
169
|
-
- 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/`
|
|
170
206
|
- Session and tool output may contain adversarial text; the plugin’s safety prompt tells the agent not to treat tool output as instructions
|
|
171
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
|
+
|
|
172
215
|
## License
|
|
173
216
|
|
|
174
217
|
MIT
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Host 通过 `installSettingsSection` 注册 `im-bridge` 命名空间;浏览器
|
|
|
14
14
|
|
|
15
15
|
- **单聊**:`single:<userid>`,该用户一条 Agent
|
|
16
16
|
- **群聊**:`group:<chatid>`,群内所有人共用一条 Agent 和同一条串行队列(两人同时发也不会并发 `followup`)
|
|
17
|
-
- `allowFrom`
|
|
17
|
+
- `allowFrom` 按**发送者** userid 拦截谁能聊天;空 = 所有人可进队。办公命令另用 `wecomCli.allowFrom`
|
|
18
18
|
- 群里 @机器人 的消息,开头的 `@昵称` 在入站就去掉:模型看到的和标题用的都是「测试一下」而不是「@MediaAgent 测试一下」;正文中间的 @某人 保留,整条只有 @ 时按原文交给模型
|
|
19
19
|
- 进程重启后用稳定 id `wecom-` + key 的短 hash 续上同一条会话:进程里已有活 Agent 就直接采用(例如浏览器已打开该行),存档里有就 `resume`,都没有才 `create`。resume 的 cwd / preset 跟存档,不跟当前配置;cwd 不一致时打警告,不改写。
|
|
20
20
|
- GUI 标题为「企微·私聊/群」+ 第一句用户话(与其它会话一样由 DSH 生成),不再露出 userid / chatid;同类型窗口若第一句话相近,侧栏仍是两行
|
|
@@ -23,7 +23,7 @@ Host 通过 `installSettingsSection` 注册 `im-bridge` 命名空间;浏览器
|
|
|
23
23
|
|
|
24
24
|
## 兼容的 DeepSeek Harness 版本
|
|
25
25
|
|
|
26
|
-
DeepSeek Harness 仍是 developer preview,对外置插件**没有 semver 兼容承诺**。本包 0.
|
|
26
|
+
DeepSeek Harness 仍是 developer preview,对外置插件**没有 semver 兼容承诺**。本包 0.4.2 按实际调用的 API 对齐已发布 tag:
|
|
27
27
|
|
|
28
28
|
| DSH | 本包 |
|
|
29
29
|
|---|---|
|
|
@@ -40,7 +40,7 @@ DeepSeek Harness 仍是 developer preview,对外置插件**没有 semver 兼
|
|
|
40
40
|
```powershell
|
|
41
41
|
dsh plugin --profile web add @mhfire/dsh-im-bridge
|
|
42
42
|
# 或钉版本:
|
|
43
|
-
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.
|
|
43
|
+
# dsh plugin --profile web add @mhfire/dsh-im-bridge@0.4.2
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
在 `$DSH_HOME/profiles/web/cordis.patch.yml`(或对应 profile)中补密钥即可(其余项已有 bundle 默认,可按需覆盖):
|
|
@@ -81,7 +81,7 @@ dsh plugin --profile web add <本包路径>
|
|
|
81
81
|
| 进入会话欢迎语 | `welcomeMessage` | 下一轮消息生效 |
|
|
82
82
|
| 企微专用 provider / model | `provider` / `model` | 只影响之后**新建**的企微窗口会话;须两项都填才覆盖,否则跟随 GUI 默认模型 |
|
|
83
83
|
|
|
84
|
-
`workspace`、`agentPreset`、`persona` / `personaFile`、`thinking`、`maxReplyBytes`、`reasoningEffort` 不在卡片上,仍在 profile patch
|
|
84
|
+
`workspace`、`agentPreset`、`persona` / `personaFile`、`thinking`、`maxReplyBytes`、`reasoningEffort`、`wecomCli` 不在卡片上,仍在 profile patch 或下表中配置。本版本不做凭证热重连;改 `wecomCli` 也须重启进程。
|
|
85
85
|
|
|
86
86
|
## 配置项
|
|
87
87
|
|
|
@@ -91,7 +91,7 @@ bundle 的 `cordis.patch.yml` 已为除 `botId` / `secret` 外的字段提供默
|
|
|
91
91
|
|---|---|
|
|
92
92
|
| `botId` / `secret` | 企业微信智能机器人凭证(`role('secret')`,UI 自动脱敏);缺省时跳过企微侧,不阻塞主进程 |
|
|
93
93
|
| `workspace` | Agent 工作目录(会话 cwd) |
|
|
94
|
-
| `allowFrom` |
|
|
94
|
+
| `allowFrom` | 聊天白名单;空 = 允许所有人问诊断。不控制 wecom-cli |
|
|
95
95
|
| `agentTimeoutSec` | 单任务最长执行时间(秒),动画进度条/剩余估算的基准 |
|
|
96
96
|
| `startHint` | 开始处理时的占位提示语 |
|
|
97
97
|
| `agentPreset` | Agent 加入的 preset(默认 `standard`) |
|
|
@@ -101,6 +101,7 @@ bundle 的 `cordis.patch.yml` 已为除 `botId` / `secret` 外的字段提供默
|
|
|
101
101
|
| `maxReplyBytes` | 回复上限(字节,默认 20000) |
|
|
102
102
|
| `deniedMessage` | 非白名单用户的拒绝文案(Settings 可编) |
|
|
103
103
|
| `welcomeMessage` | 进入会话欢迎语(Settings 可编) |
|
|
104
|
+
| `wecomCli` | 可选的企业微信办公能力(默认关闭)。见下一节 |
|
|
104
105
|
| `thinking` | 流式动画。优先级:工具活动(`toolLabels`)> 模型流式阶段(`reasoningStatus` / `outputStatus`,来自 `assistant/chunk`)> 时间轴 `phases` 兜底;另有 `spin` / `reasoningSpin` / `outputSpin` / `eggs` 等 |
|
|
105
106
|
|
|
106
107
|
企微与 GUI 使用不同模型时,在 profile `cordis.patch.yml` 同时填写:
|
|
@@ -130,6 +131,41 @@ thinking:
|
|
|
130
131
|
pwsh: PowerShell
|
|
131
132
|
```
|
|
132
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
|
+
|
|
133
169
|
## 人设(persona)
|
|
134
170
|
|
|
135
171
|
优先级:`personaFile` → `persona` 字符串 → 包内默认人设。
|
|
@@ -166,9 +202,16 @@ Agent 的最终回复若包含指向**工作区内** PNG 的 Markdown,桥会
|
|
|
166
202
|
|
|
167
203
|
## 安全
|
|
168
204
|
|
|
169
|
-
- `config.json` / `persona.md` 等含密钥文件不入库;
|
|
205
|
+
- `config.json` / `persona.md` / `.dsh/wecom-cli/` 等含密钥文件不入库;
|
|
170
206
|
- 会话与工具输出可能含对抗性文本,插件内置安全提示词约束 agent 不把工具输出当指令。
|
|
171
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
|
+
|
|
172
215
|
## License
|
|
173
216
|
|
|
174
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
|