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.
Files changed (67) hide show
  1. package/README.md +42 -10
  2. package/config.example.json +8 -1
  3. package/dist/bridge/bridge-main.js +68 -43
  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 +6943 -2024
  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 +40 -16
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,7 +11,7 @@
11
11
 
12
12
  ## 这是什么
13
13
 
14
- `weacpx` 是一个可以通过微信直接控制 Codex / Claude Code / Gemini / OpenCode 的工具。它把微信消息通过 `acpx` 连接到 Agent CLI 会话上,让你直接在手机里:
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
- > `weacpx` 基于 `weixin-agent-sdk` `acpx` 工作。正常情况下,你不需要额外全局安装 `acpx`。
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
- 完整微信命令参考见:[docs/commands.md](./docs/commands.md)。
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
- - 想查看完整微信命令参考:[docs/commands.md](./docs/commands.md)
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
  ### 排错与验证
@@ -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"
@@ -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
- if (!media) {
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
- blocks.push({
238
- type: "image",
239
- mimeType: resolveImageMimeType(imageData, media.mimeType),
240
- data: imageData.toString("base64")
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
- try {
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 = asOptionalPromptMedia(params.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
- if (value.length === 0 && media?.type !== "image") {
1577
- throw new BridgeInvalidRequestError("text must be a non-empty string unless image media is provided");
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 asOptionalPromptMedia(value) {
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
- const filePath = record.filePath;
1611
- const mimeType = record.mimeType;
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.length > 0 ? { fileName: 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,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;
@@ -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
+ }