@ai-setting/roy-agent-core 1.7.76 → 1.7.85

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 (82) hide show
  1. package/dist/config/config-protection.d.ts +89 -0
  2. package/dist/config/index.d.ts +1 -0
  3. package/dist/config/index.js +16 -0
  4. package/dist/env/agent/agent-component.d.ts +42 -21
  5. package/dist/env/agent/agent-component.js +3 -3
  6. package/dist/env/agent/index.js +3 -3
  7. package/dist/env/agent/types.d.ts +32 -12
  8. package/dist/env/error-reply.d.ts +6 -0
  9. package/dist/env/error-reply.js +1 -1
  10. package/dist/env/event-source/bounty-im-handler.d.ts +8 -0
  11. package/dist/env/event-source/es-command.d.ts +39 -0
  12. package/dist/env/event-source/es-plugins.d.ts +68 -0
  13. package/dist/env/event-source/event-source-component.d.ts +15 -0
  14. package/dist/env/event-source/event-source-handlers.d.ts +68 -0
  15. package/dist/env/event-source/event-source-handlers.js +22 -10
  16. package/dist/env/event-source/index.d.ts +5 -1
  17. package/dist/env/event-source/index.js +59 -10
  18. package/dist/env/event-source/types.d.ts +101 -1
  19. package/dist/env/event-source/wecom-card-action-handler.d.ts +141 -0
  20. package/dist/env/event-source/wecom-card-builder.d.ts +220 -0
  21. package/dist/env/event-source/wecom-cli-handler.d.ts +266 -0
  22. package/dist/env/event-source/wecom-cli-sender.d.ts +88 -0
  23. package/dist/env/event-source/wecom-media.d.ts +130 -0
  24. package/dist/env/event-source/wecom-respond-registry.d.ts +151 -0
  25. package/dist/env/event-source/wecom-ws-bridge.d.ts +213 -0
  26. package/dist/env/index.js +19 -17
  27. package/dist/env/task/backend/index.js +37 -1
  28. package/dist/env/task/backend/task-store-backend.d.ts +12 -0
  29. package/dist/env/task/backend/types.d.ts +19 -0
  30. package/dist/env/task/delegate/delegate-tool.d.ts +62 -2
  31. package/dist/env/task/delegate/delegate-tool.js +3 -1
  32. package/dist/env/task/delegate/index.js +1 -1
  33. package/dist/env/task/index.js +4 -4
  34. package/dist/env/task/mcp/index.js +3 -0
  35. package/dist/env/task/mcp/task-mcp-server.d.ts +1 -1
  36. package/dist/env/task/plugins/fake-user-message-helper.d.ts +10 -2
  37. package/dist/env/task/plugins/index.d.ts +3 -0
  38. package/dist/env/task/plugins/index.js +36 -4
  39. package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +41 -2
  40. package/dist/env/task/plugins/lark-card-patch-notifier.js +1 -1
  41. package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +16 -0
  42. package/dist/env/task/plugins/task-op-watermark-plugin.d.ts +112 -0
  43. package/dist/env/task/plugins/wecom-card-state-store.d.ts +121 -0
  44. package/dist/env/task/plugins/wecom-task-notify-plugin.d.ts +224 -0
  45. package/dist/env/task/storage/index.js +2 -2
  46. package/dist/env/task/storage/sqlite-task-store.d.ts +54 -0
  47. package/dist/env/task/task-component.d.ts +16 -0
  48. package/dist/env/task/tools/operation/operation-types.d.ts +13 -13
  49. package/dist/env/tool/built-in/index.d.ts +2 -0
  50. package/dist/env/tool/built-in/index.js +6 -1
  51. package/dist/env/tool/index.js +3 -2
  52. package/dist/env/workflow/index.js +3 -3
  53. package/dist/env/workflow/plugins/index.js +2 -2
  54. package/dist/env/workflow/tools/index.js +2 -2
  55. package/dist/env/workflow/types/event.d.ts +4 -4
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.js +71 -20
  58. package/dist/shared/@ai-setting/{roy-agent-core-v87njmq7.js → roy-agent-core-0dh8npnz.js} +1 -1
  59. package/dist/shared/@ai-setting/roy-agent-core-2stt4rp5.js +134 -0
  60. package/dist/shared/@ai-setting/{roy-agent-core-mww0mnd9.js → roy-agent-core-7bpj859q.js} +1 -1
  61. package/dist/shared/@ai-setting/{roy-agent-core-nmx9934r.js → roy-agent-core-7dd0s3xb.js} +15 -5
  62. package/dist/shared/@ai-setting/{roy-agent-core-ghnagzrh.js → roy-agent-core-b31v3qpv.js} +853 -48
  63. package/dist/shared/@ai-setting/{roy-agent-core-cxy143m4.js → roy-agent-core-begm7ytz.js} +63 -2
  64. package/dist/shared/@ai-setting/{roy-agent-core-aa2j7bk9.js → roy-agent-core-cha0jc7n.js} +40 -26
  65. package/dist/shared/@ai-setting/{roy-agent-core-p3zv1mx5.js → roy-agent-core-d1n4jbza.js} +1 -1
  66. package/dist/shared/@ai-setting/{roy-agent-core-q5r75vev.js → roy-agent-core-dkrvmjsh.js} +17 -75
  67. package/dist/shared/@ai-setting/{roy-agent-core-abqhkbn3.js → roy-agent-core-ezvpreqz.js} +23 -0
  68. package/dist/shared/@ai-setting/{roy-agent-core-99nwgj84.js → roy-agent-core-fzw9xzm1.js} +1 -1
  69. package/dist/shared/@ai-setting/roy-agent-core-h21gngxf.js +247 -0
  70. package/dist/shared/@ai-setting/roy-agent-core-m3vjepzz.js +544 -0
  71. package/dist/shared/@ai-setting/{roy-agent-core-zycadbpx.js → roy-agent-core-p6ks90tb.js} +1 -1
  72. package/dist/shared/@ai-setting/{roy-agent-core-ntq0gz7q.js → roy-agent-core-q813vafd.js} +1 -1
  73. package/dist/shared/@ai-setting/{roy-agent-core-z43aj03k.js → roy-agent-core-qk9hd89p.js} +21 -2
  74. package/dist/shared/@ai-setting/{roy-agent-core-6cy43nss.js → roy-agent-core-rykf7c04.js} +49 -9
  75. package/dist/shared/@ai-setting/{roy-agent-core-8ez6mccb.js → roy-agent-core-tk98f92b.js} +4 -0
  76. package/dist/shared/@ai-setting/{roy-agent-core-bxkjk7zx.js → roy-agent-core-vx8zc847.js} +4 -1
  77. package/dist/shared/@ai-setting/roy-agent-core-vyr6svt6.js +31 -0
  78. package/dist/shared/@ai-setting/{roy-agent-core-t3rrb959.js → roy-agent-core-xf6z96qy.js} +2086 -163
  79. package/package.json +10 -1
  80. package/dist/shared/@ai-setting/roy-agent-core-f77k3gqt.js +0 -71
  81. /package/dist/shared/@ai-setting/{roy-agent-core-b4x6x4k2.js → roy-agent-core-m1fgtnrc.js} +0 -0
  82. /package/dist/shared/@ai-setting/{roy-agent-core-v85n42ve.js → roy-agent-core-ze5cqpf2.js} +0 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @fileoverview Task #5199 — 企微媒体(图片/文件)下载解密落盘
3
+ *
4
+ * ## 背景(用户实测问题)
5
+ *
6
+ * 企微长连接模式下 `image`/`file`/`video` 消息只带:
7
+ * - `image_url`(或 `image.voice_url` 等):加密媒体下载地址,
8
+ * **5 分钟内有效**;
9
+ * - `aeskey`:Base64 编码的 AES 密钥。
10
+ *
11
+ * 此前 WecomWsBridge 只透传消息文本(图片 → `[image]` 占位符),
12
+ * agent 看不到图片内容(用户反馈:「收到图片但没获取到图片内容」)。
13
+ *
14
+ * ## 解密协议(官方文档 101463 + SDK 源码取证,Task #5142)
15
+ *
16
+ * - **AES-256-CBC**,PKCS#7 填充到 **32 字节倍数**(不是 Node 默认 16),
17
+ * 必须 `setAutoPadding(false)` + 手动去填充(对齐官方 SDK decryptFile);
18
+ * - **IV = aeskey 解码后的前 16 字节**(aeskey 是 Base64 的 32 字节密钥);
19
+ * - 下载 URL 5 分钟内有效 → 下载失败如实报错。
20
+ *
21
+ * @see wecom-cli-handler.ts(extractText 图片分支注入落盘路径)
22
+ * @see wecom-media.test.ts
23
+ */
24
+ /** 默认媒体落盘目录 */
25
+ export declare const WECOM_MEDIA_DIR = "/tmp/wecom-media";
26
+ /** 测试注入媒体目录(生产勿用) */
27
+ export declare function setWecomMediaDir(dir: string): string;
28
+ /** 测试重置(恢复默认目录) */
29
+ export declare function __resetWecomMediaDirForTest(): void;
30
+ /** 当前生效的媒体目录 */
31
+ export declare function getWecomMediaDir(): string;
32
+ /**
33
+ * 解密企微媒体(AES-256-CBC,PKCS#7 32 字节块)。
34
+ *
35
+ * 对齐官方 SDK `decryptFile`(@wecom/aibot-node-sdk dist/index.cjs.js):
36
+ * 1. aesKey 是 Base64 编码的 32 字节密钥;
37
+ * 2. IV = key 前 16 字节;
38
+ * 3. `setAutoPadding(false)`(Node 默认按 16 字节块去填充会 bad decrypt);
39
+ * 4. 手动校验并去除 PKCS#7 填充(padLen ∈ [1,32],全部字节一致)。
40
+ *
41
+ * @param encrypted 加密的媒体数据
42
+ * @param aesKeyBase64 Base64 编码的 aeskey(来自消息体 image.aeskey 等)
43
+ * @returns 解密后的明文 Buffer
44
+ * @throws 参数非法 / 密钥错误 / padding 损坏时抛错(不静默返回垃圾数据)
45
+ */
46
+ export declare function decryptWecomMedia(encrypted: Buffer, aesKeyBase64: string): Buffer;
47
+ /** 从消息体提取出的媒体引用 */
48
+ export interface WecomMediaRef {
49
+ /** 媒体类型(image / file / video / voice) */
50
+ kind: "image" | "file" | "video" | "voice";
51
+ /** 加密媒体下载地址(5 分钟内有效) */
52
+ url: string;
53
+ /** Base64 编码的 AES 密钥 */
54
+ aeskey: string;
55
+ /** 原始文件名(file 类型才有) */
56
+ filename?: string;
57
+ }
58
+ /**
59
+ * 从企微消息体提取媒体引用(image / file / video / voice)。
60
+ *
61
+ * - 单媒体消息:body.image / body.file / body.video / body.voice;
62
+ * - mixed 消息:遍历 body.mixed.msg_item[],收集所有媒体 item;
63
+ * - 缺 url 或缺 aeskey 的 item 跳过(不产生坏引用)。
64
+ */
65
+ export declare function extractWecomMediaRefs(body: Record<string, unknown>): WecomMediaRef[];
66
+ /** 下载器返回值(测试注入用) */
67
+ export interface WecomMediaFetchResult {
68
+ ok: boolean;
69
+ status: number;
70
+ buffer: Buffer;
71
+ /** 从 Content-Disposition 解析的文件名(可选) */
72
+ filename?: string;
73
+ }
74
+ /** 下载器签名(生产用全局 fetch;测试注入 mock) */
75
+ export type WecomMediaFetcher = (url: string) => Promise<WecomMediaFetchResult>;
76
+ /** fetchAndSaveWecomMedia 入参 */
77
+ export interface FetchWecomMediaInput {
78
+ msgid: string;
79
+ url: string;
80
+ aeskey: string;
81
+ kind: WecomMediaRef["kind"];
82
+ /** 下载器(测试注入;缺省用全局 fetch) */
83
+ fetcher?: WecomMediaFetcher;
84
+ /** 原始文件名(可选) */
85
+ filename?: string;
86
+ /**
87
+ * ★ Task #5313:会话 id —— 落盘子目录,实现跨群媒体隔离。
88
+ * 缺省时保持旧行为(直接落媒体根目录)。
89
+ */
90
+ chatId?: string;
91
+ }
92
+ /**
93
+ * 下载企微加密媒体 → 解密 → 落盘,返回本地路径。
94
+ *
95
+ * - 下载失败(HTTP 非 200)→ 抛错(URL 5 分钟有效,失败必须可见);
96
+ * - 解密失败(aeskey 错误/数据损坏)→ 抛错(不落垃圾文件);
97
+ * - 成功 → saveWecomMediaFile 落盘(msgid_filename)。
98
+ */
99
+ export declare function fetchAndSaveWecomMedia(input: FetchWecomMediaInput): Promise<string>;
100
+ /** saveWecomMediaFile 入参 */
101
+ export interface SaveWecomMediaInput {
102
+ /** 消息 ID(文件名前缀,防同名覆盖) */
103
+ msgid: string;
104
+ /** 原始文件名(可缺省,用 msgid 兜底) */
105
+ filename?: string;
106
+ /** 解密后的媒体数据 */
107
+ buffer: Buffer;
108
+ /**
109
+ * ★ Task #5313:会话 id —— 落盘到 `<mediaDir>/<safeChatId>/` 子目录。
110
+ *
111
+ * 背景:此前所有会话共用一个扁平目录,mediaFiles 只按 msgid 区分,
112
+ * agent 扫目录时会把别的群的历史图片当成本条消息的附件。
113
+ * 按 chatId 分层后跨群天然隔离(整目录清理也更容易)。
114
+ *
115
+ * 缺省(undefined)→ 保持旧行为,直接落媒体根目录。
116
+ */
117
+ chatId?: string;
118
+ }
119
+ /**
120
+ * 把解密后的媒体数据落盘到媒体目录。
121
+ *
122
+ * - 目录:默认 `/tmp/wecom-media`(测试可注入);传 chatId 时落到
123
+ * `<mediaDir>/<chatId>/` 子目录(Task #5313 跨群隔离);
124
+ * - 文件名:`<msgid>_<basename(filename)>`(路径穿越防护:
125
+ * 只取 basename,`../../etc/passwd` → `passwd`);
126
+ * - 目录不存在时自动创建。
127
+ *
128
+ * @returns 落盘的绝对路径
129
+ */
130
+ export declare function saveWecomMediaFile(input: SaveWecomMediaInput): Promise<string>;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @fileoverview Task #5174(用户明令 2026-11-08 11:29)— 企微回复注册表 + `wecom_respond` 工具
3
+ *
4
+ * ## 出站主路径 = wecom-cli(CLI 已授权)
5
+ *
6
+ * 用户明令:「彻底改造下回复出站用 cli 吧,已经验证可以收到消息了,不用 ws 回」。
7
+ *
8
+ * 发送顺序:
9
+ * 1. **CLI**(`wecom-cli message aibot send --msg-type markdown`)—— 主路径
10
+ * 2. **WS 回帧**(`aibot_respond_msg`,req_id 透传)—— CLI 失败时 fallback
11
+ * (两者都不可用时如实报错,绝不假装已发送)
12
+ *
13
+ * ## chat_id 语义(实测硬约束)
14
+ *
15
+ * - WS 回调里的 `from.userid`(如 `DongZhaoKun`)**不是** CLI 可用的 chat_id
16
+ * → CLI 报 40073「非法的 chat_id」
17
+ * - CLI 需要真实 userid(`wotnB7Wg...` 形态)→ 由 `contact users search` 解析
18
+ * (`wecom-cli-sender.resolveWecomCliChatId`,带内存缓存)
19
+ *
20
+ * ## 注册表
21
+ *
22
+ * `WecomCliInstance.startWithBridge()` 把实例登记进来(`sourceId → target`),
23
+ * `stop()` 注销;工具通过 `EnvContext.sourceId` 反查,从而拿到:
24
+ * - `respond(reqId, body)` —— WS fallback
25
+ * - `sourceId` 对应的配置(CLI 出站不需要实例,但保留用于 fallback)
26
+ *
27
+ * @see wecom-cli-sender.ts(CLI 出站实现)
28
+ * @see wecom-ws-bridge.ts(WS 回帧 fallback)
29
+ */
30
+ import type { Tool } from "../types";
31
+ /** 注册表条目:WS fallback 所需的最小面 */
32
+ export interface WecomRespondTarget {
33
+ /** 发送 WS 回帧(透传原消息 req_id);返回错误而不抛(best-effort) */
34
+ respond(reqId: string, body: Record<string, unknown>): Error | undefined;
35
+ /**
36
+ * 主动推送帧(Task #5203:aibot_send_msg 直发 template_card)。
37
+ * 返回错误而不抛(best-effort)。
38
+ */
39
+ sendCmd?(cmd: string, body: Record<string, unknown>): Error | undefined;
40
+ }
41
+ /** 登记一个可回复的企微实例(sourceId → target) */
42
+ export declare function registerWecomRespondTarget(sourceId: string, target: WecomRespondTarget): void;
43
+ /** 注销(实例 stop 时调用) */
44
+ export declare function unregisterWecomRespondTarget(sourceId: string): void;
45
+ /** 查询(工具执行时用) */
46
+ export declare function getWecomRespondTarget(sourceId: string): WecomRespondTarget | undefined;
47
+ /** 测试用:清空注册表 */
48
+ export declare function __resetWecomRespondRegistryForTest(): void;
49
+ /** 出站发送入参 */
50
+ export interface WecomSendInput {
51
+ /** 事件源 ID(用于查 WS fallback 实例) */
52
+ sourceId: string;
53
+ /** 原始 WS 回调的 userid / chatid(CLI 侧会自动解析成真实 userid) */
54
+ rawChatId: string;
55
+ /** 原消息帧 req_id(WS fallback 用) */
56
+ reqId?: string;
57
+ /** 回复正文 */
58
+ content: string;
59
+ }
60
+ /** 出站发送结果 */
61
+ export interface WecomSendResult {
62
+ ok: boolean;
63
+ /** 实际走通的通道 */
64
+ channel?: "cli" | "ws";
65
+ /** 失败原因(两个通道都失败时给出合并说明) */
66
+ error?: string;
67
+ }
68
+ /**
69
+ * 发送企微回复:**被动回帧优先(有 reqId 时),CLI 兜底**(永不抛)。
70
+ *
71
+ * ## 为什么被动回帧优先(Task #5287 回复引用)
72
+ *
73
+ * 取证结论(官方文档 101463 + CLI schema 实查):
74
+ * - 主动推送(CLI `message.aibot.send` / WS `aibot_send_msg`)schema
75
+ * **无 quote/引用字段** —— 协议不支持发消息时主动引用某条消息;
76
+ * - 被动回复(WS `aibot_respond_msg`,headers.req_id 原样透传)时,
77
+ * 企微服务端**自动**把回复引用到触发回调的用户消息(群聊场景)。
78
+ *
79
+ * ⇒ 要让用户看到「回复引用了哪条消息」,必须走被动回帧。
80
+ * 因此有 reqId 且 WS 实例在线时**优先** WS 被动回帧;
81
+ * CLI 降为 fallback(无 reqId / WS 失败 / 无实例时仍可用,
82
+ * #5174「CLI 主路径」行为在无 reqId 场景不回归)。
83
+ *
84
+ * 发送顺序:
85
+ * 1. **WS 被动回帧**(reqId 存在 + 注册实例在线)—— 带引用语义
86
+ * 2. **CLI**(`wecom-cli message aibot send --msg-type markdown`)—— fallback
87
+ * 3. 都失败 → `{ok:false, error}`(两个原因都带上,便于排障)
88
+ */
89
+ export declare function sendWecomReply(input: WecomSendInput): Promise<WecomSendResult>;
90
+ /** 卡片直发入参 */
91
+ export interface WecomTemplateCardSendInput {
92
+ /** 事件源 ID(查注册的 WS 实例) */
93
+ sourceId: string;
94
+ /** 会话 ID(单聊=userid,群聊=群 chatid) */
95
+ chatId: string;
96
+ /** 会话类型(决定 chat_type:1 单聊 / 2 群聊) */
97
+ chatType: "single" | "group";
98
+ /** 卡片结构体(wecom-card-builder 构造) */
99
+ card: Record<string, unknown>;
100
+ }
101
+ /**
102
+ * 经 WS 长连接直发 template_card(aibot_send_msg 主动推送)。
103
+ *
104
+ * ## 为什么不走 wecom-cli
105
+ *
106
+ * wecom-cli v1.3.4 的 msg_type 枚举只有 markdown/image/file/voice/video,
107
+ * 没封装 template_card(官方仓源码 grep 零命中,Task #5202 取证)。
108
+ * WS 协议原生支持 → 直发。
109
+ *
110
+ * best-effort:永不抛;失败返回 `{ok:false, error}`。
111
+ *
112
+ * ⚠️ 主动推送前提(官方 101463):需用户先在会话中给机器人发过消息;
113
+ * 限频 30 条/分钟、1000 条/小时(调用方节流)。
114
+ */
115
+ export declare function sendWecomTemplateCard(input: WecomTemplateCardSendInput): Promise<WecomSendResult>;
116
+ /**
117
+ * `wecom_respond` —— 回复企微用户(CLI 主路径,WS fallback)。
118
+ *
119
+ * 参数:
120
+ * - `content`:回复正文(markdown)
121
+ * - `chat_id`:**可选**,回复目标;缺省时从 EnvContext 事件 metadata 取
122
+ * (单聊=senderId;群聊=chatid)。CLI 侧会自动解析成真实 userid。
123
+ * - `req_id`:**可选**,原消息帧 req_id(WS fallback 用);缺省从 metadata.reqId 取
124
+ */
125
+ export declare const wecomRespondTool: Tool;
126
+ /** 标记本轮已发送(wecom_respond 工具成功时调用) */
127
+ export declare function markWecomReplySent(explicitBag?: Record<string, unknown>): void;
128
+ /** 本轮是否已发送过 */
129
+ export declare function wasWecomReplySent(explicitBag?: Record<string, unknown>): boolean;
130
+ /** 测试用:清空标记 */
131
+ export declare function __resetWecomReplySentForTest(explicitBag?: Record<string, unknown>): void;
132
+ /**
133
+ * 框架级兜底:turn 结束后,若本轮没发过回复且事件来自企微 → 直接把
134
+ * assistant 的最终文本发出去(CLI 主路径 → WS fallback)。
135
+ *
136
+ * 触发条件(全部满足):
137
+ * 1. `envEvent.type === "event-source.event.wecom-cli"`
138
+ * 2. 最终文本非空
139
+ * 3. 本轮**尚未**通过 `wecom_respond` 工具发送(避免双发)
140
+ *
141
+ * @returns 发送结果;不满足触发条件时返回 `{skipped: true}`
142
+ */
143
+ export declare function autoReplyWecomTurn(envEvent: unknown, finalText: string): Promise<{
144
+ skipped: true;
145
+ reason: string;
146
+ } | {
147
+ skipped: false;
148
+ ok: boolean;
149
+ channel?: "cli" | "ws";
150
+ error?: string;
151
+ }>;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @fileoverview Task #5165 — 企微长连接 WebSocket 桥
3
+ *
4
+ * ## 为什么自实现(不用 @wecom/aibot-node-sdk)
5
+ *
6
+ * 1. core 已依赖 `ws@^8.16.0`(bounty-im-handler 同款用法),
7
+ * 不新增任何 npm 依赖(npm 官方 registry 挂,见 Task #5142);
8
+ * 2. 协议极简(4 种帧),SDK 的认证/心跳/重连语义已全部吃透
9
+ * (读 SDK dist/index.cjs.js 源码取证,Task #5142);
10
+ * 3. 避免第三方依赖供应链风险。
11
+ *
12
+ * ## 协议(官方文档 101463 + SDK 源码取证)
13
+ *
14
+ * - 认证帧:`{cmd:"aibot_subscribe", headers:{req_id}, body:{bot_id, secret}}`
15
+ * 响应:无 cmd,`headers.req_id` 前缀 `aibot_subscribe_`,`errcode==0` 成功
16
+ * - 心跳:`{cmd:"ping", headers:{req_id}}`,响应 req_id 前缀 `ping_`,建议 30s
17
+ * - 消息:`{cmd:"aibot_msg_callback", headers:{req_id}, body:{msgid,...}}`
18
+ * - 事件:`{cmd:"aibot_event_callback", headers:{req_id}, body:{...}}`
19
+ * - 回复:`{cmd:"aibot_respond_msg", headers:{req_id:原样透传}, body:{...}}`
20
+ * - 断线重连:指数退避;认证失败与断线**独立计数**
21
+ * - ⚠️ 每机器人同一时刻只允许 1 条长连接(新连接踢旧连接)
22
+ *
23
+ * @see packages/core/src/env/event-source/wecom-ws-bridge.test.ts
24
+ * @see docs/research/2026-11-07-wecom-cli-and-roy-agent-es.md
25
+ */
26
+ import type { WebSocket } from "ws";
27
+ /** 企微长连接默认地址(官方文档 101463) */
28
+ export declare const WECOM_WS_URL = "wss://openws.work.weixin.qq.com";
29
+ /** 心跳间隔(官方建议 30s) */
30
+ export declare const WECOM_HEARTBEAT_INTERVAL_MS = 30000;
31
+ /** 重连基础延迟(指数退避起点) */
32
+ export declare const WECOM_RECONNECT_BASE_DELAY_MS = 1000;
33
+ /** 重连最大延迟(指数退避封顶) */
34
+ export declare const WECOM_RECONNECT_MAX_DELAY_MS = 60000;
35
+ /** 默认最大重连次数(断线场景) */
36
+ export declare const WECOM_MAX_RECONNECT_ATTEMPTS = 10;
37
+ /** 默认最大认证失败次数(凭据错误重试无用) */
38
+ export declare const WECOM_MAX_AUTH_FAILURE_ATTEMPTS = 5;
39
+ /** 生成 `<prefix>_<timestamp>_<random>` 形态的 req_id(对齐官方 SDK) */
40
+ export declare function generateWecomReqId(prefix: string): string;
41
+ /** 通用帧形状(收发共用) */
42
+ export interface WecomFrame {
43
+ cmd?: string;
44
+ headers?: {
45
+ req_id?: string;
46
+ };
47
+ body?: Record<string, unknown>;
48
+ errcode?: number;
49
+ errmsg?: string;
50
+ }
51
+ /** 构造认证帧(aibot_subscribe) */
52
+ export declare function buildWecomAuthFrame(botId: string, secret: string): WecomFrame & {
53
+ cmd: string;
54
+ body: Record<string, unknown>;
55
+ };
56
+ /** 构造心跳帧(ping) */
57
+ export declare function buildWecomHeartbeatFrame(): WecomFrame & {
58
+ cmd: string;
59
+ };
60
+ /**
61
+ * 构造回复帧(aibot_respond_msg,Task #5174)。
62
+ *
63
+ * ★ 协议硬约束(官方文档 101463 + SDK 源码取证):
64
+ * - `headers.req_id` 必须**原样透传**原消息帧的 req_id
65
+ * (服务端靠它把回复关联到原消息/会话);
66
+ * - body 是消息体(msgtype: text / markdown / image / ...);
67
+ * - 收到回调后 24h 内可回复;限频 30 条/分钟。
68
+ *
69
+ * @param reqId 原消息帧的 req_id(空则抛错 —— 关联缺失等于发不出去)
70
+ * @param body 消息体(如 `{msgtype:"text", text:{content:"..."}}`)
71
+ */
72
+ export declare function buildWecomRespondFrame(reqId: string, body: Record<string, unknown>): WecomFrame & {
73
+ cmd: string;
74
+ body: Record<string, unknown>;
75
+ };
76
+ /** 便捷构造:text 回复帧 */
77
+ export declare function buildWecomRespondTextFrame(reqId: string, content: string): WecomFrame & {
78
+ cmd: string;
79
+ body: Record<string, unknown>;
80
+ };
81
+ /** 判定是否认证响应(req_id 前缀 `aibot_subscribe_`,无论成败) */
82
+ export declare function isWecomAuthResponse(frame: WecomFrame): boolean;
83
+ /** 判定是否心跳响应(req_id 前缀 `ping_`) */
84
+ export declare function isWecomHeartbeatResponse(frame: WecomFrame): boolean;
85
+ /**
86
+ * 判定是否「服务端踢线」事件帧(Task #5203)。
87
+ *
88
+ * ## 为什么需要它(真网根因)
89
+ *
90
+ * 企微「每机器人同一时刻只允许 1 条长连接」——新连接建起来会把旧连接踢掉。
91
+ * 踢线时服务端**只发一个事件帧** `disconnected_event`,
92
+ * **TCP 连接并不关闭**(socket 仍 ESTAB,心跳 ping/pong 仍正常往返)。
93
+ *
94
+ * ⇒ 原实现只在 `ws.on("close")` 时重连 ⇒ 被踢线的 bridge **永远不重连**,
95
+ * 变成「心跳正常但业务流量全丢」的僵尸连接(真网现象:用户点卡片按钮,
96
+ * 回调送不达 ES,app.log 里只有 disconnected_event,没有 template_card_event)。
97
+ *
98
+ * @see https://developer.work.weixin.qq.com/document/path/101463(长连接规范)
99
+ */
100
+ export declare function isWecomDisconnectedEvent(frame: unknown): boolean;
101
+ /**
102
+ * 清洗帧中的 secret 字段(DS review B1,Task #5165)。
103
+ *
104
+ * 浅拷贝 + 删除顶层与 body 层的 `secret` —— 服务端 echo 请求体时
105
+ * 防止认证 secret 经 onMessage → rawEvent 泄漏给 LLM。
106
+ */
107
+ export declare function sanitizeWecomFrame(frame: WecomFrame): WecomFrame;
108
+ /** WsBridge 配置 */
109
+ export interface WecomWsBridgeOptions {
110
+ botId: string;
111
+ secret: string;
112
+ /** 覆盖默认 ws 地址(测试 / 内网代理用) */
113
+ wsUrl?: string;
114
+ /** WebSocket 工厂(测试注入 mock;生产用 `new (await import('ws')).WebSocket`) */
115
+ wsFactory?: (url: string) => WebSocket;
116
+ heartbeatIntervalMs?: number;
117
+ reconnectBaseDelayMs?: number;
118
+ reconnectMaxDelayMs?: number;
119
+ maxReconnectAttempts?: number;
120
+ maxAuthFailureAttempts?: number;
121
+ }
122
+ /**
123
+ * 企微长连接桥(对齐官方 SDK 语义,自实现于 ws)。
124
+ *
125
+ * 状态机:
126
+ * connect() → open → 发认证帧 → 认证响应
127
+ * ├─ errcode==0 → authenticated → 启动心跳
128
+ * └─ errcode!=0 → auth 失败计数 → 指数退避重连
129
+ * close/error → 断线计数 → 指数退避重连
130
+ * stop() → 手动关闭,不再重连
131
+ *
132
+ * ⚠️ 每机器人同一时刻只允许 1 条长连接(新连接踢旧连接)。
133
+ */
134
+ export declare class WecomWsBridge {
135
+ private opts;
136
+ private readonly botId;
137
+ private readonly secret;
138
+ private wsFactory;
139
+ private wsUrl;
140
+ private ws?;
141
+ private heartbeatTimer?;
142
+ private reconnectTimer?;
143
+ private reconnectAttempts;
144
+ private authFailureAttempts;
145
+ private manualClose;
146
+ private lastCloseWasAuthFailure;
147
+ private stopped;
148
+ private onMessageCb?;
149
+ private onAuthenticatedCb?;
150
+ private onReconnectingCb?;
151
+ private onErrorCb?;
152
+ constructor(opts: WecomWsBridgeOptions);
153
+ onMessage(cb: (frame: WecomFrame) => void): void;
154
+ onAuthenticated(cb: () => void): void;
155
+ onReconnecting(cb: (attempt: number) => void): void;
156
+ onError(cb: (err: Error) => void): void;
157
+ /** 建立连接(open 后自动发认证帧) */
158
+ connect(): void;
159
+ private optsBotId;
160
+ private optsSecret;
161
+ /** 处理服务端帧(认证响应 / 心跳响应 / 消息 / 事件) */
162
+ private handleFrame;
163
+ private sendFrame;
164
+ /**
165
+ * 发送回复帧(aibot_respond_msg,Task #5174)。
166
+ *
167
+ * best-effort 语义:返回错误而不抛 —— 调用方(handler)在
168
+ * 事件回调里,抛错会打断事件处理链。
169
+ *
170
+ * @param reqId 原消息帧的 req_id(原样透传)
171
+ * @param body 消息体
172
+ * @returns 发送失败时返回 Error;成功返回 undefined
173
+ */
174
+ respond(reqId: string, body: Record<string, unknown>): Error | undefined;
175
+ /**
176
+ * 主动推送帧(Task #5203:aibot_send_msg 直发 template_card)。
177
+ *
178
+ * 与 respond 的区别:respond 必须关联原消息 req_id(被动回复),
179
+ * sendCmd 是**主动推送**(req_id 由开发者自行生成,用于关联
180
+ * 请求与响应)—— 不依赖用户消息触发。
181
+ *
182
+ * best-effort 语义同 respond:返回错误而不抛。
183
+ *
184
+ * @param cmd 命令类型(如 "aibot_send_msg")
185
+ * @param body 消息体
186
+ */
187
+ sendCmd(cmd: string, body: Record<string, unknown>): Error | undefined;
188
+ /**
189
+ * 发送自定义帧(req_id 由调用方透传,Task #5203 回归修复)。
190
+ *
191
+ * ★ 为什么需要:卡片更新(aibot_respond_update_msg)必须
192
+ * **原样透传** template_card_event 回调帧的 req_id
193
+ * (5 秒窗口关联),而 sendCmd 自己生成 req_id、
194
+ * respond() 硬包成 aibot_respond_msg —— 两者都不满足。
195
+ *
196
+ * best-effort 语义同 respond/sendCmd:返回错误而不抛。
197
+ *
198
+ * @param frame 完整帧(cmd + headers.req_id + body)
199
+ */
200
+ sendFrameWithReqId(frame: {
201
+ cmd: string;
202
+ headers: {
203
+ req_id: string;
204
+ };
205
+ body: Record<string, unknown>;
206
+ }): Error | undefined;
207
+ private startHeartbeat;
208
+ private stopHeartbeat;
209
+ /** 指数退避重连(认证失败与断线独立计数) */
210
+ private scheduleReconnect;
211
+ /** 手动停止(不再重连) */
212
+ stop(): void;
213
+ }
package/dist/env/index.js CHANGED
@@ -1,55 +1,57 @@
1
1
  import {
2
2
  BaseEnvironment
3
- } from "../shared/@ai-setting/roy-agent-core-zycadbpx.js";
3
+ } from "../shared/@ai-setting/roy-agent-core-p6ks90tb.js";
4
4
  import {
5
5
  TaskComponent
6
- } from "../shared/@ai-setting/roy-agent-core-z43aj03k.js";
6
+ } from "../shared/@ai-setting/roy-agent-core-qk9hd89p.js";
7
7
  import"../shared/@ai-setting/roy-agent-core-2akfd64z.js";
8
8
  import"../shared/@ai-setting/roy-agent-core-jm136jzh.js";
9
- import"../shared/@ai-setting/roy-agent-core-aa2j7bk9.js";
9
+ import"../shared/@ai-setting/roy-agent-core-cha0jc7n.js";
10
10
  import"../shared/@ai-setting/roy-agent-core-fttgm52a.js";
11
- import"../shared/@ai-setting/roy-agent-core-ntq0gz7q.js";
11
+ import"../shared/@ai-setting/roy-agent-core-q813vafd.js";
12
12
  import"../shared/@ai-setting/roy-agent-core-8gxth0eh.js";
13
13
  import"../shared/@ai-setting/roy-agent-core-vffccgf6.js";
14
14
  import"../shared/@ai-setting/roy-agent-core-p5cz9m8v.js";
15
15
  import {
16
16
  EventSourceComponent
17
- } from "../shared/@ai-setting/roy-agent-core-cxy143m4.js";
17
+ } from "../shared/@ai-setting/roy-agent-core-begm7ytz.js";
18
18
  import {
19
19
  builtInHandlers,
20
20
  larkCliHandler
21
- } from "../shared/@ai-setting/roy-agent-core-t3rrb959.js";
21
+ } from "../shared/@ai-setting/roy-agent-core-xf6z96qy.js";
22
22
  import {
23
23
  EventSourceInitHooks,
24
24
  getDefaultConfigForType,
25
25
  isBuiltInEventSourceType,
26
26
  isValidEventSourceType,
27
27
  validateEventSourceConfig
28
- } from "../shared/@ai-setting/roy-agent-core-bxkjk7zx.js";
28
+ } from "../shared/@ai-setting/roy-agent-core-vx8zc847.js";
29
29
  import"../shared/@ai-setting/roy-agent-core-v0hf14bj.js";
30
30
  import"../shared/@ai-setting/roy-agent-core-tm1mxttk.js";
31
- import"../shared/@ai-setting/roy-agent-core-ghnagzrh.js";
31
+ import"../shared/@ai-setting/roy-agent-core-2stt4rp5.js";
32
+ import"../shared/@ai-setting/roy-agent-core-b31v3qpv.js";
33
+ import"../shared/@ai-setting/roy-agent-core-m3vjepzz.js";
32
34
  import"../shared/@ai-setting/roy-agent-core-na18zwj0.js";
33
35
  import"../shared/@ai-setting/roy-agent-core-9vxh6f8h.js";
34
- import"../shared/@ai-setting/roy-agent-core-abqhkbn3.js";
36
+ import"../shared/@ai-setting/roy-agent-core-ezvpreqz.js";
35
37
  import"../shared/@ai-setting/roy-agent-core-1pg0fepg.js";
36
38
  import"../shared/@ai-setting/roy-agent-core-tr3ep81v.js";
37
39
  import"../shared/@ai-setting/roy-agent-core-m46fe3x4.js";
38
40
  import"../shared/@ai-setting/roy-agent-core-4rtg69cn.js";
39
- import"../shared/@ai-setting/roy-agent-core-p3zv1mx5.js";
40
- import"../shared/@ai-setting/roy-agent-core-nmx9934r.js";
41
+ import"../shared/@ai-setting/roy-agent-core-d1n4jbza.js";
42
+ import"../shared/@ai-setting/roy-agent-core-7dd0s3xb.js";
41
43
  import {
42
44
  WorkflowComponent
43
- } from "../shared/@ai-setting/roy-agent-core-v87njmq7.js";
45
+ } from "../shared/@ai-setting/roy-agent-core-0dh8npnz.js";
44
46
  import"../shared/@ai-setting/roy-agent-core-es203w7p.js";
45
47
  import"../shared/@ai-setting/roy-agent-core-h0x19xgn.js";
46
48
  import"../shared/@ai-setting/roy-agent-core-1ce3fqrk.js";
47
- import"../shared/@ai-setting/roy-agent-core-mww0mnd9.js";
49
+ import"../shared/@ai-setting/roy-agent-core-7bpj859q.js";
48
50
  import"../shared/@ai-setting/roy-agent-core-ddyk1rc0.js";
49
51
  import"../shared/@ai-setting/roy-agent-core-0qyrk5k0.js";
50
52
  import"../shared/@ai-setting/roy-agent-core-hhrg314p.js";
51
53
  import"../shared/@ai-setting/roy-agent-core-12n08pg5.js";
52
- import"../shared/@ai-setting/roy-agent-core-6cy43nss.js";
54
+ import"../shared/@ai-setting/roy-agent-core-rykf7c04.js";
53
55
  import"../shared/@ai-setting/roy-agent-core-6rh8k6ba.js";
54
56
  import"../shared/@ai-setting/roy-agent-core-6atd905e.js";
55
57
  import"../shared/@ai-setting/roy-agent-core-dqxec05w.js";
@@ -68,8 +70,8 @@ import"../shared/@ai-setting/roy-agent-core-jagkferh.js";
68
70
  import {
69
71
  AgentComponent,
70
72
  AgentComponentConfigSchema
71
- } from "../shared/@ai-setting/roy-agent-core-q5r75vev.js";
72
- import"../shared/@ai-setting/roy-agent-core-v85n42ve.js";
73
+ } from "../shared/@ai-setting/roy-agent-core-dkrvmjsh.js";
74
+ import"../shared/@ai-setting/roy-agent-core-ze5cqpf2.js";
73
75
  import"../shared/@ai-setting/roy-agent-core-jvv2h3d6.js";
74
76
  import"../shared/@ai-setting/roy-agent-core-ed233k5z.js";
75
77
  import {
@@ -77,7 +79,7 @@ import {
77
79
  getXDGPath,
78
80
  getXDGPaths
79
81
  } from "../shared/@ai-setting/roy-agent-core-qxnbvgwe.js";
80
- import"../shared/@ai-setting/roy-agent-core-f77k3gqt.js";
82
+ import"../shared/@ai-setting/roy-agent-core-h21gngxf.js";
81
83
  import"../shared/@ai-setting/roy-agent-core-0snm5hc5.js";
82
84
  import"../shared/@ai-setting/roy-agent-core-fghc2m8z.js";
83
85
  import"../shared/@ai-setting/roy-agent-core-zjdzk1gg.js";
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  TASK_COMPLETED_STATUS_WORD,
3
- TASK_DELIVERABLE_RECORDED
3
+ TASK_DELIVERABLE_RECORDED,
4
+ TASK_TO_VALIDATE_STATUS_WORD
4
5
  } from "../../../shared/@ai-setting/roy-agent-core-4rtg69cn.js";
5
6
  import"../../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
6
7
  // src/env/task/backend/task-store-backend.ts
@@ -166,6 +167,41 @@ class TaskStoreBackend {
166
167
  const reread = await this.resolve(record.nativeId);
167
168
  return this.toTask(reread ?? updated);
168
169
  }
170
+ async preCompleteTask(id, sessionId, deliverable, progressSummary) {
171
+ const record = await this.resolve(id);
172
+ if (!record)
173
+ return null;
174
+ if (record.type === "cycle") {
175
+ throw new Error(`Cannot pre-complete cycle task: #${record.id} - ${record.title}. Cycle tasks are not meant to be completed or submitted for acceptance.`);
176
+ }
177
+ const toValidateWording = progressSummary ? `${TASK_TO_VALIDATE_STATUS_WORD}:${progressSummary}` : TASK_TO_VALIDATE_STATUS_WORD;
178
+ const patch = {
179
+ status: "to_validate",
180
+ current_status: toValidateWording
181
+ };
182
+ if (deliverable) {
183
+ patch.context = mergeDeliverableIntoContext(record.context, deliverable);
184
+ }
185
+ const updated = await this.port.updateTask(record.nativeId, patch);
186
+ if (!updated)
187
+ return null;
188
+ const descParts = [];
189
+ const deliverableLine = formatDeliverableForOperation(deliverable);
190
+ if (deliverableLine)
191
+ descParts.push(deliverableLine);
192
+ if (progressSummary)
193
+ descParts.push(progressSummary);
194
+ await this.createOperation({
195
+ taskId: updated.id,
196
+ sessionId: sessionId ?? this.defaultSessionId,
197
+ milestoneType: "milestone",
198
+ milestoneTitle: `${TASK_TO_VALIDATE_STATUS_WORD}(to_validate):Task #${updated.id} submitted for acceptance`,
199
+ milestoneDescription: descParts.join(" / ") || TASK_TO_VALIDATE_STATUS_WORD,
200
+ ...deliverable ? { mdPath: deliverable } : {}
201
+ });
202
+ const reread = await this.resolve(record.nativeId);
203
+ return this.toTask(reread ?? updated);
204
+ }
169
205
  async getTaskTree(id, depth = -1) {
170
206
  const root = await this.resolve(id);
171
207
  if (!root)
@@ -62,6 +62,18 @@ export declare class TaskStoreBackend implements TaskBackend {
62
62
  updateTask(id: TaskId, input: BackendUpdateTaskOptions): Promise<Task | null>;
63
63
  deleteTask(id: TaskId): Promise<boolean>;
64
64
  completeTask(id: TaskId, sessionId?: string, deliverable?: string): Promise<Task | null>;
65
+ /**
66
+ * Task #5078 — 提交待验证(两阶段验收 #5073 的 backend 实现)。
67
+ *
68
+ * 语义与 `TaskComponent.preCompleteTask` 同构:
69
+ * status → to_validate、current_status → 待验证措辞、
70
+ * deliverable merge 进 context、milestone operation 留痕。
71
+ *
72
+ * 与 TaskComponent 的唯一差别:**不 fire AFTER_COMPLETE 钩子** ——
73
+ * backend 没有 hook 链(#5035 已确认),goal 判定由上层负责
74
+ * (MCP 侧 = 插件 withGoalCheck;TaskComponent 侧 = 插件链)。
75
+ */
76
+ preCompleteTask(id: TaskId, sessionId?: string, deliverable?: string, progressSummary?: string): Promise<Task | null>;
65
77
  /**
66
78
  * review B1 — roy 的 `task-tree-tool.ts` 在子树模式下调这个方法;
67
79
  * 老 facade 缺它导致 `task_tree(parent_task_id=…)` 直接抛