@wecom/wecom-openclaw-plugin 2026.4.2 → 2026.4.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +395 -10
- package/dist/index.d.ts +2 -4
- package/dist/index.js +58 -0
- package/dist/src/accounts.d.ts +57 -0
- package/dist/src/accounts.js +222 -0
- package/dist/src/agent/api-client.d.ts +94 -0
- package/dist/src/agent/api-client.js +292 -0
- package/dist/src/agent/handler.d.ts +63 -0
- package/dist/src/agent/handler.js +551 -0
- package/dist/src/agent/index.d.ts +5 -0
- package/dist/src/agent/index.js +5 -0
- package/dist/src/agent/webhook.d.ts +24 -0
- package/dist/src/agent/webhook.js +228 -0
- package/dist/src/agent/xml.d.ts +21 -0
- package/dist/src/agent/xml.js +41 -0
- package/dist/src/channel.js +579 -0
- package/dist/src/chat-queue.d.ts +31 -0
- package/dist/src/chat-queue.js +45 -0
- package/dist/{esm/types/src → src}/const.d.ts +46 -0
- package/dist/{esm/src → src}/const.js +77 -30
- package/dist/{esm/src → src}/dm-policy.js +6 -9
- package/dist/src/dynamic-agent.d.ts +37 -0
- package/dist/src/dynamic-agent.js +60 -0
- package/dist/src/dynamic-routing.d.ts +65 -0
- package/dist/src/dynamic-routing.js +58 -0
- package/dist/{esm/src → src}/group-policy.js +8 -22
- package/dist/src/http.d.ts +26 -0
- package/dist/src/http.js +93 -0
- package/dist/{esm/types/src → src}/interface.d.ts +0 -2
- package/dist/src/interface.js +4 -0
- package/dist/src/mcp/index.js +6 -0
- package/dist/{esm/src → src}/mcp/interceptors/biz-error.js +2 -6
- package/dist/{esm/src → src}/mcp/interceptors/index.js +6 -10
- package/dist/{esm/src → src}/mcp/interceptors/msg-media.js +6 -10
- package/dist/{esm/src → src}/mcp/interceptors/smartpage-create.js +2 -6
- package/dist/{esm/src → src}/mcp/interceptors/smartpage-export.js +2 -6
- package/dist/src/mcp/interceptors/types.js +7 -0
- package/dist/{esm/src → src}/mcp/schema.js +1 -4
- package/dist/{esm/src → src}/mcp/tool.js +4 -8
- package/dist/{esm/src → src}/mcp/transport.js +15 -16
- package/dist/{esm/src → src}/media-handler.js +6 -10
- package/dist/{esm/src → src}/media-uploader.js +64 -10
- package/dist/{esm/src → src}/message-parser.js +1 -4
- package/dist/{esm/types/src → src}/message-sender.d.ts +18 -0
- package/dist/{esm/src → src}/message-sender.js +45 -11
- package/dist/{esm/src → src}/monitor.js +145 -292
- package/dist/{esm/src → src}/onboarding.js +22 -26
- package/dist/{esm/types/src → src}/openclaw-compat.d.ts +49 -0
- package/dist/{esm/src → src}/openclaw-compat.js +133 -27
- package/dist/{esm/src → src}/reqid-store.js +4 -7
- package/dist/{esm/src → src}/runtime.js +2 -5
- package/dist/src/shared/command-auth.d.ts +23 -0
- package/dist/src/shared/command-auth.js +73 -0
- package/dist/src/shared/xml-parser.d.ts +46 -0
- package/dist/src/shared/xml-parser.js +210 -0
- package/dist/{esm/src → src}/state-dir-resolve.js +3 -7
- package/dist/{esm/src → src}/state-manager.js +84 -16
- package/dist/src/target.d.ts +35 -0
- package/dist/src/target.js +67 -0
- package/dist/src/template-card-manager.d.ts +55 -0
- package/dist/src/template-card-manager.js +205 -0
- package/dist/{esm/src → src}/template-card-parser.js +17 -8
- package/dist/{esm/src → src}/timeout.js +2 -5
- package/dist/src/types/account.d.ts +29 -0
- package/dist/src/types/account.js +4 -0
- package/dist/src/types/config.d.ts +91 -0
- package/dist/src/types/config.js +7 -0
- package/dist/src/types/constants.d.ts +42 -0
- package/dist/src/types/constants.js +42 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/types/message.d.ts +238 -0
- package/dist/src/types/message.js +5 -0
- package/dist/{esm/types/src → src}/utils.d.ts +41 -0
- package/dist/{esm/src → src}/utils.js +24 -10
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +24 -0
- package/dist/src/webhook/command-auth.d.ts +47 -0
- package/dist/src/webhook/command-auth.js +96 -0
- package/dist/src/webhook/gateway.d.ts +36 -0
- package/dist/src/webhook/gateway.js +235 -0
- package/dist/src/webhook/handler.d.ts +18 -0
- package/dist/src/webhook/handler.js +420 -0
- package/dist/src/webhook/helpers.d.ts +156 -0
- package/dist/src/webhook/helpers.js +833 -0
- package/dist/src/webhook/http.d.ts +26 -0
- package/dist/src/webhook/http.js +94 -0
- package/dist/src/webhook/index.d.ts +11 -0
- package/dist/src/webhook/index.js +14 -0
- package/dist/src/webhook/media.d.ts +29 -0
- package/dist/src/webhook/media.js +88 -0
- package/dist/src/webhook/monitor.d.ts +59 -0
- package/dist/src/webhook/monitor.js +1161 -0
- package/dist/src/webhook/state.d.ts +220 -0
- package/dist/src/webhook/state.js +491 -0
- package/dist/src/webhook/target.d.ts +41 -0
- package/dist/src/webhook/target.js +132 -0
- package/dist/src/webhook/types.d.ts +347 -0
- package/dist/src/webhook/types.js +33 -0
- package/dist/src/webhook/video-frame.d.ts +13 -0
- package/dist/src/webhook/video-frame.js +30 -0
- package/package.json +15 -7
- package/dist/cjs/index.js +0 -45
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/src/channel.js +0 -339
- package/dist/cjs/src/channel.js.map +0 -1
- package/dist/cjs/src/const.js +0 -137
- package/dist/cjs/src/const.js.map +0 -1
- package/dist/cjs/src/dm-policy.js +0 -89
- package/dist/cjs/src/dm-policy.js.map +0 -1
- package/dist/cjs/src/group-policy.js +0 -133
- package/dist/cjs/src/group-policy.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/biz-error.js +0 -70
- package/dist/cjs/src/mcp/interceptors/biz-error.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/index.js +0 -73
- package/dist/cjs/src/mcp/interceptors/index.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/msg-media.js +0 -159
- package/dist/cjs/src/mcp/interceptors/msg-media.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js +0 -157
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js +0 -93
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +0 -1
- package/dist/cjs/src/mcp/schema.js +0 -104
- package/dist/cjs/src/mcp/schema.js.map +0 -1
- package/dist/cjs/src/mcp/tool.js +0 -208
- package/dist/cjs/src/mcp/tool.js.map +0 -1
- package/dist/cjs/src/mcp/transport.js +0 -417
- package/dist/cjs/src/mcp/transport.js.map +0 -1
- package/dist/cjs/src/media-handler.js +0 -129
- package/dist/cjs/src/media-handler.js.map +0 -1
- package/dist/cjs/src/media-uploader.js +0 -284
- package/dist/cjs/src/media-uploader.js.map +0 -1
- package/dist/cjs/src/message-parser.js +0 -134
- package/dist/cjs/src/message-parser.js.map +0 -1
- package/dist/cjs/src/message-sender.js +0 -89
- package/dist/cjs/src/message-sender.js.map +0 -1
- package/dist/cjs/src/monitor.js +0 -991
- package/dist/cjs/src/monitor.js.map +0 -1
- package/dist/cjs/src/onboarding.js +0 -182
- package/dist/cjs/src/onboarding.js.map +0 -1
- package/dist/cjs/src/openclaw-compat.js +0 -400
- package/dist/cjs/src/openclaw-compat.js.map +0 -1
- package/dist/cjs/src/reqid-store.js +0 -88
- package/dist/cjs/src/reqid-store.js.map +0 -1
- package/dist/cjs/src/runtime.js +0 -16
- package/dist/cjs/src/runtime.js.map +0 -1
- package/dist/cjs/src/state-dir-resolve.js +0 -35
- package/dist/cjs/src/state-dir-resolve.js.map +0 -1
- package/dist/cjs/src/state-manager.js +0 -158
- package/dist/cjs/src/state-manager.js.map +0 -1
- package/dist/cjs/src/template-card-parser.js +0 -641
- package/dist/cjs/src/template-card-parser.js.map +0 -1
- package/dist/cjs/src/timeout.js +0 -42
- package/dist/cjs/src/timeout.js.map +0 -1
- package/dist/cjs/src/utils.js +0 -60
- package/dist/cjs/src/utils.js.map +0 -1
- package/dist/cjs/src/version.js +0 -26
- package/dist/cjs/src/version.js.map +0 -1
- package/dist/esm/index.js +0 -41
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/src/channel.js +0 -337
- package/dist/esm/src/channel.js.map +0 -1
- package/dist/esm/src/const.js.map +0 -1
- package/dist/esm/src/dm-policy.js.map +0 -1
- package/dist/esm/src/group-policy.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/biz-error.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/index.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/msg-media.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +0 -1
- package/dist/esm/src/mcp/schema.js.map +0 -1
- package/dist/esm/src/mcp/tool.js.map +0 -1
- package/dist/esm/src/mcp/transport.js.map +0 -1
- package/dist/esm/src/media-handler.js.map +0 -1
- package/dist/esm/src/media-uploader.js.map +0 -1
- package/dist/esm/src/message-parser.js.map +0 -1
- package/dist/esm/src/message-sender.js.map +0 -1
- package/dist/esm/src/monitor.js.map +0 -1
- package/dist/esm/src/onboarding.js.map +0 -1
- package/dist/esm/src/openclaw-compat.js.map +0 -1
- package/dist/esm/src/reqid-store.js.map +0 -1
- package/dist/esm/src/runtime.js.map +0 -1
- package/dist/esm/src/state-dir-resolve.js.map +0 -1
- package/dist/esm/src/state-manager.js.map +0 -1
- package/dist/esm/src/template-card-parser.js.map +0 -1
- package/dist/esm/src/timeout.js.map +0 -1
- package/dist/esm/src/utils.js.map +0 -1
- package/dist/esm/src/version.js +0 -23
- package/dist/esm/src/version.js.map +0 -1
- package/dist/esm/types/index.d.ts +0 -9
- package/dist/esm/types/src/version.d.ts +0 -2
- /package/dist/{esm/types/src → src}/channel.d.ts +0 -0
- /package/dist/{esm/types/src → src}/dm-policy.d.ts +0 -0
- /package/dist/{esm/types/src → src}/group-policy.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/index.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/biz-error.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/index.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/msg-media.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-create.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-export.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/types.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/schema.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/tool.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/transport.d.ts +0 -0
- /package/dist/{esm/types/src → src}/media-handler.d.ts +0 -0
- /package/dist/{esm/types/src → src}/media-uploader.d.ts +0 -0
- /package/dist/{esm/types/src → src}/message-parser.d.ts +0 -0
- /package/dist/{esm/types/src → src}/monitor.d.ts +0 -0
- /package/dist/{esm/types/src → src}/onboarding.d.ts +0 -0
- /package/dist/{esm/types/src → src}/reqid-store.d.ts +0 -0
- /package/dist/{esm/types/src → src}/runtime.d.ts +0 -0
- /package/dist/{esm/types/src → src}/state-dir-resolve.d.ts +0 -0
- /package/dist/{esm/types/src → src}/state-manager.d.ts +0 -0
- /package/dist/{esm/types/src → src}/template-card-parser.d.ts +0 -0
- /package/dist/{esm/types/src → src}/timeout.d.ts +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook 模式专用类型定义
|
|
3
|
+
*
|
|
4
|
+
* 从 @mocrane/wecom monitor/types.ts 完整迁移,适配目标项目类型体系。
|
|
5
|
+
*/
|
|
6
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
|
8
|
+
import type { ResolvedWeComAccount } from "../utils.js";
|
|
9
|
+
/** StreamState 过期时间 (10 分钟) */
|
|
10
|
+
export declare const STREAM_TTL_MS: number;
|
|
11
|
+
/** ActiveReply 过期时间 (1 小时) */
|
|
12
|
+
export declare const ACTIVE_REPLY_TTL_MS: number;
|
|
13
|
+
/** 消息防抖间隔 (500ms) */
|
|
14
|
+
export declare const DEFAULT_DEBOUNCE_MS = 500;
|
|
15
|
+
/** stream 回复最大字节数 (20KB) */
|
|
16
|
+
export declare const STREAM_MAX_BYTES = 20480;
|
|
17
|
+
/** 企微 Bot 回复窗口 (6 分钟) */
|
|
18
|
+
export declare const BOT_WINDOW_MS: number;
|
|
19
|
+
/** 超时安全边际 (30 秒) */
|
|
20
|
+
export declare const BOT_SWITCH_MARGIN_MS = 30000;
|
|
21
|
+
/** HTTP 请求超时 (15 秒) */
|
|
22
|
+
export declare const REQUEST_TIMEOUT_MS = 15000;
|
|
23
|
+
/** 自动清理间隔 (60 秒) */
|
|
24
|
+
export declare const PRUNE_INTERVAL_MS = 60000;
|
|
25
|
+
/** 固定 Webhook 路径 */
|
|
26
|
+
export declare const WEBHOOK_PATHS: {
|
|
27
|
+
/** Bot 模式历史兼容路径 */
|
|
28
|
+
readonly BOT: "/wecom";
|
|
29
|
+
/** Bot 模式历史备用兼容路径 */
|
|
30
|
+
readonly BOT_ALT: "/wecom/bot";
|
|
31
|
+
/** Bot 模式推荐路径前缀 */
|
|
32
|
+
readonly BOT_PLUGIN: "/plugins/wecom/bot";
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Webhook 模式下的额外账号配置字段
|
|
36
|
+
*/
|
|
37
|
+
export interface WebhookAccountConfig {
|
|
38
|
+
/** 连接模式:webhook | websocket(默认 websocket) */
|
|
39
|
+
connectionMode?: "webhook" | "websocket";
|
|
40
|
+
/** Webhook 验证 token */
|
|
41
|
+
token?: string;
|
|
42
|
+
/** AES 加密密钥(43 字符 Base64) */
|
|
43
|
+
encodingAESKey?: string;
|
|
44
|
+
/** 接收方 ID */
|
|
45
|
+
receiveId?: string;
|
|
46
|
+
/** enter_chat 欢迎消息 */
|
|
47
|
+
welcomeText?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 解析后的 Webhook 账号信息
|
|
51
|
+
* 在 ResolvedWeComAccount 基础上增加 Webhook 专用字段
|
|
52
|
+
*/
|
|
53
|
+
export interface ResolvedWebhookAccount extends ResolvedWeComAccount {
|
|
54
|
+
connectionMode: "webhook";
|
|
55
|
+
token: string;
|
|
56
|
+
encodingAESKey: string;
|
|
57
|
+
receiveId: string;
|
|
58
|
+
welcomeText?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Webhook 运行时环境
|
|
62
|
+
*
|
|
63
|
+
* 包含基础的日志和错误报告接口,用于解耦对 PluginRuntime 的直接依赖。
|
|
64
|
+
*/
|
|
65
|
+
export interface WecomRuntimeEnv {
|
|
66
|
+
log?: (message: string) => void;
|
|
67
|
+
error?: (message: string) => void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Webhook 目标上下文
|
|
71
|
+
*
|
|
72
|
+
* 描述一个注册的 Bot 接收端点。包含处理该端点所需的所有上下文信息。
|
|
73
|
+
*/
|
|
74
|
+
export interface WecomWebhookTarget {
|
|
75
|
+
/** 解析后的 Bot 账号信息 (Token, AESKey 等) */
|
|
76
|
+
account: ResolvedWebhookAccount;
|
|
77
|
+
/** 插件全局配置 */
|
|
78
|
+
config: OpenClawConfig;
|
|
79
|
+
/** 运行时环境 (日志) */
|
|
80
|
+
runtime: WecomRuntimeEnv;
|
|
81
|
+
/** OpenClaw 插件核心运行时 */
|
|
82
|
+
core: PluginRuntime;
|
|
83
|
+
/** 该 Target 注册的 Webhook 路径 */
|
|
84
|
+
path: string;
|
|
85
|
+
/** 反馈最后接收/发送时间 */
|
|
86
|
+
statusSink?: (patch: {
|
|
87
|
+
lastInboundAt?: number;
|
|
88
|
+
lastOutboundAt?: number;
|
|
89
|
+
}) => void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 流式会话状态
|
|
93
|
+
*
|
|
94
|
+
* 记录一个流式请求的生命周期状态。
|
|
95
|
+
*/
|
|
96
|
+
export interface StreamState {
|
|
97
|
+
/** 唯一会话 ID */
|
|
98
|
+
streamId: string;
|
|
99
|
+
/** 关联的企业微信消息 ID(用于去重) */
|
|
100
|
+
msgid?: string;
|
|
101
|
+
/** 会话键(同一人同一会话,用于队列/批次) */
|
|
102
|
+
conversationKey?: string;
|
|
103
|
+
/** 批次键(conversationKey + 批次序号) */
|
|
104
|
+
batchKey?: string;
|
|
105
|
+
/** 触发者 userid(用于 Agent 私信兜底) */
|
|
106
|
+
userId?: string;
|
|
107
|
+
/** 会话类型(用于群聊兜底逻辑) */
|
|
108
|
+
chatType?: "group" | "direct";
|
|
109
|
+
/** 群聊 chatid(用于日志/提示,不用于 Agent 发群) */
|
|
110
|
+
chatId?: string;
|
|
111
|
+
/** 智能机器人 aibotid(用于 taskKey 生成与日志) */
|
|
112
|
+
aibotid?: string;
|
|
113
|
+
/** Bot 回调幂等键(用于最终交付幂等) */
|
|
114
|
+
taskKey?: string;
|
|
115
|
+
/** 创建时间 */
|
|
116
|
+
createdAt: number;
|
|
117
|
+
/** 最后更新时间(用于 Prune) */
|
|
118
|
+
updatedAt: number;
|
|
119
|
+
/** 是否已开始处理(Agent 已介入) */
|
|
120
|
+
started: boolean;
|
|
121
|
+
/** 是否已完成(Agent 输出完毕或出错) */
|
|
122
|
+
finished: boolean;
|
|
123
|
+
/** 错误信息 */
|
|
124
|
+
error?: string;
|
|
125
|
+
/** 已积累的响应内容(用于长轮询返回) */
|
|
126
|
+
content: string;
|
|
127
|
+
/** 过程中生成的图片(Base64 + MD5) */
|
|
128
|
+
images?: Array<{
|
|
129
|
+
base64: string;
|
|
130
|
+
md5: string;
|
|
131
|
+
}>;
|
|
132
|
+
/** 兜底模式(仅作为内部状态,不暴露给企微) */
|
|
133
|
+
fallbackMode?: "media" | "timeout" | "error";
|
|
134
|
+
/** 群内兜底提示是否已发送(用于防重复刷屏) */
|
|
135
|
+
fallbackPromptSentAt?: number;
|
|
136
|
+
/** Agent 私信最终交付是否已完成(用于防重复发送) */
|
|
137
|
+
finalDeliveredAt?: number;
|
|
138
|
+
/** 用于私信兜底的完整内容(不受 STREAM_MAX_BYTES 限制,但仍需上限保护) */
|
|
139
|
+
dmContent?: string;
|
|
140
|
+
/** 已通过 Agent 私信发送过的媒体标识(防重复发送附件) */
|
|
141
|
+
agentMediaKeys?: string[];
|
|
142
|
+
/** 是否来自 WebSocket 长链接模式(用于跳过 6 分钟超时等 webhook 特有逻辑) */
|
|
143
|
+
wsMode?: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Webhook 入站消息(解密后的 JSON 格式)
|
|
147
|
+
*
|
|
148
|
+
* 字段命名对齐企微 Bot 回调的实际 JSON 结构(与原版 WecomBotInboundBase 一致):
|
|
149
|
+
* - 发送者:`from.userid`(非 from.user_id)
|
|
150
|
+
* - 会话类型:`chattype`(扁平字段,非 chat_info.chat_type)
|
|
151
|
+
* - 群聊 ID:`chatid`(扁平字段,非 chat_info.chat_id)
|
|
152
|
+
* - 事件类型:`event.eventtype`(非 event.event_type)
|
|
153
|
+
* - 流 ID:`stream.id`(非 stream.stream_id)
|
|
154
|
+
*/
|
|
155
|
+
export interface WebhookInboundMessage {
|
|
156
|
+
msgtype: string;
|
|
157
|
+
msgid?: string;
|
|
158
|
+
/** 企微 Bot 回调中的机器人 ID */
|
|
159
|
+
aibotid?: string;
|
|
160
|
+
/** 会话类型:single | group(扁平字段) */
|
|
161
|
+
chattype?: "single" | "group";
|
|
162
|
+
/** 群聊 ID(扁平字段,仅群组时存在) */
|
|
163
|
+
chatid?: string;
|
|
164
|
+
/** 附件数量 */
|
|
165
|
+
attachment_count?: number;
|
|
166
|
+
text?: {
|
|
167
|
+
content?: string;
|
|
168
|
+
};
|
|
169
|
+
image?: {
|
|
170
|
+
url?: string;
|
|
171
|
+
aeskey?: string;
|
|
172
|
+
encrypt_file_key?: string;
|
|
173
|
+
file_url?: string;
|
|
174
|
+
base64?: string;
|
|
175
|
+
md5?: string;
|
|
176
|
+
};
|
|
177
|
+
file?: {
|
|
178
|
+
url?: string;
|
|
179
|
+
aeskey?: string;
|
|
180
|
+
encrypt_file_key?: string;
|
|
181
|
+
file_url?: string;
|
|
182
|
+
filename?: string;
|
|
183
|
+
file_name?: string;
|
|
184
|
+
fileName?: string;
|
|
185
|
+
};
|
|
186
|
+
voice?: {
|
|
187
|
+
content?: string;
|
|
188
|
+
url?: string;
|
|
189
|
+
aeskey?: string;
|
|
190
|
+
encrypt_file_key?: string;
|
|
191
|
+
file_url?: string;
|
|
192
|
+
};
|
|
193
|
+
video?: {
|
|
194
|
+
url?: string;
|
|
195
|
+
aeskey?: string;
|
|
196
|
+
encrypt_file_key?: string;
|
|
197
|
+
file_url?: string;
|
|
198
|
+
};
|
|
199
|
+
mixed?: {
|
|
200
|
+
msg_item: Array<{
|
|
201
|
+
msgtype: string;
|
|
202
|
+
text?: {
|
|
203
|
+
content?: string;
|
|
204
|
+
};
|
|
205
|
+
image?: {
|
|
206
|
+
url?: string;
|
|
207
|
+
aeskey?: string;
|
|
208
|
+
encrypt_file_key?: string;
|
|
209
|
+
file_url?: string;
|
|
210
|
+
base64?: string;
|
|
211
|
+
md5?: string;
|
|
212
|
+
};
|
|
213
|
+
file?: {
|
|
214
|
+
url?: string;
|
|
215
|
+
aeskey?: string;
|
|
216
|
+
encrypt_file_key?: string;
|
|
217
|
+
file_url?: string;
|
|
218
|
+
filename?: string;
|
|
219
|
+
};
|
|
220
|
+
[key: string]: unknown;
|
|
221
|
+
}>;
|
|
222
|
+
};
|
|
223
|
+
/** 引用消息 */
|
|
224
|
+
quote?: WebhookInboundQuote;
|
|
225
|
+
/** 发送者(企微 Bot 回调中 userid 是 from.userid) */
|
|
226
|
+
from?: {
|
|
227
|
+
userid?: string;
|
|
228
|
+
corpid?: string;
|
|
229
|
+
};
|
|
230
|
+
/** 事件(eventtype 非 event_type) */
|
|
231
|
+
event?: {
|
|
232
|
+
eventtype?: string;
|
|
233
|
+
event_key?: string;
|
|
234
|
+
template_card_event?: Record<string, unknown>;
|
|
235
|
+
[key: string]: unknown;
|
|
236
|
+
};
|
|
237
|
+
/** 流式消息 */
|
|
238
|
+
stream?: {
|
|
239
|
+
id?: string;
|
|
240
|
+
};
|
|
241
|
+
/** 下行回复 URL */
|
|
242
|
+
response_url?: string;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 引用消息结构(对齐原版 WecomInboundQuote)
|
|
246
|
+
*/
|
|
247
|
+
export interface WebhookInboundQuote {
|
|
248
|
+
msgtype?: "text" | "image" | "mixed" | "voice" | "file" | "video";
|
|
249
|
+
text?: {
|
|
250
|
+
content?: string;
|
|
251
|
+
};
|
|
252
|
+
image?: {
|
|
253
|
+
url?: string;
|
|
254
|
+
};
|
|
255
|
+
mixed?: {
|
|
256
|
+
msg_item?: Array<{
|
|
257
|
+
msgtype: "text" | "image";
|
|
258
|
+
text?: {
|
|
259
|
+
content?: string;
|
|
260
|
+
};
|
|
261
|
+
image?: {
|
|
262
|
+
url?: string;
|
|
263
|
+
};
|
|
264
|
+
}>;
|
|
265
|
+
};
|
|
266
|
+
voice?: {
|
|
267
|
+
content?: string;
|
|
268
|
+
};
|
|
269
|
+
file?: {
|
|
270
|
+
url?: string;
|
|
271
|
+
};
|
|
272
|
+
video?: {
|
|
273
|
+
url?: string;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 待处理/防抖消息
|
|
278
|
+
*
|
|
279
|
+
* 暂存在队列中的消息,等待防抖计时器结束进行聚合。
|
|
280
|
+
*/
|
|
281
|
+
export interface PendingInbound {
|
|
282
|
+
/** 预分配的流 ID */
|
|
283
|
+
streamId: string;
|
|
284
|
+
/** 会话标识 */
|
|
285
|
+
conversationKey: string;
|
|
286
|
+
/** 批次键 */
|
|
287
|
+
batchKey: string;
|
|
288
|
+
/** 目标 Webhook 上下文 */
|
|
289
|
+
target: WecomWebhookTarget;
|
|
290
|
+
/** 原始消息对象(如果聚合,通常指第一条) */
|
|
291
|
+
msg: WebhookInboundMessage;
|
|
292
|
+
/** 聚合的消息内容列表 */
|
|
293
|
+
contents: string[];
|
|
294
|
+
/** 附带的媒体文件(如果有) */
|
|
295
|
+
media?: {
|
|
296
|
+
buffer: Buffer;
|
|
297
|
+
contentType: string;
|
|
298
|
+
filename: string;
|
|
299
|
+
};
|
|
300
|
+
/** 聚合的所有消息 ID(用于去重) */
|
|
301
|
+
msgids: string[];
|
|
302
|
+
/** 回调 nonce */
|
|
303
|
+
nonce: string;
|
|
304
|
+
/** 回调 timestamp */
|
|
305
|
+
timestamp: string;
|
|
306
|
+
/** 防抖定时器句柄 */
|
|
307
|
+
timeout: ReturnType<typeof setTimeout> | null;
|
|
308
|
+
/** 已到达防抖截止时间,但因前序批次仍在处理中而暂存 */
|
|
309
|
+
readyToFlush?: boolean;
|
|
310
|
+
/** 创建时间 */
|
|
311
|
+
createdAt: number;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 主动回复地址状态
|
|
315
|
+
*
|
|
316
|
+
* 存储企业微信回调中提供的 response_url,用于后续主动推送。
|
|
317
|
+
*/
|
|
318
|
+
export interface ActiveReplyState {
|
|
319
|
+
/** 企业微信提供的回调回复 URL */
|
|
320
|
+
response_url: string;
|
|
321
|
+
/** 如果配置了代理,存储代理地址 */
|
|
322
|
+
proxyUrl?: string;
|
|
323
|
+
/** 创建时间 */
|
|
324
|
+
createdAt: number;
|
|
325
|
+
/** 使用时间(仅当 policy="once" 时有意义) */
|
|
326
|
+
usedAt?: number;
|
|
327
|
+
/** 最后一次发送失败的错误信息 */
|
|
328
|
+
lastError?: string;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Webhook Gateway 上下文
|
|
332
|
+
*/
|
|
333
|
+
export interface WebhookGatewayContext {
|
|
334
|
+
account: ResolvedWebhookAccount;
|
|
335
|
+
config: OpenClawConfig;
|
|
336
|
+
/** RuntimeEnv 日志环境 (来自 ChannelGatewayContext.runtime) */
|
|
337
|
+
runtime: RuntimeEnv;
|
|
338
|
+
/** PluginRuntime 用于访问 channel.reply 等核心功能 */
|
|
339
|
+
channelRuntime?: PluginRuntime;
|
|
340
|
+
abortSignal?: AbortSignal;
|
|
341
|
+
setStatus?: (next: Record<string, unknown>) => void;
|
|
342
|
+
log?: {
|
|
343
|
+
info: (msg: string) => void;
|
|
344
|
+
error: (msg: string) => void;
|
|
345
|
+
};
|
|
346
|
+
accountId: string;
|
|
347
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook 模式专用类型定义
|
|
3
|
+
*
|
|
4
|
+
* 从 @mocrane/wecom monitor/types.ts 完整迁移,适配目标项目类型体系。
|
|
5
|
+
*/
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// 常量
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/** StreamState 过期时间 (10 分钟) */
|
|
10
|
+
export const STREAM_TTL_MS = 10 * 60 * 1000;
|
|
11
|
+
/** ActiveReply 过期时间 (1 小时) */
|
|
12
|
+
export const ACTIVE_REPLY_TTL_MS = 60 * 60 * 1000;
|
|
13
|
+
/** 消息防抖间隔 (500ms) */
|
|
14
|
+
export const DEFAULT_DEBOUNCE_MS = 500;
|
|
15
|
+
/** stream 回复最大字节数 (20KB) */
|
|
16
|
+
export const STREAM_MAX_BYTES = 20_480;
|
|
17
|
+
/** 企微 Bot 回复窗口 (6 分钟) */
|
|
18
|
+
export const BOT_WINDOW_MS = 6 * 60 * 1000;
|
|
19
|
+
/** 超时安全边际 (30 秒) */
|
|
20
|
+
export const BOT_SWITCH_MARGIN_MS = 30_000;
|
|
21
|
+
/** HTTP 请求超时 (15 秒) */
|
|
22
|
+
export const REQUEST_TIMEOUT_MS = 15_000;
|
|
23
|
+
/** 自动清理间隔 (60 秒) */
|
|
24
|
+
export const PRUNE_INTERVAL_MS = 60_000;
|
|
25
|
+
/** 固定 Webhook 路径 */
|
|
26
|
+
export const WEBHOOK_PATHS = {
|
|
27
|
+
/** Bot 模式历史兼容路径 */
|
|
28
|
+
BOT: "/wecom",
|
|
29
|
+
/** Bot 模式历史备用兼容路径 */
|
|
30
|
+
BOT_ALT: "/wecom/bot",
|
|
31
|
+
/** Bot 模式推荐路径前缀 */
|
|
32
|
+
BOT_PLUGIN: "/plugins/wecom/bot",
|
|
33
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视频第一帧提取(ffmpeg)
|
|
3
|
+
*
|
|
4
|
+
* 独立文件,避免 child_process 与网络请求在同一文件中触发安全扫描误报。
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 使用 ffmpeg 提取视频第一帧为 JPEG 图片。
|
|
8
|
+
*
|
|
9
|
+
* @param mediaPath 视频文件路径
|
|
10
|
+
* @param timeoutMs 超时时间(默认 10s)
|
|
11
|
+
* @returns 成功返回帧图片路径,失败或 ffmpeg 不可用返回 undefined
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractVideoFirstFrame(mediaPath: string, timeoutMs?: number): Promise<string | undefined>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视频第一帧提取(ffmpeg)
|
|
3
|
+
*
|
|
4
|
+
* 独立文件,避免 child_process 与网络请求在同一文件中触发安全扫描误报。
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 使用 ffmpeg 提取视频第一帧为 JPEG 图片。
|
|
8
|
+
*
|
|
9
|
+
* @param mediaPath 视频文件路径
|
|
10
|
+
* @param timeoutMs 超时时间(默认 10s)
|
|
11
|
+
* @returns 成功返回帧图片路径,失败或 ffmpeg 不可用返回 undefined
|
|
12
|
+
*/
|
|
13
|
+
export async function extractVideoFirstFrame(mediaPath, timeoutMs = 10_000) {
|
|
14
|
+
try {
|
|
15
|
+
const { execFile } = await import("node:child_process");
|
|
16
|
+
const { promisify } = await import("node:util");
|
|
17
|
+
const fs = await import("node:fs/promises");
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
const framePath = mediaPath.replace(/\.[^.]+$/, "_frame1.jpg");
|
|
20
|
+
await execFileAsync("ffmpeg", ["-i", mediaPath, "-vframes", "1", "-q:v", "2", "-y", framePath], { timeout: timeoutMs });
|
|
21
|
+
const stat = await fs.stat(framePath);
|
|
22
|
+
if (stat.size > 0) {
|
|
23
|
+
return framePath;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wecom/wecom-openclaw-plugin",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.3-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"module": "dist/esm/index.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
7
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
8
13
|
"files": [
|
|
9
14
|
"dist",
|
|
10
15
|
"skills",
|
|
@@ -12,8 +17,8 @@
|
|
|
12
17
|
"README.md"
|
|
13
18
|
],
|
|
14
19
|
"scripts": {
|
|
15
|
-
"build": "
|
|
16
|
-
"dev": "
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsc --watch",
|
|
17
22
|
"clean": "rm -rf dist",
|
|
18
23
|
"prebuild": "npm run clean",
|
|
19
24
|
"release": "node scripts/publish-all.mjs",
|
|
@@ -37,7 +42,7 @@
|
|
|
37
42
|
"description": "OpenClaw WeCom (企业微信) channel plugin (official by Tencent WeCom team)",
|
|
38
43
|
"openclaw": {
|
|
39
44
|
"extensions": [
|
|
40
|
-
"./dist/
|
|
45
|
+
"./dist/index.js"
|
|
41
46
|
],
|
|
42
47
|
"channel": {
|
|
43
48
|
"id": "wecom",
|
|
@@ -56,7 +61,10 @@
|
|
|
56
61
|
}
|
|
57
62
|
},
|
|
58
63
|
"dependencies": {
|
|
64
|
+
"@wecom/aibot-node-sdk": "1.0.6-beta.2",
|
|
65
|
+
"fast-xml-parser": "^5.5.9",
|
|
59
66
|
"file-type": "^21.3.0",
|
|
60
|
-
"
|
|
67
|
+
"undici": "^7.24.6",
|
|
68
|
+
"zod": "^4.3.6"
|
|
61
69
|
}
|
|
62
70
|
}
|
package/dist/cjs/index.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
|
-
var openclawCompat = require('./src/openclaw-compat.js');
|
|
6
|
-
var channel = require('./src/channel.js');
|
|
7
|
-
var tool = require('./src/mcp/tool.js');
|
|
8
|
-
require('@wecom/aibot-node-sdk');
|
|
9
|
-
var _const = require('./src/const.js');
|
|
10
|
-
require('./src/version.js');
|
|
11
|
-
var runtime = require('./src/runtime.js');
|
|
12
|
-
|
|
13
|
-
const plugin = {
|
|
14
|
-
id: "wecom-openclaw-plugin",
|
|
15
|
-
name: "企业微信",
|
|
16
|
-
description: "企业微信 OpenClaw 插件",
|
|
17
|
-
configSchema: openclawCompat.emptyPluginConfigSchema(),
|
|
18
|
-
register(api) {
|
|
19
|
-
runtime.setWeComRuntime(api.runtime);
|
|
20
|
-
api.registerChannel({ plugin: channel.wecomPlugin });
|
|
21
|
-
// 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server
|
|
22
|
-
api.registerTool(tool.createWeComMcpTool(), { name: "wecom_mcp" });
|
|
23
|
-
// ── Gateway 启动时自动确保 tools.alsoAllow 包含 wecom_mcp ──────────
|
|
24
|
-
// 在 gateway_start 阶段检测并写入,保证插件安装/更新后首次启动即生效
|
|
25
|
-
// api.on("gateway_start", async () => {
|
|
26
|
-
// await ensureToolsAlsoAllow(api);
|
|
27
|
-
// });
|
|
28
|
-
// 注入媒体发送指令和文件大小限制提示词(仅对企业微信 channel 生效)
|
|
29
|
-
api.on("before_prompt_build", (_event, ctx) => {
|
|
30
|
-
// 只在企业微信 channel 的会话中注入,避免影响其他 channel 插件
|
|
31
|
-
if (ctx?.channelId !== _const.CHANNEL_ID) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
appendSystemContext: [
|
|
36
|
-
"重要:涉及发送图片/视频/语音/文件给用户时,请务必使用 `MEDIA:` 指令。详见 wecom-send-media 这个 skill(技能)。",
|
|
37
|
-
"重要:当需要向用户发送结构化卡片消息(如通知、投票、按钮选择等)时,请在回复中直接输出 JSON 代码块(```json ... ```),其中 card_type 字段标明卡片类型。详见 wecom-send-template-card 技能。"
|
|
38
|
-
].join("\n"),
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
exports.default = plugin;
|
|
45
|
-
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../index.ts"],"sourcesContent":["import type { OpenClawPluginApi } from \"openclaw/plugin-sdk/core\";\nimport { emptyPluginConfigSchema } from \"./src/openclaw-compat.js\";\nimport { wecomPlugin } from \"./src/channel.js\";\nimport { createWeComMcpTool } from \"./src/mcp\";\nimport { setWeComRuntime } from \"./src/runtime.js\";\nimport { CHANNEL_ID } from \"./src/const.js\";\n\n// ============================================================================\n// 需要自动注入 tools.alsoAllow 的工具名列表\n// ============================================================================\n\nconst REQUIRED_ALSO_ALLOW_TOOLS = [\"wecom_mcp\"] as const;\n\n/**\n * 确保 tools.alsoAllow 中包含插件所需的工具名。\n *\n * 逻辑:\n * 1. 读取当前配置\n * 2. 检查 tools.alsoAllow 中是否已包含所需工具\n * 3. 如有缺失,合并写入配置文件\n *\n * 幂等操作——重复调用不会产生副作用。\n * 若 tools.allow 已显式设置(与 alsoAllow 互斥),则跳过注入并打印提示。\n */\nasync function ensureToolsAlsoAllow(api: OpenClawPluginApi): Promise<void> {\n try {\n const cfg = api.runtime.config.loadConfig();\n const tools = cfg.tools ?? {};\n\n // 如果用户显式配置了 tools.allow(全量白名单),则 alsoAllow 与之互斥,\n // 此时不应自动追加 alsoAllow,避免产生校验冲突\n if (tools.allow && tools.allow.length > 0) {\n const missing = REQUIRED_ALSO_ALLOW_TOOLS.filter(\n (t) => !tools.allow!.includes(t),\n );\n if (missing.length > 0) {\n console.warn(\n `[wecom] tools.allow 已显式设置,无法自动注入 alsoAllow。` +\n `请手动将 ${JSON.stringify(missing)} 加入 tools.allow。`,\n );\n }\n return;\n }\n\n const existing = tools.alsoAllow ?? [];\n const missing = REQUIRED_ALSO_ALLOW_TOOLS.filter(\n (t) => !existing.includes(t),\n );\n\n if (missing.length === 0) {\n // 所有工具已在白名单中,无需操作\n return;\n }\n\n const merged = [...existing, ...missing];\n const nextConfig = {\n ...cfg,\n tools: {\n ...tools,\n alsoAllow: merged,\n },\n };\n\n await api.runtime.config.writeConfigFile(nextConfig);\n console.log(\n `[wecom] 已自动将 ${JSON.stringify(missing)} 加入 tools.alsoAllow`,\n );\n } catch (err) {\n // 配置写入失败不应阻断 Gateway 启动\n console.error(\n `[wecom] 自动注入 tools.alsoAllow 失败:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n}\n\nconst plugin = {\n id: \"wecom-openclaw-plugin\",\n name: \"企业微信\",\n description: \"企业微信 OpenClaw 插件\",\n configSchema: emptyPluginConfigSchema(),\n register(api: OpenClawPluginApi) {\n\n setWeComRuntime(api.runtime);\n api.registerChannel({ plugin: wecomPlugin });\n\n // 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server\n api.registerTool(createWeComMcpTool(), { name: \"wecom_mcp\" });\n\n // ── Gateway 启动时自动确保 tools.alsoAllow 包含 wecom_mcp ──────────\n // 在 gateway_start 阶段检测并写入,保证插件安装/更新后首次启动即生效\n // api.on(\"gateway_start\", async () => {\n // await ensureToolsAlsoAllow(api);\n // });\n\n // 注入媒体发送指令和文件大小限制提示词(仅对企业微信 channel 生效)\n api.on(\"before_prompt_build\", (_event, ctx) => {\n // 只在企业微信 channel 的会话中注入,避免影响其他 channel 插件\n if (ctx?.channelId !== CHANNEL_ID) {\n return;\n }\n return {\n appendSystemContext: [\n \"重要:涉及发送图片/视频/语音/文件给用户时,请务必使用 `MEDIA:` 指令。详见 wecom-send-media 这个 skill(技能)。\",\n \"重要:当需要向用户发送结构化卡片消息(如通知、投票、按钮选择等)时,请在回复中直接输出 JSON 代码块(```json ... ```),其中 card_type 字段标明卡片类型。详见 wecom-send-template-card 技能。\"\n ].join(\"\\n\"),\n };\n });\n },\n};\n\nexport default plugin;\n"],"names":["emptyPluginConfigSchema","setWeComRuntime","wecomPlugin","createWeComMcpTool","CHANNEL_ID"],"mappings":";;;;;;;;;;;;AA4EA,MAAM,MAAM,GAAG;AACb,IAAA,EAAE,EAAE,uBAAuB;AAC3B,IAAA,IAAI,EAAE,MAAM;AACZ,IAAA,WAAW,EAAE,kBAAkB;IAC/B,YAAY,EAAEA,sCAAuB,EAAE;AACvC,IAAA,QAAQ,CAAC,GAAsB,EAAA;AAE7B,QAAAC,uBAAe,CAAC,GAAG,CAAC,OAAO,CAAC;QAC5B,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAEC,mBAAW,EAAE,CAAC;;AAG5C,QAAA,GAAG,CAAC,YAAY,CAACC,uBAAkB,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;;;;;;;QAS7D,GAAG,CAAC,EAAE,CAAC,qBAAqB,EAAE,CAAC,MAAM,EAAE,GAAG,KAAI;;AAE5C,YAAA,IAAI,GAAG,EAAE,SAAS,KAAKC,iBAAU,EAAE;gBACjC;YACF;YACA,OAAO;AACL,gBAAA,mBAAmB,EAAE;oBACnB,6EAA6E;oBAC7E;iBACD,CAAC,IAAI,CAAC,IAAI,CAAC;aACb;AACH,QAAA,CAAC,CAAC;IACJ,CAAC;;;;;"}
|