@web-auto/webauto 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +915 -85
  3. package/apps/desktop-console/dist/main/preload.mjs +7 -0
  4. package/apps/desktop-console/dist/renderer/index.html +622 -50
  5. package/apps/desktop-console/dist/renderer/index.js +2415 -470
  6. package/apps/desktop-console/dist/renderer/run.mts +6 -5
  7. package/apps/desktop-console/entry/ui-cli.mjs +672 -0
  8. package/apps/desktop-console/entry/ui-console.mjs +416 -29
  9. package/apps/webauto/entry/account.mjs +89 -53
  10. package/apps/webauto/entry/browser-status.mjs +7 -10
  11. package/apps/webauto/entry/lib/account-detect.mjs +254 -28
  12. package/apps/webauto/entry/lib/account-store.mjs +219 -30
  13. package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
  14. package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
  15. package/apps/webauto/entry/lib/profilepool.mjs +14 -5
  16. package/apps/webauto/entry/lib/quota-status.mjs +23 -0
  17. package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
  18. package/apps/webauto/entry/profilepool.mjs +106 -17
  19. package/apps/webauto/entry/schedule.mjs +612 -0
  20. package/apps/webauto/entry/weibo-unified.mjs +134 -0
  21. package/apps/webauto/entry/xhs-install.mjs +236 -29
  22. package/apps/webauto/entry/xhs-status.mjs +5 -2
  23. package/apps/webauto/entry/xhs-unified.mjs +631 -98
  24. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
  25. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
  26. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
  27. package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
  28. package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
  29. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
  30. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
  31. package/bin/camoufox-cli.mjs +61 -0
  32. package/bin/webauto.mjs +301 -54
  33. package/dist/modules/camo-backend/src/index.js +49 -1
  34. package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
  35. package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
  36. package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
  37. package/dist/modules/collection-manager/bloom-filter.js +91 -0
  38. package/dist/modules/collection-manager/date-utils.js +275 -0
  39. package/dist/modules/collection-manager/index.js +258 -0
  40. package/dist/modules/collection-manager/storage.js +195 -0
  41. package/dist/modules/collection-manager/types.js +47 -0
  42. package/dist/modules/logging/src/index.js +1 -1
  43. package/dist/modules/process-registry/index.js +230 -0
  44. package/dist/modules/rate-limiter/index.js +242 -0
  45. package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
  46. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
  47. package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
  48. package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
  49. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
  50. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
  51. package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
  52. package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
  53. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
  54. package/dist/modules/workflow/config/workflowRegistry.js +2 -0
  55. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
  56. package/dist/modules/workflow/src/runner.js +6 -0
  57. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
  58. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
  59. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
  60. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
  61. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
  62. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
  63. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
  64. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
  65. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
  66. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
  67. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  68. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
  69. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  70. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
  71. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
  72. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
  73. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
  74. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
  75. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
  76. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
  77. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  78. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
  79. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  80. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
  81. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  82. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
  83. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  84. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
  85. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  86. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
  87. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  88. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
  89. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  90. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
  91. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  92. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
  93. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  94. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
  95. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  96. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
  97. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  98. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
  99. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  100. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
  101. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  102. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
  103. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  104. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
  105. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  106. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
  107. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  108. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
  109. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  110. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
  111. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  112. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
  113. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  114. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
  115. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  116. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
  117. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  118. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
  119. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  120. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
  121. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
  122. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
  123. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  124. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
  125. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
  126. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
  127. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  128. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
  129. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  130. package/dist/services/shared/serviceProcessLogger.js +1 -1
  131. package/dist/services/unified-api/server.js +105 -11
  132. package/modules/camo-backend/src/index.ts +46 -1
  133. package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
  134. package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
  135. package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
  136. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
  137. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
  138. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
  139. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
  140. package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
  141. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
  142. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
  143. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
  144. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
  145. package/modules/collection-manager/bloom-filter.ts +112 -0
  146. package/modules/collection-manager/date-utils.ts +316 -0
  147. package/modules/collection-manager/index.ts +309 -0
  148. package/modules/collection-manager/package.json +10 -0
  149. package/modules/collection-manager/storage.ts +174 -0
  150. package/modules/collection-manager/types.ts +156 -0
  151. package/modules/logging/src/index.ts +1 -1
  152. package/modules/process-registry/index.ts +284 -0
  153. package/modules/rate-limiter/index.ts +322 -0
  154. package/modules/state/src/paths.ts +9 -1
  155. package/modules/task-scheduler/index.ts +293 -0
  156. package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
  157. package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
  158. package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
  159. package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
  160. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
  161. package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
  162. package/modules/workflow/config/workflowRegistry.ts +2 -0
  163. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
  164. package/modules/workflow/src/runner.ts +6 -0
  165. package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
  166. package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
  167. package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
  168. package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
  169. package/package.json +13 -4
  170. package/scripts/postinstall-resources.mjs +62 -0
  171. package/scripts/test/run-coverage.mjs +76 -0
  172. package/scripts/weibo/search.ts +49 -0
  173. package/services/shared/serviceProcessLogger.ts +1 -1
  174. package/services/unified-api/server.ts +98 -12
@@ -14,6 +14,7 @@ const DEFAULT_PLATFORM = 'xiaohongshu';
14
14
  const STATUS_ACTIVE = 'active';
15
15
  const STATUS_VALID = 'valid';
16
16
  const STATUS_INVALID = 'invalid';
17
+ const STATUS_PENDING = 'pending';
17
18
 
18
19
  function nowIso() {
19
20
  return new Date().toISOString();
@@ -21,7 +22,8 @@ function nowIso() {
21
22
 
22
23
  function readJson(filePath, fallback) {
23
24
  try {
24
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
25
+ const raw = fs.readFileSync(filePath, 'utf8');
26
+ return JSON.parse(String(raw).replace(/^\uFEFF/, ''));
25
27
  } catch {
26
28
  return fallback;
27
29
  }
@@ -91,12 +93,21 @@ function formatSeq(seq) {
91
93
  return String(seq).padStart(4, '0');
92
94
  }
93
95
 
96
+ function resolveAutoTag(platform) {
97
+ const normalized = normalizePlatform(platform);
98
+ return normalized === 'xiaohongshu'
99
+ ? 'xhs'
100
+ : (toSlug(normalized).split('-')[0] || 'acct');
101
+ }
102
+
103
+ function buildAutoAccountId(platform, seq) {
104
+ return `${resolveAutoTag(platform)}-${formatSeq(seq)}`;
105
+ }
106
+
94
107
  function normalizeId(id, fallbackPlatform, seq) {
95
108
  const cleaned = toSlug(id || '');
96
109
  if (cleaned) return cleaned;
97
- const platform = normalizePlatform(fallbackPlatform);
98
- const tag = platform === 'xiaohongshu' ? 'xhs' : (toSlug(platform).split('-')[0] || 'acct');
99
- return `${tag}-${formatSeq(seq)}`;
110
+ return buildAutoAccountId(fallbackPlatform, seq);
100
111
  }
101
112
 
102
113
  function ensureSafeName(name, field) {
@@ -133,6 +144,20 @@ function resolveAccountDir(id) {
133
144
  return path.join(resolveAccountsRoot(), id);
134
145
  }
135
146
 
147
+ function removeAccountDirById(id) {
148
+ const safeId = String(id || '').trim();
149
+ if (!safeId) return;
150
+ const dir = resolveAccountDir(safeId);
151
+ if (fs.existsSync(dir)) {
152
+ fs.rmSync(dir, { recursive: true, force: true });
153
+ }
154
+ }
155
+
156
+ function hasPersistentAccountId(record) {
157
+ if (normalizeText(record?.accountId)) return true;
158
+ return normalizeStatus(record?.status) === STATUS_PENDING;
159
+ }
160
+
136
161
  function isWithinDir(rootDir, targetPath) {
137
162
  const root = path.resolve(rootDir);
138
163
  const target = path.resolve(targetPath);
@@ -142,7 +167,12 @@ function isWithinDir(rootDir, targetPath) {
142
167
  function loadIndex() {
143
168
  const fallback = { version: 1, nextSeq: 1, updatedAt: null, accounts: [] };
144
169
  const raw = readJson(resolveIndexPath(), fallback);
145
- const accounts = Array.isArray(raw?.accounts) ? raw.accounts : [];
170
+ const accountsRaw = Array.isArray(raw?.accounts) ? raw.accounts : [];
171
+ const staleIds = accountsRaw
172
+ .filter((account) => !hasPersistentAccountId(account))
173
+ .map((account) => String(account?.id || '').trim())
174
+ .filter(Boolean);
175
+ const accounts = accountsRaw.filter((account) => hasPersistentAccountId(account));
146
176
  const maxSeq = accounts.reduce((max, account) => {
147
177
  const seq = Number(account?.seq);
148
178
  return Number.isFinite(seq) ? Math.max(max, seq) : max;
@@ -150,12 +180,26 @@ function loadIndex() {
150
180
  const nextSeq = Number.isFinite(Number(raw?.nextSeq)) && Number(raw?.nextSeq) > maxSeq
151
181
  ? Number(raw.nextSeq)
152
182
  : maxSeq + 1;
153
- return {
183
+ const normalized = {
154
184
  version: 1,
155
185
  nextSeq,
156
186
  updatedAt: raw?.updatedAt || null,
157
187
  accounts,
158
188
  };
189
+
190
+ if (staleIds.length > 0 || accounts.length !== accountsRaw.length) {
191
+ writeJson(resolveIndexPath(), {
192
+ version: 1,
193
+ nextSeq,
194
+ updatedAt: nowIso(),
195
+ accounts,
196
+ });
197
+ for (const id of staleIds) {
198
+ removeAccountDirById(id);
199
+ }
200
+ }
201
+
202
+ return normalized;
159
203
  }
160
204
 
161
205
  function saveIndex(index) {
@@ -175,6 +219,43 @@ function resolveProfilePrefix(platform) {
175
219
  return `${slug}-account`;
176
220
  }
177
221
 
222
+ function resolveProfileSeq(profileId, platform) {
223
+ const value = String(profileId || '').trim();
224
+ if (!value) return null;
225
+ const prefix = resolveProfilePrefix(platform);
226
+ const match = value.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-([0-9]+)$`));
227
+ if (!match) return null;
228
+ const seq = Number(match[1]);
229
+ if (!Number.isFinite(seq) || seq < 0) return null;
230
+ return seq;
231
+ }
232
+
233
+ function resolveUsedAutoSeq(index, platform) {
234
+ const tag = resolveAutoTag(platform);
235
+ const pattern = new RegExp(`^${tag}-([0-9]+)$`);
236
+ const used = new Set();
237
+ for (const row of (index?.accounts || [])) {
238
+ const id = String(row?.id || '').trim();
239
+ const match = id.match(pattern);
240
+ if (!match) continue;
241
+ const seq = Number(match[1]);
242
+ if (!Number.isFinite(seq) || seq < 0) continue;
243
+ used.add(seq);
244
+ }
245
+ return used;
246
+ }
247
+
248
+ function resolveNextAutoSeq(index, platform, preferredSeq = null) {
249
+ const used = resolveUsedAutoSeq(index, platform);
250
+ const preferred = Number(preferredSeq);
251
+ if (Number.isFinite(preferred) && preferred >= 0 && !used.has(preferred)) {
252
+ return preferred;
253
+ }
254
+ let seq = 0;
255
+ while (used.has(seq)) seq += 1;
256
+ return seq;
257
+ }
258
+
178
259
  function ensureAliasUnique(accounts, alias, exceptId = '') {
179
260
  if (!alias) return;
180
261
  const target = alias.toLowerCase();
@@ -227,10 +308,7 @@ function persistAccountMeta(account) {
227
308
  }
228
309
 
229
310
  function deleteAccountMeta(id) {
230
- const dir = resolveAccountDir(id);
231
- if (fs.existsSync(dir)) {
232
- fs.rmSync(dir, { recursive: true, force: true });
233
- }
311
+ removeAccountDirById(id);
234
312
  }
235
313
 
236
314
  function resolveAccountName(inputName, platform, seq) {
@@ -249,9 +327,11 @@ function buildProfileAccountView(profileId, record = null) {
249
327
  const accountId = normalizeText(record?.accountId);
250
328
  const status = normalizeStatus(record?.status || (accountId ? STATUS_VALID : STATUS_INVALID));
251
329
  const valid = status === STATUS_VALID && Boolean(accountId);
330
+ const platform = normalizePlatform(record?.platform);
252
331
  return {
253
332
  profileId,
254
333
  accountRecordId: record?.id || null,
334
+ platform,
255
335
  accountId,
256
336
  alias: normalizeText(record?.alias),
257
337
  name: normalizeText(record?.name),
@@ -305,9 +385,29 @@ export function getAccount(idOrAlias) {
305
385
 
306
386
  export async function addAccount(input = {}) {
307
387
  const index = loadIndex();
308
- const seq = Number(index.nextSeq) || 1;
309
388
  const platform = normalizePlatform(input.platform);
310
- const id = ensureSafeName(normalizeId(input.id, platform, seq), 'id');
389
+ const hasCustomId = Boolean(normalizeText(input.id));
390
+ const explicitProfileId = normalizeText(input.profileId);
391
+ const autoPrefix = resolveProfilePrefix(platform);
392
+ let seq = null;
393
+ let profileId = explicitProfileId;
394
+
395
+ if (!hasCustomId && !explicitProfileId) {
396
+ // Default path: account/profile share the same minimal available slot.
397
+ seq = resolveNextAutoSeq(index, platform, null);
398
+ profileId = `${autoPrefix}-${seq}`;
399
+ } else {
400
+ profileId = explicitProfileId || resolveNextProfileId(autoPrefix);
401
+ const profileSeq = resolveProfileSeq(profileId, platform);
402
+ seq = resolveNextAutoSeq(index, platform, hasCustomId ? null : profileSeq);
403
+ }
404
+
405
+ await ensureProfile(profileId);
406
+
407
+ const id = ensureSafeName(
408
+ hasCustomId ? normalizeId(input.id, platform, seq) : buildAutoAccountId(platform, seq),
409
+ 'id',
410
+ );
311
411
  if (index.accounts.some((item) => item?.id === id)) {
312
412
  throw new Error(`account id already exists: ${id}`);
313
413
  }
@@ -315,21 +415,24 @@ export async function addAccount(input = {}) {
315
415
  const alias = normalizeAlias(input.alias) || normalizeAlias(input.username);
316
416
  ensureAliasUnique(index.accounts, alias);
317
417
 
318
- const profileId = normalizeText(input.profileId)
319
- || resolveNextProfileId(resolveProfilePrefix(platform));
320
- await ensureProfile(profileId);
321
-
322
418
  const fingerprintId = normalizeText(input.fingerprintId) || profileId;
323
419
  const createdAt = nowIso();
324
420
  const accountId = normalizeText(input.accountId || input.platformAccountId || null);
325
- const status = accountId ? STATUS_VALID : STATUS_INVALID;
421
+ const requestedStatus = normalizeStatus(input.status);
422
+ const status = accountId
423
+ ? STATUS_VALID
424
+ : (requestedStatus === STATUS_PENDING ? STATUS_PENDING : STATUS_INVALID);
425
+ const reason =
426
+ status === STATUS_VALID
427
+ ? null
428
+ : (status === STATUS_PENDING ? 'waiting_login' : 'missing_account_id');
326
429
  const account = {
327
430
  id,
328
431
  seq,
329
432
  platform,
330
433
  status,
331
- valid: status === STATUS_VALID,
332
- reason: status === STATUS_VALID ? null : 'missing_account_id',
434
+ valid: false,
435
+ reason,
333
436
  accountId,
334
437
  name: accountId || resolveAccountName(input.name, platform, seq),
335
438
  alias,
@@ -342,7 +445,7 @@ export async function addAccount(input = {}) {
342
445
  };
343
446
 
344
447
  index.accounts.push(account);
345
- index.nextSeq = seq + 1;
448
+ index.nextSeq = Math.max(Number(index.nextSeq) || 1, seq + 1);
346
449
  saveIndex(index);
347
450
  persistAccountMeta(account);
348
451
 
@@ -406,9 +509,14 @@ export async function updateAccount(idOrAlias, patch = {}) {
406
509
  next.name = String(next.accountId);
407
510
  }
408
511
  if (!next.accountId) {
409
- next.status = STATUS_INVALID;
410
- next.valid = false;
411
- if (!next.reason) next.reason = 'missing_account_id';
512
+ if (next.status === STATUS_PENDING) {
513
+ next.valid = false;
514
+ if (!next.reason) next.reason = 'waiting_login';
515
+ } else {
516
+ next.status = STATUS_INVALID;
517
+ next.valid = false;
518
+ if (!next.reason) next.reason = 'missing_account_id';
519
+ }
412
520
  } else if (next.status !== 'disabled' && next.status !== 'archived') {
413
521
  next.status = next.valid === false ? STATUS_INVALID : STATUS_VALID;
414
522
  }
@@ -418,6 +526,9 @@ export async function updateAccount(idOrAlias, patch = {}) {
418
526
  } else if (next.status === STATUS_INVALID) {
419
527
  next.valid = false;
420
528
  next.reason = next.reason || 'invalid';
529
+ } else if (next.status === STATUS_PENDING) {
530
+ next.valid = false;
531
+ next.reason = next.reason || 'waiting_login';
421
532
  }
422
533
 
423
534
  next.updatedAt = nowIso();
@@ -437,7 +548,9 @@ export function upsertProfileAccountState(input = {}) {
437
548
  const alias = normalizeAlias(input.alias);
438
549
  const reason = normalizeText(input.reason);
439
550
  const detectedAt = normalizeText(input.detectedAt) || nowIso();
440
- const status = accountId ? STATUS_VALID : STATUS_INVALID;
551
+ const statusHint = normalizeStatus(input.status);
552
+ const pendingMode = !accountId && statusHint === STATUS_PENDING;
553
+ const status = accountId ? STATUS_VALID : (pendingMode ? STATUS_PENDING : STATUS_INVALID);
441
554
 
442
555
  const index = loadIndex();
443
556
  const existingByProfile = resolveAccountByProfile(index, profileId);
@@ -445,20 +558,86 @@ export function upsertProfileAccountState(input = {}) {
445
558
  let target = existingByAccountId || existingByProfile || null;
446
559
  const purgeIds = new Set();
447
560
 
448
- if (!target && !accountId) {
561
+ if (!accountId) {
562
+ if (target && hasPersistentAccountId(target)) {
563
+ const nextStatus = pendingMode ? STATUS_PENDING : STATUS_INVALID;
564
+ const next = {
565
+ ...target,
566
+ platform,
567
+ profileId,
568
+ fingerprintId: profileId,
569
+ status: nextStatus,
570
+ valid: false,
571
+ reason: reason || (nextStatus === STATUS_PENDING ? 'waiting_login' : 'invalid'),
572
+ detectedAt,
573
+ updatedAt: nowIso(),
574
+ };
575
+ const rowIndex = index.accounts.findIndex((item) => item?.id === target.id);
576
+ if (rowIndex < 0) throw new Error(`account not found: ${target.id}`);
577
+ index.accounts[rowIndex] = next;
578
+ saveIndex(index);
579
+ persistAccountMeta(next);
580
+ return buildProfileAccountView(profileId, next);
581
+ }
582
+
583
+ if (pendingMode) {
584
+ const profileSeq = resolveProfileSeq(profileId, platform);
585
+ const seq = resolveNextAutoSeq(index, platform, profileSeq);
586
+ const id = ensureSafeName(buildAutoAccountId(platform, seq), 'id');
587
+ const createdAt = nowIso();
588
+ const record = {
589
+ id,
590
+ seq,
591
+ platform,
592
+ status: STATUS_PENDING,
593
+ valid: false,
594
+ reason: reason || 'waiting_login',
595
+ accountId: null,
596
+ name: `${platform}-${profileId}`,
597
+ alias: alias || null,
598
+ username: null,
599
+ profileId,
600
+ fingerprintId: profileId,
601
+ createdAt,
602
+ updatedAt: createdAt,
603
+ detectedAt,
604
+ aliasSource: alias ? 'auto' : null,
605
+ };
606
+ index.accounts.push(record);
607
+ index.nextSeq = Math.max(Number(index.nextSeq) || 1, seq + 1);
608
+ saveIndex(index);
609
+ persistAccountMeta(record);
610
+ return buildProfileAccountView(profileId, record);
611
+ }
612
+
613
+ const staleIds = index.accounts
614
+ .filter((item) => String(item?.profileId || '').trim() === profileId && !hasPersistentAccountId(item))
615
+ .map((item) => String(item?.id || '').trim())
616
+ .filter(Boolean);
617
+ if (staleIds.length > 0) {
618
+ index.accounts = index.accounts.filter((item) => {
619
+ const id = String(item?.id || '').trim();
620
+ return !staleIds.includes(id);
621
+ });
622
+ saveIndex(index);
623
+ for (const id of staleIds) deleteAccountMeta(id);
624
+ }
625
+
449
626
  return buildProfileAccountView(profileId, {
450
627
  profileId,
451
628
  accountId: null,
452
- status: STATUS_INVALID,
629
+ alias: null,
630
+ status,
453
631
  valid: false,
454
- reason: reason || 'missing_account_id',
632
+ reason: reason || (pendingMode ? 'waiting_login' : 'missing_account_id'),
455
633
  updatedAt: detectedAt,
456
634
  });
457
635
  }
458
636
 
459
637
  if (!target) {
460
- const seq = Number(index.nextSeq) || 1;
461
- const id = ensureSafeName(normalizeId(`${platform}-${profileId}`, platform, seq), 'id');
638
+ const profileSeq = resolveProfileSeq(profileId, platform);
639
+ const seq = resolveNextAutoSeq(index, platform, profileSeq);
640
+ const id = ensureSafeName(buildAutoAccountId(platform, seq), 'id');
462
641
  const createdAt = nowIso();
463
642
  const record = {
464
643
  id,
@@ -479,7 +658,7 @@ export function upsertProfileAccountState(input = {}) {
479
658
  aliasSource: alias ? 'auto' : null,
480
659
  };
481
660
  index.accounts.push(record);
482
- index.nextSeq = seq + 1;
661
+ index.nextSeq = Math.max(Number(index.nextSeq) || 1, seq + 1);
483
662
  saveIndex(index);
484
663
  persistAccountMeta(record);
485
664
  return buildProfileAccountView(profileId, record);
@@ -553,6 +732,16 @@ export function markProfileInvalid(profileId, reason = 'login_guard') {
553
732
  });
554
733
  }
555
734
 
735
+ export function markProfilePending(profileId, reason = 'waiting_login') {
736
+ const id = ensureSafeName(normalizeText(profileId), 'profileId');
737
+ return upsertProfileAccountState({
738
+ profileId: id,
739
+ accountId: null,
740
+ status: STATUS_PENDING,
741
+ reason,
742
+ });
743
+ }
744
+
556
745
  export function removeAccount(idOrAlias, options = {}) {
557
746
  const index = loadIndex();
558
747
  const account = resolveAccountOrThrow(index, idOrAlias);
@@ -0,0 +1,63 @@
1
+ import WebSocket from 'ws';
2
+
3
+ function resolveBusUrl() {
4
+ const explicit = String(process.env.WEBAUTO_UNIFIED_BUS_URL || '').trim();
5
+ if (explicit) return explicit;
6
+ const host = String(process.env.WEBAUTO_UNIFIED_HOST || '127.0.0.1').trim() || '127.0.0.1';
7
+ const port = Number(
8
+ process.env.WEBAUTO_FLOATING_BUS_PORT
9
+ || process.env.WEBAUTO_UNIFIED_PORT
10
+ || 7701,
11
+ );
12
+ return `ws://${host}:${Number.isFinite(port) ? port : 7701}/bus`;
13
+ }
14
+
15
+ export async function publishBusEvent(payload, options = {}) {
16
+ const message = typeof payload === 'string' ? payload : JSON.stringify(payload ?? {});
17
+ const timeoutMs = Math.max(300, Number(options.timeoutMs || 1500));
18
+ const url = resolveBusUrl();
19
+
20
+ return await new Promise((resolve) => {
21
+ let settled = false;
22
+ let sent = false;
23
+ let ws;
24
+
25
+ const done = (ok) => {
26
+ if (settled) return;
27
+ settled = true;
28
+ resolve(ok);
29
+ };
30
+
31
+ try {
32
+ ws = new WebSocket(url);
33
+ } catch {
34
+ done(false);
35
+ return;
36
+ }
37
+
38
+ const timer = setTimeout(() => {
39
+ try { ws?.terminate(); } catch {}
40
+ done(false);
41
+ }, timeoutMs);
42
+
43
+ ws.on('open', () => {
44
+ try {
45
+ ws.send(message);
46
+ sent = true;
47
+ } catch {
48
+ // ignore send failure
49
+ }
50
+ setTimeout(() => {
51
+ try { ws.close(); } catch {}
52
+ }, 30);
53
+ });
54
+ ws.on('close', () => {
55
+ clearTimeout(timer);
56
+ done(sent);
57
+ });
58
+ ws.on('error', () => {
59
+ clearTimeout(timer);
60
+ done(false);
61
+ });
62
+ });
63
+ }
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ function resolveOnPath(candidates) {
6
+ const pathEnv = process.env.PATH || process.env.Path || '';
7
+ const dirs = pathEnv.split(path.delimiter).filter(Boolean);
8
+ for (const dir of dirs) {
9
+ for (const name of candidates) {
10
+ const full = path.join(dir, name);
11
+ if (existsSync(full)) return full;
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+
17
+ function resolveInDir(dir, candidates) {
18
+ for (const name of candidates) {
19
+ const full = path.join(dir, name);
20
+ if (existsSync(full)) return full;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function wrapWindowsRunner(cmdPath, prefix = []) {
26
+ if (process.platform !== 'win32') return { cmd: cmdPath, prefix };
27
+ const lower = String(cmdPath || '').toLowerCase();
28
+ if (lower.endsWith('.ps1')) {
29
+ return {
30
+ cmd: 'powershell.exe',
31
+ prefix: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', cmdPath, ...prefix],
32
+ };
33
+ }
34
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat')) {
35
+ return {
36
+ cmd: 'cmd.exe',
37
+ prefix: ['/d', '/s', '/c', cmdPath, ...prefix],
38
+ };
39
+ }
40
+ return { cmd: cmdPath, prefix };
41
+ }
42
+
43
+ export function getCamoRunner(rootDir = process.cwd()) {
44
+ const isWin = process.platform === 'win32';
45
+ const localBin = path.join(rootDir, 'node_modules', '.bin');
46
+ const camoNames = isWin ? ['camo.cmd', 'camo.exe', 'camo.bat', 'camo.ps1'] : ['camo'];
47
+ const npxNames = isWin ? ['npx.cmd', 'npx.exe', 'npx.bat', 'npx.ps1'] : ['npx'];
48
+
49
+ const local = resolveInDir(localBin, camoNames);
50
+ if (local) return wrapWindowsRunner(local);
51
+
52
+ const global = resolveOnPath(camoNames);
53
+ if (global) return wrapWindowsRunner(global);
54
+
55
+ const npx = resolveOnPath(npxNames) || (isWin ? 'npx.cmd' : 'npx');
56
+ return wrapWindowsRunner(npx, ['--yes', '--package=@web-auto/camo', 'camo']);
57
+ }
58
+
59
+ function parseLastJson(stdout) {
60
+ const text = String(stdout || '').trim();
61
+ if (!text) return null;
62
+ const lines = text.split('\n').map((line) => line.trim()).filter(Boolean).reverse();
63
+ for (const line of lines) {
64
+ try {
65
+ return JSON.parse(line);
66
+ } catch {
67
+ continue;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ export function runCamo(args, options = {}) {
74
+ const rootDir = String(options.rootDir || process.cwd());
75
+ const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 60000;
76
+ const runner = getCamoRunner(rootDir);
77
+ const ret = spawnSync(runner.cmd, [...runner.prefix, ...args], {
78
+ cwd: rootDir,
79
+ env: { ...process.env, ...(options.env || {}) },
80
+ encoding: 'utf8',
81
+ timeout: timeoutMs,
82
+ windowsHide: true,
83
+ });
84
+ const stdout = String(ret.stdout || '').trim();
85
+ const stderr = String(ret.stderr || '').trim();
86
+ return {
87
+ ok: ret.status === 0,
88
+ code: ret.status,
89
+ stdout,
90
+ stderr,
91
+ json: parseLastJson(stdout),
92
+ };
93
+ }
@@ -7,12 +7,19 @@ function resolvePortableRoot() {
7
7
  return root ? path.join(root, '.webauto') : '';
8
8
  }
9
9
 
10
+ function resolveHomeDir() {
11
+ if (process.platform === 'win32') {
12
+ return process.env.USERPROFILE || os.homedir();
13
+ }
14
+ return process.env.HOME || os.homedir();
15
+ }
16
+
10
17
  export function resolveProfilesRoot() {
11
18
  const envProfiles = String(process.env.WEBAUTO_PATHS_PROFILES || '').trim();
12
19
  if (envProfiles) return envProfiles;
13
20
  const portableRoot = resolvePortableRoot();
14
21
  if (portableRoot) return path.join(portableRoot, 'profiles');
15
- return path.join(process.env.HOME || os.homedir(), '.webauto', 'profiles');
22
+ return path.join(resolveHomeDir(), '.webauto', 'profiles');
16
23
  }
17
24
 
18
25
  export function resolveFingerprintsRoot() {
@@ -20,7 +27,7 @@ export function resolveFingerprintsRoot() {
20
27
  if (envFps) return envFps;
21
28
  const portableRoot = resolvePortableRoot();
22
29
  if (portableRoot) return path.join(portableRoot, 'fingerprints');
23
- return path.join(process.env.HOME || os.homedir(), '.webauto', 'fingerprints');
30
+ return path.join(resolveHomeDir(), '.webauto', 'fingerprints');
24
31
  }
25
32
 
26
33
  export function listProfiles() {
@@ -52,14 +59,16 @@ export function resolveNextProfileId(prefix) {
52
59
  const normalized = String(prefix || '').trim();
53
60
  if (!normalized) throw new Error('prefix is required');
54
61
  const { profiles } = listProfilesForPool(normalized);
55
- let maxIndex = 0;
62
+ const used = new Set();
56
63
  for (const profileId of profiles) {
57
64
  const match = profileId.match(/-(\d+)$/);
58
65
  if (!match) continue;
59
66
  const index = Number(match[1]);
60
- if (Number.isFinite(index)) maxIndex = Math.max(maxIndex, index);
67
+ if (Number.isFinite(index) && index >= 0) used.add(index);
61
68
  }
62
- return `${normalized}-${maxIndex + 1}`;
69
+ let next = 0;
70
+ while (used.has(next)) next += 1;
71
+ return `${normalized}-${next}`;
63
72
  }
64
73
 
65
74
  export async function ensureProfile(profileId) {
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * quota-status.mjs - CLI tool to get rate limiter status
4
+ */
5
+
6
+ import { RateLimiter } from '../../../../dist/modules/rate-limiter/index.js';
7
+
8
+ async function main() {
9
+ const limiter = RateLimiter.getInstance();
10
+ await limiter.init();
11
+
12
+ const status = limiter.getStatus();
13
+
14
+ console.log(JSON.stringify({
15
+ ok: true,
16
+ quotas: status
17
+ }));
18
+ }
19
+
20
+ main().catch(err => {
21
+ console.error(JSON.stringify({ ok: false, error: err.message }));
22
+ process.exit(1);
23
+ });