|
@@ -1,5 +1,5 @@
|
|
|
/*! tiktok-auto v1.0.0
|
|
/*! tiktok-auto v1.0.0
|
|
|
- * built 2026-10-29T10:22:57.399Z
|
|
|
|
|
|
|
+ * built 2026-10-29T10:43:50.864Z
|
|
|
* modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, interact, flow, nurture, parse, uidset, fingerprint, checkpoint, filters, profile, idsource, resolve, idflow, dm, dmflow, main
|
|
* modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, interact, flow, nurture, parse, uidset, fingerprint, checkpoint, filters, profile, idsource, resolve, idflow, dm, dmflow, main
|
|
|
* 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。
|
|
* 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。
|
|
|
*/
|
|
*/
|
|
@@ -384,6 +384,11 @@ TikTokAuto.bridge = (function () {
|
|
|
if (summary.skipped > 0) { tail += ',跳过 ' + summary.skipped + ' 个'; }
|
|
if (summary.skipped > 0) { tail += ',跳过 ' + summary.skipped + ' 个'; }
|
|
|
if (summary.failed > 0) { tail += ',失败 ' + summary.failed + ' 个'; }
|
|
if (summary.failed > 0) { tail += ',失败 ' + summary.failed + ' 个'; }
|
|
|
if (summary.dryRun) { tail += '(演练 ' + (summary.dryRunCount || 0) + ' 条,未真发)'; }
|
|
if (summary.dryRun) { tail += '(演练 ' + (summary.dryRunCount || 0) + ' 条,未真发)'; }
|
|
|
|
|
+ /* 人是哪儿来的要说清:脚本二跑完只说"关注 3 个/私信 2 条",
|
|
|
|
|
+ * 用户没法确认它到底是不是从自己的粉丝里取的人。 */
|
|
|
|
|
+ if (summary.sourceKind === 'ownFans') {
|
|
|
|
|
+ tail += '(取材自你自己的粉丝列表,' + (summary.totalUids || 0) + ' 人)';
|
|
|
|
|
+ }
|
|
|
return tail;
|
|
return tail;
|
|
|
};
|
|
};
|
|
|
/* 续跑时"数字"要给累计口径,并明确说这是续跑 ——
|
|
/* 续跑时"数字"要给累计口径,并明确说这是续跑 ——
|
|
@@ -677,9 +682,12 @@ TikTokAuto.config = (function () {
|
|
|
* 「休息时间」是**每处理完一个 UID 之后**的间隔,也在这里传确定的区间。
|
|
* 「休息时间」是**每处理完一个 UID 之后**的间隔,也在这里传确定的区间。
|
|
|
*/
|
|
*/
|
|
|
dm: {
|
|
dm: {
|
|
|
- /** 's1' = 已提 UID 私信(读文本);'s2' = 自己粉丝私信(暂未实现) */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 's1' = 已提 UID 私信(读文本文件)
|
|
|
|
|
+ * 's2' = 自己粉丝私信(打开自己的主页 → 粉丝列表取材,不用 uidPath)
|
|
|
|
|
+ */
|
|
|
script: 's1',
|
|
script: 's1',
|
|
|
- /** s1 的 UID 来源:每行一个,`@` 可有可无,`#` 开头算注释 */
|
|
|
|
|
|
|
+ /** s1 的 UID 来源:每行一个,`@` 可有可无,`#` 开头算注释(s2 忽略) */
|
|
|
uidPath: '/sdcard/Download/tiktok_app/uid_list.txt',
|
|
uidPath: '/sdcard/Download/tiktok_app/uid_list.txt',
|
|
|
follow: { enabled: true, max: 0 },
|
|
follow: { enabled: true, max: 0 },
|
|
|
send: { enabled: true, max: 0 },
|
|
send: { enabled: true, max: 0 },
|
|
@@ -884,6 +892,14 @@ TikTokAuto.config = (function () {
|
|
|
var sendMax = Number(dm.send && dm.send.max);
|
|
var sendMax = Number(dm.send && dm.send.max);
|
|
|
var followOn = dm.follow && dm.follow.enabled === true;
|
|
var followOn = dm.follow && dm.follow.enabled === true;
|
|
|
var sendOn = dm.send && dm.send.enabled === true;
|
|
var sendOn = dm.send && dm.send.enabled === true;
|
|
|
|
|
+ if (dm.script !== 's1' && dm.script !== 's2') {
|
|
|
|
|
+ throw new ConfigError('dm.script 只能是 "s1" 或 "s2"', dm.script);
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 脚本一靠外部文本取材,路径空着必然一个都读不到 —— 提前报,
|
|
|
|
|
+ * 而不是先启动 App、跑到一半才发现"没有可用账号"。 */
|
|
|
|
|
+ if (dm.script === 's1' && !dm.uidPath) {
|
|
|
|
|
+ throw new ConfigError('脚本一必须指定 UID 文本(dm.uidPath 为空)', dm.uidPath);
|
|
|
|
|
+ }
|
|
|
if (!followOn && !sendOn) {
|
|
if (!followOn && !sendOn) {
|
|
|
throw new ConfigError('「进行关注」与「进行私信」都关着,没有可执行的动作',
|
|
throw new ConfigError('「进行关注」与「进行私信」都关着,没有可执行的动作',
|
|
|
{ follow: followOn, send: sendOn });
|
|
{ follow: followOn, send: sendOn });
|
|
@@ -963,6 +979,22 @@ TikTokAuto.config = (function () {
|
|
|
commentCount: config.comments.pool.length,
|
|
commentCount: config.comments.pool.length,
|
|
|
dryRun: config.runtime.dryRun
|
|
dryRun: config.runtime.dryRun
|
|
|
}));
|
|
}));
|
|
|
|
|
+ } else if (config.task === 'dm') {
|
|
|
|
|
+ /* 私信同样没有关键词,落到下面的 video 分支会打印
|
|
|
|
|
+ * `{"keyword":"fashion",...}` —— 排障时完全误导(曾经真被这条日志带偏过,
|
|
|
|
|
+ * 以为跑的是视频链路)。这里报"从哪儿取人、发多少、歇多久"。 */
|
|
|
|
|
+ var dm = config.dm;
|
|
|
|
|
+ logger.info('config', '生效配置(私信):' + JSON.stringify({
|
|
|
|
|
+ script: dm.script,
|
|
|
|
|
+ source: dm.script === 's2' ? '自己的粉丝列表'
|
|
|
|
|
+ : ('UID 文本 ' + dm.uidPath),
|
|
|
|
|
+ follow: dm.follow,
|
|
|
|
|
+ send: dm.send,
|
|
|
|
|
+ restSec: [Math.round(dm.rest.minMs / 1000), Math.round(dm.rest.maxMs / 1000)],
|
|
|
|
|
+ dedupeMode: dm.dedupeMode,
|
|
|
|
|
+ textCount: dm.pool.length,
|
|
|
|
|
+ dryRun: config.runtime.dryRun
|
|
|
|
|
+ }));
|
|
|
} else {
|
|
} else {
|
|
|
logger.info('config', '生效配置:' + JSON.stringify({
|
|
logger.info('config', '生效配置:' + JSON.stringify({
|
|
|
keyword: config.target.keyword,
|
|
keyword: config.target.keyword,
|
|
@@ -2072,6 +2104,30 @@ TikTokAuto.tiktok = (function () {
|
|
|
return layout.findOnce(desc('Home')) !== null && layout.findOnce(desc('Profile')) !== null;
|
|
return layout.findOnce(desc('Home')) !== null && layout.findOnce(desc('Profile')) !== null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 底部导航的「Profile」项 —— 用来打开**自己的**主页(脚本二要自己的粉丝列表)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 判据刻意很松:`desc === 'Profile'` 精确匹配 + 中心落在屏幕下缘。
|
|
|
|
|
+ * 不限定 class(它是 FrameLayout,和历史记忆里"首页顶部 Following 页签是
|
|
|
|
|
+ * FrameLayout"同一族,限定 Button 会一个都找不到),也不认文案 ——
|
|
|
|
|
+ * 底部导航这一项没有 text。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么还要判"在下缘":右侧竖排作者头像的 desc 是 `xxx profile`(小写、
|
|
|
|
|
+ * 带用户名前缀),精确匹配 `Profile` 本来就排除掉了;但万一新版把它改成
|
|
|
|
|
+ * 纯 `Profile`,这一条能挡住"点到了头像"这种最坏情况。
|
|
|
|
|
+ */
|
|
|
|
|
+ function profileNav() {
|
|
|
|
|
+ var node = layout.findOnce(desc('Profile'));
|
|
|
|
|
+ if (!node) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ var point = layout.center(node);
|
|
|
|
|
+ if (!point || point.y < screenHeight() * 0.88) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ---------------------------------------------------------------- 搜索页
|
|
// ---------------------------------------------------------------- 搜索页
|
|
|
|
|
|
|
|
/** 顶部搜索输入框(排除评论输入框:后者在屏幕下方) */
|
|
/** 顶部搜索输入框(排除评论输入框:后者在屏幕下方) */
|
|
@@ -3365,6 +3421,7 @@ TikTokAuto.tiktok = (function () {
|
|
|
searchSubmit: searchSubmit,
|
|
searchSubmit: searchSubmit,
|
|
|
searchHistoryPresent: searchHistoryPresent,
|
|
searchHistoryPresent: searchHistoryPresent,
|
|
|
bottomNavPresent: bottomNavPresent,
|
|
bottomNavPresent: bottomNavPresent,
|
|
|
|
|
+ profileNav: profileNav,
|
|
|
resultTab: resultTab,
|
|
resultTab: resultTab,
|
|
|
resultTabStripPresent: resultTabStripPresent,
|
|
resultTabStripPresent: resultTabStripPresent,
|
|
|
videoCards: videoCards,
|
|
videoCards: videoCards,
|
|
@@ -6809,9 +6866,56 @@ TikTokAuto.profile = (function () {
|
|
|
}, config.timeouts.searchResultsMs, config.probe.pollTickMs) === true;
|
|
}, config.timeouts.searchResultsMs, config.probe.pollTickMs) === true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开**自己**的主页(底部导航的 Profile)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么必须先回信息流:底部导航只在首页有。当前若停在搜索页 / 播放页 /
|
|
|
|
|
+ * 别人的主页,屏幕上根本没有那个入口 —— 直接找必然失败,而且报的是
|
|
|
|
|
+ * "找不到 Profile 入口"这种看不出原因的话。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么不用 openBySearch(自己的用户名):引擎里没有"我是谁"这个信息
|
|
|
|
|
+ * (不带登录态、也不应该去猜),而底部导航这一步是零成本且永远正确的。
|
|
|
|
|
+ */
|
|
|
|
|
+ function openOwnProfile(config) {
|
|
|
|
|
+ var step = 'openOwnProfile';
|
|
|
|
|
+ var log = logger();
|
|
|
|
|
+
|
|
|
|
|
+ if (!ensureFeed(config)) {
|
|
|
|
|
+ log.warn(step, '回不到信息流,打不开自己的主页');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var nav = TikTokAuto.tiktok.profileNav();
|
|
|
|
|
+ if (!nav) {
|
|
|
|
|
+ log.warn(step, '底部导航上找不到 Profile 入口');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ TikTokAuto.ui.clickNode(nav, step);
|
|
|
|
|
+
|
|
|
|
|
+ var ready = TikTokAuto.wait.waitFor(step, function () {
|
|
|
|
|
+ return where() === 'profile' ? true : null;
|
|
|
|
|
+ }, config.timeouts.pageReadyMs, config.probe.pollTickMs);
|
|
|
|
|
+
|
|
|
|
|
+ if (ready !== true) {
|
|
|
|
|
+ log.warn(step, '点了 Profile 但没进入用户主页');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ /* ★ 必须等**三组数字**读全,不能用 settle(3000)(2026-10-29 真机踩到)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 「username 出现」比「数字渲染完」早得多:实测点完 Profile 1 秒就
|
|
|
|
|
+ * `where() === 'profile'` 通过,而 fans 入口挂在数字上 —— 这时 `statEntry('fans')`
|
|
|
|
|
+ * 找不到,上层看到的是"用户主页上找不到 fans 入口(等了 3 秒仍没有)",
|
|
|
|
|
+ * 一次运行就此失败。`settle()` 在动态页面上会提前返回(它的判据是
|
|
|
|
|
+ * "文案连续两次不变",而加载中与渲染完的签名可能一样),挡不住这件事。
|
|
|
|
|
+ *
|
|
|
|
|
+ * `waitProfile` 的判据正好是「username + 关注/粉丝/获赞三组都读到」,
|
|
|
|
|
+ * 而且它读不到时**只警告不失败**(有的账号确实不显示某一组)。 */
|
|
|
|
|
+ waitProfile(step + ':stats');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 在当前搜索结果里按 uid 找行(找不到返回 null,不猜第一行) */
|
|
/** 在当前搜索结果里按 uid 找行(找不到返回 null,不猜第一行) */
|
|
|
- function findRowByUid(uid) {
|
|
|
|
|
- var rows = TikTokAuto.tiktok.usersResultRows();
|
|
|
|
|
|
|
+ function findRowByUid(uid) { var rows = TikTokAuto.tiktok.usersResultRows();
|
|
|
for (var i = 0; i < rows.length; i += 1) {
|
|
for (var i = 0; i < rows.length; i += 1) {
|
|
|
if (rows[i].uid === uid) {
|
|
if (rows[i].uid === uid) {
|
|
|
return rows[i];
|
|
return rows[i];
|
|
@@ -7164,6 +7268,7 @@ TikTokAuto.profile = (function () {
|
|
|
where: where,
|
|
where: where,
|
|
|
ensureFeed: ensureFeed,
|
|
ensureFeed: ensureFeed,
|
|
|
openBySearch: openBySearch,
|
|
openBySearch: openBySearch,
|
|
|
|
|
+ openOwnProfile: openOwnProfile,
|
|
|
findRowByUid: findRowByUid,
|
|
findRowByUid: findRowByUid,
|
|
|
openList: openList,
|
|
openList: openList,
|
|
|
switchListTab: switchListTab,
|
|
switchListTab: switchListTab,
|
|
@@ -9235,6 +9340,42 @@ TikTokAuto.dm = (function () {
|
|
|
return best;
|
|
return best;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 拿到发送键,拿不到就**再点一次输入框**强制聚焦后重试一次。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ 为什么需要重试(2026-10-29 真机踩到):发送键只在**键盘弹起**时才存在
|
|
|
|
|
+ * (输入框缩短,右侧空出那块 72×72 的 ImageView)。而 `typeInto` 的那次点击
|
|
|
|
|
+ * 有时被输入法吞掉 —— 文字确实写进去了,键盘却没起来,于是发送键不存在。
|
|
|
|
|
+ * 表现是"演练成功、实发失败:输入后找不到发送键",且时好时坏。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 重试的动作很小(点一下输入框),但它把"时好时坏"变成"基本必成"。
|
|
|
|
|
+ */
|
|
|
|
|
+ function ensureSendButton(config, step) {
|
|
|
|
|
+ var log = logger();
|
|
|
|
|
+ var button = TikTokAuto.wait.waitFor(step, function () {
|
|
|
|
|
+ return sendButton() || null;
|
|
|
|
|
+ }, 1500, 250);
|
|
|
|
|
+ if (button) {
|
|
|
|
|
+ return button;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var input = panelInput();
|
|
|
|
|
+ if (!input) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ var point = TikTokAuto.layout.center(input);
|
|
|
|
|
+ if (!point) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info(step, '发送键还没出现,再点一次输入框强制聚焦');
|
|
|
|
|
+ TikTokAuto.ui.tapPoint(point.x, point.y);
|
|
|
|
|
+ TikTokAuto.wait.tick(400);
|
|
|
|
|
+
|
|
|
|
|
+ return TikTokAuto.wait.waitFor(step + ':retry', function () {
|
|
|
|
|
+ return sendButton() || null;
|
|
|
|
|
+ }, 4000, 250);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 打开私信面板(前提:当前已经在目标用户的**主页**上)。
|
|
* 打开私信面板(前提:当前已经在目标用户的**主页**上)。
|
|
|
*
|
|
*
|
|
@@ -9336,9 +9477,7 @@ TikTokAuto.dm = (function () {
|
|
|
/* dryRun 也要**先把发送键找一遍**(只记录、不点)。
|
|
/* dryRun 也要**先把发送键找一遍**(只记录、不点)。
|
|
|
* 否则"演练模式"永远验证不到最不确定的那一环 —— 发送键是输入后才出现的、
|
|
* 否则"演练模式"永远验证不到最不确定的那一环 —— 发送键是输入后才出现的、
|
|
|
* 无 id 无 desc 的 ImageView,找不到就等于发不出去,而演练却会报"成功"。 */
|
|
* 无 id 无 desc 的 ImageView,找不到就等于发不出去,而演练却会报"成功"。 */
|
|
|
- var preview = TikTokAuto.wait.waitFor(story + ':btn-preview', function () {
|
|
|
|
|
- return sendButton() || null;
|
|
|
|
|
- }, 4000, 250);
|
|
|
|
|
|
|
+ var preview = ensureSendButton(config, story + ':btn-preview');
|
|
|
log.warn(story, 'dryRun:已输入但不发送。发送键' +
|
|
log.warn(story, 'dryRun:已输入但不发送。发送键' +
|
|
|
(preview ? '已定位 ' + JSON.stringify(preview.bounds()) : '**没找到**') +
|
|
(preview ? '已定位 ' + JSON.stringify(preview.bounds()) : '**没找到**') +
|
|
|
',内容=' + JSON.stringify(content));
|
|
',内容=' + JSON.stringify(content));
|
|
@@ -9353,10 +9492,8 @@ TikTokAuto.dm = (function () {
|
|
|
sendButtonFound: preview !== null };
|
|
sendButtonFound: preview !== null };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 发送键是输入之后才出现的,写完之后等一下再找 */
|
|
|
|
|
- var button = TikTokAuto.wait.waitFor(story + ':btn', function () {
|
|
|
|
|
- return sendButton() || null;
|
|
|
|
|
- }, 4000, 250);
|
|
|
|
|
|
|
+ /* 发送键是输入之后才出现的(而且依赖键盘弹起),拿不到会再聚焦重试一次 */
|
|
|
|
|
+ var button = ensureSendButton(config, story + ':btn');
|
|
|
if (!button) {
|
|
if (!button) {
|
|
|
log.warn(story, '输入后仍找不到发送键');
|
|
log.warn(story, '输入后仍找不到发送键');
|
|
|
closePanel(story);
|
|
closePanel(story);
|
|
@@ -9523,6 +9660,75 @@ TikTokAuto.dmflow = (function () {
|
|
|
return out;
|
|
return out;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 脚本二:从**自己的粉丝列表**取材。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 采够 `need` 个就停 —— 多采没意义,而且每多翻一屏就多等一次渲染。
|
|
|
|
|
+ * 停止条件挂在 `ctx.shouldStop()` 上:`walkList` 每处理一行前都会问,
|
|
|
|
|
+ * 够了就自然结束(不是抛异常,所以不会把已采到的丢掉)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ 不要在这里判"列表到底":那是 walkList 的事(它只看"翻不翻得动")。
|
|
|
|
|
+ * 早期版本用"本屏没有新人"当到底判据,结果预载历史时在列表顶部就收工。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @returns 采到的 uid 数组(可能少于 need,比如粉丝本来就没那么多)
|
|
|
|
|
+ */
|
|
|
|
|
+ function collectOwnFans(config, need, summary) {
|
|
|
|
|
+ var log = logger();
|
|
|
|
|
+ var profileApi = TikTokAuto.profile;
|
|
|
|
|
+ var uids = [];
|
|
|
|
|
+
|
|
|
|
|
+ if (!profileApi.openOwnProfile(config)) {
|
|
|
|
|
+ summary.errors.push('打不开自己的主页(底部导航 Profile),无法取材');
|
|
|
|
|
+ return uids;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!profileApi.openList('fans', config)) {
|
|
|
|
|
+ /* 主页数字区的入口第一次常常还没渲染出来(openList 内部已等 3 秒)。
|
|
|
|
|
+ * 再等 2 秒重试一次 —— 失败一次就整轮退出太亏了。 */
|
|
|
|
|
+ log.warn('dmflow', '第一次没打开粉丝列表,2 秒后重试');
|
|
|
|
|
+ TikTokAuto.wait.tick(2000);
|
|
|
|
|
+ if (!profileApi.openList('fans', config)) {
|
|
|
|
|
+ summary.errors.push('打不开自己的粉丝列表(可能主页上不显示粉丝数)');
|
|
|
|
|
+ return uids;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 列表页默认停在「粉丝」还是「关注」取决于上次的操作,必须显式切一次。
|
|
|
|
|
+ * 切错页签的后果很难发现:一样能翻、一样有人,只是全是**你关注的人**。 */
|
|
|
|
|
+ if (!profileApi.switchListTab('fans')) {
|
|
|
|
|
+ summary.warnings.push('没找到「粉丝」页签,按当前页签取材(可能取到的是关注列表)');
|
|
|
|
|
+ }
|
|
|
|
|
+ log.step('dmflow', '开始从自己的粉丝列表取材,目标 ' + need + ' 个');
|
|
|
|
|
+
|
|
|
|
|
+ var ctx = {
|
|
|
|
|
+ config: config,
|
|
|
|
|
+ seen: TikTokAuto.uidset.create(),
|
|
|
|
|
+ stat: {},
|
|
|
|
|
+ shouldStop: function () {
|
|
|
|
|
+ return uids.length >= need;
|
|
|
|
|
+ },
|
|
|
|
|
+ onSeenProfile: function () { },
|
|
|
|
|
+ onRow: function (row) {
|
|
|
|
|
+ if (uids.length < need) {
|
|
|
|
|
+ uids.push(row.uid);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ TikTokAuto.idsource.walkList(ctx);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (TikTokAuto.bridge.isStopSignal(error)) {
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.warn('dmflow', '遍历粉丝列表时出错:' + error);
|
|
|
|
|
+ summary.warnings.push('遍历粉丝列表时出错,已采到 ' + uids.length + ' 个');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ profileApi.backToProfile();
|
|
|
|
|
+ log.step('dmflow', '自己粉丝取材完成:' + uids.length + '/' + need +
|
|
|
|
|
+ ' 个(翻了 ' + (ctx.stat.screens || 0) + ' 屏)');
|
|
|
|
|
+ return uids;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 休息指定区间,**分段等**:每秒醒一次,停止信号能立刻生效 */
|
|
/** 休息指定区间,**分段等**:每秒醒一次,停止信号能立刻生效 */
|
|
|
function rest(config, step) {
|
|
function rest(config, step) {
|
|
|
var lo = config.dm.rest.minMs;
|
|
var lo = config.dm.rest.minMs;
|
|
@@ -9591,16 +9797,17 @@ TikTokAuto.dmflow = (function () {
|
|
|
function runSession(config) {
|
|
function runSession(config) {
|
|
|
var log = logger();
|
|
var log = logger();
|
|
|
var settings = config.dm;
|
|
var settings = config.dm;
|
|
|
- var parse = TikTokAuto.parse;
|
|
|
|
|
|
|
|
|
|
var summary = {
|
|
var summary = {
|
|
|
ok: false,
|
|
ok: false,
|
|
|
task: 'dm',
|
|
task: 'dm',
|
|
|
script: settings.script,
|
|
script: settings.script,
|
|
|
|
|
+ /** 人是哪儿来的:'uids' = 脚本一读文本;'ownFans' = 脚本二自己粉丝列表 */
|
|
|
|
|
+ sourceKind: settings.script === 's2' ? 'ownFans' : 'uids',
|
|
|
/** 演练模式:输入但不发送。要单独计数,否则 dryRun 会被误报成"什么都没做成" */
|
|
/** 演练模式:输入但不发送。要单独计数,否则 dryRun 会被误报成"什么都没做成" */
|
|
|
dryRun: !!(config.runtime && config.runtime.dryRun),
|
|
dryRun: !!(config.runtime && config.runtime.dryRun),
|
|
|
dryRunCount: 0,
|
|
dryRunCount: 0,
|
|
|
- /** UID 文件里读到多少个(去重后) */
|
|
|
|
|
|
|
+ /** 取材结果里有多少个人(s1 = 文件行数,s2 = 粉丝列表采到的) */
|
|
|
totalUids: 0,
|
|
totalUids: 0,
|
|
|
followPlanned: 0,
|
|
followPlanned: 0,
|
|
|
dmPlanned: 0,
|
|
dmPlanned: 0,
|
|
@@ -9613,15 +9820,7 @@ TikTokAuto.dmflow = (function () {
|
|
|
errors: []
|
|
errors: []
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- /* ① 读 UID —— 这一步不碰手机,先做完再启动 App */
|
|
|
|
|
- var uids = readUids(settings.uidPath);
|
|
|
|
|
- summary.totalUids = uids.length;
|
|
|
|
|
- if (uids.length === 0) {
|
|
|
|
|
- summary.errors.push('UID 文本里没有可用的账号(' + (settings.uidPath || '(未设置路径)') + ')');
|
|
|
|
|
- return summary;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* ② 分两批:前 N 个关注,接着 M 个私信 —— 两批人不重叠 */
|
|
|
|
|
|
|
+ /* ① 先算两批各要多少人 —— 脚本二靠这个数才知道要采多少人 */
|
|
|
var followMax = settings.follow.enabled ? settings.follow.max : 0;
|
|
var followMax = settings.follow.enabled ? settings.follow.max : 0;
|
|
|
var sendMax = settings.send.enabled ? settings.send.max : 0;
|
|
var sendMax = settings.send.enabled ? settings.send.max : 0;
|
|
|
if (followMax === 0 && sendMax === 0) {
|
|
if (followMax === 0 && sendMax === 0) {
|
|
@@ -9629,30 +9828,75 @@ TikTokAuto.dmflow = (function () {
|
|
|
return summary;
|
|
return summary;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ var need = followMax + sendMax;
|
|
|
|
|
+ var uids = [];
|
|
|
|
|
+ var appReady = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (settings.script === 's2') {
|
|
|
|
|
+ /* 脚本二:人就在**自己**主页的粉丝列表里,必须先把 App 拉起来才能取材。
|
|
|
|
|
+ * 顺序与脚本一相反(脚本一是"文件不对就立刻返回,不折腾设备")。 */
|
|
|
|
|
+ progress('打开自己的粉丝列表', 0, need);
|
|
|
|
|
+ if (!TikTokAuto.flow.launchApp(config)) {
|
|
|
|
|
+ summary.errors.push('启动应用失败');
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!TikTokAuto.flow.ensureFeed(config)) {
|
|
|
|
|
+ summary.errors.push('未能进入信息流首页');
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
|
|
+ appReady = true;
|
|
|
|
|
+ uids = collectOwnFans(config, need, summary);
|
|
|
|
|
+ if (uids.length === 0) {
|
|
|
|
|
+ if (summary.errors.length === 0) {
|
|
|
|
|
+ summary.errors.push('自己的粉丝列表里没取到人(可能还没有粉丝)');
|
|
|
|
|
+ }
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ /* 脚本一:读文本。**这一步不碰手机**,文件不对就立刻返回。 */
|
|
|
|
|
+ uids = readUids(settings.uidPath);
|
|
|
|
|
+ if (uids.length === 0) {
|
|
|
|
|
+ summary.errors.push('UID 文本里没有可用的账号(' +
|
|
|
|
|
+ (settings.uidPath || '(未设置路径)') + ')');
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ summary.totalUids = uids.length;
|
|
|
|
|
+
|
|
|
|
|
+ /* ② 分两批:前 N 个关注,接着 M 个私信 —— 两批人不重叠 */
|
|
|
|
|
+
|
|
|
var followBatch = uids.slice(0, followMax);
|
|
var followBatch = uids.slice(0, followMax);
|
|
|
var dmBatch = uids.slice(followMax, followMax + sendMax);
|
|
var dmBatch = uids.slice(followMax, followMax + sendMax);
|
|
|
summary.followPlanned = followBatch.length;
|
|
summary.followPlanned = followBatch.length;
|
|
|
summary.dmPlanned = dmBatch.length;
|
|
summary.dmPlanned = dmBatch.length;
|
|
|
|
|
|
|
|
if (followBatch.length < followMax || dmBatch.length < sendMax) {
|
|
if (followBatch.length < followMax || dmBatch.length < sendMax) {
|
|
|
- summary.warnings.push('UID 只够 ' + uids.length + ' 个,少于计划(关注 ' +
|
|
|
|
|
|
|
+ summary.warnings.push((settings.script === 's2' ? '粉丝' : 'UID') +
|
|
|
|
|
+ '只够 ' + uids.length + ' 个,少于计划(关注 ' +
|
|
|
followMax + ' + 私信 ' + sendMax + '),本轮按实际数量执行');
|
|
followMax + ' + 私信 ' + sendMax + '),本轮按实际数量执行');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var total = followBatch.length + dmBatch.length;
|
|
var total = followBatch.length + dmBatch.length;
|
|
|
- log.step('dmflow', '私信任务开始:UID ' + uids.length + ' 个;关注 ' +
|
|
|
|
|
|
|
+ log.step('dmflow', '私信任务开始(' +
|
|
|
|
|
+ (settings.script === 's2' ? '自己粉丝取材' : 'UID 文本取材') + '):' +
|
|
|
|
|
+ (settings.script === 's2' ? '粉丝' : 'UID') + ' ' + uids.length + ' 个;关注 ' +
|
|
|
followBatch.length + ' 个 + 私信 ' + dmBatch.length + ' 个;' +
|
|
followBatch.length + ' 个 + 私信 ' + dmBatch.length + ' 个;' +
|
|
|
'每个之后休息 ' + Math.round(settings.rest.minMs / 1000) + '~' +
|
|
'每个之后休息 ' + Math.round(settings.rest.minMs / 1000) + '~' +
|
|
|
Math.round(settings.rest.maxMs / 1000) + ' 秒');
|
|
Math.round(settings.rest.maxMs / 1000) + ' 秒');
|
|
|
|
|
|
|
|
- progress('启动应用', 0, total);
|
|
|
|
|
- if (!TikTokAuto.flow.launchApp(config)) {
|
|
|
|
|
- summary.errors.push('启动应用失败');
|
|
|
|
|
- return summary;
|
|
|
|
|
- }
|
|
|
|
|
- if (!TikTokAuto.flow.ensureFeed(config)) {
|
|
|
|
|
- summary.errors.push('未能进入信息流首页');
|
|
|
|
|
- return summary;
|
|
|
|
|
|
|
+ /* 脚本二在取材时已经把 App 拉起来并停在「自己的主页」上了,别重复启动:
|
|
|
|
|
+ * launchApp 会重新发一次 startActivity(已在前台也一样),把页面状态打乱。
|
|
|
|
|
+ * 后面的 handleOne → openBySearch 第一步就是 ensureFeed,会自己回信息流。 */
|
|
|
|
|
+ if (!appReady) {
|
|
|
|
|
+ progress('启动应用', 0, total);
|
|
|
|
|
+ if (!TikTokAuto.flow.launchApp(config)) {
|
|
|
|
|
+ summary.errors.push('启动应用失败');
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!TikTokAuto.flow.ensureFeed(config)) {
|
|
|
|
|
+ summary.errors.push('未能进入信息流首页');
|
|
|
|
|
+ return summary;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var usedTexts = [];
|
|
var usedTexts = [];
|
|
@@ -9761,6 +10005,7 @@ TikTokAuto.dmflow = (function () {
|
|
|
return {
|
|
return {
|
|
|
runSession: runSession,
|
|
runSession: runSession,
|
|
|
readUids: readUids,
|
|
readUids: readUids,
|
|
|
|
|
+ collectOwnFans: collectOwnFans,
|
|
|
pickText: pickText,
|
|
pickText: pickText,
|
|
|
CONSECUTIVE_FAIL_LIMIT: CONSECUTIVE_FAIL_LIMIT
|
|
CONSECUTIVE_FAIL_LIMIT: CONSECUTIVE_FAIL_LIMIT
|
|
|
};
|
|
};
|