Commit 12f2bd4b36a65f3074d7d34dfae85bfe06ba2f98
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>
Showing
2 changed files
with
23 additions
and
4 deletions
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(/ /g, ' ').replace(/&/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); | ... | ... |