uidset.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /**
  2. * uidset.js — 去重集合与结果文件
  3. *
  4. * ## 三种"已经见过"
  5. * 1. **本轮已收**:这次运行里已经写进产物文件的;
  6. * 2. **历史已收**:之前各轮跑过的(`uid_extracted.txt`,只追加不删除);
  7. * 3. **输入里已有**:文本取材那份文件本身就列着的("裂变"时,"输入里的人"不该再算新收获)。
  8. * 三者合成一个集合,`add` 之后就不再重复。区别只在**装载来源**,判定逻辑是同一个。
  9. *
  10. * ## 为什么落盘是"追加"而不是"最后一次性写"
  11. * 用户点了停止、或脚本被系统杀掉时,已经抓到的部分不能全丢。
  12. * 所以每收 N 个就 append 一次 —— 代价是几条多余的系统调用,收益是"停在哪都留着"。
  13. *
  14. * ## 为什么不用 files 的删除接口
  15. * 引擎产物要过 MCP 的危险调用黑名单(`build.js` 的 `DANGEROUS_RULES` 里有一条按
  16. * 词边界匹配 `remove` 加左括号),命中后每次下发都要求人工确认。
  17. * 所以这里**只追加不删除** —— 需要"重来"时由用户自己在文件管理器里删。
  18. *
  19. * 依赖:TikTokAuto.logger(调用时取)
  20. */
  21. TikTokAuto.uidset = (function () {
  22. 'use strict';
  23. /** 每收这么多个就 append 一次。20 是"丢掉的最多几个"与"写盘频率"的折中。 */
  24. var FLUSH_EVERY = 20;
  25. /**
  26. * 去重集合。
  27. *
  28. * 内部用普通对象而不是 Set:Rhino 对 ES6 Set 的支持在各版本不一致,
  29. * 而对象字面量的键查找在所有版本上都是稳的。uid 只会是字母数字下划线点,不会撞 `__proto__`。
  30. */
  31. function create() {
  32. var seen = {};
  33. var count = 0;
  34. return {
  35. /** 见过没有 */
  36. has: function (uid) {
  37. return seen[uid] === true;
  38. },
  39. /** 记一笔;返回 true 表示这是新加的(之前没见过) */
  40. add: function (uid) {
  41. if (seen[uid] === true) {
  42. return false;
  43. }
  44. seen[uid] = true;
  45. count += 1;
  46. return true;
  47. },
  48. /** 已记多少(含历史装载的) */
  49. size: function () {
  50. return count;
  51. }
  52. };
  53. }
  54. /**
  55. * 读一批文本行,返回去重后的 uid 数组。
  56. * 跳过空行、`#` 注释行、以及带 @ 的行(有些用户会直接粘贴 "@name",我们容忍这种输入)。
  57. */
  58. function readUidFile(path) {
  59. var out = [];
  60. try {
  61. if (!path || !files.exists(path)) {
  62. return out;
  63. }
  64. var lines = String(files.read(path)).split(/\r?\n/);
  65. for (var i = 0; i < lines.length; i += 1) {
  66. var uid = TikTokAuto.parse.username(lines[i]);
  67. if (uid !== '' && uid.charAt(0) !== '#') {
  68. out.push(uid);
  69. }
  70. }
  71. } catch (error) {
  72. TikTokAuto.logger.warn('uidset', '读取 ' + path + ' 失败:' + error);
  73. }
  74. return out;
  75. }
  76. /**
  77. * 从历史文件装载去重集合。
  78. * 文件不存在 = 第一次跑,不是错误。
  79. */
  80. function loadHistory(set, path) {
  81. var list = readUidFile(path);
  82. for (var i = 0; i < list.length; i += 1) {
  83. set.add(list[i]);
  84. }
  85. TikTokAuto.logger.info('uidset', '历史去重装载 ' + list.length + ' 条(' + path + ')');
  86. return list.length;
  87. }
  88. /**
  89. * 结果写入器。
  90. *
  91. * 两个文件同时写:
  92. * · `本次`:这一轮新收的(新建,给人看/给下游吃);
  93. * · `累计`:所有轮次的(追加,跨轮去重的基准)。
  94. *
  95. * @param options { runPath, historyPath, dryRun, historyEnabled }
  96. */
  97. function createWriter(options) {
  98. var pending = [];
  99. var collected = [];
  100. var written = 0;
  101. var flushFailed = false;
  102. function flush() {
  103. if (pending.length === 0) {
  104. return;
  105. }
  106. var block = pending.join('\n') + '\n';
  107. pending = [];
  108. if (options.dryRun) {
  109. return; /* 演练模式:不碰任何文件 */
  110. }
  111. try {
  112. files.append(options.runPath, block);
  113. if (options.historyEnabled) {
  114. files.append(options.historyPath, block);
  115. }
  116. } catch (error) {
  117. /* 写盘失败要留痕但不能中断 —— 用户更需要"继续抓"而不是"因为一个文件写不进去全停"。
  118. * 只报一次,避免几百条一模一样的警告把日志刷满。 */
  119. if (!flushFailed) {
  120. flushFailed = true;
  121. TikTokAuto.logger.error('uidset', '写结果文件失败:' + error);
  122. }
  123. }
  124. }
  125. return {
  126. /** 收一个 uid。调用方保证已经过去重。 */
  127. write: function (uid) {
  128. pending.push(uid);
  129. /* 同时留一份在内存里:收尾阶段要拿它去换数字 uid(见 resolve.js)。
  130. * 从文件读一遍也行,但那样得处理"文件还没 flush 完"的时序问题,
  131. * 而这里的量级(几十到几千)远没到需要省内存的地步。 */
  132. collected.push(uid);
  133. written += 1;
  134. if (pending.length >= FLUSH_EVERY) {
  135. flush();
  136. }
  137. },
  138. /** 本轮收到的全部 uid(按收取顺序) */
  139. list: function () {
  140. return collected.slice(0);
  141. },
  142. /** 已写多少个 */
  143. count: function () {
  144. return written;
  145. },
  146. /** 收尾:把剩下的刷出去。**收到停止信号时也必须调它**,否则"停了但文件是空的"。 */
  147. close: function () {
  148. flush();
  149. return written;
  150. },
  151. /** 是否发生过写盘失败(给 summary 用) */
  152. hadFailure: function () {
  153. return flushFailed;
  154. }
  155. };
  156. }
  157. /** 确保目录存在(结果文件与累计文件同目录) */
  158. function ensureDir(dir) {
  159. try {
  160. files.ensureDir(dir + '/');
  161. return true;
  162. } catch (error) {
  163. TikTokAuto.logger.error('uidset', '创建目录失败 ' + dir + ':' + error);
  164. return false;
  165. }
  166. }
  167. return {
  168. FLUSH_EVERY: FLUSH_EVERY,
  169. create: create,
  170. readUidFile: readUidFile,
  171. loadHistory: loadHistory,
  172. createWriter: createWriter,
  173. ensureDir: ensureDir
  174. };
  175. }());