@agent-wechat/wechat 0.8.0 → 0.8.2

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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +360 -69
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -146,8 +146,8 @@ All config lives under `channels.wechat` in OpenClaw's config file:
146
146
  | `dmPolicy` | `"open" \| "allowlist" \| "disabled"` | `"disabled"` | Who can DM the bot |
147
147
  | `allowFrom` | string[] | `[]` | wxid allowlist for DMs (when policy is `allowlist`) |
148
148
  | `groupPolicy` | `"open" \| "allowlist" \| "disabled"` | `"disabled"` | Group message policy |
149
- | `groupAllowFrom` | string[] | `[]` | wxid allowlist for group senders |
150
- | `groups` | object | `{}` | Per-group overrides (e.g. `{ "id@chatroom": { "requireMention": false } }`) |
149
+ | `groupAllowFrom` | string[] | `[]` | Global allowlist of group sender IDs (`wxid_...`) |
150
+ | `groups` | object | `{}` | Per-group overrides (e.g. `{ "id@chatroom": { "requireMention": false, "enabled": true, "groupPolicy": "allowlist", "allowFrom": ["wxid_..."] } }`) |
151
151
  | `pollIntervalMs` | integer | `1000` | Message polling interval |
152
152
  | `authPollIntervalMs` | integer | `30000` | Auth status check interval |
153
153
 
package/dist/index.js CHANGED
@@ -1085,6 +1085,12 @@ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1085
1085
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID2 } from "openclaw/plugin-sdk";
1086
1086
 
1087
1087
  // src/types.ts
1088
+ function normalizeDmPolicy(policy) {
1089
+ return policy === "allowlist" || policy === "open" || policy === "disabled" ? policy : "disabled";
1090
+ }
1091
+ function normalizeGroupPolicy(policy) {
1092
+ return policy === "open" || policy === "disabled" || policy === "allowlist" ? policy : "disabled";
1093
+ }
1088
1094
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
1089
1095
  var DEFAULT_AUTH_POLL_INTERVAL_MS = 3e4;
1090
1096
  var DEFAULT_ACCOUNT_ID = "default";
@@ -1096,9 +1102,9 @@ function resolveWeChatAccount(cfg, accountId) {
1096
1102
  enabled: wechat.enabled !== false,
1097
1103
  serverUrl: wechat.serverUrl,
1098
1104
  token: wechat.token,
1099
- dmPolicy: wechat.dmPolicy ?? "disabled",
1105
+ dmPolicy: normalizeDmPolicy(wechat.dmPolicy),
1100
1106
  allowFrom: wechat.allowFrom ?? [],
1101
- groupPolicy: wechat.groupPolicy ?? "disabled",
1107
+ groupPolicy: normalizeGroupPolicy(wechat.groupPolicy),
1102
1108
  groupAllowFrom: wechat.groupAllowFrom ?? [],
1103
1109
  groups: wechat.groups ?? {},
1104
1110
  pollIntervalMs: wechat.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
@@ -5505,6 +5511,208 @@ var agentConfigSchema = external_exports.object({
5505
5511
 
5506
5512
  // src/monitor.ts
5507
5513
  import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
5514
+
5515
+ // src/access-control.ts
5516
+ import {
5517
+ buildChannelKeyCandidates,
5518
+ resolveAllowlistProviderRuntimeGroupPolicy,
5519
+ resolveChannelEntryMatchWithFallback,
5520
+ resolveDefaultGroupPolicy,
5521
+ resolveSenderCommandAuthorization
5522
+ } from "openclaw/plugin-sdk";
5523
+ var INVISIBLE_TEXT_RE = /[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g;
5524
+ var MENTION_SEPARATOR_RE = /[\s\u2005]+/u;
5525
+ var WECHAT_MENTION_TOKEN_RE = /^[@@][^\s\u2005]+$/u;
5526
+ function unique(values) {
5527
+ return Array.from(new Set(values));
5528
+ }
5529
+ function normalizeDmPolicy2(policy) {
5530
+ if (policy === "open" || policy === "allowlist" || policy === "disabled") {
5531
+ return policy;
5532
+ }
5533
+ return "disabled";
5534
+ }
5535
+ function normalizeGroupPolicy2(policy) {
5536
+ if (policy === "open" || policy === "allowlist" || policy === "disabled") {
5537
+ return policy;
5538
+ }
5539
+ return void 0;
5540
+ }
5541
+ function firstDefined(...values) {
5542
+ for (const value of values) {
5543
+ if (value !== void 0) {
5544
+ return value;
5545
+ }
5546
+ }
5547
+ return void 0;
5548
+ }
5549
+ function normalizeWeChatId(raw) {
5550
+ const trimmed = raw.trim();
5551
+ if (!trimmed) {
5552
+ return "";
5553
+ }
5554
+ return trimmed.replace(/^wechat:/i, "").trim();
5555
+ }
5556
+ function normalizeWeChatAllowFrom(values) {
5557
+ const normalized = (values ?? []).map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry === "*" ? "*" : normalizeWeChatId(entry)).filter(Boolean);
5558
+ return unique(normalized);
5559
+ }
5560
+ function findCommandTokenStart(input) {
5561
+ const match = /(?:^|\s)([/!][A-Za-z])/u.exec(input);
5562
+ if (!match) {
5563
+ return -1;
5564
+ }
5565
+ const whole = match[0] ?? "";
5566
+ const startsWithSpace = whole.startsWith(" ");
5567
+ return (match.index ?? 0) + (startsWithSpace ? 1 : 0);
5568
+ }
5569
+ function normalizeWeChatCommandBody(raw, params) {
5570
+ const trimmed = raw.replace(INVISIBLE_TEXT_RE, "").trim();
5571
+ if (!trimmed) {
5572
+ return "";
5573
+ }
5574
+ const isGroup = params?.isGroup === true;
5575
+ const wasMentioned = params?.wasMentioned === true;
5576
+ if (!isGroup || !wasMentioned) {
5577
+ return trimmed;
5578
+ }
5579
+ const commandStart = findCommandTokenStart(trimmed);
5580
+ if (commandStart < 0) {
5581
+ return trimmed;
5582
+ }
5583
+ const prefix = trimmed.slice(0, commandStart).trim();
5584
+ if (!prefix) {
5585
+ return trimmed.slice(commandStart).trimStart();
5586
+ }
5587
+ const prefixTokens = prefix.split(MENTION_SEPARATOR_RE).map((token) => token.trim()).filter(Boolean);
5588
+ if (prefixTokens.length > 0 && prefixTokens.every((token) => WECHAT_MENTION_TOKEN_RE.test(token))) {
5589
+ return trimmed.slice(commandStart).trimStart();
5590
+ }
5591
+ return trimmed;
5592
+ }
5593
+ function isWeChatSenderAllowed(senderId, allowFrom) {
5594
+ if (allowFrom.includes("*")) {
5595
+ return true;
5596
+ }
5597
+ const normalizedSender = senderId ? normalizeWeChatId(senderId) : "";
5598
+ if (!normalizedSender) {
5599
+ return false;
5600
+ }
5601
+ return allowFrom.includes(normalizedSender);
5602
+ }
5603
+ function resolveGroupEntry(params) {
5604
+ const groups = params.account.groups ?? {};
5605
+ const normalizedChatId = normalizeWeChatId(params.chatId);
5606
+ const keys = buildChannelKeyCandidates(params.chatId, normalizedChatId);
5607
+ const match = resolveChannelEntryMatchWithFallback({
5608
+ entries: groups,
5609
+ keys,
5610
+ wildcardKey: "*",
5611
+ normalizeKey: normalizeWeChatId
5612
+ });
5613
+ return {
5614
+ groupEntry: match.entry,
5615
+ wildcardEntry: match.wildcardEntry
5616
+ };
5617
+ }
5618
+ function resolveWeChatPolicyContext(params) {
5619
+ const dmPolicy = normalizeDmPolicy2(params.account.dmPolicy);
5620
+ const configuredAllowFrom = normalizeWeChatAllowFrom(params.account.allowFrom);
5621
+ const configuredGroupAllowFrom = normalizeWeChatAllowFrom(params.account.groupAllowFrom);
5622
+ const normalizedStoreAllowFrom = dmPolicy === "allowlist" ? [] : normalizeWeChatAllowFrom(params.storeAllowFrom);
5623
+ const effectiveAllowFrom = unique([...configuredAllowFrom, ...normalizedStoreAllowFrom]);
5624
+ const groupBase = configuredGroupAllowFrom.length > 0 ? configuredGroupAllowFrom : configuredAllowFrom;
5625
+ const effectiveGroupAllowFrom = unique([...groupBase, ...normalizedStoreAllowFrom]);
5626
+ const { groupEntry, wildcardEntry } = resolveGroupEntry({
5627
+ account: params.account,
5628
+ chatId: params.chatId
5629
+ });
5630
+ const groupEnabled = firstDefined(groupEntry?.enabled, wildcardEntry?.enabled, true) !== false;
5631
+ const requireMention = firstDefined(groupEntry?.requireMention, wildcardEntry?.requireMention, true) !== false;
5632
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
5633
+ const { groupPolicy: fallbackGroupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
5634
+ providerConfigPresent: params.cfg.channels?.wechat !== void 0,
5635
+ groupPolicy: normalizeGroupPolicy2(params.account.groupPolicy),
5636
+ defaultGroupPolicy: normalizeGroupPolicy2(defaultGroupPolicy)
5637
+ });
5638
+ const groupPolicy = normalizeGroupPolicy2(
5639
+ firstDefined(
5640
+ groupEntry?.groupPolicy,
5641
+ wildcardEntry?.groupPolicy,
5642
+ params.account.groupPolicy,
5643
+ defaultGroupPolicy
5644
+ )
5645
+ ) ?? fallbackGroupPolicy;
5646
+ const groupAllowOverride = normalizeWeChatAllowFrom(
5647
+ firstDefined(groupEntry?.allowFrom, wildcardEntry?.allowFrom)
5648
+ );
5649
+ const groupAllowFrom = groupAllowOverride.length > 0 ? unique([...groupAllowOverride, ...normalizedStoreAllowFrom]) : effectiveGroupAllowFrom;
5650
+ return {
5651
+ dmPolicy,
5652
+ groupPolicy,
5653
+ requireMention,
5654
+ groupEnabled,
5655
+ effectiveAllowFrom,
5656
+ effectiveGroupAllowFrom: groupAllowFrom
5657
+ };
5658
+ }
5659
+ function resolveWeChatInboundAccessDecision(params) {
5660
+ if (params.isGroup) {
5661
+ if (!params.policy.groupEnabled) {
5662
+ return { allowed: false, reason: "group-config-disabled" };
5663
+ }
5664
+ if (params.policy.groupPolicy === "disabled") {
5665
+ return { allowed: false, reason: "groupPolicy=disabled" };
5666
+ }
5667
+ if (params.policy.groupPolicy === "allowlist") {
5668
+ if (params.policy.effectiveGroupAllowFrom.length === 0) {
5669
+ return { allowed: false, reason: "groupPolicy=allowlist (empty allowlist)" };
5670
+ }
5671
+ if (!isWeChatSenderAllowed(params.senderId, params.policy.effectiveGroupAllowFrom)) {
5672
+ return { allowed: false, reason: "groupPolicy=allowlist (sender not allowlisted)" };
5673
+ }
5674
+ }
5675
+ return { allowed: true, reason: `groupPolicy=${params.policy.groupPolicy}` };
5676
+ }
5677
+ if (params.policy.dmPolicy === "disabled") {
5678
+ return { allowed: false, reason: "dmPolicy=disabled" };
5679
+ }
5680
+ if (params.policy.dmPolicy === "allowlist") {
5681
+ if (params.policy.effectiveAllowFrom.length === 0) {
5682
+ return { allowed: false, reason: "dmPolicy=allowlist (empty allowlist)" };
5683
+ }
5684
+ if (!isWeChatSenderAllowed(params.senderId, params.policy.effectiveAllowFrom)) {
5685
+ return { allowed: false, reason: "dmPolicy=allowlist (sender not allowlisted)" };
5686
+ }
5687
+ }
5688
+ return { allowed: true, reason: `dmPolicy=${params.policy.dmPolicy}` };
5689
+ }
5690
+ async function resolveWeChatCommandAuthorization(params) {
5691
+ const normalizedSenderId = normalizeWeChatId(params.senderId ?? "");
5692
+ const { commandAuthorized } = await resolveSenderCommandAuthorization({
5693
+ cfg: params.cfg,
5694
+ rawBody: params.rawBody,
5695
+ isGroup: params.isGroup,
5696
+ dmPolicy: params.dmPolicy,
5697
+ configuredAllowFrom: params.allowFromForCommands,
5698
+ senderId: normalizedSenderId,
5699
+ isSenderAllowed: (senderId, allowFrom) => isWeChatSenderAllowed(senderId, normalizeWeChatAllowFrom(allowFrom)),
5700
+ readAllowFromStore: async () => normalizeWeChatAllowFrom(await params.deps.readAllowFromStore()),
5701
+ shouldComputeCommandAuthorized: params.deps.shouldComputeCommandAuthorized,
5702
+ resolveCommandAuthorizedFromAuthorizers: params.deps.resolveCommandAuthorizedFromAuthorizers
5703
+ });
5704
+ return commandAuthorized;
5705
+ }
5706
+ function resolveWeChatMentionGate(params) {
5707
+ const implicitMention = params.implicitMention === true;
5708
+ const baseWasMentioned = params.wasMentioned || implicitMention;
5709
+ const shouldBypassMention = params.isGroup && params.requireMention && !baseWasMentioned && params.allowTextCommands && params.hasControlCommand && params.commandAuthorized;
5710
+ const effectiveWasMentioned = baseWasMentioned || shouldBypassMention;
5711
+ const shouldSkip = params.requireMention && params.canDetectMention && !effectiveWasMentioned;
5712
+ return { effectiveWasMentioned, shouldSkip, shouldBypassMention };
5713
+ }
5714
+
5715
+ // src/monitor.ts
5508
5716
  var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34]);
5509
5717
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
5510
5718
  var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
@@ -5533,20 +5741,6 @@ async function pollMedia(client, chatId, localId, log, maxAttempts = 15, interva
5533
5741
  }
5534
5742
  return null;
5535
5743
  }
5536
- function isMessageAllowed(account, isGroup, senderId) {
5537
- if (isGroup) {
5538
- if (account.groupPolicy === "disabled") return false;
5539
- if (account.groupPolicy === "allowlist") {
5540
- return account.groupAllowFrom.includes(senderId);
5541
- }
5542
- return true;
5543
- }
5544
- if (account.dmPolicy === "disabled") return false;
5545
- if (account.dmPolicy === "allowlist") {
5546
- return account.allowFrom.includes(senderId);
5547
- }
5548
- return true;
5549
- }
5550
5744
  function enqueueWeChatSystemEvent(text, contextKey) {
5551
5745
  try {
5552
5746
  const core = getWeChatRuntime();
@@ -5684,7 +5878,7 @@ async function startWeChatMonitor(opts) {
5684
5878
  connected: false
5685
5879
  });
5686
5880
  }
5687
- async function prepareMessage(client, msg, chatId, chat, liveAccount, log) {
5881
+ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, log) {
5688
5882
  const core = getWeChatRuntime();
5689
5883
  if (msg.isSelf) {
5690
5884
  log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
@@ -5693,8 +5887,16 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, log) {
5693
5887
  const isGroup = chatId.includes("@chatroom");
5694
5888
  const senderId = msg.sender ?? chatId;
5695
5889
  const senderName = msg.senderName ?? msg.sender ?? chat.name;
5696
- if (!isMessageAllowed(liveAccount, isGroup, senderId)) {
5697
- log?.info?.(`[wechat:${liveAccount.accountId}] Blocked by policy: ${isGroup ? "group" : "dm"} from ${senderId}`);
5890
+ const wasMentioned = isGroup && msg.isMentioned === true;
5891
+ const access = resolveWeChatInboundAccessDecision({
5892
+ isGroup,
5893
+ senderId,
5894
+ policy
5895
+ });
5896
+ if (!access.allowed) {
5897
+ log?.info?.(
5898
+ `[wechat:${liveAccount.accountId}] Blocked by policy (${access.reason}) from ${senderId}`
5899
+ );
5698
5900
  return null;
5699
5901
  }
5700
5902
  let mediaPath;
@@ -5772,6 +5974,10 @@ ${replyBlock}` : replyBlock;
5772
5974
  return {
5773
5975
  msg,
5774
5976
  rawBody,
5977
+ commandBody: normalizeWeChatCommandBody(rawBody, {
5978
+ isGroup,
5979
+ wasMentioned
5980
+ }),
5775
5981
  mediaPath,
5776
5982
  mediaMime,
5777
5983
  senderName,
@@ -5779,7 +5985,7 @@ ${replyBlock}` : replyBlock;
5779
5985
  isGroup,
5780
5986
  timestamp,
5781
5987
  hasMedia,
5782
- isMentioned: isGroup && msg.isMentioned === true
5988
+ isMentioned: wasMentioned
5783
5989
  };
5784
5990
  }
5785
5991
  function buildSegments(processed) {
@@ -5801,27 +6007,50 @@ function buildSegments(processed) {
5801
6007
  }
5802
6008
  return segments;
5803
6009
  }
5804
- async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg, log, remainingSegments, groupHistory) {
6010
+ async function dispatchSegment(segment, client, chatId, chat, liveAccount, policy, storeAllowFrom, allowTextCommands, cfg, log, remainingSegments) {
5805
6011
  const core = getWeChatRuntime();
5806
6012
  const lastMsg = segment[segment.length - 1];
5807
- const { isGroup, senderId, senderName, timestamp, rawBody, msg } = lastMsg;
6013
+ const { isGroup, senderId, senderName, timestamp, rawBody, commandBody, msg } = lastMsg;
5808
6014
  const mediaMsg = segment.find((pm) => pm.mediaPath);
5809
6015
  const mediaPath = mediaMsg?.mediaPath;
5810
6016
  const mediaMime = mediaMsg?.mediaMime;
5811
6017
  log?.info?.(
5812
6018
  `[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? ` media=${mediaPath}` : ""}`
5813
6019
  );
5814
- if (isGroup) {
5815
- const wechatCfg = cfg?.channels?.wechat;
5816
- const groupEntry = wechatCfg?.groups?.[chatId];
5817
- const defaultEntry = wechatCfg?.groups?.["*"];
5818
- const requireMention = groupEntry?.requireMention ?? defaultEntry?.requireMention ?? true;
5819
- if (requireMention && !lastMsg.isMentioned) {
5820
- log?.info?.(
5821
- `[wechat:${liveAccount.accountId}] Skipping group message (mention required, not mentioned) in ${chatId}`
5822
- );
5823
- return;
6020
+ const hasControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(commandBody, cfg);
6021
+ const commandAuthorized = await resolveWeChatCommandAuthorization({
6022
+ cfg,
6023
+ rawBody: commandBody,
6024
+ isGroup,
6025
+ senderId,
6026
+ dmPolicy: policy.dmPolicy,
6027
+ allowFromForCommands: isGroup ? policy.effectiveGroupAllowFrom : policy.effectiveAllowFrom,
6028
+ deps: {
6029
+ shouldComputeCommandAuthorized: (raw, loadedCfg) => core.channel.commands.shouldComputeCommandAuthorized(raw, loadedCfg),
6030
+ resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
6031
+ readAllowFromStore: async () => storeAllowFrom
5824
6032
  }
6033
+ });
6034
+ if (isGroup && allowTextCommands && hasControlCommand && commandAuthorized !== true) {
6035
+ log?.info?.(
6036
+ `[wechat:${liveAccount.accountId}] Dropping unauthorized group control command from ${senderId} in ${chatId}`
6037
+ );
6038
+ return false;
6039
+ }
6040
+ const mentionGate = resolveWeChatMentionGate({
6041
+ isGroup,
6042
+ requireMention: policy.requireMention,
6043
+ canDetectMention: true,
6044
+ wasMentioned: segment.some((pm) => pm.isMentioned),
6045
+ allowTextCommands,
6046
+ hasControlCommand,
6047
+ commandAuthorized: commandAuthorized === true
6048
+ });
6049
+ if (isGroup && mentionGate.shouldSkip) {
6050
+ log?.info?.(
6051
+ `[wechat:${liveAccount.accountId}] Skipping group segment (mention required) in ${chatId}`
6052
+ );
6053
+ return false;
5825
6054
  }
5826
6055
  try {
5827
6056
  const route = core.channel.routing.resolveAgentRoute({
@@ -5896,7 +6125,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg,
5896
6125
  Body: body,
5897
6126
  BodyForAgent: rawBody,
5898
6127
  RawBody: rawBody,
5899
- CommandBody: rawBody,
6128
+ CommandBody: commandBody,
5900
6129
  InboundHistory: inboundHistory,
5901
6130
  From: isGroup ? `wechat:group:${chatId}` : `wechat:${senderId}`,
5902
6131
  To: `wechat:${chatId}`,
@@ -5909,7 +6138,8 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg,
5909
6138
  Provider: "wechat",
5910
6139
  Surface: "wechat",
5911
6140
  MessageSid: `wechat:${chatId}:${msg.localId}`,
5912
- WasMentioned: isGroup ? lastMsg.isMentioned : void 0,
6141
+ WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : void 0,
6142
+ CommandAuthorized: commandAuthorized,
5913
6143
  OriginatingChannel: "wechat",
5914
6144
  OriginatingTo: `wechat:${chatId}`,
5915
6145
  ...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime } : {},
@@ -6012,13 +6242,12 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg,
6012
6242
  direction: "inbound",
6013
6243
  at: timestamp
6014
6244
  });
6015
- if (isGroup && groupHistory) {
6016
- groupHistory.set(chatId, []);
6017
- }
6245
+ return true;
6018
6246
  } catch (err) {
6019
6247
  log?.error?.(
6020
6248
  `[wechat:${liveAccount.accountId}] Failed to dispatch segment (last msg ${msg.localId}): ${err}`
6021
6249
  );
6250
+ return false;
6022
6251
  }
6023
6252
  }
6024
6253
  function bufferGroupHistory(groupHistory, chatId, pm, limit) {
@@ -6035,8 +6264,20 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
6035
6264
  }
6036
6265
  }
6037
6266
  async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit) {
6267
+ const core = getWeChatRuntime();
6038
6268
  const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
6039
6269
  const chatId = chat.username ?? chat.id;
6270
+ const storeAllowFrom = await core.channel.pairing.readAllowFromStore("wechat", process.env, liveAccount.accountId).catch(() => []);
6271
+ const policy = resolveWeChatPolicyContext({
6272
+ account: liveAccount,
6273
+ cfg,
6274
+ chatId,
6275
+ storeAllowFrom
6276
+ });
6277
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
6278
+ cfg,
6279
+ surface: "wechat"
6280
+ });
6040
6281
  if (!skipOpen) {
6041
6282
  log?.info?.(`[wechat:${liveAccount.accountId}] Opening chat ${chatId}...`);
6042
6283
  try {
@@ -6094,20 +6335,18 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6094
6335
  log?.info?.(
6095
6336
  `[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}, content=${(msg.content || "").slice(0, 50)}`
6096
6337
  );
6097
- const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, log);
6338
+ const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, policy, log);
6098
6339
  if (pm) {
6099
6340
  processed.push(pm);
6100
6341
  }
6101
6342
  }
6102
6343
  const isGroup = chatId.includes("@chatroom");
6344
+ let clearBufferedHistory = false;
6345
+ const hasControlCommandInWindow = allowTextCommands && processed.some((pm) => core.channel.commands.isControlCommandMessage(pm.commandBody, cfg));
6103
6346
  if (isGroup && groupHistory) {
6104
- const wechatCfg = cfg?.channels?.wechat;
6105
- const groupEntry = wechatCfg?.groups?.[chatId];
6106
- const defaultEntry = wechatCfg?.groups?.["*"];
6107
- const requireMention = groupEntry?.requireMention ?? defaultEntry?.requireMention ?? true;
6108
- if (requireMention) {
6347
+ if (policy.requireMention) {
6109
6348
  const hasMention = processed.some((pm) => pm.isMentioned);
6110
- if (!hasMention) {
6349
+ if (!hasMention && !hasControlCommandInWindow) {
6111
6350
  const limit = groupHistoryLimit ?? 50;
6112
6351
  for (const pm of processed) {
6113
6352
  bufferGroupHistory(groupHistory, chatId, pm, limit);
@@ -6117,37 +6356,67 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6117
6356
  lastSeenId.set(chatId, maxId2);
6118
6357
  return;
6119
6358
  }
6120
- const buffered = groupHistory.get(chatId) ?? [];
6121
- if (buffered.length > 0) {
6122
- for (const pm of buffered) {
6123
- pm.isMentioned = true;
6359
+ if (hasMention) {
6360
+ clearBufferedHistory = true;
6361
+ const buffered = groupHistory.get(chatId) ?? [];
6362
+ if (buffered.length > 0) {
6363
+ for (const pm of buffered) {
6364
+ pm.isMentioned = true;
6365
+ }
6366
+ processed.unshift(...buffered);
6367
+ log?.info?.(
6368
+ `[wechat:${liveAccount.accountId}] Injected ${buffered.length} buffered msg(s) as history in ${chatId}`
6369
+ );
6124
6370
  }
6125
- processed.unshift(...buffered);
6126
- groupHistory.set(chatId, []);
6127
- log?.info?.(`[wechat:${liveAccount.accountId}] Injected ${buffered.length} buffered msg(s) as history in ${chatId}`);
6128
- }
6129
- let latestMediaIdx = -1;
6130
- for (let i = processed.length - 1; i >= 0; i--) {
6131
- if (processed[i].mediaPath) {
6132
- latestMediaIdx = i;
6133
- break;
6371
+ let latestMediaIdx = -1;
6372
+ for (let i = processed.length - 1; i >= 0; i--) {
6373
+ if (processed[i].mediaPath) {
6374
+ latestMediaIdx = i;
6375
+ break;
6376
+ }
6134
6377
  }
6135
- }
6136
- for (let i = 0; i < processed.length; i++) {
6137
- if (processed[i].mediaPath && i !== latestMediaIdx) {
6138
- processed[i] = { ...processed[i], mediaPath: void 0, mediaMime: void 0, hasMedia: false };
6378
+ for (let i = 0; i < processed.length; i++) {
6379
+ if (processed[i].mediaPath && i !== latestMediaIdx) {
6380
+ processed[i] = {
6381
+ ...processed[i],
6382
+ mediaPath: void 0,
6383
+ mediaMime: void 0,
6384
+ hasMedia: false
6385
+ };
6386
+ }
6139
6387
  }
6140
6388
  }
6389
+ } else {
6390
+ clearBufferedHistory = true;
6141
6391
  }
6142
6392
  }
6143
6393
  if (processed.length > 0) {
6144
- const segments = buildSegments(processed);
6394
+ const segments = hasControlCommandInWindow ? processed.map((pm) => [pm]) : buildSegments(processed);
6145
6395
  log?.info?.(
6146
6396
  `[wechat:${liveAccount.accountId}] ${chatId}: ${processed.length} dispatchable msg(s) in ${segments.length} segment(s)`
6147
6397
  );
6398
+ let allDispatched = true;
6148
6399
  for (let i = 0; i < segments.length; i++) {
6149
6400
  const remaining = segments.length - i - 1;
6150
- await dispatchSegment(segments[i], client, chatId, chat, liveAccount, cfg, log, remaining, groupHistory);
6401
+ const dispatched = await dispatchSegment(
6402
+ segments[i],
6403
+ client,
6404
+ chatId,
6405
+ chat,
6406
+ liveAccount,
6407
+ policy,
6408
+ storeAllowFrom,
6409
+ allowTextCommands,
6410
+ cfg,
6411
+ log,
6412
+ hasControlCommandInWindow ? void 0 : remaining
6413
+ );
6414
+ if (!dispatched) {
6415
+ allDispatched = false;
6416
+ }
6417
+ }
6418
+ if (clearBufferedHistory && allDispatched && groupHistory) {
6419
+ groupHistory.set(chatId, []);
6151
6420
  }
6152
6421
  }
6153
6422
  const maxId = Math.max(...newMessages.map((m) => m.localId));
@@ -6521,7 +6790,7 @@ var wechatOnboardingAdapter = {
6521
6790
  wechatCfg.groupPolicy = groupPolicy;
6522
6791
  if (groupPolicy === "allowlist") {
6523
6792
  const raw = await prompter.text({
6524
- message: "Allowed group IDs (comma-separated xxx@chatroom values)"
6793
+ message: "Allowed group sender IDs (comma-separated wxid_xxx values; use * to allow any sender)"
6525
6794
  });
6526
6795
  wechatCfg.groupAllowFrom = raw.split(",").map((s) => s.trim()).filter(Boolean);
6527
6796
  }
@@ -6755,7 +7024,13 @@ var wechatPlugin = {
6755
7024
  additionalProperties: {
6756
7025
  type: "object",
6757
7026
  properties: {
6758
- requireMention: { type: "boolean" }
7027
+ enabled: { type: "boolean" },
7028
+ requireMention: { type: "boolean" },
7029
+ groupPolicy: {
7030
+ type: "string",
7031
+ enum: ["open", "allowlist", "disabled"]
7032
+ },
7033
+ allowFrom: { type: "array", items: { type: "string" } }
6759
7034
  }
6760
7035
  }
6761
7036
  },
@@ -6800,7 +7075,8 @@ var wechatPlugin = {
6800
7075
  allowFrom: account.allowFrom ?? [],
6801
7076
  allowFromPath: "channels.wechat.allowFrom",
6802
7077
  policyPath: "channels.wechat.dmPolicy",
6803
- approveHint: "Add the wxid to channels.wechat.allowFrom"
7078
+ approveHint: "Add the wxid to channels.wechat.allowFrom",
7079
+ normalizeEntry: (raw) => raw.replace(/^wechat:/i, "").trim()
6804
7080
  })
6805
7081
  },
6806
7082
  // ---- Groups adapter ----
@@ -6808,12 +7084,27 @@ var wechatPlugin = {
6808
7084
  resolveRequireMention: ({ cfg, groupId }) => {
6809
7085
  const wechat = cfg?.channels?.wechat;
6810
7086
  if (!wechat) return true;
6811
- if (groupId && wechat.groups?.[groupId]?.requireMention != null) {
6812
- return wechat.groups[groupId].requireMention;
7087
+ if (!groupId) {
7088
+ return wechat.groups?.["*"]?.requireMention ?? true;
7089
+ }
7090
+ const exact = wechat.groups?.[groupId];
7091
+ if (exact?.requireMention != null) {
7092
+ return exact.requireMention;
6813
7093
  }
6814
- return true;
7094
+ const normalizedGroupId = normalizeWeChatId(groupId);
7095
+ if (normalizedGroupId && wechat.groups?.[normalizedGroupId]?.requireMention != null) {
7096
+ return wechat.groups[normalizedGroupId].requireMention;
7097
+ }
7098
+ return wechat.groups?.["*"]?.requireMention ?? true;
6815
7099
  }
6816
7100
  },
7101
+ // ---- Mention adapter ----
7102
+ mentions: {
7103
+ stripMentions: ({ text, ctx }) => normalizeWeChatCommandBody(text, {
7104
+ isGroup: ctx.ChatType === "group",
7105
+ wasMentioned: ctx.WasMentioned === true
7106
+ })
7107
+ },
6817
7108
  // ---- Messaging adapter ----
6818
7109
  messaging: {
6819
7110
  normalizeTarget: (raw) => raw.replace(/^wechat:/i, "").trim() || void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-wechat/wechat",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "scripts": {
43
43
  "build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:openclaw",
44
- "typecheck": "tsc --noEmit"
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "node --test --experimental-strip-types src/*.test.ts"
45
46
  }
46
47
  }