Commit 12f2bd4b36a65f3074d7d34dfae85bfe06ba2f98

Authored by yibo
1 parent 5e58864a

fix: 引用回复消息(type=822083633)被 tracker 过滤 + @ 检测失败

症状:owner 用引用回复发"...@小火子",机器人完全没反应,日志只有
[NOTIF] 没有 [GROUP-AT]。

根因:
1. 引用回复在 message 表的 type=822083633(0x31000031 appmsg),
   tracker fetchNew 的 `WHERE type IN (1, 10002)` 把它过滤掉,根本没 fetch
2. 即使 fetch 了,content 是 appmsg XML,"@小火子" 在 <title> 里且后面
   跟 </title>(不是空格),isAtMe 的 `@昵称(\s|\n|$)` 也匹配不上

修复:
- tracker: type IN (1, 10002, 822083633)
- handleMessage 顶部规范化:appmsg/refermsg XML → 提取 <title> 文本
  重组成 "senderPrefix:\n<title>",下游 @ 检测/quote/hermes 都用干净文本
- 保留 rawContent(含 refermsg)传给 parseApprovalCommand,
  这样 owner 用引用回复发"同意"时仍能从 refermsg 判断同意哪条 pending

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
server/services/wechat/android-wechat-tracker-sqlcipher.js
... ... @@ -94,8 +94,9 @@ class AndroidWechatTrackerSQLCipher extends EventEmitter {
94 94 * 获取新消息(msgId > lastMsgId)
95 95 */
96 96 async fetchNew() {
97   - // 只查询文本消息 (type=1) 和撤回通知 (type=10002),排除自己发送的
98   - const sql = `SELECT msgId, type, isSend, talker, content, createTime FROM message WHERE msgId > ${this.lastMsgId} AND isSend = 0 AND type IN (1, 10002) ORDER BY msgId ASC;`;
  97 + // type=1 纯文本,type=10002 撤回通知,type=822083633 引用回复(appmsg)
  98 + // 引用回复的 message.type 是这个大整数(0x31000031),@小火子 在 <appmsg><title> 里
  99 + const sql = `SELECT msgId, type, isSend, talker, content, createTime FROM message WHERE msgId > ${this.lastMsgId} AND isSend = 0 AND type IN (1, 10002, 822083633) ORDER BY msgId ASC;`;
99 100 const out = await this._query(sql, 10000);
100 101 const msgs = this._parseRows(out);
101 102 if (msgs.length > 0) {
... ...
server/services/wechat/auto-reply.js
... ... @@ -788,9 +788,26 @@ class AutoReplyBot {
788 788 async handleMessage(msg) {
789 789 if (msg.type !== 'recv') return;
790 790  
791   - const { talker, content, msgId } = msg;
  791 + const { talker, msgId } = msg;
  792 + let content = msg.content;
  793 + const rawContent = msg.content; // 原始(含 appmsg XML),授权指令解析要用 refermsg
792 794 if (!talker || !content) return;
793 795  
  796 + // 引用回复(appmsg XML,type=822083633):真正的文本在 <title> 里,
  797 + // @小火子 也在 title 里且后面跟 </title>(不是空格)→ isAtMe 匹配不上。
  798 + // 这里规范化成 "senderPrefix:\n<title 文本>",让下游 @ 检测 / quote / hermes 都用干净文本。
  799 + if (content.includes('<appmsg') || content.includes('<refermsg>')) {
  800 + let prefix = '';
  801 + const pm = content.match(/^([^:\n]+):\n/);
  802 + if (pm) prefix = pm[0];
  803 + const tm = content.match(/<title>([\s\S]*?)<\/title>/);
  804 + if (tm) {
  805 + const title = tm[1].replace(/&#x20;/g, ' ').replace(/&amp;/g, '&').trim();
  806 + content = prefix + title;
  807 + log('[NORMALIZE] quote-reply →', content.substring(0, 60));
  808 + }
  809 + }
  810 +
794 811 if (CONFIG.ignoreSelf && talker === CONFIG.myWxid) return;
795 812 if (this.state.hasReplied(msgId)) return;
796 813 if (this.state.isCooldown(talker)) {
... ... @@ -823,7 +840,8 @@ class AutoReplyBot {
823 840  
824 841 if (isFromOwner) {
825 842 // owner 自己发的(私聊 / 群里 @):先看是不是授权指令
826   - const cmd = parseApprovalCommand(content);
  843 + // 用 rawContent(含 refermsg)让 parseApprovalCommand 能判断同意哪条
  844 + const cmd = parseApprovalCommand(rawContent);
827 845 if (cmd) {
828 846 this.state.markReplied(msgId, talker);
829 847 await this.handleApproval(cmd);
... ...