@web-auto/webauto 0.1.17 → 0.1.19
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.
- package/README.md +122 -53
- package/apps/desktop-console/dist/main/index.mjs +229 -14
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +290 -21
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
|
@@ -1,691 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { normalizeArray } from '../../../container/runtime-core/utils.mjs';
|
|
3
|
-
import { callAPI } from '../../../utils/browser-service.mjs';
|
|
4
|
-
import {
|
|
5
|
-
extractEvaluateResultData,
|
|
6
|
-
extractScreenshotBase64,
|
|
7
|
-
runEvaluateScript,
|
|
8
|
-
} from './common.mjs';
|
|
9
|
-
import {
|
|
10
|
-
compileLikeRules,
|
|
11
|
-
matchLikeText,
|
|
12
|
-
normalizeText,
|
|
13
|
-
} from './like-rules.mjs';
|
|
14
|
-
import {
|
|
15
|
-
appendLikedSignature,
|
|
16
|
-
ensureDir,
|
|
17
|
-
loadLikedSignatures,
|
|
18
|
-
makeLikeSignature,
|
|
19
|
-
mergeCommentsJsonl,
|
|
20
|
-
resolveXhsOutputContext,
|
|
21
|
-
savePngBase64,
|
|
22
|
-
writeJsonFile,
|
|
23
|
-
} from './persistence.mjs';
|
|
24
|
-
|
|
25
|
-
function buildReadStateScript() {
|
|
26
|
-
return `(() => {
|
|
27
|
-
const state = window.__camoXhsState || {};
|
|
28
|
-
return {
|
|
29
|
-
keyword: state.keyword || null,
|
|
30
|
-
currentNoteId: state.currentNoteId || null,
|
|
31
|
-
lastCommentsHarvest: state.lastCommentsHarvest && typeof state.lastCommentsHarvest === 'object'
|
|
32
|
-
? state.lastCommentsHarvest
|
|
33
|
-
: null,
|
|
34
|
-
};
|
|
35
|
-
})()`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function buildCollectLikeTargetsScript() {
|
|
39
|
-
return `(() => {
|
|
40
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
41
|
-
const rows = [];
|
|
42
|
-
const findLikeControl = (item) => {
|
|
43
|
-
const selectors = [
|
|
44
|
-
'.like-wrapper',
|
|
45
|
-
'.comment-like',
|
|
46
|
-
'.interactions .like-wrapper',
|
|
47
|
-
'.interactions [class*="like"]',
|
|
48
|
-
'button[class*="like"]',
|
|
49
|
-
'[aria-label*="赞"]',
|
|
50
|
-
];
|
|
51
|
-
for (const selector of selectors) {
|
|
52
|
-
const node = item.querySelector(selector);
|
|
53
|
-
if (node instanceof HTMLElement) return node;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
};
|
|
57
|
-
const isAlreadyLiked = (node) => {
|
|
58
|
-
if (!node) return false;
|
|
59
|
-
const className = String(node.className || '').toLowerCase();
|
|
60
|
-
const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
|
|
61
|
-
const text = String(node.textContent || '');
|
|
62
|
-
const hasActiveClass = /(?:^|\\s)like-active(?:\\s|$)/.test(className);
|
|
63
|
-
return hasActiveClass
|
|
64
|
-
|| ariaPressed === 'true'
|
|
65
|
-
|| /已赞|取消赞/.test(text);
|
|
66
|
-
};
|
|
67
|
-
const readText = (item, selectors) => {
|
|
68
|
-
for (const selector of selectors) {
|
|
69
|
-
const node = item.querySelector(selector);
|
|
70
|
-
const text = String(node?.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
71
|
-
if (text) return text;
|
|
72
|
-
}
|
|
73
|
-
return '';
|
|
74
|
-
};
|
|
75
|
-
const sanitizeUserName = (raw, commentText = '') => {
|
|
76
|
-
const text = String(raw || '').replace(/\\s+/g, ' ').trim();
|
|
77
|
-
if (!text) return '';
|
|
78
|
-
if (commentText && text === commentText) return '';
|
|
79
|
-
if (text.length > 40) return '';
|
|
80
|
-
if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
|
|
81
|
-
return text;
|
|
82
|
-
};
|
|
83
|
-
const readUserName = (item, commentText = '') => {
|
|
84
|
-
const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
|
|
85
|
-
for (const attr of attrNames) {
|
|
86
|
-
const value = sanitizeUserName(item.getAttribute?.(attr), commentText);
|
|
87
|
-
if (value) return value;
|
|
88
|
-
}
|
|
89
|
-
const selectors = [
|
|
90
|
-
'.comment-user .name',
|
|
91
|
-
'.comment-user .username',
|
|
92
|
-
'.comment-user .user-name',
|
|
93
|
-
'.author .name',
|
|
94
|
-
'.author',
|
|
95
|
-
'.user-name',
|
|
96
|
-
'.username',
|
|
97
|
-
'.name',
|
|
98
|
-
'a[href*="/user/profile/"]',
|
|
99
|
-
'a[href*="/user/"]',
|
|
100
|
-
];
|
|
101
|
-
for (const selector of selectors) {
|
|
102
|
-
const node = item.querySelector(selector);
|
|
103
|
-
if (!node) continue;
|
|
104
|
-
const title = sanitizeUserName(node.getAttribute?.('title'), commentText);
|
|
105
|
-
if (title) return title;
|
|
106
|
-
const text = sanitizeUserName(node.textContent, commentText);
|
|
107
|
-
if (text) return text;
|
|
108
|
-
}
|
|
109
|
-
return '';
|
|
110
|
-
};
|
|
111
|
-
const readAttr = (item, attrNames) => {
|
|
112
|
-
for (const attr of attrNames) {
|
|
113
|
-
const value = String(item.getAttribute?.(attr) || '').trim();
|
|
114
|
-
if (value) return value;
|
|
115
|
-
}
|
|
116
|
-
return '';
|
|
117
|
-
};
|
|
118
|
-
const readUserId = (item) => {
|
|
119
|
-
const value = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
|
|
120
|
-
if (value) return value;
|
|
121
|
-
const anchor = item.querySelector('a[href*="/user/profile/"], a[href*="/user/"]');
|
|
122
|
-
const href = String(anchor?.getAttribute?.('href') || '').trim();
|
|
123
|
-
if (!href) return '';
|
|
124
|
-
const matched = href.match(/\\/user\\/(?:profile\\/)?([a-zA-Z0-9_-]+)/);
|
|
125
|
-
return matched && matched[1] ? matched[1] : '';
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const matchedSet = new Set(
|
|
129
|
-
Array.isArray(state.matchedComments)
|
|
130
|
-
? state.matchedComments.map((row) => Number(row?.index)).filter((index) => Number.isFinite(index))
|
|
131
|
-
: [],
|
|
132
|
-
);
|
|
133
|
-
const items = Array.from(document.querySelectorAll('.comment-item'));
|
|
134
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
135
|
-
const item = items[index];
|
|
136
|
-
const text = readText(item, ['.content', '.comment-content', 'p']);
|
|
137
|
-
if (!text) continue;
|
|
138
|
-
const userName = readUserName(item, text);
|
|
139
|
-
const userId = readUserId(item);
|
|
140
|
-
const timestamp = readText(item, ['.date', '.time', '.timestamp', '[class*="time"]']);
|
|
141
|
-
const likeControl = findLikeControl(item);
|
|
142
|
-
rows.push({
|
|
143
|
-
index,
|
|
144
|
-
text,
|
|
145
|
-
userName,
|
|
146
|
-
userId,
|
|
147
|
-
timestamp,
|
|
148
|
-
hasLikeControl: Boolean(likeControl),
|
|
149
|
-
alreadyLiked: isAlreadyLiked(likeControl),
|
|
150
|
-
matchedByState: matchedSet.has(index),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
noteId: state.currentNoteId || null,
|
|
155
|
-
matchedByStateCount: matchedSet.size,
|
|
156
|
-
reachedBottom: typeof state.lastCommentsHarvest?.reachedBottom === 'boolean'
|
|
157
|
-
? state.lastCommentsHarvest.reachedBottom
|
|
158
|
-
: false,
|
|
159
|
-
stopReason: String(state.lastCommentsHarvest?.exitReason || '').trim() || null,
|
|
160
|
-
rows,
|
|
161
|
-
};
|
|
162
|
-
})()`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false) {
|
|
166
|
-
return `(async () => {
|
|
167
|
-
const idx = Number(${JSON.stringify(index)});
|
|
168
|
-
const actionTrace = [];
|
|
169
|
-
const pushTrace = (payload) => {
|
|
170
|
-
actionTrace.push({
|
|
171
|
-
ts: new Date().toISOString(),
|
|
172
|
-
...payload,
|
|
173
|
-
});
|
|
174
|
-
};
|
|
175
|
-
const items = Array.from(document.querySelectorAll('.comment-item'));
|
|
176
|
-
const item = items[idx];
|
|
177
|
-
if (!item) return { clicked: false, reason: 'comment_item_not_found', index: idx, actionTrace };
|
|
178
|
-
const findLikeControl = (node) => {
|
|
179
|
-
const selectors = [
|
|
180
|
-
'.like-wrapper',
|
|
181
|
-
'.comment-like',
|
|
182
|
-
'.interactions .like-wrapper',
|
|
183
|
-
'.interactions [class*="like"]',
|
|
184
|
-
'button[class*="like"]',
|
|
185
|
-
'[aria-label*="赞"]',
|
|
186
|
-
];
|
|
187
|
-
for (const selector of selectors) {
|
|
188
|
-
const found = node.querySelector(selector);
|
|
189
|
-
if (found instanceof HTMLElement) return found;
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
};
|
|
193
|
-
const isAlreadyLiked = (node) => {
|
|
194
|
-
if (!node) return false;
|
|
195
|
-
const className = String(node.className || '').toLowerCase();
|
|
196
|
-
const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
|
|
197
|
-
const text = String(node.textContent || '');
|
|
198
|
-
const hasActiveClass = /(?:^|\\s)like-active(?:\\s|$)/.test(className);
|
|
199
|
-
return hasActiveClass
|
|
200
|
-
|| ariaPressed === 'true'
|
|
201
|
-
|| /已赞|取消赞/.test(text);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const likeControl = findLikeControl(item);
|
|
205
|
-
if (!likeControl) return { clicked: false, reason: 'like_control_not_found', index: idx, actionTrace };
|
|
206
|
-
const beforeLiked = isAlreadyLiked(likeControl);
|
|
207
|
-
if (beforeLiked && !${skipAlreadyCheck ? 'true' : 'false'}) {
|
|
208
|
-
return { clicked: false, alreadyLiked: true, reason: 'already_liked', index: idx, actionTrace };
|
|
209
|
-
}
|
|
210
|
-
pushTrace({ kind: 'scroll', stage: 'xhs_comment_like', index: idx, target: 'comment_item', via: 'scrollIntoView' });
|
|
211
|
-
item.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
212
|
-
pushTrace({ kind: 'scroll', stage: 'xhs_comment_like', index: idx, target: 'like_control', via: 'scrollIntoView' });
|
|
213
|
-
likeControl.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
214
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
215
|
-
if (${highlight ? 'true' : 'false'}) {
|
|
216
|
-
const prev = likeControl.style.outline;
|
|
217
|
-
likeControl.style.outline = '2px solid #00d6ff';
|
|
218
|
-
setTimeout(() => { likeControl.style.outline = prev; }, 450);
|
|
219
|
-
}
|
|
220
|
-
pushTrace({ kind: 'click', stage: 'xhs_comment_like', index: idx, target: 'like_control' });
|
|
221
|
-
likeControl.click();
|
|
222
|
-
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
223
|
-
return {
|
|
224
|
-
clicked: true,
|
|
225
|
-
alreadyLiked: beforeLiked,
|
|
226
|
-
likedAfter: isAlreadyLiked(likeControl),
|
|
227
|
-
reason: 'clicked',
|
|
228
|
-
index: idx,
|
|
229
|
-
actionTrace,
|
|
230
|
-
};
|
|
231
|
-
})()`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function captureScreenshotToFile({ profileId, filePath }) {
|
|
235
|
-
try {
|
|
236
|
-
const payload = await callAPI('screenshot', { profileId, fullPage: false });
|
|
237
|
-
const base64 = extractScreenshotBase64(payload);
|
|
238
|
-
if (!base64) return null;
|
|
239
|
-
return savePngBase64(filePath, base64);
|
|
240
|
-
} catch {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function emitProgress(context, payload = {}) {
|
|
246
|
-
const emit = context?.emitProgress;
|
|
247
|
-
if (typeof emit !== 'function') return;
|
|
248
|
-
emit(payload);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function emitActionTrace(context, actionTrace = [], extra = {}) {
|
|
252
|
-
if (!Array.isArray(actionTrace) || actionTrace.length === 0) return;
|
|
253
|
-
for (let i = 0; i < actionTrace.length; i += 1) {
|
|
254
|
-
const row = actionTrace[i];
|
|
255
|
-
if (!row || typeof row !== 'object') continue;
|
|
256
|
-
const kind = String(row.kind || row.action || '').trim().toLowerCase() || 'trace';
|
|
257
|
-
emitProgress(context, {
|
|
258
|
-
kind,
|
|
259
|
-
step: i + 1,
|
|
260
|
-
...extra,
|
|
261
|
-
...row,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export async function executeCommentLikeOperation({ profileId, params = {}, context = {} }) {
|
|
267
|
-
const maxLikes = Math.max(1, Number(params.maxLikes ?? params.maxLikesPerRound ?? 1) || 1);
|
|
268
|
-
const rawKeywords = normalizeArray(params.keywords || params.likeKeywords);
|
|
269
|
-
const rules = compileLikeRules(rawKeywords);
|
|
270
|
-
const highlight = params.highlight !== false;
|
|
271
|
-
const dryRun = params.dryRun === true;
|
|
272
|
-
const saveEvidence = params.saveEvidence !== false;
|
|
273
|
-
const persistLikeState = params.persistLikeState !== false;
|
|
274
|
-
const persistComments = params.persistComments === true || params.persistCollectedComments === true;
|
|
275
|
-
const fallbackPickOne = params.pickOneIfNoNew !== false;
|
|
276
|
-
|
|
277
|
-
const stateRaw = await runEvaluateScript({
|
|
278
|
-
profileId,
|
|
279
|
-
script: buildReadStateScript(),
|
|
280
|
-
highlight: false,
|
|
281
|
-
});
|
|
282
|
-
const state = extractEvaluateResultData(stateRaw) || {};
|
|
283
|
-
|
|
284
|
-
const collectedRaw = await runEvaluateScript({
|
|
285
|
-
profileId,
|
|
286
|
-
script: buildCollectLikeTargetsScript(),
|
|
287
|
-
highlight: false,
|
|
288
|
-
});
|
|
289
|
-
const collected = extractEvaluateResultData(collectedRaw) || {};
|
|
290
|
-
const rows = Array.isArray(collected.rows) ? collected.rows : [];
|
|
291
|
-
|
|
292
|
-
const output = resolveXhsOutputContext({
|
|
293
|
-
params,
|
|
294
|
-
state,
|
|
295
|
-
noteId: collected.noteId || state.currentNoteId || params.noteId,
|
|
296
|
-
});
|
|
297
|
-
const evidenceDir = dryRun ? output.virtualLikeEvidenceDir : output.likeEvidenceDir;
|
|
298
|
-
if (saveEvidence) {
|
|
299
|
-
await ensureDir(evidenceDir);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const likedSignatures = persistLikeState ? await loadLikedSignatures(output.likeStatePath) : new Set();
|
|
303
|
-
const likedComments = [];
|
|
304
|
-
const matchedByStateCount = Number(collected.matchedByStateCount || 0);
|
|
305
|
-
// If explicit like rules are provided, honor them instead of inheriting state matches.
|
|
306
|
-
const useStateMatches = matchedByStateCount > 0 && rules.length === 0;
|
|
307
|
-
|
|
308
|
-
let hitCount = 0;
|
|
309
|
-
let likedCount = 0;
|
|
310
|
-
let dedupSkipped = 0;
|
|
311
|
-
let alreadyLikedSkipped = 0;
|
|
312
|
-
let missingLikeControl = 0;
|
|
313
|
-
let clickFailed = 0;
|
|
314
|
-
let verifyFailed = 0;
|
|
315
|
-
|
|
316
|
-
if (persistComments && rows.length > 0) {
|
|
317
|
-
await mergeCommentsJsonl({
|
|
318
|
-
filePath: output.commentsPath,
|
|
319
|
-
noteId: output.noteId,
|
|
320
|
-
comments: rows,
|
|
321
|
-
}).catch(() => null);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
for (const row of rows) {
|
|
325
|
-
if (likedCount >= maxLikes) break;
|
|
326
|
-
if (!row || typeof row !== 'object') continue;
|
|
327
|
-
const text = normalizeText(row.text);
|
|
328
|
-
if (!text) continue;
|
|
329
|
-
|
|
330
|
-
let match = null;
|
|
331
|
-
if (useStateMatches) {
|
|
332
|
-
if (!row.matchedByState) continue;
|
|
333
|
-
match = { ok: true, reason: 'state_match', matchedRule: 'state_match' };
|
|
334
|
-
} else {
|
|
335
|
-
match = matchLikeText(text, rules);
|
|
336
|
-
if (!match.ok) continue;
|
|
337
|
-
}
|
|
338
|
-
hitCount += 1;
|
|
339
|
-
|
|
340
|
-
const signature = makeLikeSignature({
|
|
341
|
-
noteId: output.noteId,
|
|
342
|
-
userId: String(row.userId || ''),
|
|
343
|
-
userName: String(row.userName || ''),
|
|
344
|
-
text,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (signature && likedSignatures.has(signature)) {
|
|
348
|
-
dedupSkipped += 1;
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!row.hasLikeControl) {
|
|
353
|
-
missingLikeControl += 1;
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (row.alreadyLiked) {
|
|
358
|
-
alreadyLikedSkipped += 1;
|
|
359
|
-
if (persistLikeState && signature) {
|
|
360
|
-
likedSignatures.add(signature);
|
|
361
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
362
|
-
noteId: output.noteId,
|
|
363
|
-
userId: String(row.userId || ''),
|
|
364
|
-
userName: String(row.userName || ''),
|
|
365
|
-
reason: 'already_liked',
|
|
366
|
-
}).catch(() => null);
|
|
367
|
-
}
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (dryRun) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const beforePath = saveEvidence
|
|
376
|
-
? await captureScreenshotToFile({
|
|
377
|
-
profileId,
|
|
378
|
-
filePath: path.join(evidenceDir, `like-before-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
|
|
379
|
-
})
|
|
380
|
-
: null;
|
|
381
|
-
|
|
382
|
-
const clickRaw = await runEvaluateScript({
|
|
383
|
-
profileId,
|
|
384
|
-
script: buildClickLikeByIndexScript(row.index, highlight),
|
|
385
|
-
highlight: false,
|
|
386
|
-
});
|
|
387
|
-
const clickResult = extractEvaluateResultData(clickRaw) || {};
|
|
388
|
-
const clickTrace = Array.isArray(clickResult.actionTrace) ? clickResult.actionTrace : [];
|
|
389
|
-
if (clickTrace.length > 0) {
|
|
390
|
-
emitActionTrace(context, clickTrace, {
|
|
391
|
-
stage: 'xhs_comment_like',
|
|
392
|
-
noteId: output.noteId,
|
|
393
|
-
commentIndex: Number(row.index),
|
|
394
|
-
fallback: false,
|
|
395
|
-
});
|
|
396
|
-
delete clickResult.actionTrace;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const afterPath = saveEvidence
|
|
400
|
-
? await captureScreenshotToFile({
|
|
401
|
-
profileId,
|
|
402
|
-
filePath: path.join(evidenceDir, `like-after-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
|
|
403
|
-
})
|
|
404
|
-
: null;
|
|
405
|
-
|
|
406
|
-
if (clickResult.alreadyLiked) {
|
|
407
|
-
alreadyLikedSkipped += 1;
|
|
408
|
-
if (persistLikeState && signature) {
|
|
409
|
-
likedSignatures.add(signature);
|
|
410
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
411
|
-
noteId: output.noteId,
|
|
412
|
-
userId: String(row.userId || ''),
|
|
413
|
-
userName: String(row.userName || ''),
|
|
414
|
-
reason: 'already_liked_after_click',
|
|
415
|
-
}).catch(() => null);
|
|
416
|
-
}
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!clickResult.clicked) {
|
|
421
|
-
clickFailed += 1;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (!clickResult.likedAfter) {
|
|
426
|
-
verifyFailed += 1;
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
likedCount += 1;
|
|
431
|
-
if (persistLikeState && signature) {
|
|
432
|
-
likedSignatures.add(signature);
|
|
433
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
434
|
-
noteId: output.noteId,
|
|
435
|
-
userId: String(row.userId || ''),
|
|
436
|
-
userName: String(row.userName || ''),
|
|
437
|
-
reason: 'liked',
|
|
438
|
-
}).catch(() => null);
|
|
439
|
-
}
|
|
440
|
-
likedComments.push({
|
|
441
|
-
index: Number(row.index),
|
|
442
|
-
userId: String(row.userId || ''),
|
|
443
|
-
userName: String(row.userName || ''),
|
|
444
|
-
content: text,
|
|
445
|
-
timestamp: String(row.timestamp || ''),
|
|
446
|
-
matchedRule: match.matchedRule || match.reason,
|
|
447
|
-
screenshots: {
|
|
448
|
-
before: beforePath,
|
|
449
|
-
after: afterPath,
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (!dryRun && fallbackPickOne && likedCount < maxLikes) {
|
|
455
|
-
for (const row of rows) {
|
|
456
|
-
if (likedCount >= maxLikes) break;
|
|
457
|
-
if (!row || typeof row !== 'object') continue;
|
|
458
|
-
const text = normalizeText(row.text);
|
|
459
|
-
if (!text) continue;
|
|
460
|
-
|
|
461
|
-
const signature = makeLikeSignature({
|
|
462
|
-
noteId: output.noteId,
|
|
463
|
-
userId: String(row.userId || ''),
|
|
464
|
-
userName: String(row.userName || ''),
|
|
465
|
-
text,
|
|
466
|
-
});
|
|
467
|
-
if (!row.hasLikeControl) {
|
|
468
|
-
missingLikeControl += 1;
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
hitCount += 1;
|
|
473
|
-
if (row.alreadyLiked) {
|
|
474
|
-
alreadyLikedSkipped += 1;
|
|
475
|
-
if (persistLikeState && signature) {
|
|
476
|
-
likedSignatures.add(signature);
|
|
477
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
478
|
-
noteId: output.noteId,
|
|
479
|
-
userId: String(row.userId || ''),
|
|
480
|
-
userName: String(row.userName || ''),
|
|
481
|
-
reason: 'already_liked_fallback',
|
|
482
|
-
}).catch(() => null);
|
|
483
|
-
}
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const beforePath = saveEvidence
|
|
488
|
-
? await captureScreenshotToFile({
|
|
489
|
-
profileId,
|
|
490
|
-
filePath: path.join(evidenceDir, `like-before-fallback-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
|
|
491
|
-
})
|
|
492
|
-
: null;
|
|
493
|
-
const clickRaw = await runEvaluateScript({
|
|
494
|
-
profileId,
|
|
495
|
-
script: buildClickLikeByIndexScript(row.index, highlight, true),
|
|
496
|
-
highlight: false,
|
|
497
|
-
});
|
|
498
|
-
const clickResult = extractEvaluateResultData(clickRaw) || {};
|
|
499
|
-
const clickTrace = Array.isArray(clickResult.actionTrace) ? clickResult.actionTrace : [];
|
|
500
|
-
if (clickTrace.length > 0) {
|
|
501
|
-
emitActionTrace(context, clickTrace, {
|
|
502
|
-
stage: 'xhs_comment_like',
|
|
503
|
-
noteId: output.noteId,
|
|
504
|
-
commentIndex: Number(row.index),
|
|
505
|
-
fallback: true,
|
|
506
|
-
});
|
|
507
|
-
delete clickResult.actionTrace;
|
|
508
|
-
}
|
|
509
|
-
const afterPath = saveEvidence
|
|
510
|
-
? await captureScreenshotToFile({
|
|
511
|
-
profileId,
|
|
512
|
-
filePath: path.join(evidenceDir, `like-after-fallback-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
|
|
513
|
-
})
|
|
514
|
-
: null;
|
|
515
|
-
|
|
516
|
-
if (clickResult.alreadyLiked) {
|
|
517
|
-
alreadyLikedSkipped += 1;
|
|
518
|
-
if (persistLikeState && signature) {
|
|
519
|
-
likedSignatures.add(signature);
|
|
520
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
521
|
-
noteId: output.noteId,
|
|
522
|
-
userId: String(row.userId || ''),
|
|
523
|
-
userName: String(row.userName || ''),
|
|
524
|
-
reason: 'already_liked_after_click_fallback',
|
|
525
|
-
}).catch(() => null);
|
|
526
|
-
}
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
if (!clickResult.clicked) {
|
|
530
|
-
clickFailed += 1;
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
if (!clickResult.likedAfter) {
|
|
534
|
-
if (clickResult.alreadyLiked) {
|
|
535
|
-
// Fallback proof path: toggle back to keep original state, but keep one verified candidate.
|
|
536
|
-
await runEvaluateScript({
|
|
537
|
-
profileId,
|
|
538
|
-
script: buildClickLikeByIndexScript(row.index, false, true),
|
|
539
|
-
highlight: false,
|
|
540
|
-
}).catch(() => null);
|
|
541
|
-
likedCount += 1;
|
|
542
|
-
likedComments.push({
|
|
543
|
-
index: Number(row.index),
|
|
544
|
-
userId: String(row.userId || ''),
|
|
545
|
-
userName: String(row.userName || ''),
|
|
546
|
-
content: text,
|
|
547
|
-
timestamp: String(row.timestamp || ''),
|
|
548
|
-
matchedRule: 'fallback_already_liked_verified',
|
|
549
|
-
screenshots: {
|
|
550
|
-
before: beforePath,
|
|
551
|
-
after: afterPath,
|
|
552
|
-
},
|
|
553
|
-
});
|
|
554
|
-
if (persistLikeState && signature) {
|
|
555
|
-
likedSignatures.add(signature);
|
|
556
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
557
|
-
noteId: output.noteId,
|
|
558
|
-
userId: String(row.userId || ''),
|
|
559
|
-
userName: String(row.userName || ''),
|
|
560
|
-
reason: 'already_liked_verified',
|
|
561
|
-
}).catch(() => null);
|
|
562
|
-
}
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
verifyFailed += 1;
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
likedCount += 1;
|
|
570
|
-
if (persistLikeState && signature) {
|
|
571
|
-
likedSignatures.add(signature);
|
|
572
|
-
await appendLikedSignature(output.likeStatePath, signature, {
|
|
573
|
-
noteId: output.noteId,
|
|
574
|
-
userId: String(row.userId || ''),
|
|
575
|
-
userName: String(row.userName || ''),
|
|
576
|
-
reason: 'liked_fallback',
|
|
577
|
-
}).catch(() => null);
|
|
578
|
-
}
|
|
579
|
-
likedComments.push({
|
|
580
|
-
index: Number(row.index),
|
|
581
|
-
userId: String(row.userId || ''),
|
|
582
|
-
userName: String(row.userName || ''),
|
|
583
|
-
content: text,
|
|
584
|
-
timestamp: String(row.timestamp || ''),
|
|
585
|
-
matchedRule: 'fallback_first_available',
|
|
586
|
-
screenshots: {
|
|
587
|
-
before: beforePath,
|
|
588
|
-
after: afterPath,
|
|
589
|
-
},
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const skippedCount = missingLikeControl + clickFailed + verifyFailed;
|
|
595
|
-
const likedTotal = likedCount + dedupSkipped + alreadyLikedSkipped;
|
|
596
|
-
const hitCheckOk = likedTotal + skippedCount === hitCount;
|
|
597
|
-
const summary = {
|
|
598
|
-
noteId: output.noteId,
|
|
599
|
-
keyword: output.keyword,
|
|
600
|
-
env: output.env,
|
|
601
|
-
likeKeywords: rawKeywords,
|
|
602
|
-
maxLikes,
|
|
603
|
-
scannedCount: rows.length,
|
|
604
|
-
hitCount,
|
|
605
|
-
likedCount,
|
|
606
|
-
skippedCount,
|
|
607
|
-
likedTotal,
|
|
608
|
-
hitCheckOk,
|
|
609
|
-
skippedBreakdown: {
|
|
610
|
-
missingLikeControl,
|
|
611
|
-
clickFailed,
|
|
612
|
-
verifyFailed,
|
|
613
|
-
},
|
|
614
|
-
likedBreakdown: {
|
|
615
|
-
newLikes: likedCount,
|
|
616
|
-
alreadyLiked: alreadyLikedSkipped,
|
|
617
|
-
dedup: dedupSkipped,
|
|
618
|
-
},
|
|
619
|
-
reachedBottom: collected.reachedBottom === true,
|
|
620
|
-
stopReason: String(collected.stopReason || '').trim() || null,
|
|
621
|
-
likedComments,
|
|
622
|
-
ts: new Date().toISOString(),
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
let summaryPath = null;
|
|
626
|
-
if (saveEvidence) {
|
|
627
|
-
summaryPath = await writeJsonFile(path.join(evidenceDir, `summary-${Date.now()}.json`), summary).catch(() => null);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
return {
|
|
631
|
-
ok: true,
|
|
632
|
-
code: 'OPERATION_DONE',
|
|
633
|
-
message: 'xhs_comment_like done',
|
|
634
|
-
data: {
|
|
635
|
-
noteId: output.noteId,
|
|
636
|
-
scannedCount: rows.length,
|
|
637
|
-
hitCount,
|
|
638
|
-
likedCount,
|
|
639
|
-
skippedCount,
|
|
640
|
-
likedTotal,
|
|
641
|
-
hitCheckOk,
|
|
642
|
-
dedupSkipped,
|
|
643
|
-
alreadyLikedSkipped,
|
|
644
|
-
missingLikeControl,
|
|
645
|
-
clickFailed,
|
|
646
|
-
verifyFailed,
|
|
647
|
-
likedComments,
|
|
648
|
-
commentsPath: persistComments ? output.commentsPath : null,
|
|
649
|
-
likeStatePath: persistLikeState ? output.likeStatePath : null,
|
|
650
|
-
evidenceDir: saveEvidence ? evidenceDir : null,
|
|
651
|
-
summaryPath,
|
|
652
|
-
reachedBottom: collected.reachedBottom === true,
|
|
653
|
-
stopReason: String(collected.stopReason || '').trim() || null,
|
|
654
|
-
},
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
export function buildCommentReplyScript(params = {}) {
|
|
659
|
-
const replyText = String(params.replyText || '').trim();
|
|
660
|
-
return `(async () => {
|
|
661
|
-
const state = window.__camoXhsState || (window.__camoXhsState = {});
|
|
662
|
-
const replyText = ${JSON.stringify(replyText)};
|
|
663
|
-
const matches = Array.isArray(state.matchedComments) ? state.matchedComments : [];
|
|
664
|
-
if (matches.length === 0) return { typed: false, reason: 'no_match' };
|
|
665
|
-
const index = Number(matches[0].index);
|
|
666
|
-
const nodes = Array.from(document.querySelectorAll('.comment-item'));
|
|
667
|
-
const target = nodes[index];
|
|
668
|
-
if (!target) return { typed: false, reason: 'match_not_visible', index };
|
|
669
|
-
target.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
670
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
671
|
-
target.click();
|
|
672
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
673
|
-
const input = document.querySelector('textarea, input[placeholder*="说点"], [contenteditable="true"]');
|
|
674
|
-
if (!input) return { typed: false, reason: 'reply_input_not_found', index };
|
|
675
|
-
if (input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement) {
|
|
676
|
-
input.focus();
|
|
677
|
-
input.value = replyText;
|
|
678
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
679
|
-
} else {
|
|
680
|
-
input.focus();
|
|
681
|
-
input.textContent = replyText;
|
|
682
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
683
|
-
}
|
|
684
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
685
|
-
const sendButton = Array.from(document.querySelectorAll('button'))
|
|
686
|
-
.find((button) => /发送|回复/.test(String(button.textContent || '').trim()));
|
|
687
|
-
if (sendButton) sendButton.click();
|
|
688
|
-
state.lastReply = { typed: true, index, at: new Date().toISOString() };
|
|
689
|
-
return state.lastReply;
|
|
690
|
-
})()`;
|
|
691
|
-
}
|