@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.
- package/README.md +122 -53
- package/apps/desktop-console/dist/main/index.mjs +227 -12
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +282 -16
- 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
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
}
|