@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.
- package/CHANGELOG.md +119 -0
- package/README.en.md +89 -12
- package/README.md +103 -15
- package/docs/channels/wecom.md +28 -3
- package/openclaw.plugin.json +467 -10
- package/package.json +13 -2
- package/src/core/agent-routing.js +6 -0
- package/src/core.js +564 -35
- package/src/wecom/account-config-core.js +28 -8
- package/src/wecom/account-config.js +55 -0
- package/src/wecom/account-diagnostics.js +121 -0
- package/src/wecom/account-paths.js +39 -0
- package/src/wecom/agent-inbound-dispatch.js +9 -0
- package/src/wecom/agent-inbound-guards.js +24 -4
- package/src/wecom/agent-inbound-processor.js +27 -0
- package/src/wecom/agent-webhook-handler.js +11 -0
- package/src/wecom/bot-context.js +2 -1
- package/src/wecom/bot-dispatch-fallback.js +2 -1
- package/src/wecom/bot-inbound-content.js +73 -3
- package/src/wecom/bot-inbound-dispatch-runtime.js +2 -1
- package/src/wecom/bot-inbound-executor-helpers.js +56 -5
- package/src/wecom/bot-inbound-executor.js +19 -0
- package/src/wecom/bot-inbound-guards.js +36 -4
- package/src/wecom/bot-runtime-context.js +5 -3
- package/src/wecom/bot-webhook-dispatch.js +45 -12
- package/src/wecom/bot-webhook-handler.js +45 -13
- package/src/wecom/command-handlers.js +26 -0
- package/src/wecom/command-status-text.js +76 -7
- package/src/wecom/observability-metrics.js +133 -0
- package/src/wecom/outbound-agent-push.js +2 -1
- package/src/wecom/outbound-bot-card.js +103 -0
- package/src/wecom/outbound-delivery.js +92 -7
- package/src/wecom/outbound-response-delivery.js +10 -6
- package/src/wecom/outbound-webhook-delivery.js +42 -1
- package/src/wecom/plugin-account-policy-services.js +19 -0
- package/src/wecom/plugin-base-services.js +13 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +8 -0
- package/src/wecom/plugin-processing-deps.js +4 -0
- package/src/wecom/plugin-route-runtime-deps.js +5 -0
- package/src/wecom/plugin-services.js +7 -0
- package/src/wecom/policy-resolvers.js +82 -5
- package/src/wecom/register-runtime.js +31 -2
- package/src/wecom/route-registration.js +173 -41
- package/src/wecom/runtime-utils.js +7 -2
- package/src/wecom/webhook-adapter.js +61 -0
- 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
|
-
?
|
|
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
|
+
}
|