@wecom/wecom-openclaw-plugin 2026.3.25-beta.1 → 2026.3.25-beta.2

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.
@@ -70,3 +70,5 @@ export declare const EVENT_ENTER_CHECK_UPDATE = "event.enter_check_update";
70
70
  export declare const CMD_ENTER_EVENT_REPLY = "ww_ai_robot_enter_event";
71
71
  /** WSClient scene 参数:企微 OpenClaw 场景 */
72
72
  export declare const SCENE_WECOM_OPENCLAW = 1;
73
+ /** 合法的模板卡片 card_type 列表 */
74
+ export declare const VALID_CARD_TYPES: string[];
@@ -23,8 +23,6 @@ export interface MessageState {
23
23
  accumulatedText: string;
24
24
  /** 流式回复的 streamId,用于保持同一个流式回复使用相同的 streamId */
25
25
  streamId?: string;
26
- /** 是否有用户可见的文本内容(不包括 <think>...</think> 标签) */
27
- hasText?: boolean;
28
26
  /** 是否已成功发送过媒体文件 */
29
27
  hasMedia?: boolean;
30
28
  /** 是否有媒体发送失败(权限不足、文件过大等) */
@@ -35,6 +33,22 @@ export interface MessageState {
35
33
  deliverCalled?: boolean;
36
34
  /** 流式回复是否已过期(errcode 846608,>6分钟),需降级为主动发送 */
37
35
  streamExpired?: boolean;
36
+ /** 是否已成功发送过模板卡片 */
37
+ hasTemplateCard?: boolean;
38
+ }
39
+ /** 从文本中提取的模板卡片 */
40
+ export interface ExtractedTemplateCard {
41
+ /** 原始 JSON 对象(已验证 card_type 合法) */
42
+ cardJson: Record<string, unknown>;
43
+ /** card_type 值 */
44
+ cardType: string;
45
+ }
46
+ /** extractTemplateCards 返回值 */
47
+ export interface TemplateCardExtractionResult {
48
+ /** 提取到的合法模板卡片列表 */
49
+ cards: ExtractedTemplateCard[];
50
+ /** 移除卡片代码块后的剩余文本 */
51
+ remainingText: string;
38
52
  }
39
53
  /**
40
54
  * WebSocket 请求消息基础格式
@@ -70,20 +84,20 @@ export interface WeComSubscribeRequest extends WeComRequest {
70
84
  * 企业微信推送消息格式
71
85
  */
72
86
  export interface WeComCallbackMessage {
73
- cmd: WeComCommand.AIBOT_CALLBACK;
87
+ cmd: WeComCommand.AIBOT_CALLBACK | "aibot_event_callback";
74
88
  headers: {
75
89
  req_id: string;
76
90
  };
77
91
  body: {
78
92
  msgid: string;
79
93
  aibotid: string;
80
- chatid: string;
94
+ chatid?: string;
81
95
  chattype: "single" | "group";
82
96
  from: {
83
97
  userid: string;
84
98
  };
85
99
  response_url: string;
86
- msgtype: "text" | "image" | "voice" | "video" | "file" | "stream" | "mixed";
100
+ msgtype: "text" | "image" | "voice" | "video" | "file" | "stream" | "mixed" | "event";
87
101
  text?: {
88
102
  content: string;
89
103
  };
@@ -125,6 +139,22 @@ export interface WeComCallbackMessage {
125
139
  stream?: {
126
140
  id: string;
127
141
  };
142
+ event?: {
143
+ eventtype: string;
144
+ template_card_event?: {
145
+ card_type?: string;
146
+ event_key?: string;
147
+ task_id?: string;
148
+ selected_items?: {
149
+ selected_item?: Array<{
150
+ question_key?: string;
151
+ option_ids?: {
152
+ option_id?: string[];
153
+ };
154
+ }>;
155
+ };
156
+ };
157
+ };
128
158
  };
129
159
  }
130
160
  /**
@@ -9,6 +9,7 @@ export interface MessageBody {
9
9
  chatid?: string;
10
10
  chattype: "single" | "group";
11
11
  from: {
12
+ corpid?: string;
12
13
  userid: string;
13
14
  };
14
15
  response_url?: string;
@@ -56,6 +57,22 @@ export interface MessageBody {
56
57
  aeskey?: string;
57
58
  };
58
59
  };
60
+ event?: {
61
+ eventtype?: string;
62
+ template_card_event?: {
63
+ card_type?: string;
64
+ event_key?: string;
65
+ task_id?: string;
66
+ selected_items?: {
67
+ selected_item?: Array<{
68
+ question_key?: string;
69
+ option_ids?: {
70
+ option_id?: string[];
71
+ };
72
+ }>;
73
+ };
74
+ };
75
+ };
59
76
  }
60
77
  export interface ParsedMessageContent {
61
78
  textParts: string[];
@@ -66,7 +83,7 @@ export interface ParsedMessageContent {
66
83
  quoteContent: string | undefined;
67
84
  }
68
85
  /**
69
- * 解析消息内容(支持单条消息、图文混排和引用消息)
86
+ * 解析消息内容(支持单条消息、图文混排、事件回调和引用消息)
70
87
  * @returns 提取的文本数组、图片URL数组和引用消息内容
71
88
  */
72
89
  export declare function parseMessageContent(body: MessageBody): ParsedMessageContent;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 模板卡片解析器
3
+ *
4
+ * 从 LLM 回复文本中提取 markdown JSON 代码块,验证其是否为合法的企业微信模板卡片,
5
+ * 返回提取到的卡片列表和剩余文本。
6
+ *
7
+ * 同时提供 maskTemplateCardBlocks 函数,用于在流式中间帧中隐藏正在构建的卡片代码块,
8
+ * 避免 JSON 源码暴露给终端用户。
9
+ */
10
+ import type { TemplateCardExtractionResult } from "./interface.js";
11
+ /**
12
+ * 从文本中提取模板卡片 JSON 代码块
13
+ *
14
+ * 匹配规则:
15
+ * 1. 匹配所有 ```json ... ``` 或 ``` ... ``` 格式的代码块
16
+ * 2. 尝试 JSON.parse 解析代码块内容
17
+ * 3. 检查解析结果中是否包含合法的 card_type 字段
18
+ * 4. 合法的卡片从原文中移除,不合法的保留
19
+ *
20
+ * @param text - LLM 回复的完整文本
21
+ * @returns 提取结果,包含卡片列表和剩余文本
22
+ */
23
+ export declare function extractTemplateCards(text: string, log?: (...args: any[]) => void): TemplateCardExtractionResult;
24
+ /**
25
+ * 遮罩文本中的模板卡片代码块(用于流式中间帧展示)
26
+ *
27
+ * 在 LLM 流式输出过程中,累积文本可能包含:
28
+ * 1. 已闭合的模板卡片 JSON 代码块 → 替换为友好提示文本
29
+ * 2. 正在构建中的未闭合代码块 → 截断隐藏,避免 JSON 源码暴露
30
+ *
31
+ * 此函数仅做文本替换,不做 JSON 解析验证(中间帧性能优先)。
32
+ * 只要代码块内容中出现 "card_type" 关键词就认为是模板卡片并遮罩。
33
+ *
34
+ * @param text - 当前累积文本
35
+ * @returns 遮罩后的展示文本
36
+ */
37
+ export declare function maskTemplateCardBlocks(text: string, log?: (...args: any[]) => void): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wecom/wecom-openclaw-plugin",
3
- "version": "2026.3.25-beta.1",
3
+ "version": "2026.3.25-beta.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: wecom-send-template-card
3
+ description: 企业微信模板卡片消息技能。当对话中存在需要用户做出选择、确认、投票或查看结构化信息的场景时,自动识别并使用最合适的模板卡片类型发送消息。支持文本通知(text_notice)、图文展示(news_notice)、按钮交互(button_interaction)、投票选择(vote_interaction)、多项选择(multiple_interaction)五种卡片类型。当你需要向用户展示多个方案让其选择时、需要发起投票时、需要发送结构化通知时、需要用户在多个维度上做出选择时,都应该使用此技能而不是纯文本消息。即使用户没有明确要求"发卡片",只要对话语境适合使用卡片(如"帮我问一下张三选 A 还是 B"),也应主动使用。仅当通过 wecom 通道通信时使用此技能。
4
+ metadata:
5
+ {
6
+ "openclaw": { "emoji": "🃏", "requires": { "config": ["channels.wecom"] } },
7
+ }
8
+ ---
9
+
10
+ ---
11
+
12
+ ## 使用方式
13
+
14
+ 在回复文本中直接输出一个 markdown JSON 代码块,代码块中的 JSON 必须格式正确、合法且包含 `card_type` 字段。插件层会自动从回复中检测并提取该 JSON 字符串并 Parse,然后通过企业微信 SDK 发送模板卡片消息。
15
+
16
+ ### 输出格式
17
+
18
+ 在你的回复中,正常文本照常输出,需要发送模板卡片时,将完整的 template_card JSON 包裹在 markdown 代码块中:
19
+
20
+ ````
21
+ 你的正常回复文本...
22
+
23
+ ```json
24
+ {
25
+ "card_type": "卡片类型",
26
+ ... 卡片内容 ...
27
+ }
28
+ ```
29
+
30
+ 可以继续输出其他文本...
31
+ ````
32
+
33
+ ### 格式要求
34
+
35
+ 1. **必须使用 markdown 代码块**:用 ` ```json ` 和 ` ``` ` 包裹 JSON
36
+ 2. **必须包含 `card_type` 字段**:值为以下五种之一:`text_notice`、`news_notice`、`button_interaction`、`vote_interaction`、`multiple_interaction`
37
+ 3. **JSON 必须合法**:确保能被 `JSON.parse` 正确解析,生成后请自查一次。
38
+ 4. **一次回复可包含多个卡片**:每个卡片用独立的代码块包裹
39
+ 5. **代码块外的文本正常展示**:提取卡片后,剩余文本会作为普通流式回复发送给用户
40
+
41
+ ### 重要注意事项
42
+
43
+ - **不要使用 MCP 工具调用**来发送模板卡片,直接在回复中输出 JSON 代码块即可
44
+ - 代码块中的 JSON 就是 `template_card` 的完整结构体(即企业微信 API 中 `template_card` 字段的值)
45
+ - 不需要包裹外层的 `msgtype`、`chatid` 等字段,只需 `template_card` 本身的内容
46
+ - **字段类型必须严格匹配企业微信 API 要求**:
47
+ - `mode`、`style`、`type`、`desc_color` 等字段必须为**整数**(如 `0`、`1`),**不能**用字符串(如 `"multi"`、`"single"`)
48
+ - `disable`、`is_checked` 必须为**布尔值** `true` / `false`,**不能**用字符串
49
+ - 例:`"mode": 1`(✅ 正确) vs `"mode": "multi"`(❌ 错误)
50
+
51
+ ---
52
+
53
+ ## 场景识别与卡片类型选择
54
+
55
+ 根据对话上下文,自动判断最适合的卡片类型:
56
+
57
+ | 对话场景 | 推荐卡片类型 | card_type |
58
+ |---------|------------|-----------|
59
+ | 通知提醒、状态更新、信息汇总 | 文本通知模版卡片 | `text_notice` |
60
+ | 带图片的内容展示、新闻资讯、产品介绍 | 图文展示模版卡片 | `news_notice` |
61
+ | 需要用户点击按钮做出选择(如:确认/取消、选择方案 A/B/C) | 按钮交互模版卡片 | `button_interaction` |
62
+ | 需要用户在多个选项中投票(单选或多选) | 投票选择模版卡片 | `vote_interaction` |
63
+ | 需要用户在多个维度上分别做出选择(如:选择时间+地点+人员) | 多项选择模版卡片 | `multiple_interaction` |
64
+
65
+ ### 场景识别关键词
66
+
67
+ - **文本通知**:通知、提醒、告知、汇报、状态更新、完成通知、系统通知
68
+ - **图文展示**:展示、图文、图片说明、产品介绍、新闻推送
69
+ - **按钮交互**:选择方案、确认操作、同意/拒绝、选一个、A还是B、需要你确认
70
+ - **投票选择**:投票、表决、评选、单选/多选、最喜欢哪个
71
+ - **多项选择**:多个维度选择、分别选择、组合选择、多个下拉框
72
+
73
+ ---
74
+
75
+ ## 五种卡片类型详细说明
76
+
77
+ 请参考 [./references/api-template-card-types.md](./references/api-template-card-types.md) 的描述。
78
+
79
+ ## 核心规则
80
+
81
+ ### task_id 生成规则
82
+
83
+ `task_id` 用于唯一标识一次交互,需确保唯一性。生成规则:
84
+ - 格式:`task_{场景描述}_{时间戳}`
85
+ - 示例:`task_solution_select_1711234567`、`task_vote_team_building_1711234568`
86
+
87
+ ### card_action 规则
88
+
89
+ - `text_notice` 和 `news_notice` 的 `card_action` 为**必填**字段
90
+ - 若无实际跳转需求,可设置 `type: 1` 并使用一个通用 URL
91
+ - `button_interaction`、`vote_interaction`、`multiple_interaction` 不需要 `card_action`
92
+
93
+ ---
94
+
95
+ ## 典型工作流示例
96
+
97
+ ### 示例 1:发送投票卡片(vote_interaction)
98
+
99
+ **用户**:帮我发一个投票,让大家选团建活动:爬山、聚餐、密室逃脱
100
+
101
+ **你的回复**:
102
+
103
+ 好的,我来为你发一个团建活动投票卡片 🗳️
104
+
105
+ ```json
106
+ {
107
+ "card_type": "vote_interaction",
108
+ "source": {
109
+ "desc": "团建投票"
110
+ },
111
+ "main_title": {
112
+ "title": "团建活动投票",
113
+ "desc": "请选择你最想参加的团建活动"
114
+ },
115
+ "checkbox": {
116
+ "question_key": "team_building_vote",
117
+ "option_list": [
118
+ { "id": "opt_hiking", "text": "🏔️ 爬山" },
119
+ { "id": "opt_dinner", "text": "🍽️ 聚餐" },
120
+ { "id": "opt_escape", "text": "🔐 密室逃脱" }
121
+ ],
122
+ "mode": 1
123
+ },
124
+ "submit_button": {
125
+ "text": "提交投票",
126
+ "key": "submit_team_building"
127
+ },
128
+ "task_id": "task_team_building_vote_1711234567"
129
+ }
130
+ ```
131
+
132
+ 投票卡片已发送,大家可以直接在卡片上选择并提交!
133
+
134
+ ### 示例 2:发送按钮交互卡片(button_interaction)
135
+
136
+ **用户**:有两个技术方案让用户选:A方案重构、B方案兼容
137
+
138
+ **你的回复**:
139
+
140
+ 好的,我来发一个方案选择卡片:
141
+
142
+ ```json
143
+ {
144
+ "card_type": "button_interaction",
145
+ "main_title": {
146
+ "title": "技术方案选择",
147
+ "desc": "请选择你倾向的技术方案"
148
+ },
149
+ "sub_title_text": "A方案:全面重构,长期收益更高但开发周期长\nB方案:兼容现有架构,快速交付但技术债增加",
150
+ "button_list": [
151
+ { "text": "A方案 - 重构", "style": 1, "key": "plan_a_refactor" },
152
+ { "text": "B方案 - 兼容", "style": 4, "key": "plan_b_compat" }
153
+ ],
154
+ "task_id": "task_solution_select_1711234568"
155
+ }
156
+ ```
157
+
158
+ 方案选择卡片已发出,等待用户点击选择。
159
+
160
+ ### 示例 3:发送文本通知卡片(text_notice)
161
+
162
+ **用户**:通知一下项目进度已完成80%
163
+
164
+ **你的回复**:
165
+
166
+ ```json
167
+ {
168
+ "card_type": "text_notice",
169
+ "main_title": {
170
+ "title": "项目进度更新",
171
+ "desc": "截至当前,项目整体进度已达到 80%"
172
+ },
173
+ "emphasis_content": {
174
+ "title": "80%",
175
+ "desc": "项目完成度"
176
+ },
177
+ "sub_title_text": "各模块进展顺利,预计按期交付。",
178
+ "card_action": {
179
+ "type": 1,
180
+ "url": "https://work.weixin.qq.com"
181
+ }
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 错误处理
188
+
189
+ - 如果 JSON 格式不合法或 `card_type` 不在五种类型中,代码块会被保留在原文中作为普通文本展示(不会被提取为卡片)
190
+ - 确保每个交互型卡片(button_interaction、vote_interaction、multiple_interaction)都有唯一的 `task_id`
191
+ - `text_notice` 和 `news_notice` 必须包含 `card_action` 字段