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.
- package/README.md +109 -26
- package/config.example.json +8 -1
- package/dist/bridge/bridge-main.js +188 -7
- package/dist/channels/channel-scope.d.ts +9 -0
- package/dist/channels/cli/provider.d.ts +73 -0
- package/dist/channels/cli/registry.d.ts +7 -0
- package/dist/channels/cli/weixin-provider.d.ts +2 -0
- package/dist/channels/create-channel.d.ts +16 -0
- package/dist/channels/media-store.d.ts +29 -0
- package/dist/channels/media-types.d.ts +28 -0
- package/dist/channels/outbound-media-safety.d.ts +7 -0
- package/dist/channels/plugin.d.ts +9 -0
- package/dist/channels/types.d.ts +61 -0
- package/dist/channels/weixin-channel.d.ts +22 -0
- package/dist/cli.js +14701 -8461
- package/dist/config/types.d.ts +64 -0
- package/dist/logging/app-logger.d.ts +23 -0
- package/dist/orchestration/orchestration-types.d.ts +156 -0
- package/dist/plugin-api.d.ts +8 -0
- package/dist/plugin-api.js +180 -0
- package/dist/plugins/compatibility.d.ts +16 -0
- package/dist/plugins/known-plugins.d.ts +9 -0
- package/dist/plugins/types.d.ts +18 -0
- package/dist/version.d.ts +1 -0
- package/dist/weixin/agent/interface.d.ts +54 -0
- package/dist/weixin/api/api.d.ts +48 -0
- package/dist/weixin/api/config-cache.d.ts +18 -0
- package/dist/weixin/api/session-guard.d.ts +15 -0
- package/dist/weixin/api/types.d.ts +201 -0
- package/dist/weixin/auth/accounts.d.ts +63 -0
- package/dist/weixin/auth/login-qr.d.ts +31 -0
- package/dist/weixin/bot.d.ts +54 -0
- package/dist/weixin/cdn/aes-ecb.d.ts +6 -0
- package/dist/weixin/cdn/cdn-upload.d.ts +17 -0
- package/dist/weixin/cdn/cdn-url.d.ts +11 -0
- package/dist/weixin/cdn/pic-decrypt.d.ts +9 -0
- package/dist/weixin/cdn/upload.d.ts +42 -0
- package/dist/weixin/index.d.ts +6 -0
- package/dist/weixin/media/media-download.d.ts +18 -0
- package/dist/weixin/media/mime.d.ts +6 -0
- package/dist/weixin/media/silk-transcode.d.ts +8 -0
- package/dist/weixin/messaging/conversation-executor.d.ts +7 -0
- package/dist/weixin/messaging/debug-mode.d.ts +9 -0
- package/dist/weixin/messaging/deliver-coordinator-message.d.ts +22 -0
- package/dist/weixin/messaging/deliver-orchestration-task-notice.d.ts +18 -0
- package/dist/weixin/messaging/deliver-orchestration-task-progress.d.ts +16 -0
- package/dist/weixin/messaging/error-notice.d.ts +13 -0
- package/dist/weixin/messaging/execute-chat-turn.d.ts +12 -0
- package/dist/weixin/messaging/final-heads-up.d.ts +5 -0
- package/dist/weixin/messaging/handle-weixin-message-turn.d.ts +30 -0
- package/dist/weixin/messaging/inbound.d.ts +63 -0
- package/dist/weixin/messaging/orchestration-notice-accounts.d.ts +2 -0
- package/dist/weixin/messaging/quota-errors.d.ts +8 -0
- package/dist/weixin/messaging/quota-manager.d.ts +44 -0
- package/dist/weixin/messaging/send-errors.d.ts +39 -0
- package/dist/weixin/messaging/send-media.d.ts +23 -0
- package/dist/weixin/messaging/send-orchestration-notice.d.ts +10 -0
- package/dist/weixin/messaging/send.d.ts +71 -0
- package/dist/weixin/messaging/slash-commands.d.ts +40 -0
- package/dist/weixin/monitor/consumer-lock.d.ts +24 -0
- package/dist/weixin/monitor/monitor.d.ts +28 -0
- package/dist/weixin/storage/state-dir.d.ts +2 -0
- package/dist/weixin/storage/sync-buf.d.ts +20 -0
- package/dist/weixin/util/logger.d.ts +14 -0
- package/dist/weixin/util/random.d.ts +10 -0
- package/dist/weixin/util/redact.d.ts +21 -0
- package/package.json +41 -17
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# weacpx
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 用微信、飞书或元宝远程驱动 Codex、Claude Code 等 acpx 会话。
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/weacpx)
|
|
6
6
|
[](https://nodejs.org)
|
|
@@ -11,21 +11,21 @@
|
|
|
11
11
|
|
|
12
12
|
## 这是什么
|
|
13
13
|
|
|
14
|
-
`weacpx`
|
|
14
|
+
`weacpx` 是一个可以通过微信、飞书或元宝直接控制 Codex / Claude Code / Gemini / OpenCode 的工具。它把聊天消息通过 `acpx` 连接到 Agent CLI 会话上,让你直接在手机里:
|
|
15
15
|
|
|
16
|
-
-
|
|
16
|
+
- 新建和切换会话
|
|
17
17
|
- 让 Agent 继续在指定项目目录里工作
|
|
18
18
|
- 查看流式回复、最终结果和工具调用摘要
|
|
19
19
|
- 调整权限策略
|
|
20
|
-
-
|
|
20
|
+
- 在需要时做多 Agent 编排
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
如果你需要临时远程编码或办公,`weacpx` 提供的是一个方便快捷的**远程入口**,让你在微信或飞书里就能随时随地干活。
|
|
23
23
|
|
|
24
24
|
## 适合谁
|
|
25
25
|
|
|
26
|
-
`weacpx`
|
|
26
|
+
`weacpx` 适合轻量临时使用多 Agent 办公的用户。你可以用微信、飞书或元宝盯任务、发指令、看结果,并在同一个聊天里管理多个会话。
|
|
27
27
|
|
|
28
|
-
|
|
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
|
-
- 已可用的 Codex
|
|
38
|
-
-
|
|
37
|
+
- 已可用的 Codex / Claude Code / Gemini / OpenCode
|
|
38
|
+
- 一台装了微信、飞书或元宝的手机
|
|
39
39
|
|
|
40
|
-
>
|
|
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.
|
|
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
|
-
|
|
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
|
|
221
|
-
| `/agent add
|
|
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
|
-
-
|
|
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
|
### 排错与验证
|
package/config.example.json
CHANGED
|
@@ -11,9 +11,16 @@
|
|
|
11
11
|
"maxFiles": 5,
|
|
12
12
|
"retentionDays": 7
|
|
13
13
|
},
|
|
14
|
-
"
|
|
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(
|
|
551
|
+
async function defaultFsExists(path2) {
|
|
421
552
|
try {
|
|
422
|
-
await access(
|
|
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
|
-
|
|
944
|
-
|
|
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:
|
|
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,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;
|