/** * 回放测试:直接加载生产文件 tiktok_stats.js,只把 httpGetJson 换成「录制的真实响应」, * 用真实数据验证 getCommentsViaMirror 是否能正确跨过 p_400 那个「假 hasMore=false」页。 */ const fs = require('fs'); const path = require('path'); const pagesDir = path.join(__dirname, 'pages'); const rec = {}; // cursor -> parsed json fs.readdirSync(pagesDir).filter(f => /^p_\d+\.json$/.test(f)).forEach(f => { const cur = parseInt(f.match(/_(\d+)\./)[1], 10); rec[cur] = fs.readFileSync(path.join(pagesDir, f), 'utf8'); }); console.log('已录制页面 cursor:', Object.keys(rec).map(Number).sort((a, b) => a - b).join(', ')); let httpCalls = 0; const mock = (url) => { httpCalls++; const m = /cursor=(\d+)/.exec(url); const cur = m ? parseInt(m[1], 10) : -1; if (rec[cur] === undefined) return null; // 未录制 → 模拟空页 try { return JSON.parse(rec[cur]); } catch (e) { return null; } }; const logs = []; const consoleStub = { log: (...a) => { const s = a.join(' '); logs.push(s); console.log(' | ' + s); }, show: () => { }, error: () => { }, warn: () => { } }; const filesStub = { exists: () => false, read: () => '', write: () => { }, getSdcardPath: () => '/sdcard', cwd: () => '.' }; const javaStub = { lang: { Thread: { sleep: () => { } } } }; let src = fs.readFileSync(path.join(__dirname, '..', 'tiktok_stats.js'), 'utf8'); // 去掉结尾的 main() 调用,避免真跑主流程 src = src.replace(/\nmain\(\);\s*$/, '\n'); const factory = new Function('console', 'files', 'java', 'sleep', '__mock', '__dirname_', 'require', 'process', src + ` ;httpGetJson = function (url, headers, tag) { return __mock(url, headers, tag); }; ;return { CONFIG: CONFIG, getCommentsViaMirror: getCommentsViaMirror, getRepliesViaMirror: getRepliesViaMirror, normComment: normComment }; `); const api = factory(consoleStub, filesStub, javaStub, () => { }, mock, __dirname, require, process); console.log('\n=== 开始回放 getCommentsViaMirror(limit=0) ==='); const got = api.getCommentsViaMirror('7552202593888603410', 0); // 期望值 let expectUnique = 0; const seenAll = {}; Object.keys(rec).map(Number).sort((a, b) => a - b).forEach(cur => { const j = JSON.parse(rec[cur]); (j.data.comments || []).forEach(c => { if (!seenAll[c.id]) { seenAll[c.id] = 1; expectUnique++; } }); }); console.log('\n=== 结果 ==='); console.log('HTTP 调用次数 :', httpCalls); console.log('抓到的评论数 :', got.length); console.log('录制数据的唯一评论数:', expectUnique); console.log('是否完全一致 :', got.length === expectUnique ? '✓ 通过' : '✗ 不一致'); const bad = logs.filter(s => s.indexOf('假 hasMore=false') !== -1 || s.indexOf('hasMore=false,但实际返回') !== -1); console.log('识别出的假 hasMore :', bad.length, '次'); bad.forEach(s => console.log(' →', s)); const emptyLogs = logs.filter(s => s.indexOf('空(已重试') !== -1); console.log('空页提示 :', emptyLogs.length, '次(预期 3 次后停止)'); console.log('前 5 条评论样例 :'); got.slice(0, 5).forEach((c, i) => console.log(' ' + (i + 1) + '. [' + c.user + ' | 赞' + c.likes + ' | 回复' + c.replyCount + '] ' + c.text.slice(0, 40))); console.log('第 426 条之后是否还有数据(旧版就在此断掉):', got.length > 426 ? '✓ 有,共 ' + got.length + ' 条(旧版只有 426)' : '✗ 仍然断在 426');