@agent-wechat/wechat 0.7.10 → 0.8.1

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 +319 -68
  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,
@@ -1204,6 +1210,13 @@ var WeChatClient = class {
1204
1210
  async openChat(chatId, clearUnreads) {
1205
1211
  return this.post(`/api/chats/${encodeURIComponent(chatId)}/open${qs({ clearUnreads })}`);
1206
1212
  }
1213
+ // ---- Contacts ----
1214
+ async listContacts(limit, offset) {
1215
+ return this.get(`/api/contacts${qs({ limit, offset })}`);
1216
+ }
1217
+ async findContacts(name) {
1218
+ return this.get(`/api/contacts/find${qs({ name })}`);
1219
+ }
1207
1220
  // ---- Messages ----
1208
1221
  async listMessages(chatId, limit, offset) {
1209
1222
  return this.get(`/api/messages/${encodeURIComponent(chatId)}${qs({ limit, offset })}`);
@@ -5498,6 +5511,172 @@ var agentConfigSchema = external_exports.object({
5498
5511
 
5499
5512
  // src/monitor.ts
5500
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
+ function unique(values) {
5524
+ return Array.from(new Set(values));
5525
+ }
5526
+ function normalizeDmPolicy2(policy) {
5527
+ if (policy === "open" || policy === "allowlist" || policy === "disabled") {
5528
+ return policy;
5529
+ }
5530
+ return "disabled";
5531
+ }
5532
+ function normalizeGroupPolicy2(policy) {
5533
+ if (policy === "open" || policy === "allowlist" || policy === "disabled") {
5534
+ return policy;
5535
+ }
5536
+ return void 0;
5537
+ }
5538
+ function firstDefined(...values) {
5539
+ for (const value of values) {
5540
+ if (value !== void 0) {
5541
+ return value;
5542
+ }
5543
+ }
5544
+ return void 0;
5545
+ }
5546
+ function normalizeWeChatId(raw) {
5547
+ const trimmed = raw.trim();
5548
+ if (!trimmed) {
5549
+ return "";
5550
+ }
5551
+ return trimmed.replace(/^wechat:/i, "").trim();
5552
+ }
5553
+ function normalizeWeChatAllowFrom(values) {
5554
+ const normalized = (values ?? []).map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry === "*" ? "*" : normalizeWeChatId(entry)).filter(Boolean);
5555
+ return unique(normalized);
5556
+ }
5557
+ function isWeChatSenderAllowed(senderId, allowFrom) {
5558
+ if (allowFrom.includes("*")) {
5559
+ return true;
5560
+ }
5561
+ const normalizedSender = senderId ? normalizeWeChatId(senderId) : "";
5562
+ if (!normalizedSender) {
5563
+ return false;
5564
+ }
5565
+ return allowFrom.includes(normalizedSender);
5566
+ }
5567
+ function resolveGroupEntry(params) {
5568
+ const groups = params.account.groups ?? {};
5569
+ const normalizedChatId = normalizeWeChatId(params.chatId);
5570
+ const keys = buildChannelKeyCandidates(params.chatId, normalizedChatId);
5571
+ const match = resolveChannelEntryMatchWithFallback({
5572
+ entries: groups,
5573
+ keys,
5574
+ wildcardKey: "*",
5575
+ normalizeKey: normalizeWeChatId
5576
+ });
5577
+ return {
5578
+ groupEntry: match.entry,
5579
+ wildcardEntry: match.wildcardEntry
5580
+ };
5581
+ }
5582
+ function resolveWeChatPolicyContext(params) {
5583
+ const dmPolicy = normalizeDmPolicy2(params.account.dmPolicy);
5584
+ const configuredAllowFrom = normalizeWeChatAllowFrom(params.account.allowFrom);
5585
+ const configuredGroupAllowFrom = normalizeWeChatAllowFrom(params.account.groupAllowFrom);
5586
+ const normalizedStoreAllowFrom = dmPolicy === "allowlist" ? [] : normalizeWeChatAllowFrom(params.storeAllowFrom);
5587
+ const effectiveAllowFrom = unique([...configuredAllowFrom, ...normalizedStoreAllowFrom]);
5588
+ const groupBase = configuredGroupAllowFrom.length > 0 ? configuredGroupAllowFrom : configuredAllowFrom;
5589
+ const effectiveGroupAllowFrom = unique([...groupBase, ...normalizedStoreAllowFrom]);
5590
+ const { groupEntry, wildcardEntry } = resolveGroupEntry({
5591
+ account: params.account,
5592
+ chatId: params.chatId
5593
+ });
5594
+ const groupEnabled = firstDefined(groupEntry?.enabled, wildcardEntry?.enabled, true) !== false;
5595
+ const requireMention = firstDefined(groupEntry?.requireMention, wildcardEntry?.requireMention, true) !== false;
5596
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
5597
+ const { groupPolicy: fallbackGroupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
5598
+ providerConfigPresent: params.cfg.channels?.wechat !== void 0,
5599
+ groupPolicy: normalizeGroupPolicy2(params.account.groupPolicy),
5600
+ defaultGroupPolicy: normalizeGroupPolicy2(defaultGroupPolicy)
5601
+ });
5602
+ const groupPolicy = normalizeGroupPolicy2(
5603
+ firstDefined(
5604
+ groupEntry?.groupPolicy,
5605
+ wildcardEntry?.groupPolicy,
5606
+ params.account.groupPolicy,
5607
+ defaultGroupPolicy
5608
+ )
5609
+ ) ?? fallbackGroupPolicy;
5610
+ const groupAllowOverride = normalizeWeChatAllowFrom(
5611
+ firstDefined(groupEntry?.allowFrom, wildcardEntry?.allowFrom)
5612
+ );
5613
+ const groupAllowFrom = groupAllowOverride.length > 0 ? unique([...groupAllowOverride, ...normalizedStoreAllowFrom]) : effectiveGroupAllowFrom;
5614
+ return {
5615
+ dmPolicy,
5616
+ groupPolicy,
5617
+ requireMention,
5618
+ groupEnabled,
5619
+ effectiveAllowFrom,
5620
+ effectiveGroupAllowFrom: groupAllowFrom
5621
+ };
5622
+ }
5623
+ function resolveWeChatInboundAccessDecision(params) {
5624
+ if (params.isGroup) {
5625
+ if (!params.policy.groupEnabled) {
5626
+ return { allowed: false, reason: "group-config-disabled" };
5627
+ }
5628
+ if (params.policy.groupPolicy === "disabled") {
5629
+ return { allowed: false, reason: "groupPolicy=disabled" };
5630
+ }
5631
+ if (params.policy.groupPolicy === "allowlist") {
5632
+ if (params.policy.effectiveGroupAllowFrom.length === 0) {
5633
+ return { allowed: false, reason: "groupPolicy=allowlist (empty allowlist)" };
5634
+ }
5635
+ if (!isWeChatSenderAllowed(params.senderId, params.policy.effectiveGroupAllowFrom)) {
5636
+ return { allowed: false, reason: "groupPolicy=allowlist (sender not allowlisted)" };
5637
+ }
5638
+ }
5639
+ return { allowed: true, reason: `groupPolicy=${params.policy.groupPolicy}` };
5640
+ }
5641
+ if (params.policy.dmPolicy === "disabled") {
5642
+ return { allowed: false, reason: "dmPolicy=disabled" };
5643
+ }
5644
+ if (params.policy.dmPolicy === "allowlist") {
5645
+ if (params.policy.effectiveAllowFrom.length === 0) {
5646
+ return { allowed: false, reason: "dmPolicy=allowlist (empty allowlist)" };
5647
+ }
5648
+ if (!isWeChatSenderAllowed(params.senderId, params.policy.effectiveAllowFrom)) {
5649
+ return { allowed: false, reason: "dmPolicy=allowlist (sender not allowlisted)" };
5650
+ }
5651
+ }
5652
+ return { allowed: true, reason: `dmPolicy=${params.policy.dmPolicy}` };
5653
+ }
5654
+ async function resolveWeChatCommandAuthorization(params) {
5655
+ const normalizedSenderId = normalizeWeChatId(params.senderId ?? "");
5656
+ const { commandAuthorized } = await resolveSenderCommandAuthorization({
5657
+ cfg: params.cfg,
5658
+ rawBody: params.rawBody,
5659
+ isGroup: params.isGroup,
5660
+ dmPolicy: params.dmPolicy,
5661
+ configuredAllowFrom: params.allowFromForCommands,
5662
+ senderId: normalizedSenderId,
5663
+ isSenderAllowed: (senderId, allowFrom) => isWeChatSenderAllowed(senderId, normalizeWeChatAllowFrom(allowFrom)),
5664
+ readAllowFromStore: async () => normalizeWeChatAllowFrom(await params.deps.readAllowFromStore()),
5665
+ shouldComputeCommandAuthorized: params.deps.shouldComputeCommandAuthorized,
5666
+ resolveCommandAuthorizedFromAuthorizers: params.deps.resolveCommandAuthorizedFromAuthorizers
5667
+ });
5668
+ return commandAuthorized;
5669
+ }
5670
+ function resolveWeChatMentionGate(params) {
5671
+ const implicitMention = params.implicitMention === true;
5672
+ const baseWasMentioned = params.wasMentioned || implicitMention;
5673
+ const shouldBypassMention = params.isGroup && params.requireMention && !baseWasMentioned && params.allowTextCommands && params.hasControlCommand && params.commandAuthorized;
5674
+ const effectiveWasMentioned = baseWasMentioned || shouldBypassMention;
5675
+ const shouldSkip = params.requireMention && params.canDetectMention && !effectiveWasMentioned;
5676
+ return { effectiveWasMentioned, shouldSkip, shouldBypassMention };
5677
+ }
5678
+
5679
+ // src/monitor.ts
5501
5680
  var MEDIA_TYPES = /* @__PURE__ */ new Set([3, 34]);
5502
5681
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
5503
5682
  var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
@@ -5526,20 +5705,6 @@ async function pollMedia(client, chatId, localId, log, maxAttempts = 15, interva
5526
5705
  }
5527
5706
  return null;
5528
5707
  }
5529
- function isMessageAllowed(account, isGroup, senderId) {
5530
- if (isGroup) {
5531
- if (account.groupPolicy === "disabled") return false;
5532
- if (account.groupPolicy === "allowlist") {
5533
- return account.groupAllowFrom.includes(senderId);
5534
- }
5535
- return true;
5536
- }
5537
- if (account.dmPolicy === "disabled") return false;
5538
- if (account.dmPolicy === "allowlist") {
5539
- return account.allowFrom.includes(senderId);
5540
- }
5541
- return true;
5542
- }
5543
5708
  function enqueueWeChatSystemEvent(text, contextKey) {
5544
5709
  try {
5545
5710
  const core = getWeChatRuntime();
@@ -5677,7 +5842,7 @@ async function startWeChatMonitor(opts) {
5677
5842
  connected: false
5678
5843
  });
5679
5844
  }
5680
- async function prepareMessage(client, msg, chatId, chat, liveAccount, log) {
5845
+ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, log) {
5681
5846
  const core = getWeChatRuntime();
5682
5847
  if (msg.isSelf) {
5683
5848
  log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
@@ -5686,8 +5851,15 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, log) {
5686
5851
  const isGroup = chatId.includes("@chatroom");
5687
5852
  const senderId = msg.sender ?? chatId;
5688
5853
  const senderName = msg.senderName ?? msg.sender ?? chat.name;
5689
- if (!isMessageAllowed(liveAccount, isGroup, senderId)) {
5690
- log?.info?.(`[wechat:${liveAccount.accountId}] Blocked by policy: ${isGroup ? "group" : "dm"} from ${senderId}`);
5854
+ const access = resolveWeChatInboundAccessDecision({
5855
+ isGroup,
5856
+ senderId,
5857
+ policy
5858
+ });
5859
+ if (!access.allowed) {
5860
+ log?.info?.(
5861
+ `[wechat:${liveAccount.accountId}] Blocked by policy (${access.reason}) from ${senderId}`
5862
+ );
5691
5863
  return null;
5692
5864
  }
5693
5865
  let mediaPath;
@@ -5765,6 +5937,7 @@ ${replyBlock}` : replyBlock;
5765
5937
  return {
5766
5938
  msg,
5767
5939
  rawBody,
5940
+ commandBody: rawBody,
5768
5941
  mediaPath,
5769
5942
  mediaMime,
5770
5943
  senderName,
@@ -5794,27 +5967,50 @@ function buildSegments(processed) {
5794
5967
  }
5795
5968
  return segments;
5796
5969
  }
5797
- async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg, log, remainingSegments, groupHistory) {
5970
+ async function dispatchSegment(segment, client, chatId, chat, liveAccount, policy, storeAllowFrom, allowTextCommands, cfg, log, remainingSegments) {
5798
5971
  const core = getWeChatRuntime();
5799
5972
  const lastMsg = segment[segment.length - 1];
5800
- const { isGroup, senderId, senderName, timestamp, rawBody, msg } = lastMsg;
5973
+ const { isGroup, senderId, senderName, timestamp, rawBody, commandBody, msg } = lastMsg;
5801
5974
  const mediaMsg = segment.find((pm) => pm.mediaPath);
5802
5975
  const mediaPath = mediaMsg?.mediaPath;
5803
5976
  const mediaMime = mediaMsg?.mediaMime;
5804
5977
  log?.info?.(
5805
5978
  `[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? ` media=${mediaPath}` : ""}`
5806
5979
  );
5807
- if (isGroup) {
5808
- const wechatCfg = cfg?.channels?.wechat;
5809
- const groupEntry = wechatCfg?.groups?.[chatId];
5810
- const defaultEntry = wechatCfg?.groups?.["*"];
5811
- const requireMention = groupEntry?.requireMention ?? defaultEntry?.requireMention ?? true;
5812
- if (requireMention && !lastMsg.isMentioned) {
5813
- log?.info?.(
5814
- `[wechat:${liveAccount.accountId}] Skipping group message (mention required, not mentioned) in ${chatId}`
5815
- );
5816
- return;
5980
+ const hasControlCommand = allowTextCommands && core.channel.text.hasControlCommand(commandBody, cfg);
5981
+ const commandAuthorized = await resolveWeChatCommandAuthorization({
5982
+ cfg,
5983
+ rawBody: commandBody,
5984
+ isGroup,
5985
+ senderId,
5986
+ dmPolicy: policy.dmPolicy,
5987
+ allowFromForCommands: isGroup ? policy.effectiveGroupAllowFrom : policy.effectiveAllowFrom,
5988
+ deps: {
5989
+ shouldComputeCommandAuthorized: (raw, loadedCfg) => core.channel.commands.shouldComputeCommandAuthorized(raw, loadedCfg),
5990
+ resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
5991
+ readAllowFromStore: async () => storeAllowFrom
5817
5992
  }
5993
+ });
5994
+ if (isGroup && allowTextCommands && hasControlCommand && commandAuthorized !== true) {
5995
+ log?.info?.(
5996
+ `[wechat:${liveAccount.accountId}] Dropping unauthorized group control command from ${senderId} in ${chatId}`
5997
+ );
5998
+ return false;
5999
+ }
6000
+ const mentionGate = resolveWeChatMentionGate({
6001
+ isGroup,
6002
+ requireMention: policy.requireMention,
6003
+ canDetectMention: true,
6004
+ wasMentioned: segment.some((pm) => pm.isMentioned),
6005
+ allowTextCommands,
6006
+ hasControlCommand,
6007
+ commandAuthorized: commandAuthorized === true
6008
+ });
6009
+ if (isGroup && mentionGate.shouldSkip) {
6010
+ log?.info?.(
6011
+ `[wechat:${liveAccount.accountId}] Skipping group segment (mention required) in ${chatId}`
6012
+ );
6013
+ return false;
5818
6014
  }
5819
6015
  try {
5820
6016
  const route = core.channel.routing.resolveAgentRoute({
@@ -5889,7 +6085,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg,
5889
6085
  Body: body,
5890
6086
  BodyForAgent: rawBody,
5891
6087
  RawBody: rawBody,
5892
- CommandBody: rawBody,
6088
+ CommandBody: commandBody,
5893
6089
  InboundHistory: inboundHistory,
5894
6090
  From: isGroup ? `wechat:group:${chatId}` : `wechat:${senderId}`,
5895
6091
  To: `wechat:${chatId}`,
@@ -5902,7 +6098,8 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg,
5902
6098
  Provider: "wechat",
5903
6099
  Surface: "wechat",
5904
6100
  MessageSid: `wechat:${chatId}:${msg.localId}`,
5905
- WasMentioned: isGroup ? lastMsg.isMentioned : void 0,
6101
+ WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : void 0,
6102
+ CommandAuthorized: commandAuthorized,
5906
6103
  OriginatingChannel: "wechat",
5907
6104
  OriginatingTo: `wechat:${chatId}`,
5908
6105
  ...mediaPath ? { MediaPath: mediaPath, MediaUrl: mediaPath, MediaType: mediaMime } : {},
@@ -6005,13 +6202,12 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, cfg,
6005
6202
  direction: "inbound",
6006
6203
  at: timestamp
6007
6204
  });
6008
- if (isGroup && groupHistory) {
6009
- groupHistory.set(chatId, []);
6010
- }
6205
+ return true;
6011
6206
  } catch (err) {
6012
6207
  log?.error?.(
6013
6208
  `[wechat:${liveAccount.accountId}] Failed to dispatch segment (last msg ${msg.localId}): ${err}`
6014
6209
  );
6210
+ return false;
6015
6211
  }
6016
6212
  }
6017
6213
  function bufferGroupHistory(groupHistory, chatId, pm, limit) {
@@ -6028,8 +6224,20 @@ function bufferGroupHistory(groupHistory, chatId, pm, limit) {
6028
6224
  }
6029
6225
  }
6030
6226
  async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, skipOpen, groupHistory, groupHistoryLimit) {
6227
+ const core = getWeChatRuntime();
6031
6228
  const liveAccount = resolveWeChatAccount(cfg, account.accountId) ?? account;
6032
6229
  const chatId = chat.username ?? chat.id;
6230
+ const storeAllowFrom = await core.channel.pairing.readAllowFromStore("wechat", process.env, liveAccount.accountId).catch(() => []);
6231
+ const policy = resolveWeChatPolicyContext({
6232
+ account: liveAccount,
6233
+ cfg,
6234
+ chatId,
6235
+ storeAllowFrom
6236
+ });
6237
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
6238
+ cfg,
6239
+ surface: "wechat"
6240
+ });
6033
6241
  if (!skipOpen) {
6034
6242
  log?.info?.(`[wechat:${liveAccount.accountId}] Opening chat ${chatId}...`);
6035
6243
  try {
@@ -6087,20 +6295,18 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6087
6295
  log?.info?.(
6088
6296
  `[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}, content=${(msg.content || "").slice(0, 50)}`
6089
6297
  );
6090
- const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, log);
6298
+ const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, policy, log);
6091
6299
  if (pm) {
6092
6300
  processed.push(pm);
6093
6301
  }
6094
6302
  }
6095
6303
  const isGroup = chatId.includes("@chatroom");
6304
+ let clearBufferedHistory = false;
6305
+ const hasControlCommandInWindow = allowTextCommands && processed.some((pm) => core.channel.text.hasControlCommand(pm.commandBody, cfg));
6096
6306
  if (isGroup && groupHistory) {
6097
- const wechatCfg = cfg?.channels?.wechat;
6098
- const groupEntry = wechatCfg?.groups?.[chatId];
6099
- const defaultEntry = wechatCfg?.groups?.["*"];
6100
- const requireMention = groupEntry?.requireMention ?? defaultEntry?.requireMention ?? true;
6101
- if (requireMention) {
6307
+ if (policy.requireMention) {
6102
6308
  const hasMention = processed.some((pm) => pm.isMentioned);
6103
- if (!hasMention) {
6309
+ if (!hasMention && !hasControlCommandInWindow) {
6104
6310
  const limit = groupHistoryLimit ?? 50;
6105
6311
  for (const pm of processed) {
6106
6312
  bufferGroupHistory(groupHistory, chatId, pm, limit);
@@ -6110,37 +6316,67 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6110
6316
  lastSeenId.set(chatId, maxId2);
6111
6317
  return;
6112
6318
  }
6113
- const buffered = groupHistory.get(chatId) ?? [];
6114
- if (buffered.length > 0) {
6115
- for (const pm of buffered) {
6116
- pm.isMentioned = true;
6319
+ if (hasMention) {
6320
+ clearBufferedHistory = true;
6321
+ const buffered = groupHistory.get(chatId) ?? [];
6322
+ if (buffered.length > 0) {
6323
+ for (const pm of buffered) {
6324
+ pm.isMentioned = true;
6325
+ }
6326
+ processed.unshift(...buffered);
6327
+ log?.info?.(
6328
+ `[wechat:${liveAccount.accountId}] Injected ${buffered.length} buffered msg(s) as history in ${chatId}`
6329
+ );
6117
6330
  }
6118
- processed.unshift(...buffered);
6119
- groupHistory.set(chatId, []);
6120
- log?.info?.(`[wechat:${liveAccount.accountId}] Injected ${buffered.length} buffered msg(s) as history in ${chatId}`);
6121
- }
6122
- let latestMediaIdx = -1;
6123
- for (let i = processed.length - 1; i >= 0; i--) {
6124
- if (processed[i].mediaPath) {
6125
- latestMediaIdx = i;
6126
- break;
6331
+ let latestMediaIdx = -1;
6332
+ for (let i = processed.length - 1; i >= 0; i--) {
6333
+ if (processed[i].mediaPath) {
6334
+ latestMediaIdx = i;
6335
+ break;
6336
+ }
6127
6337
  }
6128
- }
6129
- for (let i = 0; i < processed.length; i++) {
6130
- if (processed[i].mediaPath && i !== latestMediaIdx) {
6131
- processed[i] = { ...processed[i], mediaPath: void 0, mediaMime: void 0, hasMedia: false };
6338
+ for (let i = 0; i < processed.length; i++) {
6339
+ if (processed[i].mediaPath && i !== latestMediaIdx) {
6340
+ processed[i] = {
6341
+ ...processed[i],
6342
+ mediaPath: void 0,
6343
+ mediaMime: void 0,
6344
+ hasMedia: false
6345
+ };
6346
+ }
6132
6347
  }
6133
6348
  }
6349
+ } else {
6350
+ clearBufferedHistory = true;
6134
6351
  }
6135
6352
  }
6136
6353
  if (processed.length > 0) {
6137
- const segments = buildSegments(processed);
6354
+ const segments = hasControlCommandInWindow ? processed.map((pm) => [pm]) : buildSegments(processed);
6138
6355
  log?.info?.(
6139
6356
  `[wechat:${liveAccount.accountId}] ${chatId}: ${processed.length} dispatchable msg(s) in ${segments.length} segment(s)`
6140
6357
  );
6358
+ let allDispatched = true;
6141
6359
  for (let i = 0; i < segments.length; i++) {
6142
6360
  const remaining = segments.length - i - 1;
6143
- await dispatchSegment(segments[i], client, chatId, chat, liveAccount, cfg, log, remaining, groupHistory);
6361
+ const dispatched = await dispatchSegment(
6362
+ segments[i],
6363
+ client,
6364
+ chatId,
6365
+ chat,
6366
+ liveAccount,
6367
+ policy,
6368
+ storeAllowFrom,
6369
+ allowTextCommands,
6370
+ cfg,
6371
+ log,
6372
+ hasControlCommandInWindow ? void 0 : remaining
6373
+ );
6374
+ if (!dispatched) {
6375
+ allDispatched = false;
6376
+ }
6377
+ }
6378
+ if (clearBufferedHistory && allDispatched && groupHistory) {
6379
+ groupHistory.set(chatId, []);
6144
6380
  }
6145
6381
  }
6146
6382
  const maxId = Math.max(...newMessages.map((m) => m.localId));
@@ -6514,7 +6750,7 @@ var wechatOnboardingAdapter = {
6514
6750
  wechatCfg.groupPolicy = groupPolicy;
6515
6751
  if (groupPolicy === "allowlist") {
6516
6752
  const raw = await prompter.text({
6517
- message: "Allowed group IDs (comma-separated xxx@chatroom values)"
6753
+ message: "Allowed group sender IDs (comma-separated wxid_xxx values; use * to allow any sender)"
6518
6754
  });
6519
6755
  wechatCfg.groupAllowFrom = raw.split(",").map((s) => s.trim()).filter(Boolean);
6520
6756
  }
@@ -6748,7 +6984,13 @@ var wechatPlugin = {
6748
6984
  additionalProperties: {
6749
6985
  type: "object",
6750
6986
  properties: {
6751
- requireMention: { type: "boolean" }
6987
+ enabled: { type: "boolean" },
6988
+ requireMention: { type: "boolean" },
6989
+ groupPolicy: {
6990
+ type: "string",
6991
+ enum: ["open", "allowlist", "disabled"]
6992
+ },
6993
+ allowFrom: { type: "array", items: { type: "string" } }
6752
6994
  }
6753
6995
  }
6754
6996
  },
@@ -6793,7 +7035,8 @@ var wechatPlugin = {
6793
7035
  allowFrom: account.allowFrom ?? [],
6794
7036
  allowFromPath: "channels.wechat.allowFrom",
6795
7037
  policyPath: "channels.wechat.dmPolicy",
6796
- approveHint: "Add the wxid to channels.wechat.allowFrom"
7038
+ approveHint: "Add the wxid to channels.wechat.allowFrom",
7039
+ normalizeEntry: (raw) => raw.replace(/^wechat:/i, "").trim()
6797
7040
  })
6798
7041
  },
6799
7042
  // ---- Groups adapter ----
@@ -6801,10 +7044,18 @@ var wechatPlugin = {
6801
7044
  resolveRequireMention: ({ cfg, groupId }) => {
6802
7045
  const wechat = cfg?.channels?.wechat;
6803
7046
  if (!wechat) return true;
6804
- if (groupId && wechat.groups?.[groupId]?.requireMention != null) {
6805
- return wechat.groups[groupId].requireMention;
7047
+ if (!groupId) {
7048
+ return wechat.groups?.["*"]?.requireMention ?? true;
7049
+ }
7050
+ const exact = wechat.groups?.[groupId];
7051
+ if (exact?.requireMention != null) {
7052
+ return exact.requireMention;
7053
+ }
7054
+ const normalizedGroupId = normalizeWeChatId(groupId);
7055
+ if (normalizedGroupId && wechat.groups?.[normalizedGroupId]?.requireMention != null) {
7056
+ return wechat.groups[normalizedGroupId].requireMention;
6806
7057
  }
6807
- return true;
7058
+ return wechat.groups?.["*"]?.requireMention ?? true;
6808
7059
  }
6809
7060
  },
6810
7061
  // ---- Messaging adapter ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-wechat/wechat",
3
- "version": "0.7.10",
3
+ "version": "0.8.1",
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
  }