@agent-wechat/agent-wechat 0.15.6 → 0.16.0

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 +8 -2
  2. package/dist/index.js +113 -113
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -87,6 +87,7 @@ Or edit `~/.openclaw/openclaw.json` directly:
87
87
  "enabled": true,
88
88
  "serverUrl": "http://localhost:6174",
89
89
  "dmPolicy": "open",
90
+ "allowFrom": ["*"],
90
91
  "groupPolicy": "open"
91
92
  }
92
93
  }
@@ -147,13 +148,18 @@ All config lives under `channels.agent-wechat` in OpenClaw's config file:
147
148
  | `serverUrl` | string | — | agent-wechat REST API URL |
148
149
  | `token` | string | — | Auth token (auto-read from `~/.config/agent-wechat/token` for local setups) |
149
150
  | `dmPolicy` | `"open" \| "allowlist" \| "disabled"` | `"disabled"` | Who can DM the bot |
150
- | `allowFrom` | string[] | `[]` | wxid allowlist for DMs (when policy is `allowlist`) |
151
+ | `allowFrom` | string[] | `[]` | DM senders and command owners: `wxid_...`, `"*"` for everyone, or `accessGroup:<name>`. With `dmPolicy: "open"`, use `["*"]` |
151
152
  | `groupPolicy` | `"open" \| "allowlist" \| "disabled"` | `"disabled"` | Group message policy |
152
- | `groupAllowFrom` | string[] | `[]` | Global allowlist of group sender IDs (`wxid_...`) |
153
+ | `groupAllowFrom` | string[] | `[]` | Group senders who pass `allowlist` and may run commands in groups; falls back to `allowFrom` when empty |
153
154
  | `groups` | object | `{}` | Per-group overrides (e.g. `{ "id@chatroom": { "requireMention": false, "enabled": true, "groupPolicy": "allowlist", "allowFrom": ["wxid_..."] } }`) |
154
155
  | `pollIntervalMs` | integer | `1000` | Message polling interval |
155
156
  | `authPollIntervalMs` | integer | `30000` | Auth status check interval |
156
157
 
158
+ Access decisions (DM and group senders, per-group overrides, control commands, @mention
159
+ gating) are made by OpenClaw's channel ingress policy, so `accessGroup:<name>` entries from
160
+ the top-level `accessGroups` config work in every allowlist. The pairing store is not
161
+ used: WeChat has no pairing flow.
162
+
157
163
  ## Development
158
164
 
159
165
  ### Build from source
package/dist/index.js CHANGED
@@ -5937,6 +5937,9 @@ async function pollMedia(client, chatId, localId, quality, log, maxAttempts = 15
5937
5937
  return lastResult;
5938
5938
  }
5939
5939
 
5940
+ // src/monitor.ts
5941
+ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
5942
+
5940
5943
  // src/access-control.ts
5941
5944
  import {
5942
5945
  buildChannelKeyCandidates,
@@ -6021,16 +6024,6 @@ function normalizeWeChatCommandBody(raw, params) {
6021
6024
  }
6022
6025
  return trimmed;
6023
6026
  }
6024
- function isWeChatSenderAllowed(senderId, allowFrom) {
6025
- if (allowFrom.includes("*")) {
6026
- return true;
6027
- }
6028
- const normalizedSender = senderId ? normalizeWeChatId(senderId) : "";
6029
- if (!normalizedSender) {
6030
- return false;
6031
- }
6032
- return allowFrom.includes(normalizedSender);
6033
- }
6034
6027
  function resolveGroupEntry(params) {
6035
6028
  const groups = params.account.groups ?? {};
6036
6029
  const normalizedChatId = normalizeWeChatId(params.chatId);
@@ -6047,13 +6040,6 @@ function resolveGroupEntry(params) {
6047
6040
  };
6048
6041
  }
6049
6042
  function resolveWeChatPolicyContext(params) {
6050
- const dmPolicy = normalizeDmPolicy2(params.account.dmPolicy);
6051
- const configuredAllowFrom = normalizeWeChatAllowFrom(params.account.allowFrom);
6052
- const configuredGroupAllowFrom = normalizeWeChatAllowFrom(params.account.groupAllowFrom);
6053
- const normalizedStoreAllowFrom = dmPolicy === "allowlist" ? [] : normalizeWeChatAllowFrom(params.storeAllowFrom);
6054
- const effectiveAllowFrom = unique([...configuredAllowFrom, ...normalizedStoreAllowFrom]);
6055
- const groupBase = configuredGroupAllowFrom.length > 0 ? configuredGroupAllowFrom : configuredAllowFrom;
6056
- const effectiveGroupAllowFrom = unique([...groupBase, ...normalizedStoreAllowFrom]);
6057
6043
  const { groupEntry, wildcardEntry } = resolveGroupEntry({
6058
6044
  account: params.account,
6059
6045
  chatId: params.chatId
@@ -6074,84 +6060,86 @@ function resolveWeChatPolicyContext(params) {
6074
6060
  defaultGroupPolicy
6075
6061
  )
6076
6062
  ) ?? fallbackGroupPolicy;
6077
- const groupAllowOverride = normalizeWeChatAllowFrom(
6063
+ const groupSenderAllowFrom = normalizeWeChatAllowFrom(
6078
6064
  firstDefined(groupEntry?.allowFrom, wildcardEntry?.allowFrom)
6079
6065
  );
6080
- const groupAllowFrom = groupAllowOverride.length > 0 ? unique([...groupAllowOverride, ...normalizedStoreAllowFrom]) : effectiveGroupAllowFrom;
6066
+ const dmPolicy = normalizeDmPolicy2(params.account.dmPolicy);
6067
+ const allowFrom = normalizeWeChatAllowFrom(params.account.allowFrom);
6068
+ const groupAllowFrom = normalizeWeChatAllowFrom(params.account.groupAllowFrom);
6069
+ const impliedOpenDmWildcard = dmPolicy === "open" && !allowFrom.includes("*");
6070
+ const scoped = impliedOpenDmWildcard ? {
6071
+ allowFrom: ["*", ...allowFrom],
6072
+ impliedOpenDmWildcard,
6073
+ groupAllowFrom: groupAllowFrom.length > 0 ? groupAllowFrom : allowFrom,
6074
+ groupAllowFromFallbackToAllowFrom: false,
6075
+ commandOwnerAllowFrom: allowFrom
6076
+ } : { allowFrom, groupAllowFrom, groupAllowFromFallbackToAllowFrom: true };
6081
6077
  return {
6082
6078
  dmPolicy,
6083
6079
  groupPolicy,
6084
6080
  requireMention,
6085
6081
  groupEnabled,
6086
- effectiveAllowFrom,
6087
- effectiveGroupAllowFrom: groupAllowFrom
6082
+ ...scoped,
6083
+ ...groupSenderAllowFrom.length > 0 ? { groupSenderAllowFrom } : {}
6088
6084
  };
6089
6085
  }
6090
- function resolveWeChatInboundAccessDecision(params) {
6091
- if (params.isGroup) {
6092
- if (!params.policy.groupEnabled) {
6093
- return { allowed: false, reason: "group-config-disabled" };
6094
- }
6095
- if (params.policy.groupPolicy === "disabled") {
6096
- return { allowed: false, reason: "groupPolicy=disabled" };
6097
- }
6098
- if (params.policy.groupPolicy === "allowlist") {
6099
- if (params.policy.effectiveGroupAllowFrom.length === 0) {
6100
- return { allowed: false, reason: "groupPolicy=allowlist (empty allowlist)" };
6101
- }
6102
- if (!isWeChatSenderAllowed(params.senderId, params.policy.effectiveGroupAllowFrom)) {
6103
- return { allowed: false, reason: "groupPolicy=allowlist (sender not allowlisted)" };
6104
- }
6105
- }
6106
- return { allowed: true, reason: `groupPolicy=${params.policy.groupPolicy}` };
6107
- }
6108
- if (params.policy.dmPolicy === "disabled") {
6109
- return { allowed: false, reason: "dmPolicy=disabled" };
6110
- }
6111
- if (params.policy.dmPolicy === "allowlist") {
6112
- if (params.policy.effectiveAllowFrom.length === 0) {
6113
- return { allowed: false, reason: "dmPolicy=allowlist (empty allowlist)" };
6114
- }
6115
- if (!isWeChatSenderAllowed(params.senderId, params.policy.effectiveAllowFrom)) {
6116
- return { allowed: false, reason: "dmPolicy=allowlist (sender not allowlisted)" };
6117
- }
6118
- }
6119
- return { allowed: true, reason: `dmPolicy=${params.policy.dmPolicy}` };
6120
- }
6121
- async function resolveWeChatCommandAuthorization(params) {
6122
- if (!params.deps.shouldComputeCommandAuthorized(params.rawBody, params.cfg)) {
6123
- return void 0;
6086
+ var WECHAT_INGRESS_IDENTITY = {
6087
+ key: "wechat-id",
6088
+ entryIdPrefix: "wechat",
6089
+ sensitivity: "pii",
6090
+ normalize: (value) => normalizeWeChatId(value) || null
6091
+ };
6092
+ function resolveWeChatIngress(params) {
6093
+ const { policy, isGroup } = params;
6094
+ const routes = [];
6095
+ if (isGroup && !policy.groupEnabled) {
6096
+ routes.push({
6097
+ id: "wechat-group",
6098
+ kind: "route",
6099
+ configured: true,
6100
+ matched: true,
6101
+ enabled: true,
6102
+ allowed: false,
6103
+ blockReason: "group_disabled"
6104
+ });
6124
6105
  }
6125
- const normalizedSenderId = normalizeWeChatId(params.senderId ?? "");
6126
- const usesStore = !params.isGroup && params.dmPolicy !== "allowlist" && params.dmPolicy !== "open";
6127
- const storeAllowFrom = usesStore ? normalizeWeChatAllowFrom(await params.deps.readAllowFromStore().catch(() => [])) : [];
6128
- const configuredAllowFrom = normalizeWeChatAllowFrom(params.allowFromForCommands);
6129
- const effectiveAllowFrom = unique([...configuredAllowFrom, ...storeAllowFrom]);
6130
- const effectiveGroupAllowFrom = configuredAllowFrom;
6131
- return params.deps.resolveCommandAuthorizedFromAuthorizers({
6132
- useAccessGroups: true,
6133
- authorizers: [
6134
- {
6135
- configured: effectiveAllowFrom.length > 0,
6136
- allowed: isWeChatSenderAllowed(normalizedSenderId, effectiveAllowFrom)
6137
- },
6138
- {
6139
- configured: effectiveGroupAllowFrom.length > 0,
6140
- allowed: isWeChatSenderAllowed(normalizedSenderId, effectiveGroupAllowFrom)
6141
- }
6142
- ]
6106
+ const groupAllowFrom = isGroup && policy.groupSenderAllowFrom || policy.groupAllowFrom;
6107
+ const mention = isGroup ? params.mention : void 0;
6108
+ const readStoreAllowFrom = params.readStoreAllowFrom;
6109
+ return params.resolve({
6110
+ channelId: "agent-wechat",
6111
+ accountId: params.accountId,
6112
+ identity: WECHAT_INGRESS_IDENTITY,
6113
+ cfg: params.cfg,
6114
+ subject: { stableId: normalizeWeChatId(params.senderId ?? "") || null },
6115
+ conversation: { kind: isGroup ? "group" : "direct", id: params.chatId },
6116
+ event: { kind: "message", authMode: "inbound", mayPair: false },
6117
+ dmPolicy: policy.dmPolicy,
6118
+ groupPolicy: policy.groupPolicy,
6119
+ policy: {
6120
+ groupAllowFromFallbackToAllowFrom: policy.groupAllowFromFallbackToAllowFrom,
6121
+ ...mention ? { activation: { requireMention: policy.requireMention, allowTextCommands: mention.allowTextCommands } } : {}
6122
+ },
6123
+ allowFrom: policy.allowFrom,
6124
+ groupAllowFrom,
6125
+ ...routes.length > 0 ? { route: routes } : {},
6126
+ ...readStoreAllowFrom ? { readStoreAllowFrom: async () => readStoreAllowFrom() } : {},
6127
+ ...mention ? { mentionFacts: { canDetectMention: true, wasMentioned: mention.wasMentioned } } : {},
6128
+ command: params.command ? {
6129
+ ...params.command,
6130
+ // Group commands are authorized by the group's sender allowlist only, as before.
6131
+ groupOwnerAllowFrom: "none",
6132
+ ...policy.commandOwnerAllowFrom && !isGroup ? { commandOwnerAllowFrom: policy.commandOwnerAllowFrom, commandGroupAllowFromFallbackToAllowFrom: false } : {}
6133
+ } : false
6143
6134
  });
6144
6135
  }
6145
- function resolveWeChatMentionGate(params) {
6146
- const implicitMention = params.implicitMention === true;
6147
- const baseWasMentioned = params.wasMentioned || implicitMention;
6148
- const shouldBypassMention = params.isGroup && params.requireMention && !baseWasMentioned && params.allowTextCommands && params.hasControlCommand && params.commandAuthorized;
6149
- const effectiveWasMentioned = baseWasMentioned || shouldBypassMention;
6150
- const shouldSkip = params.requireMention && params.canDetectMention && !effectiveWasMentioned;
6151
- return { effectiveWasMentioned, shouldSkip, shouldBypassMention };
6152
- }
6153
6136
 
6154
6137
  // src/monitor.ts
6138
+ var warnedOpenDmWildcard = /* @__PURE__ */ new Set();
6139
+ function getIngressResolver(core) {
6140
+ const runtimeResolve = core.channel.inbound?.ingress?.resolveStable;
6141
+ return runtimeResolve ?? resolveStableChannelMessageIngress;
6142
+ }
6155
6143
  var HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
6156
6144
  var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
6157
6145
  var voiceTranscriptCache = /* @__PURE__ */ new Map();
@@ -6378,7 +6366,7 @@ async function startWeChatMonitor(opts) {
6378
6366
  connected: false
6379
6367
  });
6380
6368
  }
6381
- async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, log) {
6369
+ async function prepareMessage(client, msg, chatId, chat, liveAccount, admitSender, log) {
6382
6370
  if (msg.isSelf) {
6383
6371
  log?.info?.(`[wechat:${liveAccount.accountId}] Skipping self-sent msg ${msg.localId}`);
6384
6372
  return null;
@@ -6387,11 +6375,7 @@ async function prepareMessage(client, msg, chatId, chat, liveAccount, policy, lo
6387
6375
  const senderId = msg.sender ?? chatId;
6388
6376
  const senderName = msg.senderName ?? msg.sender ?? chat.name;
6389
6377
  const wasMentioned = isGroup && msg.isMentioned === true;
6390
- const access = resolveWeChatInboundAccessDecision({
6391
- isGroup,
6392
- senderId,
6393
- policy
6394
- });
6378
+ const access = await admitSender({ isGroup, senderId });
6395
6379
  if (!access.allowed) {
6396
6380
  log?.info?.(
6397
6381
  `[wechat:${liveAccount.accountId}] Blocked by policy (${access.reason}) from ${senderId}`
@@ -6482,40 +6466,34 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6482
6466
  `[wechat:${liveAccount.accountId}] Dispatching segment: ${segment.length} msg(s), last=${msg.localId}${mediaPath ? " with model media" : ""} attachments=${attachmentPaths.length}`
6483
6467
  );
6484
6468
  const hasControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(commandBody, cfg);
6485
- const commandAuthorized = await resolveWeChatCommandAuthorization({
6469
+ const wasMentioned = segment.some((pm) => pm.isMentioned);
6470
+ const ingress = await resolveWeChatIngress({
6471
+ resolve: getIngressResolver(core),
6486
6472
  cfg,
6487
- rawBody: commandBody,
6473
+ accountId: liveAccount.accountId,
6474
+ policy,
6488
6475
  isGroup,
6476
+ chatId,
6489
6477
  senderId,
6490
- dmPolicy: policy.dmPolicy,
6491
- allowFromForCommands: isGroup ? policy.effectiveGroupAllowFrom : policy.effectiveAllowFrom,
6492
- deps: {
6493
- shouldComputeCommandAuthorized: (raw, loadedCfg) => core.channel.commands.shouldComputeCommandAuthorized(raw, loadedCfg),
6494
- resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
6495
- readAllowFromStore: async () => storeAllowFrom
6496
- }
6478
+ readStoreAllowFrom: async () => storeAllowFrom,
6479
+ command: { allowTextCommands, hasControlCommand },
6480
+ mention: { wasMentioned, allowTextCommands }
6497
6481
  });
6498
- if (isGroup && allowTextCommands && hasControlCommand && commandAuthorized !== true) {
6482
+ if (ingress.ingress.admission === "skip") {
6499
6483
  log?.info?.(
6500
- `[wechat:${liveAccount.accountId}] Dropping unauthorized group control command from ${senderId} in ${chatId}`
6484
+ `[wechat:${liveAccount.accountId}] Skipping group segment (${ingress.ingress.reasonCode}) in ${chatId}`
6501
6485
  );
6502
6486
  return "ignored";
6503
6487
  }
6504
- const mentionGate = resolveWeChatMentionGate({
6505
- isGroup,
6506
- requireMention: policy.requireMention,
6507
- canDetectMention: true,
6508
- wasMentioned: segment.some((pm) => pm.isMentioned),
6509
- allowTextCommands,
6510
- hasControlCommand,
6511
- commandAuthorized: commandAuthorized === true
6512
- });
6513
- if (isGroup && mentionGate.shouldSkip) {
6488
+ const dmUnauthorizedCommand = !isGroup && ingress.ingress.reasonCode === "control_command_unauthorized";
6489
+ if (ingress.ingress.admission !== "dispatch" && !dmUnauthorizedCommand) {
6514
6490
  log?.info?.(
6515
- `[wechat:${liveAccount.accountId}] Skipping group segment (mention required) in ${chatId}`
6491
+ `[wechat:${liveAccount.accountId}] Dropping segment (${ingress.ingress.reasonCode}) from ${senderId} in ${chatId}`
6516
6492
  );
6517
6493
  return "ignored";
6518
6494
  }
6495
+ const commandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(commandBody, cfg) ? ingress.commandAccess.authorized : void 0;
6496
+ const effectiveWasMentioned = ingress.activationAccess.effectiveWasMentioned ?? wasMentioned;
6519
6497
  try {
6520
6498
  for (const item of segment) {
6521
6499
  const attachment = item.attachment;
@@ -6632,7 +6610,7 @@ async function dispatchSegment(segment, client, chatId, chat, liveAccount, polic
6632
6610
  Provider: "agent-wechat",
6633
6611
  Surface: "agent-wechat",
6634
6612
  MessageSid: `wechat:${chatId}:${msg.localId}`,
6635
- WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : void 0,
6613
+ WasMentioned: isGroup ? effectiveWasMentioned : void 0,
6636
6614
  CommandAuthorized: commandAuthorized,
6637
6615
  OriginatingChannel: "agent-wechat",
6638
6616
  OriginatingTo: `wechat:${chatId}`,
@@ -6739,9 +6717,31 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6739
6717
  const policy = resolveWeChatPolicyContext({
6740
6718
  account: liveAccount,
6741
6719
  cfg,
6742
- chatId,
6743
- storeAllowFrom
6720
+ chatId
6744
6721
  });
6722
+ if (policy.impliedOpenDmWildcard && !warnedOpenDmWildcard.has(liveAccount.accountId)) {
6723
+ warnedOpenDmWildcard.add(liveAccount.accountId);
6724
+ log?.info?.(
6725
+ `[wechat:${liveAccount.accountId}] dmPolicy is "open" but allowFrom has no "*"; treating it as ["*"]. Add "*" to channels.agent-wechat.allowFrom to make this explicit.`
6726
+ );
6727
+ }
6728
+ const resolveIngress = getIngressResolver(core);
6729
+ const admitSender = async ({ isGroup: isGroup2, senderId }) => {
6730
+ const result = await resolveWeChatIngress({
6731
+ resolve: resolveIngress,
6732
+ cfg,
6733
+ accountId: liveAccount.accountId,
6734
+ policy,
6735
+ isGroup: isGroup2,
6736
+ chatId,
6737
+ senderId,
6738
+ readStoreAllowFrom: async () => storeAllowFrom
6739
+ });
6740
+ return {
6741
+ allowed: result.ingress.admission === "dispatch",
6742
+ reason: result.ingress.reasonCode
6743
+ };
6744
+ };
6745
6745
  const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
6746
6746
  cfg,
6747
6747
  surface: "agent-wechat"
@@ -6808,7 +6808,7 @@ async function processUnreadChat(client, chat, lastSeenId, account, cfg, log, sk
6808
6808
  log?.info?.(
6809
6809
  `[wechat:${liveAccount.accountId}] Processing msg ${msg.localId}: type=${msg.type}, sender=${msg.sender}, isSelf=${msg.isSelf}`
6810
6810
  );
6811
- const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, policy, log);
6811
+ const pm = await prepareMessage(client, msg, chatId, chat, liveAccount, admitSender, log);
6812
6812
  if (pm) {
6813
6813
  processed.push(pm);
6814
6814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-wechat/agent-wechat",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",