@web-auto/webauto 0.1.18 → 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 +227 -12
  3. package/apps/desktop-console/dist/renderer/index.js +237 -8
  4. package/apps/desktop-console/entry/ui-cli.mjs +282 -16
  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
@@ -1,14 +1,15 @@
1
1
  /**
2
- * Block: ReplyInteract(开发态:模拟回复)
2
+ * Block: ReplyInteract(回复评论)
3
3
  *
4
4
  * 职责:
5
- * 1) 在指定评论上点击“回复”
6
- * 2) 定位回复输入框并输入内容(系统级键盘)
7
- * 3) 截图留证(包含高亮与 DEV 叠加文案)
5
+ * 1. 在指定评论上点击"回复"
6
+ * 2. 定位回复输入框并输入内容(系统级键盘)
7
+ * 3. 点击发送按钮或按回车提交回复
8
+ * 4. 截图留证(包含高亮与 DEV 叠加文案)
8
9
  *
9
10
  * 约束:
10
11
  * - 点击必须走坐标点击(mouse:click),禁止 DOM click
11
- * - 不提交(不发送真正回复)
12
+ * - dryRun 模式下不提交(仅输入不发送)
12
13
  */
13
14
 
14
15
  import path from 'node:path';
@@ -32,12 +33,14 @@ export interface ReplyInteractOutput {
32
33
  success: boolean;
33
34
  noteId: string;
34
35
  typed: boolean;
36
+ submitted: boolean; // 是否成功提交
35
37
  evidence?: {
36
38
  screenshot?: string | null;
37
39
  };
38
40
  debug?: {
39
41
  replyButtonRect?: { x: number; y: number; width: number; height: number };
40
42
  replyInputRect?: { x: number; y: number; width: number; height: number };
43
+ sendButtonRect?: { x: number; y: number; width: number; height: number };
41
44
  };
42
45
  error?: string;
43
46
  }
@@ -73,7 +76,7 @@ async function findReplyButtonTarget(
73
76
  const raw = candidates.find(el => textEq(el, '回复')) || null;
74
77
  if (!raw) return { ok: false, reason: 'reply-button-not-found' };
75
78
 
76
- const target = raw.closest && (raw.closest('button,a,[role=\"button\"]') || raw) || raw;
79
+ const target = raw.closest && (raw.closest('button,a,[role="button"]') || raw) || raw;
77
80
  const r = target.getBoundingClientRect();
78
81
  if (!r || !r.width || !r.height) return { ok: false, reason: 'reply-rect-empty' };
79
82
 
@@ -143,7 +146,7 @@ async function findReplyInputTarget(sessionId: string, apiUrl: string): Promise<
143
146
  return { ok: true, rect: { x: Math.round(r.left), y: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) }, clickPoint: { x: mx, y: my } };
144
147
  }
145
148
 
146
- const all = Array.from(document.querySelectorAll('textarea, input[type=\"text\"], input:not([type]), [contenteditable=\"true\"], [contenteditable=\"plaintext-only\"]'))
149
+ const all = Array.from(document.querySelectorAll('textarea, input[type="text"], input:not([type]), [contenteditable="true"], [contenteditable="plaintext-only"]'))
147
150
  .filter(isVisible);
148
151
  if (!all.length) return { ok: false, reason: 'no-visible-input' };
149
152
 
@@ -163,6 +166,60 @@ async function findReplyInputTarget(sessionId: string, apiUrl: string): Promise<
163
166
  return payload as ClickTarget;
164
167
  }
165
168
 
169
+ /**
170
+ * 查找发送按钮
171
+ */
172
+ async function findSendButtonTarget(sessionId: string, apiUrl: string): Promise<ClickTarget> {
173
+ const res = await controllerAction(
174
+ 'browser:execute',
175
+ {
176
+ profile: sessionId,
177
+ script: `(() => {
178
+ const isVisible = (el) => {
179
+ const r = el.getBoundingClientRect();
180
+ return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight;
181
+ };
182
+
183
+ // 查找发送按钮 - 多种策略
184
+ const strategies = [
185
+ // 1. 查找包含"发送"文本的按钮
186
+ () => Array.from(document.querySelectorAll('button, a, div, span')).find(el =>
187
+ isVisible(el) && /发送|submit|send/i.test(el.textContent || '')
188
+ ),
189
+ // 2. 查找发送图标按钮
190
+ () => document.querySelector('button[class*="send"], button[class*="submit"], [data-type="send"]'),
191
+ // 3. 查找输入框旁边的按钮
192
+ () => {
193
+ const input = document.querySelector('textarea, input[type="text"], [contenteditable="true"]');
194
+ if (!input) return null;
195
+ const parent = input.closest('.comment-form, .reply-form, form, [class*="comment"]');
196
+ if (!parent) return null;
197
+ return parent.querySelector('button, [role="button"]');
198
+ },
199
+ ];
200
+
201
+ for (const strategy of strategies) {
202
+ const btn = strategy();
203
+ if (btn && isVisible(btn)) {
204
+ const r = btn.getBoundingClientRect();
205
+ const mx = Math.round((r.left + r.right) / 2);
206
+ const my = Math.round((r.top + r.bottom) / 2);
207
+ return {
208
+ ok: true,
209
+ rect: { x: Math.round(r.left), y: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) },
210
+ clickPoint: { x: mx, y: my },
211
+ };
212
+ }
213
+ }
214
+
215
+ return { ok: false, reason: 'send-button-not-found' };
216
+ })()`,
217
+ },
218
+ apiUrl,
219
+ );
220
+ return (res?.result || res?.data?.result || res) as ClickTarget;
221
+ }
222
+
166
223
  async function drawOverlay(
167
224
  sessionId: string,
168
225
  apiUrl: string,
@@ -262,6 +319,81 @@ async function verifyTyped(sessionId: string, apiUrl: string, expected: string):
262
319
  return Boolean(payload?.contains);
263
320
  }
264
321
 
322
+ async function stillContainsReplyInput(sessionId: string, apiUrl: string, expected: string): Promise<boolean> {
323
+ const normalized = String(expected || '').replace(/\s+/g, ' ').trim();
324
+ const prefix = normalized.slice(0, 8);
325
+ if (!prefix) return false;
326
+
327
+ const res = await controllerAction(
328
+ 'browser:execute',
329
+ {
330
+ profile: sessionId,
331
+ script: `(() => {
332
+ const needle = ${JSON.stringify(prefix)};
333
+ const norm = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
334
+ const values = [];
335
+ const nodes = Array.from(document.querySelectorAll('textarea, input[type="text"], input:not([type]), [contenteditable="true"], [contenteditable="plaintext-only"]'));
336
+ for (const node of nodes) {
337
+ if (!node || !node.getBoundingClientRect) continue;
338
+ const r = node.getBoundingClientRect();
339
+ if (!(r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight)) continue;
340
+ let v = '';
341
+ if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) v = node.value || '';
342
+ else v = node.textContent || '';
343
+ const text = norm(v);
344
+ if (text) values.push(text);
345
+ }
346
+ const contains = values.some((v) => v.includes(needle));
347
+ return { ok: true, contains };
348
+ })()`,
349
+ },
350
+ apiUrl,
351
+ ).catch((): null => null);
352
+
353
+ const payload = (res as any)?.result || (res as any)?.data?.result || res;
354
+ return Boolean(payload?.contains);
355
+ }
356
+
357
+ /**
358
+ * 提交回复 - 点击发送按钮或按回车
359
+ */
360
+ async function submitReply(
361
+ sessionId: string,
362
+ apiUrl: string,
363
+ expectedReplyText: string,
364
+ ): Promise<{ ok: boolean; method: 'button' | 'enter' | 'none'; error?: string }> {
365
+ const verifyAfterSubmit = async (method: 'button' | 'enter') => {
366
+ await delay(600);
367
+ const hasPendingInput = await stillContainsReplyInput(sessionId, apiUrl, expectedReplyText);
368
+ if (!hasPendingInput) return { ok: true, method } as const;
369
+ return { ok: false, method, error: 'submit_not_confirmed' } as const;
370
+ };
371
+
372
+ // 首先尝试找发送按钮
373
+ const sendBtn = await findSendButtonTarget(sessionId, apiUrl);
374
+
375
+ if (sendBtn.ok && sendBtn.clickPoint) {
376
+ await controllerAction(
377
+ 'mouse:click',
378
+ { profileId: sessionId, x: Math.round(sendBtn.clickPoint.x), y: Math.round(sendBtn.clickPoint.y) },
379
+ apiUrl,
380
+ );
381
+ const checked = await verifyAfterSubmit('button');
382
+ if (checked.ok) return checked;
383
+ }
384
+
385
+ // 如果没有找到按钮,尝试按回车
386
+ await controllerAction(
387
+ 'keyboard:press',
388
+ { profileId: sessionId, key: 'Enter' },
389
+ apiUrl,
390
+ );
391
+ const checked = await verifyAfterSubmit('enter');
392
+ if (checked.ok) return checked;
393
+
394
+ return { ok: false, method: 'none', error: checked.error || 'submit_failed' };
395
+ }
396
+
265
397
  export async function execute(input: ReplyInteractInput): Promise<ReplyInteractOutput> {
266
398
  const {
267
399
  sessionId,
@@ -278,6 +410,7 @@ export async function execute(input: ReplyInteractInput): Promise<ReplyInteractO
278
410
  let screenshot: string | null = null;
279
411
  let replyButtonRect: { x: number; y: number; width: number; height: number } | undefined = undefined;
280
412
  let replyInputRect: { x: number; y: number; width: number; height: number } | undefined = undefined;
413
+ let sendButtonRect: { x: number; y: number; width: number; height: number } | undefined = undefined;
281
414
 
282
415
  try {
283
416
  const btn = await findReplyButtonTarget(sessionId, unifiedApiUrl, commentVisibleIndex);
@@ -294,17 +427,13 @@ export async function execute(input: ReplyInteractInput): Promise<ReplyInteractO
294
427
  });
295
428
  await delay(350);
296
429
 
297
- if (!dryRun) {
298
- // ✅ 坐标点击(系统点击)
299
- await controllerAction(
300
- 'mouse:click',
301
- { profileId: sessionId, x: Math.round(btn.clickPoint.x), y: Math.round(btn.clickPoint.y) },
302
- unifiedApiUrl,
303
- );
304
- await delay(700);
305
- } else {
306
- await delay(450);
307
- }
430
+ // 坐标点击(系统点击)打开回复框
431
+ await controllerAction(
432
+ 'mouse:click',
433
+ { profileId: sessionId, x: Math.round(btn.clickPoint.x), y: Math.round(btn.clickPoint.y) },
434
+ unifiedApiUrl,
435
+ );
436
+ await delay(700);
308
437
 
309
438
  const inputTarget = await findReplyInputTarget(sessionId, unifiedApiUrl);
310
439
  if (inputTarget.ok && inputTarget.clickPoint && inputTarget.rect) {
@@ -318,47 +447,61 @@ export async function execute(input: ReplyInteractInput): Promise<ReplyInteractO
318
447
  });
319
448
  await delay(250);
320
449
 
321
- if (!dryRun) {
322
- // ✅ 坐标点击聚焦输入框
323
- await controllerAction(
324
- 'mouse:click',
325
- { profileId: sessionId, x: Math.round(inputTarget.clickPoint.x), y: Math.round(inputTarget.clickPoint.y) },
326
- unifiedApiUrl,
327
- );
328
- await delay(220);
329
-
330
- // 清空(可选):dev 模式仍执行,避免误拼接
331
- const isMac = process.platform === 'darwin';
332
- await controllerAction(
333
- 'keyboard:press',
334
- { profileId: sessionId, key: isMac ? 'Meta+A' : 'Control+A' },
335
- unifiedApiUrl,
336
- ).catch(() => {});
337
- await delay(80);
338
- await controllerAction('keyboard:press', { profileId: sessionId, key: 'Backspace' }, unifiedApiUrl).catch(() => {});
339
- await delay(120);
340
-
341
- // ✅ 系统级输入(不提交)
342
- await controllerAction(
343
- 'keyboard:type',
344
- { profileId: sessionId, text: String(replyText || ''), delay: 90, submit: false },
345
- unifiedApiUrl,
346
- );
347
- await delay(260);
348
- } else {
349
- await delay(180);
350
- }
450
+ // 坐标点击聚焦输入框
451
+ await controllerAction(
452
+ 'mouse:click',
453
+ { profileId: sessionId, x: Math.round(inputTarget.clickPoint.x), y: Math.round(inputTarget.clickPoint.y) },
454
+ unifiedApiUrl,
455
+ );
456
+ await delay(220);
457
+
458
+ // 清空(可选)
459
+ const isMac = process.platform === 'darwin';
460
+ await controllerAction(
461
+ 'keyboard:press',
462
+ { profileId: sessionId, key: isMac ? 'Meta+A' : 'Control+A' },
463
+ unifiedApiUrl,
464
+ ).catch(() => {});
465
+ await delay(80);
466
+ await controllerAction('keyboard:press', { profileId: sessionId, key: 'Backspace' }, unifiedApiUrl).catch(() => {});
467
+ await delay(120);
468
+
469
+ // ✅ 系统级输入
470
+ await controllerAction(
471
+ 'keyboard:type',
472
+ { profileId: sessionId, text: String(replyText || ''), delay: 90, submit: false },
473
+ unifiedApiUrl,
474
+ );
475
+ await delay(260);
351
476
  }
352
477
 
353
- const typed = !dryRun && replyInputRect
478
+ const typed = replyInputRect
354
479
  ? await verifyTyped(sessionId, unifiedApiUrl, String(replyText || '').slice(0, 6))
355
480
  : false;
356
481
 
482
+ // ✅ 提交回复(如果不是 dryRun)
483
+ let submitted = false;
484
+ let submitError: string | undefined;
485
+
486
+ if (!dryRun && typed) {
487
+ const submitResult = await submitReply(sessionId, unifiedApiUrl, replyText);
488
+ submitted = submitResult.ok;
489
+ if (!submitResult.ok) {
490
+ submitError = submitResult.error;
491
+ }
492
+
493
+ // 尝试获取发送按钮位置用于截图
494
+ const sendBtn = await findSendButtonTarget(sessionId, unifiedApiUrl);
495
+ if (sendBtn.ok && sendBtn.rect) {
496
+ sendButtonRect = sendBtn.rect;
497
+ }
498
+ }
499
+
357
500
  if (dev || dryRun) {
358
501
  await drawOverlay(sessionId, unifiedApiUrl, {
359
502
  id: 'webauto-dev-reply-label',
360
503
  color: '#00ff00',
361
- label: `[${dryRun ? 'DRYRUN' : 'DEV'}] note=${noteId} commentIdx=${commentVisibleIndex}\nreply: ${String(replyText || '').slice(0, 80)}`,
504
+ label: `[${dryRun ? 'DRYRUN' : submitted ? 'SENT' : 'TYPED'}] note=${noteId} commentIdx=${commentVisibleIndex}\nreply: ${String(replyText || '').slice(0, 80)}`,
362
505
  ttlMs: 9000,
363
506
  });
364
507
  await delay(180);
@@ -367,7 +510,7 @@ export async function execute(input: ReplyInteractInput): Promise<ReplyInteractO
367
510
  const base64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
368
511
  if (base64) {
369
512
  const outDir = path.join(resolveDownloadRoot(), 'xiaohongshu', env, keyword, 'smart-reply', noteId);
370
- const name = `reply-dev-${String(commentVisibleIndex).padStart(3, '0')}-${Date.now()}.png`;
513
+ const name = `reply-${dryRun ? 'dryrun' : submitted ? 'sent' : 'typed'}-${String(commentVisibleIndex).padStart(3, '0')}-${Date.now()}.png`;
371
514
  screenshot = await savePngBase64(base64, path.join(outDir, name));
372
515
  }
373
516
 
@@ -375,16 +518,18 @@ export async function execute(input: ReplyInteractInput): Promise<ReplyInteractO
375
518
  success: true,
376
519
  noteId,
377
520
  typed,
521
+ submitted: dryRun ? false : submitted,
378
522
  evidence: { screenshot },
379
- debug: { replyButtonRect, replyInputRect },
523
+ debug: { replyButtonRect, replyInputRect, sendButtonRect },
380
524
  };
381
525
  } catch (e: any) {
382
526
  return {
383
527
  success: false,
384
528
  noteId,
385
529
  typed: false,
530
+ submitted: false,
386
531
  evidence: { screenshot },
387
- debug: { replyButtonRect, replyInputRect },
532
+ debug: { replyButtonRect, replyInputRect, sendButtonRect },
388
533
  error: e?.message || String(e),
389
534
  };
390
535
  }