@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.
Files changed (43) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +909 -105
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +796 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +59 -9
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +70 -9
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +122 -35
  10. package/apps/webauto/entry/lib/profilepool.mjs +45 -13
  11. package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
  12. package/apps/webauto/entry/profilepool.mjs +45 -3
  13. package/apps/webauto/entry/schedule.mjs +44 -2
  14. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  15. package/apps/webauto/entry/xhs-install.mjs +248 -52
  16. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  17. package/bin/webauto.mjs +137 -5
  18. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  19. package/dist/services/unified-api/server.js +5 -0
  20. package/dist/services/unified-api/task-state.js +2 -0
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  23. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  24. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  25. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  26. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  27. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  28. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  29. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  30. package/package.json +7 -3
  31. package/runtime/infra/utils/README.md +13 -0
  32. package/runtime/infra/utils/scripts/README.md +0 -0
  33. package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
  34. package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
  35. package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
  36. package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
  37. package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
  38. package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
  39. package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
  40. package/runtime/infra/utils/scripts/test-services.mjs +94 -0
  41. package/scripts/bump-version.mjs +120 -0
  42. package/services/unified-api/server.ts +4 -0
  43. 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', 'once');
429
- await wait('#scheduler-runat-wrap', 8000, 'visible');
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', 'daily');
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 select('#scheduler-type', 'weekly');
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 select('#scheduler-type', 'interval');
436
- await wait('#scheduler-interval-wrap', 8000, 'visible');
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', 'interval');
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', 'xiaohongshu-batch-0');
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 args = minimist(process.argv.slice(2), {
14
- boolean: ['build', 'install', 'check', 'help', 'headless', 'no-daemon', 'dry-run', 'no-dry-run', 'parallel', 'do-likes'],
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(args['no-daemon']);
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/accounts(可用 WEBAUTO_PATHS_ACCOUNTS 覆盖)
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 result = listAccountProfiles();
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
- const rows = listAccountProfiles().profiles;
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 guard = Boolean(document.querySelector('.login-container, .login-dialog, #login-container'));
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 strongCandidates = candidates.filter((item) => item.source !== 'localStorage.search_history');
150
- const best = strongCandidates
161
+ const best = candidates
151
162
  .find((item) => item.source === 'initial_state.user_info')
152
- || strongCandidates.find((item) => item.source === 'nav.self')
153
- || strongCandidates.find((item) => item.source === 'anchor.self')
154
- || strongCandidates.find((item) => item.source === 'anchor' && item.alias)
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: guard,
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 = !detected.hasLoginGuard && (
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 && (!detected.accountId || detected.source === 'localStorage.search_history')) {
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