pc-selftest-id.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * pc-selftest-id.js — 在 PC 上验证 ID 提取的纯函数(不碰设备)
  3. *
  4. * 为什么单独一份:设备的自检要过 MCP 下发,一轮几十秒;而 parse/filters
  5. * 是**零依赖纯函数**,拼起来就能在 PC 上跑。先用它把逻辑验对,
  6. * 再上设备验"真机环境里也没问题",两步分开,排查时能立刻区分是逻辑错还是环境错。
  7. *
  8. * 用法:node tools/pc-selftest-id.js
  9. */
  10. 'use strict';
  11. const fs = require('fs');
  12. const path = require('path');
  13. const vm = require('vm');
  14. const SRC = path.join(__dirname, '..', 'src');
  15. /* 只拼纯函数模块 —— 它们不引用 files/device/sleep 这些 AutoX 全局 */
  16. const MODULES = ['parse', 'filters', 'uidset'];
  17. const sandbox = {
  18. console: console,
  19. Date: Date, Math: Math, JSON: JSON,
  20. String: String, Number: Number, Object: Object,
  21. Array: Array, RegExp: RegExp, isNaN: isNaN, parseInt: parseInt,
  22. TikTokAuto: {}
  23. };
  24. /* uidset 与 filters 在函数体内取 logger / files,这里给最小桩 */
  25. sandbox.TikTokAuto.logger = {
  26. info: () => { }, warn: () => { }, error: () => { }, step: () => { }
  27. };
  28. sandbox.files = {
  29. exists: () => false,
  30. read: () => '',
  31. append: () => { },
  32. ensureDir: () => { }
  33. };
  34. vm.createContext(sandbox);
  35. let source = 'var TikTokAuto = this.TikTokAuto;\n';
  36. for (const name of MODULES) {
  37. source += fs.readFileSync(path.join(SRC, `${name}.js`), 'utf8') + '\n';
  38. }
  39. vm.runInContext(source, sandbox);
  40. const parse = sandbox.TikTokAuto.parse;
  41. const filters = sandbox.TikTokAuto.filters;
  42. const uidset = sandbox.TikTokAuto.uidset;
  43. let pass = 0;
  44. let fail = 0;
  45. function check(name, condition, detail) {
  46. if (condition === true) {
  47. pass += 1;
  48. return;
  49. }
  50. fail += 1;
  51. console.log('FAIL ' + name + (detail === undefined ? '' : ' → ' + detail));
  52. }
  53. /* ---------------------------------------------------------- 计数解析 */
  54. const CASES = [
  55. ['594', 594], ['2,491', 2491], ['1.6M', 1600000], ['253.9M', 253900000],
  56. ['245K', 245000], ['371.7K', 371700], ['1.2万', 12000], ['1.5亿', 150000000],
  57. ['1.6M followers', 1600000], ['594 followers · 2.5K likes', 594],
  58. ['0', 0], ['12.3K', 12300], ['1,234', 1234], ['100万', 1000000]
  59. ];
  60. for (const [input, want] of CASES) {
  61. const got = parse.count(input);
  62. check(`parseCount("${input}") = ${want}`, got === want, `实际 ${got}`);
  63. }
  64. check('parseCount 空串 → null', parse.count('') === null);
  65. check('parseCount 非数字 → null', parse.count('abc') === null);
  66. check('parseCount 全角数字 "1.2万"', parse.count('1.2万') === 12000, String(parse.count('1.2万')));
  67. /* ---------------------------------------------------------- 语言 */
  68. check('中文 → zh', parse.language('小美爱穿搭') === 'zh');
  69. check('英文 → en', parse.language('Fashion Girl') === 'en');
  70. check('中文混英文 → zh', parse.language('小美的Fashion') === 'zh', parse.language('小美的Fashion'));
  71. check('英文混一个字 → en', parse.language('Fashion Girl 小') === 'en', parse.language('Fashion Girl 小'));
  72. check('纯 emoji → none', parse.language('🥷🏿💗✨') === 'none');
  73. check('阿拉伯文 → none(没有中英线索)', parse.language('عايشه حسين') === 'none', parse.language('عايشه حسين'));
  74. check('zh 不放行 en', parse.languageMatches('zh', 'en') === false);
  75. check('en 不放行 zh', parse.languageMatches('en', 'zh') === false);
  76. check('any 全放行', parse.languageMatches('any', 'en') === true && parse.languageMatches('any', 'none') === true);
  77. /* ---------------------------------------------------------- 区间 */
  78. check('未设区间 → 无脑通过', parse.inRange(null, 0, 0) === true);
  79. check('值在内', parse.inRange(500, 100, 1000) === true);
  80. check('值在下限外', parse.inRange(50, 100, 1000) === false);
  81. check('上限 0 = 不设限', parse.inRange(999999, 100, 0) === true);
  82. check('只设上限', parse.inRange(500, 0, 1000) === true && parse.inRange(5000, 0, 1000) === false);
  83. check('设了区间但值为 null → 拦', parse.inRange(null, 100, 1000) === false);
  84. check('isRangeOpen 双向为 0 才算空', parse.isRangeOpen(0, 0) === true && parse.isRangeOpen(1, 0) === false);
  85. /* ---------------------------------------------------------- 用户名 */
  86. check('剥 @', parse.username('@abc') === 'abc');
  87. check('剥 FID 隔离符', parse.username('\u2068ajman_fashion_week\u2069') === 'ajman_fashion_week',
  88. JSON.stringify(parse.username('\u2068ajman_fashion_week\u2069')));
  89. check('合法用户名', parse.looksLikeUsername('user3670597276145') === true);
  90. check('带点的用户名', parse.looksLikeUsername('mukhawar.shop') === true);
  91. check('文案不是用户名', parse.looksLikeUsername('This is a caption') === false);
  92. check('过长不是用户名', parse.looksLikeUsername('a'.repeat(31)) === false);
  93. /* ---------------------------------------------------------- 去重集合 */
  94. const set = uidset.create();
  95. check('首次 add → true', set.add('a') === true);
  96. check('重复 add → false', set.add('a') === false);
  97. check('size 只算唯一值', set.size() === 1);
  98. check('has 正确', set.has('a') === true && set.has('b') === false);
  99. /* ---------------------------------------------------------- 过滤 */
  100. check('无过滤全放行', filters.judge({ uid: 'x' }, {}).ok === true);
  101. check('私密被拦', filters.judge({ uid: 'x', isPrivate: true }, { private: true }).ok === false);
  102. check('公开放行', filters.judge({ uid: 'x', isPrivate: false }, { private: true }).ok === true);
  103. check('仅中文 + 中文昵称', filters.judge({ uid: 'x', nickname: '小美' }, { langRegion: 'zh' }).ok === true);
  104. check('仅中文 + 英文昵称被拦', filters.judge({ uid: 'x', nickname: 'Fashion' }, { langRegion: 'zh' }).ok === false);
  105. check('仅中文 + 纯 emoji 被拦', filters.judge({ uid: 'x', nickname: '🥷🏿' }, { langRegion: 'zh' }).ok === false);
  106. check('取样=个签:昵称英文但个签中文 → 放行',
  107. filters.judge({ uid: 'x', nickname: 'Fashion', signature: '小美' },
  108. { langRegion: 'zh', nicknameCheck: 'sign' }).ok === true);
  109. check('取样=昵称:同上应当被拦',
  110. filters.judge({ uid: 'x', nickname: 'Fashion', signature: '小美' },
  111. { langRegion: 'zh', nicknameCheck: 'nick' }).ok === false);
  112. check('粉丝达标', filters.judge({ uid: 'x', fans: 5940 }, { fans: { min: 1000, max: 0 } }).ok === true);
  113. check('粉丝不足', filters.judge({ uid: 'x', fans: 594 }, { fans: { min: 1000, max: 0 } }).ok === false);
  114. check('粉丝读不到 → 拦', filters.judge({ uid: 'x', fans: null }, { fans: { min: 1000, max: 0 } }).ok === false);
  115. check('粉丝上界', filters.judge({ uid: 'x', fans: 99999 }, { fans: { min: 0, max: 10000 } }).ok === false);
  116. const videoVerdict = filters.judge({ uid: 'x' }, { videos: { min: 10, max: 0 } });
  117. check('视频量不拦人', videoVerdict.ok === true);
  118. check('视频量要如实告知未生效', videoVerdict.soft.indexOf('视频量') >= 0, videoVerdict.soft);
  119. check('无头像:默认剪影被拦', filters.judge({ uid: 'x', hasAvatar: false }, { noAvatar: true }).ok === false);
  120. check('无头像:有头像放行', filters.judge({ uid: 'x', hasAvatar: true }, { noAvatar: true }).ok === true);
  121. const noAvatarUnknown = filters.judge({ uid: 'x', hasAvatar: null }, { noAvatar: true });
  122. check('无头像:判不了时放行', noAvatarUnknown.ok === true);
  123. check('无头像:判不了要告知', noAvatarUnknown.soft.indexOf('无头像') >= 0, noAvatarUnknown.soft);
  124. check('无过滤 → 不必开用户页', filters.needsProfile({}) === false);
  125. check('只有视频量 → 不必开用户页', filters.needsProfile({ videos: { min: 10, max: 0 } }) === false);
  126. check('有粉丝量 → 必须开', filters.needsProfile({ fans: { min: 10, max: 0 } }) === true);
  127. check('有语言 → 必须开', filters.needsProfile({ langRegion: 'zh' }) === true);
  128. /* 「读不到资料」必须拦下 —— 放行等于"打不开的人全都通过",
  129. * 过滤看着在跑、实际放进一批没检查过的账号,比不过滤更糟。
  130. * 但快路径(本来不需要资料)仍然要放行。 */
  131. check('需要资料但拿不到 → 拦下', filters.judge(null, { fans: { min: 1000, max: 0 } }).ok === false);
  132. check('不需要资料且没有资料 → 放行(快路径)', filters.judge(null, {}).ok === true);
  133. /* ---------------------------------------------------------- 文件名 */
  134. const name = parse.timestampName('uid', new Date(2026, 9, 20, 18, 31, 0));
  135. check('文件名带时分', name === 'uid_20261020_1831.txt', name);
  136. console.log('');
  137. console.log(`通过 ${pass} 项,失败 ${fail} 项`);
  138. process.exit(fail === 0 ? 0 : 1);