@web-auto/webauto 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +983 -128
  3. package/apps/desktop-console/dist/main/preload.mjs +7 -0
  4. package/apps/desktop-console/dist/renderer/index.html +622 -50
  5. package/apps/desktop-console/dist/renderer/index.js +2423 -469
  6. package/apps/desktop-console/dist/renderer/run.mts +6 -5
  7. package/apps/desktop-console/entry/ui-cli.mjs +672 -0
  8. package/apps/desktop-console/entry/ui-console.mjs +416 -29
  9. package/apps/webauto/entry/account.mjs +89 -53
  10. package/apps/webauto/entry/browser-status.mjs +7 -10
  11. package/apps/webauto/entry/lib/account-detect.mjs +254 -28
  12. package/apps/webauto/entry/lib/account-store.mjs +219 -30
  13. package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
  14. package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
  15. package/apps/webauto/entry/lib/profilepool.mjs +14 -5
  16. package/apps/webauto/entry/lib/quota-status.mjs +23 -0
  17. package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
  18. package/apps/webauto/entry/profilepool.mjs +106 -17
  19. package/apps/webauto/entry/schedule.mjs +612 -0
  20. package/apps/webauto/entry/weibo-unified.mjs +134 -0
  21. package/apps/webauto/entry/xhs-install.mjs +256 -31
  22. package/apps/webauto/entry/xhs-status.mjs +5 -2
  23. package/apps/webauto/entry/xhs-unified.mjs +631 -98
  24. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
  25. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
  26. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
  27. package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
  28. package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
  29. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
  30. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
  31. package/bin/camoufox-cli.mjs +61 -0
  32. package/bin/webauto.mjs +301 -54
  33. package/dist/modules/camo-backend/src/index.js +49 -1
  34. package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
  35. package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
  36. package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
  37. package/dist/modules/collection-manager/bloom-filter.js +91 -0
  38. package/dist/modules/collection-manager/date-utils.js +275 -0
  39. package/dist/modules/collection-manager/index.js +258 -0
  40. package/dist/modules/collection-manager/storage.js +195 -0
  41. package/dist/modules/collection-manager/types.js +47 -0
  42. package/dist/modules/logging/src/index.js +1 -1
  43. package/dist/modules/process-registry/index.js +230 -0
  44. package/dist/modules/rate-limiter/index.js +242 -0
  45. package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
  46. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
  47. package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
  48. package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
  49. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
  50. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
  51. package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
  52. package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
  53. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
  54. package/dist/modules/workflow/config/workflowRegistry.js +2 -0
  55. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
  56. package/dist/modules/workflow/src/runner.js +6 -0
  57. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
  58. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
  59. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
  60. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
  61. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
  62. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
  63. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
  64. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
  65. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
  66. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
  67. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  68. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
  69. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  70. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
  71. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
  72. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
  73. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
  74. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
  75. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
  76. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
  77. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  78. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
  79. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  80. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
  81. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  82. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
  83. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  84. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
  85. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  86. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
  87. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  88. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
  89. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  90. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
  91. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  92. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
  93. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  94. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
  95. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  96. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
  97. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  98. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
  99. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  100. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
  101. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  102. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
  103. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  104. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
  105. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  106. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
  107. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  108. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
  109. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  110. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
  111. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  112. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
  113. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  114. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
  115. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  116. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
  117. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  118. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
  119. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  120. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
  121. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
  122. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
  123. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  124. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
  125. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
  126. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
  127. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  128. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
  129. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  130. package/dist/services/shared/serviceProcessLogger.js +1 -1
  131. package/dist/services/unified-api/server.js +105 -11
  132. package/modules/camo-backend/src/index.ts +46 -1
  133. package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
  134. package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
  135. package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
  136. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
  137. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
  138. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
  139. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
  140. package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
  141. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
  142. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
  143. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
  144. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
  145. package/modules/collection-manager/bloom-filter.ts +112 -0
  146. package/modules/collection-manager/date-utils.ts +316 -0
  147. package/modules/collection-manager/index.ts +309 -0
  148. package/modules/collection-manager/package.json +10 -0
  149. package/modules/collection-manager/storage.ts +174 -0
  150. package/modules/collection-manager/types.ts +156 -0
  151. package/modules/logging/src/index.ts +1 -1
  152. package/modules/process-registry/index.ts +284 -0
  153. package/modules/rate-limiter/index.ts +322 -0
  154. package/modules/state/src/paths.ts +9 -1
  155. package/modules/task-scheduler/index.ts +293 -0
  156. package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
  157. package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
  158. package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
  159. package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
  160. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
  161. package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
  162. package/modules/workflow/config/workflowRegistry.ts +2 -0
  163. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
  164. package/modules/workflow/src/runner.ts +6 -0
  165. package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
  166. package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
  167. package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
  168. package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
  169. package/package.json +13 -4
  170. package/scripts/postinstall-resources.mjs +62 -0
  171. package/scripts/test/run-coverage.mjs +76 -0
  172. package/scripts/weibo/search.ts +49 -0
  173. package/services/shared/serviceProcessLogger.ts +1 -1
  174. package/services/unified-api/server.ts +98 -12
@@ -0,0 +1,1009 @@
1
+ /**
2
+ * Phase 3 Block: 闂備浇宕垫慨鏉懨洪妶鍥e亾濮樼厧鐏︽い銏$懇楠炲鎮欏▓鎸庨敜闂備礁婀遍崕銈夊垂娴e喚鏉介梻鍌欑劍鐎笛呯矙閹存繐鑰挎繛鎾次焑ract闂?
3
+ *
4
+ * 闂傚倷鑳堕崢褍鐣烽鍕剹濞撴埃鍋撻柟顖氳嫰铻栭柛娑卞枟濞?
5
+ * - 闂傚倷鑳堕幊鎾绘倶濮樿泛绠伴柛婵勫劜椤洟鏌熺€电校闁哄棙绮撻弻鈥崇暤椤斿吋鍣洪柟灞傚€濋弻锝夋偐閸欏銈梻鍌氬鐎氭澘鐣烽弴锛勭杸闁哄倽顔婄花鑽ょ磼閻愵剚绶叉い锕佷含缁牓宕滈崫绔慹Url闂傚倷鐒︾€笛呯矙閹达附鍤愭い鏍ㄧ矋瀹曟煡鏌嶈閸撴瑩鈥旈崘顔嘉ч柛顐亜濞堫參姊?xsec_token闂?
6
+ * - 闂備浇顕х换鎺楀磻閻愯娲冀椤愶綆娼熼梺鐟邦嚟婵數鈧碍宀搁弻娑㈠即閵娿儲鐝梺鍝ュ枎閻楁捇寮?
7
+ * - 濠电姷鏁告慨鎾晝閵夆晜鍤岄柣鎰靛墯閸欏繘鏌嶉崫鍕殲閻庢碍宀搁弻娑㈠即閵娿儲鐝梺鍝ュ枎閻楁捇寮诲☉銏犲唨妞ゆ劑鍨诲▓銈囩磽娴d粙鍝虹紒璇插閸掓帡妫冨☉鎺擃潔闂佸啿鐏堥弲娑欑閹岀唵閻犺櫣灏ㄥ銉╂煟閿曗偓閻栧ジ寮诲☉姗嗘僵闁绘挸绨肩花濠氭⒑閸涘浼曢柛銉e妿閸欏棗顪冮妶鍡橆梿闁稿鍔欓獮妤呮偄閸忚偐鍘介梺闈涱焾閸庨亶顢旈鍕厽闁挎洍鍋撴繛瀵稿厴楠?
8
+ * - 婵犲痉鏉库偓鏇㈠磹閸︻厽绠掗梺璇查閻忔氨鏁敓鐘茬畺婵炲棙鍨堕崗婊堟煕濞戝崬鐏辨繛绮瑰亾闂傚倷绀佸﹢閬嶁€﹂崼銉嬪洭鎮界粙璺唶闂佺懓澧庨弲顐㈢暤娓氣偓閺屾盯骞橀懠顒夋М婵炲濯崹鍫曞蓟濞戙垹绠i柨婵嗘噹閹偤姊虹粙娆惧劀缂佹彃娼¢獮?
9
+ * - 闂傚倷娴囬~澶愬箚鐏炲墽顩叉繝濠傚幘閻熼偊娼ㄩ柍褜鍓熼獮蹇涘礃椤旇棄浠奸柣蹇曞仜婢т粙鏁嶅鈧鍝劽虹紒妯衡枏闂佸憡鏌ㄩ鍥嚍闁稁鏁嗗〒姘处椤旀棃鎮楅崗澶婁壕闂佸憡鍔︽禍婵嬶綖瀹ュ鈷戦柟绋挎捣閳藉鏌eΔ浣瑰磳闁诡喚鏁婚、娆撴偩瀹€濠冮敜婵$偑鍊栧濠氬储瑜旇矾?闂傚倷鑳剁划顖炲礉濡ゅ懌鈧焦绻濋崟顓犵効闂佸湱澧楀妯兼喆閿曞倸绠归柟纰卞幖閻忥絿绱掗埀?
10
+ */
11
+ import path from 'node:path';
12
+ import fs, { promises as fsp } from 'node:fs';
13
+ import { controllerAction, delay } from '../utils/controllerAction.js';
14
+ import { resolveDownloadRoot, savePngBase64, takeScreenshotBase64 } from './helpers/evidence.js';
15
+ import { ensureCommentsOpened, extractVisibleComments, isCommentEnd, scrollComments, checkBottomWithBackAndForth, } from './helpers/xhsComments.js';
16
+ function formatError(err) {
17
+ if (err instanceof Error)
18
+ return err.stack || err.message;
19
+ try {
20
+ return JSON.stringify(err);
21
+ }
22
+ catch {
23
+ return String(err);
24
+ }
25
+ }
26
+ function normalizeText(s) {
27
+ return String(s || '').replace(/\s+/g, ' ').trim();
28
+ }
29
+ async function readCurrentUrl(sessionId, apiUrl) {
30
+ try {
31
+ const res = await controllerAction('browser:execute', { profile: sessionId, timeoutMs: 12000, script: 'window.location.href' }, apiUrl);
32
+ return String(res?.result || res?.data?.result || '');
33
+ }
34
+ catch {
35
+ return '';
36
+ }
37
+ }
38
+ async function gotoDetailWithRetry(sessionId, safeUrl, apiUrl) {
39
+ const attempts = [
40
+ { timeoutMs: 30000, label: 'goto_30s' },
41
+ { timeoutMs: 60000, label: 'goto_60s' },
42
+ ];
43
+ for (const attempt of attempts) {
44
+ console.log(`[Phase3Interact] goto attempt=${attempt.label} url=${safeUrl}`);
45
+ try {
46
+ const navRes = await controllerAction('browser:goto', { profile: sessionId, url: safeUrl, timeoutMs: attempt.timeoutMs }, apiUrl);
47
+ if (navRes?.success === false) {
48
+ const err = String(navRes?.error || 'goto_failed');
49
+ console.warn(`[Phase3Interact] goto failed: ${err}`);
50
+ }
51
+ else {
52
+ return { ok: true };
53
+ }
54
+ }
55
+ catch (err) {
56
+ const msg = String(err?.message || err || 'goto_error');
57
+ console.warn(`[Phase3Interact] goto error: ${msg}`);
58
+ if (!/timeout/i.test(msg)) {
59
+ return { ok: false, error: msg };
60
+ }
61
+ }
62
+ // If goto timed out, check whether we still landed on detail page.
63
+ const currentUrl = await readCurrentUrl(sessionId, apiUrl);
64
+ if (currentUrl.includes('/explore/') && currentUrl.includes('xsec_token=')) {
65
+ console.log(`[Phase3Interact] goto timeout but detail loaded: ${currentUrl}`);
66
+ return { ok: true };
67
+ }
68
+ }
69
+ return { ok: false, error: 'goto_timeout' };
70
+ }
71
+ function resolveLikeIconState(useHref) {
72
+ const href = String(useHref || '').trim().toLowerCase();
73
+ if (href.includes('#liked'))
74
+ return 'liked';
75
+ if (href.includes('#like'))
76
+ return 'unliked';
77
+ return 'unknown';
78
+ }
79
+ function parseLikeRuleToken(token) {
80
+ const raw = String(token || '').trim();
81
+ if (!raw)
82
+ return null;
83
+ const m = raw.match(/^\\{\\s*(.+?)\\s*([+\\-\\uFF0B\\uFF0D])\\s*(.+?)\\s*\\}$/);
84
+ if (!m) {
85
+ return { kind: 'contains', include: raw, raw };
86
+ }
87
+ const left = normalizeText(m[1]);
88
+ const right = normalizeText(m[3]);
89
+ if (!left || !right)
90
+ return null;
91
+ const op = m[2] === '\\uFF0B' ? '+' : m[2] === '\\uFF0D' ? '-' : m[2];
92
+ if (op === '+') {
93
+ return { kind: 'and', includeA: left, includeB: right, raw: `{${left} + ${right}}` };
94
+ }
95
+ return { kind: 'include_without', include: left, exclude: right, raw: `{${left} - ${right}}` };
96
+ }
97
+ export function compileLikeRules(likeKeywords) {
98
+ const rows = Array.isArray(likeKeywords) ? likeKeywords : [];
99
+ const rules = [];
100
+ for (const row of rows) {
101
+ const parsed = parseLikeRuleToken(String(row || '').trim());
102
+ if (!parsed)
103
+ continue;
104
+ rules.push(parsed);
105
+ }
106
+ return rules;
107
+ }
108
+ export function matchLikeText(textRaw, rules) {
109
+ const text = normalizeText(textRaw);
110
+ if (!text)
111
+ return { ok: false, reason: 'empty_text' };
112
+ if (!Array.isArray(rules) || rules.length === 0)
113
+ return { ok: false, reason: 'no_rules' };
114
+ for (const rule of rules) {
115
+ if (rule.kind === 'contains') {
116
+ if (text.includes(rule.include)) {
117
+ return { ok: true, reason: 'contains_match', matchedRule: rule.raw };
118
+ }
119
+ continue;
120
+ }
121
+ if (rule.kind === 'and') {
122
+ if (text.includes(rule.includeA) && text.includes(rule.includeB)) {
123
+ return { ok: true, reason: 'and_match', matchedRule: rule.raw };
124
+ }
125
+ continue;
126
+ }
127
+ if (text.includes(rule.include) && !text.includes(rule.exclude)) {
128
+ return { ok: true, reason: 'include_without_match', matchedRule: rule.raw };
129
+ }
130
+ }
131
+ return { ok: false, reason: 'no_rule_match' };
132
+ }
133
+ async function highlightLikeButton(sessionId, index, apiUrl) {
134
+ return controllerAction('container:operation', {
135
+ containerId: 'xiaohongshu_detail.comment_section.comment_item',
136
+ operationId: 'highlight',
137
+ sessionId,
138
+ timeoutMs: 12000,
139
+ config: {
140
+ index,
141
+ target: '.like-wrapper',
142
+ style: '12px solid #00e5ff',
143
+ duration: 8000,
144
+ channel: 'virtual-like-like',
145
+ visibleOnly: true,
146
+ },
147
+ }, apiUrl);
148
+ }
149
+ async function isLikeButtonInViewport(sessionId, index, apiUrl) {
150
+ try {
151
+ const res = await controllerAction('browser:execute', {
152
+ profile: sessionId,
153
+ timeoutMs: 12000,
154
+ script: `(() => {
155
+ const idx = ${index};
156
+ const isVisible = (el) => {
157
+ const r = el.getBoundingClientRect();
158
+ return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight;
159
+ };
160
+ const items = Array.from(document.querySelectorAll('.comment-item')).filter(isVisible);
161
+ const el = items[idx];
162
+ if (!el) return { ok: false, inViewport: false };
163
+ const like = Array.from(el.querySelectorAll('.like-wrapper')).find((node) => node.closest('.comment-item') === el);
164
+ if (!like) return { ok: false, inViewport: false };
165
+ const r = like.getBoundingClientRect();
166
+ const inViewport = r.width > 0 && r.height > 0 && r.top >= 0 && r.bottom <= window.innerHeight && r.left >= 0 && r.right <= window.innerWidth;
167
+ return { ok: true, inViewport };
168
+ })()`,
169
+ }, apiUrl);
170
+ return res?.result?.inViewport === true;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ async function ensureCommentVisibleCentered(sessionId, apiUrl, index) {
177
+ for (let i = 0; i < 3; i++) {
178
+ const rect = await controllerAction('browser:execute', {
179
+ profile: sessionId,
180
+ timeoutMs: 12000,
181
+ script: `(() => {
182
+ const idx = ${index};
183
+ const isVisible = (el) => {
184
+ const r = el.getBoundingClientRect();
185
+ return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight;
186
+ };
187
+ const items = Array.from(document.querySelectorAll('.comment-item')).filter(isVisible);
188
+ if (!items[idx]) return { ok: false };
189
+ const r = items[idx].getBoundingClientRect();
190
+ const vh = window.innerHeight;
191
+ return { ok: true, top: r.top, bottom: r.bottom, height: r.height, vh };
192
+ })()`
193
+ }, apiUrl).then(res => res?.result || res?.data?.result || null);
194
+ if (!rect || rect.ok !== true)
195
+ return false;
196
+ const pad = 80;
197
+ const visible = rect.top >= pad && rect.bottom <= (rect.vh - pad);
198
+ if (visible)
199
+ return true;
200
+ const dir = rect.top < pad ? 'up' : 'down';
201
+ const amount = Math.min(800, Math.ceil((rect.top < pad ? (pad - rect.top) : (rect.bottom - (rect.vh - pad))) + 120));
202
+ await controllerAction('container:operation', {
203
+ containerId: 'xiaohongshu_detail.comment_section',
204
+ operationId: 'scroll',
205
+ sessionId,
206
+ timeoutMs: 12000,
207
+ config: { direction: dir, amount },
208
+ }, apiUrl).catch(() => { });
209
+ await delay(500);
210
+ }
211
+ return false;
212
+ }
213
+ async function clickLikeButtonByIndex(sessionId, index, apiUrl) {
214
+ return controllerAction('container:operation', {
215
+ containerId: 'xiaohongshu_detail.comment_section.comment_item',
216
+ operationId: 'click',
217
+ sessionId,
218
+ timeoutMs: 12000,
219
+ // 闂傚倷绀侀幖顐も偓姘煎墮闇夐柣鎴f閻忚櫕淇婇姘倯閻忓繐閰i弻鐔封枔閸喗鐝濋梺绋跨箰濞层劎妲愰幘瀛樺闁告繂瀚悘浣逛繆閵堝洤孝婵炲樊鍙冮獮鍐ㄢ枎閹炬潙浠梺鍝勵槹鐎笛囨儊閺嶎厽鈷戦柟绋挎捣閳藉绻涢悡搴gickOperation 闂傚倷绀侀幉锟犲礉閺囥垹绠犳慨妞诲亾鐎规洘娲熼獮姗€顢欓挊澶夌紦?bbox/elementFromPoint 闂傚倸鍊风欢锟犲磻閸曨垁鍥焼瀹ュ懐锛涢梺鎸庣箓椤﹀崬鐣垫笟鈧弻鐔碱敍濞戞﹩妫嗗┑鐐茬墕閻栧ジ寮?systemInput.mouseClick闂?
220
+ config: { index, target: '.like-wrapper', useSystemMouse: true, visibleOnly: true },
221
+ }, apiUrl);
222
+ }
223
+ async function expandMoreComments(sessionId, apiUrl) {
224
+ await controllerAction('container:operation', {
225
+ containerId: 'xiaohongshu_detail.comment_section.show_more_button',
226
+ operationId: 'click',
227
+ sessionId,
228
+ timeoutMs: 12000,
229
+ config: { visibleOnly: true, useSystemMouse: true },
230
+ }, apiUrl).catch(() => { });
231
+ }
232
+ async function verifyLikedBySignature(sessionId, apiUrl, signature) {
233
+ const targetText = normalizeText(signature.text);
234
+ if (!targetText)
235
+ return false;
236
+ // 婵犵數鍋炲娆撳触鐎n喗鏅梻浣告啞钃辩紒瀣崌楠炴劘顦圭€殿喛娉涢埢搴ㄥ箚瑜庡▍宥夋⒒?extract闂傚倷鐒︾€笛呯矙閹达附鍋嬮柛鈩冨搸娴滃湱鎲搁弮鍫濈畾闁搞儮鏂侀崑鎾绘晲鎼粹€冲濠电偛鍚嬬敮锟犲蓟濞戞ǚ鏋庢繛鍡樺灥閸╁矂姊洪崫銉ヤ沪闁瑰憡濞婂?
237
+ try {
238
+ const rows = await extractVisibleComments(sessionId, apiUrl, 60);
239
+ const found = rows.find((r) => {
240
+ const t = normalizeText(String(r.text || ''));
241
+ if (!t || t !== targetText)
242
+ return false;
243
+ const uid = String(r.user_id || '').trim();
244
+ const un = String(r.user_name || '').trim();
245
+ if (signature.userId && uid && uid !== signature.userId)
246
+ return false;
247
+ if (!signature.userId && signature.userName && un && un !== signature.userName)
248
+ return false;
249
+ return true;
250
+ });
251
+ if (found) {
252
+ // user container-lib 闂傚倷绀侀幉锟犳偡椤栫偛鍨傞柣銏㈩焾缁€鍌涙叏濡炶浜鹃悗娈垮枤閺佸銆侀弮鍫濋唶婵犻潧妫滅粊瀵哥磽?like_status闂傚倷鐒︾€笛呯矙閹烘梻鐭欓柟閭﹀枤缁?like_active 婵犵數鍋為崹鍫曞箰閸濄儳鐭撻梻鍫熷厷閿濆绠瑰ù锝呮憸娴煎鈹戦悩璇у伐闁哥噥鍋婂畷鐢稿箳濡や胶鍘介梺褰掑亰閸n噣寮ㄩ幍顔剧<闁稿本绋戞慨宥夋煕閵婏箑鍔ら柍瑙勫灦缁绘繃鎯旈埄鍐惧悩婵犵數鍋為崹鍫曞蓟閵娾敒銊╁焵椤掑倻纾奸弶鍫涘妿缁犳﹢鏌嶈閸撴瑥锕㈤柆宥呯疇闁圭偓鏋奸弸宥夋煙閻戞ɑ鈷掔痪鎯с偢閺岀喖骞嗚閸ょ喎霉濠婂骸鐏犻棁澶愭煟濡搫绾ч柛锝呮憸缁辨挸顓奸崱妯煎弳濡炪倖娲╃徊鎯ь焽韫囨稑惟闁靛/鍐e亾閹烘梻纾藉ù锝嗗絻娴?
253
+ const hint = String(found.like_status || '');
254
+ if (hint.includes('liked') || hint.includes('like-liked'))
255
+ return true;
256
+ }
257
+ }
258
+ catch {
259
+ // fallback below
260
+ }
261
+ // fallback闂傚倷鐒︾€笛呯矙閹烘埈娼╅柕濞垮剭濞差亜閿ゆ俊銈傚亾缂佺姵濞婇弻鏇熷緞濡厧甯ラ梺?DOM闂傚倷鐒︾€笛呯矙閹达附鍋嬮柟鐐綑椤曢亶鏌嶉崫鍕櫣缂佲偓?index 濠电姷鏁告慨闈浢洪弽顓炵9閻犱礁纾弰鍌炴⒑鐠囪尙鍑圭紒鑼帛缁旂喖宕奸妷銉ユ優閻熸粌绻橀獮蹇涙偐濞茬粯鏅┑顔斤供閸撴稒瀵奸崼銉︹拺?
262
+ try {
263
+ const res = await controllerAction('browser:execute', {
264
+ profile: sessionId,
265
+ timeoutMs: 12000,
266
+ script: `(() => {
267
+ const targetText = ${JSON.stringify(targetText)};
268
+ const userName = ${JSON.stringify(String(signature.userName || ''))};
269
+ const items = Array.from(document.querySelectorAll('.comment-item'));
270
+ for (const el of items) {
271
+ const r = el.getBoundingClientRect();
272
+ const visible = r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight;
273
+ if (!visible) continue;
274
+ const textEl = el.querySelector('.content') || el.querySelector('.comment-content') || el.querySelector('p');
275
+ const t = (textEl?.textContent || '')
276
+ .replace(/\\s+/g, ' ')
277
+ .trim();
278
+ if (t !== targetText) continue;
279
+ if (userName) {
280
+ const n = (el.querySelector('.name')?.textContent || el.querySelector('.username')?.textContent || el.querySelector('.user-name')?.textContent || '')
281
+ .replace(/\\s+/g, ' ')
282
+ .trim();
283
+ if (n && n !== userName) continue;
284
+ }
285
+ const like = el.querySelector('.like-wrapper');
286
+ const use = like?.querySelector('use');
287
+ const useHref = use?.getAttribute('xlink:href') || use?.getAttribute('href') || '';
288
+ return { ok: true, useHref };
289
+ }
290
+ return { ok: false, useHref: '' };
291
+ })()`,
292
+ }, apiUrl);
293
+ const useHref = String(res?.result?.useHref || res?.useHref || '');
294
+ return useHref.includes('#liked');
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
300
+ async function getLikeStateForVisibleCommentIndex(sessionId, apiUrl, index) {
301
+ try {
302
+ const res = await controllerAction('browser:execute', {
303
+ profile: sessionId,
304
+ timeoutMs: 12000,
305
+ script: `(() => {
306
+ const idx = ${JSON.stringify(index)};
307
+ const isVisible = (el) => {
308
+ const r = el.getBoundingClientRect();
309
+ return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
310
+ };
311
+ const visibleItems = Array.from(document.querySelectorAll('.comment-item')).filter(isVisible);
312
+ const el = visibleItems[idx];
313
+ if (!el) return { ok: false, likeClass: '', useHref: '', count: '', iconState: 'unknown' };
314
+ const like = el.querySelector('.like-wrapper');
315
+ const use = like?.querySelector('svg.like-icon use') || like?.querySelector('use');
316
+ const useHref = use?.getAttribute('xlink:href') || use?.getAttribute('href') || use?.href?.baseVal || '';
317
+ const count = (like?.querySelector('.count')?.textContent || '').replace(/\\s+/g, ' ').trim();
318
+ const likeClass = like ? String(like.className || '') : '';
319
+ const iconState = useHref.includes('#liked') ? 'liked' : useHref.includes('#like') ? 'unliked' : (likeClass.includes('like-active') ? 'liked' : 'unknown');
320
+ return { ok: true, likeClass, useHref, count, iconState };
321
+ })()`,
322
+ }, apiUrl);
323
+ const useHref = String(res?.result?.useHref || res?.useHref || '');
324
+ return {
325
+ useHref,
326
+ count: String(res?.result?.count || res?.count || ''),
327
+ likeClass: String(res?.result?.likeClass || res?.likeClass || ''),
328
+ iconState: resolveLikeIconState(String(res?.result?.iconState || res?.iconState || useHref)),
329
+ };
330
+ }
331
+ catch {
332
+ return { useHref: '', count: '', likeClass: '', iconState: 'unknown' };
333
+ }
334
+ }
335
+ async function checkLikeGate(profileId) {
336
+ try {
337
+ const res = await fetch(`http://127.0.0.1:7790/like/status/${encodeURIComponent(profileId)}`);
338
+ const data = await res.json();
339
+ return {
340
+ allowed: Boolean(data?.allowed ?? data?.ok ?? true),
341
+ current: Number(data?.current ?? data?.countInWindow ?? 0),
342
+ limit: Number(data?.limit ?? data?.maxCount ?? 6),
343
+ };
344
+ }
345
+ catch {
346
+ return { allowed: true, current: 0, limit: 6 };
347
+ }
348
+ }
349
+ async function requestLikeGate(profileId) {
350
+ try {
351
+ const res = await fetch('http://127.0.0.1:7790/like', {
352
+ method: 'POST',
353
+ headers: { 'Content-Type': 'application/json' },
354
+ body: JSON.stringify({ profileId, key: profileId }),
355
+ });
356
+ const data = await res.json();
357
+ return {
358
+ allowed: Boolean(data?.allowed ?? data?.ok ?? true),
359
+ current: Number(data?.current ?? data?.countInWindow ?? 0),
360
+ limit: Number(data?.limit ?? data?.maxCount ?? 6),
361
+ };
362
+ }
363
+ catch {
364
+ return { allowed: true, current: 0, limit: 6 };
365
+ }
366
+ }
367
+ function emitLikeEvent(keyword, env, payload) {
368
+ try {
369
+ const home = process.env.HOME || process.env.USERPROFILE || require('os').homedir();
370
+ const logPath = require('path').join(home, '.webauto', 'download', 'xiaohongshu', env, keyword, 'run-events.jsonl');
371
+ const row = { ts: new Date().toISOString(), type: 'like', ...payload };
372
+ fs.appendFileSync(logPath, JSON.stringify(row) + '\n', 'utf8');
373
+ }
374
+ catch { }
375
+ }
376
+ // Like deduplication: persist liked signatures to disk
377
+ function getLikeStatePath(keyword, env) {
378
+ const home = process.env.HOME || process.env.USERPROFILE || require('os').homedir();
379
+ return path.join(home, '.webauto', 'download', 'xiaohongshu', env, keyword, '.like-state.jsonl');
380
+ }
381
+ function loadLikedSignatures(keyword, env) {
382
+ try {
383
+ const p = getLikeStatePath(keyword, env);
384
+ if (!fs.existsSync(p))
385
+ return new Set();
386
+ const lines = fs.readFileSync(p, 'utf8').split('\n').filter(Boolean);
387
+ const sigs = new Set();
388
+ for (const line of lines) {
389
+ try {
390
+ const obj = JSON.parse(line);
391
+ if (obj.signature)
392
+ sigs.add(obj.signature);
393
+ }
394
+ catch { }
395
+ }
396
+ return sigs;
397
+ }
398
+ catch {
399
+ return new Set();
400
+ }
401
+ }
402
+ function saveLikedSignature(keyword, env, signature) {
403
+ try {
404
+ const p = getLikeStatePath(keyword, env);
405
+ const row = { ts: new Date().toISOString(), signature };
406
+ fs.appendFileSync(p, JSON.stringify(row) + '\n', 'utf8');
407
+ }
408
+ catch { }
409
+ }
410
+ function makeSignature(noteId, userId, userName, text) {
411
+ const normalizedText = String(text || '').trim().slice(0, 200);
412
+ return [noteId, String(userId || ''), String(userName || ''), normalizedText].join('|');
413
+ }
414
+ function normalizeHarvestComment(noteId, row) {
415
+ return {
416
+ noteId,
417
+ userName: String(row.user_name || '').trim(),
418
+ userId: String(row.user_id || '').trim(),
419
+ content: String(row.text || '').replace(/\s+/g, ' ').trim(),
420
+ time: String(row.timestamp || '').trim(),
421
+ likeCount: 0,
422
+ ts: new Date().toISOString(),
423
+ };
424
+ }
425
+ async function readJsonlRows(filePath) {
426
+ try {
427
+ const text = await fsp.readFile(filePath, 'utf8');
428
+ return text
429
+ .split('\n')
430
+ .map((line) => line.trim())
431
+ .filter(Boolean)
432
+ .map((line) => {
433
+ try {
434
+ return JSON.parse(line);
435
+ }
436
+ catch {
437
+ return null;
438
+ }
439
+ })
440
+ .filter(Boolean);
441
+ }
442
+ catch {
443
+ return [];
444
+ }
445
+ }
446
+ async function appendJsonlRows(filePath, rows) {
447
+ if (!rows.length)
448
+ return;
449
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
450
+ const payload = rows.map((row) => JSON.stringify(row)).join('\n') + '\n';
451
+ await fsp.appendFile(filePath, payload, 'utf8');
452
+ }
453
+ export async function execute(input) {
454
+ const { sessionId, noteId, safeUrl, likeKeywords, maxLikesPerRound = 2, dryRun = false, unifiedApiUrl = 'http://127.0.0.1:7701', keyword = 'unknown', env = 'debug', reuseCurrentDetail = false, commentsAlreadyOpened = false, collectComments = false, persistCollectedComments = false, commentsFilePath = '', evidenceDir = '', onRound, } = input;
455
+ // Load persisted liked signatures for dedup (resume support)
456
+ const likedSignatures = loadLikedSignatures(keyword, env);
457
+ const compiledLikeRules = compileLikeRules(likeKeywords);
458
+ console.log(`[Phase3Interact] 闂佽瀛╅鏍窗閹烘纾婚柟鐐灱閺€鑺ャ亜閺冨倵鎷¢柛搴$箲閵囧嫰鏁傜憴鍕彋闂佽鍠栭悥鐓庣暦閸楃倣鏃堝礃椤忓懎娅ч梻? ${noteId}, 闂佽楠稿﹢閬嶁€﹂崼婵愬殨閻犺櫣灏ㄩ懓鍨€掑锝呬壕闂佽鍠楅悷褍鈽夐悽绋垮窛妞ゆ牭绲剧粊銈夋⒑閼姐倕小闁绘帪绠戦…鍨熼懖鈺冾槸? ${likedSignatures.size}`);
459
+ console.log(`[Phase3Interact] 闂傚倷鑳堕…鍫㈡崲閹寸偟绠惧┑鐘叉搐閺嬩焦銇勯幘璺盒i悗姘皑閳ь剙绠嶉崕閬嶆偋濠婂喚鐒介柟鎵閻? ${compiledLikeRules.length > 0 ? compiledLikeRules.map((r) => r.raw).join(' | ') : '(empty)'}`);
460
+ const likedComments = [];
461
+ let likedCount = 0;
462
+ let scannedCount = 0;
463
+ let reachedBottom = false;
464
+ let bottomReason = '';
465
+ let scrollCount = 0;
466
+ let totalDedupSkipped = 0;
467
+ let totalAlreadyLikedSkipped = 0;
468
+ let totalRuleHits = 0;
469
+ let totalNotVisibleSkipped = 0;
470
+ let totalNestedSkipped = 0;
471
+ let totalGateBlocked = 0;
472
+ let totalClickFailed = 0;
473
+ let totalVerifyFailed = 0;
474
+ let totalClickAttempts = 0;
475
+ let mismatchPostScreenshot = null;
476
+ const maxScrolls = Infinity;
477
+ const harvestPath = String(commentsFilePath || '').trim();
478
+ const shouldHarvest = Boolean(collectComments);
479
+ const shouldPersistHarvest = shouldHarvest && Boolean(persistCollectedComments) && Boolean(harvestPath);
480
+ const harvestedKeySet = new Set();
481
+ let harvestedAdded = 0;
482
+ let harvestedTotal = 0;
483
+ if (shouldPersistHarvest && harvestPath) {
484
+ const existingRows = await readJsonlRows(harvestPath);
485
+ for (const row of existingRows) {
486
+ const key = `${String(row?.userId || '')}:${String(row?.content || '')}`;
487
+ if (!key.endsWith(':'))
488
+ harvestedKeySet.add(key);
489
+ }
490
+ harvestedTotal = harvestedKeySet.size;
491
+ }
492
+ const likeEvidenceBaseDir = String(evidenceDir || '').trim() || path.join(resolveDownloadRoot(), 'xiaohongshu', env, keyword, dryRun ? 'virtual-like' : 'like-evidence', noteId);
493
+ const errorEvidenceBaseDir = path.join(resolveDownloadRoot(), 'xiaohongshu', env, keyword, 'phase3-error', noteId);
494
+ let likeEvidenceDir = null;
495
+ let errorEvidenceDir = null;
496
+ const ensureLikeEvidenceDir = async () => {
497
+ if (likeEvidenceDir)
498
+ return likeEvidenceDir;
499
+ await fsp.mkdir(likeEvidenceBaseDir, { recursive: true });
500
+ likeEvidenceDir = likeEvidenceBaseDir;
501
+ return likeEvidenceDir;
502
+ };
503
+ const writeHitMeta = async (prefix, payload) => {
504
+ try {
505
+ const dir = await ensureLikeEvidenceDir();
506
+ const filePath = path.join(dir, `${prefix}.json`);
507
+ await fsp.writeFile(filePath, JSON.stringify(payload, null, 2), 'utf8');
508
+ return filePath;
509
+ }
510
+ catch {
511
+ return null;
512
+ }
513
+ };
514
+ const ensureErrorEvidenceDir = async () => {
515
+ if (errorEvidenceDir)
516
+ return errorEvidenceDir;
517
+ await fsp.mkdir(errorEvidenceBaseDir, { recursive: true });
518
+ errorEvidenceDir = errorEvidenceBaseDir;
519
+ return errorEvidenceDir;
520
+ };
521
+ const captureLikeEvidence = async (prefix) => {
522
+ try {
523
+ const base64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
524
+ if (!base64)
525
+ return null;
526
+ const dir = await ensureLikeEvidenceDir();
527
+ const name = `${prefix}-${Date.now()}.png`;
528
+ return await savePngBase64(base64, path.join(dir, name));
529
+ }
530
+ catch {
531
+ return null;
532
+ }
533
+ };
534
+ const captureErrorEvidence = async (prefix) => {
535
+ try {
536
+ const base64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
537
+ if (!base64)
538
+ return null;
539
+ const dir = await ensureErrorEvidenceDir();
540
+ const name = `${prefix}-${Date.now()}.png`;
541
+ return await savePngBase64(base64, path.join(dir, name));
542
+ }
543
+ catch {
544
+ return null;
545
+ }
546
+ };
547
+ const gateStatus = await checkLikeGate(sessionId);
548
+ console.log('[Phase3Interact] Like Gate: ' + gateStatus.current + '/' + gateStatus.limit + ' ' + (gateStatus.allowed ? 'OK' : 'BLOCKED'));
549
+ if (!reuseCurrentDetail) {
550
+ const navRes = await gotoDetailWithRetry(sessionId, safeUrl, unifiedApiUrl);
551
+ if (!navRes.ok) {
552
+ const errorShot = await captureErrorEvidence('goto-failed');
553
+ return {
554
+ success: false,
555
+ noteId,
556
+ likedCount: 0,
557
+ scannedCount: 0,
558
+ likedComments: [],
559
+ evidenceDir: likeEvidenceDir || '',
560
+ dedupSkipped: 0,
561
+ alreadyLikedSkipped: 0,
562
+ reachedBottom: false,
563
+ error: navRes?.error || 'goto failed',
564
+ errorEvidence: { screenshot: errorShot },
565
+ };
566
+ }
567
+ await delay(2200);
568
+ }
569
+ else {
570
+ console.log('[Phase3Interact] reuse current detail page, skip goto');
571
+ }
572
+ // 2) 闂備浇顕х换鎺楀磻閻愯娲冀椤愶綆娼熼梺鐟邦嚟婵數鈧碍宀搁弻娑㈠即閵娿儲鐝梺鍝ュ枎閻楁捇寮诲☉銏犲唨妞ゆ劑鍨诲▓銈囩磽娴d粙鍝洪柣鐕傞檮缁岃鲸绻濋崶褏锛滃┑鐐村灦閻噣宕欐禒瀣拺闁革富鍙庨悞鐐亜閿旂偓鏆鐐村姇閻g兘宕堕敐鍛濠电偞鍨堕顏堫敂閸曟儼鈧寧銇勮箛鎾跺缂佲偓閸℃绠鹃柛鈩兠悘銉ッ归悪鈧崹鍫曞蓟濞戙垹绠f繝濠傛噸鐟曞棛绱撴笟鈧禍鑸电鐠鸿櫣鏆﹂柕澶嗘櫆閺呮繈鏌嶈閸撶喎鐣疯ぐ鎺濇晬婵犲﹤瀚弸鍌炴⒑缁洖澧叉繛鍙夌箘缁牏鈧綆鍏橀崑鎾绘偡閻楀牆鏆堥悷婊勬緲閸熸挳骞嗛崼婵愬悑闁告粈绀佸▓銊╂⒑闁偛鑻晶鎾煛?
573
+ if (!commentsAlreadyOpened) {
574
+ await ensureCommentsOpened(sessionId, unifiedApiUrl);
575
+ }
576
+ else {
577
+ console.log('[Phase3Interact] comments already opened, skip open click');
578
+ }
579
+ // 3) 濠电姷鏁告慨鎾晝閵夆晜鍤岄柣鎰靛墯閸欏繘鏌嶉崫鍕殲閻庢碍宀搁弻娑㈠即閵娿儲鐝梺鍝ュ枎閻楁捇寮?+ 缂傚倸鍊烽悞锔剧矙閹烘鍎庢い鏍仜閻?+ 闂傚倷鑳剁划顖炲礉濡ゅ懌鈧焦绻濋崟顓犵効?
580
+ while (likedCount < maxLikesPerRound && scrollCount < maxScrolls) {
581
+ scrollCount += 1;
582
+ const roundStartMs = Date.now();
583
+ let roundRuleHits = 0;
584
+ let roundGateBlocked = 0;
585
+ let roundDedupSkipped = 0;
586
+ let roundAlreadyLikedSkipped = 0;
587
+ let roundNotVisibleSkipped = 0;
588
+ let roundNestedSkipped = 0;
589
+ let roundClickFailed = 0;
590
+ let roundVerifyFailed = 0;
591
+ let roundNewLikes = 0;
592
+ let extracted = [];
593
+ try {
594
+ extracted = await extractVisibleComments(sessionId, unifiedApiUrl, 40);
595
+ }
596
+ catch (err) {
597
+ const errMsg = formatError(err);
598
+ const errorShot = await captureErrorEvidence('extract-comments-failed');
599
+ console.error(`[Phase3Interact] extractVisibleComments failed: ${errMsg}`);
600
+ return {
601
+ success: false,
602
+ noteId,
603
+ likedCount,
604
+ scannedCount,
605
+ likedComments,
606
+ commentsAdded: shouldHarvest ? harvestedAdded : undefined,
607
+ commentsTotal: shouldHarvest ? harvestedTotal : undefined,
608
+ commentsPath: shouldPersistHarvest ? harvestPath : undefined,
609
+ evidenceDir: likeEvidenceDir || '',
610
+ dedupSkipped: totalDedupSkipped,
611
+ alreadyLikedSkipped: totalAlreadyLikedSkipped,
612
+ reachedBottom,
613
+ error: `extract_visible_comments_failed: ${errMsg}`,
614
+ errorEvidence: { screenshot: errorShot },
615
+ };
616
+ }
617
+ scannedCount += extracted.length;
618
+ let roundHarvestedNew = 0;
619
+ if (shouldHarvest && extracted.length > 0) {
620
+ const rowsToAppend = [];
621
+ for (const row of extracted) {
622
+ const normalized = normalizeHarvestComment(noteId, row);
623
+ if (!normalized.content)
624
+ continue;
625
+ const key = `${normalized.userId}:${normalized.content}`;
626
+ if (harvestedKeySet.has(key))
627
+ continue;
628
+ harvestedKeySet.add(key);
629
+ harvestedAdded += 1;
630
+ harvestedTotal += 1;
631
+ roundHarvestedNew += 1;
632
+ if (shouldPersistHarvest)
633
+ rowsToAppend.push(normalized);
634
+ }
635
+ if (shouldPersistHarvest && rowsToAppend.length > 0) {
636
+ try {
637
+ await appendJsonlRows(harvestPath, rowsToAppend);
638
+ }
639
+ catch {
640
+ // ignore comment append errors to avoid blocking like flow
641
+ }
642
+ }
643
+ }
644
+ const candidates = [];
645
+ for (let i = 0; i < extracted.length; i++) {
646
+ const c = extracted[i] || {};
647
+ const text = String(c.text || '').trim();
648
+ if (!text)
649
+ continue;
650
+ const likeMatch = matchLikeText(text, compiledLikeRules);
651
+ if (!likeMatch.ok)
652
+ continue;
653
+ const domIndex = typeof c.domIndex === 'number' ? c.domIndex : i;
654
+ const isLeaf = c.isLeaf !== false;
655
+ candidates.push({ index: i, domIndex, text, likeMatch, row: c, isLeaf });
656
+ }
657
+ // 闂備礁婀遍…鍫ニ囬弶瑁も偓鎺楀醇閺囩偞顥濋梺瑙勵問閸犳鎮?DOM 濠碉紕鍋戦崐鏇㈠箹椤愩倛濮虫い鎾跺枎椤曡鲸淇婇姘儓閻㈩垬鍎甸弻锝夊箛椤曞懏鏁紓?
658
+ candidates.sort((a, b) => {
659
+ if (a.domIndex !== b.domIndex)
660
+ return b.domIndex - a.domIndex;
661
+ return b.index - a.index;
662
+ });
663
+ for (const candidate of candidates) {
664
+ if (likedCount >= maxLikesPerRound)
665
+ break;
666
+ const { index: i, domIndex, text, likeMatch, row: c, isLeaf } = candidate;
667
+ const visibleIndex = i;
668
+ roundRuleHits += 1;
669
+ console.log(`[Phase3Interact] 闂備礁鎲$粙鎺楀垂濠靛鍤堥柟瀵稿У閸犲棝鏌涢弴銊ヤ簻闁?note=${noteId} visibleRow=${visibleIndex} domRow=${domIndex >= 0 ? domIndex : 'na'} rule=${likeMatch.matchedRule || likeMatch.reason}`);
670
+ const hitMeta = {
671
+ noteId,
672
+ visibleIndex,
673
+ domIndex,
674
+ isLeaf,
675
+ userId: String(c.user_id || '').trim(),
676
+ userName: String(c.user_name || '').trim(),
677
+ text,
678
+ matchedRule: likeMatch.matchedRule || likeMatch.reason,
679
+ ts: new Date().toISOString(),
680
+ };
681
+ if (!isLeaf) {
682
+ roundNestedSkipped += 1;
683
+ console.log(`[Phase3Interact] 闂佽崵濮撮幖顐︽偪閸モ晜宕查柛鎰靛枟閸婇鐥鐐村櫧妞?note=${noteId} visibleRow=${visibleIndex} reason=nested_parent`);
684
+ continue;
685
+ }
686
+ let inViewport = true;
687
+ if (dryRun) {
688
+ // dry-run 闂備礁缍婂褏绮旇ぐ鎺撳仧妞ゆ梻鏅々? await highlightCommentRow(sessionId, visibleIndex, unifiedApiUrl, 'virtual-like-row').catch((): null => null);
689
+ const highlightRes = await highlightLikeButton(sessionId, visibleIndex, unifiedApiUrl);
690
+ inViewport = highlightRes?.inViewport === true;
691
+ await delay(450);
692
+ }
693
+ else {
694
+ inViewport = await isLikeButtonInViewport(sessionId, visibleIndex, unifiedApiUrl);
695
+ }
696
+ if (!inViewport) {
697
+ roundNotVisibleSkipped += 1;
698
+ console.log(`[Phase3Interact] 闂佽崵濮撮幖顐︽偪閸モ晜宕查柛鎰靛枟閸婇鐥鐐村櫧妞?note=${noteId} visibleRow=${visibleIndex} reason=not_in_viewport`);
699
+ continue;
700
+ }
701
+ // 缂備胶铏庨崣搴ㄥ窗閺囩姵宕叉慨妯块哺鐎氭岸鏌涢弴銊ユ珮闁哥喎鐗嗛—鍐Χ閸ャ劌娈屽┑鐘亾妞ゅ繐妫欓崰鍡涙煕閳╁喚娈旂紓宥嗘尭閳藉骞欓崘銊ョ睄闂佺瀛╅幐鎶藉极瀹ュ懐鏆嗛柛鏇ㄤ簽缁辨岸姊虹粙璺ㄧ缂佸鏁诲畷娲川閺夋垹鍊?闂備胶绮崝妤呭箠閹捐鍚规い鏇楀亾鐎规洘鍨肩粻娑㈠即?
702
+ const centered = await ensureCommentVisibleCentered(sessionId, unifiedApiUrl, visibleIndex);
703
+ if (!centered) {
704
+ roundNotVisibleSkipped += 1;
705
+ console.log(`[Phase3Interact] 闂佽崵濮撮幖顐︽偪閸モ晜宕查柛鎰靛枟閸婇鐥鐐村櫧妞?note=${noteId} visibleRow=${visibleIndex} reason=center_failed`);
706
+ continue;
707
+ }
708
+ if (dryRun) {
709
+ // dry-run 闂備礁缍婂褏绮旇ぐ鎺撳仧妞ゆ梻鏅々? await highlightCommentRow(sessionId, visibleIndex, unifiedApiUrl, 'virtual-like-row').catch((): null => null);
710
+ await highlightLikeButton(sessionId, visibleIndex, unifiedApiUrl).catch(() => null);
711
+ await delay(300);
712
+ }
713
+ const signature = {
714
+ userId: String(c.user_id || '').trim() || undefined,
715
+ userName: String(c.user_name || '').trim() || undefined,
716
+ text,
717
+ };
718
+ const sigKey = makeSignature(noteId, String(signature.userId || ''), String(signature.userName || ''), text);
719
+ if (likedSignatures.has(sigKey)) {
720
+ roundDedupSkipped += 1;
721
+ continue;
722
+ }
723
+ const beforeState = await getLikeStateForVisibleCommentIndex(sessionId, unifiedApiUrl, visibleIndex);
724
+ let beforeLiked = beforeState.iconState === 'liked';
725
+ if (!beforeLiked && beforeState.iconState === 'unknown') {
726
+ beforeLiked = await verifyLikedBySignature(sessionId, unifiedApiUrl, signature);
727
+ }
728
+ if (beforeLiked) {
729
+ roundAlreadyLikedSkipped += 1;
730
+ likedSignatures.add(sigKey);
731
+ if (!dryRun) {
732
+ saveLikedSignature(keyword, env, sigKey);
733
+ }
734
+ continue;
735
+ }
736
+ let beforePath = null;
737
+ let afterPath = null;
738
+ let beforeBase64 = null;
739
+ let afterBase64 = null;
740
+ let didClick = false;
741
+ if (!dryRun) {
742
+ // 闂佽崵濮村ú顓㈠绩闁秵鍎戝ù鐓庣摠閸婇鐥鐐村櫧妞も晝鏁婚幃瑙勬媴娓氼垳鍔搁悷婊呭閻擄繝寮澶婇唶闁靛鍨规禍楣冩煟閺傛寧鎯堥柤鐑樺▕濮婂宕熼鈧慨鍥煕閳哄偆娈滈柡浣哥У瀵板嫭绻濋崒娑㈡暘闂備線娼荤拹鐔煎礉婢舵劕鐒垫い鎺嶈兌閻﹦鈧鎸稿Λ娑欑閹间礁骞㈡俊顖濄€€閹稿啴鏌i悙瀵糕槈闁兼椿鍨堕幃鍧楀礋椤栨稈鎸冮梺鍛婁緱閸ㄧ増绂掗鐐村仯鐟滃繘宕戦悢鐓庣;闁挎繂顦粈鍕煟閹存梹鏉归柛瀣尭閳规垿宕堕…鎴炐濋梺鑽ゅ枑閻熻京绮婚幋锝冧汗?
743
+ const likePermit = await requestLikeGate(sessionId);
744
+ if (process.env.WEBAUTO_LIKE_GATE_BYPASS === '1') {
745
+ likePermit.allowed = true;
746
+ }
747
+ if (!likePermit.allowed) {
748
+ roundGateBlocked += 1;
749
+ console.log(`[Phase3Interact] 闂?闂備胶绮崝妤呫€佹繝鍕焿闁规壆澧楅悞濠氭煟閺傛寧鎯堥柤鐑樺▕濮婂宕熼鈧慨鍥煕閳哄偆娈滈柡?{likePermit.current}/${likePermit.limit}`);
750
+ await delay(1000);
751
+ continue;
752
+ }
753
+ beforeBase64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
754
+ const clickRes = await clickLikeButtonByIndex(sessionId, visibleIndex, unifiedApiUrl);
755
+ if (!clickRes?.success) {
756
+ roundClickFailed += 1;
757
+ continue;
758
+ }
759
+ didClick = true;
760
+ totalClickAttempts += 1;
761
+ await delay(650);
762
+ afterBase64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
763
+ }
764
+ else {
765
+ // dry-run: do not actually like; leave evidence only
766
+ await delay(450);
767
+ }
768
+ if (didClick) {
769
+ const hitMetaPath = await writeHitMeta('hit-idx-' + String(i).padStart(3, '0'), hitMeta);
770
+ if (hitMetaPath) {
771
+ console.log(`[Phase3Interact] hit meta saved: ${hitMetaPath}`);
772
+ }
773
+ if (beforeBase64) {
774
+ const dir = await ensureLikeEvidenceDir();
775
+ const hitName = `hit-idx-${String(i).padStart(3, '0')}-${Date.now()}.png`;
776
+ await savePngBase64(beforeBase64, path.join(dir, hitName));
777
+ const beforeName = `like-before-idx-${String(i).padStart(3, '0')}-${Date.now()}.png`;
778
+ beforePath = await savePngBase64(beforeBase64, path.join(dir, beforeName));
779
+ }
780
+ if (afterBase64) {
781
+ const dir = await ensureLikeEvidenceDir();
782
+ const afterName = `like-after-idx-${String(i).padStart(3, '0')}-${Date.now()}.png`;
783
+ afterPath = await savePngBase64(afterBase64, path.join(dir, afterName));
784
+ }
785
+ }
786
+ if (!dryRun) {
787
+ const afterState = await getLikeStateForVisibleCommentIndex(sessionId, unifiedApiUrl, visibleIndex);
788
+ const nowLiked = afterState.iconState === 'liked' || (await verifyLikedBySignature(sessionId, unifiedApiUrl, signature));
789
+ if (!nowLiked) {
790
+ roundVerifyFailed += 1;
791
+ continue;
792
+ }
793
+ }
794
+ likedCount += 1;
795
+ roundNewLikes += 1;
796
+ likedSignatures.add(sigKey);
797
+ if (!dryRun) {
798
+ saveLikedSignature(keyword, env, sigKey);
799
+ }
800
+ likedComments.push({
801
+ index: i,
802
+ userId: String(signature.userId || ''),
803
+ userName: String(signature.userName || ''),
804
+ content: String(text || ''),
805
+ timestamp: String(c.timestamp || ''),
806
+ screenshots: { before: beforePath, after: afterPath },
807
+ matchedRule: likeMatch.matchedRule,
808
+ });
809
+ // 闂備胶绮崝妤呫€佹繝鍕焿闁规壆澧楅埛鎺楁煙缂併垹鏋熸い?
810
+ await delay(900);
811
+ }
812
+ totalDedupSkipped += roundDedupSkipped;
813
+ totalAlreadyLikedSkipped += roundAlreadyLikedSkipped;
814
+ totalRuleHits += roundRuleHits;
815
+ totalNotVisibleSkipped += roundNotVisibleSkipped;
816
+ totalNestedSkipped += roundNestedSkipped;
817
+ totalGateBlocked += roundGateBlocked;
818
+ totalClickFailed += roundClickFailed;
819
+ totalVerifyFailed += roundVerifyFailed;
820
+ const roundLikedTotal = roundNewLikes + roundDedupSkipped + roundAlreadyLikedSkipped;
821
+ const roundSkippedTotal = roundNotVisibleSkipped + roundNestedSkipped + roundGateBlocked + roundClickFailed + roundVerifyFailed;
822
+ const roundOutcomeTotal = roundLikedTotal + roundSkippedTotal;
823
+ const roundHitOk = roundOutcomeTotal === roundRuleHits;
824
+ if (!roundHitOk) {
825
+ console.warn(`[Phase3Interact] hit-check mismatch round=${scrollCount} hits=${roundRuleHits} outcomes=${roundOutcomeTotal} liked=${roundLikedTotal} skipped=${roundSkippedTotal}`);
826
+ }
827
+ // 闂備浇顕х换鎰崲閹寸姵宕查柛鈩冪⊕閸庡﹥銇勯弽銊х焼婵炲矈浜弻锟犲炊閳轰椒鎴风紒鐐劤椤兘寮婚敓鐘茬倞闁靛濡囩粙鍥ь渻閵堝繒鐣虫繛澶嬫礋楠炲繘鎮╃拠鑼槹濡炪倖娲栭幊搴ㄥ疾閵夆晜鈷戦柟鑲╁仜閸斻倝鏌涚€n偆娲撮挊婵嬫煛鐏炶鍔滈柡鍜佸墰閳ь剙鍘滈崑鎾绘煕閺囥劋绨界紒杈ㄥ哺濮婅櫣绮欑捄銊ь唶缂備礁顦遍ˉ鎰板Φ閹邦垼妲炬繛瀵稿缁犳挸顕i幘顔藉€烽柣銏㈩暜缁卞崬鈹戦悙鏉戠仸闁瑰憡鎸冲畷鎴﹀箻缂佹ê鈧爼鏌i幇顓炵祷闁逞屽墯閹倿銆佸鑸电劶鐎广儱妫楀▓婊堟⒑閸濆嫷妲归柛銊ョ埣瀹曠敻骞掗幋鏃€顫嶉梺鐟扮仢閸燁偄顕i娴庡綊鎮╅崘鎻掝潓濡炪値鍘奸悿鍥╃不濞戞埃鍋撻敍鍗炲暕婢? await expandMoreComments(sessionId, unifiedApiUrl);
828
+ await delay(350);
829
+ // 闂備礁婀遍崢褔鎮洪妸銉冩椽鎮㈤悡搴o紵闂佸搫顦伴崵锕€鈽夐姀鐘靛姦濡炪倖宸婚崑鎾绘婢舵劖鍊甸柨婵嗘噹椤e磭绱?
830
+ // - 婵犵數鍋炲娆撳触鐎n喗鏅梻浣告啞钃辩紒瀣浮楠炲繘宕ㄩ娑樼/闂侀潧顭梽鍕枔閵忋倖鈷戦柛婵嗗椤忊晜绻涚€电鍘撮柛?end marker / 缂傚倸鍊风粈渚€鎯岄崒婊呯=婵鍩栭崕濠囧箹鏉堝墽绋诲┑顖氥偢閺岋綁骞嬮悜鍡欏姺闂佹悶鍔岄妶鎼佸箖?
831
+ // - 闂備浇顕у锕傦綖婢跺苯鏋堢€广儱鎷戦懓鍧楁煃閳轰礁鏆炲┑顖涙尦閹綊骞侀幒鎴濐瀴闂佸搫妫楃换鎺楀焵椤掆偓閻忔碍绔熺€n喖纾婚柟鎹愵嚙缁狙囨煃閸濆嫬鈧綊鍩涢幇顔剧<妞ゆ棁濮ょ亸锔锯偓瑙勬礃缁诲牊淇婇幖浣规櫆缂備焦菤閹稿嫭绻濋悽闈涗沪婵炲吋鐟╅、鏍箣閻樻剚娼熷┑鐘绘涧椤戝棝鎮炴總鍛婄厱妞ゎ厽鍨甸弸娑㈡偨椤栨稑鈻曢柡灞剧☉椤啰鎷犻幓鎺嗘嫟婵$偑鍊栫敮妤呭箰閸愯尙鏆﹂柣鎴f鎯熼梺鍐茬亪閺呮稒绂嶉悙顒傜瘈濠电姴鍊归崳铏圭磼閻樺磭鎳囬柟顔绢攰椤﹀綊鏌¢埀顒勫础閻戝棛鍔烽梺鍓茬厛閸嬪懏绂嶉妶鍡曠箚妞ゆ牗姘ㄦ禒銏ゆ煕濡櫣鎽犵紒缁樼箖缁绘繈宕橀鍡楀綆缂傚倷鐒﹁ぐ鍐╂櫠娴犲鐒垫い鎺嗗亾婵犫偓闁秴纾婚柕鍫濇媼閻庤埖銇勯弽銊с€掗柍缁樻⒒閳ь剙绠嶉崕閬嶅箠鎼淬劌鐤炬繛鍡樻尰閻撴洟鏌¢崘銊モ偓鎼佺€锋繝鐢靛仦濞兼瑥煤椤撱垹鍨傞柟顖嗏偓閺€浠嬫煕椤愩倕鏋旈柡鍡欏█濮婅櫣绱掑鍡欏姼闂佺硶鏅滈悧鐘诲春閳?
832
+ const basicEnd = await isCommentEnd(sessionId, unifiedApiUrl);
833
+ if (basicEnd) {
834
+ reachedBottom = true;
835
+ bottomReason = 'end_marker_or_empty';
836
+ }
837
+ else if (scrollCount % 10 === 0) {
838
+ const bf = await checkBottomWithBackAndForth(sessionId, unifiedApiUrl, 3).catch(() => ({ reachedBottom: false, reason: 'error' }));
839
+ reachedBottom = bf.reachedBottom;
840
+ bottomReason = bf.reason;
841
+ }
842
+ if (reachedBottom) {
843
+ const roundMs = Date.now() - roundStartMs;
844
+ console.log(`[Phase3Interact] round=${scrollCount} visible=${extracted.length} harvestedNew=${roundHarvestedNew} harvestedTotal=${harvestedTotal} ruleHits=${roundRuleHits} gateBlocked=${roundGateBlocked} dedup=${roundDedupSkipped} alreadyLiked=${roundAlreadyLikedSkipped} notVisible=${roundNotVisibleSkipped} nestedParent=${roundNestedSkipped} clickFailed=${roundClickFailed} verifyFailed=${roundVerifyFailed} newLikes=${roundNewLikes} likedTotal=${likedCount}/${maxLikesPerRound} end=${bottomReason} ms=${roundMs}`);
845
+ try {
846
+ onRound?.({
847
+ round: scrollCount,
848
+ visible: extracted.length,
849
+ harvestedNew: roundHarvestedNew,
850
+ harvestedTotal,
851
+ ruleHits: roundRuleHits,
852
+ hitTotal: roundRuleHits,
853
+ skippedTotal: roundSkippedTotal,
854
+ likedTotalActual: roundLikedTotal,
855
+ hitCheckOk: roundHitOk,
856
+ gateBlocked: roundGateBlocked,
857
+ dedupSkipped: roundDedupSkipped,
858
+ alreadyLikedSkipped: roundAlreadyLikedSkipped,
859
+ notVisibleSkipped: roundNotVisibleSkipped,
860
+ nestedParentSkipped: roundNestedSkipped,
861
+ clickFailed: roundClickFailed,
862
+ verifyFailed: roundVerifyFailed,
863
+ newLikes: roundNewLikes,
864
+ likedTotal: likedCount,
865
+ reachedBottom: true,
866
+ endReason: bottomReason,
867
+ ms: roundMs,
868
+ });
869
+ }
870
+ catch {
871
+ // ignore onRound callback failures
872
+ }
873
+ console.log(`[Phase3Interact] reachedBottom=true reason=${bottomReason}`);
874
+ break;
875
+ }
876
+ // 缂傚倸鍊风欢锟犲垂闂堟稓鏆﹂柣銏ゆ涧閸ㄦ繈鏌ц箛鎾磋础婵☆偒鍨抽幉鍛婃償閿濆懎鐏婇梺鍦檸閸犳牜绮堟径瀣ㄤ簻妞ゆ挾鍠庣粭褏绱掗埀顒勫礋椤栨稈鎷哄銈嗗坊閸嬫捇鏌h箛鏃傜疄鐎规洏鍨芥俊鍫曞幢濡ゅ啰鐛柣鐔哥矋閸ㄧ敻鍩㈤弮鍫濆嵆闁绘梻顭堥崝鍛存⒑閹稿孩顥嗛柕鍡忓亾闂佺顑嗛幐濠氬箯閸涱噮妲归幖杈剧稻閸g鈹戦悜鍥╁埌婵炶濡囬幑銏ゅ幢濞戞瑥鍓ㄦ繝銏e煐閸旀洟骞戦崼鏇熺厪濠电偛鐏濇俊鎸庝繆閸欏鐏撮柟顔款潐閹峰懘宕ㄦ繝鍐ㄥ壍闂佽绻愬ù姘跺垂鐠鸿櫣鏆︽繛宸簼閸嬪嫰鏌涢幘鑼跺厡闁瑰樊浜滈埞鎴︽偐鐠囇勬暰闂佺厧婀遍崑鎾诲箞閵娾晛鐓涢柛娑卞枟濞?
877
+ await scrollComments(sessionId, unifiedApiUrl, 650);
878
+ await delay(900);
879
+ const roundMs = Date.now() - roundStartMs;
880
+ console.log(`[Phase3Interact] round=${scrollCount} visible=${extracted.length} harvestedNew=${roundHarvestedNew} harvestedTotal=${harvestedTotal} ruleHits=${roundRuleHits} gateBlocked=${roundGateBlocked} dedup=${roundDedupSkipped} alreadyLiked=${roundAlreadyLikedSkipped} notVisible=${roundNotVisibleSkipped} nestedParent=${roundNestedSkipped} clickFailed=${roundClickFailed} verifyFailed=${roundVerifyFailed} newLikes=${roundNewLikes} likedTotal=${likedCount}/${maxLikesPerRound} end=no ms=${roundMs}`);
881
+ try {
882
+ onRound?.({
883
+ round: scrollCount,
884
+ visible: extracted.length,
885
+ harvestedNew: roundHarvestedNew,
886
+ harvestedTotal,
887
+ ruleHits: roundRuleHits,
888
+ hitTotal: roundRuleHits,
889
+ skippedTotal: roundSkippedTotal,
890
+ likedTotalActual: roundLikedTotal,
891
+ hitCheckOk: roundHitOk,
892
+ gateBlocked: roundGateBlocked,
893
+ dedupSkipped: roundDedupSkipped,
894
+ alreadyLikedSkipped: roundAlreadyLikedSkipped,
895
+ notVisibleSkipped: roundNotVisibleSkipped,
896
+ nestedParentSkipped: roundNestedSkipped,
897
+ clickFailed: roundClickFailed,
898
+ verifyFailed: roundVerifyFailed,
899
+ newLikes: roundNewLikes,
900
+ likedTotal: likedCount,
901
+ reachedBottom: false,
902
+ ms: roundMs,
903
+ });
904
+ }
905
+ catch {
906
+ // ignore onRound callback failures
907
+ }
908
+ }
909
+ // 闂傚倷娴囪闁稿鎹囬弻锝夋晲閸涱喗鎷辩紒鎯у⒔椤牓鍩ユ径鎰妞ゆ牗鐭竟鏇炩攽閻愬樊鍤熷┑顔惧亾閹便劑濡舵径濠勭枀閻庤娲栧ú锕傚疮閸濆嫨鈧帒顫濋濠傚缂備讲鍋撳璺侯焾閳ь剚甯掗~婵嬵敄閸欍儳閽电紓鍌欑贰閻撳牓宕滃顒夊殫闁告洦鍋掗崥瀣煕閵夛絽濡块柨娑欙耿濮婃椽骞愭惔锝傛闂佺粯顨呴幊鎰垝椤撶喎绶為幖瀛樼◥濮规姊洪崨濠庢畼闁稿鍋ら獮鍡椻枎韫囧﹥顫嶉梺瑙勫劤婢у海鏁☉姘辩<濞撴艾锕ら々顒傜磼椤旂晫鎳呴柍褜鍓ㄧ徊鑺ユ櫠鎼达絿鐭?
910
+ const likedTotal = likedCount + totalDedupSkipped + totalAlreadyLikedSkipped;
911
+ const skippedTotal = totalNotVisibleSkipped + totalNestedSkipped + totalGateBlocked + totalClickFailed + totalVerifyFailed;
912
+ const outcomeTotal = likedTotal + skippedTotal;
913
+ const hitCheckOk = outcomeTotal === totalRuleHits;
914
+ console.log(`[Phase3Interact] hit-check summary: hits=${totalRuleHits} liked=${likedTotal} skipped=${skippedTotal} ok=${hitCheckOk}`);
915
+ if (totalClickAttempts > 0) {
916
+ try {
917
+ const dir = await ensureLikeEvidenceDir();
918
+ await fsp.writeFile(path.join(dir, `summary-${Date.now()}.json`), JSON.stringify({
919
+ noteId,
920
+ safeUrl,
921
+ likeKeywords,
922
+ likedCount,
923
+ hitCount: totalRuleHits,
924
+ likedTotal,
925
+ skippedTotal,
926
+ hitCheckOk,
927
+ skippedBreakdown: {
928
+ notVisible: totalNotVisibleSkipped,
929
+ nestedParent: totalNestedSkipped,
930
+ gateBlocked: totalGateBlocked,
931
+ clickFailed: totalClickFailed,
932
+ verifyFailed: totalVerifyFailed,
933
+ },
934
+ likedBreakdown: {
935
+ newLikes: likedCount,
936
+ alreadyLiked: totalAlreadyLikedSkipped,
937
+ dedup: totalDedupSkipped,
938
+ },
939
+ clickAttempts: totalClickAttempts,
940
+ mismatchEvidence: {
941
+ postScreenshot: mismatchPostScreenshot,
942
+ },
943
+ reachedBottom,
944
+ likedComments,
945
+ ts: new Date().toISOString(),
946
+ }, null, 2), 'utf8');
947
+ }
948
+ catch {
949
+ // ignore
950
+ }
951
+ }
952
+ if (!hitCheckOk && totalClickAttempts > 0) {
953
+ try {
954
+ const navRes = await gotoDetailWithRetry(sessionId, safeUrl, unifiedApiUrl);
955
+ if (navRes.ok) {
956
+ await delay(1800);
957
+ mismatchPostScreenshot = await captureLikeEvidence('hit-mismatch-post');
958
+ }
959
+ }
960
+ catch {
961
+ // ignore mismatch evidence failures
962
+ }
963
+ }
964
+ const strictHitCheck = process.env.WEBAUTO_PHASE3_HIT_ASSERT === '1';
965
+ if (strictHitCheck && !hitCheckOk) {
966
+ return {
967
+ success: false,
968
+ noteId,
969
+ likedCount,
970
+ scannedCount,
971
+ likedComments,
972
+ commentsAdded: shouldHarvest ? harvestedAdded : undefined,
973
+ commentsTotal: shouldHarvest ? harvestedTotal : undefined,
974
+ commentsPath: shouldPersistHarvest ? harvestPath : undefined,
975
+ evidenceDir: likeEvidenceDir || '',
976
+ dedupSkipped: totalDedupSkipped,
977
+ alreadyLikedSkipped: totalAlreadyLikedSkipped,
978
+ reachedBottom,
979
+ stopReason: reachedBottom ? bottomReason : undefined,
980
+ hitCount: totalRuleHits,
981
+ skippedCount: skippedTotal,
982
+ likedTotal,
983
+ hitCheckOk,
984
+ mismatchEvidence: { postScreenshot: mismatchPostScreenshot },
985
+ error: `hit_count_mismatch hits=${totalRuleHits} outcomes=${outcomeTotal}`,
986
+ };
987
+ }
988
+ return {
989
+ success: true,
990
+ noteId,
991
+ likedCount,
992
+ scannedCount,
993
+ hitCount: totalRuleHits,
994
+ skippedCount: skippedTotal,
995
+ likedTotal,
996
+ hitCheckOk,
997
+ mismatchEvidence: { postScreenshot: mismatchPostScreenshot },
998
+ likedComments,
999
+ commentsAdded: shouldHarvest ? harvestedAdded : undefined,
1000
+ commentsTotal: shouldHarvest ? harvestedTotal : undefined,
1001
+ commentsPath: shouldPersistHarvest ? harvestPath : undefined,
1002
+ evidenceDir: likeEvidenceDir || '',
1003
+ dedupSkipped: totalDedupSkipped,
1004
+ alreadyLikedSkipped: totalAlreadyLikedSkipped,
1005
+ reachedBottom,
1006
+ stopReason: reachedBottom ? bottomReason : undefined,
1007
+ };
1008
+ }
1009
+ //# sourceMappingURL=Phase3InteractBlock.js.map