comment.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /**
  2. * comment.js — 评论:挑文案 → 打开面板 → 输入 → 发送 → **验证真的发出去了**
  3. *
  4. * 关键设计:发送后必须**验证**,而且要"多条证据同时成立"才敢说成功。
  5. * 只看着"面板关掉了"就宣布成功,是这类脚本最典型的假阳性来源
  6. * (面板也可能因为别的原因关掉,而评论被平台拒了)。
  7. *
  8. * 实测有效的两条证据:
  9. * 1. 评论计数自增: "Read or add comments. 149 comments" → "150 comments"
  10. * 2. 输入框被清空并恢复占位符 "Add comment...",且面板收起
  11. * 默认以"计数自增"为强证据;计数有延迟时退回到第 2 条(可用
  12. * comments.strictPostCheck 强制只认第 1 条)。
  13. *
  14. * 依赖:TikTokAuto.logger、TikTokAuto.layout、TikTokAuto.ui、TikTokAuto.wait、TikTokAuto.tiktok
  15. */
  16. TikTokAuto.comment = (function () {
  17. 'use strict';
  18. var PLACEHOLDER = 'Add comment...';
  19. /** 从评论池里随机挑一条,尽量不重复(池子用完了就允许重复并告警) */
  20. function pickText(config, used, step) {
  21. var pool = config.comments.pool;
  22. var candidates = [];
  23. for (var i = 0; i < pool.length; i += 1) {
  24. if (!config.comments.avoidRepeatWithinRun || used.indexOf(pool[i]) === -1) {
  25. candidates.push(pool[i]);
  26. }
  27. }
  28. if (candidates.length === 0) {
  29. TikTokAuto.logger.warn(step || 'comment', '评论池已用尽,本轮允许重复');
  30. candidates = pool;
  31. }
  32. return candidates[Math.floor(Math.random() * candidates.length)];
  33. }
  34. /** 打开评论面板:点内联输入框 → 等面板输入框出现 */
  35. function openPanel(config, step) {
  36. var story = step || 'openCommentPanel';
  37. var logger = TikTokAuto.logger;
  38. var tiktok = TikTokAuto.tiktok;
  39. if (tiktok.panelInput() !== null) {
  40. logger.info(story, '评论面板已经是打开状态');
  41. return true;
  42. }
  43. var inline = tiktok.inlineInput();
  44. if (!inline) {
  45. logger.error(story, '找不到评论入口(内联输入框)');
  46. return false;
  47. }
  48. TikTokAuto.ui.clickNode(inline, story);
  49. var ready = TikTokAuto.wait.waitFor(story, function () {
  50. return tiktok.panelInput() !== null ? true : null;
  51. }, config.timeouts.panelReadyMs, config.probe.pollTickMs);
  52. return ready === true;
  53. }
  54. /** 收起面板(只在确认面板处于打开状态时调用,绝不无脑 back) */
  55. function closePanel(step) {
  56. var tiktok = TikTokAuto.tiktok;
  57. if (!tiktok.commentPanelOpen()) {
  58. return false;
  59. }
  60. TikTokAuto.logger.info(step || 'closeCommentPanel', '收起评论面板');
  61. back();
  62. TikTokAuto.wait.until(function () {
  63. return tiktok.commentPanelOpen() ? null : true;
  64. }, 4000, 200);
  65. return true;
  66. }
  67. /**
  68. * 发送一条评论。
  69. *
  70. * @returns {ok, via, text, countBefore, countAfter, reason}
  71. */
  72. function post(config, text, step) {
  73. var story = step || 'postComment';
  74. var logger = TikTokAuto.logger;
  75. var tiktok = TikTokAuto.tiktok;
  76. var countBefore = tiktok.commentCount();
  77. var before = { countBefore: countBefore };
  78. if (!openPanel(config, story)) {
  79. return { ok: false, via: 'panel-not-opened', text: text, reason: '评论面板未打开' };
  80. }
  81. var input = tiktok.panelInput();
  82. var typed = TikTokAuto.ui.typeInto(input, text, story);
  83. if (!typed.ok) {
  84. return { ok: false, via: 'type-failed', text: text, reason: '输入框回读不一致' };
  85. }
  86. if (config.runtime.dryRun) {
  87. logger.warn(story, 'dryRun 模式:已输入但**不发送**,清空并收起面板。待发文案=' + JSON.stringify(text));
  88. var restore = tiktok.panelInput();
  89. if (restore) {
  90. restore.setText('');
  91. }
  92. TikTokAuto.wait.tick(300);
  93. closePanel(story);
  94. return { ok: true, via: 'dry-run', text: text, countBefore: countBefore, countAfter: countBefore, skipped: true };
  95. }
  96. var button = tiktok.sendButton(config);
  97. if (!button) {
  98. return { ok: false, via: 'send-button-not-found', text: text, reason: '定位不到发送按钮' };
  99. }
  100. TikTokAuto.ui.clickNode(button, story);
  101. return awaitOutcome(config, text, before, story);
  102. }
  103. /**
  104. * 等待并判定"评论是否真的发出去了",输出**带置信度**的结论。
  105. *
  106. * 证据分三级,从强到弱:
  107. * 高 comment-list —— 评论列表里真的出现了这条文案(地面真相)
  108. * 中 comment-count —— 列表头部计数自增
  109. * 低 panel-closed —— 面板收起 + 输入框复位为占位符
  110. *
  111. * 为什么不能只用计数(实测踩到):视频页那个评论角标不一定会立刻刷新,
  112. * 明明发成功却读回旧值。反过来,只看"面板关了"又可能被平台拒评骗过。
  113. * 所以用 waitFor 逐级升级证据,并把最终置信度**如实**报给上层。
  114. */
  115. function awaitOutcome(config, text, before, step) {
  116. var logger = TikTokAuto.logger;
  117. var tiktok = TikTokAuto.tiktok;
  118. // 必要条件:面板收起且输入框复位
  119. var closed = TikTokAuto.wait.waitFor(step, function () {
  120. if (tiktok.panelInput() !== null) {
  121. return null;
  122. }
  123. return tiktok.inlineInput() !== null ? true : null;
  124. }, config.timeouts.postMs, config.probe.pollTickMs);
  125. if (closed === null) {
  126. logger.error(step, '发送后面板未收起,疑似未发布成功');
  127. closePanel(step);
  128. return { ok: false, via: 'panel-still-open', text: text, reason: '发送后面板没有收起' };
  129. }
  130. // 中级证据:评论数自增。
  131. // **必须"等"而不是"看一眼"**——实测角标有延迟:发布返回后立刻读还是旧值,
  132. // 大约 1~2 秒后才 +1。看一眼就下结论的话,明明发成功了也会被判成弱证据。
  133. var counted = TikTokAuto.wait.waitFor(step, function () {
  134. var after = tiktok.commentCount();
  135. if (after === null || before.countBefore === null) {
  136. return null;
  137. }
  138. return after !== before.countBefore ? after : null;
  139. }, config.timeouts.countRefreshMs, 400);
  140. if (counted !== null) {
  141. logger.info(step, '评论已发布(计数自增)' + before.countBefore + '→' + counted + ' ' + JSON.stringify(text));
  142. return {
  143. ok: true, via: 'comment-count', confidence: 'medium', text: text,
  144. countBefore: before.countBefore, countAfter: counted
  145. };
  146. }
  147. // 高级证据:去评论列表里找这条文案(地面真相)
  148. if (config.comments.verifyInList) {
  149. var inList = verifyInCommentList(config, text, before, step);
  150. if (inList) {
  151. logger.info(step, '评论已发布(列表确认 via=' + inList.via + ')内容=' +
  152. JSON.stringify(text) + ' 计数=' + inList.count);
  153. return {
  154. ok: true, via: 'comment-list', confidence: 'high', text: text,
  155. countBefore: before.countBefore, countAfter: inList.count
  156. };
  157. }
  158. if (config.comments.strictPostCheck) {
  159. logger.error(step, '严格校验开启但评论列表里找不到该文案,判定为失败');
  160. return {
  161. ok: false, via: 'not-found-in-list', text: text,
  162. countBefore: before.countBefore, countAfter: tiktok.commentCount(),
  163. reason: '评论列表里找不到刚发布的文案'
  164. };
  165. }
  166. }
  167. logger.warn(step, '评论已发布,但只有弱证据(面板收起)。计数=' +
  168. before.countBefore + '→' + tiktok.commentCount() + ' 内容=' + JSON.stringify(text));
  169. return {
  170. ok: true, via: 'panel-closed-and-input-reset', confidence: 'low', text: text,
  171. countBefore: before.countBefore, countAfter: tiktok.commentCount()
  172. };
  173. }
  174. /**
  175. * 打开评论列表,找这条评论是否在列表里。
  176. * 返回 {count} 或 null。无论成败都会把面板收回去,不污染后续步骤。
  177. *
  178. * 为什么要"重试 + 等页面安静":紧跟着发布之后点评论角标,面板还在收起动画里,
  179. * 这一下点击会被吞掉(实测:列表死活不出来,等到超时)。
  180. * 所以每次点击前先等签名稳定,并且允许最多点三次。
  181. */
  182. function verifyInCommentList(config, text, before, step) {
  183. var tiktok = TikTokAuto.tiktok;
  184. var logger = TikTokAuto.logger;
  185. var clickAttempts = 0;
  186. var maxClicks = 3;
  187. var deadline = Date.now() + config.timeouts.panelReadyMs;
  188. while (Date.now() < deadline) {
  189. var headerCount = tiktok.listCommentCount();
  190. if (headerCount !== null) {
  191. // 最强:列表里真的出现了这条文案
  192. if (tiktok.commentListContains(text)) {
  193. closePanel(step);
  194. return { via: 'text-in-list', count: headerCount };
  195. }
  196. // 次强:列表头部计数变了(说明确实多了一条)
  197. if (before.countBefore !== null && headerCount !== before.countBefore) {
  198. closePanel(step);
  199. return { via: 'list-count', count: headerCount };
  200. }
  201. } else if (clickAttempts < maxClicks) {
  202. settle();
  203. var button = tiktok.commentButton();
  204. if (!button) {
  205. logger.warn(step, '找不到评论角标,跳过列表校验');
  206. return null;
  207. }
  208. clickAttempts += 1;
  209. logger.info(step, '第 ' + clickAttempts + ' 次打开评论列表');
  210. TikTokAuto.ui.clickNode(button, step);
  211. }
  212. TikTokAuto.wait.tick(400);
  213. }
  214. logger.warn(step, '评论列表校验未通过(点了 ' + clickAttempts + ' 次,列表计数=' +
  215. tiktok.listCommentCount() + ',发布前计数=' + before.countBefore + ')');
  216. closePanel(step);
  217. return null;
  218. }
  219. /** 等页面签名稳定下来(收起动画结束),再去做下一次点击 */
  220. function settle() {
  221. TikTokAuto.wait.waitQuiet(function () {
  222. return TikTokAuto.layout.signature();
  223. }, { maxMs: 1500, tickMs: 200, stableTimes: 2 });
  224. }
  225. return {
  226. PLACEHOLDER: PLACEHOLDER,
  227. pickText: pickText,
  228. openPanel: openPanel,
  229. closePanel: closePanel,
  230. post: post,
  231. awaitOutcome: awaitOutcome,
  232. verifyInCommentList: verifyInCommentList
  233. };
  234. }());