Commit 1a63dbe0bc3ceb4bff93fd4634e1c6f87bc436ce

Authored by yibo
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>
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) {
... ...