@yanhaidao/wecom 2.3.12 → 2.3.14
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 +160 -0
- package/changelog/v2.3.12.md +2 -0
- package/changelog/v2.3.13.md +19 -0
- package/changelog/v2.3.14.md +48 -0
- package/index.ts +4 -0
- package/package.json +4 -3
- package/src/agent/handler.event-filter.test.ts +15 -5
- package/src/agent/handler.ts +217 -74
- package/src/app/account-runtime.ts +1 -1
- package/src/app/index.ts +4 -0
- package/src/capability/agent/delivery-service.ts +16 -8
- package/src/capability/bot/fallback-delivery.ts +1 -1
- package/src/capability/bot/stream-orchestrator.ts +10 -10
- package/src/capability/doc/client.ts +910 -0
- package/src/capability/doc/schema.ts +1404 -0
- package/src/capability/doc/tool.ts +1165 -0
- package/src/capability/doc/types.ts +408 -0
- package/src/channel.ts +1 -1
- package/src/dynamic-agent.ts +2 -1
- package/src/outbound.ts +5 -5
- package/src/runtime/session-manager.ts +4 -4
- package/src/shared/media-service.ts +20 -0
- package/src/target.ts +11 -3
- package/src/transport/bot-webhook/active-reply.ts +4 -1
- package/src/transport/bot-ws/inbound.test.ts +50 -0
- package/src/transport/bot-ws/inbound.ts +12 -22
- package/src/transport/bot-ws/reply.test.ts +152 -37
- package/src/transport/bot-ws/reply.ts +135 -8
- package/src/transport/bot-ws/sdk-adapter.ts +1 -0
- package/src/types/runtime.ts +3 -0
package/src/agent/handler.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
9
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
10
|
-
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
10
|
+
import type { ResolvedAgentAccount, UnifiedInboundEvent, WecomInboundKind } from "../types/index.js";
|
|
11
11
|
import {
|
|
12
12
|
extractMsgType,
|
|
13
13
|
extractFromUser,
|
|
@@ -22,6 +22,7 @@ import { downloadAgentApiMedia, sendAgentApiText } from "../transport/agent-api/
|
|
|
22
22
|
import { getWecomRuntime } from "../runtime.js";
|
|
23
23
|
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
24
24
|
import type { TransportSessionPatch } from "../types/index.js";
|
|
25
|
+
import type { WecomAccountRuntime } from "../app/account-runtime.js";
|
|
25
26
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
26
27
|
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../config/index.js";
|
|
27
28
|
import { buildAgentSessionTarget, generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
@@ -35,6 +36,23 @@ const ERROR_HELP = "\n\n遇到问题?联系作者: YanHaidao (微信: YanHaida
|
|
|
35
36
|
const RECENT_MSGID_TTL_MS = 10 * 60 * 1000;
|
|
36
37
|
const recentAgentMsgIds = new Map<string, number>();
|
|
37
38
|
|
|
39
|
+
// Event deduplication (e.g. for ENTER_AGENT/subscribe welcome messages)
|
|
40
|
+
// We only want to send a welcome message once every 5 minutes per user
|
|
41
|
+
const RECENT_EVENT_TTL_MS =3 * 60 * 1000;
|
|
42
|
+
const recentAgentEvents = new Map<string, number>();
|
|
43
|
+
|
|
44
|
+
function rememberAgentEvent(key: string): boolean {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const existing = recentAgentEvents.get(key);
|
|
47
|
+
if (existing && now - existing < RECENT_EVENT_TTL_MS) return false;
|
|
48
|
+
recentAgentEvents.set(key, now);
|
|
49
|
+
// Prune expired
|
|
50
|
+
for (const [k, ts] of recentAgentEvents) {
|
|
51
|
+
if (now - ts >= RECENT_EVENT_TTL_MS) recentAgentEvents.delete(k);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
function rememberAgentMsgId(msgId: string): boolean {
|
|
39
57
|
const now = Date.now();
|
|
40
58
|
const existing = recentAgentMsgIds.get(msgId);
|
|
@@ -150,6 +168,28 @@ export function shouldProcessAgentInboundMessage(params: {
|
|
|
150
168
|
const eventType = String(params.eventType ?? "").trim().toLowerCase();
|
|
151
169
|
|
|
152
170
|
if (msgType === "event") {
|
|
171
|
+
const allowedEvents = [
|
|
172
|
+
"subscribe",
|
|
173
|
+
"enter_agent",
|
|
174
|
+
"batch_job_result",
|
|
175
|
+
// WeCom Doc events
|
|
176
|
+
"doc_create",
|
|
177
|
+
"doc_delete",
|
|
178
|
+
"doc_content_change",
|
|
179
|
+
"doc_member_change",
|
|
180
|
+
// WeCom Form events
|
|
181
|
+
"wedoc_collect_submit",
|
|
182
|
+
// SmartSheet events
|
|
183
|
+
"smartsheet_record_change",
|
|
184
|
+
"smartsheet_field_change",
|
|
185
|
+
"smartsheet_view_change"
|
|
186
|
+
];
|
|
187
|
+
if (allowedEvents.includes(eventType) || eventType.startsWith("doc_") || eventType.startsWith("wedoc_") || eventType.startsWith("smartsheet_")) {
|
|
188
|
+
return {
|
|
189
|
+
shouldProcess: true,
|
|
190
|
+
reason: `allowed_event:${eventType}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
153
193
|
return {
|
|
154
194
|
shouldProcess: false,
|
|
155
195
|
reason: `event:${eventType || "unknown"}`,
|
|
@@ -241,6 +281,7 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
241
281
|
const chatId = extractChatId(msg);
|
|
242
282
|
const msgId = extractMsgId(msg);
|
|
243
283
|
const eventType = String((msg as Record<string, unknown>).Event ?? "").trim().toLowerCase();
|
|
284
|
+
|
|
244
285
|
if (msgId) {
|
|
245
286
|
const ok = rememberAgentMsgId(msgId);
|
|
246
287
|
if (!ok) {
|
|
@@ -262,6 +303,15 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
262
303
|
return true;
|
|
263
304
|
}
|
|
264
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
|
+
}
|
|
265
315
|
const content = String(extractContent(msg) ?? "");
|
|
266
316
|
|
|
267
317
|
const preview = content.length > 100 ? `${content.slice(0, 100)}…` : content;
|
|
@@ -341,11 +391,41 @@ async function processAgentMessage(params: {
|
|
|
341
391
|
|
|
342
392
|
const isGroup = Boolean(chatId);
|
|
343
393
|
const peerId = isGroup ? chatId! : fromUser;
|
|
394
|
+
const eventType = String(msg.Event ?? "").trim().toLowerCase();
|
|
395
|
+
|
|
396
|
+
const resolveInboundKind = (): WecomInboundKind => {
|
|
397
|
+
if (msgType === "event") {
|
|
398
|
+
if (eventType === "subscribe" || eventType === "enter_agent") return "welcome";
|
|
399
|
+
return "event";
|
|
400
|
+
}
|
|
401
|
+
if (msgType === "image") return "image";
|
|
402
|
+
if (msgType === "voice") return "voice";
|
|
403
|
+
if (msgType === "video") return "video";
|
|
404
|
+
if (msgType === "file") return "file";
|
|
405
|
+
if (msgType === "location") return "location";
|
|
406
|
+
if (msgType === "link") return "link";
|
|
407
|
+
return "text";
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const inboundKind = resolveInboundKind();
|
|
411
|
+
const resolveEventText = (): string => {
|
|
412
|
+
if (inboundKind === "welcome" && agent.config.welcomeText) {
|
|
413
|
+
return agent.config.welcomeText;
|
|
414
|
+
}
|
|
415
|
+
if (msgType === "event") {
|
|
416
|
+
return `[event:${eventType || "unknown"}]`;
|
|
417
|
+
}
|
|
418
|
+
return content;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// BUG FIX: 真正调用 resolveEventText() 获取欢迎语或事件描述
|
|
422
|
+
const resolvedContent = resolveEventText();
|
|
423
|
+
let finalContent = resolvedContent;
|
|
424
|
+
|
|
344
425
|
const mediaMaxBytes = resolveWecomMediaMaxBytes(config);
|
|
345
426
|
|
|
346
427
|
// 处理媒体文件
|
|
347
|
-
const attachments:
|
|
348
|
-
let finalContent = content;
|
|
428
|
+
const attachments: NonNullable<UnifiedInboundEvent["attachments"]> = [];
|
|
349
429
|
let mediaPath: string | undefined;
|
|
350
430
|
let mediaType: string | undefined;
|
|
351
431
|
|
|
@@ -369,9 +449,9 @@ async function processAgentMessage(params: {
|
|
|
369
449
|
const originalExt = path.extname(originalFileName).toLowerCase();
|
|
370
450
|
const normalizedContentType =
|
|
371
451
|
looksText && originalExt === ".md" ? "text/markdown" :
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
452
|
+
looksText && (!contentType || contentType === "application/octet-stream")
|
|
453
|
+
? "text/plain; charset=utf-8"
|
|
454
|
+
: contentType;
|
|
375
455
|
|
|
376
456
|
const ext = extMap[normalizedContentType] || (looksText ? "txt" : "bin");
|
|
377
457
|
const filename = `${mediaId}.${ext}`;
|
|
@@ -400,8 +480,8 @@ async function processAgentMessage(params: {
|
|
|
400
480
|
// 构建附件
|
|
401
481
|
attachments.push({
|
|
402
482
|
name: originalFileName,
|
|
403
|
-
|
|
404
|
-
|
|
483
|
+
contentType: normalizedContentType,
|
|
484
|
+
remoteUrl: pathToFileURL(saved.path).href, // 使用跨平台安全的文件 URL
|
|
405
485
|
});
|
|
406
486
|
|
|
407
487
|
// 更新文本提示
|
|
@@ -510,7 +590,7 @@ async function processAgentMessage(params: {
|
|
|
510
590
|
route.agentId = targetAgentId;
|
|
511
591
|
route.sessionKey = `agent:${targetAgentId}:wecom:${agent.accountId}:${isGroup ? "group" : "dm"}:${peerId}`;
|
|
512
592
|
// 异步添加到 agents.list(不阻塞)
|
|
513
|
-
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
593
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => { });
|
|
514
594
|
log?.(`[wecom-agent] dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
515
595
|
}
|
|
516
596
|
// ===== 动态 Agent 路由注入结束 =====
|
|
@@ -566,32 +646,31 @@ async function processAgentMessage(params: {
|
|
|
566
646
|
}
|
|
567
647
|
return;
|
|
568
648
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
});
|
|
649
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
650
|
+
Body: body,
|
|
651
|
+
RawBody: finalContent,
|
|
652
|
+
CommandBody: finalContent,
|
|
653
|
+
Attachments: attachments.length > 0 ? attachments : undefined,
|
|
654
|
+
From: isGroup ? `wecom:group:${peerId}` : `wecom:user:${fromUser}`,
|
|
655
|
+
To: `wecom:user:${peerId}`,
|
|
656
|
+
SessionKey: route.sessionKey,
|
|
657
|
+
AccountId: route.accountId,
|
|
658
|
+
ChatType: isGroup ? "group" : "direct",
|
|
659
|
+
ConversationLabel: fromLabel,
|
|
660
|
+
SenderName: fromUser,
|
|
661
|
+
SenderId: fromUser,
|
|
662
|
+
Provider: "wecom",
|
|
663
|
+
Surface: "webchat",
|
|
664
|
+
OriginatingChannel: "wecom",
|
|
665
|
+
// 标记为 Agent 会话的回复路由目标,避免与 Bot 会话混淆:
|
|
666
|
+
// - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
|
|
667
|
+
// - 群聊场景也统一路由为私信触发者(与 deliver 策略一致)
|
|
668
|
+
OriginatingTo: buildAgentSessionTarget(fromUser, agent.accountId),
|
|
669
|
+
CommandAuthorized: authz.commandAuthorized ?? true,
|
|
670
|
+
MediaPath: mediaPath,
|
|
671
|
+
MediaType: mediaType,
|
|
672
|
+
MediaUrl: mediaPath,
|
|
673
|
+
});
|
|
595
674
|
|
|
596
675
|
// 记录会话
|
|
597
676
|
await core.channel.session.recordInboundSession({
|
|
@@ -603,46 +682,110 @@ async function processAgentMessage(params: {
|
|
|
603
682
|
},
|
|
604
683
|
});
|
|
605
684
|
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
// 统一策略:Agent 模式在群聊场景默认只私信触发者(避免 wr/wc chatId 86008)
|
|
623
|
-
await sendAgentApiText({ agent, toUser: fromUser, chatId: undefined, text });
|
|
624
|
-
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
625
|
-
log?.(`[wecom-agent] reply delivered (${info.kind}) to ${fromUser}`);
|
|
626
|
-
} catch (err: unknown) {
|
|
627
|
-
const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
|
|
628
|
-
error?.(`[wecom-agent] reply failed: ${message}`);
|
|
629
|
-
auditSink?.({
|
|
630
|
-
transport: "agent-callback",
|
|
631
|
-
category: "fallback-delivery-failed",
|
|
632
|
-
summary: `agent callback reply failed user=${fromUser} kind=${info.kind}`,
|
|
633
|
-
raw: {
|
|
634
|
-
transport: "agent-callback",
|
|
635
|
-
envelopeType: "xml",
|
|
636
|
-
body: msg,
|
|
637
|
-
},
|
|
638
|
-
error: message,
|
|
639
|
-
});
|
|
640
|
-
} },
|
|
641
|
-
onError: (err: unknown, info: { kind: string }) => {
|
|
642
|
-
error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
|
|
643
|
-
},
|
|
685
|
+
// 5秒无响应自动回复进度提示
|
|
686
|
+
let hasResponseSent = false;
|
|
687
|
+
const processingTimer = setTimeout(async () => {
|
|
688
|
+
if (hasResponseSent) return;
|
|
689
|
+
try {
|
|
690
|
+
await sendAgentApiText({
|
|
691
|
+
agent,
|
|
692
|
+
toUser: fromUser,
|
|
693
|
+
chatId: undefined,
|
|
694
|
+
text: "正在处理中,请稍候..."
|
|
695
|
+
});
|
|
696
|
+
log?.(`[wecom-agent] sent processing notification to ${fromUser}`);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
error?.(`[wecom-agent] failed to send processing notification: ${String(err)}`);
|
|
644
699
|
}
|
|
645
|
-
});
|
|
700
|
+
}, 5000);
|
|
701
|
+
|
|
702
|
+
// 发送队列锁:确保所有 deliver 调用(以及内部的分片发送)严格串行执行
|
|
703
|
+
let messageSendQueue = Promise.resolve();
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
// 调度回复
|
|
707
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
708
|
+
ctx: ctxPayload,
|
|
709
|
+
cfg: config,
|
|
710
|
+
replyOptions: {
|
|
711
|
+
disableBlockStreaming: true,
|
|
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
|
+
// 强制延时:确保企业微信有足够时间处理顺序
|
|
739
|
+
if (i + MAX_CHUNK_SIZE < text.length) {
|
|
740
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
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 之间也增加一点间隔
|
|
760
|
+
if (info.kind !== "final") {
|
|
761
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
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
|
+
},
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
} finally {
|
|
785
|
+
clearTimeout(processingTimer);
|
|
786
|
+
// 确保所有排队的消息都发完了才退出(虽然对于 HTTP 响应来说,res.end 早就调用了)
|
|
787
|
+
await messageSendQueue;
|
|
788
|
+
}
|
|
646
789
|
}
|
|
647
790
|
|
|
648
791
|
/**
|
|
@@ -30,7 +30,7 @@ export class WecomAccountRuntime {
|
|
|
30
30
|
readonly core: PluginRuntime,
|
|
31
31
|
readonly cfg: OpenClawConfig,
|
|
32
32
|
readonly resolved: ResolvedRuntimeAccount,
|
|
33
|
-
|
|
33
|
+
readonly log: {
|
|
34
34
|
info?: (message: string) => void;
|
|
35
35
|
warn?: (message: string) => void;
|
|
36
36
|
error?: (message: string) => void;
|
package/src/app/index.ts
CHANGED
|
@@ -27,6 +27,10 @@ export function registerAccountRuntime(accountRuntime: WecomAccountRuntime): voi
|
|
|
27
27
|
console.log(`[wecom-runtime] register account=${accountRuntime.account.accountId}`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function getAccountRuntime(accountId: string): WecomAccountRuntime | undefined {
|
|
31
|
+
return runtimes.get(accountId);
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
export function getAccountRuntimeSnapshot(accountId: string) {
|
|
31
35
|
return runtimes.get(accountId)?.buildRuntimeStatus();
|
|
32
36
|
}
|
|
@@ -2,9 +2,10 @@ import type { ResolvedAgentAccount } from "../../types/index.js";
|
|
|
2
2
|
import { resolveScopedWecomTarget } from "../../target.js";
|
|
3
3
|
import { deliverAgentApiMedia, deliverAgentApiText } from "../../transport/agent-api/delivery.js";
|
|
4
4
|
import { canUseAgentApiDelivery } from "./fallback-policy.js";
|
|
5
|
+
import { getWecomRuntime } from "../../runtime.js";
|
|
5
6
|
|
|
6
7
|
export class WecomAgentDeliveryService {
|
|
7
|
-
constructor(private readonly agent: ResolvedAgentAccount) {}
|
|
8
|
+
constructor(private readonly agent: ResolvedAgentAccount) { }
|
|
8
9
|
|
|
9
10
|
assertAvailable(): void {
|
|
10
11
|
if (!canUseAgentApiDelivery(this.agent)) {
|
|
@@ -35,8 +36,8 @@ export class WecomAgentDeliveryService {
|
|
|
35
36
|
);
|
|
36
37
|
throw new Error(
|
|
37
38
|
`企业微信(WeCom)Agent 主动发送不支持向群 chatId 发送(chatId=${target.chatid})。` +
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
`该路径在实际环境中经常失败(例如 86008:无权限访问该会话/会话由其他应用创建)。` +
|
|
40
|
+
`请改为发送给用户(userid / user:xxx),或由 Bot 模式在群内交付。`,
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
return target;
|
|
@@ -48,11 +49,18 @@ export class WecomAgentDeliveryService {
|
|
|
48
49
|
console.log(
|
|
49
50
|
`[wecom-agent-delivery] sendText account=${this.agent.accountId} to=${String(params.to ?? "")} len=${params.text.length}`,
|
|
50
51
|
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
|
|
53
|
+
const runtime = getWecomRuntime();
|
|
54
|
+
const chunks = runtime.channel.text.chunkText(params.text, 2048);
|
|
55
|
+
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
if (!chunk.trim()) continue;
|
|
58
|
+
await deliverAgentApiText({
|
|
59
|
+
agent: this.agent,
|
|
60
|
+
target,
|
|
61
|
+
text: chunk,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
async sendMedia(params: {
|
|
@@ -96,7 +96,7 @@ export async function sendAgentDmText(params: {
|
|
|
96
96
|
text: string;
|
|
97
97
|
core: PluginRuntime;
|
|
98
98
|
}): Promise<void> {
|
|
99
|
-
const chunks = params.core.channel.text.chunkText(params.text,
|
|
99
|
+
const chunks = params.core.channel.text.chunkText(params.text, 2048);
|
|
100
100
|
for (const chunk of chunks) {
|
|
101
101
|
const trimmed = chunk.trim();
|
|
102
102
|
if (!trimmed) continue;
|
|
@@ -201,7 +201,7 @@ export function createBotStreamOrchestrator(params: {
|
|
|
201
201
|
const targetAgentId = generateAgentId(chatType === "group" ? "group" : "dm", chatId, account.accountId);
|
|
202
202
|
route.agentId = targetAgentId;
|
|
203
203
|
route.sessionKey = `agent:${targetAgentId}:wecom:${account.accountId}:${chatType === "group" ? "group" : "dm"}:${chatId}`;
|
|
204
|
-
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
204
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => { });
|
|
205
205
|
logVerbose(target, `dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -254,12 +254,12 @@ export function createBotStreamOrchestrator(params: {
|
|
|
254
254
|
|
|
255
255
|
const attachments = mediaPath
|
|
256
256
|
? [
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
{
|
|
258
|
+
name: media?.filename || "file",
|
|
259
|
+
mimeType: mediaType,
|
|
260
|
+
url: pathToFileURL(mediaPath).href,
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
263
|
: undefined;
|
|
264
264
|
|
|
265
265
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
@@ -267,8 +267,8 @@ export function createBotStreamOrchestrator(params: {
|
|
|
267
267
|
RawBody: rawBody,
|
|
268
268
|
CommandBody: rawBody,
|
|
269
269
|
Attachments: attachments,
|
|
270
|
-
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${userId}`,
|
|
271
|
-
To: `wecom:${chatId}`,
|
|
270
|
+
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:user:${userId}`,
|
|
271
|
+
To: chatType === "group" ? `wecom:group:${chatId}` : `wecom:user:${chatId}`,
|
|
272
272
|
SessionKey: route.sessionKey,
|
|
273
273
|
AccountId: route.accountId,
|
|
274
274
|
ChatType: chatType,
|
|
@@ -280,7 +280,7 @@ export function createBotStreamOrchestrator(params: {
|
|
|
280
280
|
MessageSid: msg.msgid,
|
|
281
281
|
CommandAuthorized: commandAuthorized,
|
|
282
282
|
OriginatingChannel: "wecom",
|
|
283
|
-
OriginatingTo: `wecom:${chatId}`,
|
|
283
|
+
OriginatingTo: chatType === "group" ? `wecom:group:${chatId}` : `wecom:user:${chatId}`,
|
|
284
284
|
MediaPath: mediaPath,
|
|
285
285
|
MediaType: mediaType,
|
|
286
286
|
MediaUrl: mediaPath,
|