@dingxiang-me/openclaw-wechat 1.4.1 → 1.7.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.en.md +89 -12
  3. package/README.md +103 -15
  4. package/docs/channels/wecom.md +28 -3
  5. package/openclaw.plugin.json +467 -10
  6. package/package.json +13 -2
  7. package/src/core/agent-routing.js +6 -0
  8. package/src/core.js +564 -35
  9. package/src/wecom/account-config-core.js +28 -8
  10. package/src/wecom/account-config.js +55 -0
  11. package/src/wecom/account-diagnostics.js +121 -0
  12. package/src/wecom/account-paths.js +39 -0
  13. package/src/wecom/agent-inbound-dispatch.js +9 -0
  14. package/src/wecom/agent-inbound-guards.js +24 -4
  15. package/src/wecom/agent-inbound-processor.js +27 -0
  16. package/src/wecom/agent-webhook-handler.js +11 -0
  17. package/src/wecom/bot-context.js +2 -1
  18. package/src/wecom/bot-dispatch-fallback.js +2 -1
  19. package/src/wecom/bot-inbound-content.js +73 -3
  20. package/src/wecom/bot-inbound-dispatch-runtime.js +2 -1
  21. package/src/wecom/bot-inbound-executor-helpers.js +56 -5
  22. package/src/wecom/bot-inbound-executor.js +19 -0
  23. package/src/wecom/bot-inbound-guards.js +36 -4
  24. package/src/wecom/bot-runtime-context.js +5 -3
  25. package/src/wecom/bot-webhook-dispatch.js +45 -12
  26. package/src/wecom/bot-webhook-handler.js +45 -13
  27. package/src/wecom/command-handlers.js +26 -0
  28. package/src/wecom/command-status-text.js +76 -7
  29. package/src/wecom/observability-metrics.js +133 -0
  30. package/src/wecom/outbound-agent-push.js +2 -1
  31. package/src/wecom/outbound-bot-card.js +103 -0
  32. package/src/wecom/outbound-delivery.js +92 -7
  33. package/src/wecom/outbound-response-delivery.js +10 -6
  34. package/src/wecom/outbound-webhook-delivery.js +42 -1
  35. package/src/wecom/plugin-account-policy-services.js +19 -0
  36. package/src/wecom/plugin-base-services.js +13 -0
  37. package/src/wecom/plugin-constants.js +1 -1
  38. package/src/wecom/plugin-delivery-inbound-services.js +8 -0
  39. package/src/wecom/plugin-processing-deps.js +4 -0
  40. package/src/wecom/plugin-route-runtime-deps.js +5 -0
  41. package/src/wecom/plugin-services.js +7 -0
  42. package/src/wecom/policy-resolvers.js +82 -5
  43. package/src/wecom/register-runtime.js +31 -2
  44. package/src/wecom/route-registration.js +173 -41
  45. package/src/wecom/runtime-utils.js +7 -2
  46. package/src/wecom/webhook-adapter.js +61 -0
  47. package/src/wecom/webhook-bot.js +26 -0
@@ -9,6 +9,12 @@ export function createWecomCommandHandlers({
9
9
  resolveWecomVoiceTranscriptionConfig,
10
10
  resolveWecomCommandPolicy,
11
11
  resolveWecomAllowFromPolicy,
12
+ resolveWecomDmPolicy,
13
+ resolveWecomEventPolicy = () => ({
14
+ enabled: true,
15
+ enterAgentWelcomeEnabled: false,
16
+ enterAgentWelcomeText: "",
17
+ }),
12
18
  resolveWecomGroupChatPolicy,
13
19
  resolveWecomTextDebouncePolicy,
14
20
  resolveWecomReplyStreamingPolicy,
@@ -17,6 +23,7 @@ export function createWecomCommandHandlers({
17
23
  resolveWecomWebhookBotDeliveryPolicy,
18
24
  resolveWecomDynamicAgentPolicy,
19
25
  resolveWecomBotConfig,
26
+ getWecomObservabilityMetrics = () => ({}),
20
27
  pluginVersion,
21
28
  } = {}) {
22
29
  if (typeof sendWecomText !== "function") throw new Error("createWecomCommandHandlers: sendWecomText is required");
@@ -37,6 +44,12 @@ export function createWecomCommandHandlers({
37
44
  if (typeof resolveWecomAllowFromPolicy !== "function") {
38
45
  throw new Error("createWecomCommandHandlers: resolveWecomAllowFromPolicy is required");
39
46
  }
47
+ if (typeof resolveWecomDmPolicy !== "function") {
48
+ throw new Error("createWecomCommandHandlers: resolveWecomDmPolicy is required");
49
+ }
50
+ if (typeof resolveWecomEventPolicy !== "function") {
51
+ throw new Error("createWecomCommandHandlers: resolveWecomEventPolicy is required");
52
+ }
40
53
  if (typeof resolveWecomGroupChatPolicy !== "function") {
41
54
  throw new Error("createWecomCommandHandlers: resolveWecomGroupChatPolicy is required");
42
55
  }
@@ -67,6 +80,7 @@ export function createWecomCommandHandlers({
67
80
 
68
81
  可用命令:
69
82
  /help - 显示此帮助信息
83
+ /new - 新建会话(兼容命令,等价于 /reset)
70
84
  /clear - 重置会话(等价于 /reset)
71
85
  /status - 查看系统状态
72
86
 
@@ -84,6 +98,8 @@ export function createWecomCommandHandlers({
84
98
  const voiceConfig = resolveWecomVoiceTranscriptionConfig(api);
85
99
  const commandPolicy = resolveWecomCommandPolicy(api);
86
100
  const allowFromPolicy = resolveWecomAllowFromPolicy(api, config?.accountId, config);
101
+ const dmPolicy = resolveWecomDmPolicy(api, config?.accountId, config);
102
+ const eventPolicy = resolveWecomEventPolicy(api, config?.accountId, config);
87
103
  const groupPolicy = resolveWecomGroupChatPolicy(api);
88
104
  const debouncePolicy = resolveWecomTextDebouncePolicy(api);
89
105
  const streamingPolicy = resolveWecomReplyStreamingPolicy(api);
@@ -91,6 +107,7 @@ export function createWecomCommandHandlers({
91
107
  const streamManagerPolicy = resolveWecomStreamManagerPolicy(api);
92
108
  const webhookBotPolicy = resolveWecomWebhookBotDeliveryPolicy(api);
93
109
  const dynamicAgentPolicy = resolveWecomDynamicAgentPolicy(api);
110
+ const observabilityMetrics = getWecomObservabilityMetrics();
94
111
 
95
112
  const statusText = buildAgentStatusText({
96
113
  fromUser,
@@ -101,6 +118,8 @@ export function createWecomCommandHandlers({
101
118
  voiceConfig,
102
119
  commandPolicy,
103
120
  allowFromPolicy,
121
+ dmPolicy,
122
+ eventPolicy,
104
123
  groupPolicy,
105
124
  debouncePolicy,
106
125
  streamingPolicy,
@@ -108,6 +127,7 @@ export function createWecomCommandHandlers({
108
127
  streamManagerPolicy,
109
128
  webhookBotPolicy,
110
129
  dynamicAgentPolicy,
130
+ observabilityMetrics,
111
131
  });
112
132
 
113
133
  await sendWecomText({
@@ -126,12 +146,15 @@ export function createWecomCommandHandlers({
126
146
  const allWebhookTargetAliases = listAllWebhookTargetAliases(api);
127
147
  const commandPolicy = resolveWecomCommandPolicy(api);
128
148
  const allowFromPolicy = resolveWecomAllowFromPolicy(api, "default", {});
149
+ const dmPolicy = resolveWecomDmPolicy(api, "default", {});
150
+ const eventPolicy = resolveWecomEventPolicy(api, "default", {});
129
151
  const groupPolicy = resolveWecomGroupChatPolicy(api);
130
152
  const botConfig = resolveWecomBotConfig(api);
131
153
  const deliveryFallbackPolicy = resolveWecomDeliveryFallbackPolicy(api);
132
154
  const streamManagerPolicy = resolveWecomStreamManagerPolicy(api);
133
155
  const webhookBotPolicy = resolveWecomWebhookBotDeliveryPolicy(api);
134
156
  const dynamicAgentPolicy = resolveWecomDynamicAgentPolicy(api);
157
+ const observabilityMetrics = getWecomObservabilityMetrics();
135
158
  return buildBotStatusText({
136
159
  fromUser,
137
160
  pluginVersion,
@@ -139,11 +162,14 @@ export function createWecomCommandHandlers({
139
162
  allWebhookTargetAliases,
140
163
  commandPolicy,
141
164
  allowFromPolicy,
165
+ dmPolicy,
166
+ eventPolicy,
142
167
  groupPolicy,
143
168
  deliveryFallbackPolicy,
144
169
  streamManagerPolicy,
145
170
  webhookBotPolicy,
146
171
  dynamicAgentPolicy,
172
+ observabilityMetrics,
147
173
  });
148
174
  }
149
175
 
@@ -8,6 +8,52 @@ function buildWebhookTargetStatusLine({ aliases, scope = "当前账户", maxPrev
8
8
  return `✅ 命名 Webhook 目标(${scope}):${preview}${suffix}`;
9
9
  }
10
10
 
11
+ function buildDmPolicyStatusLine(dmPolicy = {}) {
12
+ const mode = String(dmPolicy?.mode ?? "open").trim().toLowerCase() || "open";
13
+ if (mode === "deny") {
14
+ return "⚠️ 私聊策略:已关闭(deny)";
15
+ }
16
+ if (mode === "allowlist") {
17
+ const count = Array.isArray(dmPolicy?.allowFrom) ? dmPolicy.allowFrom.length : 0;
18
+ return `✅ 私聊策略:白名单(${count} 个用户)`;
19
+ }
20
+ return "ℹ️ 私聊策略:开放(open)";
21
+ }
22
+
23
+ function buildEventPolicyStatusLine(eventPolicy = {}) {
24
+ if (eventPolicy?.enabled === false) {
25
+ return "⚠️ 事件策略:已关闭";
26
+ }
27
+ if (eventPolicy?.enterAgentWelcomeEnabled) {
28
+ return "✅ 事件策略:enter_agent 自动欢迎语已启用";
29
+ }
30
+ return "ℹ️ 事件策略:已启用(enter_agent 欢迎语未启用)";
31
+ }
32
+
33
+ function buildObservabilityStatusLines(observabilityMetrics = {}) {
34
+ const inboundTotal = Number(observabilityMetrics?.inboundTotal || 0);
35
+ const deliveryTotal = Number(observabilityMetrics?.deliveryTotal || 0);
36
+ const deliverySuccess = Number(observabilityMetrics?.deliverySuccess || 0);
37
+ const deliveryFailed = Number(observabilityMetrics?.deliveryFailed || 0);
38
+ const errorsTotal = Number(observabilityMetrics?.errorsTotal || 0);
39
+ const successRate = deliveryTotal > 0 ? `${Math.round((deliverySuccess / deliveryTotal) * 100)}%` : "n/a";
40
+ const status = `📈 观测统计:入站 ${inboundTotal} / 回包 ${deliveryTotal}(成功 ${deliverySuccess},失败 ${deliveryFailed},成功率 ${successRate}) / 错误 ${errorsTotal}`;
41
+
42
+ const recent = Array.isArray(observabilityMetrics?.recentFailures)
43
+ ? observabilityMetrics.recentFailures.slice(-1)[0]
44
+ : null;
45
+ if (!recent) {
46
+ return {
47
+ status,
48
+ recent: "ℹ️ 最近失败:无",
49
+ };
50
+ }
51
+ return {
52
+ status,
53
+ recent: `⚠️ 最近失败:${recent.scope || "unknown"} ${recent.reason || "unknown"}`.slice(0, 140),
54
+ };
55
+ }
56
+
11
57
  export function buildAgentStatusText({
12
58
  fromUser,
13
59
  config,
@@ -17,6 +63,8 @@ export function buildAgentStatusText({
17
63
  voiceConfig,
18
64
  commandPolicy,
19
65
  allowFromPolicy,
66
+ dmPolicy,
67
+ eventPolicy,
20
68
  groupPolicy,
21
69
  debouncePolicy,
22
70
  streamingPolicy,
@@ -24,6 +72,7 @@ export function buildAgentStatusText({
24
72
  streamManagerPolicy,
25
73
  webhookBotPolicy,
26
74
  dynamicAgentPolicy,
75
+ observabilityMetrics,
27
76
  } = {}) {
28
77
  const proxyEnabled = Boolean(config?.outboundProxy);
29
78
  const voiceStatusLine = voiceConfig.enabled
@@ -36,6 +85,8 @@ export function buildAgentStatusText({
36
85
  allowFromPolicy.allowFrom.length === 0 || allowFromPolicy.allowFrom.includes("*")
37
86
  ? "ℹ️ 发送者授权:未限制(allowFrom 未配置)"
38
87
  : `✅ 发送者授权:已限制 ${allowFromPolicy.allowFrom.length} 个用户`;
88
+ const dmPolicyLine = buildDmPolicyStatusLine(dmPolicy);
89
+ const eventPolicyLine = buildEventPolicyStatusLine(eventPolicy);
39
90
  const groupPolicyLine = groupPolicy.enabled
40
91
  ? groupPolicy.triggerMode === "mention"
41
92
  ? "✅ 群聊触发:仅 @ 命中后处理"
@@ -65,6 +116,8 @@ export function buildAgentStatusText({
65
116
  const dynamicAgentPolicyLine = dynamicAgentPolicy.enabled
66
117
  ? `✅ 动态 Agent 路由已启用(mode=${dynamicAgentPolicy.mode},用户映射 ${Object.keys(dynamicAgentPolicy.userMap || {}).length},群映射 ${Object.keys(dynamicAgentPolicy.groupMap || {}).length})`
67
118
  : "ℹ️ 动态 Agent 路由未启用";
119
+ const entryVisibilityLine = "✅ 微信插件入口联系人:Agent 模式可见(自建应用)";
120
+ const observabilityLines = buildObservabilityStatusLines(observabilityMetrics);
68
121
 
69
122
  return `📊 系统状态
70
123
 
@@ -84,6 +137,8 @@ export function buildAgentStatusText({
84
137
  ✅ 多账户支持
85
138
  ${commandPolicyLine}
86
139
  ${allowFromPolicyLine}
140
+ ${dmPolicyLine}
141
+ ${eventPolicyLine}
87
142
  ${groupPolicyLine}
88
143
  ${debouncePolicyLine}
89
144
  ${streamingPolicyLine}
@@ -92,8 +147,11 @@ ${streamManagerPolicyLine}
92
147
  ${webhookBotPolicyLine}
93
148
  ${webhookTargetsLine}
94
149
  ${dynamicAgentPolicyLine}
150
+ ${entryVisibilityLine}
95
151
  ${proxyEnabled ? "✅ WeCom 出站代理已启用" : "ℹ️ WeCom 出站代理未启用"}
96
- ${voiceStatusLine}`;
152
+ ${voiceStatusLine}
153
+ ${observabilityLines.status}
154
+ ${observabilityLines.recent}`;
97
155
  }
98
156
 
99
157
  export function buildBotStatusText({
@@ -103,11 +161,14 @@ export function buildBotStatusText({
103
161
  allWebhookTargetAliases,
104
162
  commandPolicy,
105
163
  allowFromPolicy,
164
+ dmPolicy,
165
+ eventPolicy,
106
166
  groupPolicy,
107
167
  deliveryFallbackPolicy,
108
168
  streamManagerPolicy,
109
169
  webhookBotPolicy,
110
170
  dynamicAgentPolicy,
171
+ observabilityMetrics,
111
172
  } = {}) {
112
173
  const commandPolicyLine = commandPolicy.enabled
113
174
  ? `✅ 指令白名单已启用(${commandPolicy.allowlist.length} 条,管理员 ${commandPolicy.adminUsers.length} 人)`
@@ -116,12 +177,10 @@ export function buildBotStatusText({
116
177
  allowFromPolicy.allowFrom.length === 0 || allowFromPolicy.allowFrom.includes("*")
117
178
  ? "ℹ️ 发送者授权:未限制(allowFrom 未配置)"
118
179
  : `✅ 发送者授权:已限制 ${allowFromPolicy.allowFrom.length} 个用户`;
180
+ const dmPolicyLine = buildDmPolicyStatusLine(dmPolicy);
181
+ const eventPolicyLine = buildEventPolicyStatusLine(eventPolicy);
119
182
  const groupPolicyLine = groupPolicy.enabled
120
- ? groupPolicy.triggerMode === "mention"
121
- ? "✅ 群聊触发:仅 @ 命中后处理"
122
- : groupPolicy.triggerMode === "keyword"
123
- ? `✅ 群聊触发:关键词模式(${(groupPolicy.triggerKeywords || []).join(" / ") || "未配置关键词"})`
124
- : "✅ 群聊触发:无需 @(全部处理)"
183
+ ? "✅ 群聊触发:仅 @ 机器人后处理(企业微信 Bot 平台限制)"
125
184
  : "⚠️ 群聊处理未启用";
126
185
  const fallbackPolicyLine = deliveryFallbackPolicy.enabled
127
186
  ? `✅ 回包兜底链路已启用(${deliveryFallbackPolicy.order.join(" > ")})`
@@ -139,6 +198,8 @@ export function buildBotStatusText({
139
198
  const dynamicAgentPolicyLine = dynamicAgentPolicy.enabled
140
199
  ? `✅ 动态 Agent 路由已启用(mode=${dynamicAgentPolicy.mode},用户映射 ${Object.keys(dynamicAgentPolicy.userMap || {}).length},群映射 ${Object.keys(dynamicAgentPolicy.groupMap || {}).length})`
141
200
  : "ℹ️ 动态 Agent 路由未启用";
201
+ const entryVisibilityLine = "ℹ️ 微信插件入口联系人:Bot 模式通常不显示(请通过机器人会话/群聊入口触发)";
202
+ const observabilityLines = buildObservabilityStatusLines(observabilityMetrics);
142
203
  return `📊 系统状态
143
204
 
144
205
  渠道:企业微信 AI 机器人 (Bot)
@@ -150,12 +211,17 @@ Bot Webhook:${botConfig.webhookPath}
150
211
  ✅ 原生流式回复(stream)
151
212
  ${commandPolicyLine}
152
213
  ${allowFromPolicyLine}
214
+ ${dmPolicyLine}
215
+ ${eventPolicyLine}
153
216
  ${groupPolicyLine}
154
217
  ${fallbackPolicyLine}
155
218
  ${streamManagerPolicyLine}
156
219
  ${webhookBotPolicyLine}
157
220
  ${webhookTargetsLine}
158
- ${dynamicAgentPolicyLine}`;
221
+ ${dynamicAgentPolicyLine}
222
+ ${entryVisibilityLine}
223
+ ${observabilityLines.status}
224
+ ${observabilityLines.recent}`;
159
225
  }
160
226
 
161
227
  export function buildWecomBotHelpText() {
@@ -163,8 +229,11 @@ export function buildWecomBotHelpText() {
163
229
 
164
230
  可用命令:
165
231
  /help - 显示帮助信息
232
+ /new - 新建会话(兼容命令,等价于 /reset)
166
233
  /status - 查看系统状态
167
234
  /clear - 重置会话(等价于 /reset)
168
235
 
236
+ 说明:企业微信 Bot 群聊通常仅对 @ 机器人消息触发回调。
237
+
169
238
  直接发送消息即可与 AI 对话。`;
170
239
  }
@@ -0,0 +1,133 @@
1
+ function incrementCounter(map, key, delta = 1) {
2
+ const normalizedKey = String(key ?? "").trim() || "unknown";
3
+ map[normalizedKey] = Number(map[normalizedKey] || 0) + Number(delta || 0);
4
+ }
5
+
6
+ function safeNow(nowFn) {
7
+ try {
8
+ return Number(nowFn?.() || Date.now());
9
+ } catch {
10
+ return Date.now();
11
+ }
12
+ }
13
+
14
+ export function createWecomObservabilityMetricsStore({
15
+ maxRecentFailures = 40,
16
+ now = () => Date.now(),
17
+ } = {}) {
18
+ const state = {
19
+ inboundTotal: 0,
20
+ inboundByMode: { agent: 0, bot: 0 },
21
+ inboundByType: {},
22
+ inboundByAccount: {},
23
+ deliveryTotal: 0,
24
+ deliverySuccess: 0,
25
+ deliveryFailed: 0,
26
+ deliveryByLayer: {},
27
+ deliveryByStatus: {},
28
+ errorsTotal: 0,
29
+ errorsByScope: {},
30
+ recentFailures: [],
31
+ };
32
+
33
+ function pushRecentFailure(entry = {}) {
34
+ const row = {
35
+ at: safeNow(now),
36
+ scope: String(entry.scope ?? "unknown").trim() || "unknown",
37
+ reason: String(entry.reason ?? "unknown").trim().slice(0, 240) || "unknown",
38
+ accountId: String(entry.accountId ?? "").trim() || undefined,
39
+ layer: String(entry.layer ?? "").trim() || undefined,
40
+ };
41
+ state.recentFailures.push(row);
42
+ if (state.recentFailures.length > maxRecentFailures) {
43
+ state.recentFailures.splice(0, state.recentFailures.length - maxRecentFailures);
44
+ }
45
+ }
46
+
47
+ function recordInboundMetric({
48
+ mode = "agent",
49
+ msgType = "",
50
+ accountId = "default",
51
+ } = {}) {
52
+ const normalizedMode = String(mode ?? "agent").trim().toLowerCase() === "bot" ? "bot" : "agent";
53
+ const normalizedType = String(msgType ?? "").trim().toLowerCase() || "unknown";
54
+ const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
55
+ state.inboundTotal += 1;
56
+ state.inboundByMode[normalizedMode] = Number(state.inboundByMode[normalizedMode] || 0) + 1;
57
+ incrementCounter(state.inboundByType, normalizedType, 1);
58
+ incrementCounter(state.inboundByAccount, normalizedAccountId, 1);
59
+ }
60
+
61
+ function recordDeliveryMetric({
62
+ layer = "",
63
+ ok = false,
64
+ finalStatus = "",
65
+ accountId = "default",
66
+ attempts = [],
67
+ } = {}) {
68
+ const normalizedLayer = String(layer ?? "").trim().toLowerCase() || "unknown";
69
+ const normalizedStatus = String(finalStatus ?? "").trim().toLowerCase() || (ok ? "ok" : "failed");
70
+ const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
71
+ state.deliveryTotal += 1;
72
+ if (ok) state.deliverySuccess += 1;
73
+ else state.deliveryFailed += 1;
74
+ incrementCounter(state.deliveryByLayer, normalizedLayer, 1);
75
+ incrementCounter(state.deliveryByStatus, normalizedStatus, 1);
76
+ incrementCounter(state.inboundByAccount, normalizedAccountId, 0);
77
+
78
+ const normalizedAttempts = Array.isArray(attempts) ? attempts : [];
79
+ for (const attempt of normalizedAttempts) {
80
+ if (attempt?.status === "error") {
81
+ pushRecentFailure({
82
+ scope: "delivery",
83
+ reason: String(attempt?.reason ?? "unknown"),
84
+ accountId: normalizedAccountId,
85
+ layer: String(attempt?.layer ?? ""),
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ function recordRuntimeErrorMetric({
92
+ scope = "runtime",
93
+ reason = "",
94
+ accountId = "default",
95
+ layer = "",
96
+ } = {}) {
97
+ const normalizedScope = String(scope ?? "runtime").trim().toLowerCase() || "runtime";
98
+ const normalizedReason = String(reason ?? "").trim() || "unknown";
99
+ const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
100
+ state.errorsTotal += 1;
101
+ incrementCounter(state.errorsByScope, normalizedScope, 1);
102
+ pushRecentFailure({
103
+ scope: normalizedScope,
104
+ reason: normalizedReason,
105
+ accountId: normalizedAccountId,
106
+ layer,
107
+ });
108
+ }
109
+
110
+ function getWecomObservabilityMetrics() {
111
+ return {
112
+ inboundTotal: state.inboundTotal,
113
+ inboundByMode: { ...state.inboundByMode },
114
+ inboundByType: { ...state.inboundByType },
115
+ inboundByAccount: { ...state.inboundByAccount },
116
+ deliveryTotal: state.deliveryTotal,
117
+ deliverySuccess: state.deliverySuccess,
118
+ deliveryFailed: state.deliveryFailed,
119
+ deliveryByLayer: { ...state.deliveryByLayer },
120
+ deliveryByStatus: { ...state.deliveryByStatus },
121
+ errorsTotal: state.errorsTotal,
122
+ errorsByScope: { ...state.errorsByScope },
123
+ recentFailures: state.recentFailures.slice(-10).map((item) => ({ ...item })),
124
+ };
125
+ }
126
+
127
+ return {
128
+ recordInboundMetric,
129
+ recordDeliveryMetric,
130
+ recordRuntimeErrorMetric,
131
+ getWecomObservabilityMetrics,
132
+ };
133
+ }
@@ -14,11 +14,12 @@ export function createWecomAgentPushDeliverer({
14
14
  return async function deliverAgentPushReply({
15
15
  api,
16
16
  fromUser,
17
+ accountId = "default",
17
18
  content = "",
18
19
  fallbackText = "",
19
20
  mediaFallbackSuffix = "",
20
21
  } = {}) {
21
- const account = getWecomConfig(api, "default") ?? getWecomConfig(api);
22
+ const account = getWecomConfig(api, accountId) ?? getWecomConfig(api, "default") ?? getWecomConfig(api);
22
23
  if (!account?.corpId || !account?.corpSecret || !account?.agentId) {
23
24
  return { ok: false, reason: "agent-config-missing" };
24
25
  }
@@ -0,0 +1,103 @@
1
+ function clampCardContent(text, maxLength = 1400) {
2
+ const normalized = String(text ?? "").trim();
3
+ if (!normalized) return "";
4
+ const limit = Math.max(200, Number(maxLength) || 1400);
5
+ if (normalized.length <= limit) return normalized;
6
+ return `${normalized.slice(0, Math.max(0, limit - 3)).trim()}...`;
7
+ }
8
+
9
+ function buildMarkdownCardBody({
10
+ title,
11
+ subtitle,
12
+ content,
13
+ footer,
14
+ } = {}) {
15
+ const sections = [];
16
+ const normalizedTitle = String(title ?? "").trim();
17
+ const normalizedSubtitle = String(subtitle ?? "").trim();
18
+ const normalizedFooter = String(footer ?? "").trim();
19
+ const normalizedContent = String(content ?? "").trim();
20
+
21
+ if (normalizedTitle) {
22
+ sections.push(`### ${normalizedTitle}`);
23
+ }
24
+ if (normalizedSubtitle) {
25
+ sections.push(`> ${normalizedSubtitle}`);
26
+ }
27
+ if (normalizedContent) {
28
+ sections.push(normalizedContent);
29
+ }
30
+ if (normalizedFooter) {
31
+ sections.push(`> ${normalizedFooter}`);
32
+ }
33
+ return sections.join("\n\n").trim();
34
+ }
35
+
36
+ function buildTemplateCardBody({
37
+ title,
38
+ subtitle,
39
+ content,
40
+ footer,
41
+ } = {}) {
42
+ const normalizedTitle = String(title ?? "").trim() || "OpenClaw-Wechat";
43
+ const normalizedSubtitle = String(subtitle ?? "").trim();
44
+ const normalizedContent = String(content ?? "").trim();
45
+ const normalizedFooter = String(footer ?? "").trim();
46
+
47
+ const payload = {
48
+ msgtype: "template_card",
49
+ template_card: {
50
+ card_type: "text_notice",
51
+ main_title: {
52
+ title: normalizedTitle,
53
+ },
54
+ sub_title_text: normalizedContent,
55
+ },
56
+ };
57
+ if (normalizedSubtitle) {
58
+ payload.template_card.main_title.desc = normalizedSubtitle;
59
+ }
60
+ if (normalizedFooter) {
61
+ payload.template_card.quote_area = {
62
+ type: 0,
63
+ quote_text: normalizedFooter,
64
+ };
65
+ }
66
+ return payload;
67
+ }
68
+
69
+ export function buildWecomBotCardPayload({
70
+ text = "",
71
+ cardPolicy = {},
72
+ hasMedia = false,
73
+ } = {}) {
74
+ const enabled = cardPolicy?.enabled === true;
75
+ if (!enabled || hasMedia) return null;
76
+
77
+ const content = clampCardContent(text, cardPolicy?.maxContentLength);
78
+ if (!content) return null;
79
+
80
+ const mode = String(cardPolicy?.mode ?? "markdown").trim().toLowerCase();
81
+ if (mode === "template_card") {
82
+ return buildTemplateCardBody({
83
+ title: cardPolicy?.title,
84
+ subtitle: cardPolicy?.subtitle,
85
+ content,
86
+ footer: cardPolicy?.footer,
87
+ });
88
+ }
89
+
90
+ const markdownContent = buildMarkdownCardBody({
91
+ title: cardPolicy?.title,
92
+ subtitle: cardPolicy?.subtitle,
93
+ content,
94
+ footer: cardPolicy?.footer,
95
+ });
96
+ if (!markdownContent) return null;
97
+ return {
98
+ msgtype: "markdown",
99
+ markdown: {
100
+ content: markdownContent,
101
+ },
102
+ };
103
+ }