gewe-openclaw 2026.3.13 → 2026.3.23
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 +455 -3
- package/index.ts +39 -1
- package/package.json +12 -1
- package/skills/gewe-agent-tools/SKILL.md +113 -0
- package/skills/gewe-channel-rules/SKILL.md +7 -0
- package/src/accounts.ts +51 -5
- package/src/api-tools.ts +1264 -0
- package/src/api.ts +37 -2
- package/src/binary-command.ts +65 -0
- package/src/channel-actions.ts +536 -0
- package/src/channel-allowlist.ts +150 -0
- package/src/channel-directory.ts +419 -0
- package/src/channel-status.ts +186 -0
- package/src/channel.ts +155 -58
- package/src/config-edit.ts +94 -0
- package/src/config-schema.ts +78 -3
- package/src/contacts-api.ts +113 -0
- package/src/delivery.ts +502 -62
- package/src/directory-cache.ts +164 -0
- package/src/gewe-account-api.ts +27 -0
- package/src/group-allowlist-tool.ts +242 -0
- package/src/group-binding-tool.ts +154 -0
- package/src/group-binding.ts +405 -0
- package/src/groups-api.ts +146 -0
- package/src/inbound-batch.ts +5 -2
- package/src/inbound.ts +248 -41
- package/src/media-server.ts +73 -93
- package/src/moments-api.ts +138 -0
- package/src/monitor.ts +81 -24
- package/src/onboarding.ts +9 -4
- package/src/openclaw-compat.ts +1070 -0
- package/src/pairing-store.ts +478 -0
- package/src/personal-api.ts +45 -0
- package/src/policy.ts +130 -22
- package/src/quote-context-cache.ts +97 -0
- package/src/reply-options.ts +101 -2
- package/src/s3.ts +1 -1
- package/src/send.ts +235 -16
- package/src/setup-wizard-types.ts +162 -0
- package/src/setup-wizard.ts +464 -0
- package/src/silk.ts +2 -1
- package/src/state-paths.ts +55 -14
- package/src/types.ts +66 -7
- package/src/xml.ts +158 -0
package/src/inbound.ts
CHANGED
|
@@ -2,8 +2,13 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
logInboundDrop,
|
|
7
|
+
resolveControlCommandGate,
|
|
8
|
+
type OpenClawConfig,
|
|
9
|
+
type ReplyPayload,
|
|
10
|
+
type RuntimeEnv,
|
|
11
|
+
} from "./openclaw-compat.js";
|
|
7
12
|
|
|
8
13
|
import {
|
|
9
14
|
buildGeweInboundMediaPayload,
|
|
@@ -12,23 +17,49 @@ import {
|
|
|
12
17
|
import type { GeweDownloadQueue } from "./download-queue.js";
|
|
13
18
|
import { downloadGeweFile, downloadGeweImage, downloadGeweVideo, downloadGeweVoice } from "./download.js";
|
|
14
19
|
import { deliverGewePayload } from "./delivery.js";
|
|
15
|
-
import { resolveGeweReplyOptions } from "./reply-options.js";
|
|
20
|
+
import { applyGeweReplyModeToPayload, resolveGeweReplyOptions } from "./reply-options.js";
|
|
21
|
+
import { rememberGeweDirectoryObservation } from "./directory-cache.js";
|
|
16
22
|
import { getGeweRuntime } from "./runtime.js";
|
|
17
23
|
import { ensureRustSilkBinary } from "./silk.js";
|
|
24
|
+
import { readGeweAllowFromStore, redeemGewePairCode } from "./pairing-store.js";
|
|
18
25
|
import {
|
|
19
26
|
normalizeGeweAllowlist,
|
|
20
27
|
resolveGeweAllowlistMatch,
|
|
28
|
+
resolveGeweDmMatch,
|
|
29
|
+
resolveGeweDmReplyMode,
|
|
30
|
+
resolveGeweDmTriggerMode,
|
|
21
31
|
resolveGeweGroupAllow,
|
|
22
32
|
resolveGeweGroupMatch,
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
resolveGeweGroupReplyMode,
|
|
34
|
+
resolveGeweGroupTriggerMode,
|
|
35
|
+
resolveGeweTriggerGate,
|
|
25
36
|
} from "./policy.js";
|
|
26
|
-
import type {
|
|
27
|
-
|
|
37
|
+
import type {
|
|
38
|
+
CoreConfig,
|
|
39
|
+
GeweDmReplyMode,
|
|
40
|
+
GeweGroupReplyMode,
|
|
41
|
+
GeweInboundMessage,
|
|
42
|
+
ResolvedGeweAccount,
|
|
43
|
+
} from "./types.js";
|
|
44
|
+
import {
|
|
45
|
+
extractAppMsgType,
|
|
46
|
+
extractFileName,
|
|
47
|
+
extractLinkDetails,
|
|
48
|
+
extractQuoteDetails,
|
|
49
|
+
extractQuoteSummary,
|
|
50
|
+
type GeweQuoteDetails,
|
|
51
|
+
} from "./xml.js";
|
|
28
52
|
import { CHANNEL_ID } from "./constants.js";
|
|
53
|
+
import { rememberGeweQuoteReplyContext } from "./quote-context-cache.js";
|
|
29
54
|
|
|
30
55
|
type PreparedInbound = {
|
|
31
56
|
rawBody: string;
|
|
57
|
+
messageType: number;
|
|
58
|
+
rawXml?: string;
|
|
59
|
+
appMsgXml?: string;
|
|
60
|
+
appMsgType?: number;
|
|
61
|
+
quoteXml?: string;
|
|
62
|
+
quoteDetails?: GeweQuoteDetails;
|
|
32
63
|
commandAuthorized: boolean;
|
|
33
64
|
isGroup: boolean;
|
|
34
65
|
senderId: string;
|
|
@@ -36,6 +67,8 @@ type PreparedInbound = {
|
|
|
36
67
|
groupId?: string;
|
|
37
68
|
groupName?: string;
|
|
38
69
|
groupSystemPrompt?: string;
|
|
70
|
+
groupSkillFilter?: string[];
|
|
71
|
+
replyMode: GeweGroupReplyMode | GeweDmReplyMode;
|
|
39
72
|
route: ReturnType<ReturnType<typeof getGeweRuntime>["channel"]["routing"]["resolveAgentRoute"]>;
|
|
40
73
|
storePath: string;
|
|
41
74
|
toWxid: string;
|
|
@@ -49,6 +82,11 @@ type PreparedInbound = {
|
|
|
49
82
|
type NormalizedInboundEntry = {
|
|
50
83
|
message: GeweInboundMessage;
|
|
51
84
|
rawBody: string;
|
|
85
|
+
rawXml?: string;
|
|
86
|
+
appMsgXml?: string;
|
|
87
|
+
appMsgType?: number;
|
|
88
|
+
quoteXml?: string;
|
|
89
|
+
quoteDetails?: GeweQuoteDetails;
|
|
52
90
|
download?: {
|
|
53
91
|
msgType: number;
|
|
54
92
|
xml: string;
|
|
@@ -58,6 +96,10 @@ type NormalizedInboundEntry = {
|
|
|
58
96
|
const DEFAULT_VOICE_SAMPLE_RATE = 24000;
|
|
59
97
|
const DEFAULT_VOICE_DECODE_TIMEOUT_MS = 30_000;
|
|
60
98
|
const SILK_HEADER = "#!SILK_V3";
|
|
99
|
+
const GEWE_PAIR_CODE_REGEX = /^[A-HJ-NP-Z2-9]{8}$/i;
|
|
100
|
+
const GEWE_PAIR_CODE_PREFIX_REGEX = /^配对码\s*[::]?\s*([A-HJ-NP-Z2-9]{8})$/i;
|
|
101
|
+
const GEWE_PAIR_CODE_SUCCESS_REPLY = "配对成功,已加入允许列表。请重新发送上一条消息。";
|
|
102
|
+
const GEWE_PAIR_CODE_INVALID_REPLY = "配对码无效或已过期。";
|
|
61
103
|
|
|
62
104
|
function resolveMediaPlaceholder(msgType: number): string {
|
|
63
105
|
if (msgType === 3) return "<media:image>";
|
|
@@ -67,6 +109,20 @@ function resolveMediaPlaceholder(msgType: number): string {
|
|
|
67
109
|
return "";
|
|
68
110
|
}
|
|
69
111
|
|
|
112
|
+
function resolveAppMsgPlaceholder(appType?: number): string {
|
|
113
|
+
return typeof appType === "number" ? `<appmsg:${appType}>` : "<appmsg>";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveGewePairCodeCandidate(rawBody: string): string | null {
|
|
117
|
+
const trimmed = rawBody.trim();
|
|
118
|
+
if (!trimmed) return null;
|
|
119
|
+
if (GEWE_PAIR_CODE_REGEX.test(trimmed)) {
|
|
120
|
+
return trimmed.toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
const prefixed = trimmed.match(GEWE_PAIR_CODE_PREFIX_REGEX);
|
|
123
|
+
return prefixed?.[1]?.toUpperCase() ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
70
126
|
function looksLikeSilkVoice(params: {
|
|
71
127
|
buffer: Buffer;
|
|
72
128
|
contentType?: string | null;
|
|
@@ -299,25 +355,56 @@ function normalizeInboundEntry(params: {
|
|
|
299
355
|
|
|
300
356
|
if (msgType === 49 && xml) {
|
|
301
357
|
const appType = extractAppMsgType(xml);
|
|
358
|
+
if (appType === 57 || /<refermsg>/i.test(xml)) {
|
|
359
|
+
return {
|
|
360
|
+
message,
|
|
361
|
+
rawBody: extractQuoteSummary(xml)?.body || resolveAppMsgPlaceholder(appType),
|
|
362
|
+
rawXml: xml,
|
|
363
|
+
appMsgXml: xml,
|
|
364
|
+
appMsgType: appType,
|
|
365
|
+
quoteXml: xml,
|
|
366
|
+
quoteDetails: extractQuoteDetails(xml),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
302
369
|
if (appType === 5) {
|
|
303
370
|
return {
|
|
304
371
|
message,
|
|
305
372
|
rawBody: resolveLinkBody(xml) || rawBodyCandidate,
|
|
373
|
+
rawXml: xml,
|
|
374
|
+
appMsgXml: xml,
|
|
375
|
+
appMsgType: appType,
|
|
306
376
|
};
|
|
307
377
|
}
|
|
308
378
|
if (appType === 74) {
|
|
309
|
-
runtime.log?.("gewe: file notification received (skip download)");
|
|
310
|
-
return
|
|
379
|
+
runtime.log?.("gewe: file notification received (preserve xml, skip download)");
|
|
380
|
+
return {
|
|
381
|
+
message,
|
|
382
|
+
rawBody: resolveAppMsgPlaceholder(appType),
|
|
383
|
+
rawXml: xml,
|
|
384
|
+
appMsgXml: xml,
|
|
385
|
+
appMsgType: appType,
|
|
386
|
+
};
|
|
311
387
|
}
|
|
312
388
|
if (appType !== 6) {
|
|
313
|
-
runtime.log?.(`gewe:
|
|
314
|
-
return
|
|
389
|
+
runtime.log?.(`gewe: preserve appmsg type ${appType ?? "unknown"} without download`);
|
|
390
|
+
return {
|
|
391
|
+
message,
|
|
392
|
+
rawBody: resolveAppMsgPlaceholder(appType),
|
|
393
|
+
rawXml: xml,
|
|
394
|
+
appMsgXml: xml,
|
|
395
|
+
appMsgType: appType,
|
|
396
|
+
};
|
|
315
397
|
}
|
|
316
398
|
}
|
|
317
399
|
|
|
318
400
|
return {
|
|
319
401
|
message,
|
|
320
402
|
rawBody: rawBodyCandidate,
|
|
403
|
+
rawXml: xml,
|
|
404
|
+
appMsgXml: msgType === 49 && xml ? xml : undefined,
|
|
405
|
+
appMsgType: msgType === 49 && xml ? extractAppMsgType(xml) : undefined,
|
|
406
|
+
quoteXml: undefined,
|
|
407
|
+
quoteDetails: undefined,
|
|
321
408
|
download:
|
|
322
409
|
(msgType === 3 || msgType === 34 || msgType === 43 || msgType === 49) && xml
|
|
323
410
|
? { msgType, xml }
|
|
@@ -448,7 +535,48 @@ async function dispatchGeweInbound(params: {
|
|
|
448
535
|
MessageSids: prepared.messageSids,
|
|
449
536
|
MessageSidFirst: prepared.messageSidFirst,
|
|
450
537
|
MessageSidLast: prepared.messageSidLast,
|
|
538
|
+
MsgType: prepared.messageType,
|
|
451
539
|
...mediaPayload,
|
|
540
|
+
...(prepared.rawXml ? { GeWeXml: prepared.rawXml } : {}),
|
|
541
|
+
...(prepared.appMsgXml ? { GeWeAppMsgXml: prepared.appMsgXml } : {}),
|
|
542
|
+
...(typeof prepared.appMsgType === "number"
|
|
543
|
+
? { GeWeAppMsgType: prepared.appMsgType }
|
|
544
|
+
: {}),
|
|
545
|
+
...(prepared.quoteXml ? { GeWeQuoteXml: prepared.quoteXml } : {}),
|
|
546
|
+
...(prepared.quoteDetails?.title ? { GeWeQuoteTitle: prepared.quoteDetails.title } : {}),
|
|
547
|
+
...(typeof prepared.quoteDetails?.referType === "number"
|
|
548
|
+
? { GeWeQuoteType: prepared.quoteDetails.referType }
|
|
549
|
+
: {}),
|
|
550
|
+
...(prepared.quoteDetails?.svrid ? { GeWeQuoteSvrid: prepared.quoteDetails.svrid } : {}),
|
|
551
|
+
...(prepared.quoteDetails?.fromUsr ? { GeWeQuoteFromUsr: prepared.quoteDetails.fromUsr } : {}),
|
|
552
|
+
...(prepared.quoteDetails?.chatUsr ? { GeWeQuoteChatUsr: prepared.quoteDetails.chatUsr } : {}),
|
|
553
|
+
...(prepared.quoteDetails?.displayName
|
|
554
|
+
? { GeWeQuoteDisplayName: prepared.quoteDetails.displayName }
|
|
555
|
+
: {}),
|
|
556
|
+
...(prepared.quoteDetails?.content
|
|
557
|
+
? { GeWeQuoteContent: prepared.quoteDetails.content }
|
|
558
|
+
: {}),
|
|
559
|
+
...(prepared.quoteDetails?.partialText?.start
|
|
560
|
+
? { GeWeQuotePartialStart: prepared.quoteDetails.partialText.start }
|
|
561
|
+
: {}),
|
|
562
|
+
...(prepared.quoteDetails?.partialText?.end
|
|
563
|
+
? { GeWeQuotePartialEnd: prepared.quoteDetails.partialText.end }
|
|
564
|
+
: {}),
|
|
565
|
+
...(typeof prepared.quoteDetails?.partialText?.startIndex === "number"
|
|
566
|
+
? { GeWeQuotePartialStartIndex: prepared.quoteDetails.partialText.startIndex }
|
|
567
|
+
: {}),
|
|
568
|
+
...(typeof prepared.quoteDetails?.partialText?.endIndex === "number"
|
|
569
|
+
? { GeWeQuotePartialEndIndex: prepared.quoteDetails.partialText.endIndex }
|
|
570
|
+
: {}),
|
|
571
|
+
...(prepared.quoteDetails?.partialText?.quoteMd5
|
|
572
|
+
? { GeWeQuotePartialQuoteMd5: prepared.quoteDetails.partialText.quoteMd5 }
|
|
573
|
+
: {}),
|
|
574
|
+
...(prepared.quoteDetails?.partialText?.text
|
|
575
|
+
? { GeWeQuotePartialText: prepared.quoteDetails.partialText.text }
|
|
576
|
+
: {}),
|
|
577
|
+
...(prepared.quoteDetails?.msgSource
|
|
578
|
+
? { GeWeQuoteMsgSource: prepared.quoteDetails.msgSource }
|
|
579
|
+
: {}),
|
|
452
580
|
GroupSystemPrompt: prepared.groupSystemPrompt,
|
|
453
581
|
OriginatingChannel: CHANNEL_ID,
|
|
454
582
|
OriginatingTo: `${CHANNEL_ID}:${prepared.toWxid}`,
|
|
@@ -463,14 +591,31 @@ async function dispatchGeweInbound(params: {
|
|
|
463
591
|
},
|
|
464
592
|
});
|
|
465
593
|
|
|
594
|
+
rememberGeweQuoteReplyContext({
|
|
595
|
+
accountId: account.accountId,
|
|
596
|
+
messageId: prepared.messageSid,
|
|
597
|
+
svrid: prepared.quoteDetails?.svrid,
|
|
598
|
+
partialText: prepared.quoteDetails?.partialText,
|
|
599
|
+
});
|
|
600
|
+
const repliedRef = { value: false };
|
|
601
|
+
|
|
466
602
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
467
603
|
ctx: ctxPayload,
|
|
468
604
|
cfg: config as OpenClawConfig,
|
|
469
|
-
replyOptions: resolveGeweReplyOptions(account
|
|
605
|
+
replyOptions: resolveGeweReplyOptions(account, {
|
|
606
|
+
skillFilter: prepared.groupSkillFilter,
|
|
607
|
+
}),
|
|
470
608
|
dispatcherOptions: {
|
|
471
609
|
deliver: async (payload: ReplyPayload) => {
|
|
610
|
+
const nextPayload = applyGeweReplyModeToPayload(payload, {
|
|
611
|
+
mode: prepared.replyMode,
|
|
612
|
+
isGroup: prepared.isGroup,
|
|
613
|
+
senderId: prepared.senderId,
|
|
614
|
+
defaultReplyToId: prepared.messageSid,
|
|
615
|
+
repliedRef,
|
|
616
|
+
});
|
|
472
617
|
await deliverGewePayload({
|
|
473
|
-
payload,
|
|
618
|
+
payload: nextPayload,
|
|
474
619
|
account,
|
|
475
620
|
cfg: config as OpenClawConfig,
|
|
476
621
|
toWxid: prepared.toWxid,
|
|
@@ -540,6 +685,12 @@ export async function handleGeweInboundBatch(params: {
|
|
|
540
685
|
}
|
|
541
686
|
|
|
542
687
|
statusSink?.({ lastInboundAt: Date.now() });
|
|
688
|
+
rememberGeweDirectoryObservation({
|
|
689
|
+
accountId: account.accountId,
|
|
690
|
+
senderId,
|
|
691
|
+
senderName,
|
|
692
|
+
groupId,
|
|
693
|
+
});
|
|
543
694
|
|
|
544
695
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
545
696
|
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
|
|
@@ -547,9 +698,14 @@ export async function handleGeweInboundBatch(params: {
|
|
|
547
698
|
|
|
548
699
|
const configAllowFrom = normalizeGeweAllowlist(account.config.allowFrom);
|
|
549
700
|
const configGroupAllowFrom = normalizeGeweAllowlist(account.config.groupAllowFrom);
|
|
550
|
-
const storeAllowFrom = await
|
|
551
|
-
.
|
|
552
|
-
|
|
701
|
+
const storeAllowFrom = await readGeweAllowFromStore({
|
|
702
|
+
accountId: account.accountId,
|
|
703
|
+
}).catch((err) => {
|
|
704
|
+
runtime.error?.(
|
|
705
|
+
`gewe: failed reading local allowFrom store for ${account.accountId}: ${String(err)}`,
|
|
706
|
+
);
|
|
707
|
+
return [];
|
|
708
|
+
});
|
|
553
709
|
const storeAllowList = normalizeGeweAllowlist(storeAllowFrom);
|
|
554
710
|
|
|
555
711
|
const groupMatch = isGroup
|
|
@@ -559,17 +715,27 @@ export async function handleGeweInboundBatch(params: {
|
|
|
559
715
|
groupName: undefined,
|
|
560
716
|
})
|
|
561
717
|
: undefined;
|
|
718
|
+
const dmMatch = !isGroup
|
|
719
|
+
? resolveGeweDmMatch({
|
|
720
|
+
dms: account.config.dms,
|
|
721
|
+
senderId,
|
|
722
|
+
senderName,
|
|
723
|
+
})
|
|
724
|
+
: undefined;
|
|
562
725
|
|
|
563
726
|
if (isGroup && groupMatch && !groupMatch.allowed) {
|
|
564
727
|
runtime.log?.(`gewe: drop group ${groupId} (not allowlisted)`);
|
|
565
728
|
return;
|
|
566
729
|
}
|
|
567
|
-
if (groupMatch?.groupConfig?.enabled === false) {
|
|
730
|
+
if (groupMatch?.groupConfig?.enabled === false || groupMatch?.wildcardConfig?.enabled === false) {
|
|
568
731
|
runtime.log?.(`gewe: drop group ${groupId} (disabled)`);
|
|
569
732
|
return;
|
|
570
733
|
}
|
|
571
734
|
|
|
572
|
-
const
|
|
735
|
+
const directRoomAllowFrom = normalizeGeweAllowlist(groupMatch?.groupConfig?.allowFrom);
|
|
736
|
+
const wildcardRoomAllowFrom = normalizeGeweAllowlist(groupMatch?.wildcardConfig?.allowFrom);
|
|
737
|
+
const roomAllowFrom =
|
|
738
|
+
directRoomAllowFrom.length > 0 ? directRoomAllowFrom : wildcardRoomAllowFrom;
|
|
573
739
|
const baseGroupAllowFrom =
|
|
574
740
|
configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
|
|
575
741
|
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowList].filter(Boolean);
|
|
@@ -627,26 +793,28 @@ export async function handleGeweInboundBatch(params: {
|
|
|
627
793
|
}).allowed;
|
|
628
794
|
if (!dmAllowed) {
|
|
629
795
|
if (dmPolicy === "pairing") {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
796
|
+
const pairCode = resolveGewePairCodeCandidate(rawBodyCandidate);
|
|
797
|
+
if (pairCode) {
|
|
798
|
+
const redeemed = await redeemGewePairCode({
|
|
799
|
+
accountId: account.accountId,
|
|
800
|
+
code: pairCode,
|
|
801
|
+
id: senderId,
|
|
802
|
+
}).catch((err) => {
|
|
803
|
+
runtime.error?.(`gewe: pair code redeem failed for ${senderId}: ${String(err)}`);
|
|
804
|
+
return null;
|
|
805
|
+
});
|
|
636
806
|
try {
|
|
637
807
|
await deliverGewePayload({
|
|
638
|
-
payload: {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
code,
|
|
642
|
-
}) },
|
|
808
|
+
payload: {
|
|
809
|
+
text: redeemed ? GEWE_PAIR_CODE_SUCCESS_REPLY : GEWE_PAIR_CODE_INVALID_REPLY,
|
|
810
|
+
},
|
|
643
811
|
account,
|
|
644
812
|
cfg: config as OpenClawConfig,
|
|
645
813
|
toWxid,
|
|
646
814
|
statusSink: (patch) => statusSink?.(patch),
|
|
647
815
|
});
|
|
648
816
|
} catch (err) {
|
|
649
|
-
runtime.error?.(`gewe:
|
|
817
|
+
runtime.error?.(`gewe: pair code reply failed for ${senderId}: ${String(err)}`);
|
|
650
818
|
}
|
|
651
819
|
}
|
|
652
820
|
}
|
|
@@ -667,25 +835,38 @@ export async function handleGeweInboundBatch(params: {
|
|
|
667
835
|
}
|
|
668
836
|
|
|
669
837
|
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config as OpenClawConfig);
|
|
670
|
-
const
|
|
838
|
+
const wasAtTriggered = mentionRegexes.length
|
|
671
839
|
? core.channel.mentions.matchesMentionPatterns(rawBodyCandidate, mentionRegexes)
|
|
672
840
|
: false;
|
|
673
|
-
const
|
|
674
|
-
|
|
841
|
+
const latestQuote = entries.at(-1)?.quoteDetails;
|
|
842
|
+
const wasQuoteTriggered = Boolean(
|
|
843
|
+
latestQuote &&
|
|
844
|
+
(!isGroup || latestQuote.fromUsr?.trim() === lastMessage.botWxid.trim()),
|
|
845
|
+
);
|
|
846
|
+
const triggerMode = isGroup
|
|
847
|
+
? resolveGeweGroupTriggerMode({
|
|
675
848
|
groupConfig: groupMatch?.groupConfig,
|
|
676
849
|
wildcardConfig: groupMatch?.wildcardConfig,
|
|
677
850
|
})
|
|
678
|
-
:
|
|
679
|
-
|
|
851
|
+
: resolveGeweDmTriggerMode({
|
|
852
|
+
dmConfig: dmMatch?.dmConfig,
|
|
853
|
+
wildcardConfig: dmMatch?.wildcardConfig,
|
|
854
|
+
});
|
|
855
|
+
const triggerGate = resolveGeweTriggerGate({
|
|
680
856
|
isGroup,
|
|
681
|
-
|
|
682
|
-
|
|
857
|
+
triggerMode,
|
|
858
|
+
wasAtTriggered,
|
|
859
|
+
wasQuoteTriggered,
|
|
683
860
|
allowTextCommands,
|
|
684
861
|
hasControlCommand,
|
|
685
862
|
commandAuthorized,
|
|
686
863
|
});
|
|
687
|
-
if (
|
|
688
|
-
runtime.log?.(
|
|
864
|
+
if (triggerGate.shouldSkip) {
|
|
865
|
+
runtime.log?.(
|
|
866
|
+
isGroup
|
|
867
|
+
? `gewe: drop group ${groupId} (trigger=${triggerMode})`
|
|
868
|
+
: `gewe: drop DM sender ${senderId} (trigger=${triggerMode})`,
|
|
869
|
+
);
|
|
689
870
|
return;
|
|
690
871
|
}
|
|
691
872
|
|
|
@@ -694,7 +875,7 @@ export async function handleGeweInboundBatch(params: {
|
|
|
694
875
|
channel: CHANNEL_ID,
|
|
695
876
|
accountId: account.accountId,
|
|
696
877
|
peer: {
|
|
697
|
-
kind: isGroup ? "group" : "
|
|
878
|
+
kind: isGroup ? "group" : "direct",
|
|
698
879
|
id: isGroup ? groupId ?? "" : senderId,
|
|
699
880
|
},
|
|
700
881
|
});
|
|
@@ -705,13 +886,39 @@ export async function handleGeweInboundBatch(params: {
|
|
|
705
886
|
|
|
706
887
|
const prepared: PreparedInbound = {
|
|
707
888
|
rawBody: rawBodyCandidate,
|
|
889
|
+
messageType: lastMessage.msgType,
|
|
890
|
+
rawXml: entries.at(-1)?.rawXml,
|
|
891
|
+
appMsgXml: entries.at(-1)?.appMsgXml,
|
|
892
|
+
appMsgType: entries.at(-1)?.appMsgType,
|
|
893
|
+
quoteXml: entries.at(-1)?.quoteXml,
|
|
894
|
+
quoteDetails: entries.at(-1)?.quoteDetails,
|
|
708
895
|
commandAuthorized,
|
|
709
896
|
isGroup,
|
|
710
897
|
senderId,
|
|
711
898
|
senderName: senderName || undefined,
|
|
712
899
|
groupId,
|
|
713
900
|
groupName: undefined,
|
|
714
|
-
groupSystemPrompt:
|
|
901
|
+
groupSystemPrompt: isGroup
|
|
902
|
+
? groupMatch?.groupConfig?.systemPrompt?.trim() ||
|
|
903
|
+
groupMatch?.wildcardConfig?.systemPrompt?.trim() ||
|
|
904
|
+
undefined
|
|
905
|
+
: dmMatch?.dmConfig?.systemPrompt?.trim() ||
|
|
906
|
+
dmMatch?.wildcardConfig?.systemPrompt?.trim() ||
|
|
907
|
+
undefined,
|
|
908
|
+
groupSkillFilter: isGroup
|
|
909
|
+
? groupMatch?.groupConfig?.skills ?? groupMatch?.wildcardConfig?.skills
|
|
910
|
+
: dmMatch?.dmConfig?.skills ?? dmMatch?.wildcardConfig?.skills,
|
|
911
|
+
replyMode: isGroup
|
|
912
|
+
? resolveGeweGroupReplyMode({
|
|
913
|
+
groupConfig: groupMatch?.groupConfig,
|
|
914
|
+
wildcardConfig: groupMatch?.wildcardConfig,
|
|
915
|
+
autoQuoteReply: account.config.autoQuoteReply,
|
|
916
|
+
})
|
|
917
|
+
: resolveGeweDmReplyMode({
|
|
918
|
+
dmConfig: dmMatch?.dmConfig,
|
|
919
|
+
wildcardConfig: dmMatch?.wildcardConfig,
|
|
920
|
+
autoQuoteReply: account.config.autoQuoteReply,
|
|
921
|
+
}),
|
|
715
922
|
route,
|
|
716
923
|
storePath,
|
|
717
924
|
toWxid,
|
package/src/media-server.ts
CHANGED
|
@@ -1,59 +1,23 @@
|
|
|
1
|
-
import { createReadStream
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
|
|
7
|
-
import { detectMime } from "openclaw
|
|
6
|
+
import { detectMime } from "./openclaw-compat.js";
|
|
7
|
+
import { resolveOpenClawStateDir } from "./state-paths.js";
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_MEDIA_HOST = "0.0.0.0";
|
|
10
10
|
export const DEFAULT_MEDIA_PORT = 18787;
|
|
11
11
|
export const DEFAULT_MEDIA_PATH = "/gewe-media";
|
|
12
12
|
|
|
13
|
-
function
|
|
13
|
+
export function normalizeMediaPath(value: string): string {
|
|
14
14
|
const trimmed = value.trim() || "/";
|
|
15
15
|
if (trimmed === "/") return "/";
|
|
16
16
|
return trimmed.startsWith("/") ? trimmed.replace(/\/+$/, "") : `/${trimmed.replace(/\/+$/, "")}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function resolveUserPath(input: string): string {
|
|
20
|
-
const trimmed = input.trim();
|
|
21
|
-
if (!trimmed) return trimmed;
|
|
22
|
-
if (trimmed.startsWith("~")) {
|
|
23
|
-
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
24
|
-
return path.resolve(expanded);
|
|
25
|
-
}
|
|
26
|
-
return path.resolve(trimmed);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function resolveConfigDir(
|
|
30
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
31
|
-
homedir: () => string = os.homedir,
|
|
32
|
-
): string {
|
|
33
|
-
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
|
|
34
|
-
if (override) return resolveUserPath(override);
|
|
35
|
-
const legacyDirs = [".clawdbot", ".moltbot", ".moldbot"].map((dir) =>
|
|
36
|
-
path.join(homedir(), dir),
|
|
37
|
-
);
|
|
38
|
-
const newDir = path.join(homedir(), ".openclaw");
|
|
39
|
-
try {
|
|
40
|
-
if (existsSync(newDir)) return newDir;
|
|
41
|
-
const existingLegacy = legacyDirs.find((dir) => {
|
|
42
|
-
try {
|
|
43
|
-
return existsSync(dir);
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
if (existingLegacy) return existingLegacy;
|
|
49
|
-
} catch {
|
|
50
|
-
// best-effort
|
|
51
|
-
}
|
|
52
|
-
return newDir;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
19
|
function resolveMediaDir() {
|
|
56
|
-
return path.join(
|
|
20
|
+
return path.join(resolveOpenClawStateDir(), "media");
|
|
57
21
|
}
|
|
58
22
|
|
|
59
23
|
function resolveBaseUrl(req: IncomingMessage): string {
|
|
@@ -74,67 +38,83 @@ export type GeweMediaServerOptions = {
|
|
|
74
38
|
abortSignal?: AbortSignal;
|
|
75
39
|
};
|
|
76
40
|
|
|
41
|
+
export async function maybeHandleGeweMediaRequest(params: {
|
|
42
|
+
req: IncomingMessage;
|
|
43
|
+
res: ServerResponse;
|
|
44
|
+
path?: string;
|
|
45
|
+
mediaBaseDir?: string;
|
|
46
|
+
}): Promise<boolean> {
|
|
47
|
+
if (!params.req.url) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const basePath = normalizeMediaPath(params.path ?? DEFAULT_MEDIA_PATH);
|
|
52
|
+
const url = new URL(params.req.url, resolveBaseUrl(params.req));
|
|
53
|
+
if (!url.pathname.startsWith(`${basePath}/`)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (params.req.method !== "GET" && params.req.method !== "HEAD") {
|
|
58
|
+
params.res.writeHead(405);
|
|
59
|
+
params.res.end();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const id = decodeURIComponent(url.pathname.slice(basePath.length + 1));
|
|
64
|
+
if (!isSafeMediaId(id)) {
|
|
65
|
+
params.res.writeHead(400);
|
|
66
|
+
params.res.end();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mediaBaseDir = params.mediaBaseDir ?? path.join(resolveMediaDir(), "outbound");
|
|
71
|
+
const filePath = path.join(mediaBaseDir, id);
|
|
72
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
73
|
+
if (!stat || !stat.isFile()) {
|
|
74
|
+
params.res.writeHead(404);
|
|
75
|
+
params.res.end();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const contentType = await detectMime({ filePath }).catch(() => undefined);
|
|
80
|
+
const headers: Record<string, string> = {
|
|
81
|
+
"Content-Length": String(stat.size),
|
|
82
|
+
"Cache-Control": "private, max-age=60",
|
|
83
|
+
};
|
|
84
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
85
|
+
|
|
86
|
+
params.res.writeHead(200, headers);
|
|
87
|
+
if (params.req.method === "HEAD") {
|
|
88
|
+
params.res.end();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const stream = createReadStream(filePath);
|
|
93
|
+
stream.on("error", () => {
|
|
94
|
+
if (!params.res.headersSent) params.res.writeHead(500);
|
|
95
|
+
params.res.end();
|
|
96
|
+
});
|
|
97
|
+
stream.pipe(params.res);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
export function createGeweMediaServer(
|
|
78
102
|
opts: GeweMediaServerOptions,
|
|
79
103
|
): { server: Server; start: () => Promise<void>; stop: () => void } {
|
|
80
104
|
const host = opts.host ?? DEFAULT_MEDIA_HOST;
|
|
81
105
|
const port = opts.port ?? DEFAULT_MEDIA_PORT;
|
|
82
|
-
const basePath = normalizePath(opts.path ?? DEFAULT_MEDIA_PATH);
|
|
83
106
|
const mediaBaseDir = path.join(resolveMediaDir(), "outbound");
|
|
84
107
|
|
|
85
108
|
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
res
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
92
|
-
res.writeHead(405);
|
|
93
|
-
res.end();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const url = new URL(req.url, resolveBaseUrl(req));
|
|
98
|
-
if (!url.pathname.startsWith(`${basePath}/`)) {
|
|
99
|
-
res.writeHead(404);
|
|
100
|
-
res.end();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const id = decodeURIComponent(url.pathname.slice(basePath.length + 1));
|
|
105
|
-
if (!isSafeMediaId(id)) {
|
|
106
|
-
res.writeHead(400);
|
|
107
|
-
res.end();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const filePath = path.join(mediaBaseDir, id);
|
|
112
|
-
const stat = await fs.stat(filePath).catch(() => null);
|
|
113
|
-
if (!stat || !stat.isFile()) {
|
|
114
|
-
res.writeHead(404);
|
|
115
|
-
res.end();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const contentType = await detectMime({ filePath }).catch(() => undefined);
|
|
120
|
-
const headers: Record<string, string> = {
|
|
121
|
-
"Content-Length": String(stat.size),
|
|
122
|
-
"Cache-Control": "private, max-age=60",
|
|
123
|
-
};
|
|
124
|
-
if (contentType) headers["Content-Type"] = contentType;
|
|
125
|
-
|
|
126
|
-
res.writeHead(200, headers);
|
|
127
|
-
if (req.method === "HEAD") {
|
|
128
|
-
res.end();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const stream = createReadStream(filePath);
|
|
133
|
-
stream.on("error", () => {
|
|
134
|
-
if (!res.headersSent) res.writeHead(500);
|
|
135
|
-
res.end();
|
|
109
|
+
const handled = await maybeHandleGeweMediaRequest({
|
|
110
|
+
req,
|
|
111
|
+
res,
|
|
112
|
+
path: opts.path,
|
|
113
|
+
mediaBaseDir,
|
|
136
114
|
});
|
|
137
|
-
|
|
115
|
+
if (handled) return;
|
|
116
|
+
res.writeHead(404);
|
|
117
|
+
res.end();
|
|
138
118
|
});
|
|
139
119
|
|
|
140
120
|
const start = (): Promise<void> =>
|