Commit c7863acf867c0f089df7a2fff7e9db5a2007a927

Authored by yibo
1 parent fd8d30a9

fix: 授权指令支持引用回复(type 57 appmsg)

症状:"同意 宜创AI全员群" 用引用回复发出后没被识别,pending 没清。

根因:WeChat 引用回复存成 type=57 appmsg XML,真正的回复文本在
<title> 里、被引用的原消息在 <refermsg> 里。parseApprovalCommand
拿到整段 XML 匹配不上 ^(同意|...) 正则。

修复:
- parseApprovalCommand 检测 <appmsg>/<refermsg>,提取 <title> 作为
  回复文本(指令动词),提取 <refermsg> 作为 quotedContext
- handleApproval 目标匹配优先级:显式 target → quotedContext 里出现的
  pending displayName(引用哪条授权请求就同意哪个)→ 最近一个 pending

实测:引用回复"同意"(refermsg 含"宜创")→ [AUTH] Approved 宜创AI全员群
→ 加白名单 + 自动回复 pending 的 "@小火子 在吗"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
server/services/wechat/auto-reply.js
... ... @@ -587,15 +587,25 @@ function escapeRegex(str) {
587 587 */
588 588 function parseApprovalCommand(content) {
589 589 if (!content) return null;
  590 + let replyText = content;
  591 + let quotedContext = '';
  592 + // 引用回复(type 57 appmsg):真正的回复文本在 <title> 里,
  593 + // 被引用的原消息内容在 <refermsg> 里(用来判断"同意哪条")
  594 + if (content.includes('<appmsg') || content.includes('<refermsg>')) {
  595 + const tm = content.match(/<title>([\s\S]*?)<\/title>/);
  596 + if (tm) replyText = tm[1];
  597 + const rm = content.match(/<refermsg>([\s\S]*?)<\/refermsg>/);
  598 + if (rm) quotedContext = rm[1];
  599 + }
590 600 // 群消息前缀剥离(owner 私聊一般没有,保险起见)
591   - let t = content;
  601 + let t = replyText;
592 602 const ci = t.indexOf(':\n');
593 603 if (ci > 0 && ci < 30) t = t.substring(ci + 2);
594 604 t = t.trim();
595 605 const m = t.match(/^(同意|授权|批准|approve|allow|yes|拒绝|忽略|不同意|deny|no)\s*(.*)$/i);
596 606 if (!m) return null;
597 607 const approve = /同意|授权|批准|approve|allow|yes/i.test(m[1]);
598   - return { approve, target: (m[2] || '').trim() };
  608 + return { approve, target: (m[2] || '').trim(), quotedContext };
599 609 }
600 610  
601 611 // ═══════════════════════════════════════════════════════════════
... ... @@ -973,14 +983,17 @@ class AutoReplyBot {
973 983  
974 984 // 处理 owner 的授权指令
975 985 async handleApproval(cmd) {
976   - const { approve, target } = cmd;
  986 + const { approve, target, quotedContext } = cmd;
977 987 const pendings = this.whitelist.pendingTalkers();
978 988 if (pendings.length === 0) {
979 989 await this.sendPlainTo(CONFIG.ownerWxid, CONFIG.ownerDisplayName, '当前没有待授权的请求。');
980 990 return;
981 991 }
982 992  
983   - // 选定目标:指定了就按 wxid / 昵称 / senderName 匹配,没指定取最近一个
  993 + // 选定目标,优先级:
  994 + // 1. 显式 target("同意 宜创AI全员群")按 wxid/昵称/senderName 匹配
  995 + // 2. 引用回复的被引用内容里出现哪个 pending 的 displayName("同意"引用了某条授权请求)
  996 + // 3. 都没有 → 取最近一个 pending
984 997 let talker = null;
985 998 if (target) {
986 999 talker = pendings.find(t => {
... ... @@ -988,7 +1001,13 @@ class AutoReplyBot {
988 1001 return t === target
989 1002 || p.displayName === target
990 1003 || p.senderName === target
991   - || (p.displayName && p.displayName.includes(target));
  1004 + || (p.displayName && (p.displayName.includes(target) || target.includes(p.displayName)));
  1005 + });
  1006 + }
  1007 + if (!talker && quotedContext) {
  1008 + talker = pendings.find(t => {
  1009 + const dn = this.whitelist.pending[t].displayName;
  1010 + return dn && quotedContext.includes(dn);
992 1011 });
993 1012 }
994 1013 if (!talker) talker = pendings[pendings.length - 1];
... ...