@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.
- package/README.md +122 -53
- package/apps/desktop-console/dist/main/index.mjs +229 -14
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +290 -21
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 =
|
|
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' : '
|
|
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
|
|
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
|
}
|