weacpx 0.3.2 → 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 +42 -10
- package/config.example.json +8 -1
- package/dist/bridge/bridge-main.js +68 -43
- 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 +6943 -2024
- 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 +40 -16
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,7 +11,7 @@
|
|
|
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 继续在指定项目目录里工作
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
- 调整权限策略
|
|
20
20
|
- 在需要时做多 Agent 编排
|
|
21
21
|
|
|
22
|
-
如果你需要临时远程编码或办公,`weacpx`
|
|
22
|
+
如果你需要临时远程编码或办公,`weacpx` 提供的是一个方便快捷的**远程入口**,让你在微信或飞书里就能随时随地干活。
|
|
23
23
|
|
|
24
24
|
## 适合谁
|
|
25
25
|
|
|
26
|
-
`weacpx` 适合轻量临时使用多 Agent
|
|
26
|
+
`weacpx` 适合轻量临时使用多 Agent 办公的用户。你可以用微信、飞书或元宝盯任务、发指令、看结果,并在同一个聊天里管理多个会话。
|
|
27
27
|
|
|
28
28
|
> `weacpx` 的会话是跟本地隔离的,它目前还不能使用 CLI 已有的会话,你在 weacpx 也无法看到本地的 CLI 会话记录。
|
|
29
29
|
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
- Node.js 22+ 或 Bun
|
|
37
37
|
- 已可用的 Codex / Claude Code / Gemini / OpenCode
|
|
38
|
-
-
|
|
38
|
+
- 一台装了微信、飞书或元宝的手机
|
|
39
39
|
|
|
40
|
-
>
|
|
40
|
+
> 微信频道基于 `weixin-agent-sdk` 工作,飞书频道使用飞书自建应用凭据,元宝频道使用 `appKey` / `appSecret`;底层 Agent 会话由 `acpx` 驱动。正常情况下,你不需要额外全局安装 `acpx`。
|
|
41
41
|
|
|
42
42
|
### 安装
|
|
43
43
|
|
|
@@ -55,6 +55,8 @@ weacpx login
|
|
|
55
55
|
|
|
56
56
|
终端会显示二维码,请继续用微信扫码登录。
|
|
57
57
|
|
|
58
|
+
如果你想使用飞书或元宝而不是微信,请先看下面的“切换/添加其它频道”。
|
|
59
|
+
|
|
58
60
|
### 启动服务
|
|
59
61
|
|
|
60
62
|
```bash
|
|
@@ -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
|
最常见的使用顺序只有四步:
|
|
@@ -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,9 +236,9 @@ weacpx doctor --smoke --agent codex --workspace backend
|
|
|
206
236
|
- `--agent` / `--workspace` 只影响 `--smoke`
|
|
207
237
|
- 如果不传 `--smoke`,相关检查会显示为 `SKIP`
|
|
208
238
|
|
|
209
|
-
##
|
|
239
|
+
## 常用聊天命令
|
|
210
240
|
|
|
211
|
-
|
|
241
|
+
这些命令在微信或飞书聊天里发送。完整命令参考见:[docs/commands.md](./docs/commands.md)。
|
|
212
242
|
|
|
213
243
|
### Agent 管理
|
|
214
244
|
|
|
@@ -262,7 +292,7 @@ weacpx doctor --smoke --agent codex --workspace backend
|
|
|
262
292
|
|
|
263
293
|
| 命令 | 说明 |
|
|
264
294
|
|------|------|
|
|
265
|
-
| `/config` |
|
|
295
|
+
| `/config` | 查看支持通过聊天命令修改的配置路径 |
|
|
266
296
|
| `/config set <path> <value>` | 修改一个白名单配置项 |
|
|
267
297
|
| `/pm` / `/permission` | 查看当前权限模式 |
|
|
268
298
|
| `/pm set allow` | 切到 `approve-all` |
|
|
@@ -446,12 +476,14 @@ bun run dev
|
|
|
446
476
|
|
|
447
477
|
### 安装与配置
|
|
448
478
|
|
|
479
|
+
- 想配置微信、飞书、元宝、或第三方插件频道:[docs/channel-management.md](./docs/channel-management.md)
|
|
480
|
+
- 想自己写一个频道插件:[docs/plugin-development.md](./docs/plugin-development.md)
|
|
449
481
|
- 想看完整配置字段:[docs/config-reference.md](./docs/config-reference.md)
|
|
450
482
|
- 想在微信里改配置:[docs/config-command.md](./docs/config-command.md)
|
|
451
483
|
|
|
452
484
|
### 日常使用
|
|
453
485
|
|
|
454
|
-
-
|
|
486
|
+
- 想查看完整聊天命令参考:[docs/commands.md](./docs/commands.md)
|
|
455
487
|
- 想理解什么时候该用 delegate、什么时候该开 group:[docs/weacpx-group-usage-guide.md](./docs/weacpx-group-usage-guide.md)
|
|
456
488
|
|
|
457
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"
|
|
@@ -216,46 +216,68 @@ var init_spawn_command = __esm(() => {
|
|
|
216
216
|
import { mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
217
217
|
import { tmpdir as defaultTmpdir } from "node:os";
|
|
218
218
|
import path from "node:path";
|
|
219
|
+
import { pathToFileURL } from "node:url";
|
|
219
220
|
async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
|
|
220
|
-
|
|
221
|
+
const mediaList = normalizePromptMedia(media);
|
|
222
|
+
if (mediaList.length === 0) {
|
|
221
223
|
return null;
|
|
222
224
|
}
|
|
223
|
-
if (media.type !== "image") {
|
|
224
|
-
throw new Error("prompt media type is not supported; only image media is supported");
|
|
225
|
-
}
|
|
226
|
-
const imageData = await deps.readImageFile(media.filePath, MAX_STRUCTURED_IMAGE_BYTES);
|
|
227
|
-
if (imageData.byteLength === 0) {
|
|
228
|
-
throw new Error("image prompt must not be empty");
|
|
229
|
-
}
|
|
230
|
-
if (imageData.byteLength > MAX_STRUCTURED_IMAGE_BYTES) {
|
|
231
|
-
throw new Error(`image prompt exceeds ${MAX_STRUCTURED_IMAGE_BYTES} bytes`);
|
|
232
|
-
}
|
|
233
225
|
const blocks = [];
|
|
234
226
|
if (text.trim().length > 0) {
|
|
235
227
|
blocks.push({ type: "text", text });
|
|
236
228
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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) {
|
|
242
272
|
let dir = "";
|
|
243
273
|
try {
|
|
244
274
|
dir = await deps.mkdtemp(path.join(deps.tmpdir(), "weacpx-acp-prompt-"));
|
|
245
275
|
const filePath = path.join(dir, "prompt.json");
|
|
246
276
|
await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
|
|
247
|
-
return {
|
|
248
|
-
filePath,
|
|
249
|
-
cleanup: async () => {
|
|
250
|
-
await deps.rm(dir, { recursive: true, force: true });
|
|
251
|
-
}
|
|
252
|
-
};
|
|
277
|
+
return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
|
|
253
278
|
} catch (error) {
|
|
254
|
-
if (dir)
|
|
255
|
-
|
|
256
|
-
await deps.rm(dir, { recursive: true, force: true });
|
|
257
|
-
} catch {}
|
|
258
|
-
}
|
|
279
|
+
if (dir)
|
|
280
|
+
await deps.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
259
281
|
throw error;
|
|
260
282
|
}
|
|
261
283
|
}
|
|
@@ -1452,7 +1474,7 @@ class BridgeServer {
|
|
|
1452
1474
|
}
|
|
1453
1475
|
});
|
|
1454
1476
|
case "prompt":
|
|
1455
|
-
const media =
|
|
1477
|
+
const media = asOptionalPromptMediaInput(params.media);
|
|
1456
1478
|
return await this.runtime.prompt({
|
|
1457
1479
|
agent: requireString(params, "agent"),
|
|
1458
1480
|
agentCommand: asOptionalString(params.agentCommand),
|
|
@@ -1573,8 +1595,9 @@ function requirePromptText(params, media) {
|
|
|
1573
1595
|
if (typeof value !== "string") {
|
|
1574
1596
|
throw new BridgeInvalidRequestError("text must be a non-empty string");
|
|
1575
1597
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
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");
|
|
1578
1601
|
}
|
|
1579
1602
|
return value;
|
|
1580
1603
|
}
|
|
@@ -1598,31 +1621,33 @@ function asOptionalString(value) {
|
|
|
1598
1621
|
}
|
|
1599
1622
|
return value;
|
|
1600
1623
|
}
|
|
1601
|
-
function
|
|
1602
|
-
if (value === undefined)
|
|
1624
|
+
function asOptionalPromptMediaInput(value) {
|
|
1625
|
+
if (value === undefined)
|
|
1603
1626
|
return;
|
|
1604
|
-
|
|
1627
|
+
if (Array.isArray(value))
|
|
1628
|
+
return value.map(asPromptMedia);
|
|
1629
|
+
return asPromptMedia(value);
|
|
1630
|
+
}
|
|
1631
|
+
function asPromptMedia(value) {
|
|
1605
1632
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1606
|
-
throw new BridgeInvalidRequestError("media must be an object when provided");
|
|
1633
|
+
throw new BridgeInvalidRequestError("media must be an object or array of objects when provided");
|
|
1607
1634
|
}
|
|
1608
1635
|
const record = value;
|
|
1609
1636
|
const type = record.type;
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
if (type !== "image") {
|
|
1613
|
-
throw new BridgeInvalidRequestError("media.type must be image");
|
|
1637
|
+
if (type !== "image" && type !== "audio" && type !== "video" && type !== "file") {
|
|
1638
|
+
throw new BridgeInvalidRequestError("media.type must be image, audio, video, or file");
|
|
1614
1639
|
}
|
|
1615
|
-
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
1640
|
+
if (typeof record.filePath !== "string" || record.filePath.trim().length === 0) {
|
|
1616
1641
|
throw new BridgeInvalidRequestError("media.filePath must be a non-empty string");
|
|
1617
1642
|
}
|
|
1618
|
-
if (typeof mimeType !== "string" || mimeType.length === 0) {
|
|
1643
|
+
if (typeof record.mimeType !== "string" || record.mimeType.trim().length === 0) {
|
|
1619
1644
|
throw new BridgeInvalidRequestError("media.mimeType must be a non-empty string");
|
|
1620
1645
|
}
|
|
1621
1646
|
return {
|
|
1622
1647
|
type,
|
|
1623
|
-
filePath,
|
|
1624
|
-
mimeType,
|
|
1625
|
-
...typeof record.fileName === "string" && record.fileName
|
|
1648
|
+
filePath: record.filePath,
|
|
1649
|
+
mimeType: record.mimeType,
|
|
1650
|
+
...typeof record.fileName === "string" && record.fileName ? { fileName: record.fileName } : {}
|
|
1626
1651
|
};
|
|
1627
1652
|
}
|
|
1628
1653
|
var VALID_REPLY_MODES = new Set(["stream", "final", "verbose"]);
|
|
@@ -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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ChannelId = string;
|
|
2
|
+
export type ChannelMediaKind = "image" | "file" | "audio" | "video";
|
|
3
|
+
export interface ChannelMediaAttachment {
|
|
4
|
+
kind: ChannelMediaKind;
|
|
5
|
+
/** @deprecated Legacy field — prefer `kind`. Kept for backward compat during migration. */
|
|
6
|
+
type?: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
fileName?: string;
|
|
10
|
+
sizeBytes: number;
|
|
11
|
+
source: {
|
|
12
|
+
channelId: ChannelId;
|
|
13
|
+
accountId: string;
|
|
14
|
+
chatKey: string;
|
|
15
|
+
messageId: string;
|
|
16
|
+
resourceId?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface OutboundChannelMedia {
|
|
20
|
+
kind: ChannelMediaKind;
|
|
21
|
+
filePath: string;
|
|
22
|
+
mimeType?: string;
|
|
23
|
+
fileName?: string;
|
|
24
|
+
caption?: string;
|
|
25
|
+
}
|
|
26
|
+
export type MaybeArray<T> = T | T[] | undefined;
|
|
27
|
+
export declare function normalizeMediaArray<T>(media: MaybeArray<T>): T[];
|
|
28
|
+
export declare function firstMediaOrUndefined<T>(media: T[]): T | undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an outbound media file path to a safe absolute realpath.
|
|
3
|
+
* Rejects remote URLs, non-existent paths, symlinks that escape allowed roots,
|
|
4
|
+
* and paths outside all allowed root directories.
|
|
5
|
+
* Returns the resolved realpath on success, or null on rejection.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveSafeOutboundMediaPath(mediaPath: string, allowedRoots: string[]): Promise<string | null>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChannelFactory } from "./create-channel.js";
|
|
2
|
+
import type { ChannelCliProvider } from "./cli/provider.js";
|
|
3
|
+
export interface ChannelPluginDefinition {
|
|
4
|
+
/** Stable channel type used in channels[].type and chatKey prefix. */
|
|
5
|
+
type: string;
|
|
6
|
+
factory: ChannelFactory;
|
|
7
|
+
cliProvider?: ChannelCliProvider;
|
|
8
|
+
}
|
|
9
|
+
export declare function registerChannelPlugin(plugin: ChannelPluginDefinition): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Agent as ChatAgent } from "../weixin/agent/interface.js";
|
|
2
|
+
import type { OrchestrationTaskRecord } from "../orchestration/orchestration-types.js";
|
|
3
|
+
import type { AppLogger } from "../logging/app-logger.js";
|
|
4
|
+
import type { PendingFinalChunk } from "../weixin/messaging/quota-manager.js";
|
|
5
|
+
export type { ChatAgent };
|
|
6
|
+
export interface OutboundQuota {
|
|
7
|
+
onInbound(chatKey: string): void;
|
|
8
|
+
reserveMidSegment(chatKey: string): boolean;
|
|
9
|
+
reserveFinal(chatKey: string): boolean;
|
|
10
|
+
finalRemaining(chatKey: string): number;
|
|
11
|
+
hasPendingFinal(chatKey: string): boolean;
|
|
12
|
+
drainPendingFinalUpToBudget(chatKey: string, available: number): PendingFinalChunk[];
|
|
13
|
+
prependPendingFinal(chatKey: string, chunks: PendingFinalChunk[]): void;
|
|
14
|
+
enqueuePendingFinal(chatKey: string, chunks: PendingFinalChunk[]): void;
|
|
15
|
+
clearPendingFinal(chatKey: string): void;
|
|
16
|
+
}
|
|
17
|
+
export interface CoordinatorMessageInput {
|
|
18
|
+
coordinatorSession: string;
|
|
19
|
+
chatKey: string;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
replyContextToken?: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ChannelStartInput {
|
|
25
|
+
agent: ChatAgent;
|
|
26
|
+
abortSignal: AbortSignal;
|
|
27
|
+
quota: OutboundQuota;
|
|
28
|
+
logger: AppLogger;
|
|
29
|
+
}
|
|
30
|
+
export interface OrchestrationDeliveryCallbacks {
|
|
31
|
+
markTaskNoticeDelivered: (taskId: string, accountId: string) => Promise<void>;
|
|
32
|
+
markTaskNoticeFailed: (taskId: string, errorMessage: string) => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface ConsumerLockMetadata {
|
|
35
|
+
pid: number;
|
|
36
|
+
mode: "foreground" | "daemon";
|
|
37
|
+
startedAt: string;
|
|
38
|
+
configPath: string;
|
|
39
|
+
statePath: string;
|
|
40
|
+
hostname?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ConsumerLock {
|
|
43
|
+
acquire(meta: ConsumerLockMetadata): Promise<void>;
|
|
44
|
+
release(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export interface ConsumerLockOptions {
|
|
47
|
+
lockFilePath?: string;
|
|
48
|
+
onDiagnostic?: (event: string, context: Record<string, string | number | boolean | undefined>) => void | Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export interface MessageChannelRuntime {
|
|
51
|
+
id: string;
|
|
52
|
+
isLoggedIn(): boolean;
|
|
53
|
+
login(): Promise<string>;
|
|
54
|
+
logout(): void;
|
|
55
|
+
start(input: ChannelStartInput): Promise<void>;
|
|
56
|
+
createConsumerLock?(options?: ConsumerLockOptions): ConsumerLock;
|
|
57
|
+
configureOrchestration?(callbacks: OrchestrationDeliveryCallbacks): void;
|
|
58
|
+
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
|
|
59
|
+
notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
|
|
60
|
+
sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MessageChannelRuntime, ChannelStartInput, CoordinatorMessageInput, OrchestrationDeliveryCallbacks, ConsumerLock, ConsumerLockOptions } from "./types.js";
|
|
2
|
+
import type { RuntimeMediaStore } from "./media-store.js";
|
|
3
|
+
import type { OrchestrationTaskRecord } from "../orchestration/orchestration-types.js";
|
|
4
|
+
export declare class WeixinChannel implements MessageChannelRuntime {
|
|
5
|
+
readonly id = "weixin";
|
|
6
|
+
private quota;
|
|
7
|
+
private logger;
|
|
8
|
+
private markDelivered;
|
|
9
|
+
private markFailed;
|
|
10
|
+
private mediaStore;
|
|
11
|
+
private allowedMediaRoots;
|
|
12
|
+
constructor(mediaStore?: RuntimeMediaStore, allowedMediaRoots?: string[]);
|
|
13
|
+
isLoggedIn(): boolean;
|
|
14
|
+
login(): Promise<string>;
|
|
15
|
+
logout(): void;
|
|
16
|
+
createConsumerLock(options?: ConsumerLockOptions): ConsumerLock;
|
|
17
|
+
configureOrchestration(callbacks: OrchestrationDeliveryCallbacks): void;
|
|
18
|
+
start(input: ChannelStartInput): Promise<void>;
|
|
19
|
+
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
|
|
20
|
+
notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
|
|
21
|
+
sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
|
|
22
|
+
}
|