@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
@@ -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
  import path from 'node:path';
14
15
  import { controllerAction, delay } from '../utils/controllerAction.js';
@@ -31,7 +32,7 @@ async function findReplyButtonTarget(sessionId, apiUrl, commentVisibleIndex) {
31
32
  const raw = candidates.find(el => textEq(el, '回复')) || null;
32
33
  if (!raw) return { ok: false, reason: 'reply-button-not-found' };
33
34
 
34
- const target = raw.closest && (raw.closest('button,a,[role=\"button\"]') || raw) || raw;
35
+ const target = raw.closest && (raw.closest('button,a,[role="button"]') || raw) || raw;
35
36
  const r = target.getBoundingClientRect();
36
37
  if (!r || !r.width || !r.height) return { ok: false, reason: 'reply-rect-empty' };
37
38
 
@@ -96,7 +97,7 @@ async function findReplyInputTarget(sessionId, apiUrl) {
96
97
  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 } };
97
98
  }
98
99
 
99
- const all = Array.from(document.querySelectorAll('textarea, input[type=\"text\"], input:not([type]), [contenteditable=\"true\"], [contenteditable=\"plaintext-only\"]'))
100
+ const all = Array.from(document.querySelectorAll('textarea, input[type="text"], input:not([type]), [contenteditable="true"], [contenteditable="plaintext-only"]'))
100
101
  .filter(isVisible);
101
102
  if (!all.length) return { ok: false, reason: 'no-visible-input' };
102
103
 
@@ -113,6 +114,55 @@ async function findReplyInputTarget(sessionId, apiUrl) {
113
114
  const payload = res?.result || res?.data?.result || res;
114
115
  return payload;
115
116
  }
117
+ /**
118
+ * 查找发送按钮
119
+ */
120
+ async function findSendButtonTarget(sessionId, apiUrl) {
121
+ const res = await controllerAction('browser:execute', {
122
+ profile: sessionId,
123
+ script: `(() => {
124
+ const isVisible = (el) => {
125
+ const r = el.getBoundingClientRect();
126
+ return r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight;
127
+ };
128
+
129
+ // 查找发送按钮 - 多种策略
130
+ const strategies = [
131
+ // 1. 查找包含"发送"文本的按钮
132
+ () => Array.from(document.querySelectorAll('button, a, div, span')).find(el =>
133
+ isVisible(el) && /发送|submit|send/i.test(el.textContent || '')
134
+ ),
135
+ // 2. 查找发送图标按钮
136
+ () => document.querySelector('button[class*="send"], button[class*="submit"], [data-type="send"]'),
137
+ // 3. 查找输入框旁边的按钮
138
+ () => {
139
+ const input = document.querySelector('textarea, input[type="text"], [contenteditable="true"]');
140
+ if (!input) return null;
141
+ const parent = input.closest('.comment-form, .reply-form, form, [class*="comment"]');
142
+ if (!parent) return null;
143
+ return parent.querySelector('button, [role="button"]');
144
+ },
145
+ ];
146
+
147
+ for (const strategy of strategies) {
148
+ const btn = strategy();
149
+ if (btn && isVisible(btn)) {
150
+ const r = btn.getBoundingClientRect();
151
+ const mx = Math.round((r.left + r.right) / 2);
152
+ const my = Math.round((r.top + r.bottom) / 2);
153
+ return {
154
+ ok: true,
155
+ rect: { x: Math.round(r.left), y: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) },
156
+ clickPoint: { x: mx, y: my },
157
+ };
158
+ }
159
+ }
160
+
161
+ return { ok: false, reason: 'send-button-not-found' };
162
+ })()`,
163
+ }, apiUrl);
164
+ return (res?.result || res?.data?.result || res);
165
+ }
116
166
  async function drawOverlay(sessionId, apiUrl, opts) {
117
167
  const ttlMs = typeof opts.ttlMs === 'number' ? Math.max(600, Math.min(15000, Math.floor(opts.ttlMs))) : 4000;
118
168
  await controllerAction('browser:execute', {
@@ -197,11 +247,67 @@ async function verifyTyped(sessionId, apiUrl, expected) {
197
247
  const payload = res?.result || res?.data?.result || res;
198
248
  return Boolean(payload?.contains);
199
249
  }
250
+ async function stillContainsReplyInput(sessionId, apiUrl, expected) {
251
+ const normalized = String(expected || '').replace(/\s+/g, ' ').trim();
252
+ const prefix = normalized.slice(0, 8);
253
+ if (!prefix)
254
+ return false;
255
+ const res = await controllerAction('browser:execute', {
256
+ profile: sessionId,
257
+ script: `(() => {
258
+ const needle = ${JSON.stringify(prefix)};
259
+ const norm = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
260
+ const values = [];
261
+ const nodes = Array.from(document.querySelectorAll('textarea, input[type="text"], input:not([type]), [contenteditable="true"], [contenteditable="plaintext-only"]'));
262
+ for (const node of nodes) {
263
+ if (!node || !node.getBoundingClientRect) continue;
264
+ const r = node.getBoundingClientRect();
265
+ if (!(r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight)) continue;
266
+ let v = '';
267
+ if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) v = node.value || '';
268
+ else v = node.textContent || '';
269
+ const text = norm(v);
270
+ if (text) values.push(text);
271
+ }
272
+ const contains = values.some((v) => v.includes(needle));
273
+ return { ok: true, contains };
274
+ })()`,
275
+ }, apiUrl).catch(() => null);
276
+ const payload = res?.result || res?.data?.result || res;
277
+ return Boolean(payload?.contains);
278
+ }
279
+ /**
280
+ * 提交回复 - 点击发送按钮或按回车
281
+ */
282
+ async function submitReply(sessionId, apiUrl, expectedReplyText) {
283
+ const verifyAfterSubmit = async (method) => {
284
+ await delay(600);
285
+ const hasPendingInput = await stillContainsReplyInput(sessionId, apiUrl, expectedReplyText);
286
+ if (!hasPendingInput)
287
+ return { ok: true, method };
288
+ return { ok: false, method, error: 'submit_not_confirmed' };
289
+ };
290
+ // 首先尝试找发送按钮
291
+ const sendBtn = await findSendButtonTarget(sessionId, apiUrl);
292
+ if (sendBtn.ok && sendBtn.clickPoint) {
293
+ await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(sendBtn.clickPoint.x), y: Math.round(sendBtn.clickPoint.y) }, apiUrl);
294
+ const checked = await verifyAfterSubmit('button');
295
+ if (checked.ok)
296
+ return checked;
297
+ }
298
+ // 如果没有找到按钮,尝试按回车
299
+ await controllerAction('keyboard:press', { profileId: sessionId, key: 'Enter' }, apiUrl);
300
+ const checked = await verifyAfterSubmit('enter');
301
+ if (checked.ok)
302
+ return checked;
303
+ return { ok: false, method: 'none', error: checked.error || 'submit_failed' };
304
+ }
200
305
  export async function execute(input) {
201
306
  const { sessionId, noteId, commentVisibleIndex, replyText, dryRun = false, unifiedApiUrl = 'http://127.0.0.1:7701', env = 'debug', keyword = 'unknown', dev = true, } = input;
202
307
  let screenshot = null;
203
308
  let replyButtonRect = undefined;
204
309
  let replyInputRect = undefined;
310
+ let sendButtonRect = undefined;
205
311
  try {
206
312
  const btn = await findReplyButtonTarget(sessionId, unifiedApiUrl, commentVisibleIndex);
207
313
  if (!btn.ok || !btn.clickPoint || !btn.rect) {
@@ -215,14 +321,9 @@ export async function execute(input) {
215
321
  ttlMs: 6000,
216
322
  });
217
323
  await delay(350);
218
- if (!dryRun) {
219
- // 坐标点击(系统点击)
220
- await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(btn.clickPoint.x), y: Math.round(btn.clickPoint.y) }, unifiedApiUrl);
221
- await delay(700);
222
- }
223
- else {
224
- await delay(450);
225
- }
324
+ // 坐标点击(系统点击)打开回复框
325
+ await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(btn.clickPoint.x), y: Math.round(btn.clickPoint.y) }, unifiedApiUrl);
326
+ await delay(700);
226
327
  const inputTarget = await findReplyInputTarget(sessionId, unifiedApiUrl);
227
328
  if (inputTarget.ok && inputTarget.clickPoint && inputTarget.rect) {
228
329
  replyInputRect = inputTarget.rect;
@@ -233,32 +334,42 @@ export async function execute(input) {
233
334
  ttlMs: 6000,
234
335
  });
235
336
  await delay(250);
236
- if (!dryRun) {
237
- // 坐标点击聚焦输入框
238
- await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(inputTarget.clickPoint.x), y: Math.round(inputTarget.clickPoint.y) }, unifiedApiUrl);
239
- await delay(220);
240
- // 清空(可选):dev 模式仍执行,避免误拼接
241
- const isMac = process.platform === 'darwin';
242
- await controllerAction('keyboard:press', { profileId: sessionId, key: isMac ? 'Meta+A' : 'Control+A' }, unifiedApiUrl).catch(() => { });
243
- await delay(80);
244
- await controllerAction('keyboard:press', { profileId: sessionId, key: 'Backspace' }, unifiedApiUrl).catch(() => { });
245
- await delay(120);
246
- // 系统级输入(不提交)
247
- await controllerAction('keyboard:type', { profileId: sessionId, text: String(replyText || ''), delay: 90, submit: false }, unifiedApiUrl);
248
- await delay(260);
249
- }
250
- else {
251
- await delay(180);
252
- }
337
+ // 坐标点击聚焦输入框
338
+ await controllerAction('mouse:click', { profileId: sessionId, x: Math.round(inputTarget.clickPoint.x), y: Math.round(inputTarget.clickPoint.y) }, unifiedApiUrl);
339
+ await delay(220);
340
+ // 清空(可选)
341
+ const isMac = process.platform === 'darwin';
342
+ await controllerAction('keyboard:press', { profileId: sessionId, key: isMac ? 'Meta+A' : 'Control+A' }, unifiedApiUrl).catch(() => { });
343
+ await delay(80);
344
+ await controllerAction('keyboard:press', { profileId: sessionId, key: 'Backspace' }, unifiedApiUrl).catch(() => { });
345
+ await delay(120);
346
+ // ✅ 系统级输入
347
+ await controllerAction('keyboard:type', { profileId: sessionId, text: String(replyText || ''), delay: 90, submit: false }, unifiedApiUrl);
348
+ await delay(260);
253
349
  }
254
- const typed = !dryRun && replyInputRect
350
+ const typed = replyInputRect
255
351
  ? await verifyTyped(sessionId, unifiedApiUrl, String(replyText || '').slice(0, 6))
256
352
  : false;
353
+ // ✅ 提交回复(如果不是 dryRun)
354
+ let submitted = false;
355
+ let submitError;
356
+ if (!dryRun && typed) {
357
+ const submitResult = await submitReply(sessionId, unifiedApiUrl, replyText);
358
+ submitted = submitResult.ok;
359
+ if (!submitResult.ok) {
360
+ submitError = submitResult.error;
361
+ }
362
+ // 尝试获取发送按钮位置用于截图
363
+ const sendBtn = await findSendButtonTarget(sessionId, unifiedApiUrl);
364
+ if (sendBtn.ok && sendBtn.rect) {
365
+ sendButtonRect = sendBtn.rect;
366
+ }
367
+ }
257
368
  if (dev || dryRun) {
258
369
  await drawOverlay(sessionId, unifiedApiUrl, {
259
370
  id: 'webauto-dev-reply-label',
260
371
  color: '#00ff00',
261
- label: `[${dryRun ? 'DRYRUN' : 'DEV'}] note=${noteId} commentIdx=${commentVisibleIndex}\nreply: ${String(replyText || '').slice(0, 80)}`,
372
+ label: `[${dryRun ? 'DRYRUN' : submitted ? 'SENT' : 'TYPED'}] note=${noteId} commentIdx=${commentVisibleIndex}\nreply: ${String(replyText || '').slice(0, 80)}`,
262
373
  ttlMs: 9000,
263
374
  });
264
375
  await delay(180);
@@ -266,15 +377,16 @@ export async function execute(input) {
266
377
  const base64 = await takeScreenshotBase64(sessionId, unifiedApiUrl);
267
378
  if (base64) {
268
379
  const outDir = path.join(resolveDownloadRoot(), 'xiaohongshu', env, keyword, 'smart-reply', noteId);
269
- const name = `reply-dev-${String(commentVisibleIndex).padStart(3, '0')}-${Date.now()}.png`;
380
+ const name = `reply-${dryRun ? 'dryrun' : submitted ? 'sent' : 'typed'}-${String(commentVisibleIndex).padStart(3, '0')}-${Date.now()}.png`;
270
381
  screenshot = await savePngBase64(base64, path.join(outDir, name));
271
382
  }
272
383
  return {
273
384
  success: true,
274
385
  noteId,
275
386
  typed,
387
+ submitted: dryRun ? false : submitted,
276
388
  evidence: { screenshot },
277
- debug: { replyButtonRect, replyInputRect },
389
+ debug: { replyButtonRect, replyInputRect, sendButtonRect },
278
390
  };
279
391
  }
280
392
  catch (e) {
@@ -282,8 +394,9 @@ export async function execute(input) {
282
394
  success: false,
283
395
  noteId,
284
396
  typed: false,
397
+ submitted: false,
285
398
  evidence: { screenshot },
286
- debug: { replyButtonRect, replyInputRect },
399
+ debug: { replyButtonRect, replyInputRect, sendButtonRect },
287
400
  error: e?.message || String(e),
288
401
  };
289
402
  }