@web-auto/webauto 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +122 -53
  2. package/apps/desktop-console/dist/main/index.mjs +229 -14
  3. package/apps/desktop-console/dist/renderer/index.js +237 -8
  4. package/apps/desktop-console/entry/ui-cli.mjs +290 -21
  5. package/apps/desktop-console/entry/ui-console.mjs +46 -15
  6. package/apps/webauto/entry/account.mjs +126 -27
  7. package/apps/webauto/entry/lib/account-detect.mjs +399 -9
  8. package/apps/webauto/entry/lib/account-store.mjs +201 -109
  9. package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
  10. package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
  11. package/apps/webauto/entry/lib/profilepool.mjs +12 -0
  12. package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
  13. package/apps/webauto/entry/lib/session-init.mjs +227 -0
  14. package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
  15. package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
  16. package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
  17. package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
  18. package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
  19. package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
  20. package/apps/webauto/entry/profilepool.mjs +56 -9
  21. package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
  22. package/apps/webauto/entry/weibo-unified.mjs +84 -11
  23. package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
  24. package/apps/webauto/entry/xhs-unified.mjs +92 -997
  25. package/bin/webauto.mjs +22 -4
  26. package/dist/modules/camo-backend/src/index.js +33 -0
  27. package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
  28. package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
  29. package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
  30. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
  31. package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
  32. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
  33. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
  34. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
  35. package/dist/modules/workflow/src/runner.js +2 -0
  36. package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
  37. package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
  38. package/modules/camo-backend/src/index.ts +31 -0
  39. package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
  40. package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
  41. package/modules/camo-backend/src/internal/ws-server.ts +17 -17
  42. package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
  43. package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
  44. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
  45. package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
  46. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
  47. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
  48. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
  49. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
  50. package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
  51. package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
  52. package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
  53. package/modules/workflow/blocks/EnsureSession.ts +0 -4
  54. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
  55. package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
  56. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
  57. package/modules/workflow/src/runner.ts +2 -0
  58. package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
  59. package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
  60. package/package.json +2 -2
  61. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
  62. package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
  63. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
  64. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
  65. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
@@ -0,0 +1,436 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { nowIso } from './xhs-unified-blocks.mjs';
5
+
6
+ async function ensureDir(dirPath) {
7
+ await fsp.mkdir(dirPath, { recursive: true });
8
+ }
9
+
10
+ async function writeJson(filePath, payload) {
11
+ await ensureDir(path.dirname(filePath));
12
+ await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
13
+ }
14
+
15
+ function resolveUnifiedApiBaseUrl() {
16
+ const raw = String(
17
+ process.env.WEBAUTO_UNIFIED_API
18
+ || process.env.WEBAUTO_UNIFIED_URL
19
+ || 'http://127.0.0.1:7701',
20
+ ).trim();
21
+ return raw.replace(/\/+$/, '');
22
+ }
23
+
24
+ async function postUnifiedTaskRequest(baseUrl, pathname, payload) {
25
+ try {
26
+ const response = await fetch(`${baseUrl}${pathname}`, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify(payload || {}),
30
+ signal: AbortSignal.timeout(2000),
31
+ });
32
+ if (!response.ok) return false;
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function pushUnique(arr, value) {
40
+ const text = String(value || '').trim();
41
+ if (!text) return;
42
+ if (!arr.includes(text)) arr.push(text);
43
+ }
44
+
45
+ export function toNumber(value, fallback = 0) {
46
+ const num = Number(value);
47
+ return Number.isFinite(num) ? num : fallback;
48
+ }
49
+
50
+ async function readJsonlRows(filePath) {
51
+ if (!filePath) return [];
52
+ try {
53
+ const raw = await fsp.readFile(filePath, 'utf8');
54
+ return raw
55
+ .split('\n')
56
+ .map((line) => line.trim())
57
+ .filter(Boolean)
58
+ .map((line) => {
59
+ try {
60
+ return JSON.parse(line);
61
+ } catch {
62
+ return null;
63
+ }
64
+ })
65
+ .filter(Boolean);
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ function buildCommentDedupKey(row) {
72
+ const noteId = String(row?.noteId || '').trim();
73
+ const userId = String(row?.userId || '').trim();
74
+ const content = String(row?.content || '').replace(/\s+/g, ' ').trim();
75
+ return `${noteId}|${userId}|${content}`;
76
+ }
77
+
78
+ export function createTaskReporter(seed = {}) {
79
+ const baseUrl = resolveUnifiedApiBaseUrl();
80
+ const staticSeed = {
81
+ profileId: String(seed.profileId || 'unknown').trim() || 'unknown',
82
+ keyword: String(seed.keyword || '').trim(),
83
+ phase: 'unified',
84
+ uiTriggerId: String(seed.uiTriggerId || '').trim(),
85
+ };
86
+ const createdRunIds = new Set();
87
+
88
+ const ensureCreated = async (runId, extra = {}) => {
89
+ const rid = String(runId || '').trim();
90
+ if (!rid) return false;
91
+ if (createdRunIds.has(rid)) return true;
92
+ const ok = await postUnifiedTaskRequest(baseUrl, '/api/v1/tasks', {
93
+ runId: rid,
94
+ ...staticSeed,
95
+ ...extra,
96
+ });
97
+ if (ok) createdRunIds.add(rid);
98
+ return ok;
99
+ };
100
+
101
+ const update = async (runId, patch = {}) => {
102
+ const rid = String(runId || '').trim();
103
+ if (!rid) return false;
104
+ await ensureCreated(rid, patch);
105
+ return postUnifiedTaskRequest(baseUrl, `/api/v1/tasks/${encodeURIComponent(rid)}/update`, {
106
+ ...staticSeed,
107
+ ...patch,
108
+ });
109
+ };
110
+
111
+ const pushEvent = async (runId, type, data = {}) => {
112
+ const rid = String(runId || '').trim();
113
+ if (!rid) return false;
114
+ await ensureCreated(rid, data);
115
+ return postUnifiedTaskRequest(baseUrl, `/api/v1/tasks/${encodeURIComponent(rid)}/events`, {
116
+ type: String(type || 'event').trim() || 'event',
117
+ data,
118
+ });
119
+ };
120
+
121
+ const setError = async (runId, message, code = 'TASK_ERROR', recoverable = false) => {
122
+ const rid = String(runId || '').trim();
123
+ if (!rid) return false;
124
+ return update(rid, {
125
+ error: {
126
+ message: String(message || 'task_error'),
127
+ code: String(code || 'TASK_ERROR'),
128
+ timestamp: Date.now(),
129
+ recoverable: recoverable === true,
130
+ },
131
+ });
132
+ };
133
+
134
+ return {
135
+ ensureCreated,
136
+ update,
137
+ pushEvent,
138
+ setError,
139
+ };
140
+ }
141
+
142
+ export function createProfileStats(spec) {
143
+ return {
144
+ assignedNotes: spec.assignedNotes,
145
+ linksCollected: 0,
146
+ linksPaths: [],
147
+ openedNotes: 0,
148
+ commentsHarvestRuns: 0,
149
+ commentsCollected: 0,
150
+ commentsExpected: 0,
151
+ commentsReachedBottomCount: 0,
152
+ likesHitCount: 0,
153
+ likesNewCount: 0,
154
+ likesSkippedCount: 0,
155
+ likesAlreadyCount: 0,
156
+ likesDedupCount: 0,
157
+ searchCount: 0,
158
+ rollbackCount: 0,
159
+ returnToSearchCount: 0,
160
+ operationErrors: 0,
161
+ recoveryFailed: 0,
162
+ terminalCode: null,
163
+ commentPaths: [],
164
+ likeSummaryPaths: [],
165
+ likeStatePaths: [],
166
+ };
167
+ }
168
+
169
+ export function resolveUnifiedPhaseLabel(operationId, fallback = '运行中') {
170
+ const op = String(operationId || '').trim();
171
+ if (!op) return fallback;
172
+ if (
173
+ op === 'sync_window_viewport'
174
+ || op === 'goto_home'
175
+ || op === 'fill_keyword'
176
+ || op === 'submit_search'
177
+ || op === 'xhs_assert_logged_in'
178
+ || op === 'abort_on_login_guard'
179
+ || op === 'abort_on_risk_guard'
180
+ ) {
181
+ return '登录校验';
182
+ }
183
+ if (op === 'ensure_tab_pool' || op === 'verify_subscriptions_all_pages' || op === 'collect_links') {
184
+ return '采集链接';
185
+ }
186
+ if (
187
+ op === 'open_first_detail'
188
+ || op === 'open_next_detail'
189
+ || op === 'wait_between_notes'
190
+ || op === 'switch_tab_round_robin'
191
+ ) {
192
+ return '打开详情';
193
+ }
194
+ if (
195
+ op === 'detail_harvest'
196
+ || op === 'expand_replies'
197
+ || op === 'comments_harvest'
198
+ || op === 'comment_match_gate'
199
+ || op === 'comment_like'
200
+ || op === 'comment_reply'
201
+ || op === 'close_detail'
202
+ ) {
203
+ return '详情采集点赞';
204
+ }
205
+ return fallback;
206
+ }
207
+
208
+ export function resolveUnifiedActionLabel(eventName, payload = {}, fallback = '运行中') {
209
+ const opId = String(payload?.operationId || '').trim();
210
+ if (opId) {
211
+ if (eventName === 'autoscript:operation_error' || eventName === 'autoscript:operation_recovery_failed') {
212
+ const err = String(payload?.code || payload?.message || '').trim();
213
+ return err ? `${opId}: ${err}` : `${opId}: failed`;
214
+ }
215
+ const stage = String(payload?.stage || '').trim();
216
+ if (stage) return `${opId}:${stage}`;
217
+ return opId;
218
+ }
219
+ const msg = String(payload?.message || payload?.reason || '').trim();
220
+ if (msg) return msg;
221
+ return fallback;
222
+ }
223
+
224
+ export function updateProfileStatsFromEvent(stats, payload) {
225
+ const event = String(payload?.event || '').trim();
226
+ if (!event) return;
227
+
228
+ if (event === 'autoscript:operation_error') {
229
+ stats.operationErrors += 1;
230
+ return;
231
+ }
232
+ if (event === 'autoscript:operation_recovery_failed') {
233
+ stats.recoveryFailed += 1;
234
+ return;
235
+ }
236
+ if (event === 'autoscript:operation_terminal') {
237
+ stats.terminalCode = String(payload.code || '').trim() || stats.terminalCode;
238
+ return;
239
+ }
240
+ if (event !== 'autoscript:operation_done') return;
241
+
242
+ const operationId = String(payload.operationId || '').trim();
243
+ const rawResult = payload.result && typeof payload.result === 'object' ? payload.result : {};
244
+ const result = rawResult.result && typeof rawResult.result === 'object'
245
+ ? rawResult.result
246
+ : rawResult;
247
+
248
+ if (operationId === 'open_first_detail' || operationId === 'open_next_detail') {
249
+ if (result.opened === true) {
250
+ stats.openedNotes += 1;
251
+ }
252
+ return;
253
+ }
254
+
255
+ if (operationId === 'submit_search') {
256
+ stats.searchCount = Math.max(stats.searchCount, toNumber(result.searchCount, stats.searchCount));
257
+ return;
258
+ }
259
+
260
+ if (operationId === 'collect_links') {
261
+ stats.linksCollected = Math.max(
262
+ stats.linksCollected,
263
+ toNumber(result.linksWithXsecToken, toNumber(result.collected, stats.linksCollected)),
264
+ );
265
+ pushUnique(stats.linksPaths, result.linksPath);
266
+ return;
267
+ }
268
+
269
+ if (operationId === 'comments_harvest') {
270
+ stats.commentsHarvestRuns += 1;
271
+ stats.commentsCollected += toNumber(result.collected, 0);
272
+ stats.commentsExpected += Math.max(0, toNumber(result.expectedCommentsCount, 0));
273
+ if (result.reachedBottom === true) stats.commentsReachedBottomCount += 1;
274
+ pushUnique(stats.commentPaths, result.commentsPath);
275
+ return;
276
+ }
277
+
278
+ if (operationId === 'comment_like') {
279
+ stats.likesHitCount += toNumber(result.hitCount, 0);
280
+ stats.likesNewCount += toNumber(result.likedCount, 0);
281
+ stats.likesSkippedCount += toNumber(result.skippedCount, 0);
282
+ stats.likesAlreadyCount += toNumber(result.alreadyLikedSkipped, 0);
283
+ stats.likesDedupCount += toNumber(result.dedupSkipped, 0);
284
+ pushUnique(stats.likeSummaryPaths, result.summaryPath);
285
+ pushUnique(stats.likeStatePaths, result.likeStatePath);
286
+ pushUnique(stats.commentPaths, result.commentsPath);
287
+ return;
288
+ }
289
+
290
+ if (operationId === 'close_detail') {
291
+ stats.rollbackCount = Math.max(stats.rollbackCount, toNumber(result.rollbackCount, stats.rollbackCount));
292
+ stats.returnToSearchCount = Math.max(stats.returnToSearchCount, toNumber(result.returnToSearchCount, stats.returnToSearchCount));
293
+ }
294
+ }
295
+
296
+ export async function mergeProfileOutputs({
297
+ results,
298
+ mergedDir,
299
+ keyword,
300
+ env,
301
+ totalNotes,
302
+ parallel,
303
+ concurrency,
304
+ skippedProfiles = [],
305
+ }) {
306
+ const success = results.filter((item) => item && item.ok);
307
+ const failed = results.filter((item) => !item || item.ok === false);
308
+
309
+ const mergedComments = [];
310
+ const seenCommentKeys = new Set();
311
+ const mergedLikeSummaries = [];
312
+
313
+ for (const result of success) {
314
+ for (const commentsPath of result.stats.commentPaths || []) {
315
+ const rows = await readJsonlRows(commentsPath);
316
+ for (const row of rows) {
317
+ const key = buildCommentDedupKey(row);
318
+ if (!key || seenCommentKeys.has(key)) continue;
319
+ seenCommentKeys.add(key);
320
+ mergedComments.push({
321
+ profileId: result.profileId,
322
+ ...row,
323
+ });
324
+ }
325
+ }
326
+
327
+ for (const summaryPath of result.stats.likeSummaryPaths || []) {
328
+ try {
329
+ const raw = await fsp.readFile(summaryPath, 'utf8');
330
+ const summary = JSON.parse(raw);
331
+ mergedLikeSummaries.push({ profileId: result.profileId, summaryPath, summary });
332
+ } catch {
333
+ continue;
334
+ }
335
+ }
336
+ }
337
+
338
+ await ensureDir(mergedDir);
339
+ const mergedCommentsPath = path.join(mergedDir, 'comments.merged.jsonl');
340
+ if (mergedComments.length > 0) {
341
+ const payload = mergedComments.map((row) => JSON.stringify(row)).join('\n');
342
+ await fsp.writeFile(mergedCommentsPath, `${payload}\n`, 'utf8');
343
+ }
344
+
345
+ const mergedLikeSummaryPath = path.join(mergedDir, 'likes.merged.json');
346
+ const likeTotals = {
347
+ noteSummaries: mergedLikeSummaries.length,
348
+ scannedCount: 0,
349
+ hitCount: 0,
350
+ likedCount: 0,
351
+ skippedCount: 0,
352
+ reachedBottomCount: 0,
353
+ };
354
+ for (const item of mergedLikeSummaries) {
355
+ const summary = item.summary || {};
356
+ likeTotals.scannedCount += toNumber(summary.scannedCount, 0);
357
+ likeTotals.hitCount += toNumber(summary.hitCount, 0);
358
+ likeTotals.likedCount += toNumber(summary.likedCount, 0);
359
+ likeTotals.skippedCount += toNumber(summary.skippedCount, 0);
360
+ if (summary.reachedBottom === true) likeTotals.reachedBottomCount += 1;
361
+ }
362
+ await writeJson(mergedLikeSummaryPath, {
363
+ generatedAt: nowIso(),
364
+ totals: likeTotals,
365
+ items: mergedLikeSummaries,
366
+ });
367
+
368
+ const totals = {
369
+ profilesTotal: results.length,
370
+ profilesSucceeded: success.length,
371
+ profilesFailed: failed.length,
372
+ assignedNotes: 0,
373
+ linksCollected: 0,
374
+ openedNotes: 0,
375
+ commentsHarvestRuns: 0,
376
+ commentsCollected: 0,
377
+ commentsExpected: 0,
378
+ commentsReachedBottomCount: 0,
379
+ likesHitCount: 0,
380
+ likesNewCount: 0,
381
+ likesSkippedCount: 0,
382
+ likesAlreadyCount: 0,
383
+ likesDedupCount: 0,
384
+ searchCount: 0,
385
+ rollbackCount: 0,
386
+ returnToSearchCount: 0,
387
+ operationErrors: 0,
388
+ recoveryFailed: 0,
389
+ };
390
+
391
+ for (const result of results) {
392
+ const stats = result?.stats || {};
393
+ totals.assignedNotes += toNumber(result?.assignedNotes ?? stats.assignedNotes, 0);
394
+ totals.linksCollected += toNumber(stats.linksCollected, 0);
395
+ totals.openedNotes += toNumber(stats.openedNotes, 0);
396
+ totals.commentsHarvestRuns += toNumber(stats.commentsHarvestRuns, 0);
397
+ totals.commentsCollected += toNumber(stats.commentsCollected, 0);
398
+ totals.commentsExpected += toNumber(stats.commentsExpected, 0);
399
+ totals.commentsReachedBottomCount += toNumber(stats.commentsReachedBottomCount, 0);
400
+ totals.likesHitCount += toNumber(stats.likesHitCount, 0);
401
+ totals.likesNewCount += toNumber(stats.likesNewCount, 0);
402
+ totals.likesSkippedCount += toNumber(stats.likesSkippedCount, 0);
403
+ totals.likesAlreadyCount += toNumber(stats.likesAlreadyCount, 0);
404
+ totals.likesDedupCount += toNumber(stats.likesDedupCount, 0);
405
+ totals.searchCount += toNumber(stats.searchCount, 0);
406
+ totals.rollbackCount += toNumber(stats.rollbackCount, 0);
407
+ totals.returnToSearchCount += toNumber(stats.returnToSearchCount, 0);
408
+ totals.operationErrors += toNumber(stats.operationErrors, 0);
409
+ totals.recoveryFailed += toNumber(stats.recoveryFailed, 0);
410
+ }
411
+
412
+ const mergedSummary = {
413
+ generatedAt: nowIso(),
414
+ keyword,
415
+ env,
416
+ totalNotes: Number.isFinite(totalNotes) ? totalNotes : null,
417
+ execution: {
418
+ parallel,
419
+ concurrency,
420
+ },
421
+ skippedProfiles,
422
+ totals,
423
+ artifacts: {
424
+ mergedCommentsPath: mergedComments.length > 0 ? mergedCommentsPath : null,
425
+ mergedLikeSummaryPath,
426
+ },
427
+ profiles: results,
428
+ };
429
+
430
+ const summaryPath = path.join(mergedDir, 'summary.json');
431
+ await writeJson(summaryPath, mergedSummary);
432
+ return {
433
+ summaryPath,
434
+ mergedSummary,
435
+ };
436
+ }
@@ -3,6 +3,7 @@ import minimist from 'minimist';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import {
6
+ assertProfileExists,
6
7
  ensureProfile,
7
8
  listProfilesForPool,
8
9
  output,
@@ -26,6 +27,40 @@ function sleep(ms) {
26
27
  return new Promise((resolve) => setTimeout(resolve, ms));
27
28
  }
28
29
 
30
+ function parseIntWithFallback(value, fallback) {
31
+ const parsed = Math.floor(Number(value));
32
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
33
+ return parsed;
34
+ }
35
+
36
+ function resolveLoginViewport() {
37
+ const width = Math.max(900, parseIntWithFallback(process.env.WEBAUTO_VIEWPORT_WIDTH, 1440));
38
+ const height = Math.max(700, parseIntWithFallback(process.env.WEBAUTO_VIEWPORT_HEIGHT, 1100));
39
+ return { width, height };
40
+ }
41
+
42
+ async function applyLoginViewport(profileId) {
43
+ const id = String(profileId || '').trim();
44
+ if (!id) return { ok: false, error: 'missing_profile_id' };
45
+ const viewport = resolveLoginViewport();
46
+ try {
47
+ const { callAPI } = await import('../../../modules/camo-runtime/src/utils/browser-service.mjs');
48
+ const payload = await callAPI('page:setViewport', {
49
+ profileId: id,
50
+ width: viewport.width,
51
+ height: viewport.height,
52
+ });
53
+ const result = payload?.result || payload?.body || payload || {};
54
+ return {
55
+ ok: true,
56
+ width: Number(result.width) || viewport.width,
57
+ height: Number(result.height) || viewport.height,
58
+ };
59
+ } catch (error) {
60
+ return { ok: false, error: error?.message || String(error) };
61
+ }
62
+ }
63
+
29
64
  async function waitForAccountSync(profileId, timeoutSec, intervalSec) {
30
65
  const timeoutMs = Math.max(30, Math.floor(Number(timeoutSec || 900))) * 1000;
31
66
  const intervalMs = Math.max(1, Math.floor(Number(intervalSec || 2))) * 1000;
@@ -78,7 +113,7 @@ async function cmdAdd(prefix, jsonMode) {
78
113
  async function cmdLoginProfile(profileId, argv, jsonMode) {
79
114
  const id = String(profileId || '').trim();
80
115
  if (!id) throw new Error('profileId is required');
81
- await ensureProfile(id);
116
+ assertProfileExists(id);
82
117
  const url = String(argv.url || 'https://www.xiaohongshu.com').trim();
83
118
  const idleTimeout = String(argv['idle-timeout'] || process.env.WEBAUTO_LOGIN_IDLE_TIMEOUT || '30m').trim() || '30m';
84
119
  const timeoutSec = Math.max(30, Math.floor(Number(argv['timeout-sec'] || 900)));
@@ -105,6 +140,7 @@ async function cmdLoginProfile(profileId, argv, jsonMode) {
105
140
  output({ ok: false, code: startRet.code, step: 'start', stderr: startRet.stderr || startRet.stdout }, jsonMode);
106
141
  process.exit(1);
107
142
  }
143
+ const viewport = await applyLoginViewport(id);
108
144
  const cookieAutoRet = runCamo(['cookies', 'auto', 'start', id, '--interval', String(cookieIntervalMs)], {
109
145
  rootDir: ROOT,
110
146
  timeoutMs: 20000,
@@ -121,6 +157,7 @@ async function cmdLoginProfile(profileId, argv, jsonMode) {
121
157
  url,
122
158
  idleTimeout,
123
159
  session: startRet.json || null,
160
+ viewport,
124
161
  pendingProfile,
125
162
  cookieMonitor,
126
163
  waitSync: null,
@@ -136,6 +173,7 @@ async function cmdLoginProfile(profileId, argv, jsonMode) {
136
173
  url,
137
174
  idleTimeout,
138
175
  session: startRet.json || null,
176
+ viewport,
139
177
  pendingProfile,
140
178
  cookieMonitor,
141
179
  waitSync: syncResult,
@@ -144,21 +182,26 @@ async function cmdLoginProfile(profileId, argv, jsonMode) {
144
182
 
145
183
  async function cmdLogin(prefix, argv, jsonMode) {
146
184
  const ensureCount = Math.max(0, Number(argv['ensure-count'] || 0) || 0);
185
+ if (ensureCount > 0) {
186
+ throw new Error('ensure-count is disabled; automatic profile creation is forbidden');
187
+ }
147
188
  const known = listProfilesForPool(prefix).profiles;
148
189
  const created = [];
149
- while (known.length + created.length < ensureCount) {
150
- const profileId = resolveNextProfileId(prefix);
151
- await ensureProfile(profileId);
152
- created.push(profileId);
153
- known.push(profileId);
154
- }
155
190
 
156
191
  const all = [...known];
192
+ if (all.length === 0) {
193
+ throw new Error(`no profiles found for prefix: ${prefix}`);
194
+ }
157
195
  const started = [];
158
196
  const idleTimeout = String(argv['idle-timeout'] || process.env.WEBAUTO_LOGIN_IDLE_TIMEOUT || '30m').trim() || '30m';
159
197
  for (const profileId of all) {
160
198
  const ret = runCamo(['start', profileId, '--url', 'https://www.xiaohongshu.com', '--idle-timeout', idleTimeout], { rootDir: ROOT });
161
- if (ret.ok) started.push(profileId);
199
+ if (ret.ok) {
200
+ started.push(profileId);
201
+ // Keep login windows readable by default across all platforms.
202
+ // This does not depend on workflow-level EnsureSession.
203
+ await applyLoginViewport(profileId);
204
+ }
162
205
  }
163
206
  output({ ok: true, keyword: prefix, profiles: all, created, started }, jsonMode);
164
207
  }
@@ -178,15 +221,17 @@ async function cmdGotoProfile(profileId, argv, jsonMode) {
178
221
  if (!id) throw new Error('profileId is required');
179
222
  const url = String(argv.url || argv._?.[2] || '').trim();
180
223
  if (!url) throw new Error('url is required');
181
- await ensureProfile(id);
224
+ assertProfileExists(id);
182
225
 
183
226
  const gotoRet = runCamo(['goto', id, url], { rootDir: ROOT, timeoutMs: 30000 });
184
227
  if (gotoRet.ok) {
228
+ const viewport = await applyLoginViewport(id);
185
229
  output({
186
230
  ok: true,
187
231
  profileId: id,
188
232
  url,
189
233
  mode: 'goto',
234
+ viewport,
190
235
  result: gotoRet.json || null,
191
236
  }, jsonMode);
192
237
  return;
@@ -204,11 +249,13 @@ async function cmdGotoProfile(profileId, argv, jsonMode) {
204
249
  }, jsonMode);
205
250
  process.exit(1);
206
251
  }
252
+ const viewport = await applyLoginViewport(id);
207
253
  output({
208
254
  ok: true,
209
255
  profileId: id,
210
256
  url,
211
257
  mode: 'start',
258
+ viewport,
212
259
  session: startRet.json || null,
213
260
  }, jsonMode);
214
261
  }