|
@@ -1,5 +1,5 @@
|
|
|
/*! tiktok-auto v1.0.0
|
|
/*! tiktok-auto v1.0.0
|
|
|
- * built 2026-10-28T14:42:55.685Z
|
|
|
|
|
|
|
+ * built 2026-10-28T14:48:00.291Z
|
|
|
* modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, interact, flow, parse, uidset, fingerprint, checkpoint, filters, profile, idsource, resolve, idflow, main
|
|
* modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, interact, flow, parse, uidset, fingerprint, checkpoint, filters, profile, idsource, resolve, idflow, main
|
|
|
* 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。
|
|
* 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。
|
|
|
*/
|
|
*/
|
|
@@ -208,6 +208,12 @@ TikTokAuto.bridge = (function () {
|
|
|
} else if (token === '--log') {
|
|
} else if (token === '--log') {
|
|
|
options.logPath = argv[i + 1];
|
|
options.logPath = argv[i + 1];
|
|
|
i += 1;
|
|
i += 1;
|
|
|
|
|
+ } else if (token === '--resume') {
|
|
|
|
|
+ /* 裸开关,不取值:断点续跑由界面点「继续」时带上 */
|
|
|
|
|
+ options.resume = true;
|
|
|
|
|
+ } else if (token === '--checkpoint') {
|
|
|
|
|
+ options.checkpointPath = argv[i + 1];
|
|
|
|
|
+ i += 1;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return options;
|
|
return options;
|
|
@@ -230,9 +236,24 @@ TikTokAuto.bridge = (function () {
|
|
|
injected.runtime = injected.runtime || {};
|
|
injected.runtime = injected.runtime || {};
|
|
|
injected.runtime.configPath = options.configPath;
|
|
injected.runtime.configPath = options.configPath;
|
|
|
}
|
|
}
|
|
|
|
|
+ /* 续跑开关只改 `id.resume`,**不动 sources** ——
|
|
|
|
|
+ * `merge` 是深合并,所以不会把配置里的取材清单覆盖掉。 */
|
|
|
|
|
+ if (options.resume) {
|
|
|
|
|
+ injected.id = injected.id || {};
|
|
|
|
|
+ injected.id.resume = injected.id.resume || {};
|
|
|
|
|
+ injected.id.resume.enabled = true;
|
|
|
|
|
+ if (options.checkpointPath) {
|
|
|
|
|
+ injected.id.resume.path = options.checkpointPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
return injected;
|
|
return injected;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 本次运行的 runId(断点里要记下"这是哪次运行写的",界面靠它把记录与断点对上) */
|
|
|
|
|
+ function currentRunId() {
|
|
|
|
|
+ return state.runId || '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 日志落盘(界面侧「执行日志」的来源)。
|
|
* 日志落盘(界面侧「执行日志」的来源)。
|
|
|
*
|
|
*
|
|
@@ -327,7 +348,10 @@ TikTokAuto.bridge = (function () {
|
|
|
/* ID 提取的 `done` 是"已收个数",而最后一次 `report` 往往发生在收下一个人之前 ——
|
|
/* ID 提取的 `done` 是"已收个数",而最后一次 `report` 往往发生在收下一个人之前 ——
|
|
|
* 于是终态里 done 会比实际少一个(实测 11 个却报 10)。
|
|
* 于是终态里 done 会比实际少一个(实测 11 个却报 10)。
|
|
|
* 收尾时按 summary 校正一次,免得界面/记录上的数字自相矛盾。 */
|
|
* 收尾时按 summary 校正一次,免得界面/记录上的数字自相矛盾。 */
|
|
|
- if (summary && typeof summary.extracted === 'number') {
|
|
|
|
|
|
|
+ if (summary && typeof summary.extractedTotal === 'number') {
|
|
|
|
|
+ /* 续跑时 extracted 只是"本轮新增",累计数才和界面上的进度一致 */
|
|
|
|
|
+ patch.done = summary.extractedTotal;
|
|
|
|
|
+ } else if (summary && typeof summary.extracted === 'number') {
|
|
|
patch.done = summary.extracted;
|
|
patch.done = summary.extracted;
|
|
|
}
|
|
}
|
|
|
report(patch);
|
|
report(patch);
|
|
@@ -340,10 +364,19 @@ TikTokAuto.bridge = (function () {
|
|
|
* 但界面只有"一句人话"这一个位置 —— 所以在这里分支,让两条链路都说得清楚。
|
|
* 但界面只有"一句人话"这一个位置 —— 所以在这里分支,让两条链路都说得清楚。
|
|
|
* 判据是 `extracted` 字段是否存在,与 task 解耦(summary 自己就带得动语义)。 */
|
|
* 判据是 `extracted` 字段是否存在,与 task 解耦(summary 自己就带得动语义)。 */
|
|
|
var isIdTask = summary && typeof summary.extracted === 'number';
|
|
var isIdTask = summary && typeof summary.extracted === 'number';
|
|
|
|
|
+ /* 续跑时"数字"要给累计口径,并明确说这是续跑 ——
|
|
|
|
|
+ * 否则用户看到"提取 5 个"会以为之前那 37 个丢了。 */
|
|
|
|
|
+ var total = summary && typeof summary.extractedTotal === 'number'
|
|
|
|
|
+ ? summary.extractedTotal
|
|
|
|
|
+ : (summary ? summary.extracted : 0);
|
|
|
|
|
+ var resumedTag = summary && summary.resumed ? '(续跑)' : '';
|
|
|
|
|
+ var thisRound = summary && summary.resumed && summary.extracted !== total
|
|
|
|
|
+ ? ',本轮新增 ' + summary.extracted + ' 个' : '';
|
|
|
|
|
|
|
|
if (status === 'stopped') {
|
|
if (status === 'stopped') {
|
|
|
return isIdTask
|
|
return isIdTask
|
|
|
- ? '已停止:本次已收 ' + summary.extracted + ' 个 ID'
|
|
|
|
|
|
|
+ ? '已停止' + resumedTag + ':累计 ' + total + ' 个 ID' + thisRound +
|
|
|
|
|
+ '(可点「继续」接着跑)'
|
|
|
: '已停止';
|
|
: '已停止';
|
|
|
}
|
|
}
|
|
|
if (status === 'finished') {
|
|
if (status === 'finished') {
|
|
@@ -351,7 +384,7 @@ TikTokAuto.bridge = (function () {
|
|
|
var tail = summary.skipped > 0 ? ',按条件跳过 ' + summary.skipped + ' 个' : '';
|
|
var tail = summary.skipped > 0 ? ',按条件跳过 ' + summary.skipped + ' 个' : '';
|
|
|
/* 生成了 uid 映射就顺带说一句 —— 用户不一定知道还有第二个文件 */
|
|
/* 生成了 uid 映射就顺带说一句 —— 用户不一定知道还有第二个文件 */
|
|
|
var mapped = summary.mapFile ? '(已生成 uid 映射)' : '';
|
|
var mapped = summary.mapFile ? '(已生成 uid 映射)' : '';
|
|
|
- return '完成:提取 ' + summary.extracted + ' 个 ID' + tail + mapped;
|
|
|
|
|
|
|
+ return '完成' + resumedTag + ':提取 ' + total + ' 个 ID' + thisRound + tail + mapped;
|
|
|
}
|
|
}
|
|
|
var watched = summary ? summary.attemptedVideos : 0;
|
|
var watched = summary ? summary.attemptedVideos : 0;
|
|
|
var posted = summary ? summary.postedComments : 0;
|
|
var posted = summary ? summary.postedComments : 0;
|
|
@@ -426,6 +459,7 @@ TikTokAuto.bridge = (function () {
|
|
|
fromExecArgv: fromExecArgv,
|
|
fromExecArgv: fromExecArgv,
|
|
|
isEnabled: isEnabled,
|
|
isEnabled: isEnabled,
|
|
|
checkStop: checkStop,
|
|
checkStop: checkStop,
|
|
|
|
|
+ currentRunId: currentRunId,
|
|
|
report: report,
|
|
report: report,
|
|
|
finish: finish,
|
|
finish: finish,
|
|
|
describe: describe,
|
|
describe: describe,
|
|
@@ -517,6 +551,20 @@ TikTokAuto.config = (function () {
|
|
|
* 两者共用一个预算时,抓满 = 收尾一点时间都不剩 = 一个 uid 都换不到。
|
|
* 两者共用一个预算时,抓满 = 收尾一点时间都不剩 = 一个 uid 都换不到。
|
|
|
*/
|
|
*/
|
|
|
resolveMaxMs: 10 * 60 * 1000
|
|
resolveMaxMs: 10 * 60 * 1000
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 断点续跑。默认**关**:只有界面明确点「继续」才会带 `--resume` 进来。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么不默认开:一份手写的 engine-config.json 如果默认就去读断点,
|
|
|
|
|
+ * 会出现"我只想从头跑一次,结果它接着上次跑"这种反直觉行为。
|
|
|
|
|
+ * 而且 `state:'none'`/`'done'` 时它本来也不会续 —— 但**默认关**更安全。
|
|
|
|
|
+ *
|
|
|
|
|
+ * path 留空则由 `output.dir` 派生(`checkpoint.pathOf`),
|
|
|
|
|
+ * 这样用户改产物目录时断点跟着走,不会出现"产物在新目录、断点在旧目录"。
|
|
|
|
|
+ */
|
|
|
|
|
+ resume: {
|
|
|
|
|
+ enabled: false,
|
|
|
|
|
+ path: ''
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
watch: {
|
|
watch: {
|
|
@@ -5138,6 +5186,22 @@ TikTokAuto.checkpoint = (function () {
|
|
|
return '';
|
|
return '';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取一条取材的记录(找不到返回 null)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 续跑时要用它上次翻了多少屏来**放大** `maxScreens` 预算:
|
|
|
|
|
+ * 从头重放旧人也要翻屏,不放大就会"还没走到新人就撞上 60 屏硬顶"。
|
|
|
|
|
+ */
|
|
|
|
|
+ function sourceOf(cp, key) {
|
|
|
|
|
+ return findSource(cp, key);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 上次这条取材翻了多少屏(没记录就 0) */
|
|
|
|
|
+ function screensOf(cp, key) {
|
|
|
|
|
+ var one = findSource(cp, key);
|
|
|
|
|
+ return one ? (one.screens || 0) : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/* ------------------------------------------------------------ 深度展开 */
|
|
/* ------------------------------------------------------------ 深度展开 */
|
|
|
|
|
|
|
|
function expandKey(uid, which) {
|
|
function expandKey(uid, which) {
|
|
@@ -5212,6 +5276,25 @@ TikTokAuto.checkpoint = (function () {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 「提取满 N 个就停」—— 续跑时必须**按累计数**算。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 续跑的人均语义是"这一轮凑够 N 个",那就变成设了 100 个、跑了 60 个被停、
|
|
|
|
|
+ * 接着再收 100 个 = 一共 160 个。所以基数是"上次已落盘的条数",
|
|
|
|
|
+ * 而那个数以**结果文件的行数**为准(文件是真相,不信 JSON 里的计数)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param cp 断点(读 `limitBase`)
|
|
|
|
|
+ * @param limit 用户设的上限(0 = 不限)
|
|
|
|
|
+ * @param currentExtracted 本轮新增
|
|
|
|
|
+ */
|
|
|
|
|
+ function limitReached(cp, limit, currentExtracted) {
|
|
|
|
|
+ if (!(limit > 0)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ var base = (cp && cp.limitBase) || 0;
|
|
|
|
|
+ return (base + (currentExtracted || 0)) >= limit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
DEFAULT_NAME: DEFAULT_NAME,
|
|
DEFAULT_NAME: DEFAULT_NAME,
|
|
|
VERSION: VERSION,
|
|
VERSION: VERSION,
|
|
@@ -5226,11 +5309,14 @@ TikTokAuto.checkpoint = (function () {
|
|
|
markSource: markSource,
|
|
markSource: markSource,
|
|
|
sourceProgress: sourceProgress,
|
|
sourceProgress: sourceProgress,
|
|
|
stoppedAt: stoppedAt,
|
|
stoppedAt: stoppedAt,
|
|
|
|
|
+ sourceOf: sourceOf,
|
|
|
|
|
+ screensOf: screensOf,
|
|
|
enqueueExpand: enqueueExpand,
|
|
enqueueExpand: enqueueExpand,
|
|
|
expandDone: expandDone,
|
|
expandDone: expandDone,
|
|
|
markExpandDone: markExpandDone,
|
|
markExpandDone: markExpandDone,
|
|
|
pendingExpand: pendingExpand,
|
|
pendingExpand: pendingExpand,
|
|
|
- allDone: allDone
|
|
|
|
|
|
|
+ allDone: allDone,
|
|
|
|
|
+ limitReached: limitReached
|
|
|
};
|
|
};
|
|
|
}());
|
|
}());
|
|
|
|
|
|
|
@@ -7420,7 +7506,17 @@ TikTokAuto.idflow = (function () {
|
|
|
userMaxRuntimeMs: userMaxMs,
|
|
userMaxRuntimeMs: userMaxMs,
|
|
|
/** 实际生效的抓取预算(按 limit 自动放宽) */
|
|
/** 实际生效的抓取预算(按 limit 自动放宽) */
|
|
|
maxRuntimeMs: budgetFor(limit, userMaxMs),
|
|
maxRuntimeMs: budgetFor(limit, userMaxMs),
|
|
|
- resolveMaxMs: Number(guard.resolveMaxMs) || 10 * 60 * 1000
|
|
|
|
|
|
|
+ resolveMaxMs: Number(guard.resolveMaxMs) || 10 * 60 * 1000,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 断点续跑开关。默认关:只有界面点「继续」时才由 `--resume` 打开。
|
|
|
|
|
+ * path 留空时由 output.dir 派生 —— 用户改产物目录,断点跟着走。
|
|
|
|
|
+ */
|
|
|
|
|
+ resume: {
|
|
|
|
|
+ enabled: (id.resume && id.resume.enabled === true),
|
|
|
|
|
+ path: (id.resume && id.resume.path) ||
|
|
|
|
|
+ ((output.dir || '/sdcard/Download/tiktok_app') + '/' +
|
|
|
|
|
+ TikTokAuto.checkpoint.DEFAULT_NAME)
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -7438,13 +7534,21 @@ TikTokAuto.idflow = (function () {
|
|
|
var summary = {
|
|
var summary = {
|
|
|
ok: false,
|
|
ok: false,
|
|
|
task: 'id',
|
|
task: 'id',
|
|
|
- extracted: 0, /* 本次写进产物文件的个数 */
|
|
|
|
|
|
|
+ extracted: 0, /* **本轮**写进产物文件的个数 */
|
|
|
skipped: 0, /* 因过滤被排除 */
|
|
skipped: 0, /* 因过滤被排除 */
|
|
|
duplicates: 0, /* 因去重被排除 */
|
|
duplicates: 0, /* 因去重被排除 */
|
|
|
openedProfiles: 0, /* 打开过多少次用户页(成本指标) */
|
|
openedProfiles: 0, /* 打开过多少次用户页(成本指标) */
|
|
|
sources: [],
|
|
sources: [],
|
|
|
warnings: [],
|
|
warnings: [],
|
|
|
resultFile: '',
|
|
resultFile: '',
|
|
|
|
|
+ /* —— 断点续跑相关 ——
|
|
|
|
|
+ * resumed:本次是不是接着上次跑
|
|
|
|
|
+ * extractedTotal:累计口径(上次已落盘 + 本轮新增),上限与文案都按它算
|
|
|
|
|
+ * resumeRejected:要求续跑但被拒时,把"为什么"结构化成一份给界面看
|
|
|
|
|
+ */
|
|
|
|
|
+ resumed: false,
|
|
|
|
|
+ extractedTotal: 0,
|
|
|
|
|
+ resumeRejected: null,
|
|
|
errors: []
|
|
errors: []
|
|
|
};
|
|
};
|
|
|
lastSummary = summary;
|
|
lastSummary = summary;
|
|
@@ -7453,10 +7557,21 @@ TikTokAuto.idflow = (function () {
|
|
|
var expandQueue = [];
|
|
var expandQueue = [];
|
|
|
var softReported = {};
|
|
var softReported = {};
|
|
|
var writer = null;
|
|
var writer = null;
|
|
|
|
|
+ var runFile = '';
|
|
|
|
|
+
|
|
|
|
|
+ /* 断点(cp)在"决定续跑"之后才会有值;base 是"上次已落盘的条数",
|
|
|
|
|
+ * 上限、文案、ok 判定都按 base + 本轮 来算。 */
|
|
|
|
|
+ var cp = null;
|
|
|
|
|
+ var cpPath = settings.resume.path;
|
|
|
|
|
+ var resumed = false;
|
|
|
|
|
+ var base = 0;
|
|
|
|
|
+ var sig = TikTokAuto.fingerprint.ofIdSection(config.id);
|
|
|
|
|
|
|
|
/* —— 停止条件:上限 / 时长 / 用户叫停,三处统一问它 —— */
|
|
/* —— 停止条件:上限 / 时长 / 用户叫停,三处统一问它 —— */
|
|
|
function limitReached() {
|
|
function limitReached() {
|
|
|
- return settings.limit > 0 && summary.extracted >= settings.limit;
|
|
|
|
|
|
|
+ /* ★ 上限必须按**累计**算:不然设 100 个、跑了 60 个被停,
|
|
|
|
|
+ * 续跑后又会再收 100 个 = 一共 160 个。 */
|
|
|
|
|
+ return TikTokAuto.checkpoint.limitReached(cp, settings.limit, summary.extracted);
|
|
|
}
|
|
}
|
|
|
function timeUp() {
|
|
function timeUp() {
|
|
|
return Date.now() - began >= settings.maxRuntimeMs;
|
|
return Date.now() - began >= settings.maxRuntimeMs;
|
|
@@ -7536,12 +7651,19 @@ TikTokAuto.idflow = (function () {
|
|
|
* 两份都留着,因为它们的用途真的不同,而且映射只有这一轮的数据。
|
|
* 两份都留着,因为它们的用途真的不同,而且映射只有这一轮的数据。
|
|
|
*/
|
|
*/
|
|
|
function resolvePhase(target, settings) {
|
|
function resolvePhase(target, settings) {
|
|
|
- var names = writer.list();
|
|
|
|
|
|
|
+ /* ★ 续跑时解析**整份结果文件**,不是只解析本轮新增。
|
|
|
|
|
+ * 映射文件是"结果文件的对照表",只含本轮新增就成了残表 ——
|
|
|
|
|
+ * 上次已收的 37 个永远拿不到数字 uid。
|
|
|
|
|
+ * 代价是这些名字要重新请求一遍(每个约 2.5 秒),先要正确性。 */
|
|
|
|
|
+ var names = resumed ? TikTokAuto.uidset.readUidFile(runFile) : writer.list();
|
|
|
if (names.length === 0) {
|
|
if (names.length === 0) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- var mapFile = settings.output.dir + '/' +
|
|
|
|
|
- TikTokAuto.parse.timestampName('uid_map', new Date());
|
|
|
|
|
|
|
+ /* 续跑复用上次的映射文件路径(覆盖写整份):
|
|
|
|
|
+ * 否则一次任务会产出两个 uid_map,下游还得自己合并。 */
|
|
|
|
|
+ var mapFile = (resumed && cp && cp.mapFile)
|
|
|
|
|
+ ? cp.mapFile
|
|
|
|
|
+ : settings.output.dir + '/' + TikTokAuto.parse.timestampName('uid_map', new Date());
|
|
|
|
|
|
|
|
log.step('idflow', '开始解析数字 uid:' + names.length + ' 个(每个约 2~3 秒)');
|
|
log.step('idflow', '开始解析数字 uid:' + names.length + ' 个(每个约 2~3 秒)');
|
|
|
var resolveStarted = Date.now();
|
|
var resolveStarted = Date.now();
|
|
@@ -7579,6 +7701,10 @@ TikTokAuto.idflow = (function () {
|
|
|
target.unresolved = result.failed.length;
|
|
target.unresolved = result.failed.length;
|
|
|
if (result.map.length > 0 && TikTokAuto.resolve.writeMap(mapFile, result.map)) {
|
|
if (result.map.length > 0 && TikTokAuto.resolve.writeMap(mapFile, result.map)) {
|
|
|
target.mapFile = mapFile;
|
|
target.mapFile = mapFile;
|
|
|
|
|
+ if (cp) {
|
|
|
|
|
+ cp.mapFile = mapFile;
|
|
|
|
|
+ TikTokAuto.checkpoint.save(cpPath, cp);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
if (result.failed.length > 0) {
|
|
if (result.failed.length > 0) {
|
|
|
target.warnings.push(result.failed.length +
|
|
target.warnings.push(result.failed.length +
|
|
@@ -7632,9 +7758,32 @@ TikTokAuto.idflow = (function () {
|
|
|
|
|
|
|
|
if (row.expandTo) {
|
|
if (row.expandTo) {
|
|
|
expandQueue.push({ uid: uid, which: row.expandTo });
|
|
expandQueue.push({ uid: uid, which: row.expandTo });
|
|
|
|
|
+ /* 同步记进断点:不然续跑时"这个人还要展开"这条信息就丢了 */
|
|
|
|
|
+ TikTokAuto.checkpoint.enqueueExpand(cp, uid, row.expandTo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 节流落盘,与产物刷盘同拍(每 20 个)。
|
|
|
|
|
+ * 不追每一个:粒度是取材条目级,写那么频只换来"少重走 19 个人"。 */
|
|
|
|
|
+ if (cp && summary.extracted % TikTokAuto.uidset.FLUSH_EVERY === 0) {
|
|
|
|
|
+ saveCheckpoint();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 把当前进度写进断点文件(失败只记 warn,不影响主流程) */
|
|
|
|
|
+ function saveCheckpoint() {
|
|
|
|
|
+ if (!cp) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ cp.counts = {
|
|
|
|
|
+ extracted: base + summary.extracted,
|
|
|
|
|
+ skipped: summary.skipped,
|
|
|
|
|
+ duplicates: summary.duplicates,
|
|
|
|
|
+ openedProfiles: summary.openedProfiles
|
|
|
|
|
+ };
|
|
|
|
|
+ cp.limitBase = base;
|
|
|
|
|
+ TikTokAuto.checkpoint.save(cpPath, cp);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
log.step('idflow', '=== TikTok ID 提取开始 ===');
|
|
log.step('idflow', '=== TikTok ID 提取开始 ===');
|
|
|
log.info('idflow', '取材 ' + settings.sources.length + ' 条,过滤:' +
|
|
log.info('idflow', '取材 ' + settings.sources.length + ' 条,过滤:' +
|
|
@@ -7647,6 +7796,39 @@ TikTokAuto.idflow = (function () {
|
|
|
Math.round(settings.userMaxRuntimeMs / 60000) + ' 分钟)'
|
|
Math.round(settings.userMaxRuntimeMs / 60000) + ' 分钟)'
|
|
|
: '(未设提取上限,按你设的时长)'));
|
|
: '(未设提取上限,按你设的时长)'));
|
|
|
|
|
|
|
|
|
|
+ /* —— 续跑决策(★ 全部在启动 App 之前做完)——
|
|
|
|
|
+ * 拒绝续跑时零成本返回:不启动 TikTok、不动用户手机上的任何一个页面。
|
|
|
|
|
+ * 判定顺序见 checkpoint.planResume(文件不在 → 已跑完 → 演练 → 配置变 → 结果文件没了)。 */
|
|
|
|
|
+ if (settings.resume.enabled) {
|
|
|
|
|
+ var prev = TikTokAuto.checkpoint.read(cpPath);
|
|
|
|
|
+ var decision = TikTokAuto.checkpoint.planResume(
|
|
|
|
|
+ prev, sig,
|
|
|
|
|
+ !!(prev && prev.runFile && files.exists(prev.runFile)),
|
|
|
|
|
+ { dryRun: settings.output.dryRun });
|
|
|
|
|
+
|
|
|
|
|
+ if (decision.mode === 'reject') {
|
|
|
|
|
+ summary.errors.push(decision.message);
|
|
|
|
|
+ summary.resumeRejected = {
|
|
|
|
|
+ reason: decision.reason,
|
|
|
|
|
+ changed: decision.changed || [],
|
|
|
|
|
+ message: decision.message,
|
|
|
|
|
+ runFile: (prev && prev.runFile) || ''
|
|
|
|
|
+ };
|
|
|
|
|
+ log.warn('idflow', '续跑被拒绝:' + decision.message);
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (decision.mode === 'resume') {
|
|
|
|
|
+ cp = prev;
|
|
|
|
|
+ resumed = true;
|
|
|
|
|
+ summary.resumed = true;
|
|
|
|
|
+ log.info('idflow', '续跑:上次停在「' +
|
|
|
|
|
+ (TikTokAuto.checkpoint.stoppedAt(cp) || '未知') + '」,已收 ' +
|
|
|
|
|
+ (cp.counts && cp.counts.extracted ? cp.counts.extracted : 0) + ' 个');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info('idflow', '没有可用的断点(' + decision.reason + '),从零开始');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
TikTokAuto.bridge.report({ status: 'running', phase: '准备', done: 0, total: 0 });
|
|
TikTokAuto.bridge.report({ status: 'running', phase: '准备', done: 0, total: 0 });
|
|
|
|
|
|
|
|
if (!TikTokAuto.uidset.ensureDir(settings.output.dir)) {
|
|
if (!TikTokAuto.uidset.ensureDir(settings.output.dir)) {
|
|
@@ -7654,9 +7836,16 @@ TikTokAuto.idflow = (function () {
|
|
|
return summary;
|
|
return summary;
|
|
|
}
|
|
}
|
|
|
/* 文件名只生成一次:分两次调 timestampName 有可能跨过整分钟,
|
|
/* 文件名只生成一次:分两次调 timestampName 有可能跨过整分钟,
|
|
|
- * 于是"写进去的文件"和"报给界面的文件名"不是同一个 —— 这种错几乎无法复现。 */
|
|
|
|
|
- var runFile = settings.output.dir + '/' +
|
|
|
|
|
- TikTokAuto.parse.timestampName('uid', new Date());
|
|
|
|
|
|
|
+ * 于是"写进去的文件"和"报给界面的文件名"不是同一个 —— 这种错几乎无法复现。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ 续跑时**复用上次的文件**(不新开):一次任务只该有一个 uid_*.txt,
|
|
|
|
|
+ * 否则用户拿到两个文件还得自己合并。 */
|
|
|
|
|
+ if (resumed && cp && cp.runFile) {
|
|
|
|
|
+ runFile = cp.runFile;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ runFile = settings.output.dir + '/' +
|
|
|
|
|
+ TikTokAuto.parse.timestampName('uid', new Date());
|
|
|
|
|
+ }
|
|
|
writer = TikTokAuto.uidset.createWriter({
|
|
writer = TikTokAuto.uidset.createWriter({
|
|
|
runPath: runFile,
|
|
runPath: runFile,
|
|
|
historyPath: settings.output.historyPath,
|
|
historyPath: settings.output.historyPath,
|
|
@@ -7668,6 +7857,32 @@ TikTokAuto.idflow = (function () {
|
|
|
if (settings.output.dedupeHistory) {
|
|
if (settings.output.dedupeHistory) {
|
|
|
TikTokAuto.uidset.loadHistory(seen, settings.output.historyPath);
|
|
TikTokAuto.uidset.loadHistory(seen, settings.output.historyPath);
|
|
|
}
|
|
}
|
|
|
|
|
+ /* ★ 续跑时**无条件**把上次的产物载入去重集合(不依赖 dedupeHistory):
|
|
|
|
|
+ * 否则会把已经收过的人再写一遍。base 以**文件行数**为准 —— 文件是真相,
|
|
|
|
|
+ * 不信断点里那个计数(它可能是被杀之前写的旧值)。 */
|
|
|
|
|
+ if (resumed) {
|
|
|
|
|
+ base = TikTokAuto.uidset.readUidFile(runFile).length;
|
|
|
|
|
+ TikTokAuto.uidset.loadHistory(seen, runFile);
|
|
|
|
|
+ summary.warnings.push('本次为续跑:结果继续追加到 ' + runFile);
|
|
|
|
|
+ log.info('idflow', '续跑:跳过已收 ' + base + ' 个');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 建/续断点,并**立刻写一次** —— 刚起步就被杀也能接着跑 */
|
|
|
|
|
+ if (!resumed) {
|
|
|
|
|
+ var entries = [];
|
|
|
|
|
+ for (var si = 0; si < settings.sources.length; si += 1) {
|
|
|
|
|
+ entries.push({
|
|
|
|
|
+ key: settings.sources[si].key ||
|
|
|
|
|
+ ('i' + si + ':' + (settings.sources[si].kind || '')),
|
|
|
|
|
+ kind: settings.sources[si].kind || '',
|
|
|
|
|
+ label: TikTokAuto.idsource.describe(settings.sources[si])
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ cp = TikTokAuto.checkpoint.empty(
|
|
|
|
|
+ TikTokAuto.bridge.currentRunId ? TikTokAuto.bridge.currentRunId() : '',
|
|
|
|
|
+ sig, runFile, entries);
|
|
|
|
|
+ }
|
|
|
|
|
+ saveCheckpoint();
|
|
|
|
|
|
|
|
/* —— 启动与就位(复用视频链路的成熟实现) —— */
|
|
/* —— 启动与就位(复用视频链路的成熟实现) —— */
|
|
|
progress('启动应用', 0);
|
|
progress('启动应用', 0);
|
|
@@ -7700,13 +7915,44 @@ TikTokAuto.idflow = (function () {
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
var source = settings.sources[i];
|
|
var source = settings.sources[i];
|
|
|
|
|
+ var sourceKey = source.key || ('i' + i + ':' + (source.kind || ''));
|
|
|
var before = summary.extracted;
|
|
var before = summary.extracted;
|
|
|
|
|
|
|
|
|
|
+ /* 上次已走完的取材直接跳过 —— 这就是"断点续跑"的全部含义:
|
|
|
|
|
+ * 不是从第几屏接着滚,而是**不再重走已经走完的那几条**。 */
|
|
|
|
|
+ if (resumed && TikTokAuto.checkpoint.sourceDone(cp, sourceKey)) {
|
|
|
|
|
+ summary.sources.push({
|
|
|
|
|
+ label: TikTokAuto.idsource.describe(source),
|
|
|
|
|
+ handled: 0,
|
|
|
|
|
+ added: 0,
|
|
|
|
|
+ reused: true
|
|
|
|
|
+ });
|
|
|
|
|
+ log.info('idflow', '续跑跳过(上次已完成):' + TikTokAuto.idsource.describe(source));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TikTokAuto.checkpoint.markSource(cp, sourceKey, 'running');
|
|
|
|
|
+ saveCheckpoint();
|
|
|
|
|
+
|
|
|
progress('取材 ' + (i + 1) + '/' + settings.sources.length, summary.extracted);
|
|
progress('取材 ' + (i + 1) + '/' + settings.sources.length, summary.extracted);
|
|
|
|
|
+
|
|
|
|
|
+ /* 续跑时这条取材上次翻了多少屏 → 这次把预算放大相应倍数。
|
|
|
|
|
+ * 从头重放旧人也要翻屏,不放大就会"还没走到新人就撞上 60 屏硬顶"。 */
|
|
|
|
|
+ var prevScreens = resumed ? TikTokAuto.checkpoint.screensOf(cp, sourceKey) : 0;
|
|
|
|
|
+ var maxScreens = 0;
|
|
|
|
|
+ if (prevScreens > 0) {
|
|
|
|
|
+ maxScreens = Math.min(TikTokAuto.idsource.MAX_SCREENS * 2,
|
|
|
|
|
+ TikTokAuto.idsource.MAX_SCREENS +
|
|
|
|
|
+ Math.min(prevScreens, TikTokAuto.idsource.MAX_SCREENS));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var stat = {};
|
|
|
var ctx = {
|
|
var ctx = {
|
|
|
config: config,
|
|
config: config,
|
|
|
seen: seen,
|
|
seen: seen,
|
|
|
shouldStop: shouldStop,
|
|
shouldStop: shouldStop,
|
|
|
|
|
+ maxScreens: maxScreens,
|
|
|
|
|
+ stat: stat,
|
|
|
onSeenProfile: function (prof) {
|
|
onSeenProfile: function (prof) {
|
|
|
log.info('idflow', '取材账号 @' + prof.uid + ':粉丝 ' + prof.fans +
|
|
log.info('idflow', '取材账号 @' + prof.uid + ':粉丝 ' + prof.fans +
|
|
|
' / 关注 ' + prof.follow + ' / 获赞 ' + prof.likes);
|
|
' / 关注 ' + prof.follow + ' / 获赞 ' + prof.likes);
|
|
@@ -7721,6 +7967,7 @@ TikTokAuto.idflow = (function () {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
var handled = 0;
|
|
var handled = 0;
|
|
|
|
|
+ var errorsBefore = summary.errors.length;
|
|
|
try {
|
|
try {
|
|
|
handled = TikTokAuto.idsource.run(source, ctx);
|
|
handled = TikTokAuto.idsource.run(source, ctx);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -7737,24 +7984,51 @@ TikTokAuto.idflow = (function () {
|
|
|
});
|
|
});
|
|
|
log.info('idflow', '取材完成:' + TikTokAuto.idsource.describe(source) +
|
|
log.info('idflow', '取材完成:' + TikTokAuto.idsource.describe(source) +
|
|
|
',处理 ' + handled + ' 行,新增 ' + (summary.extracted - before) + ' 个');
|
|
',处理 ' + handled + ' 行,新增 ' + (summary.extracted - before) + ' 个');
|
|
|
|
|
+
|
|
|
|
|
+ /* ★ 走完一条就记一条 —— 下次续跑正是靠它跳过这一条。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 但**失败的那条不能标 done**:`idsource.run` 打不开列表时会记 errors
|
|
|
|
|
+ * 然后返回 0(不抛异常),如果一律标 done,续跑就再也不会重试它,
|
|
|
|
|
+ * 用户会以为"这条取材就是这样、没数据"。所以:
|
|
|
|
|
+ * · 本轮新增了错误 → 保持 running,下次接着试
|
|
|
|
|
+ * · 否则(正常走完,哪怕列表本来就是空的)→ done
|
|
|
|
|
+ *
|
|
|
|
|
+ * 中途被停止时这里不会执行,那条取材保持 running,
|
|
|
|
|
+ * 续跑就从它开头重走 —— 这就是"最多回退一条取材"。 */
|
|
|
|
|
+ var sourceFailed = summary.errors.length > errorsBefore;
|
|
|
|
|
+ TikTokAuto.checkpoint.markSource(cp, sourceKey,
|
|
|
|
|
+ sourceFailed ? 'running' : 'done', {
|
|
|
|
|
+ screens: stat.screens || 0,
|
|
|
|
|
+ handled: handled,
|
|
|
|
|
+ added: summary.extracted - before
|
|
|
|
|
+ });
|
|
|
|
|
+ saveCheckpoint();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* —— 第二层:深度展开 ——
|
|
/* —— 第二层:深度展开 ——
|
|
|
* 与第一层分开跑,是为了避免嵌套列表上下文(见 idsource.js 文件头)。
|
|
* 与第一层分开跑,是为了避免嵌套列表上下文(见 idsource.js 文件头)。
|
|
|
- * 展开进来的人**同样要过一遍过滤** —— 否则"粉丝的粉丝"会绕过所有条件。 */
|
|
|
|
|
- if (expandQueue.length > 0 && shouldStop() && limitReached()) {
|
|
|
|
|
|
|
+ * 展开进来的人**同样要过一遍过滤** —— 否则"粉丝的粉丝"会绕过所有条件。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 续跑时先用断点过滤掉"上次已经展开过"的人:展开一个人要 5~10 秒,
|
|
|
|
|
+ * 重做一遍纯属浪费。 */
|
|
|
|
|
+ var todoExpand = TikTokAuto.checkpoint.pendingExpand(cp, expandQueue);
|
|
|
|
|
+ if (expandQueue.length > 0 && todoExpand.length === 0) {
|
|
|
|
|
+ log.info('idflow', '展开项上次已全部完成(' + expandQueue.length + ' 个),跳过');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (todoExpand.length > 0 && shouldStop() && limitReached()) {
|
|
|
/* 上限在主层就满了 → 展开根本没跑。这必须说出来:
|
|
/* 上限在主层就满了 → 展开根本没跑。这必须说出来:
|
|
|
* 用户明明勾了「再取粉丝列表」,看到"提取满 3 个"很容易以为展开做了。 */
|
|
* 用户明明勾了「再取粉丝列表」,看到"提取满 3 个"很容易以为展开做了。 */
|
|
|
summary.warnings.push('提取上限已到,深度展开(再取粉丝列表)本次没有执行');
|
|
summary.warnings.push('提取上限已到,深度展开(再取粉丝列表)本次没有执行');
|
|
|
- log.warn('idflow', '提取上限已到,' + expandQueue.length + ' 个待展开的账号本次没有展开');
|
|
|
|
|
|
|
+ log.warn('idflow', '提取上限已到,' + todoExpand.length + ' 个待展开的账号本次没有展开');
|
|
|
}
|
|
}
|
|
|
- if (expandQueue.length > 0 && !shouldStop()) {
|
|
|
|
|
- log.step('idflow', '开始深度展开:' + expandQueue.length + ' 个账号');
|
|
|
|
|
- for (var e = 0; e < expandQueue.length; e += 1) {
|
|
|
|
|
|
|
+ if (todoExpand.length > 0 && !shouldStop()) {
|
|
|
|
|
+ log.step('idflow', '开始深度展开:' + todoExpand.length + ' 个账号' +
|
|
|
|
|
+ (resumed ? '(已跳过上次做完的)' : ''));
|
|
|
|
|
+ for (var e = 0; e < todoExpand.length; e += 1) {
|
|
|
if (shouldStop()) {
|
|
if (shouldStop()) {
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
- var target = expandQueue[e];
|
|
|
|
|
|
|
+ var target = todoExpand[e];
|
|
|
progress('展开 @' + target.uid + ' 的' +
|
|
progress('展开 @' + target.uid + ' 的' +
|
|
|
(target.which === 'following' ? '关注' : '粉丝'), summary.extracted);
|
|
(target.which === 'following' ? '关注' : '粉丝'), summary.extracted);
|
|
|
|
|
|
|
@@ -7784,10 +8058,15 @@ TikTokAuto.idflow = (function () {
|
|
|
}
|
|
}
|
|
|
log.warn('idflow', '展开 @' + target.uid + ' 失败:' + error2);
|
|
log.warn('idflow', '展开 @' + target.uid + ' 失败:' + error2);
|
|
|
}
|
|
}
|
|
|
|
|
+ /* 做完一个就记一个:展开成本高(5~10 秒/人),重做代价大 */
|
|
|
|
|
+ TikTokAuto.checkpoint.markExpandDone(cp, target.uid, target.which);
|
|
|
|
|
+ saveCheckpoint();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- summary.ok = summary.extracted > 0;
|
|
|
|
|
|
|
+ /* 成功判定按**累计**:续跑本轮新增 0 个、但上次已有 37 个,这是成功不是失败 */
|
|
|
|
|
+ var totalSoFar = base + summary.extracted;
|
|
|
|
|
+ summary.ok = totalSoFar > 0;
|
|
|
if (!summary.ok && summary.errors.length === 0) {
|
|
if (!summary.ok && summary.errors.length === 0) {
|
|
|
summary.errors.push('没有提取到任何 ID(取材范围内都被过滤或去重掉了)');
|
|
summary.errors.push('没有提取到任何 ID(取材范围内都被过滤或去重掉了)');
|
|
|
}
|
|
}
|
|
@@ -7799,7 +8078,15 @@ TikTokAuto.idflow = (function () {
|
|
|
* ② 失败**只记 warning**,绝不把已经抓到的 username 算成失败;
|
|
* ② 失败**只记 warning**,绝不把已经抓到的 username 算成失败;
|
|
|
* ③ 换不到的**不写进映射文件** —— 文件里全是可用的对应关系。
|
|
* ③ 换不到的**不写进映射文件** —— 文件里全是可用的对应关系。
|
|
|
*/
|
|
*/
|
|
|
- if (settings.output.resolveUid && summary.extracted > 0 && !settings.output.dryRun) {
|
|
|
|
|
|
|
+ var needResolve = settings.output.resolveUid && !settings.output.dryRun && totalSoFar > 0;
|
|
|
|
|
+ /* 续跑但本轮一个都没新增、上次也已经生成过映射 → 直接沿用,不重算。
|
|
|
|
|
+ * 否则每次续跑都要把整份文件重新解析一遍(每个 2.5 秒)。 */
|
|
|
|
|
+ if (needResolve && resumed && summary.extracted === 0 && cp && cp.mapFile) {
|
|
|
|
|
+ log.info('idflow', '本轮没有新增,映射文件沿用上次的:' + cp.mapFile);
|
|
|
|
|
+ summary.mapFile = cp.mapFile;
|
|
|
|
|
+ needResolve = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (needResolve) {
|
|
|
try {
|
|
try {
|
|
|
writer.close();
|
|
writer.close();
|
|
|
resolvePhase(summary, settings);
|
|
resolvePhase(summary, settings);
|
|
@@ -7824,9 +8111,28 @@ TikTokAuto.idflow = (function () {
|
|
|
summary.warnings.push('结果文件写入失败(部分结果可能没有落盘)');
|
|
summary.warnings.push('结果文件写入失败(部分结果可能没有落盘)');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ /* 累计口径给上层(bridge 的 done、界面状态条、记录文案都用它) */
|
|
|
|
|
+ summary.extractedTotal = base + summary.extracted;
|
|
|
|
|
+
|
|
|
|
|
+ /* 断点收尾:无论正常结束还是被停止/出错解栈,都要把"走到哪"落盘。
|
|
|
|
|
+ * `allDone` 为真才算真跑完(界面上不再提供"继续");否则留成 stopped 等续跑。 */
|
|
|
|
|
+ if (cp) {
|
|
|
|
|
+ /* 三种情况都算"这一轮没有可续的了":
|
|
|
|
|
+ * ① 取材与展开都走完了;
|
|
|
|
|
+ * ② 达到了用户设的提取上限 —— 否则会留下一个"点了继续立刻又停"的断点,
|
|
|
|
|
+ * 因为 limitReached 按累计算,续跑一进来就已经到顶了。
|
|
|
|
|
+ */
|
|
|
|
|
+ cp.state = (TikTokAuto.checkpoint.allDone(cp) || limitReached()) ? 'done' : 'stopped';
|
|
|
|
|
+ saveCheckpoint();
|
|
|
|
|
+ log.info('idflow', '断点已保存(' + cp.state + '):取材 ' +
|
|
|
|
|
+ TikTokAuto.checkpoint.sourceProgress(cp).done + '/' +
|
|
|
|
|
+ TikTokAuto.checkpoint.sourceProgress(cp).total + ' 条已完成');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/* 一个都没收时**不报文件名**:那个文件根本没被创建(writer 只在有内容时 append),
|
|
/* 一个都没收时**不报文件名**:那个文件根本没被创建(writer 只在有内容时 append),
|
|
|
- * 报一个不存在的路径,用户去文件管理器找会以为"抓到了但丢了"。 */
|
|
|
|
|
- if (summary.extracted === 0) {
|
|
|
|
|
|
|
+ * 报一个不存在的路径,用户去文件管理器找会以为"抓到了但丢了"。
|
|
|
|
|
+ * 续跑时按累计判:上次已收 37 个,文件是存在的。 */
|
|
|
|
|
+ if (summary.extractedTotal === 0) {
|
|
|
summary.resultFile = '';
|
|
summary.resultFile = '';
|
|
|
}
|
|
}
|
|
|
if (limitReached()) {
|
|
if (limitReached()) {
|
|
@@ -7843,17 +8149,21 @@ TikTokAuto.idflow = (function () {
|
|
|
* 那么收满 L 个需要 L × T / E 秒。实测 1800 秒 / 83 个 → 100 个约 36 分钟。
|
|
* 那么收满 L 个需要 L × T / E 秒。实测 1800 秒 / 83 个 → 100 个约 36 分钟。
|
|
|
*/
|
|
*/
|
|
|
var elapsedMs = Date.now() - began;
|
|
var elapsedMs = Date.now() - began;
|
|
|
|
|
+ /* 文案与换算都按**累计**:续跑时本轮可能只新增 5 个,
|
|
|
|
|
+ * 但用户真正关心的是"总共 42/100",说"只收了 5/100"会吓人。 */
|
|
|
if (summary.extracted > 0) {
|
|
if (summary.extracted > 0) {
|
|
|
var needMin = Math.ceil(
|
|
var needMin = Math.ceil(
|
|
|
- settings.limit * elapsedMs / summary.extracted / 60000
|
|
|
|
|
|
|
+ (settings.limit - summary.extractedTotal + summary.extracted) *
|
|
|
|
|
+ elapsedMs / summary.extracted / 60000
|
|
|
);
|
|
);
|
|
|
summary.warnings.push(
|
|
summary.warnings.push(
|
|
|
- '时间到,只收了 ' + summary.extracted + '/' + settings.limit + ' 个。' +
|
|
|
|
|
- '按本轮速度,收满 ' + settings.limit + ' 个约需 ' + needMin +
|
|
|
|
|
- ' 分钟 —— 建议把「单次最长运行」调到 ' + (needMin + 5) + ' 分钟以上'
|
|
|
|
|
|
|
+ '时间到,共收了 ' + summary.extractedTotal + '/' + settings.limit +
|
|
|
|
|
+ ' 个(本轮新增 ' + summary.extracted + ' 个)。' +
|
|
|
|
|
+ '按本轮速度,收满 ' + settings.limit + ' 个还需约 ' + needMin +
|
|
|
|
|
+ ' 分钟 —— 可以点「继续」接着跑'
|
|
|
);
|
|
);
|
|
|
- log.warn('idflow', '时间到,只收 ' + summary.extracted + ' 个;' +
|
|
|
|
|
- '按本轮速度收满 ' + settings.limit + ' 个约需 ' + needMin + ' 分钟');
|
|
|
|
|
|
|
+ log.warn('idflow', '时间到,累计 ' + summary.extractedTotal + ' 个;' +
|
|
|
|
|
+ '按本轮速度收满 ' + settings.limit + ' 个还需约 ' + needMin + ' 分钟');
|
|
|
} else {
|
|
} else {
|
|
|
/* 一个都没收到就超时:多半不是时间不够,而是过滤太严或取材取不到人。
|
|
/* 一个都没收到就超时:多半不是时间不够,而是过滤太严或取材取不到人。
|
|
|
* 这时给"调大时间"的建议是害人 —— 要说清该查哪里。 */
|
|
* 这时给"调大时间"的建议是害人 —— 要说清该查哪里。 */
|
|
@@ -7862,15 +8172,16 @@ TikTokAuto.idflow = (function () {
|
|
|
'过滤条件是否过严(见日志里的跳过原因)'
|
|
'过滤条件是否过严(见日志里的跳过原因)'
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
- } else if (settings.limit > 0 && summary.extracted < settings.limit) {
|
|
|
|
|
|
|
+ } else if (settings.limit > 0 && summary.extractedTotal < settings.limit) {
|
|
|
/* 第三种"没到上限":取材本身跑完了(列表到底/文件读完),不是时间问题。
|
|
/* 第三种"没到上限":取材本身跑完了(列表到底/文件读完),不是时间问题。
|
|
|
* 这时调时间没用,得加取材量。 */
|
|
* 这时调时间没用,得加取材量。 */
|
|
|
summary.warnings.push(
|
|
summary.warnings.push(
|
|
|
- '取材范围已取尽,共收 ' + summary.extracted + '/' + settings.limit +
|
|
|
|
|
|
|
+ '取材范围已取尽,共收 ' + summary.extractedTotal + '/' + settings.limit +
|
|
|
' 个 —— 想收更多请增加取材(换关键词、加账号,或打开「再取粉丝列表」)'
|
|
' 个 —— 想收更多请增加取材(换关键词、加账号,或打开「再取粉丝列表」)'
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
- log.step('idflow', '=== 提取结束:新增 ' + summary.extracted +
|
|
|
|
|
|
|
+ log.step('idflow', '=== 提取结束' + (resumed ? '(续跑)' : '') +
|
|
|
|
|
+ ':新增 ' + summary.extracted + ' 个,累计 ' + summary.extractedTotal +
|
|
|
' 个,跳过 ' + summary.skipped + ' 个,重复 ' + summary.duplicates +
|
|
' 个,跳过 ' + summary.skipped + ' 个,重复 ' + summary.duplicates +
|
|
|
' 个,耗时 ' + Math.round((Date.now() - began) / 1000) + ' 秒 ===');
|
|
' 个,耗时 ' + Math.round((Date.now() - began) / 1000) + ' 秒 ===');
|
|
|
}
|
|
}
|