|
|
@@ -1,6 +1,6 @@
|
|
|
/*! tiktok-auto v1.0.0
|
|
|
- * built 2026-10-22T06:54:25.473Z
|
|
|
- * modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, flow, parse, uidset, filters, profile, idsource, resolve, idflow, main
|
|
|
+ * built 2026-10-28T09:52:51.438Z
|
|
|
+ * modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, interact, flow, parse, uidset, filters, profile, idsource, resolve, idflow, main
|
|
|
* 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。
|
|
|
*/
|
|
|
(function () {
|
|
|
@@ -355,7 +355,19 @@ TikTokAuto.bridge = (function () {
|
|
|
}
|
|
|
var watched = summary ? summary.attemptedVideos : 0;
|
|
|
var posted = summary ? summary.postedComments : 0;
|
|
|
- return '完成:处理 ' + watched + ' 个视频 / 发出 ' + posted + ' 条评论';
|
|
|
+ /* 只写"真的做了"的:跳过(已赞/已关注)不计入,否则用户会以为
|
|
|
+ * 自己点了 N 个赞,实际全是"本来就已经赞过"。 */
|
|
|
+ var liked = summary && summary.likedVideos ? summary.likedVideos : 0;
|
|
|
+ var followed = summary && summary.followedAuthors ? summary.followedAuthors : 0;
|
|
|
+ var actions = [];
|
|
|
+ if (liked > 0 || (summary && summary.likesRequested)) {
|
|
|
+ actions.push('点赞 ' + liked);
|
|
|
+ }
|
|
|
+ if (followed > 0 || (summary && summary.followsRequested)) {
|
|
|
+ actions.push('关注 ' + followed);
|
|
|
+ }
|
|
|
+ actions.push('评论 ' + posted);
|
|
|
+ return '完成:处理 ' + watched + ' 个视频 / ' + actions.join(' / ');
|
|
|
}
|
|
|
var reason = summary && summary.errors && summary.errors.length > 0
|
|
|
? summary.errors[0]
|
|
|
@@ -511,6 +523,21 @@ TikTokAuto.config = (function () {
|
|
|
minMs: 10000,
|
|
|
maxMs: 35000
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 点赞 / 关注:与 comments 平级的两个动作开关。
|
|
|
+ *
|
|
|
+ * 界面的「执行操作」是四项多选(关注 / 私信 / 点赞 / 评论),
|
|
|
+ * 这四项各自映射到一个 enabled —— 用户没勾就不做,不能因为"顺手"就点上。
|
|
|
+ *
|
|
|
+ * 与 ID 提取里的 `id.filter.likes`(按获赞数量筛账号)**不是一回事**,
|
|
|
+ * 只是名字撞了,层级不同。
|
|
|
+ */
|
|
|
+ likes: {
|
|
|
+ enabled: true
|
|
|
+ },
|
|
|
+ follows: {
|
|
|
+ enabled: true
|
|
|
+ },
|
|
|
comments: {
|
|
|
/* 是否执行评论动作。关掉就是「只浏览不发评论」——
|
|
|
* 界面把「执行操作」里的评论开关映射到这里:
|
|
|
@@ -1695,6 +1722,10 @@ TikTokAuto.tiktok = (function () {
|
|
|
var PKG = 'com.zhiliaoapp.musically';
|
|
|
var COMMENT_DESC_KEYS = ['Read or add comments', '评论'];
|
|
|
var LIKE_DESC_KEYS = ['Like video.', '点赞'];
|
|
|
+ /** 已点赞后同一个按钮的 desc(实测是 `Video liked`,不是 `Unlike`) */
|
|
|
+ var LIKED_DESC_KEYS = ['Video liked', '已点赞', 'Unlike'];
|
|
|
+ /** 关注按钮 desc 前缀(后面跟作者名)。`Followers` 会被 followButton 里单独排除。 */
|
|
|
+ var FOLLOW_DESC_KEYS = ['Follow ', '关注'];
|
|
|
var COMMENT_PLACEHOLDER = ['Add comment...', '添加评论', 'Add comment'];
|
|
|
var TOOL_ROW_ANCHORS = ['Stickers', 'Mention someone', 'Microphone', '表情', '提及', '语音'];
|
|
|
var RESULT_TABS = ['Top', 'Users', 'Videos', 'Sounds', 'Shop', 'LIVE', 'Places', 'Photos'];
|
|
|
@@ -1898,6 +1929,102 @@ TikTokAuto.tiktok = (function () {
|
|
|
return hits.length > 0 ? hits[0] : null;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 「已点赞」状态的点赞按钮。
|
|
|
+ *
|
|
|
+ * ★ 实测(2026-10-28,TikTok 47.1.4):点过赞之后同一个 `Button` 的 desc
|
|
|
+ * 从 `Like video. 2.4M likes` 变成 **`Video liked`**(不是 `Unlike`),
|
|
|
+ * 而里面那个 ImageView 的 `selected` 从 false 变成 true。
|
|
|
+ *
|
|
|
+ * 两个判据都要留:desc 受本地化影响,selected 是状态位不受文案影响 ——
|
|
|
+ * 任一命中就算已赞,比只认一个更耐改版。
|
|
|
+ */
|
|
|
+ function likedButton() {
|
|
|
+ var hits = findByLabel(className('android.widget.Button'), function (node) {
|
|
|
+ return startsWithAny(layout.desc(node), LIKED_DESC_KEYS);
|
|
|
+ });
|
|
|
+ return hits.length > 0 ? hits[0] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 点赞按钮里的图标节点(用它读 `selected`)。
|
|
|
+ * 实测:未赞 selected=false,已赞 selected=true。
|
|
|
+ */
|
|
|
+ function likeIcon() {
|
|
|
+ var button = likeButton() || likedButton();
|
|
|
+ if (!button) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ var icons = button.find(className('android.widget.ImageView'));
|
|
|
+ return icons.size() > 0 ? icons.get(0) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 'liked' | 'unliked' | 'unknown' */
|
|
|
+ function likeState() {
|
|
|
+ if (likedButton() !== null) {
|
|
|
+ return 'liked';
|
|
|
+ }
|
|
|
+ if (likeButton() !== null) {
|
|
|
+ return 'unliked';
|
|
|
+ }
|
|
|
+ /* 按钮本身找不到时,退回看图标状态位(有些版本用同一个 desc,只翻 selected) */
|
|
|
+ var icon = null;
|
|
|
+ try { icon = likeIcon(); } catch (ignored) { icon = null; }
|
|
|
+ if (icon !== null && icon.selected()) {
|
|
|
+ return 'liked';
|
|
|
+ }
|
|
|
+ return 'unknown';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 播放页的「关注作者」按钮。
|
|
|
+ *
|
|
|
+ * 实测 desc 形如 `Follow Marcgagilang`(`Follow` + 空格 + 作者名),
|
|
|
+ * 是 `android.widget.Button`。
|
|
|
+ * ⚠️ 首页顶部那个 `Following` 页签是 **FrameLayout**,不是 Button,
|
|
|
+ * 所以限定 className 就能把它排除掉,不必额外按坐标过滤。
|
|
|
+ */
|
|
|
+ function followButton() {
|
|
|
+ var hits = findByLabel(className('android.widget.Button'), function (node) {
|
|
|
+ var value = layout.desc(node);
|
|
|
+ if (!startsWithAny(value, FOLLOW_DESC_KEYS)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ /* `Followers` 是粉丝列表的入口,不是关注按钮 —— 前缀匹配会误命中 */
|
|
|
+ return value.indexOf('Followers') !== 0;
|
|
|
+ });
|
|
|
+ return hits.length > 0 ? hits[0] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 作者头像节点。用来区分「已关注(按钮消失)」和「页面还没渲染」。
|
|
|
+ * 实测 desc 形如 `Marcgagilang profile`。
|
|
|
+ */
|
|
|
+ function authorAvatar() {
|
|
|
+ var hits = findByLabel(className('android.widget.ImageView'), function (node) {
|
|
|
+ var value = layout.desc(node);
|
|
|
+ return value !== '' && / profile$/i.test(value);
|
|
|
+ });
|
|
|
+ return hits.length > 0 ? hits[0] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 'following' | 'not-following' | 'unknown'
|
|
|
+ *
|
|
|
+ * ★ 「已关注」的唯一表现是**按钮整个消失**(实测),所以不能只看"找不到按钮"
|
|
|
+ * —— 页面没渲染完也找不到。用作者头像是否存在来区分这两种情况:
|
|
|
+ * 头像在、按钮不在 = 已关注;两个都不在 = 页面还没好。
|
|
|
+ */
|
|
|
+ function followState() {
|
|
|
+ if (followButton() !== null) {
|
|
|
+ return 'not-following';
|
|
|
+ }
|
|
|
+ if (authorAvatar() !== null) {
|
|
|
+ return 'following';
|
|
|
+ }
|
|
|
+ return 'unknown';
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 播放页判据。
|
|
|
*
|
|
|
@@ -2658,6 +2785,12 @@ TikTokAuto.tiktok = (function () {
|
|
|
videoCards: videoCards,
|
|
|
commentButton: commentButton,
|
|
|
likeButton: likeButton,
|
|
|
+ likedButton: likedButton,
|
|
|
+ likeIcon: likeIcon,
|
|
|
+ likeState: likeState,
|
|
|
+ followButton: followButton,
|
|
|
+ authorAvatar: authorAvatar,
|
|
|
+ followState: followState,
|
|
|
onPlayerPage: onPlayerPage,
|
|
|
backButton: backButton,
|
|
|
commentCount: commentCount,
|
|
|
@@ -3237,6 +3370,125 @@ TikTokAuto.comment = (function () {
|
|
|
}());
|
|
|
|
|
|
|
|
|
+/* ============================== src/interact.js ============================== */
|
|
|
+/**
|
|
|
+ * interact.js — 播放页的两个轻量互动动作:**点赞** 与 **关注作者**
|
|
|
+ *
|
|
|
+ * ## 为什么单独一个模块
|
|
|
+ * 它们和评论一样是"在播放页上点一下、然后验证真的生效",但比评论简单得多:
|
|
|
+ * 不用开面板、不用输入、不用回读。所以不塞进 comment.js(那是评论发布的地盘)。
|
|
|
+ *
|
|
|
+ * ## 实测锚点(2026-10-28 / TikTok 47.1.4 / 1080×1920)
|
|
|
+ *
|
|
|
+ * | 元素 | 类 | content-desc | 备注 |
|
|
|
+ * |---|---|---|---|
|
|
|
+ * | 点赞(未赞) | Button | `Like video. 2.4M likes` | 里面的 ImageView selected=false |
|
|
|
+ * | 点赞(已赞) | Button | **`Video liked`** | ImageView selected=**true** |
|
|
|
+ * | 关注 | Button | `Follow <作者名>` | 已关注时**整个按钮消失** |
|
|
|
+ * | 作者头像 | ImageView | `<作者名> profile` | 用来区分"已关注"和"页面没渲染" |
|
|
|
+ *
|
|
|
+ * 点赞按钮和分享按钮**共用同一个 resource-id**(`g8g`),所以只能靠 desc 区分,
|
|
|
+ * 不要试图用 id 定位。
|
|
|
+ *
|
|
|
+ * ## 两条安全原则
|
|
|
+ *
|
|
|
+ * ① **状态不明就不点。** 读不到"当前是否已赞"时,点一下可能把已赞的取消掉 ——
|
|
|
+ * 和用户意图正好相反。宁可跳过并如实记录。
|
|
|
+ * ② **点了必须验。** 点击成功不代表生效(可能被弹窗挡、可能点偏)。
|
|
|
+ * 只有状态真的变了才算数,否则记失败,不虚报。
|
|
|
+ *
|
|
|
+ * 依赖:TikTokAuto.tiktok / ui / wait / logger
|
|
|
+ */
|
|
|
+TikTokAuto.interact = (function () {
|
|
|
+ 'use strict';
|
|
|
+
|
|
|
+ function logger() { return TikTokAuto.logger; }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 给当前视频点赞。
|
|
|
+ *
|
|
|
+ * @returns { ok, skipped?, via, reason? }
|
|
|
+ */
|
|
|
+ function like(step) {
|
|
|
+ var tiktok = TikTokAuto.tiktok;
|
|
|
+ var tag = step || 'like';
|
|
|
+
|
|
|
+ var state = tiktok.likeState();
|
|
|
+ if (state === 'liked') {
|
|
|
+ return { ok: true, skipped: true, via: 'already-liked', reason: '这个视频已经点过赞' };
|
|
|
+ }
|
|
|
+ if (state !== 'unliked') {
|
|
|
+ return {
|
|
|
+ ok: false, via: 'state-unknown',
|
|
|
+ reason: '读不到点赞状态,跳过(不盲点:可能是已赞,点一下会变成取消)'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ var button = tiktok.likeButton();
|
|
|
+ if (!button) {
|
|
|
+ return { ok: false, via: 'button-missing', reason: '找不到点赞按钮' };
|
|
|
+ }
|
|
|
+ TikTokAuto.ui.clickNode(button, tag);
|
|
|
+
|
|
|
+ /* 验证:desc 从 `Like video.` 变成 `Video liked`(或图标 selected 翻转) */
|
|
|
+ var done = TikTokAuto.wait.waitFor(tag + ':verify', function () {
|
|
|
+ return tiktok.likeState() === 'liked' ? true : null;
|
|
|
+ }, 6000, 300);
|
|
|
+ if (done === true) {
|
|
|
+ logger().info(tag, '点赞成功(状态已变为已赞)');
|
|
|
+ return { ok: true, via: 'state-changed' };
|
|
|
+ }
|
|
|
+ logger().warn(tag, '点赞后状态没有变化(可能被弹窗挡住或点偏了)');
|
|
|
+ return { ok: false, via: 'not-verified', reason: '点击后没有变成已赞' };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关注当前视频的作者。
|
|
|
+ *
|
|
|
+ * @returns { ok, skipped?, via, reason? }
|
|
|
+ */
|
|
|
+ function follow(step) {
|
|
|
+ var tiktok = TikTokAuto.tiktok;
|
|
|
+ var tag = step || 'follow';
|
|
|
+
|
|
|
+ var state = tiktok.followState();
|
|
|
+ if (state === 'following') {
|
|
|
+ return { ok: true, skipped: true, via: 'already-following', reason: '已经关注过这个作者' };
|
|
|
+ }
|
|
|
+ if (state !== 'not-following') {
|
|
|
+ /* 头像和按钮都读不到 = 页面还没渲染好。
|
|
|
+ * 这时不点:点到空白处可能触发别的动作(播放/暂停),没有意义。 */
|
|
|
+ return {
|
|
|
+ ok: false, via: 'page-not-ready',
|
|
|
+ reason: '作者头像与关注按钮都读不到,页面可能还没渲染完'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ var button = tiktok.followButton();
|
|
|
+ if (!button) {
|
|
|
+ return { ok: false, via: 'button-missing', reason: '找不到关注按钮' };
|
|
|
+ }
|
|
|
+ TikTokAuto.ui.clickNode(button, tag);
|
|
|
+
|
|
|
+ /* 验证:关注按钮**整个消失**(实测就是这种表现,不是改成 Following) */
|
|
|
+ var done = TikTokAuto.wait.waitFor(tag + ':verify', function () {
|
|
|
+ return tiktok.followState() === 'following' ? true : null;
|
|
|
+ }, 6000, 300);
|
|
|
+ if (done === true) {
|
|
|
+ logger().info(tag, '关注成功(关注按钮已消失)');
|
|
|
+ return { ok: true, via: 'button-gone' };
|
|
|
+ }
|
|
|
+ logger().warn(tag, '关注后按钮仍在,可能没生效');
|
|
|
+ return { ok: false, via: 'not-verified', reason: '点击后关注按钮仍然存在' };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ like: like,
|
|
|
+ follow: follow
|
|
|
+ };
|
|
|
+}());
|
|
|
+
|
|
|
+
|
|
|
/* ============================== src/flow.js ============================== */
|
|
|
/**
|
|
|
* flow.js — 业务流程编排
|
|
|
@@ -3526,6 +3778,13 @@ TikTokAuto.flow = (function () {
|
|
|
requestedVideos: config.target.maxVideos,
|
|
|
attemptedVideos: 0,
|
|
|
postedComments: 0,
|
|
|
+ /* 四个动作各自的成功计数:只有"真的做了"才加,跳过不算 */
|
|
|
+ likedVideos: 0,
|
|
|
+ followedAuthors: 0,
|
|
|
+ /* 用户是否勾了这两个动作 —— 用来决定结果文案里要不要提它们
|
|
|
+ * (勾了但全是"已赞"时显示 0 是有意义的信息;没勾就干脆不提) */
|
|
|
+ likesRequested: config.likes && config.likes.enabled === true,
|
|
|
+ followsRequested: config.follows && config.follows.enabled === true,
|
|
|
details: [],
|
|
|
errors: []
|
|
|
};
|
|
|
@@ -3587,6 +3846,27 @@ TikTokAuto.flow = (function () {
|
|
|
|
|
|
detail.watch = TikTokAuto.player.watch(config, { step: 'watch' });
|
|
|
|
|
|
+ /* 点赞 / 关注:都在播放页上点一下就好,不必跳转。
|
|
|
+ * 顺序放在评论**之前** —— 评论会打开面板盖住动作栏,
|
|
|
+ * 之后再点赞就得先把面板收掉,多一步且更容易出错。 */
|
|
|
+ if (config.likes && config.likes.enabled) {
|
|
|
+ detail.like = TikTokAuto.interact.like('like');
|
|
|
+ if (detail.like.ok && !detail.like.skipped) {
|
|
|
+ summary.likedVideos += 1;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ detail.like = { ok: true, skipped: true, reason: '本次未启用点赞动作' };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (config.follows && config.follows.enabled) {
|
|
|
+ detail.follow = TikTokAuto.interact.follow('follow');
|
|
|
+ if (detail.follow.ok && !detail.follow.skipped) {
|
|
|
+ summary.followedAuthors += 1;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ detail.follow = { ok: true, skipped: true, reason: '本次未启用关注动作' };
|
|
|
+ }
|
|
|
+
|
|
|
/* 没勾「评论」时只浏览:这不是失败,是一种合法的运行模式
|
|
|
* (界面上的「执行操作」没勾评论就是这个意思)。 */
|
|
|
if (config.comments.enabled) {
|