| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- /**
- * uidset.js — 去重集合与结果文件
- *
- * ## 三种"已经见过"
- * 1. **本轮已收**:这次运行里已经写进产物文件的;
- * 2. **历史已收**:之前各轮跑过的(`uid_extracted.txt`,只追加不删除);
- * 3. **输入里已有**:文本取材那份文件本身就列着的("裂变"时,"输入里的人"不该再算新收获)。
- * 三者合成一个集合,`add` 之后就不再重复。区别只在**装载来源**,判定逻辑是同一个。
- *
- * ## 为什么落盘是"追加"而不是"最后一次性写"
- * 用户点了停止、或脚本被系统杀掉时,已经抓到的部分不能全丢。
- * 所以每收 N 个就 append 一次 —— 代价是几条多余的系统调用,收益是"停在哪都留着"。
- *
- * ## 为什么不用 files 的删除接口
- * 引擎产物要过 MCP 的危险调用黑名单(`build.js` 的 `DANGEROUS_RULES` 里有一条按
- * 词边界匹配 `remove` 加左括号),命中后每次下发都要求人工确认。
- * 所以这里**只追加不删除** —— 需要"重来"时由用户自己在文件管理器里删。
- *
- * 依赖:TikTokAuto.logger(调用时取)
- */
- TikTokAuto.uidset = (function () {
- 'use strict';
- /** 每收这么多个就 append 一次。20 是"丢掉的最多几个"与"写盘频率"的折中。 */
- var FLUSH_EVERY = 20;
- /**
- * 去重集合。
- *
- * 内部用普通对象而不是 Set:Rhino 对 ES6 Set 的支持在各版本不一致,
- * 而对象字面量的键查找在所有版本上都是稳的。uid 只会是字母数字下划线点,不会撞 `__proto__`。
- */
- function create() {
- var seen = {};
- var count = 0;
- return {
- /** 见过没有 */
- has: function (uid) {
- return seen[uid] === true;
- },
- /** 记一笔;返回 true 表示这是新加的(之前没见过) */
- add: function (uid) {
- if (seen[uid] === true) {
- return false;
- }
- seen[uid] = true;
- count += 1;
- return true;
- },
- /** 已记多少(含历史装载的) */
- size: function () {
- return count;
- }
- };
- }
- /**
- * 读一批文本行,返回去重后的 uid 数组。
- * 跳过空行、`#` 注释行、以及带 @ 的行(有些用户会直接粘贴 "@name",我们容忍这种输入)。
- */
- function readUidFile(path) {
- var out = [];
- try {
- if (!path || !files.exists(path)) {
- return out;
- }
- var lines = String(files.read(path)).split(/\r?\n/);
- for (var i = 0; i < lines.length; i += 1) {
- var uid = TikTokAuto.parse.username(lines[i]);
- if (uid !== '' && uid.charAt(0) !== '#') {
- out.push(uid);
- }
- }
- } catch (error) {
- TikTokAuto.logger.warn('uidset', '读取 ' + path + ' 失败:' + error);
- }
- return out;
- }
- /**
- * 从历史文件装载去重集合。
- * 文件不存在 = 第一次跑,不是错误。
- */
- function loadHistory(set, path) {
- var list = readUidFile(path);
- for (var i = 0; i < list.length; i += 1) {
- set.add(list[i]);
- }
- TikTokAuto.logger.info('uidset', '历史去重装载 ' + list.length + ' 条(' + path + ')');
- return list.length;
- }
- /**
- * 结果写入器。
- *
- * 两个文件同时写:
- * · `本次`:这一轮新收的(新建,给人看/给下游吃);
- * · `累计`:所有轮次的(追加,跨轮去重的基准)。
- *
- * @param options { runPath, historyPath, dryRun, historyEnabled }
- */
- function createWriter(options) {
- var pending = [];
- var collected = [];
- var written = 0;
- var flushFailed = false;
- function flush() {
- if (pending.length === 0) {
- return;
- }
- var block = pending.join('\n') + '\n';
- pending = [];
- if (options.dryRun) {
- return; /* 演练模式:不碰任何文件 */
- }
- try {
- files.append(options.runPath, block);
- if (options.historyEnabled) {
- files.append(options.historyPath, block);
- }
- } catch (error) {
- /* 写盘失败要留痕但不能中断 —— 用户更需要"继续抓"而不是"因为一个文件写不进去全停"。
- * 只报一次,避免几百条一模一样的警告把日志刷满。 */
- if (!flushFailed) {
- flushFailed = true;
- TikTokAuto.logger.error('uidset', '写结果文件失败:' + error);
- }
- }
- }
- return {
- /** 收一个 uid。调用方保证已经过去重。 */
- write: function (uid) {
- pending.push(uid);
- /* 同时留一份在内存里:收尾阶段要拿它去换数字 uid(见 resolve.js)。
- * 从文件读一遍也行,但那样得处理"文件还没 flush 完"的时序问题,
- * 而这里的量级(几十到几千)远没到需要省内存的地步。 */
- collected.push(uid);
- written += 1;
- if (pending.length >= FLUSH_EVERY) {
- flush();
- }
- },
- /** 本轮收到的全部 uid(按收取顺序) */
- list: function () {
- return collected.slice(0);
- },
- /** 已写多少个 */
- count: function () {
- return written;
- },
- /** 收尾:把剩下的刷出去。**收到停止信号时也必须调它**,否则"停了但文件是空的"。 */
- close: function () {
- flush();
- return written;
- },
- /** 是否发生过写盘失败(给 summary 用) */
- hadFailure: function () {
- return flushFailed;
- }
- };
- }
- /** 确保目录存在(结果文件与累计文件同目录) */
- function ensureDir(dir) {
- try {
- files.ensureDir(dir + '/');
- return true;
- } catch (error) {
- TikTokAuto.logger.error('uidset', '创建目录失败 ' + dir + ':' + error);
- return false;
- }
- }
- return {
- FLUSH_EVERY: FLUSH_EVERY,
- create: create,
- readUidFile: readUidFile,
- loadHistory: loadHistory,
- createWriter: createWriter,
- ensureDir: ensureDir
- };
- }());
|