@yanhaidao/wecom 2.2.28 → 2.3.3
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 +33 -28
- package/assets/register.png +0 -0
- package/changelog/v2.3.2.md +28 -0
- package/package.json +1 -1
- package/src/agent/api-client.ts +76 -34
- package/src/agent/api-client.upload.test.ts +110 -0
- package/src/agent/handler.ts +4 -4
- package/src/channel.lifecycle.test.ts +24 -6
- package/src/channel.ts +11 -6
- package/src/config/network.ts +9 -5
- package/src/gateway-monitor.ts +51 -20
- package/src/http.ts +16 -2
- package/src/media.test.ts +28 -1
- package/src/media.ts +59 -1
- package/src/monitor.active.test.ts +9 -6
- package/src/monitor.integration.test.ts +4 -2
- package/src/monitor.ts +511 -82
- package/src/monitor.webhook.test.ts +104 -11
- package/src/onboarding.ts +219 -43
- package/src/outbound.ts +6 -0
- package/src/types/constants.ts +7 -3
package/src/monitor.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { WecomBotInboundMessage as WecomInboundMessage, WecomInboundQuote }
|
|
|
11
11
|
import { decryptWecomEncrypted, encryptWecomPlaintext, verifyWecomSignature, computeWecomMsgSignature } from "./crypto.js";
|
|
12
12
|
import { extractEncryptFromXml } from "./crypto/xml.js";
|
|
13
13
|
import { getWecomRuntime } from "./runtime.js";
|
|
14
|
-
import {
|
|
14
|
+
import { decryptWecomMediaWithMeta } from "./media.js";
|
|
15
15
|
import { WEBHOOK_PATHS, LIMITS as WECOM_LIMITS } from "./types/constants.js";
|
|
16
16
|
import { handleAgentWebhook } from "./agent/index.js";
|
|
17
17
|
import { resolveWecomAccount, resolveWecomEgressProxyUrl, resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "./config/index.js";
|
|
@@ -220,16 +220,24 @@ type RouteFailureReason =
|
|
|
220
220
|
| "wecom_identity_mismatch"
|
|
221
221
|
| "wecom_matrix_path_required";
|
|
222
222
|
|
|
223
|
-
function
|
|
224
|
-
return
|
|
223
|
+
function isNonMatrixWecomBasePath(path: string): boolean {
|
|
224
|
+
return (
|
|
225
|
+
path === WEBHOOK_PATHS.BOT ||
|
|
226
|
+
path === WEBHOOK_PATHS.BOT_ALT ||
|
|
227
|
+
path === WEBHOOK_PATHS.AGENT ||
|
|
228
|
+
path === WEBHOOK_PATHS.BOT_PLUGIN ||
|
|
229
|
+
path === WEBHOOK_PATHS.AGENT_PLUGIN
|
|
230
|
+
);
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
function hasMatrixExplicitRoutesRegistered(): boolean {
|
|
228
234
|
for (const key of webhookTargets.keys()) {
|
|
229
235
|
if (key.startsWith(`${WEBHOOK_PATHS.BOT_ALT}/`)) return true;
|
|
236
|
+
if (key.startsWith(`${WEBHOOK_PATHS.BOT_PLUGIN}/`)) return true;
|
|
230
237
|
}
|
|
231
238
|
for (const key of agentTargets.keys()) {
|
|
232
239
|
if (key.startsWith(`${WEBHOOK_PATHS.AGENT}/`)) return true;
|
|
240
|
+
if (key.startsWith(`${WEBHOOK_PATHS.AGENT_PLUGIN}/`)) return true;
|
|
233
241
|
}
|
|
234
242
|
return false;
|
|
235
243
|
}
|
|
@@ -437,6 +445,27 @@ async function sendBotFallbackPromptNow(params: { streamId: string; text: string
|
|
|
437
445
|
});
|
|
438
446
|
}
|
|
439
447
|
|
|
448
|
+
async function pushFinalStreamReplyNow(streamId: string): Promise<void> {
|
|
449
|
+
const state = streamStore.getStream(streamId);
|
|
450
|
+
const responseUrl = getActiveReplyUrl(streamId);
|
|
451
|
+
if (!state || !responseUrl) return;
|
|
452
|
+
const finalReply = buildStreamReplyFromState(state) as unknown as Record<string, unknown>;
|
|
453
|
+
await useActiveReplyOnce(streamId, async ({ responseUrl, proxyUrl }) => {
|
|
454
|
+
const res = await wecomFetch(
|
|
455
|
+
responseUrl,
|
|
456
|
+
{
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: { "Content-Type": "application/json" },
|
|
459
|
+
body: JSON.stringify(finalReply),
|
|
460
|
+
},
|
|
461
|
+
{ proxyUrl, timeoutMs: LIMITS.REQUEST_TIMEOUT_MS },
|
|
462
|
+
);
|
|
463
|
+
if (!res.ok) {
|
|
464
|
+
throw new Error(`final stream push failed: ${res.status}`);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
440
469
|
async function sendAgentDmText(params: {
|
|
441
470
|
agent: ResolvedAgentAccount;
|
|
442
471
|
userId: string;
|
|
@@ -500,10 +529,10 @@ function extractLocalImagePathsFromText(params: {
|
|
|
500
529
|
const mustAlsoAppearIn = params.mustAlsoAppearIn;
|
|
501
530
|
if (!text.trim()) return [];
|
|
502
531
|
|
|
503
|
-
// Conservative: only accept common
|
|
532
|
+
// Conservative: only accept common absolute paths for macOS/Linux hosts.
|
|
504
533
|
// Also require that the exact path appeared in the user's original message to prevent exfil.
|
|
505
534
|
const exts = "(png|jpg|jpeg|gif|webp|bmp)";
|
|
506
|
-
const re = new RegExp(String.raw`(\/(?:Users|tmp)\/[^\s"'<>]+?\.${exts})`, "gi");
|
|
535
|
+
const re = new RegExp(String.raw`(\/(?:Users|tmp|root|home)\/[^\s"'<>]+?\.${exts})`, "gi");
|
|
507
536
|
const found = new Set<string>();
|
|
508
537
|
let m: RegExpExecArray | null;
|
|
509
538
|
while ((m = re.exec(text))) {
|
|
@@ -518,9 +547,9 @@ function extractLocalImagePathsFromText(params: {
|
|
|
518
547
|
function extractLocalFilePathsFromText(text: string): string[] {
|
|
519
548
|
if (!text.trim()) return [];
|
|
520
549
|
|
|
521
|
-
// Conservative: only accept common
|
|
550
|
+
// Conservative: only accept common absolute paths for macOS/Linux hosts.
|
|
522
551
|
// This is primarily for “send local file” style requests (operator/debug usage).
|
|
523
|
-
const re = new RegExp(String.raw`(\/(?:Users|tmp)\/[^\s"'<>]+)`, "g");
|
|
552
|
+
const re = new RegExp(String.raw`(\/(?:Users|tmp|root|home)\/[^\s"'<>]+)`, "g");
|
|
524
553
|
const found = new Set<string>();
|
|
525
554
|
let m: RegExpExecArray | null;
|
|
526
555
|
while ((m = re.exec(text))) {
|
|
@@ -531,23 +560,287 @@ function extractLocalFilePathsFromText(text: string): string[] {
|
|
|
531
560
|
return Array.from(found);
|
|
532
561
|
}
|
|
533
562
|
|
|
563
|
+
const MIME_BY_EXT: Record<string, string> = {
|
|
564
|
+
png: "image/png",
|
|
565
|
+
jpg: "image/jpeg",
|
|
566
|
+
jpeg: "image/jpeg",
|
|
567
|
+
gif: "image/gif",
|
|
568
|
+
webp: "image/webp",
|
|
569
|
+
bmp: "image/bmp",
|
|
570
|
+
pdf: "application/pdf",
|
|
571
|
+
txt: "text/plain",
|
|
572
|
+
csv: "text/csv",
|
|
573
|
+
tsv: "text/tab-separated-values",
|
|
574
|
+
md: "text/markdown",
|
|
575
|
+
json: "application/json",
|
|
576
|
+
xml: "application/xml",
|
|
577
|
+
yaml: "application/yaml",
|
|
578
|
+
yml: "application/yaml",
|
|
579
|
+
zip: "application/zip",
|
|
580
|
+
rar: "application/vnd.rar",
|
|
581
|
+
"7z": "application/x-7z-compressed",
|
|
582
|
+
tar: "application/x-tar",
|
|
583
|
+
gz: "application/gzip",
|
|
584
|
+
tgz: "application/gzip",
|
|
585
|
+
doc: "application/msword",
|
|
586
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
587
|
+
xls: "application/vnd.ms-excel",
|
|
588
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
589
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
590
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
591
|
+
rtf: "application/rtf",
|
|
592
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
593
|
+
mp3: "audio/mpeg",
|
|
594
|
+
wav: "audio/wav",
|
|
595
|
+
ogg: "audio/ogg",
|
|
596
|
+
amr: "voice/amr",
|
|
597
|
+
m4a: "audio/mp4",
|
|
598
|
+
mp4: "video/mp4",
|
|
599
|
+
mov: "video/quicktime",
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const EXT_BY_MIME: Record<string, string> = {
|
|
603
|
+
...Object.fromEntries(Object.entries(MIME_BY_EXT).map(([ext, mime]) => [mime, ext])),
|
|
604
|
+
"application/octet-stream": "bin",
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const GENERIC_CONTENT_TYPES = new Set([
|
|
608
|
+
"application/octet-stream",
|
|
609
|
+
"binary/octet-stream",
|
|
610
|
+
"application/download",
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
function normalizeContentType(raw?: string | null): string | undefined {
|
|
614
|
+
const normalized = String(raw ?? "").trim().split(";")[0]?.trim().toLowerCase();
|
|
615
|
+
return normalized || undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isGenericContentType(raw?: string | null): boolean {
|
|
619
|
+
const normalized = normalizeContentType(raw);
|
|
620
|
+
if (!normalized) return true;
|
|
621
|
+
return GENERIC_CONTENT_TYPES.has(normalized);
|
|
622
|
+
}
|
|
623
|
+
|
|
534
624
|
function guessContentTypeFromPath(filePath: string): string | undefined {
|
|
535
625
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
536
626
|
if (!ext) return undefined;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
627
|
+
return MIME_BY_EXT[ext];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function guessExtensionFromContentType(contentType?: string): string | undefined {
|
|
631
|
+
const normalized = normalizeContentType(contentType);
|
|
632
|
+
if (!normalized) return undefined;
|
|
633
|
+
if (normalized === "image/jpeg") return "jpg";
|
|
634
|
+
return EXT_BY_MIME[normalized];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function extractFileNameFromUrl(rawUrl?: string): string | undefined {
|
|
638
|
+
const s = String(rawUrl ?? "").trim();
|
|
639
|
+
if (!s) return undefined;
|
|
640
|
+
try {
|
|
641
|
+
const u = new URL(s);
|
|
642
|
+
const name = decodeURIComponent(u.pathname.split("/").pop() ?? "").trim();
|
|
643
|
+
return name || undefined;
|
|
644
|
+
} catch {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function sanitizeInboundFilename(raw?: string): string | undefined {
|
|
650
|
+
const s = String(raw ?? "").trim();
|
|
651
|
+
if (!s) return undefined;
|
|
652
|
+
const base = s.split(/[\\/]/).pop()?.trim() ?? "";
|
|
653
|
+
if (!base) return undefined;
|
|
654
|
+
const sanitized = base.replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
|
|
655
|
+
return sanitized || undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function hasLikelyExtension(name?: string): boolean {
|
|
659
|
+
if (!name) return false;
|
|
660
|
+
return /\.[a-z0-9]{1,16}$/i.test(name);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function detectMimeFromBuffer(buffer: Buffer): string | undefined {
|
|
664
|
+
if (!buffer || buffer.length < 4) return undefined;
|
|
665
|
+
|
|
666
|
+
// PNG
|
|
667
|
+
if (
|
|
668
|
+
buffer.length >= 8 &&
|
|
669
|
+
buffer[0] === 0x89 &&
|
|
670
|
+
buffer[1] === 0x50 &&
|
|
671
|
+
buffer[2] === 0x4e &&
|
|
672
|
+
buffer[3] === 0x47 &&
|
|
673
|
+
buffer[4] === 0x0d &&
|
|
674
|
+
buffer[5] === 0x0a &&
|
|
675
|
+
buffer[6] === 0x1a &&
|
|
676
|
+
buffer[7] === 0x0a
|
|
677
|
+
) {
|
|
678
|
+
return "image/png";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// JPEG
|
|
682
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
683
|
+
return "image/jpeg";
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// GIF
|
|
687
|
+
if (buffer.subarray(0, 6).toString("ascii") === "GIF87a" || buffer.subarray(0, 6).toString("ascii") === "GIF89a") {
|
|
688
|
+
return "image/gif";
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// WEBP
|
|
692
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
693
|
+
return "image/webp";
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// BMP
|
|
697
|
+
if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
|
|
698
|
+
return "image/bmp";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// PDF
|
|
702
|
+
if (buffer.subarray(0, 5).toString("ascii") === "%PDF-") {
|
|
703
|
+
return "application/pdf";
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// OGG
|
|
707
|
+
if (buffer.subarray(0, 4).toString("ascii") === "OggS") {
|
|
708
|
+
return "audio/ogg";
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// WAV
|
|
712
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WAVE") {
|
|
713
|
+
return "audio/wav";
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// MP3
|
|
717
|
+
if (buffer.subarray(0, 3).toString("ascii") === "ID3" || (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0)) {
|
|
718
|
+
return "audio/mpeg";
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// MP4/MOV family
|
|
722
|
+
if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") {
|
|
723
|
+
return "video/mp4";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Legacy Office (OLE Compound File)
|
|
727
|
+
if (
|
|
728
|
+
buffer.length >= 8 &&
|
|
729
|
+
buffer[0] === 0xd0 &&
|
|
730
|
+
buffer[1] === 0xcf &&
|
|
731
|
+
buffer[2] === 0x11 &&
|
|
732
|
+
buffer[3] === 0xe0 &&
|
|
733
|
+
buffer[4] === 0xa1 &&
|
|
734
|
+
buffer[5] === 0xb1 &&
|
|
735
|
+
buffer[6] === 0x1a &&
|
|
736
|
+
buffer[7] === 0xe1
|
|
737
|
+
) {
|
|
738
|
+
return "application/msword";
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ZIP / OOXML
|
|
742
|
+
const zipMagic =
|
|
743
|
+
(buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x03 && buffer[3] === 0x04) ||
|
|
744
|
+
(buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x05 && buffer[3] === 0x06) ||
|
|
745
|
+
(buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x07 && buffer[3] === 0x08);
|
|
746
|
+
if (zipMagic) {
|
|
747
|
+
const probe = buffer.subarray(0, Math.min(buffer.length, 512 * 1024));
|
|
748
|
+
if (probe.includes(Buffer.from("word/"))) {
|
|
749
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
750
|
+
}
|
|
751
|
+
if (probe.includes(Buffer.from("xl/"))) {
|
|
752
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
753
|
+
}
|
|
754
|
+
if (probe.includes(Buffer.from("ppt/"))) {
|
|
755
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
756
|
+
}
|
|
757
|
+
return "application/zip";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Plain text heuristic
|
|
761
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
|
|
762
|
+
let printable = 0;
|
|
763
|
+
for (const b of sample) {
|
|
764
|
+
if (b === 0x00) return undefined;
|
|
765
|
+
if (b === 0x09 || b === 0x0a || b === 0x0d || (b >= 0x20 && b <= 0x7e)) {
|
|
766
|
+
printable += 1;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (sample.length > 0 && printable / sample.length > 0.95) {
|
|
770
|
+
return "text/plain";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function resolveInlineFileName(input: unknown): string | undefined {
|
|
777
|
+
const raw = String(input ?? "").trim();
|
|
778
|
+
return sanitizeInboundFilename(raw);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function pickBotFileName(msg: WecomInboundMessage, item?: Record<string, any>): string | undefined {
|
|
782
|
+
const fromItem = item
|
|
783
|
+
? resolveInlineFileName(
|
|
784
|
+
item?.filename ??
|
|
785
|
+
item?.file_name ??
|
|
786
|
+
item?.fileName ??
|
|
787
|
+
item?.name ??
|
|
788
|
+
item?.title,
|
|
789
|
+
)
|
|
790
|
+
: undefined;
|
|
791
|
+
if (fromItem) return fromItem;
|
|
792
|
+
|
|
793
|
+
const fromFile = resolveInlineFileName(
|
|
794
|
+
(msg as any)?.file?.filename ??
|
|
795
|
+
(msg as any)?.file?.file_name ??
|
|
796
|
+
(msg as any)?.file?.fileName ??
|
|
797
|
+
(msg as any)?.file?.name ??
|
|
798
|
+
(msg as any)?.file?.title ??
|
|
799
|
+
(msg as any)?.filename ??
|
|
800
|
+
(msg as any)?.fileName ??
|
|
801
|
+
(msg as any)?.FileName,
|
|
802
|
+
);
|
|
803
|
+
return fromFile;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function inferInboundMediaMeta(params: {
|
|
807
|
+
kind: "image" | "file";
|
|
808
|
+
buffer: Buffer;
|
|
809
|
+
sourceUrl?: string;
|
|
810
|
+
sourceContentType?: string;
|
|
811
|
+
sourceFilename?: string;
|
|
812
|
+
explicitFilename?: string;
|
|
813
|
+
}): { contentType: string; filename: string } {
|
|
814
|
+
const headerType = normalizeContentType(params.sourceContentType);
|
|
815
|
+
const magicType = detectMimeFromBuffer(params.buffer);
|
|
816
|
+
const rawUrlName = sanitizeInboundFilename(extractFileNameFromUrl(params.sourceUrl));
|
|
817
|
+
const guessedByUrl = hasLikelyExtension(rawUrlName) ? rawUrlName : undefined;
|
|
818
|
+
const explicitName = sanitizeInboundFilename(params.explicitFilename);
|
|
819
|
+
const sourceName = sanitizeInboundFilename(params.sourceFilename);
|
|
820
|
+
const chosenName = explicitName || sourceName || guessedByUrl;
|
|
821
|
+
const typeByName = chosenName ? guessContentTypeFromPath(chosenName) : undefined;
|
|
822
|
+
|
|
823
|
+
let contentType: string;
|
|
824
|
+
if (params.kind === "image") {
|
|
825
|
+
if (magicType?.startsWith("image/")) contentType = magicType;
|
|
826
|
+
else if (headerType?.startsWith("image/")) contentType = headerType;
|
|
827
|
+
else if (typeByName?.startsWith("image/")) contentType = typeByName;
|
|
828
|
+
else contentType = "image/jpeg";
|
|
829
|
+
} else {
|
|
830
|
+
contentType =
|
|
831
|
+
magicType ||
|
|
832
|
+
(!isGenericContentType(headerType) ? headerType! : undefined) ||
|
|
833
|
+
typeByName ||
|
|
834
|
+
"application/octet-stream";
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const hasExt = Boolean(chosenName && /\.[a-z0-9]{1,16}$/i.test(chosenName));
|
|
838
|
+
const ext = guessExtensionFromContentType(contentType) || (params.kind === "image" ? "jpg" : "bin");
|
|
839
|
+
const filename = chosenName
|
|
840
|
+
? (hasExt ? chosenName : `${chosenName}.${ext}`)
|
|
841
|
+
: `${params.kind}.${ext}`;
|
|
842
|
+
|
|
843
|
+
return { contentType, filename };
|
|
551
844
|
}
|
|
552
845
|
|
|
553
846
|
function looksLikeSendLocalFileIntent(rawBody: string): boolean {
|
|
@@ -670,13 +963,21 @@ async function processInboundMessage(target: WecomWebhookTarget, msg: WecomInbou
|
|
|
670
963
|
const url = String((msg as any).image?.url ?? "").trim();
|
|
671
964
|
if (url && aesKey) {
|
|
672
965
|
try {
|
|
673
|
-
const
|
|
966
|
+
const decrypted = await decryptWecomMediaWithMeta(url, aesKey, { maxBytes, http: { proxyUrl } });
|
|
967
|
+
const inferred = inferInboundMediaMeta({
|
|
968
|
+
kind: "image",
|
|
969
|
+
buffer: decrypted.buffer,
|
|
970
|
+
sourceUrl: decrypted.sourceUrl || url,
|
|
971
|
+
sourceContentType: decrypted.sourceContentType,
|
|
972
|
+
sourceFilename: decrypted.sourceFilename,
|
|
973
|
+
explicitFilename: pickBotFileName(msg),
|
|
974
|
+
});
|
|
674
975
|
return {
|
|
675
976
|
body: "[image]",
|
|
676
977
|
media: {
|
|
677
|
-
buffer:
|
|
678
|
-
contentType:
|
|
679
|
-
filename:
|
|
978
|
+
buffer: decrypted.buffer,
|
|
979
|
+
contentType: inferred.contentType,
|
|
980
|
+
filename: inferred.filename,
|
|
680
981
|
}
|
|
681
982
|
};
|
|
682
983
|
} catch (err) {
|
|
@@ -684,7 +985,10 @@ async function processInboundMessage(target: WecomWebhookTarget, msg: WecomInbou
|
|
|
684
985
|
target.runtime.error?.(
|
|
685
986
|
`图片解密失败: ${String(err)}; 可调大 channels.wecom.media.maxBytes(当前=${maxBytes})例如:openclaw config set channels.wecom.media.maxBytes ${50 * 1024 * 1024}`,
|
|
686
987
|
);
|
|
687
|
-
|
|
988
|
+
const errorMessage = typeof err === 'object' && err
|
|
989
|
+
? `${(err as any).message}${((err as any).cause) ? ` (cause: ${String((err as any).cause)})` : ''}`
|
|
990
|
+
: String(err);
|
|
991
|
+
return { body: `[image] (decryption failed: ${errorMessage})` };
|
|
688
992
|
}
|
|
689
993
|
}
|
|
690
994
|
}
|
|
@@ -693,20 +997,31 @@ async function processInboundMessage(target: WecomWebhookTarget, msg: WecomInbou
|
|
|
693
997
|
const url = String((msg as any).file?.url ?? "").trim();
|
|
694
998
|
if (url && aesKey) {
|
|
695
999
|
try {
|
|
696
|
-
const
|
|
1000
|
+
const decrypted = await decryptWecomMediaWithMeta(url, aesKey, { maxBytes, http: { proxyUrl } });
|
|
1001
|
+
const inferred = inferInboundMediaMeta({
|
|
1002
|
+
kind: "file",
|
|
1003
|
+
buffer: decrypted.buffer,
|
|
1004
|
+
sourceUrl: decrypted.sourceUrl || url,
|
|
1005
|
+
sourceContentType: decrypted.sourceContentType,
|
|
1006
|
+
sourceFilename: decrypted.sourceFilename,
|
|
1007
|
+
explicitFilename: pickBotFileName(msg),
|
|
1008
|
+
});
|
|
697
1009
|
return {
|
|
698
1010
|
body: "[file]",
|
|
699
1011
|
media: {
|
|
700
|
-
buffer:
|
|
701
|
-
contentType:
|
|
702
|
-
filename:
|
|
1012
|
+
buffer: decrypted.buffer,
|
|
1013
|
+
contentType: inferred.contentType,
|
|
1014
|
+
filename: inferred.filename,
|
|
703
1015
|
}
|
|
704
1016
|
};
|
|
705
1017
|
} catch (err) {
|
|
706
1018
|
target.runtime.error?.(
|
|
707
1019
|
`Failed to decrypt inbound file: ${String(err)}; 可调大 channels.wecom.media.maxBytes(当前=${maxBytes})例如:openclaw config set channels.wecom.media.maxBytes ${50 * 1024 * 1024}`,
|
|
708
1020
|
);
|
|
709
|
-
|
|
1021
|
+
const errorMessage = typeof err === 'object' && err
|
|
1022
|
+
? `${(err as any).message}${((err as any).cause) ? ` (cause: ${String((err as any).cause)})` : ''}`
|
|
1023
|
+
: String(err);
|
|
1024
|
+
return { body: `[file] (decryption failed: ${errorMessage})` };
|
|
710
1025
|
}
|
|
711
1026
|
}
|
|
712
1027
|
}
|
|
@@ -728,18 +1043,29 @@ async function processInboundMessage(target: WecomWebhookTarget, msg: WecomInbou
|
|
|
728
1043
|
const url = String(item[t]?.url ?? "").trim();
|
|
729
1044
|
if (url) {
|
|
730
1045
|
try {
|
|
731
|
-
const
|
|
1046
|
+
const decrypted = await decryptWecomMediaWithMeta(url, aesKey, { maxBytes, http: { proxyUrl } });
|
|
1047
|
+
const inferred = inferInboundMediaMeta({
|
|
1048
|
+
kind: t,
|
|
1049
|
+
buffer: decrypted.buffer,
|
|
1050
|
+
sourceUrl: decrypted.sourceUrl || url,
|
|
1051
|
+
sourceContentType: decrypted.sourceContentType,
|
|
1052
|
+
sourceFilename: decrypted.sourceFilename,
|
|
1053
|
+
explicitFilename: pickBotFileName(msg, item?.[t]),
|
|
1054
|
+
});
|
|
732
1055
|
foundMedia = {
|
|
733
|
-
buffer:
|
|
734
|
-
contentType:
|
|
735
|
-
filename:
|
|
1056
|
+
buffer: decrypted.buffer,
|
|
1057
|
+
contentType: inferred.contentType,
|
|
1058
|
+
filename: inferred.filename,
|
|
736
1059
|
};
|
|
737
1060
|
bodyParts.push(`[${t}]`);
|
|
738
1061
|
} catch (err) {
|
|
739
1062
|
target.runtime.error?.(
|
|
740
1063
|
`Failed to decrypt mixed ${t}: ${String(err)}; 可调大 channels.wecom.media.maxBytes(当前=${maxBytes})例如:openclaw config set channels.wecom.media.maxBytes ${50 * 1024 * 1024}`,
|
|
741
1064
|
);
|
|
742
|
-
|
|
1065
|
+
const errorMessage = typeof err === 'object' && err
|
|
1066
|
+
? `${(err as any).message}${((err as any).cause) ? ` (cause: ${String((err as any).cause)})` : ''}`
|
|
1067
|
+
: String(err);
|
|
1068
|
+
bodyParts.push(`[${t}] (decryption failed: ${errorMessage})`);
|
|
743
1069
|
}
|
|
744
1070
|
} else {
|
|
745
1071
|
bodyParts.push(`[${t}]`);
|
|
@@ -965,6 +1291,60 @@ async function startAgentForStream(params: {
|
|
|
965
1291
|
streamStore.onStreamFinished(streamId);
|
|
966
1292
|
return;
|
|
967
1293
|
}
|
|
1294
|
+
|
|
1295
|
+
// 图片路径都读取失败时,切换到 Agent 私信兜底,并主动结束 Bot 流。
|
|
1296
|
+
const agentCfg = resolveAgentAccountOrUndefined(config, account.accountId);
|
|
1297
|
+
const agentOk = Boolean(agentCfg);
|
|
1298
|
+
const fallbackName = imagePaths.length === 1
|
|
1299
|
+
? (imagePaths[0]!.split("/").pop() || "image")
|
|
1300
|
+
: `${imagePaths.length} 张图片`;
|
|
1301
|
+
const prompt = buildFallbackPrompt({
|
|
1302
|
+
kind: "media",
|
|
1303
|
+
agentConfigured: agentOk,
|
|
1304
|
+
userId: userid,
|
|
1305
|
+
filename: fallbackName,
|
|
1306
|
+
chatType,
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
streamStore.updateStream(streamId, (s) => {
|
|
1310
|
+
s.fallbackMode = "error";
|
|
1311
|
+
s.finished = true;
|
|
1312
|
+
s.content = prompt;
|
|
1313
|
+
s.fallbackPromptSentAt = s.fallbackPromptSentAt ?? Date.now();
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
try {
|
|
1317
|
+
await sendBotFallbackPromptNow({ streamId, text: prompt });
|
|
1318
|
+
logVerbose(target, `local-path: 图片读取失败后已推送兜底提示`);
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
target.runtime.error?.(`local-path: 图片读取失败后的兜底提示推送失败: ${String(err)}`);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (agentCfg && userid && userid !== "unknown") {
|
|
1324
|
+
for (const p of imagePaths) {
|
|
1325
|
+
const guessedType = guessContentTypeFromPath(p);
|
|
1326
|
+
try {
|
|
1327
|
+
await sendAgentDmMedia({
|
|
1328
|
+
agent: agentCfg,
|
|
1329
|
+
userId: userid,
|
|
1330
|
+
mediaUrlOrPath: p,
|
|
1331
|
+
contentType: guessedType,
|
|
1332
|
+
filename: p.split("/").pop() || "image",
|
|
1333
|
+
});
|
|
1334
|
+
streamStore.updateStream(streamId, (s) => {
|
|
1335
|
+
s.agentMediaKeys = Array.from(new Set([...(s.agentMediaKeys ?? []), p]));
|
|
1336
|
+
});
|
|
1337
|
+
logVerbose(
|
|
1338
|
+
target,
|
|
1339
|
+
`local-path: 图片已通过 Agent 私信发送 user=${userid} path=${p} contentType=${guessedType ?? "unknown"}`,
|
|
1340
|
+
);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
target.runtime.error?.(`local-path: 图片 Agent 私信兜底失败 path=${p}: ${String(err)}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
streamStore.onStreamFinished(streamId);
|
|
1347
|
+
return;
|
|
968
1348
|
}
|
|
969
1349
|
|
|
970
1350
|
// 2) 非图片文件:Bot 会话里提示 + Agent 私信兜底(目标锁定 userId)
|
|
@@ -1008,18 +1388,22 @@ async function startAgentForStream(params: {
|
|
|
1008
1388
|
for (const p of otherPaths) {
|
|
1009
1389
|
const alreadySent = streamStore.getStream(streamId)?.agentMediaKeys?.includes(p);
|
|
1010
1390
|
if (alreadySent) continue;
|
|
1391
|
+
const guessedType = guessContentTypeFromPath(p);
|
|
1011
1392
|
try {
|
|
1012
1393
|
await sendAgentDmMedia({
|
|
1013
1394
|
agent: agentCfg,
|
|
1014
1395
|
userId: userid,
|
|
1015
1396
|
mediaUrlOrPath: p,
|
|
1016
|
-
contentType:
|
|
1397
|
+
contentType: guessedType,
|
|
1017
1398
|
filename: p.split("/").pop() || "file",
|
|
1018
1399
|
});
|
|
1019
1400
|
streamStore.updateStream(streamId, (s) => {
|
|
1020
1401
|
s.agentMediaKeys = Array.from(new Set([...(s.agentMediaKeys ?? []), p]));
|
|
1021
1402
|
});
|
|
1022
|
-
logVerbose(
|
|
1403
|
+
logVerbose(
|
|
1404
|
+
target,
|
|
1405
|
+
`local-path: 文件已通过 Agent 私信发送 user=${userid} path=${p} contentType=${guessedType ?? "unknown"}`,
|
|
1406
|
+
);
|
|
1023
1407
|
} catch (err) {
|
|
1024
1408
|
target.runtime.error?.(`local-path: Agent 私信发送文件失败 path=${p}: ${String(err)}`);
|
|
1025
1409
|
}
|
|
@@ -1173,6 +1557,10 @@ async function startAgentForStream(params: {
|
|
|
1173
1557
|
SenderName: userid,
|
|
1174
1558
|
SenderId: userid,
|
|
1175
1559
|
Provider: "wecom",
|
|
1560
|
+
// Keep Surface aligned with OriginatingChannel for Bot-mode delivery.
|
|
1561
|
+
// If Surface is "webchat", core dispatch treats this as cross-channel
|
|
1562
|
+
// and routes replies via routeReply -> wecom outbound (Agent API),
|
|
1563
|
+
// bypassing the Bot stream deliver path.
|
|
1176
1564
|
Surface: "wecom",
|
|
1177
1565
|
MessageSid: msg.msgid,
|
|
1178
1566
|
CommandAuthorized: commandAuthorized,
|
|
@@ -1212,8 +1600,10 @@ async function startAgentForStream(params: {
|
|
|
1212
1600
|
const baseTools = (config as any)?.tools ?? {};
|
|
1213
1601
|
const baseSandbox = (baseTools as any)?.sandbox ?? {};
|
|
1214
1602
|
const baseSandboxTools = (baseSandbox as any)?.tools ?? {};
|
|
1215
|
-
const
|
|
1216
|
-
const
|
|
1603
|
+
const existingTopLevelDeny = Array.isArray((baseTools as any).deny) ? ((baseTools as any).deny as string[]) : [];
|
|
1604
|
+
const existingSandboxDeny = Array.isArray((baseSandboxTools as any).deny) ? ((baseSandboxTools as any).deny as string[]) : [];
|
|
1605
|
+
const topLevelDeny = Array.from(new Set([...existingTopLevelDeny, "message"]));
|
|
1606
|
+
const sandboxDeny = Array.from(new Set([...existingSandboxDeny, "message"]));
|
|
1217
1607
|
return {
|
|
1218
1608
|
...(config as any),
|
|
1219
1609
|
agents: {
|
|
@@ -1237,17 +1627,18 @@ async function startAgentForStream(params: {
|
|
|
1237
1627
|
},
|
|
1238
1628
|
tools: {
|
|
1239
1629
|
...baseTools,
|
|
1630
|
+
deny: topLevelDeny,
|
|
1240
1631
|
sandbox: {
|
|
1241
1632
|
...baseSandbox,
|
|
1242
1633
|
tools: {
|
|
1243
1634
|
...baseSandboxTools,
|
|
1244
|
-
deny,
|
|
1635
|
+
deny: sandboxDeny,
|
|
1245
1636
|
},
|
|
1246
1637
|
},
|
|
1247
1638
|
},
|
|
1248
1639
|
} as OpenClawConfig;
|
|
1249
1640
|
})();
|
|
1250
|
-
logVerbose(target, `tool-policy: WeCom Bot 会话已禁用 message 工具(tools.sandbox.tools.deny
|
|
1641
|
+
logVerbose(target, `tool-policy: WeCom Bot 会话已禁用 message 工具(tools.deny += message;并同步到 tools.sandbox.tools.deny,防止绕过 Bot 交付)`);
|
|
1251
1642
|
|
|
1252
1643
|
// 调度 Agent 回复
|
|
1253
1644
|
// 使用 dispatchReplyWithBufferedBlockDispatcher 可以处理流式输出 buffer
|
|
@@ -1331,9 +1722,10 @@ async function startAgentForStream(params: {
|
|
|
1331
1722
|
if (!current.images) current.images = [];
|
|
1332
1723
|
if (!current.agentMediaKeys) current.agentMediaKeys = [];
|
|
1333
1724
|
|
|
1725
|
+
const deliverKind = info?.kind ?? "block";
|
|
1334
1726
|
logVerbose(
|
|
1335
1727
|
target,
|
|
1336
|
-
`deliver: kind=${
|
|
1728
|
+
`deliver: kind=${deliverKind} chatType=${current.chatType ?? chatType} user=${current.userId ?? userid} textLen=${text.length} mediaCount=${(payload.mediaUrls?.length ?? 0) + (payload.mediaUrl ? 1 : 0)}`,
|
|
1337
1729
|
);
|
|
1338
1730
|
|
|
1339
1731
|
// If the model referenced a local image path in its reply but did not emit mediaUrl(s),
|
|
@@ -1379,7 +1771,7 @@ async function startAgentForStream(params: {
|
|
|
1379
1771
|
});
|
|
1380
1772
|
}
|
|
1381
1773
|
|
|
1382
|
-
// Timeout fallback
|
|
1774
|
+
// Timeout fallback: near 6min window, stop bot stream and switch to Agent DM.
|
|
1383
1775
|
const now = Date.now();
|
|
1384
1776
|
const deadline = current.createdAt + BOT_WINDOW_MS;
|
|
1385
1777
|
const switchAt = deadline - BOT_SWITCH_MARGIN_MS;
|
|
@@ -1414,10 +1806,10 @@ async function startAgentForStream(params: {
|
|
|
1414
1806
|
|
|
1415
1807
|
const mediaUrls = payload.mediaUrls || (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
1416
1808
|
for (const mediaPath of mediaUrls) {
|
|
1809
|
+
let contentType: string | undefined;
|
|
1810
|
+
let filename = mediaPath.split("/").pop() || "attachment";
|
|
1417
1811
|
try {
|
|
1418
1812
|
let buf: Buffer;
|
|
1419
|
-
let contentType: string | undefined;
|
|
1420
|
-
let filename: string;
|
|
1421
1813
|
|
|
1422
1814
|
const looksLikeUrl = /^https?:\/\//i.test(mediaPath);
|
|
1423
1815
|
|
|
@@ -1494,6 +1886,48 @@ async function startAgentForStream(params: {
|
|
|
1494
1886
|
}
|
|
1495
1887
|
} catch (err) {
|
|
1496
1888
|
target.runtime.error?.(`Failed to process outbound media: ${mediaPath}: ${String(err)}`);
|
|
1889
|
+
const agentCfg = resolveAgentAccountOrUndefined(config, account.accountId);
|
|
1890
|
+
const agentOk = Boolean(agentCfg);
|
|
1891
|
+
const fallbackFilename = filename || mediaPath.split("/").pop() || "attachment";
|
|
1892
|
+
if (agentCfg && current.userId && !current.agentMediaKeys.includes(mediaPath)) {
|
|
1893
|
+
try {
|
|
1894
|
+
await sendAgentDmMedia({
|
|
1895
|
+
agent: agentCfg,
|
|
1896
|
+
userId: current.userId,
|
|
1897
|
+
mediaUrlOrPath: mediaPath,
|
|
1898
|
+
contentType,
|
|
1899
|
+
filename: fallbackFilename,
|
|
1900
|
+
});
|
|
1901
|
+
streamStore.updateStream(streamId, (s) => {
|
|
1902
|
+
s.agentMediaKeys = Array.from(new Set([...(s.agentMediaKeys ?? []), mediaPath]));
|
|
1903
|
+
});
|
|
1904
|
+
logVerbose(target, `fallback(error): 媒体处理失败后已通过 Agent 私信发送 user=${current.userId}`);
|
|
1905
|
+
} catch (sendErr) {
|
|
1906
|
+
target.runtime.error?.(`fallback(error): 媒体处理失败后的 Agent 私信发送也失败: ${String(sendErr)}`);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (!current.fallbackMode) {
|
|
1910
|
+
const prompt = buildFallbackPrompt({
|
|
1911
|
+
kind: "error",
|
|
1912
|
+
agentConfigured: agentOk,
|
|
1913
|
+
userId: current.userId,
|
|
1914
|
+
filename: fallbackFilename,
|
|
1915
|
+
chatType: current.chatType,
|
|
1916
|
+
});
|
|
1917
|
+
streamStore.updateStream(streamId, (s) => {
|
|
1918
|
+
s.fallbackMode = "error";
|
|
1919
|
+
s.finished = true;
|
|
1920
|
+
s.content = prompt;
|
|
1921
|
+
s.fallbackPromptSentAt = s.fallbackPromptSentAt ?? Date.now();
|
|
1922
|
+
});
|
|
1923
|
+
try {
|
|
1924
|
+
await sendBotFallbackPromptNow({ streamId, text: prompt });
|
|
1925
|
+
logVerbose(target, `fallback(error): 群内提示已推送`);
|
|
1926
|
+
} catch (pushErr) {
|
|
1927
|
+
target.runtime.error?.(`wecom bot fallback prompt push failed (error) streamId=${streamId}: ${String(pushErr)}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return;
|
|
1497
1931
|
}
|
|
1498
1932
|
}
|
|
1499
1933
|
|
|
@@ -1532,6 +1966,12 @@ async function startAgentForStream(params: {
|
|
|
1532
1966
|
}
|
|
1533
1967
|
}
|
|
1534
1968
|
|
|
1969
|
+
streamStore.updateStream(streamId, (s) => {
|
|
1970
|
+
if (!s.content.trim() && !(s.images?.length ?? 0)) {
|
|
1971
|
+
s.content = "✅ 已处理完成。";
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1535
1975
|
streamStore.markFinished(streamId);
|
|
1536
1976
|
|
|
1537
1977
|
// Timeout fallback final delivery (Agent DM): send once after the agent run completes.
|
|
@@ -1556,35 +1996,19 @@ async function startAgentForStream(params: {
|
|
|
1556
1996
|
}
|
|
1557
1997
|
}
|
|
1558
1998
|
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
if (
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
responseUrl,
|
|
1573
|
-
{
|
|
1574
|
-
method: "POST",
|
|
1575
|
-
headers: { "Content-Type": "application/json" },
|
|
1576
|
-
body: JSON.stringify(finalReply),
|
|
1577
|
-
},
|
|
1578
|
-
{ proxyUrl, timeoutMs: LIMITS.REQUEST_TIMEOUT_MS },
|
|
1579
|
-
);
|
|
1580
|
-
if (!res.ok) {
|
|
1581
|
-
throw new Error(`final stream push failed: ${res.status}`);
|
|
1582
|
-
}
|
|
1583
|
-
});
|
|
1584
|
-
logVerbose(target, `final stream pushed via response_url (group) streamId=${streamId}, images=${state.images?.length ?? 0}`);
|
|
1585
|
-
} catch (err) {
|
|
1586
|
-
target.runtime.error?.(`final stream push via response_url failed (group) streamId=${streamId}: ${String(err)}`);
|
|
1587
|
-
}
|
|
1999
|
+
// 统一终结:只要 response_url 可用,尽量主动推一次最终流帧,确保“思考中”能及时收口。
|
|
2000
|
+
// 失败仅记录日志,不影响 stream_refresh 被动拉取链路。
|
|
2001
|
+
const stateAfterFinish = streamStore.getStream(streamId);
|
|
2002
|
+
const responseUrl = getActiveReplyUrl(streamId);
|
|
2003
|
+
if (stateAfterFinish && responseUrl) {
|
|
2004
|
+
try {
|
|
2005
|
+
await pushFinalStreamReplyNow(streamId);
|
|
2006
|
+
logVerbose(
|
|
2007
|
+
target,
|
|
2008
|
+
`final stream pushed via response_url streamId=${streamId}, chatType=${chatType}, images=${stateAfterFinish.images?.length ?? 0}`,
|
|
2009
|
+
);
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
target.runtime.error?.(`final stream push via response_url failed streamId=${streamId}: ${String(err)}`);
|
|
1588
2012
|
}
|
|
1589
2013
|
}
|
|
1590
2014
|
|
|
@@ -1698,7 +2122,7 @@ export function registerAgentWebhookTarget(target: AgentWebhookTarget): () => vo
|
|
|
1698
2122
|
*
|
|
1699
2123
|
* 处理来自企业微信的所有 Webhook 请求。
|
|
1700
2124
|
* 职责:
|
|
1701
|
-
* 1.
|
|
2125
|
+
* 1. 路由分发:优先按 `/plugins/wecom/{bot|agent}/{accountId}` 分流,并兼容历史 `/wecom/*` 路径。
|
|
1702
2126
|
* 2. 安全校验:验证企业微信签名 (Signature)。
|
|
1703
2127
|
* 3. 消息解密:处理企业微信的加密包。
|
|
1704
2128
|
* 4. 响应处理:
|
|
@@ -1722,7 +2146,7 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
1722
2146
|
`[wecom] inbound(http): reqId=${reqId} path=${path} method=${req.method ?? "UNKNOWN"} remote=${remote} ua=${ua ? `"${ua}"` : "N/A"} contentLength=${cl || "N/A"} query={timestamp:${hasTimestamp},nonce:${hasNonce},echostr:${hasEchostr},msg_signature:${hasMsgSig},signature:${hasSignature}}`,
|
|
1723
2147
|
);
|
|
1724
2148
|
|
|
1725
|
-
if (hasMatrixExplicitRoutesRegistered() &&
|
|
2149
|
+
if (hasMatrixExplicitRoutesRegistered() && isNonMatrixWecomBasePath(path)) {
|
|
1726
2150
|
logRouteFailure({
|
|
1727
2151
|
reqId,
|
|
1728
2152
|
path,
|
|
@@ -1733,14 +2157,19 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
1733
2157
|
writeRouteFailure(
|
|
1734
2158
|
res,
|
|
1735
2159
|
"wecom_matrix_path_required",
|
|
1736
|
-
"Matrix mode requires explicit account path. Use /wecom/bot/{accountId} or /wecom/agent/{accountId}.",
|
|
2160
|
+
"Matrix mode requires explicit account path. Use /plugins/wecom/bot/{accountId} or /plugins/wecom/agent/{accountId}.",
|
|
1737
2161
|
);
|
|
1738
2162
|
return true;
|
|
1739
2163
|
}
|
|
1740
2164
|
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
2165
|
+
const isAgentPathCandidate =
|
|
2166
|
+
path === WEBHOOK_PATHS.AGENT ||
|
|
2167
|
+
path === WEBHOOK_PATHS.AGENT_PLUGIN ||
|
|
2168
|
+
path.startsWith(`${WEBHOOK_PATHS.AGENT}/`) ||
|
|
2169
|
+
path.startsWith(`${WEBHOOK_PATHS.AGENT_PLUGIN}/`);
|
|
2170
|
+
const matchedAgentTargets = agentTargets.get(path) ?? [];
|
|
2171
|
+
if (matchedAgentTargets.length > 0 || isAgentPathCandidate) {
|
|
2172
|
+
const targets = matchedAgentTargets;
|
|
1744
2173
|
if (targets.length > 0) {
|
|
1745
2174
|
const query = resolveQueryParams(req);
|
|
1746
2175
|
const timestamp = query.get("timestamp") ?? "";
|
|
@@ -1916,7 +2345,7 @@ export async function handleWecomWebhookRequest(req: IncomingMessage, res: Serve
|
|
|
1916
2345
|
return true;
|
|
1917
2346
|
}
|
|
1918
2347
|
|
|
1919
|
-
// Bot 模式路由: /wecom
|
|
2348
|
+
// Bot 模式路由: /plugins/wecom/bot(推荐)以及 /wecom、/wecom/bot(兼容)
|
|
1920
2349
|
const targets = webhookTargets.get(path);
|
|
1921
2350
|
if (!targets || targets.length === 0) return false;
|
|
1922
2351
|
|