@yanhaidao/wecom 2.3.150 → 2.3.180
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 +238 -385
- package/SKILLS_CAL.md +895 -0
- package/SKILLS_DOC.md +2136 -0
- package/changelog/v2.3.16.md +11 -0
- package/changelog/v2.3.18.md +22 -0
- package/index.ts +39 -3
- package/package.json +2 -3
- package/src/agent/handler.event-filter.test.ts +11 -0
- package/src/agent/handler.ts +732 -643
- package/src/app/account-runtime.ts +46 -20
- package/src/app/index.ts +19 -1
- package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
- package/src/capability/calendar/client.ts +815 -0
- package/src/capability/calendar/index.ts +3 -0
- package/src/capability/calendar/schema.ts +417 -0
- package/src/capability/calendar/tool.ts +417 -0
- package/src/capability/calendar/types.ts +309 -0
- package/src/capability/doc/client.ts +567 -62
- package/src/capability/doc/schema.ts +419 -318
- package/src/capability/doc/tool.ts +1510 -1178
- package/src/capability/doc/types.ts +130 -14
- package/src/capability/mcp/index.ts +10 -0
- package/src/capability/mcp/schema.ts +107 -0
- package/src/capability/mcp/tool.ts +170 -0
- package/src/capability/mcp/transport.ts +394 -0
- package/src/channel.ts +70 -28
- package/src/config/schema.ts +71 -102
- package/src/outbound.test.ts +91 -14
- package/src/outbound.ts +143 -30
- package/src/runtime/reply-orchestrator.test.ts +35 -2
- package/src/runtime/reply-orchestrator.ts +14 -2
- package/src/runtime/session-manager.ts +20 -6
- package/src/runtime/source-registry.ts +165 -0
- package/src/target.ts +7 -4
- package/src/transport/bot-ws/inbound.test.ts +46 -0
- package/src/transport/bot-ws/inbound.ts +23 -5
- package/src/transport/bot-ws/media.ts +269 -0
- package/src/transport/bot-ws/reply.test.ts +85 -17
- package/src/transport/bot-ws/reply.ts +109 -21
- package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
- package/src/transport/bot-ws/sdk-adapter.ts +88 -12
- package/.claude/settings.local.json +0 -11
- package/docs/update-content-fix.md +0 -135
package/src/agent/handler.ts
CHANGED
|
@@ -3,29 +3,42 @@
|
|
|
3
3
|
* 处理 XML 格式回调
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
|
-
import path from "node:path";
|
|
8
6
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
9
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
10
|
-
import type {
|
|
10
|
+
import type { WecomAccountRuntime } from "../app/account-runtime.js";
|
|
11
|
+
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../config/index.js";
|
|
12
|
+
import {
|
|
13
|
+
buildAgentSessionTarget,
|
|
14
|
+
generateAgentId,
|
|
15
|
+
shouldUseDynamicAgent,
|
|
16
|
+
ensureDynamicAgentListed,
|
|
17
|
+
} from "../dynamic-agent.js";
|
|
18
|
+
import { getWecomRuntime } from "../runtime.js";
|
|
19
|
+
import { registerWecomSourceSnapshot } from "../runtime/source-registry.js";
|
|
11
20
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
buildWecomUnauthorizedCommandPrompt,
|
|
22
|
+
resolveWecomCommandAuthorization,
|
|
23
|
+
} from "../shared/command-auth.js";
|
|
24
|
+
import {
|
|
25
|
+
extractMsgType,
|
|
26
|
+
extractFromUser,
|
|
27
|
+
extractContent,
|
|
28
|
+
extractChatId,
|
|
29
|
+
extractMediaId,
|
|
30
|
+
extractMsgId,
|
|
31
|
+
extractFileName,
|
|
32
|
+
extractAgentId,
|
|
20
33
|
} from "../shared/xml-parser.js";
|
|
21
34
|
import { downloadAgentApiMedia, sendAgentApiText } from "../transport/agent-api/client.js";
|
|
22
|
-
import {
|
|
35
|
+
import type {
|
|
36
|
+
ResolvedAgentAccount,
|
|
37
|
+
UnifiedInboundEvent,
|
|
38
|
+
WecomInboundKind,
|
|
39
|
+
} from "../types/index.js";
|
|
23
40
|
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
24
41
|
import type { TransportSessionPatch } from "../types/index.js";
|
|
25
|
-
import type { WecomAccountRuntime } from "../app/account-runtime.js";
|
|
26
|
-
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
27
|
-
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../config/index.js";
|
|
28
|
-
import { buildAgentSessionTarget, generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
29
42
|
import type { WecomRuntimeAuditEvent } from "../types/runtime-context.js";
|
|
30
43
|
|
|
31
44
|
/** 错误提示信息 */
|
|
@@ -38,81 +51,81 @@ const recentAgentMsgIds = new Map<string, number>();
|
|
|
38
51
|
|
|
39
52
|
// Event deduplication (e.g. for ENTER_AGENT/subscribe welcome messages)
|
|
40
53
|
// We only want to send a welcome message once every 5 minutes per user
|
|
41
|
-
const RECENT_EVENT_TTL_MS =3 * 60 * 1000;
|
|
54
|
+
const RECENT_EVENT_TTL_MS = 3 * 60 * 1000;
|
|
42
55
|
const recentAgentEvents = new Map<string, number>();
|
|
43
56
|
|
|
44
57
|
function rememberAgentEvent(key: string): boolean {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const existing = recentAgentEvents.get(key);
|
|
60
|
+
if (existing && now - existing < RECENT_EVENT_TTL_MS) return false;
|
|
61
|
+
recentAgentEvents.set(key, now);
|
|
62
|
+
// Prune expired
|
|
63
|
+
for (const [k, ts] of recentAgentEvents) {
|
|
64
|
+
if (now - ts >= RECENT_EVENT_TTL_MS) recentAgentEvents.delete(k);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
function rememberAgentMsgId(msgId: string): boolean {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const existing = recentAgentMsgIds.get(msgId);
|
|
72
|
+
if (existing && now - existing < RECENT_MSGID_TTL_MS) return false;
|
|
73
|
+
recentAgentMsgIds.set(msgId, now);
|
|
74
|
+
// 简单清理:只在写入时做一次线性 prune,避免无界增长
|
|
75
|
+
for (const [k, ts] of recentAgentMsgIds) {
|
|
76
|
+
if (now - ts >= RECENT_MSGID_TTL_MS) recentAgentMsgIds.delete(k);
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
function looksLikeTextFile(buffer: Buffer): boolean {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const sampleSize = Math.min(buffer.length, 4096);
|
|
83
|
+
if (sampleSize === 0) return true;
|
|
84
|
+
let bad = 0;
|
|
85
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
86
|
+
const b = buffer[i]!;
|
|
87
|
+
const isWhitespace = b === 0x09 || b === 0x0a || b === 0x0d; // \t \n \r
|
|
88
|
+
const isPrintable = b >= 0x20 && b !== 0x7f;
|
|
89
|
+
if (!isWhitespace && !isPrintable) bad++;
|
|
90
|
+
}
|
|
91
|
+
// 非可打印字符占比太高,基本可判断为二进制
|
|
92
|
+
return bad / sampleSize <= 0.02;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
|
-
function analyzeTextHeuristic(buffer: Buffer): {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
function analyzeTextHeuristic(buffer: Buffer): {
|
|
96
|
+
sampleSize: number;
|
|
97
|
+
badCount: number;
|
|
98
|
+
badRatio: number;
|
|
99
|
+
} {
|
|
100
|
+
const sampleSize = Math.min(buffer.length, 4096);
|
|
101
|
+
if (sampleSize === 0) return { sampleSize: 0, badCount: 0, badRatio: 0 };
|
|
102
|
+
let badCount = 0;
|
|
103
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
104
|
+
const b = buffer[i]!;
|
|
105
|
+
const isWhitespace = b === 0x09 || b === 0x0a || b === 0x0d;
|
|
106
|
+
const isPrintable = b >= 0x20 && b !== 0x7f;
|
|
107
|
+
if (!isWhitespace && !isPrintable) badCount++;
|
|
108
|
+
}
|
|
109
|
+
return { sampleSize, badCount, badRatio: badCount / sampleSize };
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
function previewHex(buffer: Buffer, maxBytes = 32): string {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.subarray(0, n)
|
|
100
|
-
.toString("hex")
|
|
101
|
-
.replace(/(..)/g, "$1 ")
|
|
102
|
-
.trim();
|
|
113
|
+
const n = Math.min(buffer.length, maxBytes);
|
|
114
|
+
if (n <= 0) return "";
|
|
115
|
+
return buffer.subarray(0, n).toString("hex").replace(/(..)/g, "$1 ").trim();
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
function buildTextFilePreview(buffer: Buffer, maxChars: number): string | undefined {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
if (!looksLikeTextFile(buffer)) return undefined;
|
|
120
|
+
const text = buffer.toString("utf8");
|
|
121
|
+
if (!text.trim()) return undefined;
|
|
122
|
+
const truncated = text.length > maxChars ? `${text.slice(0, maxChars)}\n…(已截断)` : text;
|
|
123
|
+
return truncated;
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
/**
|
|
114
127
|
* **AgentWebhookParams (Webhook 处理器参数)**
|
|
115
|
-
*
|
|
128
|
+
*
|
|
116
129
|
* 传递给 Agent Webhook 处理函数的上下文参数集合。
|
|
117
130
|
* @property req Node.js 原始请求对象
|
|
118
131
|
* @property res Node.js 原始响应对象
|
|
@@ -123,32 +136,32 @@ function buildTextFilePreview(buffer: Buffer, maxChars: number): string | undefi
|
|
|
123
136
|
* @property error 可选错误输出函数
|
|
124
137
|
*/
|
|
125
138
|
export type AgentWebhookParams = {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
req: IncomingMessage;
|
|
140
|
+
res: ServerResponse;
|
|
141
|
+
/**
|
|
142
|
+
* 上游已完成验签/解密时传入,避免重复协议处理。
|
|
143
|
+
* 仅用于 POST 消息回调流程。
|
|
144
|
+
*/
|
|
145
|
+
verifiedPost?: {
|
|
146
|
+
timestamp: string;
|
|
147
|
+
nonce: string;
|
|
148
|
+
signature: string;
|
|
149
|
+
encrypted: string;
|
|
150
|
+
decrypted: string;
|
|
151
|
+
parsed: WecomAgentInboundMessage;
|
|
152
|
+
};
|
|
153
|
+
agent: ResolvedAgentAccount;
|
|
154
|
+
config: OpenClawConfig;
|
|
155
|
+
core: PluginRuntime;
|
|
156
|
+
log?: (msg: string) => void;
|
|
157
|
+
error?: (msg: string) => void;
|
|
158
|
+
auditSink?: (event: WecomRuntimeAuditEvent) => void;
|
|
159
|
+
touchTransportSession?: (patch: TransportSessionPatch) => void;
|
|
147
160
|
};
|
|
148
161
|
|
|
149
162
|
export type AgentInboundProcessDecision = {
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
shouldProcess: boolean;
|
|
164
|
+
reason: string;
|
|
152
165
|
};
|
|
153
166
|
|
|
154
167
|
/**
|
|
@@ -158,213 +171,241 @@ export type AgentInboundProcessDecision = {
|
|
|
158
171
|
* - 缺失发送者时默认丢弃,避免写入异常会话
|
|
159
172
|
*/
|
|
160
173
|
export function shouldProcessAgentInboundMessage(params: {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
msgType: string;
|
|
175
|
+
fromUser: string;
|
|
176
|
+
chatId?: string;
|
|
177
|
+
eventType?: string;
|
|
164
178
|
}): AgentInboundProcessDecision {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
reason: "missing_sender",
|
|
203
|
-
};
|
|
179
|
+
const msgType = String(params.msgType ?? "")
|
|
180
|
+
.trim()
|
|
181
|
+
.toLowerCase();
|
|
182
|
+
const fromUser = String(params.fromUser ?? "").trim();
|
|
183
|
+
const chatId = String(params.chatId ?? "").trim();
|
|
184
|
+
const normalizedFromUser = fromUser.toLowerCase();
|
|
185
|
+
const eventType = String(params.eventType ?? "")
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase();
|
|
188
|
+
|
|
189
|
+
if (msgType === "event") {
|
|
190
|
+
const allowedEvents = [
|
|
191
|
+
"subscribe",
|
|
192
|
+
"enter_agent",
|
|
193
|
+
"batch_job_result",
|
|
194
|
+
// WeCom Doc events
|
|
195
|
+
"doc_create",
|
|
196
|
+
"doc_delete",
|
|
197
|
+
"doc_content_change",
|
|
198
|
+
"doc_member_change",
|
|
199
|
+
// WeCom Form events
|
|
200
|
+
"wedoc_collect_submit",
|
|
201
|
+
// SmartSheet events
|
|
202
|
+
"smartsheet_record_change",
|
|
203
|
+
"smartsheet_field_change",
|
|
204
|
+
"smartsheet_view_change",
|
|
205
|
+
];
|
|
206
|
+
if (
|
|
207
|
+
allowedEvents.includes(eventType) ||
|
|
208
|
+
eventType.startsWith("doc_") ||
|
|
209
|
+
eventType.startsWith("wedoc_") ||
|
|
210
|
+
eventType.startsWith("smartsheet_")
|
|
211
|
+
) {
|
|
212
|
+
return {
|
|
213
|
+
shouldProcess: true,
|
|
214
|
+
reason: `allowed_event:${eventType}`,
|
|
215
|
+
};
|
|
204
216
|
}
|
|
217
|
+
return {
|
|
218
|
+
shouldProcess: false,
|
|
219
|
+
reason: `event:${eventType || "unknown"}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
205
222
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
if (!fromUser) {
|
|
224
|
+
if (chatId) {
|
|
225
|
+
return {
|
|
226
|
+
shouldProcess: true,
|
|
227
|
+
reason: "missing_sender_but_group_chat",
|
|
228
|
+
};
|
|
211
229
|
}
|
|
230
|
+
return {
|
|
231
|
+
shouldProcess: false,
|
|
232
|
+
reason: "missing_sender",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
212
235
|
|
|
236
|
+
if (normalizedFromUser === "sys") {
|
|
213
237
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
shouldProcess: false,
|
|
239
|
+
reason: "system_sender",
|
|
216
240
|
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
shouldProcess: true,
|
|
245
|
+
reason: "user_message",
|
|
246
|
+
};
|
|
217
247
|
}
|
|
218
248
|
|
|
219
249
|
function normalizeAgentId(value: unknown): number | undefined {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
250
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
251
|
+
const raw = String(value ?? "").trim();
|
|
252
|
+
if (!raw) return undefined;
|
|
253
|
+
const parsed = Number(raw);
|
|
254
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
225
255
|
}
|
|
226
256
|
|
|
227
257
|
/**
|
|
228
258
|
* **resolveQueryParams (解析查询参数)**
|
|
229
|
-
*
|
|
259
|
+
*
|
|
230
260
|
* 辅助函数:从 IncomingMessage 中解析 URL 查询字符串,用于获取签名、时间戳等参数。
|
|
231
261
|
*/
|
|
232
262
|
function resolveQueryParams(req: IncomingMessage): URLSearchParams {
|
|
233
|
-
|
|
234
|
-
|
|
263
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
264
|
+
return url.searchParams;
|
|
235
265
|
}
|
|
236
266
|
|
|
237
267
|
/**
|
|
238
268
|
* 处理消息回调 (POST)
|
|
239
269
|
*/
|
|
240
270
|
async function handleMessageCallback(params: AgentWebhookParams): Promise<boolean> {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
log?.(`[wecom-agent] inbound: method=${req.method ?? "UNKNOWN"} remote=${req.socket?.remoteAddress ?? "unknown"}`);
|
|
253
|
-
const query = resolveQueryParams(req);
|
|
254
|
-
const querySignature = query.get("msg_signature") ?? "";
|
|
271
|
+
const { req, res, verifiedPost, agent, config, core, log, error, auditSink } = params;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
if (!verifiedPost) {
|
|
275
|
+
error?.("[wecom-agent] inbound: missing preverified envelope");
|
|
276
|
+
res.statusCode = 400;
|
|
277
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
278
|
+
res.end(`invalid request - 缺少上游验签结果${ERROR_HELP}`);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
255
281
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
log?.(
|
|
283
|
+
`[wecom-agent] inbound: method=${req.method ?? "UNKNOWN"} remote=${req.socket?.remoteAddress ?? "unknown"}`,
|
|
284
|
+
);
|
|
285
|
+
const query = resolveQueryParams(req);
|
|
286
|
+
const querySignature = query.get("msg_signature") ?? "";
|
|
287
|
+
|
|
288
|
+
const encrypted = verifiedPost.encrypted;
|
|
289
|
+
const decrypted = verifiedPost.decrypted;
|
|
290
|
+
const msg = verifiedPost.parsed;
|
|
291
|
+
const timestamp = verifiedPost.timestamp;
|
|
292
|
+
const nonce = verifiedPost.nonce;
|
|
293
|
+
const signature = verifiedPost.signature || querySignature;
|
|
294
|
+
log?.(
|
|
295
|
+
`[wecom-agent] inbound: using preverified envelope timestamp=${timestamp ? "yes" : "no"} nonce=${nonce ? "yes" : "no"} msg_signature=${signature ? "yes" : "no"} encryptLen=${encrypted.length}`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
log?.(`[wecom-agent] inbound: decryptedBytes=${Buffer.byteLength(decrypted, "utf8")}`);
|
|
299
|
+
|
|
300
|
+
const inboundAgentId = normalizeAgentId(extractAgentId(msg));
|
|
301
|
+
if (
|
|
302
|
+
inboundAgentId !== undefined &&
|
|
303
|
+
typeof agent.agentId === "number" &&
|
|
304
|
+
Number.isFinite(agent.agentId) &&
|
|
305
|
+
inboundAgentId !== agent.agentId
|
|
306
|
+
) {
|
|
307
|
+
error?.(
|
|
308
|
+
`[wecom-agent] inbound: agentId mismatch ignored expectedAgentId=${agent.agentId} actualAgentId=${String(extractAgentId(msg) ?? "")}`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const msgType = extractMsgType(msg);
|
|
312
|
+
const fromUser = extractFromUser(msg);
|
|
313
|
+
const chatId = extractChatId(msg);
|
|
314
|
+
const msgId = extractMsgId(msg);
|
|
315
|
+
const eventType = String((msg as Record<string, unknown>).Event ?? "")
|
|
316
|
+
.trim()
|
|
317
|
+
.toLowerCase();
|
|
318
|
+
|
|
319
|
+
if (msgId) {
|
|
320
|
+
const ok = rememberAgentMsgId(msgId);
|
|
321
|
+
if (!ok) {
|
|
262
322
|
log?.(
|
|
263
|
-
|
|
323
|
+
`[wecom-agent] duplicate msgId=${msgId} from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}; skipped`,
|
|
264
324
|
);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
`[wecom-agent] inbound: agentId mismatch ignored expectedAgentId=${agent.agentId} actualAgentId=${String(extractAgentId(msg) ?? "")}`,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
const msgType = extractMsgType(msg);
|
|
280
|
-
const fromUser = extractFromUser(msg);
|
|
281
|
-
const chatId = extractChatId(msg);
|
|
282
|
-
const msgId = extractMsgId(msg);
|
|
283
|
-
const eventType = String((msg as Record<string, unknown>).Event ?? "").trim().toLowerCase();
|
|
284
|
-
|
|
285
|
-
if (msgId) {
|
|
286
|
-
const ok = rememberAgentMsgId(msgId);
|
|
287
|
-
if (!ok) {
|
|
288
|
-
log?.(`[wecom-agent] duplicate msgId=${msgId} from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}; skipped`);
|
|
289
|
-
auditSink?.({
|
|
290
|
-
transport: "agent-callback",
|
|
291
|
-
category: "duplicate-reply",
|
|
292
|
-
messageId: msgId,
|
|
293
|
-
summary: `duplicate agent callback from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}`,
|
|
294
|
-
raw: {
|
|
295
|
-
transport: "agent-callback",
|
|
296
|
-
envelopeType: "xml",
|
|
297
|
-
body: msg,
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
res.statusCode = 200;
|
|
301
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
302
|
-
res.end("success");
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Agent 模式下 enter_agent / subscribe 不做任何处理,静默回 success
|
|
308
|
-
if (msgType === "event" && (eventType === "enter_agent" || eventType === "subscribe")) {
|
|
309
|
-
log?.(`[wecom-agent] ignoring ${eventType} from=${fromUser}; agent does not handle welcome events`);
|
|
310
|
-
res.statusCode = 200;
|
|
311
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
312
|
-
res.end("success");
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
const content = String(extractContent(msg) ?? "");
|
|
316
|
-
|
|
317
|
-
const preview = content.length > 100 ? `${content.slice(0, 100)}…` : content;
|
|
318
|
-
log?.(`[wecom-agent] ${msgType} from=${fromUser} chatId=${chatId ?? "N/A"} msgId=${msgId ?? "N/A"} content=${preview}`);
|
|
319
|
-
|
|
320
|
-
// 先返回 success (Agent 模式使用 API 发送回复,不用被动回复)
|
|
325
|
+
auditSink?.({
|
|
326
|
+
transport: "agent-callback",
|
|
327
|
+
category: "duplicate-reply",
|
|
328
|
+
messageId: msgId,
|
|
329
|
+
summary: `duplicate agent callback from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}`,
|
|
330
|
+
raw: {
|
|
331
|
+
transport: "agent-callback",
|
|
332
|
+
envelopeType: "xml",
|
|
333
|
+
body: msg,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
321
336
|
res.statusCode = 200;
|
|
322
337
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
323
338
|
res.end("success");
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
324
342
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
// Agent 模式下 enter_agent / subscribe 不做任何处理,静默回 success
|
|
344
|
+
if (msgType === "event" && (eventType === "enter_agent" || eventType === "subscribe")) {
|
|
345
|
+
log?.(
|
|
346
|
+
`[wecom-agent] ignoring ${eventType} from=${fromUser}; agent does not handle welcome events`,
|
|
347
|
+
);
|
|
348
|
+
res.statusCode = 200;
|
|
349
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
350
|
+
res.end("success");
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
const content = String(extractContent(msg) ?? "");
|
|
354
|
+
|
|
355
|
+
const preview = content.length > 100 ? `${content.slice(0, 100)}…` : content;
|
|
356
|
+
log?.(
|
|
357
|
+
`[wecom-agent] ${msgType} from=${fromUser} chatId=${chatId ?? "N/A"} msgId=${msgId ?? "N/A"} content=${preview}`,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// 先返回 success (Agent 模式使用 API 发送回复,不用被动回复)
|
|
361
|
+
res.statusCode = 200;
|
|
362
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
363
|
+
res.end("success");
|
|
364
|
+
|
|
365
|
+
const decision = shouldProcessAgentInboundMessage({
|
|
366
|
+
msgType,
|
|
367
|
+
fromUser,
|
|
368
|
+
chatId,
|
|
369
|
+
eventType,
|
|
370
|
+
});
|
|
371
|
+
if (!decision.shouldProcess) {
|
|
372
|
+
log?.(
|
|
373
|
+
`[wecom-agent] skip processing: type=${msgType || "unknown"} event=${eventType || "N/A"} from=${fromUser || "N/A"} reason=${decision.reason}`,
|
|
374
|
+
);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
336
377
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
378
|
+
// 异步处理消息
|
|
379
|
+
processAgentMessage({
|
|
380
|
+
agent,
|
|
381
|
+
config,
|
|
382
|
+
core,
|
|
383
|
+
fromUser,
|
|
384
|
+
chatId,
|
|
385
|
+
msgType,
|
|
386
|
+
content,
|
|
387
|
+
msg,
|
|
388
|
+
log,
|
|
389
|
+
error,
|
|
390
|
+
auditSink,
|
|
391
|
+
touchTransportSession: params.touchTransportSession,
|
|
392
|
+
}).catch((err) => {
|
|
393
|
+
error?.(`[wecom-agent] process failed: ${String(err)}`);
|
|
394
|
+
});
|
|
354
395
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
396
|
+
return true;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
error?.(`[wecom-agent] callback failed: ${String(err)}`);
|
|
399
|
+
res.statusCode = 400;
|
|
400
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
401
|
+
res.end(`error - 回调处理失败${ERROR_HELP}`);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
363
404
|
}
|
|
364
405
|
|
|
365
406
|
/**
|
|
366
407
|
* **processAgentMessage (处理 Agent 消息)**
|
|
367
|
-
*
|
|
408
|
+
*
|
|
368
409
|
* 异步处理解密后的消息内容,并触发 OpenClaw Agent。
|
|
369
410
|
* 流程:
|
|
370
411
|
* 1. 路由解析:根据 userid或群ID 确定 Agent 路由。
|
|
@@ -374,279 +415,321 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
374
415
|
* 5. 调度回复:将 Agent 的响应通过 `api-client` 发送回企业微信。
|
|
375
416
|
*/
|
|
376
417
|
async function processAgentMessage(params: {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
418
|
+
agent: ResolvedAgentAccount;
|
|
419
|
+
config: OpenClawConfig;
|
|
420
|
+
core: PluginRuntime;
|
|
421
|
+
fromUser: string;
|
|
422
|
+
chatId?: string;
|
|
423
|
+
msgType: string;
|
|
424
|
+
content: string;
|
|
425
|
+
msg: WecomAgentInboundMessage;
|
|
426
|
+
log?: (msg: string) => void;
|
|
427
|
+
error?: (msg: string) => void;
|
|
428
|
+
auditSink?: (event: WecomRuntimeAuditEvent) => void;
|
|
429
|
+
touchTransportSession?: (patch: TransportSessionPatch) => void;
|
|
389
430
|
}): Promise<void> {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
431
|
+
const {
|
|
432
|
+
agent,
|
|
433
|
+
config,
|
|
434
|
+
core,
|
|
435
|
+
fromUser,
|
|
436
|
+
chatId,
|
|
437
|
+
content,
|
|
438
|
+
msg,
|
|
439
|
+
msgType,
|
|
440
|
+
log,
|
|
441
|
+
error,
|
|
442
|
+
auditSink,
|
|
443
|
+
touchTransportSession,
|
|
444
|
+
} = params;
|
|
445
|
+
|
|
446
|
+
const isGroup = Boolean(chatId);
|
|
447
|
+
const peerId = isGroup ? chatId! : fromUser;
|
|
448
|
+
const replyTarget = isGroup
|
|
449
|
+
? ({ toUser: undefined, chatId: peerId } as const)
|
|
450
|
+
: ({ toUser: fromUser, chatId: undefined } as const);
|
|
451
|
+
const eventType = String(msg.Event ?? "")
|
|
452
|
+
.trim()
|
|
453
|
+
.toLowerCase();
|
|
454
|
+
|
|
455
|
+
const resolveInboundKind = (): WecomInboundKind => {
|
|
456
|
+
if (msgType === "event") {
|
|
457
|
+
if (eventType === "subscribe" || eventType === "enter_agent") return "welcome";
|
|
458
|
+
return "event";
|
|
459
|
+
}
|
|
460
|
+
if (msgType === "image") return "image";
|
|
461
|
+
if (msgType === "voice") return "voice";
|
|
462
|
+
if (msgType === "video") return "video";
|
|
463
|
+
if (msgType === "file") return "file";
|
|
464
|
+
if (msgType === "location") return "location";
|
|
465
|
+
if (msgType === "link") return "link";
|
|
466
|
+
return "text";
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const inboundKind = resolveInboundKind();
|
|
470
|
+
const resolveEventText = (): string => {
|
|
471
|
+
if (inboundKind === "welcome" && agent.config.welcomeText) {
|
|
472
|
+
return agent.config.welcomeText;
|
|
473
|
+
}
|
|
474
|
+
if (msgType === "event") {
|
|
475
|
+
return `[event:${eventType || "unknown"}]`;
|
|
476
|
+
}
|
|
477
|
+
return content;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// BUG FIX: 真正调用 resolveEventText() 获取欢迎语或事件描述
|
|
481
|
+
const resolvedContent = resolveEventText();
|
|
482
|
+
let finalContent = resolvedContent;
|
|
483
|
+
|
|
484
|
+
const mediaMaxBytes = resolveWecomMediaMaxBytes(config);
|
|
485
|
+
|
|
486
|
+
// 处理媒体文件
|
|
487
|
+
const attachments: NonNullable<UnifiedInboundEvent["attachments"]> = [];
|
|
488
|
+
let mediaPath: string | undefined;
|
|
489
|
+
let mediaType: string | undefined;
|
|
490
|
+
|
|
491
|
+
if (["image", "voice", "video", "file"].includes(msgType)) {
|
|
492
|
+
const mediaId = extractMediaId(msg);
|
|
493
|
+
if (mediaId) {
|
|
494
|
+
try {
|
|
495
|
+
log?.(`[wecom-agent] downloading media: ${mediaId} (${msgType})`);
|
|
496
|
+
const {
|
|
497
|
+
buffer,
|
|
498
|
+
contentType,
|
|
499
|
+
filename: headerFileName,
|
|
500
|
+
} = await downloadAgentApiMedia({ agent, mediaId, maxBytes: mediaMaxBytes });
|
|
501
|
+
const xmlFileName = extractFileName(msg);
|
|
502
|
+
const originalFileName = (xmlFileName || headerFileName || `${mediaId}.bin`).trim();
|
|
503
|
+
const heuristic = analyzeTextHeuristic(buffer);
|
|
504
|
+
|
|
505
|
+
// 推断文件名后缀
|
|
506
|
+
const extMap: Record<string, string> = {
|
|
507
|
+
"image/jpeg": "jpg",
|
|
508
|
+
"image/png": "png",
|
|
509
|
+
"image/gif": "gif",
|
|
510
|
+
"audio/amr": "amr",
|
|
511
|
+
"audio/speex": "speex",
|
|
512
|
+
"video/mp4": "mp4",
|
|
513
|
+
};
|
|
514
|
+
const textPreview = msgType === "file" ? buildTextFilePreview(buffer, 12_000) : undefined;
|
|
515
|
+
const looksText = Boolean(textPreview);
|
|
516
|
+
const originalExt = path.extname(originalFileName).toLowerCase();
|
|
517
|
+
const normalizedContentType =
|
|
518
|
+
looksText && originalExt === ".md"
|
|
519
|
+
? "text/markdown"
|
|
520
|
+
: looksText && (!contentType || contentType === "application/octet-stream")
|
|
521
|
+
? "text/plain; charset=utf-8"
|
|
522
|
+
: contentType;
|
|
523
|
+
|
|
524
|
+
const ext = extMap[normalizedContentType] || (looksText ? "txt" : "bin");
|
|
525
|
+
const filename = `${mediaId}.${ext}`;
|
|
458
526
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
527
|
+
log?.(
|
|
528
|
+
`[wecom-agent] file meta: msgType=${msgType} mediaId=${mediaId} size=${buffer.length} maxBytes=${mediaMaxBytes} ` +
|
|
529
|
+
`contentType=${contentType} normalizedContentType=${normalizedContentType} originalFileName=${originalFileName} ` +
|
|
530
|
+
`xmlFileName=${xmlFileName ?? "N/A"} headerFileName=${headerFileName ?? "N/A"} ` +
|
|
531
|
+
`textHeuristic(sample=${heuristic.sampleSize}, bad=${heuristic.badCount}, ratio=${heuristic.badRatio.toFixed(4)}) ` +
|
|
532
|
+
`headHex="${previewHex(buffer)}"`,
|
|
533
|
+
);
|
|
466
534
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
535
|
+
// 使用 Core SDK 保存媒体文件
|
|
536
|
+
const saved = await core.channel.media.saveMediaBuffer(
|
|
537
|
+
buffer,
|
|
538
|
+
normalizedContentType,
|
|
539
|
+
"inbound", // context/scope
|
|
540
|
+
mediaMaxBytes, // limit
|
|
541
|
+
originalFileName,
|
|
542
|
+
);
|
|
475
543
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
544
|
+
log?.(`[wecom-agent] media saved to: ${saved.path}`);
|
|
545
|
+
mediaPath = saved.path;
|
|
546
|
+
mediaType = normalizedContentType;
|
|
479
547
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
548
|
+
// 构建附件
|
|
549
|
+
attachments.push({
|
|
550
|
+
name: originalFileName,
|
|
551
|
+
contentType: normalizedContentType,
|
|
552
|
+
remoteUrl: pathToFileURL(saved.path).href, // 使用跨平台安全的文件 URL
|
|
553
|
+
});
|
|
486
554
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
} else {
|
|
499
|
-
if (msgType === "file") {
|
|
500
|
-
finalContent = [
|
|
501
|
-
content,
|
|
502
|
-
"",
|
|
503
|
-
`已收到文件:${originalFileName}`,
|
|
504
|
-
`文件类型:${normalizedContentType || contentType || "未知"}`,
|
|
505
|
-
"提示:当前仅对文本/Markdown/JSON/CSV/HTML/PDF(可选)做内容抽取;其他二进制格式请转为 PDF 或复制文本内容。",
|
|
506
|
-
`(已下载 ${buffer.length} 字节)`,
|
|
507
|
-
].join("\n");
|
|
508
|
-
} else {
|
|
509
|
-
finalContent = `${content} (已下载 ${buffer.length} 字节)`;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
log?.(`[wecom-agent] file preview: enabled=${looksText} finalContentLen=${finalContent.length} attachments=${attachments.length}`);
|
|
513
|
-
} catch (err) {
|
|
514
|
-
error?.(`[wecom-agent] media processing failed: ${String(err)}`);
|
|
515
|
-
auditSink?.({
|
|
516
|
-
transport: "agent-callback",
|
|
517
|
-
category: "runtime-error",
|
|
518
|
-
messageId: extractMsgId(msg) ?? undefined,
|
|
519
|
-
summary: `agent media processing failed mediaId=${mediaId}`,
|
|
520
|
-
raw: {
|
|
521
|
-
transport: "agent-callback",
|
|
522
|
-
envelopeType: "xml",
|
|
523
|
-
body: msg,
|
|
524
|
-
},
|
|
525
|
-
error: err instanceof Error ? err.message : String(err),
|
|
526
|
-
});
|
|
527
|
-
finalContent = [
|
|
528
|
-
content,
|
|
529
|
-
"",
|
|
530
|
-
`媒体处理失败:${String(err)}`,
|
|
531
|
-
`提示:可在 OpenClaw 配置中提高 channels.wecom.media.maxBytes(当前=${mediaMaxBytes})`,
|
|
532
|
-
`例如:openclaw config set channels.wecom.media.maxBytes ${50 * 1024 * 1024}`,
|
|
533
|
-
].join("\n");
|
|
534
|
-
}
|
|
555
|
+
// 更新文本提示
|
|
556
|
+
if (textPreview) {
|
|
557
|
+
finalContent = [
|
|
558
|
+
content,
|
|
559
|
+
"",
|
|
560
|
+
"文件内容预览:",
|
|
561
|
+
"```",
|
|
562
|
+
textPreview,
|
|
563
|
+
"```",
|
|
564
|
+
`(已下载 ${buffer.length} 字节)`,
|
|
565
|
+
].join("\n");
|
|
535
566
|
} else {
|
|
536
|
-
|
|
537
|
-
|
|
567
|
+
if (msgType === "file") {
|
|
568
|
+
finalContent = [
|
|
569
|
+
content,
|
|
570
|
+
"",
|
|
571
|
+
`已收到文件:${originalFileName}`,
|
|
572
|
+
`文件类型:${normalizedContentType || contentType || "未知"}`,
|
|
573
|
+
"提示:当前仅对文本/Markdown/JSON/CSV/HTML/PDF(可选)做内容抽取;其他二进制格式请转为 PDF 或复制文本内容。",
|
|
574
|
+
`(已下载 ${buffer.length} 字节)`,
|
|
575
|
+
].join("\n");
|
|
576
|
+
} else {
|
|
577
|
+
finalContent = `${content} (已下载 ${buffer.length} 字节)`;
|
|
578
|
+
}
|
|
538
579
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// 解析路由
|
|
542
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
543
|
-
cfg: config,
|
|
544
|
-
channel: "wecom",
|
|
545
|
-
accountId: agent.accountId,
|
|
546
|
-
peer: { kind: isGroup ? "group" : "direct", id: peerId },
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// ===== 动态 Agent 路由注入 =====
|
|
550
|
-
const useDynamicAgent = shouldUseDynamicAgent({
|
|
551
|
-
chatType: isGroup ? "group" : "dm",
|
|
552
|
-
senderId: fromUser,
|
|
553
|
-
config,
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
if (shouldRejectWecomDefaultRoute({ cfg: config, matchedBy: route.matchedBy, useDynamicAgent })) {
|
|
557
|
-
const prompt =
|
|
558
|
-
`当前账号(${agent.accountId})未绑定 OpenClaw Agent,已拒绝回退到默认主智能体。` +
|
|
559
|
-
`请在 bindings 中添加:{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${agent.accountId}"}}`;
|
|
560
|
-
error?.(
|
|
561
|
-
`[wecom-agent] routing guard: blocked default fallback accountId=${agent.accountId} matchedBy=${route.matchedBy} from=${fromUser}`,
|
|
580
|
+
log?.(
|
|
581
|
+
`[wecom-agent] file preview: enabled=${looksText} finalContentLen=${finalContent.length} attachments=${attachments.length}`,
|
|
562
582
|
);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
583
|
+
} catch (err) {
|
|
584
|
+
error?.(`[wecom-agent] media processing failed: ${String(err)}`);
|
|
585
|
+
auditSink?.({
|
|
586
|
+
transport: "agent-callback",
|
|
587
|
+
category: "runtime-error",
|
|
588
|
+
messageId: extractMsgId(msg) ?? undefined,
|
|
589
|
+
summary: `agent media processing failed mediaId=${mediaId}`,
|
|
590
|
+
raw: {
|
|
591
|
+
transport: "agent-callback",
|
|
592
|
+
envelopeType: "xml",
|
|
593
|
+
body: msg,
|
|
594
|
+
},
|
|
595
|
+
error: err instanceof Error ? err.message : String(err),
|
|
596
|
+
});
|
|
597
|
+
finalContent = [
|
|
598
|
+
content,
|
|
599
|
+
"",
|
|
600
|
+
`媒体处理失败:${String(err)}`,
|
|
601
|
+
`提示:可在 OpenClaw 配置中提高 channels.wecom.media.maxBytes(当前=${mediaMaxBytes})`,
|
|
602
|
+
`例如:openclaw config set channels.wecom.media.maxBytes ${50 * 1024 * 1024}`,
|
|
603
|
+
].join("\n");
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
const keys = Object.keys((msg as unknown as Record<string, unknown>) ?? {})
|
|
607
|
+
.slice(0, 50)
|
|
608
|
+
.join(",");
|
|
609
|
+
error?.(`[wecom-agent] mediaId not found for ${msgType}; keys=${keys}`);
|
|
582
610
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 解析路由
|
|
614
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
615
|
+
cfg: config,
|
|
616
|
+
channel: "wecom",
|
|
617
|
+
accountId: agent.accountId,
|
|
618
|
+
peer: { kind: isGroup ? "group" : "direct", id: peerId },
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ===== 动态 Agent 路由注入 =====
|
|
622
|
+
const useDynamicAgent = shouldUseDynamicAgent({
|
|
623
|
+
chatType: isGroup ? "group" : "dm",
|
|
624
|
+
senderId: fromUser,
|
|
625
|
+
config,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (shouldRejectWecomDefaultRoute({ cfg: config, matchedBy: route.matchedBy, useDynamicAgent })) {
|
|
629
|
+
const prompt =
|
|
630
|
+
`当前账号(${agent.accountId})未绑定 OpenClaw Agent,已拒绝回退到默认主智能体。` +
|
|
631
|
+
`请在 bindings 中添加:{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${agent.accountId}"}}`;
|
|
632
|
+
error?.(
|
|
633
|
+
`[wecom-agent] routing guard: blocked default fallback accountId=${agent.accountId} matchedBy=${route.matchedBy} from=${fromUser}`,
|
|
634
|
+
);
|
|
635
|
+
try {
|
|
636
|
+
await sendAgentApiText({ agent, ...replyTarget, text: prompt });
|
|
637
|
+
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
638
|
+
log?.(`[wecom-agent] routing guard prompt delivered to ${fromUser}`);
|
|
639
|
+
} catch (err: unknown) {
|
|
640
|
+
error?.(`[wecom-agent] routing guard prompt failed: ${String(err)}`);
|
|
641
|
+
auditSink?.({
|
|
642
|
+
transport: "agent-callback",
|
|
643
|
+
category: "fallback-delivery-failed",
|
|
644
|
+
summary: `routing guard prompt failed user=${fromUser}`,
|
|
645
|
+
raw: {
|
|
646
|
+
transport: "agent-callback",
|
|
647
|
+
envelopeType: "xml",
|
|
648
|
+
body: msg,
|
|
649
|
+
},
|
|
650
|
+
error: err instanceof Error ? err.message : String(err),
|
|
651
|
+
});
|
|
595
652
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (useDynamicAgent) {
|
|
657
|
+
const targetAgentId = generateAgentId(isGroup ? "group" : "dm", peerId, agent.accountId);
|
|
658
|
+
route.agentId = targetAgentId;
|
|
659
|
+
route.sessionKey = `agent:${targetAgentId}:wecom:${agent.accountId}:${isGroup ? "group" : "dm"}:${peerId}`;
|
|
660
|
+
// 异步添加到 agents.list(不阻塞)
|
|
661
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
662
|
+
log?.(`[wecom-agent] dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
663
|
+
}
|
|
664
|
+
// ===== 动态 Agent 路由注入结束 =====
|
|
665
|
+
|
|
666
|
+
registerWecomSourceSnapshot({
|
|
667
|
+
accountId: agent.accountId,
|
|
668
|
+
source: "agent-callback",
|
|
669
|
+
messageId: extractMsgId(msg) ?? undefined,
|
|
670
|
+
sessionKey: route.sessionKey,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// 构建上下文
|
|
674
|
+
const fromLabel = isGroup ? `group:${peerId}` : `user:${fromUser}`;
|
|
675
|
+
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
676
|
+
agentId: route.agentId,
|
|
677
|
+
});
|
|
678
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
|
|
679
|
+
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
680
|
+
storePath,
|
|
681
|
+
sessionKey: route.sessionKey,
|
|
682
|
+
});
|
|
683
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
684
|
+
channel: "WeCom",
|
|
685
|
+
from: fromLabel,
|
|
686
|
+
previousTimestamp,
|
|
687
|
+
envelope: envelopeOptions,
|
|
688
|
+
body: finalContent,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const authz = await resolveWecomCommandAuthorization({
|
|
692
|
+
core,
|
|
693
|
+
cfg: config,
|
|
694
|
+
// Agent 门禁应读取 channels.wecom.agent.dm(即 agent.config.dm),而不是 channels.wecom.dm(不存在)
|
|
695
|
+
accountConfig: agent.config,
|
|
696
|
+
rawBody: finalContent,
|
|
697
|
+
senderUserId: fromUser,
|
|
698
|
+
});
|
|
699
|
+
log?.(
|
|
700
|
+
`[wecom-agent] authz: dmPolicy=${authz.dmPolicy} shouldCompute=${authz.shouldComputeAuth} sender=${fromUser.toLowerCase()} senderAllowed=${authz.senderAllowed} authorizerConfigured=${authz.authorizerConfigured} commandAuthorized=${String(authz.commandAuthorized)}`,
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
// 命令门禁:未授权时必须明确回复(Agent 侧用私信提示)
|
|
704
|
+
if (authz.shouldComputeAuth && authz.commandAuthorized !== true) {
|
|
705
|
+
const prompt = buildWecomUnauthorizedCommandPrompt({
|
|
706
|
+
senderUserId: fromUser,
|
|
707
|
+
dmPolicy: authz.dmPolicy,
|
|
708
|
+
scope: "agent",
|
|
623
709
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
},
|
|
644
|
-
error: err instanceof Error ? err.message : String(err),
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
return;
|
|
710
|
+
try {
|
|
711
|
+
await sendAgentApiText({ agent, ...replyTarget, text: prompt });
|
|
712
|
+
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
713
|
+
log?.(
|
|
714
|
+
`[wecom-agent] unauthorized command: replied to ${isGroup ? `chat:${peerId}` : fromUser}`,
|
|
715
|
+
);
|
|
716
|
+
} catch (err: unknown) {
|
|
717
|
+
error?.(`[wecom-agent] unauthorized command reply failed: ${String(err)}`);
|
|
718
|
+
auditSink?.({
|
|
719
|
+
transport: "agent-callback",
|
|
720
|
+
category: "fallback-delivery-failed",
|
|
721
|
+
summary: `unauthorized prompt failed user=${fromUser}`,
|
|
722
|
+
raw: {
|
|
723
|
+
transport: "agent-callback",
|
|
724
|
+
envelopeType: "xml",
|
|
725
|
+
body: msg,
|
|
726
|
+
},
|
|
727
|
+
error: err instanceof Error ? err.message : String(err),
|
|
728
|
+
});
|
|
648
729
|
}
|
|
649
|
-
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
650
733
|
Body: body,
|
|
651
734
|
RawBody: finalContent,
|
|
652
735
|
CommandBody: finalContent,
|
|
@@ -670,136 +753,142 @@ const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
|
670
753
|
MediaPath: mediaPath,
|
|
671
754
|
MediaType: mediaType,
|
|
672
755
|
MediaUrl: mediaPath,
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// 记录会话
|
|
759
|
+
await core.channel.session.recordInboundSession({
|
|
760
|
+
storePath,
|
|
761
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
762
|
+
ctx: ctxPayload,
|
|
763
|
+
onRecordError: (err: unknown) => {
|
|
764
|
+
error?.(`[wecom-agent] session record failed: ${String(err)}`);
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// 5秒无响应自动回复进度提示
|
|
769
|
+
let hasResponseSent = false;
|
|
770
|
+
const processingTimer = setTimeout(async () => {
|
|
771
|
+
if (hasResponseSent) return;
|
|
772
|
+
try {
|
|
773
|
+
await sendAgentApiText({
|
|
774
|
+
agent,
|
|
775
|
+
...replyTarget,
|
|
776
|
+
text: "正在处理中,请稍候...",
|
|
777
|
+
});
|
|
778
|
+
log?.(
|
|
779
|
+
`[wecom-agent] sent processing notification to ${isGroup ? `chat:${peerId}` : fromUser}`,
|
|
780
|
+
);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
error?.(`[wecom-agent] failed to send processing notification: ${String(err)}`);
|
|
783
|
+
}
|
|
784
|
+
}, 5000);
|
|
785
|
+
|
|
786
|
+
// 发送队列锁:确保所有 deliver 调用(以及内部的分片发送)严格串行执行
|
|
787
|
+
let messageSendQueue = Promise.resolve();
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
// 调度回复
|
|
791
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
792
|
+
ctx: ctxPayload,
|
|
793
|
+
cfg: config,
|
|
794
|
+
replyOptions: {
|
|
795
|
+
disableBlockStreaming: false,
|
|
796
|
+
},
|
|
797
|
+
dispatcherOptions: {
|
|
798
|
+
deliver: async (payload: { text?: string }, info: { kind: string }) => {
|
|
799
|
+
const text = payload.text ?? "";
|
|
800
|
+
// 忽略空文本消息
|
|
801
|
+
if (!text || !text.trim()) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 标记已有回复,清除/失效定时器
|
|
806
|
+
hasResponseSent = true;
|
|
807
|
+
clearTimeout(processingTimer);
|
|
808
|
+
|
|
809
|
+
// 将本次发送任务加入队列
|
|
810
|
+
// 即使 deliver 被并发调用,队列中的任务也会按入队顺序串行执行
|
|
811
|
+
const currentTask = async () => {
|
|
812
|
+
const MAX_CHUNK_SIZE = 600;
|
|
813
|
+
// 确保分片顺序发送
|
|
814
|
+
for (let i = 0; i < text.length; i += MAX_CHUNK_SIZE) {
|
|
815
|
+
const chunk = text.slice(i, i + MAX_CHUNK_SIZE);
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
await sendAgentApiText({ agent, ...replyTarget, text: chunk });
|
|
819
|
+
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
820
|
+
log?.(
|
|
821
|
+
`[wecom-agent] reply chunk delivered (${info.kind}) to ${isGroup ? `chat:${peerId}` : fromUser}, len=${chunk.length}`,
|
|
822
|
+
);
|
|
701
823
|
|
|
702
|
-
|
|
703
|
-
|
|
824
|
+
// 强制延时:确保企业微信有足够时间处理顺序(优化:200ms → 50ms)
|
|
825
|
+
if (i + MAX_CHUNK_SIZE < text.length) {
|
|
826
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
827
|
+
}
|
|
828
|
+
} catch (err: unknown) {
|
|
829
|
+
const message =
|
|
830
|
+
err instanceof Error
|
|
831
|
+
? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}`
|
|
832
|
+
: String(err);
|
|
833
|
+
error?.(`[wecom-agent] reply failed: ${message}`);
|
|
834
|
+
auditSink?.({
|
|
835
|
+
transport: "agent-callback",
|
|
836
|
+
category: "fallback-delivery-failed",
|
|
837
|
+
summary: `agent callback reply failed user=${fromUser} kind=${info.kind}`,
|
|
838
|
+
raw: {
|
|
839
|
+
transport: "agent-callback",
|
|
840
|
+
envelopeType: "xml",
|
|
841
|
+
body: msg,
|
|
842
|
+
},
|
|
843
|
+
error: message,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
704
847
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
ctx: ctxPayload,
|
|
709
|
-
cfg: config,
|
|
710
|
-
replyOptions: {
|
|
711
|
-
disableBlockStreaming: false,
|
|
712
|
-
},
|
|
713
|
-
dispatcherOptions: {
|
|
714
|
-
deliver: async (payload: { text?: string }, info: { kind: string }) => {
|
|
715
|
-
const text = payload.text ?? "";
|
|
716
|
-
// 忽略空文本消息
|
|
717
|
-
if (!text || !text.trim()) {
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// 标记已有回复,清除/失效定时器
|
|
722
|
-
hasResponseSent = true;
|
|
723
|
-
clearTimeout(processingTimer);
|
|
724
|
-
|
|
725
|
-
// 将本次发送任务加入队列
|
|
726
|
-
// 即使 deliver 被并发调用,队列中的任务也会按入队顺序串行执行
|
|
727
|
-
const currentTask = async () => {
|
|
728
|
-
const MAX_CHUNK_SIZE = 600;
|
|
729
|
-
// 确保分片顺序发送
|
|
730
|
-
for (let i = 0; i < text.length; i += MAX_CHUNK_SIZE) {
|
|
731
|
-
const chunk = text.slice(i, i + MAX_CHUNK_SIZE);
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
await sendAgentApiText({ agent, toUser: fromUser, chatId: undefined, text: chunk });
|
|
735
|
-
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
736
|
-
log?.(`[wecom-agent] reply chunk delivered (${info.kind}) to ${fromUser}, len=${chunk.length}`);
|
|
737
|
-
|
|
738
|
-
// 强制延时:确保企业微信有足够时间处理顺序(优化:200ms → 50ms)
|
|
739
|
-
if (i + MAX_CHUNK_SIZE < text.length) {
|
|
740
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
741
|
-
}
|
|
742
|
-
} catch (err: unknown) {
|
|
743
|
-
const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
|
|
744
|
-
error?.(`[wecom-agent] reply failed: ${message}`);
|
|
745
|
-
auditSink?.({
|
|
746
|
-
transport: "agent-callback",
|
|
747
|
-
category: "fallback-delivery-failed",
|
|
748
|
-
summary: `agent callback reply failed user=${fromUser} kind=${info.kind}`,
|
|
749
|
-
raw: {
|
|
750
|
-
transport: "agent-callback",
|
|
751
|
-
envelopeType: "xml",
|
|
752
|
-
body: msg,
|
|
753
|
-
},
|
|
754
|
-
error: message,
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// 不同 Block 之间也增加一点间隔(优化:200ms → 50ms)
|
|
760
|
-
if (info.kind !== "final") {
|
|
761
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
// 更新队列链
|
|
766
|
-
// 使用 then 链接,并捕获前一个任务可能的错误,确保当前任务总能执行
|
|
767
|
-
messageSendQueue = messageSendQueue
|
|
768
|
-
.then(() => currentTask())
|
|
769
|
-
.catch((err) => {
|
|
770
|
-
error?.(`[wecom-agent] previous send task failed: ${String(err)}`);
|
|
771
|
-
// 前一个失败不应阻止当前任务,继续尝试执行当前任务
|
|
772
|
-
return currentTask();
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
// 等待当前任务完成(保持背压,虽然对于 http callback 模式这可能只是延迟了整体结束时间)
|
|
776
|
-
await messageSendQueue;
|
|
777
|
-
},
|
|
778
|
-
onError: (err: unknown, info: { kind: string }) => {
|
|
779
|
-
clearTimeout(processingTimer);
|
|
780
|
-
error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
|
|
781
|
-
},
|
|
848
|
+
// 不同 Block 之间也增加一点间隔(优化:200ms → 50ms)
|
|
849
|
+
if (info.kind !== "final") {
|
|
850
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
782
851
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// 更新队列链
|
|
855
|
+
// 使用 then 链接,并捕获前一个任务可能的错误,确保当前任务总能执行
|
|
856
|
+
messageSendQueue = messageSendQueue
|
|
857
|
+
.then(() => currentTask())
|
|
858
|
+
.catch((err) => {
|
|
859
|
+
error?.(`[wecom-agent] previous send task failed: ${String(err)}`);
|
|
860
|
+
// 前一个失败不应阻止当前任务,继续尝试执行当前任务
|
|
861
|
+
return currentTask();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// 等待当前任务完成(保持背压,虽然对于 http callback 模式这可能只是延迟了整体结束时间)
|
|
865
|
+
await messageSendQueue;
|
|
866
|
+
},
|
|
867
|
+
onError: (err: unknown, info: { kind: string }) => {
|
|
868
|
+
clearTimeout(processingTimer);
|
|
869
|
+
error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
} finally {
|
|
874
|
+
clearTimeout(processingTimer);
|
|
875
|
+
// 确保所有排队的消息都发完了才退出(虽然对于 HTTP 响应来说,res.end 早就调用了)
|
|
876
|
+
await messageSendQueue;
|
|
877
|
+
}
|
|
789
878
|
}
|
|
790
879
|
|
|
791
880
|
/**
|
|
792
881
|
* **handleAgentWebhook (Agent Webhook 入口)**
|
|
793
|
-
*
|
|
882
|
+
*
|
|
794
883
|
* 统一处理 Agent 模式的 POST 消息回调请求。
|
|
795
884
|
* URL 验证与验签/解密由 monitor 层统一处理后再调用本函数。
|
|
796
885
|
*/
|
|
797
886
|
export async function handleAgentWebhook(params: AgentWebhookParams): Promise<boolean> {
|
|
798
|
-
|
|
887
|
+
const { req } = params;
|
|
799
888
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
889
|
+
if (req.method === "POST") {
|
|
890
|
+
return handleMessageCallback(params);
|
|
891
|
+
}
|
|
803
892
|
|
|
804
|
-
|
|
893
|
+
return false;
|
|
805
894
|
}
|