@ai-setting/roy-agent-core 1.7.77 → 1.7.88
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/dist/config/config-protection.d.ts +89 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +23 -7
- package/dist/env/agent/agent-component.d.ts +43 -22
- package/dist/env/agent/agent-component.js +13 -12
- package/dist/env/agent/index.js +13 -12
- package/dist/env/agent/types.d.ts +46 -12
- package/dist/env/commands/index.js +7 -7
- package/dist/env/context/index.js +3 -3
- package/dist/env/debug/index.js +7 -7
- package/dist/env/error-reply.d.ts +6 -0
- package/dist/env/error-reply.js +1 -1
- package/dist/env/event-source/bounty-im-handler.d.ts +8 -0
- package/dist/env/event-source/es-command.d.ts +39 -0
- package/dist/env/event-source/es-plugins.d.ts +68 -0
- package/dist/env/event-source/event-source-component.d.ts +15 -0
- package/dist/env/event-source/event-source-handlers.d.ts +84 -0
- package/dist/env/event-source/event-source-handlers.js +40 -23
- package/dist/env/event-source/event-store.js +2 -2
- package/dist/env/event-source/index.d.ts +5 -1
- package/dist/env/event-source/index.js +75 -23
- package/dist/env/event-source/timer-handler.d.ts +98 -0
- package/dist/env/event-source/types.d.ts +154 -2
- package/dist/env/event-source/wecom-card-action-handler.d.ts +373 -0
- package/dist/env/event-source/wecom-card-builder.d.ts +426 -0
- package/dist/env/event-source/wecom-cli-handler.d.ts +291 -0
- package/dist/env/event-source/wecom-cli-sender.d.ts +88 -0
- package/dist/env/event-source/wecom-media.d.ts +130 -0
- package/dist/env/event-source/wecom-respond-registry.d.ts +151 -0
- package/dist/env/event-source/wecom-respond-registry.js +33 -0
- package/dist/env/event-source/wecom-session-card-sender.d.ts +135 -0
- package/dist/env/event-source/wecom-session-card-sender.js +153 -0
- package/dist/env/event-source/wecom-ws-bridge.d.ts +213 -0
- package/dist/env/hook/index.js +5 -5
- package/dist/env/index.js +51 -45
- package/dist/env/llm/index.js +9 -8
- package/dist/env/log-trace/index.js +7 -7
- package/dist/env/log-trace/trace-filters.d.ts +6 -1
- package/dist/env/mcp/index.js +7 -7
- package/dist/env/memory/index.js +7 -7
- package/dist/env/plugin/index.js +7 -7
- package/dist/env/prompt/index.js +8 -8
- package/dist/env/prompt/prompts-index.d.ts +1 -1
- package/dist/env/session/group-session-link.d.ts +99 -0
- package/dist/env/session/index.js +11 -10
- package/dist/env/session/session-component.d.ts +89 -0
- package/dist/env/session/storage/index.js +5 -4
- package/dist/env/session/types.d.ts +10 -0
- package/dist/env/skill/index.js +7 -7
- package/dist/env/task/delegate/delegate-channel-hint.d.ts +39 -0
- package/dist/env/task/delegate/delegate-tool.d.ts +132 -7
- package/dist/env/task/delegate/delegate-tool.js +13 -9
- package/dist/env/task/delegate/index.js +8 -8
- package/dist/env/task/index.js +18 -18
- package/dist/env/task/mcp/index.js +4 -4
- package/dist/env/task/plugins/card-subject.d.ts +117 -0
- package/dist/env/task/plugins/index.d.ts +2 -0
- package/dist/env/task/plugins/index.js +39 -16
- package/dist/env/task/plugins/lark-card-action-handler.d.ts +261 -6
- package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +542 -2
- package/dist/env/task/plugins/lark-card-patch-notifier.js +12 -2
- package/dist/env/task/plugins/lark-card-state-store.d.ts +60 -0
- package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +61 -0
- package/dist/env/task/plugins/sub-session-activity.d.ts +415 -0
- package/dist/env/task/plugins/wecom-card-state-store.d.ts +121 -0
- package/dist/env/task/plugins/wecom-session-card-store.d.ts +134 -0
- package/dist/env/task/plugins/wecom-session-card-store.js +23 -0
- package/dist/env/task/plugins/wecom-task-notify-plugin.d.ts +264 -0
- package/dist/env/task/storage/index.js +3 -3
- package/dist/env/task/tools/index.js +4 -4
- package/dist/env/tool/built-in/index.d.ts +2 -0
- package/dist/env/tool/built-in/index.js +9 -2
- package/dist/env/tool/index.js +14 -9
- package/dist/env/tool/types.d.ts +25 -0
- package/dist/env/tool/validator.d.ts +8 -0
- package/dist/env/workflow/engine/index.js +12 -12
- package/dist/env/workflow/hooks/index.js +7 -7
- package/dist/env/workflow/index.js +24 -23
- package/dist/env/workflow/nodes/index.js +2 -2
- package/dist/env/workflow/plugins/index.js +10 -8
- package/dist/env/workflow/service/index.js +2 -2
- package/dist/env/workflow/storage/index.js +3 -3
- package/dist/env/workflow/tools/index.js +16 -15
- package/dist/env/workflow/utils/index.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +118 -62
- package/dist/shared/@ai-setting/{roy-agent-core-dc9zq75z.js → roy-agent-core-0crmsc12.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-aa2j7bk9.js → roy-agent-core-0sbmdz8d.js} +171 -36
- package/dist/shared/@ai-setting/{roy-agent-core-87942bw4.js → roy-agent-core-1mhyh4c5.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-v6vr6za7.js → roy-agent-core-1vv4fx15.js} +4 -4
- package/dist/shared/@ai-setting/roy-agent-core-2stt4rp5.js +134 -0
- package/dist/shared/@ai-setting/{roy-agent-core-nwbfepba.js → roy-agent-core-3e040tc5.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-418zc9ha.js +288 -0
- package/dist/shared/@ai-setting/{roy-agent-core-r5wt1h3c.js → roy-agent-core-4fkxy2gv.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-na18zwj0.js → roy-agent-core-4t3yskp8.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-5pr4zp88.js → roy-agent-core-5kzhaap1.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-qq96p0tj.js → roy-agent-core-5s2t8z00.js} +22 -1
- package/dist/shared/@ai-setting/{roy-agent-core-6rh8k6ba.js → roy-agent-core-6gj1hc5z.js} +7 -7
- package/dist/shared/@ai-setting/{roy-agent-core-ezvpreqz.js → roy-agent-core-6k8mb7zx.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-g8htfwxj.js → roy-agent-core-7f10p7m2.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-7kvpc0w5.js +177 -0
- package/dist/shared/@ai-setting/{roy-agent-core-h21gngxf.js → roy-agent-core-7yh5wps0.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-efkjyh69.js → roy-agent-core-9710zwwz.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-6cy43nss.js → roy-agent-core-9c565frx.js} +49 -9
- package/dist/shared/@ai-setting/{roy-agent-core-fghc2m8z.js → roy-agent-core-aa8kv18f.js} +34 -2
- package/dist/shared/@ai-setting/{roy-agent-core-ew6qb5yx.js → roy-agent-core-an35dzbf.js} +6 -6
- package/dist/shared/@ai-setting/{roy-agent-core-mww0mnd9.js → roy-agent-core-azyz78ky.js} +7 -7
- package/dist/shared/@ai-setting/{roy-agent-core-264qexac.js → roy-agent-core-b7h39vrc.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-dqxec05w.js → roy-agent-core-bjtc9d35.js} +2 -2
- package/dist/shared/@ai-setting/roy-agent-core-c4ec7k0p.js +426 -0
- package/dist/shared/@ai-setting/{roy-agent-core-ddyk1rc0.js → roy-agent-core-cre2y2ct.js} +160 -4
- package/dist/shared/@ai-setting/{roy-agent-core-tr3ep81v.js → roy-agent-core-d6zdbgtc.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-0qyrk5k0.js → roy-agent-core-ds6qzmf8.js} +14 -5
- package/dist/shared/@ai-setting/{roy-agent-core-cgtq6427.js → roy-agent-core-e08bvpaq.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-12n08pg5.js → roy-agent-core-e69qxdyy.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-rhtdspx6.js → roy-agent-core-fan8g0z1.js} +10 -10
- package/dist/shared/@ai-setting/{roy-agent-core-xcp8vwax.js → roy-agent-core-fepm835e.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-6j02sbee.js → roy-agent-core-fhg03v43.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-99nwgj84.js → roy-agent-core-fzw9xzm1.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-dvtg4sn0.js → roy-agent-core-g8yr733d.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-3gpygqcx.js → roy-agent-core-gr5h1dyr.js} +2753 -358
- package/dist/shared/@ai-setting/roy-agent-core-h1e1fm9b.js +380 -0
- package/dist/shared/@ai-setting/{roy-agent-core-c2gbt68e.js → roy-agent-core-h83zdsrq.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-8ez6mccb.js → roy-agent-core-k3gcvybs.js} +5 -1
- package/dist/shared/@ai-setting/{roy-agent-core-pgvcf91v.js → roy-agent-core-m39v7qpx.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-tm1mxttk.js → roy-agent-core-mf5p38yf.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-83bewqx8.js → roy-agent-core-mvh0td7p.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-v85n42ve.js → roy-agent-core-mx855td4.js} +17 -38
- package/dist/shared/@ai-setting/{roy-agent-core-v87njmq7.js → roy-agent-core-n197medr.js} +13 -13
- package/dist/shared/@ai-setting/{roy-agent-core-cmprygrp.js → roy-agent-core-nqkfkpf4.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-7cbpmk4q.js → roy-agent-core-p31ftgq9.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-vffccgf6.js → roy-agent-core-qwd2dq5q.js} +10 -3
- package/dist/shared/@ai-setting/{roy-agent-core-rsn44n9y.js → roy-agent-core-ra66sf50.js} +2526 -942
- package/dist/shared/@ai-setting/{roy-agent-core-1v4g0d4x.js → roy-agent-core-rm7seqwb.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-w0qjeq2v.js → roy-agent-core-shb1qsyg.js} +1041 -18
- package/dist/shared/@ai-setting/{roy-agent-core-ax9e9szb.js → roy-agent-core-smf94h73.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-acj8ag08.js → roy-agent-core-tjnabyvq.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-qqaak24r.js → roy-agent-core-tkpp1ba0.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-9vhq7fvv.js → roy-agent-core-tpm1dd1g.js} +34 -88
- package/dist/shared/@ai-setting/{roy-agent-core-ed233k5z.js → roy-agent-core-v1n0106v.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-zxz5fkwj.js → roy-agent-core-v6yshv0z.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-mcy5apay.js → roy-agent-core-vf9sp6s3.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-q813vafd.js → roy-agent-core-vpcxv1ty.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-bxkjk7zx.js → roy-agent-core-vx8zc847.js} +4 -1
- package/dist/shared/@ai-setting/{roy-agent-core-xvt7kf7p.js → roy-agent-core-w9ryeg2p.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-es203w7p.js → roy-agent-core-wqs6yypq.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-hd3n57zs.js → roy-agent-core-wr1qexzq.js} +5 -5
- package/dist/shared/@ai-setting/roy-agent-core-wxrkcb17.js +56 -0
- package/dist/shared/@ai-setting/{roy-agent-core-0snm5hc5.js → roy-agent-core-y6shd91r.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-9vxh6f8h.js → roy-agent-core-y7fqtzk2.js} +7 -7
- package/dist/shared/@ai-setting/{roy-agent-core-cpcd24n2.js → roy-agent-core-yet36q5k.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-3fh1f7s7.js → roy-agent-core-yfpnkc4z.js} +5 -5
- package/dist/shared/@ai-setting/{roy-agent-core-0v27y99n.js → roy-agent-core-yh68z1bg.js} +65 -4
- package/package.json +18 -1
- /package/dist/shared/@ai-setting/{roy-agent-core-b4x6x4k2.js → roy-agent-core-m1fgtnrc.js} +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Task #5142 — `wecom-cli` EventSource handler
|
|
3
|
+
*
|
|
4
|
+
* ## 定位
|
|
5
|
+
*
|
|
6
|
+
* 企业微信(WeCom)官方 CLI 是 `@wecom/cli`(Rust 编译,npm 名 `@wecom/cli`,
|
|
7
|
+
* 二进制名 `wecom-cli`,仓库 https://github.com/WeComTeam/wecom-cli)。
|
|
8
|
+
* 本 handler 把企微接入 roy-agent 的 EventSource 体系,esType = **`wecom-cli`**。
|
|
9
|
+
*
|
|
10
|
+
* ## 为什么 esType 必须是新类型名(不能抄 `lark-cli`)
|
|
11
|
+
*
|
|
12
|
+
* `getHandlerForEsType(esType)` 是 v2 notify prompt 的**唯一分派 key**。
|
|
13
|
+
* 若落库写成 `lark-cli`,主 agent 会拿到「用 lark-cli 发到 oc_xxx」的 prompt ——
|
|
14
|
+
* 而企微根本没有 `oc_xxx`,agent 必然发不出去。
|
|
15
|
+
*
|
|
16
|
+
* ## 入站 transport 三方案(调研结论)
|
|
17
|
+
*
|
|
18
|
+
* | 方案 | 机制 | 结论 |
|
|
19
|
+
* |---|---|---|
|
|
20
|
+
* | A | 企微 webhook 回调 URL | 需**公网可达 URL** + 消息加解密(EncodingAESKey)。本机是内网 → 不可直接用 |
|
|
21
|
+
* | B | 轮询 `wecom-cli chat messages list` | 只支持**最近 7 天**,且需先知道 `chat_id` → 只能作补充,不能作主路径 |
|
|
22
|
+
* | C | 第三方桥接(CLI-WeChat-Bridge / golembot) | 依赖非官方项目(AGPL-3.0 传染风险)→ 只作参考实现 |
|
|
23
|
+
* | **D(采用)** | **企微长连接 WebSocket** `wss://openws.work.weixin.qq.com` | ✅ **无需公网 IP**、无需加解密;官方 SDK `@wecom/aibot-node-sdk` 已封装认证/心跳/重连/reply 串行队列 |
|
|
24
|
+
*
|
|
25
|
+
* 企微官方文档:
|
|
26
|
+
* - 长连接:https://developer.work.weixin.qq.com/document/path/101463
|
|
27
|
+
* - 接收消息(webhook 模式):https://developer.work.weixin.qq.com/document/path/100719
|
|
28
|
+
* - 官方 Node SDK:https://www.npmjs.com/package/@wecom/aibot-node-sdk
|
|
29
|
+
*
|
|
30
|
+
* ## 长连接协议要点(来自官方文档 101463)
|
|
31
|
+
*
|
|
32
|
+
* - 握手:`wss://openws.work.weixin.qq.com` → 发送 `aibot_subscribe`(body: bot_id + secret)
|
|
33
|
+
* - 入站:`aibot_msg_callback`(消息)/ `aibot_event_callback`(事件)
|
|
34
|
+
* - 出站:`aibot_respond_msg`(被动回复,**必须透传回调的 req_id**)/
|
|
35
|
+
* `aibot_send_msg`(主动推送)/ `aibot_respond_welcome_msg` / `aibot_respond_update_msg`
|
|
36
|
+
* - 心跳:`ping`,建议 30s
|
|
37
|
+
* - **每个机器人同一时刻只允许 1 条长连接**(新连接会踢掉旧连接)⇒ 必须做单实例互斥
|
|
38
|
+
* - 收到回调后 **24h 内**可回复该会话;限频 **30 条/分钟、1000 条/小时**
|
|
39
|
+
* - 流式消息需 10 分钟内 `finish=true`
|
|
40
|
+
*
|
|
41
|
+
* ## 本 handler 的边界(Phase 1 骨架)
|
|
42
|
+
*
|
|
43
|
+
* Phase 1 只做「spawn 子进程 + 解析行 JSON + 产出 EventSourceEvent」:
|
|
44
|
+
* 入站通过 `wecom-cli` 的**行 JSON 输出**(未来由长连接桥接进程产出 NDJSON),
|
|
45
|
+
* 出站由 notify prompt 指导 agent 直接调 `wecom-cli message aibot send --json ...`。
|
|
46
|
+
*
|
|
47
|
+
* @see packages/core/src/env/event-source/wecom-cli-handler.test.ts
|
|
48
|
+
*/
|
|
49
|
+
import type { EventSourceHandler, EventSourceConfig, EventMetadata, ReplyChannel } from "./types";
|
|
50
|
+
/** 企微事件源类型名(必须是**新**类型名,不能抄 lark-cli) */
|
|
51
|
+
export declare const WECOM_CLI_ES_TYPE = "wecom-cli";
|
|
52
|
+
/**
|
|
53
|
+
* 企微 chatId 前缀。
|
|
54
|
+
*
|
|
55
|
+
* 企微的 `chatid` 与飞书的 `oc_xxx` 语义不同(企微群聊 id 是业务方自己的
|
|
56
|
+
* chatid,单聊则是 userid),下游按前缀即可反解「这是企微会话」,
|
|
57
|
+
* 避免与 lark / cli 的 chatId 撞车。
|
|
58
|
+
*/
|
|
59
|
+
export declare const WECOM_CHAT_ID_PREFIX = "wecom:";
|
|
60
|
+
/** 企微长连接默认地址(官方文档 101463) */
|
|
61
|
+
export declare const WECOM_WS_DEFAULT_URL = "wss://openws.work.weixin.qq.com";
|
|
62
|
+
interface WecomCliConfig {
|
|
63
|
+
botId?: string;
|
|
64
|
+
secret?: string;
|
|
65
|
+
/** 覆盖默认命令(高级用法) */
|
|
66
|
+
command?: string;
|
|
67
|
+
/** 群机器人 webhook(可选) */
|
|
68
|
+
webhook?: string;
|
|
69
|
+
/**
|
|
70
|
+
* 独立凭据文件路径(Task #5228)。
|
|
71
|
+
*
|
|
72
|
+
* 多 bot 场景(如 wecom-bot-2)用独立凭据文件隔离凭据;
|
|
73
|
+
* 曾是 dead config(声明≠实现),现已接入 resolveWecomCredentials。
|
|
74
|
+
*/
|
|
75
|
+
credentialsPath?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 从 EventSourceConfig 读取企微相关字段。
|
|
79
|
+
*
|
|
80
|
+
* 优先级:顶级字段 → `options.*`(CLI 透传路径)。
|
|
81
|
+
*/
|
|
82
|
+
export declare function readWecomCliConfig(config: EventSourceConfig): WecomCliConfig;
|
|
83
|
+
/** 默认凭据文件路径(~/.roy-agent/wecom-credentials.json,权限 600) */
|
|
84
|
+
export declare const WECOM_CREDENTIALS_DEFAULT_PATH: string;
|
|
85
|
+
/** 凭据文件解析结果 */
|
|
86
|
+
export interface WecomCredentials {
|
|
87
|
+
botId: string;
|
|
88
|
+
secret: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 读取企微凭据文件(不抛)。
|
|
92
|
+
*
|
|
93
|
+
* 文件形态:`{"agent_id":"1000003","secret":"..."}`
|
|
94
|
+
* (agent_id 即智能机器人 BotID)。
|
|
95
|
+
*
|
|
96
|
+
* 缺失 / 坏 JSON / 字段不全 → null(调用方据此降级)。
|
|
97
|
+
*/
|
|
98
|
+
export declare function readWecomCredentialsFile(path: string): WecomCredentials | null;
|
|
99
|
+
/**
|
|
100
|
+
* 解析企微凭据(优先级:config 显式配置 > options.credentialsPath
|
|
101
|
+
* 独立凭据文件 > 默认凭据文件)。
|
|
102
|
+
*
|
|
103
|
+
* ★ Task #5228:`options.credentialsPath` 曾是 dead config——
|
|
104
|
+
* wecom-bot-2 配置了独立凭据文件却仍用生产默认凭据连接 → 无回复。
|
|
105
|
+
* 声明 ≠ 实现(与 #4946 provider temperature 同类坑)。
|
|
106
|
+
*
|
|
107
|
+
* 全缺 → undefined:调用方(start)据此**优雅降级**为骨架模式,
|
|
108
|
+
* 不 crash、不假装在收消息。
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveWecomCredentials(config: EventSourceConfig, credentialsPath?: string): WecomCredentials | undefined;
|
|
111
|
+
/** `buildWecomReplyChannel` 的入参 */
|
|
112
|
+
export interface WecomReplyChannelInput {
|
|
113
|
+
/** 企微会话 id(群聊有,单聊通常无) */
|
|
114
|
+
chatId?: string;
|
|
115
|
+
/** 消息 id(msgid) */
|
|
116
|
+
messageId?: string;
|
|
117
|
+
/** 会话类型:single / group */
|
|
118
|
+
chatType?: string;
|
|
119
|
+
/** 发送者 userid */
|
|
120
|
+
userId?: string;
|
|
121
|
+
/**
|
|
122
|
+
* ★ Task #5311(CV M1):事件源 ID —— 失败回执的 WS fallback 需要它
|
|
123
|
+
* 反查注册实例(`getWecomRespondTarget(sourceId)`)。缺了只剩 CLI 一条路。
|
|
124
|
+
*/
|
|
125
|
+
sourceId?: string;
|
|
126
|
+
/** ★ Task #5311:原始 req_id —— WS 被动回帧(带引用语义)需要 */
|
|
127
|
+
reqId?: string;
|
|
128
|
+
/**
|
|
129
|
+
* ★ Task #5360(补充需求):状态通知文本开关(来自 ES 配置)。
|
|
130
|
+
*
|
|
131
|
+
* 写进 `params` 让 delegate 快照带下去 —— 与 lark 侧同字段同语义。
|
|
132
|
+
* 严格 `=== true`;显式写 false(不省略,保留"源上显式关"的信息)。
|
|
133
|
+
*/
|
|
134
|
+
statusNotifications?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 构造企微的 replyChannel。
|
|
138
|
+
*
|
|
139
|
+
* ★ 硬约束:`_captureReplyChannelSnapshot()`(delegate-tool.ts:2864)要求
|
|
140
|
+
* `chatId || messageId` **至少一个非空**,否则整份快照被丢弃 →
|
|
141
|
+
* notify event 无回复目标 → 规则通知静默丢失。
|
|
142
|
+
*
|
|
143
|
+
* 因此本函数**恒不返回 chatId/messageId 双空**:
|
|
144
|
+
* - 群聊 → chatId = 原始 `chatid`
|
|
145
|
+
* - 单聊 → chatId = 原始 `userid`(企微单聊的回复目标就是 userid)
|
|
146
|
+
* - 全空 → chatId = `"wecom:unknown"`(占位,保证下游不丢快照)
|
|
147
|
+
*
|
|
148
|
+
* ⚠️ **`chatId` 必须是「可直接用于回复命令」的原始值**,不能加前缀:
|
|
149
|
+
* `extractReplyContext()` 返回的 chatId 会一路流到 notify prompt,
|
|
150
|
+
* agent 会把它原样塞进 `wecom-cli message aibot send --json '{"chat_id":"..."}'`。
|
|
151
|
+
* 加了前缀就等于给 agent 塞了个错误的 chat_id。
|
|
152
|
+
*
|
|
153
|
+
* 渠道区分靠 `type: "wecom-cli"` + `params.chatType`,
|
|
154
|
+
* 原始值同时冗余存进 `params.wecomChatId` / `params.wecomUserId`。
|
|
155
|
+
*/
|
|
156
|
+
export declare function buildWecomReplyChannel(input: WecomReplyChannelInput): ReplyChannel;
|
|
157
|
+
/** 解析结果 */
|
|
158
|
+
export interface ParsedWecomFrame {
|
|
159
|
+
metadata: EventMetadata;
|
|
160
|
+
text: string;
|
|
161
|
+
/** 原始 req_id(长连接回复必须透传) */
|
|
162
|
+
reqId?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* ★ Task #5271:构建企微 user message(payload.message)。
|
|
166
|
+
*
|
|
167
|
+
* 企微的 user message 是**纯文本**(lark 是
|
|
168
|
+
* JSON.stringify({rawData, recommendedAction}),recommendedAction
|
|
169
|
+
* 能进 user message;企微不能)。#5251 把 quotedText 挂到
|
|
170
|
+
* recommendedAction.replyContext,但 LLM 永远看不到
|
|
171
|
+
* (#5212 MAJOR-2 同型问题在企微重演)。
|
|
172
|
+
*
|
|
173
|
+
* replyContext 存在时把引用内容拼进正文,格式:
|
|
174
|
+
*
|
|
175
|
+
* [用户正文]
|
|
176
|
+
*
|
|
177
|
+
* [引用消息] 被引用的原文内容
|
|
178
|
+
*
|
|
179
|
+
* 无 replyContext 时返回原正文(字节不变,向后兼容)。
|
|
180
|
+
*/
|
|
181
|
+
export declare function buildWecomUserMessage(text: string, replyContext?: {
|
|
182
|
+
quotedText?: string;
|
|
183
|
+
}): string;
|
|
184
|
+
/**
|
|
185
|
+
* 解析企微长连接帧 → EventSourceEvent 元数据。
|
|
186
|
+
*
|
|
187
|
+
* 无效帧返回 `null`(不抛)—— 非法输入不能拖垮整个事件源。
|
|
188
|
+
*
|
|
189
|
+
* @param frame 原始帧(`aibot_msg_callback` / `aibot_event_callback`)
|
|
190
|
+
*/
|
|
191
|
+
export declare function parseWecomFrame(raw: unknown): ParsedWecomFrame | null;
|
|
192
|
+
/**
|
|
193
|
+
* 企微渠道**恒需**放行的 channel-only 工具。
|
|
194
|
+
*
|
|
195
|
+
* `wecom_respond` 是企微唯一出站通道(wecom-cli 主路径 + WS 回帧 fallback)。
|
|
196
|
+
* 它带有 `metadata.channelOnly = true`(Task #5333),默认对所有 agent 隐藏
|
|
197
|
+
* ——如果不在这里显式放行,企微事件的 agent 将**无法回复**用户。
|
|
198
|
+
*/
|
|
199
|
+
export declare const WECOM_DEFAULT_EXTEND_TOOLS: readonly string[];
|
|
200
|
+
/**
|
|
201
|
+
* 解析企微渠道的 extendTools(默认 + 配置,去重保序)。
|
|
202
|
+
*
|
|
203
|
+
* 顺序:WECOM_DEFAULT_EXTEND_TOOLS 在前(确定性前缀),config.extendTools 在后。
|
|
204
|
+
* 去重保持首次出现位置 —— 保证同一 config 多次调用结果**逐字节一致**
|
|
205
|
+
* (对齐 buildWecomChannelSystemPrompt 的稳定性纪律,避免无意义的 diff)。
|
|
206
|
+
*
|
|
207
|
+
* @param config 事件源配置(可选字段 extendTools)
|
|
208
|
+
*/
|
|
209
|
+
export declare function resolveWecomExtendTools(config: EventSourceConfig | undefined): string[];
|
|
210
|
+
/** 企微渠道 system prompt 的章节标题(lean 卡片指回这里) */
|
|
211
|
+
export declare const WECOM_CHANNEL_PROMPT_SECTION_TITLE = "\u2B50 \u4F01\u4E1A\u5FAE\u4FE1\u56DE\u590D\u89C4\u8303\uFF08system prompt \u6CE8\u5165\u7248\uFF09";
|
|
212
|
+
/** `buildWecomChannelSystemPrompt` 入参(只允许渠道级配置,禁 event) */
|
|
213
|
+
export interface WecomChannelSystemPromptInput {
|
|
214
|
+
/** 智能机器人 BotID(渠道身份,可选) */
|
|
215
|
+
botId?: string;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 构造企微渠道的 system prompt(单点构造,只读渠道配置)。
|
|
219
|
+
*
|
|
220
|
+
* ★ 与 buildLarkChannelSystemPrompt 同款纪律:
|
|
221
|
+
* - 构造入参**只来自渠道配置**,禁止读 event/rawEvent
|
|
222
|
+
* (否则字节漂移破坏 LLM prompt cache 前缀一致性);
|
|
223
|
+
* - 同一入参多次调用**逐字节一致**;
|
|
224
|
+
* - per-event 动态部分(chatId/messageId/正文)只进
|
|
225
|
+
* recommendedAction 动态卡片,不进这里。
|
|
226
|
+
*
|
|
227
|
+
* 内容:回复命令模板 + chatId 语义(单聊=userid / 群聊=chatid)+
|
|
228
|
+
* 出站纪律 + 文档交付规范(文档类生成不落本地路径,给企微可读内容)。
|
|
229
|
+
*/
|
|
230
|
+
export declare function buildWecomChannelSystemPrompt(opts: WecomChannelSystemPromptInput): string;
|
|
231
|
+
/** `buildWecomNotifyPrompt` 入参 */
|
|
232
|
+
export interface WecomNotifyPromptInput {
|
|
233
|
+
chatId?: string;
|
|
234
|
+
messageId?: string;
|
|
235
|
+
chatType?: string;
|
|
236
|
+
userId?: string;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 构造 notify prompt —— 四段式(与 lark-cli 版结构对齐,内容换成企微)。
|
|
240
|
+
*
|
|
241
|
+
* ① 用哪个 CLI 回消息
|
|
242
|
+
* ② 什么 flag(--json / chat_id / msg_type / markdown.content)
|
|
243
|
+
* ③ 禁什么(禁 curl、禁直连 qyapi、禁 lark-cli flag)
|
|
244
|
+
* ④ 怎么判成功(exit code + 解析 error.type / error.code)
|
|
245
|
+
*/
|
|
246
|
+
export declare function buildWecomNotifyPrompt(input: WecomNotifyPromptInput): string;
|
|
247
|
+
/**
|
|
248
|
+
* 探测 `wecom-cli` 二进制是否存在。
|
|
249
|
+
*
|
|
250
|
+
* 为什么需要预探测:`spawn("exec <cmd>", {shell:true})` 在二进制缺失时
|
|
251
|
+
* **不会同步抛错**,而是让 shell 以 exit 127 退出 —— 事件源会短暂进入
|
|
252
|
+
* "running" 再变 error,状态闪烁且 CLI status 展示不及时。
|
|
253
|
+
*
|
|
254
|
+
* 预探测让「二进制不存在」在 start() 里**同步**变成可见的 error 状态。
|
|
255
|
+
*
|
|
256
|
+
* ⚠️ **Windows 兼容**(DS review MINOR M3):Windows 上可执行文件可能是
|
|
257
|
+
* `wecom-cli.exe` / `wecom-cli.cmd`,纯命令名探测必须带这些扩展名兜底,
|
|
258
|
+
* 否则在 Windows 上永远判定「找不到」。
|
|
259
|
+
*
|
|
260
|
+
* @param binName 二进制名(默认 `wecom-cli`)
|
|
261
|
+
*/
|
|
262
|
+
export declare function resolveWecomBinary(binName?: string): {
|
|
263
|
+
found: boolean;
|
|
264
|
+
path?: string;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* 从自定义命令串里解析出「可执行文件」并探测是否存在。
|
|
268
|
+
*
|
|
269
|
+
* 自定义 command 的形态是 shell 命令串(`exec <cmd> <args...>`),
|
|
270
|
+
* 可能带 `exec ` 前缀与引号。这里只做**保守**判定:
|
|
271
|
+
* - 取第一个「像路径」的 token(含 `/`)
|
|
272
|
+
* - 若是绝对路径且不存在 → 判定不可用
|
|
273
|
+
* - 其它情况一律返回 found=true(不误伤,避免把正常命令判死)
|
|
274
|
+
*
|
|
275
|
+
* @returns found=false 表示该命令的绝对路径可执行文件不存在
|
|
276
|
+
*/
|
|
277
|
+
export declare function probeCommandBinary(command: string): {
|
|
278
|
+
found: boolean;
|
|
279
|
+
path?: string;
|
|
280
|
+
};
|
|
281
|
+
/** WsBridge 需要的最小 WebSocket 面(测试 mock 友好) */
|
|
282
|
+
export interface WebSocketLike {
|
|
283
|
+
on(ev: string, cb: (...args: unknown[]) => void): void;
|
|
284
|
+
send(data: string): void;
|
|
285
|
+
close(): void;
|
|
286
|
+
terminate(): void;
|
|
287
|
+
}
|
|
288
|
+
/** 生产默认工厂:动态 import('ws')(bun/Node 双 runtime 兼容) */
|
|
289
|
+
export declare function defaultWecomWsFactory(): Promise<(url: string) => WebSocketLike>;
|
|
290
|
+
export declare const wecomCliHandler: EventSourceHandler;
|
|
291
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Task #5174(用户明令 2026-11-08 11:29)— 企微出站发送器(CLI 主路径)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么改 CLI
|
|
5
|
+
*
|
|
6
|
+
* 用户明令:「彻底改造下回复出站用 cli 吧,已经验证可以收到消息了,不用 ws 回」。
|
|
7
|
+
* `wecom-cli auth init` 扫码授权已完成(auth show = authorized),CLI 出站实测
|
|
8
|
+
* `success: true`;WS 回帧(`aibot_respond_msg`)保留为 **fallback**。
|
|
9
|
+
*
|
|
10
|
+
* ## CLI 出站的三个硬约束(实测)
|
|
11
|
+
*
|
|
12
|
+
* 1. **msg_type 没有 text** —— 只支持 markdown / image / file / voice / video;
|
|
13
|
+
* 传 `text` 报「存在未知字段'text'」⇒ 统一用 `markdown`。
|
|
14
|
+
* 2. **chat_id 必须是真实 userid**(形如 `wotnB7Wg...`),WS 回调里的
|
|
15
|
+
* `from.userid`(如 `DongZhaoKun`)会被拒(40073 非法的 chat_id)。
|
|
16
|
+
* 因此需要 `contact users search` 解析 + 内存缓存。
|
|
17
|
+
* 3. 响应是 JSON,成功形如 `{"success": true}`,失败形如
|
|
18
|
+
* `{"error": {"code": ..., "message": ...}}`。
|
|
19
|
+
*
|
|
20
|
+
* @see Task #5173 操作记录 #40148(实测取证)
|
|
21
|
+
*/
|
|
22
|
+
/** 默认发送硬超时(CLI 正常 < 2s,15s 是充裕余量) */
|
|
23
|
+
export declare const WECOM_CLI_SEND_TIMEOUT_MS = 15000;
|
|
24
|
+
/** contact 解析硬超时 */
|
|
25
|
+
export declare const WECOM_CLI_RESOLVE_TIMEOUT_MS = 15000;
|
|
26
|
+
/**
|
|
27
|
+
* 解析 wecom-cli 可执行文件路径。
|
|
28
|
+
*
|
|
29
|
+
* 优先级:`WECOM_CLI_BIN` 环境变量 → `wecom-cli`(PATH)。
|
|
30
|
+
* 本机 CLI 是 npm 装的平台子包,未在 PATH 里时必须用 env 指定。
|
|
31
|
+
*
|
|
32
|
+
* ## ⚠️ 隔离 HOME 部署必读(Task #5174 实测)
|
|
33
|
+
*
|
|
34
|
+
* CLI 授权凭据在 `$HOME/.config/wecom/credentials.enc`。若 roy-agent 跑在
|
|
35
|
+
* 隔离 HOME(测试 / 多租户)下,**只拷 credentials.enc 无效** ——
|
|
36
|
+
* `auth show` 仍会报 unauthorized。
|
|
37
|
+
*
|
|
38
|
+
* CLI 支持 `WECOM_CLI_CONFIG_DIR` 覆盖配置目录,部署时设置它指向
|
|
39
|
+
* **已授权**的配置目录即可(本机示例:`/home/dzk/.config/wecom`)。
|
|
40
|
+
* 该变量由子进程从 `process.env` 继承,本模块无需显式处理。
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveWecomCliBinary(): string;
|
|
43
|
+
/** 构造 `message aibot send` 的 argv(不含二进制名) */
|
|
44
|
+
export declare function buildWecomCliSendArgs(input: {
|
|
45
|
+
chatId: string;
|
|
46
|
+
content: string;
|
|
47
|
+
}): string[];
|
|
48
|
+
/** 构造 `contact users search` 的 argv(用于把 WS userid 解析成 CLI chat_id) */
|
|
49
|
+
export declare function buildWecomCliResolveArgs(keywords: string): string[];
|
|
50
|
+
/**
|
|
51
|
+
* 解析 CLI 输出(stdout 是 JSON)。
|
|
52
|
+
*
|
|
53
|
+
* - `{"error": {...}}` → 失败(读 error.code / error.message)
|
|
54
|
+
* - `{"success": true}` → 成功
|
|
55
|
+
* - 无法 parse → 失败(把原文片段带出来便于排查)
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseWecomCliResult(stdout: string): {
|
|
58
|
+
ok: true;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
error: string;
|
|
62
|
+
};
|
|
63
|
+
/** 从 contact search 输出解析真实 userid */
|
|
64
|
+
export declare function parseWecomCliUserSearch(stdout: string, fallback: string): string;
|
|
65
|
+
/** 测试用:清空解析缓存 */
|
|
66
|
+
export declare function __resetWecomCliChatIdCacheForTest(): void;
|
|
67
|
+
/**
|
|
68
|
+
* 把 WS 回调里的 userid 解析成 CLI 可用的 chat_id。
|
|
69
|
+
*
|
|
70
|
+
* 已有缓存 → 直接返回;否则 `contact users search --set keywords=<userid>`
|
|
71
|
+
* 取 `users[0].userid`;解析失败返回原值(让 CLI 自己报 40073,便于排障)。
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveWecomCliChatId(userId: string): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* 用 CLI 发 markdown 消息(**永不抛**)。
|
|
76
|
+
*
|
|
77
|
+
* @returns `{ok:true}` / `{ok:false, error}`;调用方据此决定是否走 WS fallback
|
|
78
|
+
*/
|
|
79
|
+
export declare function sendWecomCliMarkdown(input: {
|
|
80
|
+
chatId: string;
|
|
81
|
+
content: string;
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
ok: true;
|
|
85
|
+
} | {
|
|
86
|
+
ok: false;
|
|
87
|
+
error: string;
|
|
88
|
+
}>;
|
|
@@ -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
|
+
}>;
|