@web-auto/webauto 0.1.8 → 0.1.9
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/apps/desktop-console/dist/main/index.mjs +800 -89
- package/apps/desktop-console/dist/main/preload.mjs +3 -0
- package/apps/desktop-console/dist/renderer/index.html +9 -1
- package/apps/desktop-console/dist/renderer/index.js +784 -331
- package/apps/desktop-console/entry/ui-cli.mjs +23 -8
- package/apps/desktop-console/entry/ui-console.mjs +8 -3
- package/apps/webauto/entry/account.mjs +69 -8
- package/apps/webauto/entry/lib/account-detect.mjs +106 -25
- package/apps/webauto/entry/lib/account-store.mjs +121 -22
- package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
- package/apps/webauto/entry/profilepool.mjs +45 -3
- package/apps/webauto/entry/schedule.mjs +44 -2
- package/apps/webauto/entry/weibo-unified.mjs +2 -2
- package/apps/webauto/entry/xhs-install.mjs +220 -51
- package/apps/webauto/entry/xhs-unified.mjs +33 -6
- package/bin/webauto.mjs +80 -4
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/dist/services/unified-api/server.js +5 -0
- package/dist/services/unified-api/task-state.js +2 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
- package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
- package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
- package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
- package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/package.json +6 -3
- package/scripts/bump-version.mjs +120 -0
- package/services/unified-api/server.ts +4 -0
- package/services/unified-api/task-state.ts +5 -0
|
@@ -59,12 +59,10 @@ function buildCollectLikeTargetsScript() {
|
|
|
59
59
|
const className = String(node.className || '').toLowerCase();
|
|
60
60
|
const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
|
|
61
61
|
const text = String(node.textContent || '');
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
return className.includes('like-active')
|
|
62
|
+
const hasActiveClass = /(?:^|\\s)like-active(?:\\s|$)/.test(className);
|
|
63
|
+
return hasActiveClass
|
|
65
64
|
|| ariaPressed === 'true'
|
|
66
|
-
|| /已赞|取消赞/.test(text)
|
|
67
|
-
|| useHref.includes('liked');
|
|
65
|
+
|| /已赞|取消赞/.test(text);
|
|
68
66
|
};
|
|
69
67
|
const readText = (item, selectors) => {
|
|
70
68
|
for (const selector of selectors) {
|
|
@@ -164,7 +162,7 @@ function buildCollectLikeTargetsScript() {
|
|
|
164
162
|
})()`;
|
|
165
163
|
}
|
|
166
164
|
|
|
167
|
-
function buildClickLikeByIndexScript(index, highlight) {
|
|
165
|
+
function buildClickLikeByIndexScript(index, highlight, skipAlreadyCheck = false) {
|
|
168
166
|
return `(async () => {
|
|
169
167
|
const idx = Number(${JSON.stringify(index)});
|
|
170
168
|
const items = Array.from(document.querySelectorAll('.comment-item'));
|
|
@@ -190,18 +188,16 @@ function buildClickLikeByIndexScript(index, highlight) {
|
|
|
190
188
|
const className = String(node.className || '').toLowerCase();
|
|
191
189
|
const ariaPressed = String(node.getAttribute?.('aria-pressed') || '').toLowerCase();
|
|
192
190
|
const text = String(node.textContent || '');
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
return className.includes('like-active')
|
|
191
|
+
const hasActiveClass = /(?:^|\\s)like-active(?:\\s|$)/.test(className);
|
|
192
|
+
return hasActiveClass
|
|
196
193
|
|| ariaPressed === 'true'
|
|
197
|
-
|| /已赞|取消赞/.test(text)
|
|
198
|
-
|| useHref.includes('liked');
|
|
194
|
+
|| /已赞|取消赞/.test(text);
|
|
199
195
|
};
|
|
200
196
|
|
|
201
197
|
const likeControl = findLikeControl(item);
|
|
202
198
|
if (!likeControl) return { clicked: false, reason: 'like_control_not_found', index: idx };
|
|
203
199
|
const beforeLiked = isAlreadyLiked(likeControl);
|
|
204
|
-
if (beforeLiked) {
|
|
200
|
+
if (beforeLiked && !${skipAlreadyCheck ? 'true' : 'false'}) {
|
|
205
201
|
return { clicked: false, alreadyLiked: true, reason: 'already_liked', index: idx };
|
|
206
202
|
}
|
|
207
203
|
item.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
@@ -216,7 +212,7 @@ function buildClickLikeByIndexScript(index, highlight) {
|
|
|
216
212
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
217
213
|
return {
|
|
218
214
|
clicked: true,
|
|
219
|
-
alreadyLiked:
|
|
215
|
+
alreadyLiked: beforeLiked,
|
|
220
216
|
likedAfter: isAlreadyLiked(likeControl),
|
|
221
217
|
reason: 'clicked',
|
|
222
218
|
index: idx,
|
|
@@ -244,6 +240,7 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
|
|
|
244
240
|
const saveEvidence = params.saveEvidence !== false;
|
|
245
241
|
const persistLikeState = params.persistLikeState !== false;
|
|
246
242
|
const persistComments = params.persistComments === true || params.persistCollectedComments === true;
|
|
243
|
+
const fallbackPickOne = params.pickOneIfNoNew !== false;
|
|
247
244
|
|
|
248
245
|
const stateRaw = await runEvaluateScript({
|
|
249
246
|
profileId,
|
|
@@ -273,7 +270,8 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
|
|
|
273
270
|
const likedSignatures = persistLikeState ? await loadLikedSignatures(output.likeStatePath) : new Set();
|
|
274
271
|
const likedComments = [];
|
|
275
272
|
const matchedByStateCount = Number(collected.matchedByStateCount || 0);
|
|
276
|
-
|
|
273
|
+
// If explicit like rules are provided, honor them instead of inheriting state matches.
|
|
274
|
+
const useStateMatches = matchedByStateCount > 0 && rules.length === 0;
|
|
277
275
|
|
|
278
276
|
let hitCount = 0;
|
|
279
277
|
let likedCount = 0;
|
|
@@ -411,6 +409,136 @@ export async function executeCommentLikeOperation({ profileId, params = {} }) {
|
|
|
411
409
|
});
|
|
412
410
|
}
|
|
413
411
|
|
|
412
|
+
if (!dryRun && fallbackPickOne && likedCount < maxLikes) {
|
|
413
|
+
for (const row of rows) {
|
|
414
|
+
if (likedCount >= maxLikes) break;
|
|
415
|
+
if (!row || typeof row !== 'object') continue;
|
|
416
|
+
const text = normalizeText(row.text);
|
|
417
|
+
if (!text) continue;
|
|
418
|
+
|
|
419
|
+
const signature = makeLikeSignature({
|
|
420
|
+
noteId: output.noteId,
|
|
421
|
+
userId: String(row.userId || ''),
|
|
422
|
+
userName: String(row.userName || ''),
|
|
423
|
+
text,
|
|
424
|
+
});
|
|
425
|
+
if (!row.hasLikeControl) {
|
|
426
|
+
missingLikeControl += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
hitCount += 1;
|
|
431
|
+
if (row.alreadyLiked) {
|
|
432
|
+
alreadyLikedSkipped += 1;
|
|
433
|
+
if (persistLikeState && signature) {
|
|
434
|
+
likedSignatures.add(signature);
|
|
435
|
+
await appendLikedSignature(output.likeStatePath, signature, {
|
|
436
|
+
noteId: output.noteId,
|
|
437
|
+
userId: String(row.userId || ''),
|
|
438
|
+
userName: String(row.userName || ''),
|
|
439
|
+
reason: 'already_liked_fallback',
|
|
440
|
+
}).catch(() => null);
|
|
441
|
+
}
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const beforePath = saveEvidence
|
|
446
|
+
? await captureScreenshotToFile({
|
|
447
|
+
profileId,
|
|
448
|
+
filePath: path.join(evidenceDir, `like-before-fallback-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
|
|
449
|
+
})
|
|
450
|
+
: null;
|
|
451
|
+
const clickRaw = await runEvaluateScript({
|
|
452
|
+
profileId,
|
|
453
|
+
script: buildClickLikeByIndexScript(row.index, highlight, true),
|
|
454
|
+
highlight: false,
|
|
455
|
+
});
|
|
456
|
+
const clickResult = extractEvaluateResultData(clickRaw) || {};
|
|
457
|
+
const afterPath = saveEvidence
|
|
458
|
+
? await captureScreenshotToFile({
|
|
459
|
+
profileId,
|
|
460
|
+
filePath: path.join(evidenceDir, `like-after-fallback-idx-${String(row.index).padStart(3, '0')}-${Date.now()}.png`),
|
|
461
|
+
})
|
|
462
|
+
: null;
|
|
463
|
+
|
|
464
|
+
if (clickResult.alreadyLiked) {
|
|
465
|
+
alreadyLikedSkipped += 1;
|
|
466
|
+
if (persistLikeState && signature) {
|
|
467
|
+
likedSignatures.add(signature);
|
|
468
|
+
await appendLikedSignature(output.likeStatePath, signature, {
|
|
469
|
+
noteId: output.noteId,
|
|
470
|
+
userId: String(row.userId || ''),
|
|
471
|
+
userName: String(row.userName || ''),
|
|
472
|
+
reason: 'already_liked_after_click_fallback',
|
|
473
|
+
}).catch(() => null);
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (!clickResult.clicked) {
|
|
478
|
+
clickFailed += 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (!clickResult.likedAfter) {
|
|
482
|
+
if (clickResult.alreadyLiked) {
|
|
483
|
+
// Fallback proof path: toggle back to keep original state, but keep one verified candidate.
|
|
484
|
+
await runEvaluateScript({
|
|
485
|
+
profileId,
|
|
486
|
+
script: buildClickLikeByIndexScript(row.index, false, true),
|
|
487
|
+
highlight: false,
|
|
488
|
+
}).catch(() => null);
|
|
489
|
+
likedCount += 1;
|
|
490
|
+
likedComments.push({
|
|
491
|
+
index: Number(row.index),
|
|
492
|
+
userId: String(row.userId || ''),
|
|
493
|
+
userName: String(row.userName || ''),
|
|
494
|
+
content: text,
|
|
495
|
+
timestamp: String(row.timestamp || ''),
|
|
496
|
+
matchedRule: 'fallback_already_liked_verified',
|
|
497
|
+
screenshots: {
|
|
498
|
+
before: beforePath,
|
|
499
|
+
after: afterPath,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
if (persistLikeState && signature) {
|
|
503
|
+
likedSignatures.add(signature);
|
|
504
|
+
await appendLikedSignature(output.likeStatePath, signature, {
|
|
505
|
+
noteId: output.noteId,
|
|
506
|
+
userId: String(row.userId || ''),
|
|
507
|
+
userName: String(row.userName || ''),
|
|
508
|
+
reason: 'already_liked_verified',
|
|
509
|
+
}).catch(() => null);
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
verifyFailed += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
likedCount += 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: 'liked_fallback',
|
|
525
|
+
}).catch(() => null);
|
|
526
|
+
}
|
|
527
|
+
likedComments.push({
|
|
528
|
+
index: Number(row.index),
|
|
529
|
+
userId: String(row.userId || ''),
|
|
530
|
+
userName: String(row.userName || ''),
|
|
531
|
+
content: text,
|
|
532
|
+
timestamp: String(row.timestamp || ''),
|
|
533
|
+
matchedRule: 'fallback_first_available',
|
|
534
|
+
screenshots: {
|
|
535
|
+
before: beforePath,
|
|
536
|
+
after: afterPath,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
414
542
|
const skippedCount = missingLikeControl + clickFailed + verifyFailed;
|
|
415
543
|
const likedTotal = likedCount + dedupSkipped + alreadyLikedSkipped;
|
|
416
544
|
const hitCheckOk = likedTotal + skippedCount === hitCount;
|
|
@@ -213,6 +213,12 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
213
213
|
'.note-detail-mask .interaction-container',
|
|
214
214
|
'.note-detail-mask .comments-container',
|
|
215
215
|
];
|
|
216
|
+
const searchSelectors = [
|
|
217
|
+
'.note-item',
|
|
218
|
+
'.search-result-list',
|
|
219
|
+
'#search-input',
|
|
220
|
+
'.feeds-page',
|
|
221
|
+
];
|
|
216
222
|
const isVisible = (node) => {
|
|
217
223
|
if (!node || !(node instanceof HTMLElement)) return false;
|
|
218
224
|
const style = window.getComputedStyle(node);
|
|
@@ -221,7 +227,16 @@ export function buildOpenDetailScript(params = {}) {
|
|
|
221
227
|
const rect = node.getBoundingClientRect();
|
|
222
228
|
return rect.width > 1 && rect.height > 1;
|
|
223
229
|
};
|
|
224
|
-
const
|
|
230
|
+
const hasVisible = (selectors) => selectors.some((selector) => isVisible(document.querySelector(selector)));
|
|
231
|
+
const isDetailReady = () => {
|
|
232
|
+
const detailVisible = hasVisible(detailSelectors);
|
|
233
|
+
if (!detailVisible) return false;
|
|
234
|
+
const href = String(window.location.href || '');
|
|
235
|
+
const isDetailUrl = href.includes('/explore/') && !href.includes('/search_result');
|
|
236
|
+
if (isDetailUrl) return true;
|
|
237
|
+
const searchVisible = hasVisible(searchSelectors);
|
|
238
|
+
return !searchVisible;
|
|
239
|
+
};
|
|
225
240
|
|
|
226
241
|
next.cover.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
227
242
|
await new Promise((resolve) => setTimeout(resolve, 140));
|
|
@@ -253,6 +253,109 @@ async function readXhsRuntimeState(profileId) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
function buildAssertLoggedInScript(params = {}) {
|
|
257
|
+
const selectors = Array.isArray(params.loginSelectors) && params.loginSelectors.length > 0
|
|
258
|
+
? params.loginSelectors.map((item) => String(item || '').trim()).filter(Boolean)
|
|
259
|
+
: [
|
|
260
|
+
'.login-container',
|
|
261
|
+
'.login-dialog',
|
|
262
|
+
'#login-container',
|
|
263
|
+
];
|
|
264
|
+
const loginPattern = String(
|
|
265
|
+
params.loginPattern || '登录|扫码|验证码|手机号|请先登录|注册|sign\\s*in',
|
|
266
|
+
).trim();
|
|
267
|
+
|
|
268
|
+
return `(() => {
|
|
269
|
+
const guardSelectors = ${JSON.stringify(selectors)};
|
|
270
|
+
const loginPattern = new RegExp(${JSON.stringify(loginPattern || '登录|扫码|验证码|手机号|请先登录|注册|sign\\\\s*in')}, 'i');
|
|
271
|
+
|
|
272
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
273
|
+
const isVisible = (node) => {
|
|
274
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
275
|
+
const style = window.getComputedStyle(node);
|
|
276
|
+
if (!style) return false;
|
|
277
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
278
|
+
const rect = node.getBoundingClientRect();
|
|
279
|
+
return rect.width > 0 && rect.height > 0;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const guardNodes = guardSelectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
|
|
283
|
+
const visibleGuardNodes = guardNodes.filter((node) => isVisible(node));
|
|
284
|
+
const guardTexts = visibleGuardNodes
|
|
285
|
+
.slice(0, 10)
|
|
286
|
+
.map((node) => normalize(node.textContent || ''))
|
|
287
|
+
.filter(Boolean);
|
|
288
|
+
const mergedGuardText = guardTexts.join(' ');
|
|
289
|
+
const hasLoginText = loginPattern.test(mergedGuardText);
|
|
290
|
+
const loginUrl = /\\/login|signin|passport|account\\/login/i.test(String(location.href || ''));
|
|
291
|
+
|
|
292
|
+
let accountId = '';
|
|
293
|
+
try {
|
|
294
|
+
const initialState = (typeof window !== 'undefined' && window.__INITIAL_STATE__) || null;
|
|
295
|
+
const rawUserInfo = initialState && initialState.user && initialState.user.userInfo
|
|
296
|
+
? (
|
|
297
|
+
(initialState.user.userInfo._rawValue && typeof initialState.user.userInfo._rawValue === 'object' && initialState.user.userInfo._rawValue)
|
|
298
|
+
|| (initialState.user.userInfo._value && typeof initialState.user.userInfo._value === 'object' && initialState.user.userInfo._value)
|
|
299
|
+
|| (typeof initialState.user.userInfo === 'object' ? initialState.user.userInfo : null)
|
|
300
|
+
)
|
|
301
|
+
: null;
|
|
302
|
+
accountId = normalize(rawUserInfo?.user_id || rawUserInfo?.userId || '');
|
|
303
|
+
} catch {}
|
|
304
|
+
|
|
305
|
+
if (!accountId) {
|
|
306
|
+
const selfAnchor = Array.from(document.querySelectorAll('a[href*="/user/profile/"]'))
|
|
307
|
+
.find((node) => {
|
|
308
|
+
const text = normalize(node.textContent || '');
|
|
309
|
+
const title = normalize(node.getAttribute('title') || '');
|
|
310
|
+
const aria = normalize(node.getAttribute('aria-label') || '');
|
|
311
|
+
return ['我', '我的', '个人主页', '我的主页'].includes(text)
|
|
312
|
+
|| ['我', '我的', '个人主页', '我的主页'].includes(title)
|
|
313
|
+
|| ['我', '我的', '个人主页', '我的主页'].includes(aria);
|
|
314
|
+
});
|
|
315
|
+
if (selfAnchor) {
|
|
316
|
+
const href = normalize(selfAnchor.getAttribute('href') || '');
|
|
317
|
+
const matched = href.match(/\\/user\\/profile\\/([^/?#]+)/);
|
|
318
|
+
if (matched && matched[1]) accountId = normalize(matched[1]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const hasAccountSignal = Boolean(accountId);
|
|
323
|
+
const hasLoginGuard = (visibleGuardNodes.length > 0 && hasLoginText) || loginUrl;
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
hasLoginGuard,
|
|
327
|
+
hasAccountSignal,
|
|
328
|
+
accountId: accountId || null,
|
|
329
|
+
url: String(location.href || ''),
|
|
330
|
+
visibleGuardCount: visibleGuardNodes.length,
|
|
331
|
+
guardTextPreview: mergedGuardText.slice(0, 240),
|
|
332
|
+
loginUrl,
|
|
333
|
+
hasLoginText,
|
|
334
|
+
guardSelectors,
|
|
335
|
+
};
|
|
336
|
+
})()`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function executeAssertLoggedInOperation({ profileId, params = {} }) {
|
|
340
|
+
const highlight = params.highlight !== false;
|
|
341
|
+
const payload = await runEvaluateScript({
|
|
342
|
+
profileId,
|
|
343
|
+
script: buildAssertLoggedInScript(params),
|
|
344
|
+
highlight,
|
|
345
|
+
});
|
|
346
|
+
const data = extractEvaluateResultData(payload) || {};
|
|
347
|
+
if (data?.hasLoginGuard === true) {
|
|
348
|
+
const code = String(params.code || 'LOGIN_GUARD_DETECTED').trim() || 'LOGIN_GUARD_DETECTED';
|
|
349
|
+
return asErrorPayload('OPERATION_FAILED', code, { guard: data });
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
ok: true,
|
|
353
|
+
code: 'OPERATION_DONE',
|
|
354
|
+
message: 'xhs_assert_logged_in done',
|
|
355
|
+
data,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
256
359
|
async function handleRaiseError({ params }) {
|
|
257
360
|
const code = String(params.code || params.message || 'AUTOSCRIPT_ABORT').trim();
|
|
258
361
|
return asErrorPayload('OPERATION_FAILED', code || 'AUTOSCRIPT_ABORT');
|
|
@@ -314,6 +417,7 @@ async function executeCommentsHarvestOperation({ profileId, params = {} }) {
|
|
|
314
417
|
|
|
315
418
|
const XHS_ACTION_HANDLERS = {
|
|
316
419
|
raise_error: handleRaiseError,
|
|
420
|
+
xhs_assert_logged_in: executeAssertLoggedInOperation,
|
|
317
421
|
xhs_submit_search: executeSubmitSearchOperation,
|
|
318
422
|
xhs_open_detail: executeOpenDetailOperation,
|
|
319
423
|
xhs_detail_harvest: createEvaluateHandler('xhs_detail_harvest done', buildDetailHarvestScript),
|
|
@@ -340,6 +340,7 @@ export class AutoscriptRunner {
|
|
|
340
340
|
'sync_window_viewport',
|
|
341
341
|
'verify_subscriptions',
|
|
342
342
|
'xhs_submit_search',
|
|
343
|
+
'xhs_assert_logged_in',
|
|
343
344
|
'xhs_open_detail',
|
|
344
345
|
'xhs_detail_harvest',
|
|
345
346
|
'xhs_expand_replies',
|
|
@@ -359,10 +360,19 @@ export class AutoscriptRunner {
|
|
|
359
360
|
|
|
360
361
|
resolveTimeoutMs(operation) {
|
|
361
362
|
const pacing = this.resolvePacing(operation);
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
const operationDisableTimeout = operation?.disableTimeout;
|
|
364
|
+
if (operationDisableTimeout === true) return 0;
|
|
365
|
+
|
|
366
|
+
const rawOperationTimeout = operation?.timeoutMs ?? operation?.pacing?.timeoutMs;
|
|
367
|
+
const hasOperationTimeout = Number.isFinite(Number(rawOperationTimeout))
|
|
368
|
+
&& Number(rawOperationTimeout) > 0;
|
|
369
|
+
const defaultDisableTimeout = Boolean(this.script?.defaults?.disableTimeout);
|
|
370
|
+
|
|
371
|
+
// Keep default "no-timeout" mode, but allow operation-level timeout to opt in.
|
|
372
|
+
if (defaultDisableTimeout && operationDisableTimeout !== false && !hasOperationTimeout) {
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
366
376
|
if (pacing.timeoutMs === 0) return 0;
|
|
367
377
|
if (Number.isFinite(pacing.timeoutMs) && pacing.timeoutMs > 0) return pacing.timeoutMs;
|
|
368
378
|
return this.getDefaultTimeoutMs(operation);
|
|
@@ -126,10 +126,19 @@ function normalizeSubscription(item, index, defaults) {
|
|
|
126
126
|
const id = toTrimmedString(item.id) || `subscription_${index + 1}`;
|
|
127
127
|
const selector = toTrimmedString(item.selector);
|
|
128
128
|
if (!selector) return null;
|
|
129
|
+
const pageUrlIncludes = toArray(item.pageUrlIncludes || item.urlIncludes)
|
|
130
|
+
.map((value) => toTrimmedString(value))
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
const pageUrlExcludes = toArray(item.pageUrlExcludes || item.urlExcludes)
|
|
133
|
+
.map((value) => toTrimmedString(value))
|
|
134
|
+
.filter(Boolean);
|
|
129
135
|
const events = toArray(item.events).map((name) => toTrimmedString(name)).filter(Boolean);
|
|
130
136
|
return {
|
|
131
137
|
id,
|
|
132
138
|
selector,
|
|
139
|
+
visible: item.visible === false ? false : true,
|
|
140
|
+
pageUrlIncludes,
|
|
141
|
+
pageUrlExcludes,
|
|
133
142
|
events: events.length > 0 ? events : ['appear', 'exist', 'disappear', 'change'],
|
|
134
143
|
dependsOn: toArray(item.dependsOn).map((x) => toTrimmedString(x)).filter(Boolean),
|
|
135
144
|
retry: normalizeRetry(item.retry, defaults.retry),
|
|
@@ -575,7 +575,14 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
575
575
|
{ id: 'home_search_input', selector: '#search-input, input.search-input', events: ['appear', 'exist', 'disappear'] },
|
|
576
576
|
{ id: 'home_search_button', selector: '.input-button, .input-button .search-icon', events: ['exist'] },
|
|
577
577
|
{ id: 'search_result_item', selector: '.note-item', events: ['appear', 'exist', 'change'] },
|
|
578
|
-
{
|
|
578
|
+
{
|
|
579
|
+
id: 'detail_modal',
|
|
580
|
+
selector: 'body',
|
|
581
|
+
visible: false,
|
|
582
|
+
pageUrlIncludes: ['/explore/'],
|
|
583
|
+
pageUrlExcludes: ['/search_result'],
|
|
584
|
+
events: ['appear', 'exist', 'disappear'],
|
|
585
|
+
},
|
|
579
586
|
{ id: 'detail_comment_item', selector: '.comment-item, [class*="comment-item"]', events: ['appear', 'exist', 'change'] },
|
|
580
587
|
{ id: 'detail_show_more', selector: '.note-detail-mask .show-more, .note-detail-mask .reply-expand, .note-detail-mask [class*="expand"], .note-detail-page .show-more, .note-detail-page .reply-expand, .note-detail-page [class*="expand"]', events: ['appear', 'exist'] },
|
|
581
588
|
{ id: 'detail_discover_button', selector: 'a[href*="/explore?channel_id=homefeed_recommend"]', events: ['appear', 'exist'] },
|
|
@@ -884,7 +891,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
|
|
|
884
891
|
},
|
|
885
892
|
{
|
|
886
893
|
id: 'abort_on_login_guard',
|
|
887
|
-
action: '
|
|
894
|
+
action: 'xhs_assert_logged_in',
|
|
888
895
|
params: { code: 'LOGIN_GUARD_DETECTED' },
|
|
889
896
|
trigger: 'login_guard.appear',
|
|
890
897
|
once: false,
|
|
@@ -40,6 +40,105 @@ export const XHS_CHECKPOINTS = {
|
|
|
40
40
|
],
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
function extractEvaluateResult(payload) {
|
|
44
|
+
if (!payload || typeof payload !== 'object') return {};
|
|
45
|
+
if (payload.result && typeof payload.result === 'object') return payload.result;
|
|
46
|
+
if (payload.data && typeof payload.data === 'object') return payload.data;
|
|
47
|
+
return payload;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function detectXhsLoginSignal(profileId) {
|
|
51
|
+
const script = `(() => {
|
|
52
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
53
|
+
const selectors = ${JSON.stringify(XHS_CHECKPOINTS.login_guard)};
|
|
54
|
+
const isVisible = (node) => {
|
|
55
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
56
|
+
const style = window.getComputedStyle(node);
|
|
57
|
+
if (!style) return false;
|
|
58
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
59
|
+
const rect = node.getBoundingClientRect();
|
|
60
|
+
return rect.width > 0 && rect.height > 0;
|
|
61
|
+
};
|
|
62
|
+
const nodes = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
|
|
63
|
+
const visibleNodes = nodes.filter((node) => isVisible(node));
|
|
64
|
+
const guardText = visibleNodes
|
|
65
|
+
.slice(0, 10)
|
|
66
|
+
.map((node) => normalize(node.textContent || ''))
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(' ');
|
|
69
|
+
const hasLoginText = /登录|扫码|验证码|手机号|请先登录|注册|sign\\s*in/i.test(guardText);
|
|
70
|
+
const loginUrl = /\\/login|signin|passport|account\\/login/i.test(String(location.href || ''));
|
|
71
|
+
|
|
72
|
+
let accountId = '';
|
|
73
|
+
try {
|
|
74
|
+
const initialState = (typeof window !== 'undefined' && window.__INITIAL_STATE__) || null;
|
|
75
|
+
const rawUserInfo = initialState && initialState.user && initialState.user.userInfo
|
|
76
|
+
? (
|
|
77
|
+
(initialState.user.userInfo._rawValue && typeof initialState.user.userInfo._rawValue === 'object' && initialState.user.userInfo._rawValue)
|
|
78
|
+
|| (initialState.user.userInfo._value && typeof initialState.user.userInfo._value === 'object' && initialState.user.userInfo._value)
|
|
79
|
+
|| (typeof initialState.user.userInfo === 'object' ? initialState.user.userInfo : null)
|
|
80
|
+
)
|
|
81
|
+
: null;
|
|
82
|
+
accountId = normalize(rawUserInfo?.user_id || rawUserInfo?.userId || '');
|
|
83
|
+
} catch {}
|
|
84
|
+
|
|
85
|
+
if (!accountId) {
|
|
86
|
+
const selfAnchor = Array.from(document.querySelectorAll('a[href*="/user/profile/"]'))
|
|
87
|
+
.find((node) => {
|
|
88
|
+
const text = normalize(node.textContent || '');
|
|
89
|
+
const title = normalize(node.getAttribute('title') || '');
|
|
90
|
+
const aria = normalize(node.getAttribute('aria-label') || '');
|
|
91
|
+
return ['我', '我的', '个人主页', '我的主页'].includes(text)
|
|
92
|
+
|| ['我', '我的', '个人主页', '我的主页'].includes(title)
|
|
93
|
+
|| ['我', '我的', '个人主页', '我的主页'].includes(aria);
|
|
94
|
+
});
|
|
95
|
+
if (selfAnchor) {
|
|
96
|
+
const href = normalize(selfAnchor.getAttribute('href') || '');
|
|
97
|
+
const matched = href.match(/\\/user\\/profile\\/([^/?#]+)/);
|
|
98
|
+
if (matched && matched[1]) accountId = normalize(matched[1]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasAccountSignal = Boolean(accountId);
|
|
103
|
+
const hasLoginGuard = (visibleNodes.length > 0 && hasLoginText) || loginUrl;
|
|
104
|
+
return {
|
|
105
|
+
hasLoginGuard,
|
|
106
|
+
hasLoginText,
|
|
107
|
+
hasAccountSignal,
|
|
108
|
+
accountId: accountId || null,
|
|
109
|
+
visibleGuardCount: visibleNodes.length,
|
|
110
|
+
guardTextPreview: guardText.slice(0, 240),
|
|
111
|
+
loginUrl,
|
|
112
|
+
};
|
|
113
|
+
})()`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const ret = await callAPI('evaluate', { profileId, script });
|
|
117
|
+
const data = extractEvaluateResult(ret);
|
|
118
|
+
return {
|
|
119
|
+
hasLoginGuard: data?.hasLoginGuard === true,
|
|
120
|
+
hasLoginText: data?.hasLoginText === true,
|
|
121
|
+
hasAccountSignal: data?.hasAccountSignal === true,
|
|
122
|
+
accountId: String(data?.accountId || '').trim() || null,
|
|
123
|
+
visibleGuardCount: Math.max(0, Number(data?.visibleGuardCount || 0) || 0),
|
|
124
|
+
guardTextPreview: String(data?.guardTextPreview || '').trim(),
|
|
125
|
+
loginUrl: data?.loginUrl === true,
|
|
126
|
+
ok: true,
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return {
|
|
130
|
+
hasLoginGuard: false,
|
|
131
|
+
hasLoginText: false,
|
|
132
|
+
hasAccountSignal: false,
|
|
133
|
+
accountId: null,
|
|
134
|
+
visibleGuardCount: 0,
|
|
135
|
+
guardTextPreview: '',
|
|
136
|
+
loginUrl: false,
|
|
137
|
+
ok: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
43
142
|
export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' }) {
|
|
44
143
|
if (platform !== 'xiaohongshu') {
|
|
45
144
|
return asErrorPayload('UNSUPPORTED_PLATFORM', `Unsupported platform: ${platform}`);
|
|
@@ -72,10 +171,16 @@ export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' })
|
|
|
72
171
|
addCount('login_guard', XHS_CHECKPOINTS.login_guard);
|
|
73
172
|
addCount('risk_control', XHS_CHECKPOINTS.risk_control);
|
|
74
173
|
|
|
174
|
+
const loginSignal = await detectXhsLoginSignal(resolvedProfile);
|
|
175
|
+
const loginGuardBySelector = signals.some((item) => item.startsWith('login_guard:'));
|
|
176
|
+
const loginGuardConfirmed = loginSignal.ok
|
|
177
|
+
? loginSignal.hasLoginGuard
|
|
178
|
+
: loginGuardBySelector;
|
|
179
|
+
|
|
75
180
|
let checkpoint = 'unknown';
|
|
76
181
|
if (!url || !url.includes('xiaohongshu.com')) checkpoint = 'offsite';
|
|
77
182
|
else if (isCheckpointRiskUrl(url)) checkpoint = 'risk_control';
|
|
78
|
-
else if (
|
|
183
|
+
else if (loginGuardConfirmed) checkpoint = 'login_guard';
|
|
79
184
|
else if (signals.some((item) => item.startsWith('comments_ready:'))) checkpoint = 'comments_ready';
|
|
80
185
|
else if (signals.some((item) => item.startsWith('detail_ready:'))) checkpoint = 'detail_ready';
|
|
81
186
|
else if (signals.some((item) => item.startsWith('search_ready:'))) checkpoint = 'search_ready';
|
|
@@ -92,6 +197,7 @@ export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' })
|
|
|
92
197
|
url,
|
|
93
198
|
signals,
|
|
94
199
|
selectorHits: counter,
|
|
200
|
+
loginSignal,
|
|
95
201
|
},
|
|
96
202
|
};
|
|
97
203
|
} catch (err) {
|
|
@@ -18,8 +18,22 @@ export async function watchSubscriptions({
|
|
|
18
18
|
const id = String(item.id || `sub_${index + 1}`);
|
|
19
19
|
const selector = String(item.selector || '').trim();
|
|
20
20
|
if (!selector) return null;
|
|
21
|
+
const visible = item.visible !== false;
|
|
22
|
+
const pageUrlIncludes = normalizeArray(item.pageUrlIncludes || item.urlIncludes)
|
|
23
|
+
.map((value) => String(value || '').trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const pageUrlExcludes = normalizeArray(item.pageUrlExcludes || item.urlExcludes)
|
|
26
|
+
.map((value) => String(value || '').trim())
|
|
27
|
+
.filter(Boolean);
|
|
21
28
|
const events = normalizeArray(item.events).map((name) => String(name).trim()).filter(Boolean);
|
|
22
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
selector,
|
|
32
|
+
visible,
|
|
33
|
+
pageUrlIncludes,
|
|
34
|
+
pageUrlExcludes,
|
|
35
|
+
events: events.length > 0 ? new Set(events) : null,
|
|
36
|
+
};
|
|
23
37
|
})
|
|
24
38
|
.filter(Boolean);
|
|
25
39
|
|
|
@@ -40,9 +54,17 @@ export async function watchSubscriptions({
|
|
|
40
54
|
try {
|
|
41
55
|
const snapshot = await getDomSnapshotByProfile(resolvedProfile);
|
|
42
56
|
const ts = new Date().toISOString();
|
|
57
|
+
const currentUrl = String(snapshot?.__url || '');
|
|
43
58
|
for (const item of items) {
|
|
44
59
|
const prev = state.get(item.id) || { exists: false, stateSig: '', appearCount: 0 };
|
|
45
|
-
const
|
|
60
|
+
const includeOk = item.pageUrlIncludes.length === 0
|
|
61
|
+
|| item.pageUrlIncludes.some((token) => currentUrl.includes(token));
|
|
62
|
+
const excludeHit = item.pageUrlExcludes.length > 0
|
|
63
|
+
&& item.pageUrlExcludes.some((token) => currentUrl.includes(token));
|
|
64
|
+
const pageUrlMatched = includeOk && !excludeHit;
|
|
65
|
+
const elements = pageUrlMatched
|
|
66
|
+
? notifier.findElements(snapshot, { css: item.selector, visible: item.visible })
|
|
67
|
+
: [];
|
|
46
68
|
const exists = elements.length > 0;
|
|
47
69
|
const stateSig = elements.map((node) => node.path).sort().join(',');
|
|
48
70
|
const changed = stateSig !== prev.stateSig;
|
|
@@ -215,6 +215,7 @@ function buildDomSnapshotScript(maxDepth, maxChildren) {
|
|
|
215
215
|
const root = collect(document.body || document.documentElement, 0, 'root');
|
|
216
216
|
return {
|
|
217
217
|
dom_tree: root,
|
|
218
|
+
current_url: String(window.location.href || ''),
|
|
218
219
|
viewport: {
|
|
219
220
|
width: viewportWidth,
|
|
220
221
|
height: viewportHeight,
|
|
@@ -238,6 +239,9 @@ export async function getDomSnapshotByProfile(profileId, options = {}) {
|
|
|
238
239
|
height: Number(payload.viewport.height) || 0,
|
|
239
240
|
};
|
|
240
241
|
}
|
|
242
|
+
if (tree && payload.current_url) {
|
|
243
|
+
tree.__url = String(payload.current_url);
|
|
244
|
+
}
|
|
241
245
|
return tree;
|
|
242
246
|
}
|
|
243
247
|
|