Commit 1a63dbe0bc3ceb4bff93fd4634e1c6f87bc436ce
1 parent
508fbfb3
fix: sqlcipher tracker _parseRows 支持多行 TEXT 字段(修群聊 @ 检测失败)
症状:群里 @小火子 发消息,auto-reply 完全没响应,日志里只有 [NOTIF]
没有 [GROUP-AT],但 DB 里这条消息确实存在。
根因:sqlcipher .mode line 输出多行 TEXT(如 message.content 是
"yibooo:\n@小火子 在吗")时长这样:
content = yibooo:
@小火子 在吗
createTime = 1779591001000
旧 _parseRows 看到第二行 "@小火子 在吗" 没有 ": " 或 " = " 分隔符,
就直接 IGNORED,导致 content 被截断成 "yibooo:"。isAtMe("yibooo:")
返回 false,replyType=null,handleMessage 静默 return(没 log)。
修复:
- 引入 KNOWN_COLS 白名单(msgId/type/isSend/talker/content/createTime),
只在 key 是已知列名时才当作新字段,避免内容里碰巧带 "xxx = yyy"
误判为新字段
- 不匹配新字段的行作为上一字段 value 的续行追加(带 \n)
- 添加单元测试式的语义注释解释 .mode line 输出格式
unit test (synthetic input):
input: 'content = yibooo:\n@小火子 在吗\ncreateTime = 1779...'
before: { content: 'yibooo:', createTime: '1779...' } ❌
after: { content: 'yibooo:\n@小火子 在吗', createTime: '1779...' } ✅
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing
1 changed file
with
35 additions
and
10 deletions
server/services/wechat/android-wechat-tracker-sqlcipher.js
| ... | ... | @@ -188,25 +188,50 @@ class AndroidWechatTrackerSQLCipher extends EventEmitter { |
| 188 | 188 | const messages = []; |
| 189 | 189 | if (!text) return messages; |
| 190 | 190 | |
| 191 | - // .mode line 输出格式:每行一个 "column = value",记录之间用空行分隔 | |
| 191 | + // .mode line 输出格式: | |
| 192 | + // key1 = value1 | |
| 193 | + // key2 = value2_line1 | |
| 194 | + // value2_line2 <-- 续行(带前导空格对齐,不含 ":"/"=") | |
| 195 | + // key3 = value3 | |
| 196 | + // (空行分隔下一条记录) | |
| 197 | + // | |
| 198 | + // 注意:多行 TEXT 字段(如 message.content)的换行在输出里保留, | |
| 199 | + // 续行没有 ": " / " = ",必须并入上一个 key 的 value,否则会 | |
| 200 | + // 丢内容并导致 @ / 引用等检测失败。 | |
| 192 | 201 | const records = []; |
| 193 | 202 | let current = {}; |
| 194 | - for (const line of text.split('\n')) { | |
| 195 | - const trimmed = line.trim(); | |
| 196 | - if (!trimmed) { | |
| 203 | + let lastKey = null; | |
| 204 | + // 已知所有合法 column 名(用于判断一行是不是新 key=value),避免 | |
| 205 | + // 把内容里碰巧带 "xxx = yyy" 的文本误识别为新字段 | |
| 206 | + const KNOWN_COLS = new Set(['msgId', 'type', 'isSend', 'talker', 'content', 'createTime']); | |
| 207 | + | |
| 208 | + for (const rawLine of text.split('\n')) { | |
| 209 | + if (!rawLine.trim()) { | |
| 197 | 210 | if (Object.keys(current).length > 0) { |
| 198 | 211 | records.push(current); |
| 199 | 212 | current = {}; |
| 213 | + lastKey = null; | |
| 200 | 214 | } |
| 201 | 215 | continue; |
| 202 | 216 | } |
| 203 | - // sqlcipher .mode line 输出格式: "key: value" 或 "key = value" | |
| 204 | - let sepIdx = trimmed.indexOf(': '); | |
| 205 | - if (sepIdx < 0) sepIdx = trimmed.indexOf(' = '); | |
| 217 | + | |
| 218 | + // 试图解析为 "key = value" 或 "key: value";只在 key 是已知列名时才当作新字段 | |
| 219 | + let sepIdx = rawLine.indexOf(' = '); | |
| 220 | + let sepLen = 3; | |
| 221 | + if (sepIdx < 0) { sepIdx = rawLine.indexOf(': '); sepLen = 2; } | |
| 222 | + let isNewField = false; | |
| 206 | 223 | if (sepIdx > 0) { |
| 207 | - const key = trimmed.substring(0, sepIdx).trim(); | |
| 208 | - const val = trimmed.substring(sepIdx + 2).trim(); | |
| 209 | - current[key] = val; | |
| 224 | + const key = rawLine.substring(0, sepIdx).trim(); | |
| 225 | + if (KNOWN_COLS.has(key)) { | |
| 226 | + current[key] = rawLine.substring(sepIdx + sepLen); | |
| 227 | + lastKey = key; | |
| 228 | + isNewField = true; | |
| 229 | + } | |
| 230 | + } | |
| 231 | + if (!isNewField && lastKey) { | |
| 232 | + // 续行:sqlcipher 给续行加了和首行 value 等宽的前导空白对齐, | |
| 233 | + // 去掉与上一字段名等长 + " = " 的前缀长度即可;保守起见用 trimStart | |
| 234 | + current[lastKey] += '\n' + rawLine.replace(/^\s+/, ''); | |
| 210 | 235 | } |
| 211 | 236 | } |
| 212 | 237 | if (Object.keys(current).length > 0) { | ... | ... |