@web-auto/webauto 0.1.8 → 0.1.11
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 +909 -105
- 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 +796 -331
- package/apps/desktop-console/entry/ui-cli.mjs +59 -9
- package/apps/desktop-console/entry/ui-console.mjs +8 -3
- package/apps/webauto/entry/account.mjs +70 -9
- package/apps/webauto/entry/lib/account-detect.mjs +106 -25
- package/apps/webauto/entry/lib/account-store.mjs +122 -35
- package/apps/webauto/entry/lib/profilepool.mjs +45 -13
- package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
- 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 +248 -52
- package/apps/webauto/entry/xhs-unified.mjs +33 -6
- package/bin/webauto.mjs +137 -5
- 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 +7 -3
- package/runtime/infra/utils/README.md +13 -0
- package/runtime/infra/utils/scripts/README.md +0 -0
- package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
- package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
- package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
- package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
- package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
- package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
- package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
- package/runtime/infra/utils/scripts/test-services.mjs +94 -0
- package/scripts/bump-version.mjs +120 -0
- package/services/unified-api/server.ts +4 -0
- package/services/unified-api/task-state.ts +5 -0
|
@@ -9,10 +9,45 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const APP_ROOT = path.resolve(__dirname, '..');
|
|
11
11
|
const ROOT = path.resolve(APP_ROOT, '..', '..');
|
|
12
|
-
const CONTROL_FILE = path.join(os.homedir(), '.webauto', 'run', 'ui-cli.json');
|
|
13
12
|
const DEFAULT_HOST = process.env.WEBAUTO_UI_CLI_HOST || '127.0.0.1';
|
|
14
13
|
const DEFAULT_PORT = Number(process.env.WEBAUTO_UI_CLI_PORT || 7716);
|
|
15
14
|
|
|
15
|
+
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
16
|
+
const input = String(raw || '').trim();
|
|
17
|
+
const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(input);
|
|
18
|
+
const pathApi = isWinPath ? path.win32 : path;
|
|
19
|
+
return isWinPath ? pathApi.normalize(input) : path.resolve(input);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
|
|
23
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
24
|
+
const resolved = normalizePathForPlatform(raw, platform);
|
|
25
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
26
|
+
return (base === '.webauto' || base === 'webauto')
|
|
27
|
+
? resolved
|
|
28
|
+
: pathApi.join(resolved, '.webauto');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveWebautoRoot() {
|
|
32
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || '').trim();
|
|
33
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome);
|
|
34
|
+
|
|
35
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim();
|
|
36
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot);
|
|
37
|
+
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync('D:\\')) return 'D:\\webauto';
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore drive probing errors
|
|
43
|
+
}
|
|
44
|
+
return path.join(os.homedir(), '.webauto');
|
|
45
|
+
}
|
|
46
|
+
return path.join(os.homedir(), '.webauto');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CONTROL_FILE = path.join(resolveWebautoRoot(), 'run', 'ui-cli.json');
|
|
50
|
+
|
|
16
51
|
const args = minimist(process.argv.slice(2), {
|
|
17
52
|
boolean: ['help', 'json', 'auto-start', 'build', 'install', 'continue-on-error', 'exact', 'keep-open', 'detailed'],
|
|
18
53
|
string: ['host', 'port', 'selector', 'value', 'text', 'key', 'tab', 'label', 'state', 'file', 'output', 'timeout', 'interval', 'nth'],
|
|
@@ -409,6 +444,8 @@ async function runFullCover(endpoint) {
|
|
|
409
444
|
await runProbe('scheduler', '#scheduler-name');
|
|
410
445
|
await runProbe('scheduler', '#scheduler-enabled');
|
|
411
446
|
await runProbe('scheduler', '#scheduler-type');
|
|
447
|
+
await runProbe('scheduler', '#scheduler-periodic-type-wrap');
|
|
448
|
+
await runProbe('scheduler', '#scheduler-periodic-type');
|
|
412
449
|
await runProbe('scheduler', '#scheduler-interval-wrap');
|
|
413
450
|
await runProbe('scheduler', '#scheduler-runat-wrap');
|
|
414
451
|
await runProbe('scheduler', '#scheduler-interval');
|
|
@@ -424,21 +461,34 @@ async function runFullCover(endpoint) {
|
|
|
424
461
|
await runProbe('scheduler', '#scheduler-dryrun');
|
|
425
462
|
await runProbe('scheduler', '#scheduler-like-keywords');
|
|
426
463
|
await runProbe('scheduler', '#scheduler-save-btn');
|
|
464
|
+
await runProbe('scheduler', '#scheduler-run-now-btn');
|
|
427
465
|
await runProbe('scheduler', '#scheduler-reset-btn');
|
|
428
|
-
await select('#scheduler-type', '
|
|
429
|
-
await wait('#scheduler-
|
|
466
|
+
await select('#scheduler-type', 'immediate');
|
|
467
|
+
await wait('#scheduler-periodic-type-wrap', 8000, 'hidden');
|
|
468
|
+
await wait('#scheduler-runat-wrap', 8000, 'hidden');
|
|
430
469
|
await wait('#scheduler-interval-wrap', 8000, 'hidden');
|
|
431
|
-
await select('#scheduler-type', '
|
|
470
|
+
await select('#scheduler-type', 'periodic');
|
|
471
|
+
await wait('#scheduler-periodic-type-wrap', 8000, 'visible');
|
|
472
|
+
await wait('#scheduler-interval-wrap', 8000, 'visible');
|
|
473
|
+
await wait('#scheduler-runat-wrap', 8000, 'hidden');
|
|
474
|
+
await select('#scheduler-periodic-type', 'daily');
|
|
432
475
|
await wait('#scheduler-runat-wrap', 8000, 'visible');
|
|
433
|
-
await
|
|
476
|
+
await wait('#scheduler-interval-wrap', 8000, 'hidden');
|
|
477
|
+
await select('#scheduler-periodic-type', 'weekly');
|
|
434
478
|
await wait('#scheduler-runat-wrap', 8000, 'visible');
|
|
435
|
-
await
|
|
436
|
-
await
|
|
479
|
+
await wait('#scheduler-interval-wrap', 8000, 'hidden');
|
|
480
|
+
await select('#scheduler-periodic-type', 'interval');
|
|
437
481
|
await wait('#scheduler-runat-wrap', 8000, 'hidden');
|
|
482
|
+
await wait('#scheduler-interval-wrap', 8000, 'visible');
|
|
483
|
+
await select('#scheduler-type', 'scheduled');
|
|
484
|
+
await wait('#scheduler-periodic-type-wrap', 8000, 'hidden');
|
|
485
|
+
await wait('#scheduler-runat-wrap', 8000, 'visible');
|
|
486
|
+
await wait('#scheduler-interval-wrap', 8000, 'hidden');
|
|
438
487
|
await input('#scheduler-name', taskName);
|
|
439
|
-
await select('#scheduler-type', '
|
|
488
|
+
await select('#scheduler-type', 'periodic');
|
|
489
|
+
await select('#scheduler-periodic-type', 'interval');
|
|
440
490
|
await input('#scheduler-interval', '20');
|
|
441
|
-
await input('#scheduler-profile', '
|
|
491
|
+
await input('#scheduler-profile', '');
|
|
442
492
|
await input('#scheduler-keyword', keywordSeed);
|
|
443
493
|
await input('#scheduler-max-notes', '20');
|
|
444
494
|
await select('#scheduler-env', 'debug');
|
|
@@ -10,11 +10,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const APP_ROOT = path.resolve(__dirname, '..');
|
|
11
11
|
const DIST_MAIN = path.join(APP_ROOT, 'dist', 'main', 'index.mjs');
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const rawArgv = process.argv.slice(2);
|
|
14
|
+
const args = minimist(rawArgv, {
|
|
15
|
+
boolean: ['build', 'install', 'check', 'help', 'headless', 'no-daemon', 'foreground', 'dry-run', 'no-dry-run', 'parallel', 'do-likes'],
|
|
15
16
|
string: ['profile', 'profiles', 'keyword', 'target', 'scenario', 'output', 'concurrency', 'like-keywords', 'max-likes'],
|
|
16
17
|
alias: { h: 'help', p: 'profile', k: 'keyword', t: 'target', o: 'output' }
|
|
17
18
|
});
|
|
19
|
+
// minimist treats `--no-foo` as negation of `foo`, so `--no-daemon` must be
|
|
20
|
+
// detected from raw argv to keep backward-compatible CLI behavior.
|
|
21
|
+
const noDaemon = rawArgv.includes('--no-daemon') || rawArgv.includes('--foreground') || args.foreground === true;
|
|
18
22
|
|
|
19
23
|
function printHelp() {
|
|
20
24
|
console.log(`webauto ui console
|
|
@@ -35,6 +39,7 @@ Options:
|
|
|
35
39
|
--build Auto-build if missing
|
|
36
40
|
--install Auto-install if missing deps
|
|
37
41
|
--no-daemon Run in foreground mode
|
|
42
|
+
--foreground Alias of --no-daemon
|
|
38
43
|
--scenario Test scenario name
|
|
39
44
|
--profile Test profile ID
|
|
40
45
|
--profiles Test profile IDs (comma-separated)
|
|
@@ -677,7 +682,7 @@ async function main() {
|
|
|
677
682
|
return;
|
|
678
683
|
}
|
|
679
684
|
|
|
680
|
-
await startConsole(
|
|
685
|
+
await startConsole(noDaemon);
|
|
681
686
|
}
|
|
682
687
|
|
|
683
688
|
main().catch((err) => {
|
|
@@ -46,6 +46,12 @@ function normalizeAlias(input) {
|
|
|
46
46
|
return value || null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function normalizePlatform(input, fallback = 'xiaohongshu') {
|
|
50
|
+
const raw = String(input || fallback).trim().toLowerCase();
|
|
51
|
+
if (!raw || raw === 'xhs') return 'xiaohongshu';
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
async function publishAccountEvent(type, payload) {
|
|
50
56
|
try {
|
|
51
57
|
await publishBusEvent({
|
|
@@ -60,9 +66,42 @@ async function publishAccountEvent(type, payload) {
|
|
|
60
66
|
|
|
61
67
|
async function detectAliasFromActivePage(profileId, selector) {
|
|
62
68
|
const { callAPI } = await import('../../../modules/camo-runtime/src/utils/browser-service.mjs');
|
|
63
|
-
const script = `(() => {
|
|
69
|
+
const script = `(async () => {
|
|
70
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
71
|
+
const isVisible = (node) => {
|
|
72
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
73
|
+
const style = window.getComputedStyle(node);
|
|
74
|
+
if (!style) return false;
|
|
75
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
76
|
+
const rect = node.getBoundingClientRect();
|
|
77
|
+
return rect.width > 0 && rect.height > 0;
|
|
78
|
+
};
|
|
79
|
+
const isSelfTabText = (value) => {
|
|
80
|
+
const text = normalize(value);
|
|
81
|
+
return text === '我' || text === '我的' || text === '个人主页' || text === '我的主页';
|
|
82
|
+
};
|
|
83
|
+
const selfTabCandidates = Array.from(document.querySelectorAll('a, button, [role="tab"], [role="link"], [class*="tab"]'))
|
|
84
|
+
.map((node) => ({
|
|
85
|
+
node,
|
|
86
|
+
text: normalize(node.textContent || ''),
|
|
87
|
+
title: normalize(node.getAttribute?.('title') || ''),
|
|
88
|
+
aria: normalize(node.getAttribute?.('aria-label') || ''),
|
|
89
|
+
}))
|
|
90
|
+
.filter((item) => isSelfTabText(item.text) || isSelfTabText(item.title) || isSelfTabText(item.aria));
|
|
91
|
+
const selfTarget = selfTabCandidates.find((item) => isVisible(item.node)) || selfTabCandidates[0] || null;
|
|
92
|
+
if (selfTarget?.node) {
|
|
93
|
+
try {
|
|
94
|
+
selfTarget.node.click();
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
96
|
+
} catch {
|
|
97
|
+
// ignore self-tab click failure
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
64
101
|
const requested = ${JSON.stringify(String(selector || '').trim())};
|
|
65
102
|
const defaultSelectors = [
|
|
103
|
+
'[data-testid*="nickname"]',
|
|
104
|
+
'[class*="profile"] [class*="name"]',
|
|
66
105
|
'[class*="user"] [class*="name"]',
|
|
67
106
|
'[class*="nickname"]',
|
|
68
107
|
'[class*="account"] [class*="name"]',
|
|
@@ -76,14 +115,19 @@ async function detectAliasFromActivePage(profileId, selector) {
|
|
|
76
115
|
for (const sel of selectors) {
|
|
77
116
|
const nodes = Array.from(document.querySelectorAll(sel)).slice(0, 6);
|
|
78
117
|
for (const node of nodes) {
|
|
79
|
-
const text = clean(node.textContent || '');
|
|
118
|
+
const text = clean(node.textContent || node.getAttribute?.('title') || '');
|
|
80
119
|
if (!text) continue;
|
|
81
120
|
candidates.push({ text, selector: sel });
|
|
82
121
|
}
|
|
83
122
|
}
|
|
123
|
+
const userInfo = document.querySelector('[class*="user"] [class*="nickname"], [class*="profile"] [class*="nickname"]');
|
|
124
|
+
if (userInfo) {
|
|
125
|
+
const text = clean(userInfo.textContent || '');
|
|
126
|
+
if (text) candidates.push({ text, selector: 'profile.nickname' });
|
|
127
|
+
}
|
|
84
128
|
const title = clean(document.title || '');
|
|
85
129
|
if (title) candidates.push({ text: title, selector: 'document.title' });
|
|
86
|
-
const bad = ['小红书', '登录', '注册', '搜索'];
|
|
130
|
+
const bad = ['小红书', '登录', '注册', '搜索', '我', '消息', '通知'];
|
|
87
131
|
const picked = candidates.find((item) => {
|
|
88
132
|
if (!item?.text) return false;
|
|
89
133
|
if (item.text.length < 2) return false;
|
|
@@ -114,7 +158,7 @@ function printHelp() {
|
|
|
114
158
|
|
|
115
159
|
Usage:
|
|
116
160
|
webauto account --help
|
|
117
|
-
webauto account list [--json]
|
|
161
|
+
webauto account list [--platform <name>] [--json]
|
|
118
162
|
webauto account list --records [--json]
|
|
119
163
|
webauto account add [--platform <name>] [--alias <alias>] [--name <name>] [--username <username>] [--profile <id>] [--fingerprint <id>] [--status pending|active|disabled|archived] [--json]
|
|
120
164
|
webauto account get <id|alias> [--json]
|
|
@@ -125,7 +169,7 @@ Usage:
|
|
|
125
169
|
webauto account sync <profileId|all> [--pending-while-login] [--resolve-alias] [--json]
|
|
126
170
|
|
|
127
171
|
Notes:
|
|
128
|
-
- 账号数据默认保存到 ~/.webauto
|
|
172
|
+
- 账号数据默认保存到 WEBAUTO 根目录下的 accounts(Windows 优先 D:/webauto,缺失时回落 ~/.webauto,可用 WEBAUTO_HOME 覆盖)
|
|
129
173
|
- list 默认按 profile 展示账号有效态(valid/invalid)
|
|
130
174
|
- add 会自动创建并关联 profile/fingerprint(未指定时自动编号)
|
|
131
175
|
- login 会通过 @web-auto/camo 拉起浏览器并绑定账号 profile
|
|
@@ -143,8 +187,9 @@ Examples:
|
|
|
143
187
|
`);
|
|
144
188
|
}
|
|
145
189
|
|
|
146
|
-
async function cmdList(jsonMode) {
|
|
147
|
-
const
|
|
190
|
+
async function cmdList(jsonMode, platformArg = '') {
|
|
191
|
+
const platform = normalizeAlias(platformArg) ? normalizePlatform(platformArg) : '';
|
|
192
|
+
const result = listAccountProfiles(platform ? { platform } : {});
|
|
148
193
|
output({ ok: true, ...result }, jsonMode);
|
|
149
194
|
}
|
|
150
195
|
|
|
@@ -229,8 +274,20 @@ async function cmdLogin(idOrAlias, argv, jsonMode) {
|
|
|
229
274
|
const account = getAccount(idOrAlias);
|
|
230
275
|
await ensureProfile(account.profileId);
|
|
231
276
|
const url = String(argv.url || inferLoginUrl(account.platform)).trim();
|
|
277
|
+
// Default idle timeout: 30 minutes, configurable via env or CLI.
|
|
278
|
+
// Keep validation semantics aligned with camo parseDurationMs.
|
|
232
279
|
const idleTimeout = String(argv['idle-timeout'] || process.env.WEBAUTO_LOGIN_IDLE_TIMEOUT || '30m').trim() || '30m';
|
|
233
280
|
|
|
281
|
+
const idleTimeoutLower = idleTimeout.toLowerCase();
|
|
282
|
+
const idleTimeoutOk = /^(?:\d+(?:\.\d+)?(?:ms|s|m|h)?|0|off|none|disable|disabled)$/.test(idleTimeoutLower);
|
|
283
|
+
if (!idleTimeoutOk) {
|
|
284
|
+
output({
|
|
285
|
+
ok: false,
|
|
286
|
+
error: 'Invalid idle-timeout format. Use forms like 30m, 1800s, 5000ms, 1h, 0, off.',
|
|
287
|
+
}, jsonMode);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
234
291
|
const pendingProfile = await syncXhsAccountByProfile(account.profileId, { pendingWhileLogin: true }).catch((error) => ({
|
|
235
292
|
profileId: account.profileId,
|
|
236
293
|
valid: false,
|
|
@@ -340,9 +397,13 @@ async function cmdSyncAlias(idOrAlias, argv, jsonMode) {
|
|
|
340
397
|
async function cmdSync(target, argv, jsonMode) {
|
|
341
398
|
const pendingWhileLogin = parseBoolean(argv['pending-while-login'], false);
|
|
342
399
|
const resolveAlias = parseBoolean(argv['resolve-alias'], false);
|
|
400
|
+
const platform = normalizePlatform(argv.platform || 'xiaohongshu');
|
|
343
401
|
const value = String(target || '').trim().toLowerCase();
|
|
344
402
|
if (!value || value === 'all') {
|
|
345
|
-
|
|
403
|
+
if (platform !== 'xiaohongshu') {
|
|
404
|
+
throw new Error(`account sync currently supports platform=xiaohongshu only, got: ${platform}`);
|
|
405
|
+
}
|
|
406
|
+
const rows = listAccountProfiles({ platform: 'xiaohongshu' }).profiles;
|
|
346
407
|
const profileIds = rows.map((item) => item.profileId);
|
|
347
408
|
const synced = await syncXhsAccountsByProfiles(profileIds, { pendingWhileLogin, resolveAlias });
|
|
348
409
|
output({ ok: true, count: synced.length, profiles: synced }, jsonMode);
|
|
@@ -373,7 +434,7 @@ async function main() {
|
|
|
373
434
|
return;
|
|
374
435
|
}
|
|
375
436
|
|
|
376
|
-
if (cmd === 'list') return argv.records ? cmdListRecords(jsonMode) : cmdList(jsonMode);
|
|
437
|
+
if (cmd === 'list') return argv.records ? cmdListRecords(jsonMode) : cmdList(jsonMode, argv.platform);
|
|
377
438
|
if (cmd === 'add') return cmdAdd(argv, jsonMode);
|
|
378
439
|
if (cmd === 'get') return cmdGet(arg1, jsonMode);
|
|
379
440
|
if (cmd === 'update') return cmdUpdate(arg1, argv, jsonMode);
|
|
@@ -14,7 +14,29 @@ function sleep(ms) {
|
|
|
14
14
|
|
|
15
15
|
function buildDetectScript() {
|
|
16
16
|
return `(() => {
|
|
17
|
-
const
|
|
17
|
+
const isVisible = (node) => {
|
|
18
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
19
|
+
const style = window.getComputedStyle(node);
|
|
20
|
+
if (!style) return false;
|
|
21
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
22
|
+
const rect = node.getBoundingClientRect();
|
|
23
|
+
return rect.width > 0 && rect.height > 0;
|
|
24
|
+
};
|
|
25
|
+
const loginGuardSelectors = [
|
|
26
|
+
'.login-container',
|
|
27
|
+
'.login-dialog',
|
|
28
|
+
'#login-container',
|
|
29
|
+
];
|
|
30
|
+
const loginGuardNodes = loginGuardSelectors
|
|
31
|
+
.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
|
|
32
|
+
const visibleLoginGuardNodes = loginGuardNodes.filter((node) => isVisible(node));
|
|
33
|
+
const loginGuardText = visibleLoginGuardNodes
|
|
34
|
+
.slice(0, 6)
|
|
35
|
+
.map((node) => String(node.textContent || '').replace(/\\s+/g, ' ').trim())
|
|
36
|
+
.join(' ');
|
|
37
|
+
const hasLoginText = /登录|扫码|验证码|手机号|请先登录|注册|sign\\s*in/i.test(loginGuardText);
|
|
38
|
+
const loginUrl = /\\/login|signin|passport|account\\/login/i.test(String(location.href || ''));
|
|
39
|
+
const hasGuardSignal = (visibleLoginGuardNodes.length > 0 && hasLoginText) || loginUrl;
|
|
18
40
|
const candidates = [];
|
|
19
41
|
const normalizeAlias = (value) => {
|
|
20
42
|
const text = String(value || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -100,14 +122,6 @@ function buildDetectScript() {
|
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
124
|
|
|
103
|
-
const searchHistoryKey = Object.keys(localStorage || {}).find((key) => String(key || '').startsWith('xhs-pc-search-history-'));
|
|
104
|
-
if (searchHistoryKey) {
|
|
105
|
-
const matched = String(searchHistoryKey).match(/^xhs-pc-search-history-(.+)$/);
|
|
106
|
-
if (matched && matched[1]) {
|
|
107
|
-
pushCandidate(matched[1], null, 'localStorage.search_history');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
125
|
const selfNavEntry = Array.from(document.querySelectorAll('a[href*="/user/profile/"]'))
|
|
112
126
|
.find((node) => {
|
|
113
127
|
const labels = readLabelCandidates(node);
|
|
@@ -132,8 +146,6 @@ function buildDetectScript() {
|
|
|
132
146
|
const labels = readLabelCandidates(anchor);
|
|
133
147
|
if (labels.some(isSelfLabel)) {
|
|
134
148
|
pushCandidate(matched[1], alias, 'anchor.self');
|
|
135
|
-
} else {
|
|
136
|
-
pushCandidate(matched[1], alias, 'anchor');
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -146,15 +158,11 @@ function buildDetectScript() {
|
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
const
|
|
150
|
-
const best = strongCandidates
|
|
161
|
+
const best = candidates
|
|
151
162
|
.find((item) => item.source === 'initial_state.user_info')
|
|
152
|
-
||
|
|
153
|
-
||
|
|
154
|
-
||
|
|
155
|
-
|| strongCandidates.find((item) => item.source === 'anchor')
|
|
156
|
-
|| strongCandidates.find((item) => item.id && item.id.length >= 6)
|
|
157
|
-
|| candidates.find((item) => item.source === 'localStorage.search_history')
|
|
163
|
+
|| candidates.find((item) => item.source === 'nav.self')
|
|
164
|
+
|| candidates.find((item) => item.source === 'anchor.self')
|
|
165
|
+
|| candidates.find((item) => item.id && item.id.length >= 6)
|
|
158
166
|
|| candidates[0]
|
|
159
167
|
|| null;
|
|
160
168
|
let alias = best ? best.alias : null;
|
|
@@ -169,9 +177,10 @@ function buildDetectScript() {
|
|
|
169
177
|
const picked = findAliasFromDom();
|
|
170
178
|
if (picked) alias = picked;
|
|
171
179
|
}
|
|
180
|
+
const hasAccountSignal = Boolean(best && best.id);
|
|
172
181
|
return {
|
|
173
182
|
url: location.href,
|
|
174
|
-
hasLoginGuard:
|
|
183
|
+
hasLoginGuard: hasGuardSignal,
|
|
175
184
|
accountId: best ? best.id : null,
|
|
176
185
|
alias: alias || null,
|
|
177
186
|
source: best ? best.source : null,
|
|
@@ -241,8 +250,77 @@ function buildAliasResolveScript() {
|
|
|
241
250
|
})()`;
|
|
242
251
|
}
|
|
243
252
|
|
|
253
|
+
function buildGotoSelfTabScript() {
|
|
254
|
+
return `(() => {
|
|
255
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
256
|
+
const isSelfLabel = (value) => {
|
|
257
|
+
const text = normalize(value);
|
|
258
|
+
if (!text) return false;
|
|
259
|
+
return text === '我' || text === '我的' || text === '个人主页' || text === '我的主页';
|
|
260
|
+
};
|
|
261
|
+
const isVisible = (node) => {
|
|
262
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
263
|
+
const style = window.getComputedStyle(node);
|
|
264
|
+
if (!style) return false;
|
|
265
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
|
|
266
|
+
const rect = node.getBoundingClientRect();
|
|
267
|
+
return rect.width > 0 && rect.height > 0;
|
|
268
|
+
};
|
|
269
|
+
const candidates = Array.from(document.querySelectorAll('a, button, [role="tab"], [role="link"], [class*="tab"]'))
|
|
270
|
+
.map((node) => {
|
|
271
|
+
const text = normalize(node.textContent || '');
|
|
272
|
+
const title = normalize(node.getAttribute?.('title') || '');
|
|
273
|
+
const aria = normalize(node.getAttribute?.('aria-label') || '');
|
|
274
|
+
return {
|
|
275
|
+
node,
|
|
276
|
+
text,
|
|
277
|
+
title,
|
|
278
|
+
aria,
|
|
279
|
+
};
|
|
280
|
+
})
|
|
281
|
+
.filter((item) => isSelfLabel(item.text) || isSelfLabel(item.title) || isSelfLabel(item.aria));
|
|
282
|
+
const target = candidates.find((item) => isVisible(item.node)) || candidates[0] || null;
|
|
283
|
+
if (!target?.node) {
|
|
284
|
+
return { clicked: false, reason: 'self_tab_not_found' };
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
target.node.click();
|
|
288
|
+
return {
|
|
289
|
+
clicked: true,
|
|
290
|
+
reason: 'ok',
|
|
291
|
+
label: target.text || target.title || target.aria || null,
|
|
292
|
+
};
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return { clicked: false, reason: String(error?.message || error || 'click_failed') };
|
|
295
|
+
}
|
|
296
|
+
})()`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function resolveAliasFromSelfTab(profileId) {
|
|
300
|
+
if (!profileId) return null;
|
|
301
|
+
try {
|
|
302
|
+
await callAPI('evaluate', { profileId, script: buildGotoSelfTabScript() });
|
|
303
|
+
await sleep(900);
|
|
304
|
+
const payload = await callAPI('evaluate', { profileId, script: buildAliasResolveScript() });
|
|
305
|
+
const result = payload?.result || payload?.data || payload || {};
|
|
306
|
+
const alias = normalizeText(result.alias);
|
|
307
|
+
if (!alias) return null;
|
|
308
|
+
return {
|
|
309
|
+
alias,
|
|
310
|
+
source: normalizeText(result.source) ? `self_tab:${normalizeText(result.source)}` : 'self_tab',
|
|
311
|
+
candidates: Array.isArray(result.candidates) ? result.candidates : [],
|
|
312
|
+
};
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
244
318
|
async function resolveAliasFromProfilePage(profileId, accountId) {
|
|
245
319
|
if (!profileId || !accountId) return null;
|
|
320
|
+
const fromSelfTab = await resolveAliasFromSelfTab(profileId);
|
|
321
|
+
if (fromSelfTab?.alias) {
|
|
322
|
+
return fromSelfTab;
|
|
323
|
+
}
|
|
246
324
|
let originalUrl = null;
|
|
247
325
|
try {
|
|
248
326
|
const urlPayload = await callAPI('evaluate', { profileId, script: 'window.location.href' });
|
|
@@ -296,15 +374,15 @@ export async function detectXhsAccountIdentity(profileId, options = {}) {
|
|
|
296
374
|
};
|
|
297
375
|
|
|
298
376
|
let detected = await runDetect();
|
|
299
|
-
const shouldRetry =
|
|
377
|
+
const shouldRetry = (
|
|
300
378
|
!detected.accountId
|
|
301
|
-
|| detected.source === 'localStorage.search_history'
|
|
302
379
|
|| !detected.alias
|
|
380
|
+
|| detected.hasLoginGuard
|
|
303
381
|
);
|
|
304
382
|
if (shouldRetry) {
|
|
305
383
|
await sleep(1200);
|
|
306
384
|
const retry = await runDetect();
|
|
307
|
-
if (retry.accountId &&
|
|
385
|
+
if (retry.accountId && !detected.accountId) {
|
|
308
386
|
detected.accountId = retry.accountId;
|
|
309
387
|
detected.source = retry.source || detected.source;
|
|
310
388
|
detected.candidates = retry.candidates;
|
|
@@ -314,6 +392,9 @@ export async function detectXhsAccountIdentity(profileId, options = {}) {
|
|
|
314
392
|
if (!detected.source) detected.source = retry.source || detected.source;
|
|
315
393
|
}
|
|
316
394
|
if (retry.url && !detected.url) detected.url = retry.url;
|
|
395
|
+
if (detected.hasLoginGuard && !retry.hasLoginGuard) {
|
|
396
|
+
detected.hasLoginGuard = false;
|
|
397
|
+
}
|
|
317
398
|
}
|
|
318
399
|
if (options?.resolveAlias === true && detected.accountId && !detected.alias) {
|
|
319
400
|
const resolved = await resolveAliasFromProfilePage(detected.profileId, detected.accountId);
|
|
@@ -330,7 +411,7 @@ export async function syncXhsAccountByProfile(profileId, options = {}) {
|
|
|
330
411
|
if (!normalizedProfileId) throw new Error('profileId is required');
|
|
331
412
|
const pendingWhileLogin = options?.pendingWhileLogin === true;
|
|
332
413
|
try {
|
|
333
|
-
const existing = listAccountProfiles().profiles.find(
|
|
414
|
+
const existing = listAccountProfiles({ platform: 'xiaohongshu' }).profiles.find(
|
|
334
415
|
(item) => String(item?.profileId || '').trim() === normalizedProfileId,
|
|
335
416
|
);
|
|
336
417
|
const shouldResolveAlias = options?.resolveAlias === true
|
|
@@ -362,7 +443,7 @@ export async function syncXhsAccountByProfile(profileId, options = {}) {
|
|
|
362
443
|
const msg = String(error?.message || error || '');
|
|
363
444
|
if (msg.toLowerCase().includes('operation is insecure')) {
|
|
364
445
|
try {
|
|
365
|
-
const existing = listAccountProfiles().profiles.find((item) => String(item?.profileId || '').trim() === normalizedProfileId);
|
|
446
|
+
const existing = listAccountProfiles({ platform: 'xiaohongshu' }).profiles.find((item) => String(item?.profileId || '').trim() === normalizedProfileId);
|
|
366
447
|
if (existing && existing.valid) return existing;
|
|
367
448
|
} catch {
|
|
368
449
|
// ignore fallback lookup
|