weacpx 0.3.1 → 0.4.0-beta.0

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.
Files changed (67) hide show
  1. package/README.md +109 -26
  2. package/config.example.json +8 -1
  3. package/dist/bridge/bridge-main.js +188 -7
  4. package/dist/channels/channel-scope.d.ts +9 -0
  5. package/dist/channels/cli/provider.d.ts +73 -0
  6. package/dist/channels/cli/registry.d.ts +7 -0
  7. package/dist/channels/cli/weixin-provider.d.ts +2 -0
  8. package/dist/channels/create-channel.d.ts +16 -0
  9. package/dist/channels/media-store.d.ts +29 -0
  10. package/dist/channels/media-types.d.ts +28 -0
  11. package/dist/channels/outbound-media-safety.d.ts +7 -0
  12. package/dist/channels/plugin.d.ts +9 -0
  13. package/dist/channels/types.d.ts +61 -0
  14. package/dist/channels/weixin-channel.d.ts +22 -0
  15. package/dist/cli.js +14701 -8461
  16. package/dist/config/types.d.ts +64 -0
  17. package/dist/logging/app-logger.d.ts +23 -0
  18. package/dist/orchestration/orchestration-types.d.ts +156 -0
  19. package/dist/plugin-api.d.ts +8 -0
  20. package/dist/plugin-api.js +180 -0
  21. package/dist/plugins/compatibility.d.ts +16 -0
  22. package/dist/plugins/known-plugins.d.ts +9 -0
  23. package/dist/plugins/types.d.ts +18 -0
  24. package/dist/version.d.ts +1 -0
  25. package/dist/weixin/agent/interface.d.ts +54 -0
  26. package/dist/weixin/api/api.d.ts +48 -0
  27. package/dist/weixin/api/config-cache.d.ts +18 -0
  28. package/dist/weixin/api/session-guard.d.ts +15 -0
  29. package/dist/weixin/api/types.d.ts +201 -0
  30. package/dist/weixin/auth/accounts.d.ts +63 -0
  31. package/dist/weixin/auth/login-qr.d.ts +31 -0
  32. package/dist/weixin/bot.d.ts +54 -0
  33. package/dist/weixin/cdn/aes-ecb.d.ts +6 -0
  34. package/dist/weixin/cdn/cdn-upload.d.ts +17 -0
  35. package/dist/weixin/cdn/cdn-url.d.ts +11 -0
  36. package/dist/weixin/cdn/pic-decrypt.d.ts +9 -0
  37. package/dist/weixin/cdn/upload.d.ts +42 -0
  38. package/dist/weixin/index.d.ts +6 -0
  39. package/dist/weixin/media/media-download.d.ts +18 -0
  40. package/dist/weixin/media/mime.d.ts +6 -0
  41. package/dist/weixin/media/silk-transcode.d.ts +8 -0
  42. package/dist/weixin/messaging/conversation-executor.d.ts +7 -0
  43. package/dist/weixin/messaging/debug-mode.d.ts +9 -0
  44. package/dist/weixin/messaging/deliver-coordinator-message.d.ts +22 -0
  45. package/dist/weixin/messaging/deliver-orchestration-task-notice.d.ts +18 -0
  46. package/dist/weixin/messaging/deliver-orchestration-task-progress.d.ts +16 -0
  47. package/dist/weixin/messaging/error-notice.d.ts +13 -0
  48. package/dist/weixin/messaging/execute-chat-turn.d.ts +12 -0
  49. package/dist/weixin/messaging/final-heads-up.d.ts +5 -0
  50. package/dist/weixin/messaging/handle-weixin-message-turn.d.ts +30 -0
  51. package/dist/weixin/messaging/inbound.d.ts +63 -0
  52. package/dist/weixin/messaging/orchestration-notice-accounts.d.ts +2 -0
  53. package/dist/weixin/messaging/quota-errors.d.ts +8 -0
  54. package/dist/weixin/messaging/quota-manager.d.ts +44 -0
  55. package/dist/weixin/messaging/send-errors.d.ts +39 -0
  56. package/dist/weixin/messaging/send-media.d.ts +23 -0
  57. package/dist/weixin/messaging/send-orchestration-notice.d.ts +10 -0
  58. package/dist/weixin/messaging/send.d.ts +71 -0
  59. package/dist/weixin/messaging/slash-commands.d.ts +40 -0
  60. package/dist/weixin/monitor/consumer-lock.d.ts +24 -0
  61. package/dist/weixin/monitor/monitor.d.ts +28 -0
  62. package/dist/weixin/storage/state-dir.d.ts +2 -0
  63. package/dist/weixin/storage/sync-buf.d.ts +20 -0
  64. package/dist/weixin/util/logger.d.ts +14 -0
  65. package/dist/weixin/util/random.d.ts +10 -0
  66. package/dist/weixin/util/redact.d.ts +21 -0
  67. package/package.json +41 -17
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # weacpx
2
2
 
3
- > 用微信远程驱动 Codex、Claude Code 等 acpx 会话。
3
+ > 用微信、飞书或元宝远程驱动 Codex、Claude Code 等 acpx 会话。
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/weacpx?style=flat-square)](https://www.npmjs.com/package/weacpx)
6
6
  [![Node.js Version](https://img.shields.io/node/v/weacpx?style=flat-square)](https://nodejs.org)
@@ -11,21 +11,21 @@
11
11
 
12
12
  ## 这是什么
13
13
 
14
- `weacpx` 是一个微信控制台。它把微信消息接到 `acpx` 会话上,让你直接在手机里:
14
+ `weacpx` 是一个可以通过微信、飞书或元宝直接控制 Codex / Claude Code / Gemini / OpenCode 的工具。它把聊天消息通过 `acpx` 连接到 Agent CLI 会话上,让你直接在手机里:
15
15
 
16
- - 新建和切换 Codex / Claude Code 会话
16
+ - 新建和切换会话
17
17
  - 让 Agent 继续在指定项目目录里工作
18
18
  - 查看流式回复、最终结果和工具调用摘要
19
19
  - 调整权限策略
20
- - 在需要时做最小可用的多 Agent 编排
20
+ - 在需要时做多 Agent 编排
21
21
 
22
- 如果你已经习惯在本地终端里用 `acpx`,`weacpx` 提供的是一个**远程入口**,而不是另一套全新的工作流。
22
+ 如果你需要临时远程编码或办公,`weacpx` 提供的是一个方便快捷的**远程入口**,让你在微信或飞书里就能随时随地干活。
23
23
 
24
24
  ## 适合谁
25
25
 
26
- `weacpx` 适合已经在用 Codex、Claude Code 或其他 `acpx` driver 的开发者。你可以用微信盯任务、发指令、看结果,并在同一个聊天里管理多个会话。
26
+ `weacpx` 适合轻量临时使用多 Agent 办公的用户。你可以用微信、飞书或元宝盯任务、发指令、看结果,并在同一个聊天里管理多个会话。
27
27
 
28
- 它不是云端 IDE,也不是可视化编排平台。
28
+ > `weacpx` 的会话是跟本地隔离的,它目前还不能使用 CLI 已有的会话,你在 weacpx 也无法看到本地的 CLI 会话记录。
29
29
 
30
30
  ## 5 分钟快速开始
31
31
 
@@ -34,15 +34,15 @@
34
34
  开始前,你至少需要:
35
35
 
36
36
  - Node.js 22+ 或 Bun
37
- - 已可用的 CodexClaude Code 或其他 `acpx` driver
38
- - 一台装了微信的手机
37
+ - 已可用的 Codex / Claude Code / Gemini / OpenCode
38
+ - 一台装了微信、飞书或元宝的手机
39
39
 
40
- > `weacpx` 基于 `weixin-agent-sdk` `acpx` 工作。正常情况下,你不需要额外全局安装 `acpx`。
40
+ > 微信频道基于 `weixin-agent-sdk` 工作,飞书频道使用飞书自建应用凭据,元宝频道使用 `appKey` / `appSecret`;底层 Agent 会话由 `acpx` 驱动。正常情况下,你不需要额外全局安装 `acpx`。
41
41
 
42
42
  ### 安装
43
43
 
44
44
  ```bash
45
- npm install -g weacpx
45
+ npm install -g weacpx --registry=https://registry.npmjs.org
46
46
  # 或
47
47
  bun add -g weacpx
48
48
  ```
@@ -53,7 +53,9 @@ bun add -g weacpx
53
53
  weacpx login
54
54
  ```
55
55
 
56
- 终端会显示二维码。你用微信扫码登录。
56
+ 终端会显示二维码,请继续用微信扫码登录。
57
+
58
+ 如果你想使用飞书或元宝而不是微信,请先看下面的“切换/添加其它频道”。
57
59
 
58
60
  ### 启动服务
59
61
 
@@ -78,6 +80,29 @@ hello
78
80
 
79
81
  如果一切正常,普通文本会进入当前会话,Agent 的回复会回到微信。
80
82
 
83
+ ### 切换/添加其它频道
84
+
85
+ 微信是内置默认频道。飞书和元宝以官方插件包分发,第三方频道也走同样的插件流程。如果记不住包名,先看一眼官方插件清单:
86
+
87
+ ```bash
88
+ weacpx plugin known
89
+ # 安装:weacpx plugin add <package>
90
+ ```
91
+
92
+ ```bash
93
+ # 飞书
94
+ weacpx plugin add @ganglion/weacpx-channel-feishu
95
+ weacpx channel add feishu # 按提示输入 appId/appSecret
96
+ weacpx restart
97
+
98
+ # 元宝
99
+ weacpx plugin add @ganglion/weacpx-channel-yuanbao
100
+ weacpx channel add yuanbao # 按提示输入 appKey/appSecret
101
+ weacpx restart
102
+ ```
103
+
104
+ 完整的密钥配置、参数、`enable/disable/rm` 等管理命令见 [docs/channel-management.md](./docs/channel-management.md)。如果你想自己写一个频道插件,见 [docs/plugin-development.md](./docs/plugin-development.md)。
105
+
81
106
  ## 你的日常使用流程
82
107
 
83
108
  最常见的使用顺序只有四步:
@@ -85,7 +110,7 @@ hello
85
110
  1. **启动后台服务**:`weacpx start`
86
111
  2. **创建或切换会话**:`/ss ...`、`/use ...`
87
112
  3. **直接发普通文本**:让当前会话继续工作
88
- 4. **必要时查看状态或取消**:`/status`、`/cancel`
113
+ 4. **必要时查看状态或取消当前任务**:`/status`、`/cancel`
89
114
 
90
115
  ### 1) 创建会话
91
116
 
@@ -95,7 +120,7 @@ hello
95
120
  /ss codex -d /absolute/path/to/your/repo
96
121
  ```
97
122
 
98
- 它会使用 `codex`,绑定这个目录,并自动切换到新会话。
123
+ 它会使用 `codex`,绑定这个工作目录,并自动切换到新会话。
99
124
 
100
125
  ### 2) 发普通消息
101
126
 
@@ -109,9 +134,9 @@ hello
109
134
 
110
135
  `weacpx` 支持三种常用回复模式:
111
136
 
112
- - `stream`:默认,流式返回中间文本
137
+ - `stream`:流式返回中间文本
113
138
  - `final`:只返回最终结果
114
- - `verbose`:在流式文本之外,额外显示工具调用摘要
139
+ - `verbose`:默认,在流式文本之外,额外显示工具调用摘要
115
140
 
116
141
  例如 `verbose` 模式下,你会看到:
117
142
 
@@ -143,6 +168,11 @@ hello
143
168
  | `weacpx start` | 后台启动服务 |
144
169
  | `weacpx status` | 查看后台状态、PID、配置路径、日志路径 |
145
170
  | `weacpx stop` | 停止后台实例 |
171
+ | `weacpx restart` | 重启后台实例,让频道配置变更生效 |
172
+ | `weacpx channel list` | 查看已配置的消息频道 |
173
+ | `weacpx plugin known` | 查看官方插件清单(飞书/元宝包名) |
174
+ | `weacpx plugin add @ganglion/weacpx-channel-feishu && weacpx channel add feishu` | 安装并添加飞书频道,会提示输入飞书应用凭据 |
175
+ | `weacpx plugin add @ganglion/weacpx-channel-yuanbao && weacpx channel add yuanbao` | 安装并添加元宝频道,会提示输入元宝 appKey/appSecret |
146
176
  | `weacpx doctor` | 运行环境诊断 |
147
177
  | `weacpx version` | 查看当前版本 |
148
178
  | `weacpx workspace list` | 查看本机已注册的 workspace |
@@ -206,19 +236,21 @@ weacpx doctor --smoke --agent codex --workspace backend
206
236
  - `--agent` / `--workspace` 只影响 `--smoke`
207
237
  - 如果不传 `--smoke`,相关检查会显示为 `SKIP`
208
238
 
209
- ## 常用微信命令
239
+ ## 常用聊天命令
210
240
 
211
- 下面这部分保留一份**中等长度**的日常手册。够你上手和日常使用,但不把 README 写成完整参考手册。
212
-
213
- 完整微信命令参考见:[docs/commands.md](./docs/commands.md)。
241
+ 这些命令在微信或飞书聊天里发送。完整命令参考见:[docs/commands.md](./docs/commands.md)。
214
242
 
215
243
  ### Agent 管理
216
244
 
245
+ 已内置常用的 Codex 与 Claude Code;
246
+
247
+ 可以使用 `/agent add opencode` 添加你所需要的 agents。
248
+
217
249
  | 命令 | 说明 |
218
250
  |------|------|
219
251
  | `/agents` | 查看 agent 列表 |
220
- | `/agent add codex` | 添加 `codex` |
221
- | `/agent add claude` | 添加 `claude` |
252
+ | `/agent add gemini` | 添加 `Gemini` Agent |
253
+ | `/agent add opencode` | 添加 `OpenCode` Agent |
222
254
  | `/agent rm <name>` | 删除 agent |
223
255
 
224
256
  ### Workspace 管理
@@ -226,7 +258,7 @@ weacpx doctor --smoke --agent codex --workspace backend
226
258
  | 命令 | 说明 |
227
259
  |------|------|
228
260
  | `/workspaces` / `/workspace` / `/ws` | 查看 workspace 列表 |
229
- | `/ws new <name> -d <path>` | 添加 workspace,`path` 是电脑上的绝对路径 |
261
+ | `/ws new <name> -d <path>` | 添加 workspace,`path` 是电脑上的绝对路径,Windows 不用区分正反斜杠 |
230
262
  | `/workspace rm <name>` | 删除 workspace |
231
263
 
232
264
  ### Session 会话
@@ -246,7 +278,7 @@ weacpx doctor --smoke --agent codex --workspace backend
246
278
  | `/replymode reset` | 回退到全局默认 reply mode |
247
279
  | `/session reset` | 重置当前会话上下文 |
248
280
  | `/clear` | `/session reset` 的快捷别名 |
249
- | `/cancel` / `/stop` | 取消当前会话 |
281
+ | `/cancel` / `/stop` | 停止当前任务 |
250
282
 
251
283
  建议你优先记住这三个:
252
284
 
@@ -260,7 +292,7 @@ weacpx doctor --smoke --agent codex --workspace backend
260
292
 
261
293
  | 命令 | 说明 |
262
294
  |------|------|
263
- | `/config` | 查看支持通过微信修改的配置路径 |
295
+ | `/config` | 查看支持通过聊天命令修改的配置路径 |
264
296
  | `/config set <path> <value>` | 修改一个白名单配置项 |
265
297
  | `/pm` / `/permission` | 查看当前权限模式 |
266
298
  | `/pm set allow` | 切到 `approve-all` |
@@ -316,6 +348,55 @@ README 里只保留用户视角的最常用命令。
316
348
 
317
349
  - [docs/weacpx-group-usage-guide.md](./docs/weacpx-group-usage-guide.md)
318
350
 
351
+
352
+ ### MCP 集成:外部 coordinator
353
+
354
+ 如果你想让 Codex、Claude Code 等外部 MCP host 直接使用 weacpx 的多 Agent 编排能力,可以把 `weacpx mcp-stdio` 配成一个 stdio MCP server。
355
+
356
+ 先启动 daemon:
357
+
358
+ ```bash
359
+ weacpx start
360
+ ```
361
+
362
+ MCP 配置推荐保持简单,不要在启动参数里绑定 workspace:
363
+
364
+ ```json
365
+ {
366
+ "mcpServers": {
367
+ "weacpx": {
368
+ "command": "weacpx",
369
+ "args": ["mcp-stdio"]
370
+ }
371
+ }
372
+ }
373
+ ```
374
+
375
+ 外部 host 调用 `delegate_request` 时传 `workingDirectory`,weacpx 会让被委派的 worker 在这个目录工作:
376
+
377
+ ```json
378
+ {
379
+ "targetAgent": "claude",
380
+ "task": "审查这个改动的风险点",
381
+ "workingDirectory": "/absolute/path/to/your/repo"
382
+ }
383
+ ```
384
+
385
+ Windows 上如果 MCP host 不会帮你解析带参数的 `command`,把 `node.exe` 放在 `command`,把 weacpx 脚本和参数放在 `args`:
386
+
387
+ ```json
388
+ {
389
+ "type": "stdio",
390
+ "command": "D:\\Users\\you\\.nvmd\\versions\\22.19.0\\node.exe",
391
+ "args": [
392
+ "E:\\projects\\weacpx\\dist\\cli.js",
393
+ "mcp-stdio"
394
+ ]
395
+ }
396
+ ```
397
+
398
+ 更多身份规则、`workingDirectory` 语义、工具列表、流程图和故障排查见:[docs/external-mcp.md](./docs/external-mcp.md)。
399
+
319
400
  ## 常见场景
320
401
 
321
402
  ### 在手机上继续盯一个本地项目
@@ -395,12 +476,14 @@ bun run dev
395
476
 
396
477
  ### 安装与配置
397
478
 
479
+ - 想配置微信、飞书、元宝、或第三方插件频道:[docs/channel-management.md](./docs/channel-management.md)
480
+ - 想自己写一个频道插件:[docs/plugin-development.md](./docs/plugin-development.md)
398
481
  - 想看完整配置字段:[docs/config-reference.md](./docs/config-reference.md)
399
482
  - 想在微信里改配置:[docs/config-command.md](./docs/config-command.md)
400
483
 
401
484
  ### 日常使用
402
485
 
403
- - 想查看完整微信命令参考:[docs/commands.md](./docs/commands.md)
486
+ - 想查看完整聊天命令参考:[docs/commands.md](./docs/commands.md)
404
487
  - 想理解什么时候该用 delegate、什么时候该开 group:[docs/weacpx-group-usage-guide.md](./docs/weacpx-group-usage-guide.md)
405
488
 
406
489
  ### 排错与验证
@@ -11,9 +11,16 @@
11
11
  "maxFiles": 5,
12
12
  "retentionDays": 7
13
13
  },
14
- "wechat": {
14
+ "channel": {
15
15
  "replyMode": "stream"
16
16
  },
17
+ "channels": [
18
+ {
19
+ "id": "weixin",
20
+ "type": "weixin",
21
+ "enabled": true
22
+ }
23
+ ],
17
24
  "agents": {
18
25
  "codex": {
19
26
  "driver": "codex"
@@ -212,6 +212,137 @@ var init_spawn_command = __esm(() => {
212
212
  SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
213
213
  });
214
214
 
215
+ // src/transport/prompt-media.ts
216
+ import { mkdtemp, open, rm, writeFile } from "node:fs/promises";
217
+ import { tmpdir as defaultTmpdir } from "node:os";
218
+ import path from "node:path";
219
+ import { pathToFileURL } from "node:url";
220
+ async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
221
+ const mediaList = normalizePromptMedia(media);
222
+ if (mediaList.length === 0) {
223
+ return null;
224
+ }
225
+ const blocks = [];
226
+ if (text.trim().length > 0) {
227
+ blocks.push({ type: "text", text });
228
+ }
229
+ const nonImages = mediaList.filter((item) => item.type !== "image");
230
+ if (nonImages.length > 0) {
231
+ blocks.push({ type: "text", text: buildAttachmentSummary(nonImages) });
232
+ }
233
+ for (const item of mediaList) {
234
+ if (item.type === "image") {
235
+ const imageData = await deps.readImageFile(item.filePath, MAX_STRUCTURED_IMAGE_BYTES);
236
+ if (imageData.byteLength === 0)
237
+ throw new Error("image prompt must not be empty");
238
+ if (imageData.byteLength > MAX_STRUCTURED_IMAGE_BYTES) {
239
+ throw new Error(`image prompt exceeds ${MAX_STRUCTURED_IMAGE_BYTES} bytes`);
240
+ }
241
+ blocks.push({
242
+ type: "image",
243
+ mimeType: resolveImageMimeType(imageData, item.mimeType),
244
+ data: imageData.toString("base64")
245
+ });
246
+ continue;
247
+ }
248
+ blocks.push({
249
+ type: "resource",
250
+ resource: {
251
+ uri: pathToFileURL(item.filePath).toString(),
252
+ text: `${item.fileName ?? path.basename(item.filePath)} ${item.mimeType} ${item.type}`
253
+ }
254
+ });
255
+ }
256
+ return await writeStructuredPromptBlocks(blocks, deps);
257
+ }
258
+ function normalizePromptMedia(media) {
259
+ if (!media)
260
+ return [];
261
+ return Array.isArray(media) ? media : [media];
262
+ }
263
+ function buildAttachmentSummary(items) {
264
+ const lines = ["Attachments available as local files:"];
265
+ for (const [index, item] of items.entries()) {
266
+ lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
267
+ }
268
+ return lines.join(`
269
+ `);
270
+ }
271
+ async function writeStructuredPromptBlocks(blocks, deps) {
272
+ let dir = "";
273
+ try {
274
+ dir = await deps.mkdtemp(path.join(deps.tmpdir(), "weacpx-acp-prompt-"));
275
+ const filePath = path.join(dir, "prompt.json");
276
+ await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
277
+ return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
278
+ } catch (error) {
279
+ if (dir)
280
+ await deps.rm(dir, { recursive: true, force: true }).catch(() => {});
281
+ throw error;
282
+ }
283
+ }
284
+ async function readImageFileBounded(filePath, maxBytes) {
285
+ const handle = await open(filePath, "r");
286
+ try {
287
+ const imageStats = await handle.stat();
288
+ if (!imageStats.isFile()) {
289
+ throw new Error("image prompt path must be a regular file");
290
+ }
291
+ if (imageStats.size > maxBytes) {
292
+ throw new Error(`image prompt exceeds ${maxBytes} bytes`);
293
+ }
294
+ const chunks = [];
295
+ let total = 0;
296
+ let position = 0;
297
+ const chunkSize = 1024 * 1024;
298
+ while (total <= maxBytes) {
299
+ const buffer = Buffer.allocUnsafe(Math.min(chunkSize, maxBytes + 1 - total));
300
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
301
+ if (bytesRead === 0)
302
+ break;
303
+ chunks.push(buffer.subarray(0, bytesRead));
304
+ total += bytesRead;
305
+ position += bytesRead;
306
+ }
307
+ return Buffer.concat(chunks, total);
308
+ } finally {
309
+ await handle.close();
310
+ }
311
+ }
312
+ function resolveImageMimeType(buffer, declaredMimeType) {
313
+ if (/^image\/[A-Za-z0-9.+-]+$/.test(declaredMimeType) && declaredMimeType !== "image/*") {
314
+ return declaredMimeType;
315
+ }
316
+ if (buffer.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) {
317
+ return "image/png";
318
+ }
319
+ if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
320
+ return "image/jpeg";
321
+ }
322
+ const header6 = buffer.subarray(0, 6).toString("ascii");
323
+ if (header6 === "GIF87a" || header6 === "GIF89a") {
324
+ return "image/gif";
325
+ }
326
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
327
+ return "image/webp";
328
+ }
329
+ if (buffer.length >= 2 && buffer.subarray(0, 2).toString("ascii") === "BM") {
330
+ return "image/bmp";
331
+ }
332
+ return "image/png";
333
+ }
334
+ var MAX_STRUCTURED_IMAGE_BYTES, defaultStructuredPromptFileDeps;
335
+ var init_prompt_media = __esm(() => {
336
+ MAX_STRUCTURED_IMAGE_BYTES = 100 * 1024 * 1024;
337
+ defaultStructuredPromptFileDeps = {
338
+ readImageFile: readImageFileBounded,
339
+ mkdtemp,
340
+ writeFile,
341
+ rm,
342
+ tmpdir: defaultTmpdir
343
+ };
344
+ });
345
+
215
346
  // src/transport/streaming-prompt.ts
216
347
  function createStreamingPromptState(formatToolCalls = false) {
217
348
  return {
@@ -417,9 +548,9 @@ function isUnder(child, parent) {
417
548
  const p = parent.replace(/[\\/]+$/, "");
418
549
  return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
419
550
  }
420
- async function defaultFsExists(path) {
551
+ async function defaultFsExists(path2) {
421
552
  try {
422
- await access(path);
553
+ await access(path2);
423
554
  return true;
424
555
  } catch {
425
556
  return false;
@@ -787,6 +918,7 @@ class BridgeRequestScheduler {
787
918
  // src/bridge/bridge-runtime.ts
788
919
  init_spawn_command();
789
920
  init_prompt_output();
921
+ init_prompt_media();
790
922
  init_streaming_prompt();
791
923
  import { copyFile, readdir } from "node:fs/promises";
792
924
  import { homedir as homedir3 } from "node:os";
@@ -933,15 +1065,22 @@ class BridgeRuntime {
933
1065
  }
934
1066
  async prompt(input, onEvent) {
935
1067
  await this.launchMcpQueueOwnerIfNeeded(input);
1068
+ const structuredPrompt = await createStructuredPromptFile(input.text, input.media);
936
1069
  const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
937
1070
  "prompt",
938
1071
  "-s",
939
1072
  input.name,
940
- input.text
1073
+ ...structuredPrompt ? ["--file", structuredPrompt.filePath] : [input.text]
941
1074
  ]));
942
1075
  const formatToolCalls = input.replyMode === "verbose";
943
- const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent, { formatToolCalls }) : await this.run(spawnSpec.command, spawnSpec.args);
944
- return { text: getPromptText(result) };
1076
+ try {
1077
+ const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent, { formatToolCalls }) : await this.run(spawnSpec.command, spawnSpec.args);
1078
+ return { text: getPromptText(result) };
1079
+ } finally {
1080
+ try {
1081
+ await structuredPrompt?.cleanup();
1082
+ } catch {}
1083
+ }
945
1084
  }
946
1085
  async launchMcpQueueOwnerIfNeeded(input) {
947
1086
  if (!input.mcpCoordinatorSession) {
@@ -1335,6 +1474,7 @@ class BridgeServer {
1335
1474
  }
1336
1475
  });
1337
1476
  case "prompt":
1477
+ const media = asOptionalPromptMediaInput(params.media);
1338
1478
  return await this.runtime.prompt({
1339
1479
  agent: requireString(params, "agent"),
1340
1480
  agentCommand: asOptionalString(params.agentCommand),
@@ -1342,8 +1482,9 @@ class BridgeServer {
1342
1482
  name: requireString(params, "name"),
1343
1483
  mcpCoordinatorSession: asOptionalString(params.mcpCoordinatorSession),
1344
1484
  mcpSourceHandle: asOptionalString(params.mcpSourceHandle),
1345
- text: requireString(params, "text"),
1346
- replyMode: asOptionalReplyMode(params.replyMode)
1485
+ text: requirePromptText(params, media),
1486
+ replyMode: asOptionalReplyMode(params.replyMode),
1487
+ media
1347
1488
  }, (event) => {
1348
1489
  if (event.type === "prompt.segment") {
1349
1490
  writeLine?.(encodeBridgePromptSegmentEvent({
@@ -1449,6 +1590,17 @@ function requireString(params, key) {
1449
1590
  }
1450
1591
  return value;
1451
1592
  }
1593
+ function requirePromptText(params, media) {
1594
+ const value = params.text;
1595
+ if (typeof value !== "string") {
1596
+ throw new BridgeInvalidRequestError("text must be a non-empty string");
1597
+ }
1598
+ const hasMedia = Array.isArray(media) ? media.length > 0 : Boolean(media);
1599
+ if (value.length === 0 && !hasMedia) {
1600
+ throw new BridgeInvalidRequestError("text must be a non-empty string unless media is provided");
1601
+ }
1602
+ return value;
1603
+ }
1452
1604
  function requirePermissionMode(params, key) {
1453
1605
  const value = params[key];
1454
1606
  if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
@@ -1469,6 +1621,35 @@ function asOptionalString(value) {
1469
1621
  }
1470
1622
  return value;
1471
1623
  }
1624
+ function asOptionalPromptMediaInput(value) {
1625
+ if (value === undefined)
1626
+ return;
1627
+ if (Array.isArray(value))
1628
+ return value.map(asPromptMedia);
1629
+ return asPromptMedia(value);
1630
+ }
1631
+ function asPromptMedia(value) {
1632
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1633
+ throw new BridgeInvalidRequestError("media must be an object or array of objects when provided");
1634
+ }
1635
+ const record = value;
1636
+ const type = record.type;
1637
+ if (type !== "image" && type !== "audio" && type !== "video" && type !== "file") {
1638
+ throw new BridgeInvalidRequestError("media.type must be image, audio, video, or file");
1639
+ }
1640
+ if (typeof record.filePath !== "string" || record.filePath.trim().length === 0) {
1641
+ throw new BridgeInvalidRequestError("media.filePath must be a non-empty string");
1642
+ }
1643
+ if (typeof record.mimeType !== "string" || record.mimeType.trim().length === 0) {
1644
+ throw new BridgeInvalidRequestError("media.mimeType must be a non-empty string");
1645
+ }
1646
+ return {
1647
+ type,
1648
+ filePath: record.filePath,
1649
+ mimeType: record.mimeType,
1650
+ ...typeof record.fileName === "string" && record.fileName ? { fileName: record.fileName } : {}
1651
+ };
1652
+ }
1472
1653
  var VALID_REPLY_MODES = new Set(["stream", "final", "verbose"]);
1473
1654
  function asOptionalReplyMode(value) {
1474
1655
  if (typeof value !== "string" || !VALID_REPLY_MODES.has(value)) {
@@ -0,0 +1,9 @@
1
+ export declare function registerKnownChannelId(channelId: string): void;
2
+ export declare function listKnownChannelIds(): string[];
3
+ export declare function getChannelIdFromChatKey(chatKey: string): string;
4
+ export declare function isLegacyWeixinChatKey(chatKey: string): boolean;
5
+ export declare function toInternalSessionAlias(channelId: string, displayAlias: string): string;
6
+ export declare function toDisplaySessionAlias(internalAlias: string): string;
7
+ export declare function isSessionAliasVisibleInChannel(alias: string, channelId: string): boolean;
8
+ export declare function resolveSessionAliasForInput(channelId: string, displayAlias: string, existingAliases: Iterable<string>): string;
9
+ export declare function buildDefaultTransportSession(channelId: string, displayAlias: string): string;
@@ -0,0 +1,73 @@
1
+ import type { ChannelRuntimeConfig } from "../../config/types";
2
+ export type ChannelCliInput = Record<string, string | boolean | undefined>;
3
+ export type ChannelCliParseResult = {
4
+ ok: true;
5
+ input: ChannelCliInput;
6
+ } | {
7
+ ok: false;
8
+ message: string;
9
+ };
10
+ export type ChannelCliValidationIssue = {
11
+ kind: "missing-required-field";
12
+ flag: string;
13
+ message: string;
14
+ } | {
15
+ kind: "invalid-config";
16
+ message: string;
17
+ };
18
+ export interface ChannelCliIo {
19
+ print: (line: string) => void;
20
+ stderr: (text: string) => void;
21
+ isInteractive: () => boolean;
22
+ promptText: (message: string) => Promise<string>;
23
+ promptSecret: (message: string) => Promise<string>;
24
+ }
25
+ export interface ChannelCliProvider {
26
+ type: string;
27
+ displayName: string;
28
+ supportsLogin: boolean;
29
+ parseAddArgs(args: string[]): ChannelCliParseResult;
30
+ buildDefaultConfig(input: ChannelCliInput): ChannelRuntimeConfig;
31
+ validateConfig(config: ChannelRuntimeConfig): ChannelCliValidationIssue[];
32
+ renderSummary(config: ChannelRuntimeConfig): string[];
33
+ promptForMissingFields(input: ChannelCliInput, io: ChannelCliIo): Promise<ChannelCliInput>;
34
+ /**
35
+ * Optional: declares this plugin supports the `weacpx channel ... --account <id>`
36
+ * multi-bot CLI surface. Plugins that opt in must also implement
37
+ * {@link buildAccountOverride} and {@link channelLevelOptionKeys}.
38
+ */
39
+ supportsMultipleAccounts?: boolean;
40
+ /**
41
+ * Optional: builds the per-account override object that core nests under
42
+ * `options.accounts.<accountId>`. Should NOT include channel-level fields
43
+ * (those live on top-level `options`).
44
+ */
45
+ buildAccountOverride?(input: ChannelCliInput): Record<string, unknown>;
46
+ /**
47
+ * Optional: option keys that stay on top-level `options` (not duplicated into
48
+ * each account). Used to migrate a legacy flat single-bot config into the
49
+ * accounts shape on first `--account` add.
50
+ */
51
+ channelLevelOptionKeys?: readonly string[];
52
+ /**
53
+ * Optional: renders summary lines for a single account inside a multi-bot
54
+ * channel. Falls back to {@link renderSummary} on the whole channel when
55
+ * unspecified.
56
+ */
57
+ renderAccountSummary?(config: ChannelRuntimeConfig, accountId: string): string[] | null;
58
+ }
59
+ export declare function parseBooleanFlag(value: string, flagName: string): {
60
+ ok: true;
61
+ value: boolean;
62
+ } | {
63
+ ok: false;
64
+ message: string;
65
+ };
66
+ export declare function takeFlagValue(args: string[], index: number, flagName: string): {
67
+ ok: true;
68
+ value: string;
69
+ nextIndex: number;
70
+ } | {
71
+ ok: false;
72
+ message: string;
73
+ };
@@ -0,0 +1,7 @@
1
+ import type { ChannelCliProvider } from "./provider";
2
+ export declare function registerChannelCliProvider(provider: ChannelCliProvider): void;
3
+ export declare function hasChannelCliProvider(type: string): boolean;
4
+ export declare function getRegisteredChannelCliProviderTypes(): string[];
5
+ export declare function bootstrapBuiltinChannelCliProviders(): void;
6
+ export declare function listChannelCliProviders(): ChannelCliProvider[];
7
+ export declare function getChannelCliProvider(type: string): ChannelCliProvider | null;
@@ -0,0 +1,2 @@
1
+ import type { ChannelCliProvider } from "./provider";
2
+ export declare const weixinCliProvider: ChannelCliProvider;
@@ -0,0 +1,16 @@
1
+ import type { ChannelConfig, ChannelRuntimeConfig } from "../config/types.js";
2
+ import type { MessageChannelRuntime } from "./types.js";
3
+ import type { RuntimeMediaStore } from "./media-store.js";
4
+ export declare function getMovedChannelInstallHint(type: string): string | null;
5
+ export interface CreateChannelDeps {
6
+ mediaStore?: RuntimeMediaStore;
7
+ allowedMediaRoots?: string[];
8
+ }
9
+ export type ChannelFactory = (options: Record<string, unknown> | undefined, deps?: CreateChannelDeps) => MessageChannelRuntime;
10
+ export declare function registerChannelFactory(type: string, factory: ChannelFactory): void;
11
+ export declare function hasChannelFactory(type: string): boolean;
12
+ export declare function getRegisteredChannelTypes(): string[];
13
+ export declare function bootstrapBuiltinChannelFactories(): void;
14
+ export declare function createMessageChannel(type: string, config?: Partial<ChannelConfig>, deps?: CreateChannelDeps): MessageChannelRuntime;
15
+ export declare function createMessageChannelFromRuntimeConfig(config: ChannelRuntimeConfig, deps?: CreateChannelDeps): MessageChannelRuntime;
16
+ export declare function createMessageChannels(configs: ChannelRuntimeConfig[], deps?: CreateChannelDeps): MessageChannelRuntime[];
@@ -0,0 +1,29 @@
1
+ import type { ChannelId, ChannelMediaAttachment, ChannelMediaKind } from "./media-types";
2
+ export declare const DEFAULT_IMAGE_MAX_BYTES: number;
3
+ export declare const DEFAULT_ATTACHMENT_MAX_BYTES: number;
4
+ export declare const DEFAULT_MAX_ATTACHMENTS_PER_MESSAGE = 10;
5
+ export declare const DEFAULT_MEDIA_RETENTION_MS: number;
6
+ export interface RuntimeMediaStoreOptions {
7
+ rootDir: string;
8
+ retentionMs?: number;
9
+ }
10
+ export interface SaveMediaBufferInput {
11
+ channelId: ChannelId;
12
+ accountId: string;
13
+ chatKey: string;
14
+ messageId: string;
15
+ fileName?: string;
16
+ mimeType: string;
17
+ kind: ChannelMediaKind;
18
+ buffer: Buffer;
19
+ sourceResourceId?: string;
20
+ maxBytes: number;
21
+ }
22
+ export declare class RuntimeMediaStore {
23
+ readonly rootDir: string;
24
+ readonly retentionMs: number;
25
+ constructor(options: RuntimeMediaStoreOptions);
26
+ saveMediaBuffer(input: SaveMediaBufferInput): Promise<ChannelMediaAttachment>;
27
+ cleanupExpired(now?: Date): Promise<void>;
28
+ }
29
+ export declare function sanitizeMediaFileName(fileName: string, mimeType: string): string;