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.
Files changed (44) hide show
  1. package/README.md +455 -3
  2. package/index.ts +39 -1
  3. package/package.json +12 -1
  4. package/skills/gewe-agent-tools/SKILL.md +113 -0
  5. package/skills/gewe-channel-rules/SKILL.md +7 -0
  6. package/src/accounts.ts +51 -5
  7. package/src/api-tools.ts +1264 -0
  8. package/src/api.ts +37 -2
  9. package/src/binary-command.ts +65 -0
  10. package/src/channel-actions.ts +536 -0
  11. package/src/channel-allowlist.ts +150 -0
  12. package/src/channel-directory.ts +419 -0
  13. package/src/channel-status.ts +186 -0
  14. package/src/channel.ts +155 -58
  15. package/src/config-edit.ts +94 -0
  16. package/src/config-schema.ts +78 -3
  17. package/src/contacts-api.ts +113 -0
  18. package/src/delivery.ts +502 -62
  19. package/src/directory-cache.ts +164 -0
  20. package/src/gewe-account-api.ts +27 -0
  21. package/src/group-allowlist-tool.ts +242 -0
  22. package/src/group-binding-tool.ts +154 -0
  23. package/src/group-binding.ts +405 -0
  24. package/src/groups-api.ts +146 -0
  25. package/src/inbound-batch.ts +5 -2
  26. package/src/inbound.ts +248 -41
  27. package/src/media-server.ts +73 -93
  28. package/src/moments-api.ts +138 -0
  29. package/src/monitor.ts +81 -24
  30. package/src/onboarding.ts +9 -4
  31. package/src/openclaw-compat.ts +1070 -0
  32. package/src/pairing-store.ts +478 -0
  33. package/src/personal-api.ts +45 -0
  34. package/src/policy.ts +130 -22
  35. package/src/quote-context-cache.ts +97 -0
  36. package/src/reply-options.ts +101 -2
  37. package/src/s3.ts +1 -1
  38. package/src/send.ts +235 -16
  39. package/src/setup-wizard-types.ts +162 -0
  40. package/src/setup-wizard.ts +464 -0
  41. package/src/silk.ts +2 -1
  42. package/src/state-paths.ts +55 -14
  43. package/src/types.ts +66 -7
  44. 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 type { OpenClawConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk";
6
- import { logInboundDrop, resolveControlCommandGate } from "openclaw/plugin-sdk";
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
- resolveGeweMentionGate,
24
- resolveGeweRequireMention,
33
+ resolveGeweGroupReplyMode,
34
+ resolveGeweGroupTriggerMode,
35
+ resolveGeweTriggerGate,
25
36
  } from "./policy.js";
26
- import type { CoreConfig, GeweInboundMessage, ResolvedGeweAccount } from "./types.js";
27
- import { extractAppMsgType, extractFileName, extractLinkDetails } from "./xml.js";
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 null;
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: unhandled appmsg type ${appType ?? "unknown"}`);
314
- return null;
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 core.channel.pairing
551
- .readAllowFromStore(CHANNEL_ID)
552
- .catch(() => []);
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 roomAllowFrom = normalizeGeweAllowlist(groupMatch?.groupConfig?.allowFrom);
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 { code, created } = await core.channel.pairing.upsertPairingRequest({
631
- channel: CHANNEL_ID,
632
- id: senderId,
633
- meta: { name: senderName || undefined },
634
- });
635
- if (created) {
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: { text: core.channel.pairing.buildPairingReply({
639
- channel: CHANNEL_ID,
640
- idLine: `Your WeChat id: ${senderId}`,
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: pairing reply failed for ${senderId}: ${String(err)}`);
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 wasMentioned = mentionRegexes.length
838
+ const wasAtTriggered = mentionRegexes.length
671
839
  ? core.channel.mentions.matchesMentionPatterns(rawBodyCandidate, mentionRegexes)
672
840
  : false;
673
- const shouldRequireMention = isGroup
674
- ? resolveGeweRequireMention({
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
- : false;
679
- const mentionGate = resolveGeweMentionGate({
851
+ : resolveGeweDmTriggerMode({
852
+ dmConfig: dmMatch?.dmConfig,
853
+ wildcardConfig: dmMatch?.wildcardConfig,
854
+ });
855
+ const triggerGate = resolveGeweTriggerGate({
680
856
  isGroup,
681
- requireMention: shouldRequireMention,
682
- wasMentioned,
857
+ triggerMode,
858
+ wasAtTriggered,
859
+ wasQuoteTriggered,
683
860
  allowTextCommands,
684
861
  hasControlCommand,
685
862
  commandAuthorized,
686
863
  });
687
- if (isGroup && mentionGate.shouldSkip) {
688
- runtime.log?.(`gewe: drop group ${groupId} (no mention)`);
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" : "dm",
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: groupMatch?.groupConfig?.systemPrompt?.trim() || undefined,
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,
@@ -1,59 +1,23 @@
1
- import { createReadStream, existsSync } from "node:fs";
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/plugin-sdk";
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 normalizePath(value: string): string {
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(resolveConfigDir(), "media");
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
- if (!req.url) {
87
- res.writeHead(404);
88
- res.end();
89
- return;
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
- stream.pipe(res);
115
+ if (handled) return;
116
+ res.writeHead(404);
117
+ res.end();
138
118
  });
139
119
 
140
120
  const start = (): Promise<void> =>