@reconcrap/boss-recommend-mcp 2.0.12 → 2.0.14
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/package.json +1 -1
- package/src/chat-mcp.js +256 -23
- package/src/cli.js +2 -8
- package/src/core/browser/index.js +35 -8
- package/src/core/capture/index.js +323 -20
- package/src/core/cv-acquisition/index.js +3 -0
- package/src/domains/chat/jobs.js +100 -9
- package/src/domains/chat/run-service.js +8 -0
- package/src/domains/recommend/run-service.js +29 -9
- package/src/domains/recruit/run-service.js +3 -0
package/package.json
CHANGED
package/src/chat-mcp.js
CHANGED
|
@@ -71,6 +71,43 @@ const TERMINAL_STATUSES = new Set([
|
|
|
71
71
|
RUN_STATUS_CANCELED
|
|
72
72
|
]);
|
|
73
73
|
|
|
74
|
+
const ARTIFACT_STATUSES = new Set([
|
|
75
|
+
RUN_STATUS_COMPLETED,
|
|
76
|
+
RUN_STATUS_FAILED,
|
|
77
|
+
RUN_STATUS_CANCELED,
|
|
78
|
+
RUN_STATUS_PAUSED
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const STALE_PROCESS_STATUSES = new Set([
|
|
82
|
+
"queued",
|
|
83
|
+
"running",
|
|
84
|
+
RUN_STATUS_CANCELING
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const CHAT_REQUEST_RESUME_ACTIONS = new Set([
|
|
88
|
+
"request_cv",
|
|
89
|
+
"ask_cv",
|
|
90
|
+
"request_resume",
|
|
91
|
+
"求简历",
|
|
92
|
+
"索要简历"
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const CHAT_DISABLE_REQUEST_RESUME_ACTIONS = new Set([
|
|
96
|
+
"none",
|
|
97
|
+
"no",
|
|
98
|
+
"false",
|
|
99
|
+
"off",
|
|
100
|
+
"skip",
|
|
101
|
+
"do_nothing",
|
|
102
|
+
"nothing",
|
|
103
|
+
"不做",
|
|
104
|
+
"什么都不做",
|
|
105
|
+
"无",
|
|
106
|
+
"不用",
|
|
107
|
+
"不求简历",
|
|
108
|
+
"不请求简历"
|
|
109
|
+
]);
|
|
110
|
+
|
|
74
111
|
let chatWorkflowImpl = runChatWorkflow;
|
|
75
112
|
let chatConnectorImpl = connectChatChromeSession;
|
|
76
113
|
let chatJobReaderImpl = readChatJobOptionsFromSession;
|
|
@@ -156,8 +193,13 @@ function readJsonFile(filePath) {
|
|
|
156
193
|
}
|
|
157
194
|
}
|
|
158
195
|
|
|
159
|
-
function selectedChatJobForCsv(meta = {}) {
|
|
160
|
-
const job = normalizeText(
|
|
196
|
+
function selectedChatJobForCsv(meta = {}, snapshot = {}) {
|
|
197
|
+
const job = normalizeText(
|
|
198
|
+
meta.normalized?.job
|
|
199
|
+
|| meta.args?.job
|
|
200
|
+
|| snapshot.context?.job
|
|
201
|
+
|| ""
|
|
202
|
+
);
|
|
161
203
|
return {
|
|
162
204
|
value: job,
|
|
163
205
|
title: job,
|
|
@@ -167,31 +209,32 @@ function selectedChatJobForCsv(meta = {}) {
|
|
|
167
209
|
|
|
168
210
|
function buildChatCsvInputRows(snapshot = {}, meta = {}) {
|
|
169
211
|
const normalized = meta.normalized || {};
|
|
170
|
-
const
|
|
212
|
+
const context = snapshot.context || {};
|
|
213
|
+
const postAction = shouldRequestChatResume(meta.args, context)
|
|
171
214
|
? "request_cv"
|
|
172
215
|
: normalizeText(meta.args?.post_action || meta.args?.action || "") || "none";
|
|
173
216
|
const searchParams = {
|
|
174
|
-
job: normalized.job || meta.args?.job || "",
|
|
175
|
-
start_from: normalized.startFrom || meta.args?.start_from || "",
|
|
217
|
+
job: normalized.job || meta.args?.job || context.job || "",
|
|
218
|
+
start_from: normalized.startFrom || meta.args?.start_from || context.start_from || "",
|
|
176
219
|
target_count: normalized.publicTargetCount ?? normalized.targetCount ?? snapshot.progress?.target_count ?? "",
|
|
177
|
-
detail_source: meta.args?.detail_source || snapshot.summary?.detail_source ||
|
|
220
|
+
detail_source: meta.args?.detail_source || snapshot.summary?.detail_source || context.detail_source || ""
|
|
178
221
|
};
|
|
179
222
|
return buildLegacyScreenInputRows({
|
|
180
223
|
instruction: meta.args?.instruction || "启动boss聊天任务",
|
|
181
224
|
selectedPage: "chat",
|
|
182
|
-
selectedJob: selectedChatJobForCsv(meta),
|
|
225
|
+
selectedJob: selectedChatJobForCsv(meta, snapshot),
|
|
183
226
|
userSearchParams: cloneReportInput(searchParams, {}),
|
|
184
227
|
effectiveSearchParams: cloneReportInput(searchParams, {}),
|
|
185
228
|
screenParams: {
|
|
186
|
-
criteria: normalized.criteria || meta.args?.criteria || "",
|
|
229
|
+
criteria: normalized.criteria || meta.args?.criteria || context.criteria || "",
|
|
187
230
|
target_count: searchParams.target_count,
|
|
188
231
|
post_action: postAction,
|
|
189
232
|
max_greet_count: meta.args?.max_greet_count ?? ""
|
|
190
233
|
},
|
|
191
234
|
followUp: meta.args?.follow_up || null,
|
|
192
235
|
extraRows: [
|
|
193
|
-
["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || DEFAULT_CHAT_GREETING_TEXT],
|
|
194
|
-
["chat_params.profile", normalized.profile || meta.args?.profile || "default"]
|
|
236
|
+
["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || context.greeting_text || DEFAULT_CHAT_GREETING_TEXT],
|
|
237
|
+
["chat_params.profile", normalized.profile || meta.args?.profile || context.profile || "default"]
|
|
195
238
|
]
|
|
196
239
|
});
|
|
197
240
|
}
|
|
@@ -314,6 +357,12 @@ function ensureChatRunArtifacts(snapshot) {
|
|
|
314
357
|
partial: true,
|
|
315
358
|
partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
|
|
316
359
|
results: checkpointResults
|
|
360
|
+
} : ARTIFACT_STATUSES.has(snapshot?.status || snapshot?.state) ? {
|
|
361
|
+
domain: "chat",
|
|
362
|
+
partial: (snapshot?.status || snapshot?.state) !== RUN_STATUS_COMPLETED,
|
|
363
|
+
partial_reason: snapshot?.status || snapshot?.state || "unknown",
|
|
364
|
+
completion_reason: completionReason(snapshot?.status || snapshot?.state),
|
|
365
|
+
results: []
|
|
317
366
|
} : null);
|
|
318
367
|
if (artifactSummary) {
|
|
319
368
|
const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
|
|
@@ -337,6 +386,143 @@ function ensureChatRunArtifacts(snapshot) {
|
|
|
337
386
|
return artifacts;
|
|
338
387
|
}
|
|
339
388
|
|
|
389
|
+
function isPidAlive(pid) {
|
|
390
|
+
const numericPid = Number(pid);
|
|
391
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
|
|
392
|
+
if (numericPid === process.pid) return true;
|
|
393
|
+
try {
|
|
394
|
+
process.kill(numericPid, 0);
|
|
395
|
+
return true;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return error?.code === "EPERM";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function snapshotFromPersistedChatRun(persisted = {}) {
|
|
402
|
+
return {
|
|
403
|
+
runId: persisted.run_id || persisted.runId,
|
|
404
|
+
name: persisted.name || persisted.run_id || persisted.runId,
|
|
405
|
+
status: persisted.status || persisted.state,
|
|
406
|
+
phase: persisted.stage || persisted.phase,
|
|
407
|
+
progress: persisted.progress || {},
|
|
408
|
+
context: persisted.context || {},
|
|
409
|
+
checkpoint: persisted.checkpoint || {},
|
|
410
|
+
startedAt: persisted.started_at || persisted.startedAt,
|
|
411
|
+
updatedAt: persisted.updated_at || persisted.updatedAt,
|
|
412
|
+
completedAt: persisted.completed_at || persisted.completedAt || null,
|
|
413
|
+
error: persisted.error || null,
|
|
414
|
+
summary: persisted.summary || null
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function persistDiskChatRun(runId, payload) {
|
|
419
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
420
|
+
if (!artifacts) return payload;
|
|
421
|
+
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
422
|
+
return payload;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function attachLegacyArtifactsToPersistedChatRun(persisted = {}) {
|
|
426
|
+
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
427
|
+
if (!runId) return persisted;
|
|
428
|
+
const snapshot = snapshotFromPersistedChatRun(persisted);
|
|
429
|
+
const result = buildLegacyChatResult(snapshot);
|
|
430
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
431
|
+
const next = {
|
|
432
|
+
...persisted,
|
|
433
|
+
result,
|
|
434
|
+
resume: {
|
|
435
|
+
...(persisted.resume || {}),
|
|
436
|
+
checkpoint_path: result?.checkpoint_path || persisted.resume?.checkpoint_path || artifacts?.checkpoint_path || null,
|
|
437
|
+
output_csv: result?.output_csv || persisted.resume?.output_csv || artifacts?.output_csv || null
|
|
438
|
+
},
|
|
439
|
+
artifacts: artifacts || persisted.artifacts || null
|
|
440
|
+
};
|
|
441
|
+
return persistDiskChatRun(runId, next);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function finalizePersistedChatRun(persisted = {}, {
|
|
445
|
+
status = RUN_STATUS_FAILED,
|
|
446
|
+
error = null,
|
|
447
|
+
message = ""
|
|
448
|
+
} = {}) {
|
|
449
|
+
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
450
|
+
if (!runId) return persisted;
|
|
451
|
+
const now = new Date().toISOString();
|
|
452
|
+
const normalizedError = status === RUN_STATUS_FAILED
|
|
453
|
+
? {
|
|
454
|
+
name: error?.name || "Error",
|
|
455
|
+
code: error?.code || "STALE_RUN_PROCESS_EXITED",
|
|
456
|
+
message: error?.message || message || "Boss chat run process exited before it wrote a terminal state."
|
|
457
|
+
}
|
|
458
|
+
: null;
|
|
459
|
+
const next = {
|
|
460
|
+
...persisted,
|
|
461
|
+
run_id: runId,
|
|
462
|
+
state: status,
|
|
463
|
+
status,
|
|
464
|
+
stage: persisted.stage || persisted.phase || "chat:stale",
|
|
465
|
+
updated_at: now,
|
|
466
|
+
heartbeat_at: now,
|
|
467
|
+
completed_at: persisted.completed_at || now,
|
|
468
|
+
last_message: normalizedError?.message || message || status,
|
|
469
|
+
control: {
|
|
470
|
+
...(persisted.control || {}),
|
|
471
|
+
cancel_requested: false
|
|
472
|
+
},
|
|
473
|
+
error: normalizedError,
|
|
474
|
+
summary: persisted.summary || null
|
|
475
|
+
};
|
|
476
|
+
return attachLegacyArtifactsToPersistedChatRun(next);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function persistedChatRunArtifactMissing(persisted = {}) {
|
|
480
|
+
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
481
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
482
|
+
const outputCsv = persisted.result?.output_csv
|
|
483
|
+
|| persisted.resume?.output_csv
|
|
484
|
+
|| persisted.artifacts?.output_csv
|
|
485
|
+
|| artifacts?.output_csv;
|
|
486
|
+
const reportJson = persisted.result?.report_json
|
|
487
|
+
|| persisted.artifacts?.report_json
|
|
488
|
+
|| artifacts?.report_json;
|
|
489
|
+
return Boolean(
|
|
490
|
+
!outputCsv
|
|
491
|
+
|| !reportJson
|
|
492
|
+
|| !fs.existsSync(outputCsv)
|
|
493
|
+
|| !fs.existsSync(reportJson)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function reconcilePersistedChatRun(persisted = {}, { cancelStale = false } = {}) {
|
|
498
|
+
const status = persisted.status || persisted.state;
|
|
499
|
+
if (STALE_PROCESS_STATUSES.has(status) && !isPidAlive(persisted.pid)) {
|
|
500
|
+
const shouldCancel = cancelStale || status === RUN_STATUS_CANCELING || persisted.control?.cancel_requested === true;
|
|
501
|
+
return {
|
|
502
|
+
run: finalizePersistedChatRun(persisted, {
|
|
503
|
+
status: shouldCancel ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
|
|
504
|
+
error: shouldCancel ? null : {
|
|
505
|
+
code: "STALE_RUN_PROCESS_EXITED",
|
|
506
|
+
message: `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
|
|
507
|
+
},
|
|
508
|
+
message: shouldCancel
|
|
509
|
+
? "Boss chat run was canceled after its worker process was no longer active."
|
|
510
|
+
: `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
|
|
511
|
+
}),
|
|
512
|
+
stale_finalized: true
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (ARTIFACT_STATUSES.has(status) && persistedChatRunArtifactMissing(persisted)) {
|
|
516
|
+
return {
|
|
517
|
+
run: attachLegacyArtifactsToPersistedChatRun(persisted),
|
|
518
|
+
artifacts_repaired: true
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
run: persisted
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
340
526
|
function buildLegacyChatResult(snapshot) {
|
|
341
527
|
if (!snapshot) return null;
|
|
342
528
|
const artifacts = ensureChatRunArtifacts(snapshot);
|
|
@@ -791,14 +977,32 @@ async function buildNeedInputResponse({ args, missingFields, normalized }) {
|
|
|
791
977
|
};
|
|
792
978
|
}
|
|
793
979
|
|
|
794
|
-
function shouldRequestChatResume(args = {}) {
|
|
795
|
-
|
|
980
|
+
function shouldRequestChatResume(args = {}, context = {}) {
|
|
981
|
+
const action = normalizeText(args.post_action || args.action).toLowerCase();
|
|
982
|
+
if (
|
|
983
|
+
args.request_cv === false
|
|
984
|
+
|| args.request_resume === false
|
|
985
|
+
|| args.ask_cv === false
|
|
986
|
+
|| args.execute_post_action === false
|
|
987
|
+
|| args.no_request_cv === true
|
|
988
|
+
|| args.no_request_resume === true
|
|
989
|
+
|| CHAT_DISABLE_REQUEST_RESUME_ACTIONS.has(action)
|
|
990
|
+
) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
if (
|
|
796
994
|
args.request_cv === true
|
|
797
995
|
|| args.request_resume === true
|
|
798
996
|
|| args.ask_cv === true
|
|
799
997
|
|| args.execute_post_action === true
|
|
800
|
-
||
|
|
801
|
-
)
|
|
998
|
+
|| CHAT_REQUEST_RESUME_ACTIONS.has(action)
|
|
999
|
+
) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
if (typeof context.request_resume_for_passed === "boolean") {
|
|
1003
|
+
return context.request_resume_for_passed;
|
|
1004
|
+
}
|
|
1005
|
+
return true;
|
|
802
1006
|
}
|
|
803
1007
|
|
|
804
1008
|
function isDebugTestMode(args = {}) {
|
|
@@ -1300,12 +1504,15 @@ export function getBossChatRunTool({ args = {} } = {}) {
|
|
|
1300
1504
|
} catch {
|
|
1301
1505
|
const persisted = readChatRunState(runId);
|
|
1302
1506
|
if (persisted) {
|
|
1507
|
+
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1303
1508
|
return {
|
|
1304
1509
|
status: "RUN_STATUS",
|
|
1305
|
-
run:
|
|
1510
|
+
run: reconciled.run,
|
|
1306
1511
|
persistence: {
|
|
1307
1512
|
source: "disk",
|
|
1308
|
-
active_control_available: false
|
|
1513
|
+
active_control_available: false,
|
|
1514
|
+
stale_finalized: reconciled.stale_finalized === true,
|
|
1515
|
+
artifacts_repaired: reconciled.artifacts_repaired === true
|
|
1309
1516
|
},
|
|
1310
1517
|
runtime_evaluate_used: false,
|
|
1311
1518
|
method_summary: {},
|
|
@@ -1354,9 +1561,10 @@ export function pauseBossChatRunTool({ args = {} } = {}) {
|
|
|
1354
1561
|
} catch {
|
|
1355
1562
|
const persisted = readChatRunState(runId);
|
|
1356
1563
|
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1564
|
+
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1357
1565
|
return {
|
|
1358
1566
|
status: "PAUSE_IGNORED",
|
|
1359
|
-
run:
|
|
1567
|
+
run: reconciled.run,
|
|
1360
1568
|
message: "目标任务已结束,无需暂停。",
|
|
1361
1569
|
runtime_evaluate_used: false,
|
|
1362
1570
|
method_summary: {},
|
|
@@ -1412,19 +1620,23 @@ export function resumeBossChatRunTool({ args = {} } = {}) {
|
|
|
1412
1620
|
} catch {
|
|
1413
1621
|
const persisted = readChatRunState(runId);
|
|
1414
1622
|
if (persisted) {
|
|
1623
|
+
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1624
|
+
const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
|
|
1415
1625
|
return {
|
|
1416
1626
|
status: "FAILED",
|
|
1417
1627
|
error: {
|
|
1418
|
-
code: TERMINAL_STATUSES.has(
|
|
1419
|
-
message: TERMINAL_STATUSES.has(
|
|
1628
|
+
code: TERMINAL_STATUSES.has(reconciledStatus) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
|
|
1629
|
+
message: TERMINAL_STATUSES.has(reconciledStatus)
|
|
1420
1630
|
? "目标任务已结束,无法继续。"
|
|
1421
1631
|
: "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
|
|
1422
|
-
retryable: !TERMINAL_STATUSES.has(
|
|
1632
|
+
retryable: !TERMINAL_STATUSES.has(reconciledStatus)
|
|
1423
1633
|
},
|
|
1424
|
-
run:
|
|
1634
|
+
run: reconciled.run,
|
|
1425
1635
|
persistence: {
|
|
1426
1636
|
source: "disk",
|
|
1427
|
-
active_control_available: false
|
|
1637
|
+
active_control_available: false,
|
|
1638
|
+
stale_finalized: reconciled.stale_finalized === true,
|
|
1639
|
+
artifacts_repaired: reconciled.artifacts_repaired === true
|
|
1428
1640
|
},
|
|
1429
1641
|
runtime_evaluate_used: false,
|
|
1430
1642
|
method_summary: {},
|
|
@@ -1458,9 +1670,10 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
|
|
|
1458
1670
|
} catch {
|
|
1459
1671
|
const persisted = readChatRunState(runId);
|
|
1460
1672
|
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1673
|
+
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1461
1674
|
return {
|
|
1462
1675
|
status: "CANCEL_IGNORED",
|
|
1463
|
-
run:
|
|
1676
|
+
run: reconciled.run,
|
|
1464
1677
|
message: "目标任务已结束,无需取消。",
|
|
1465
1678
|
runtime_evaluate_used: false,
|
|
1466
1679
|
method_summary: {},
|
|
@@ -1468,6 +1681,26 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
|
|
|
1468
1681
|
chrome: null
|
|
1469
1682
|
};
|
|
1470
1683
|
}
|
|
1684
|
+
if (persisted) {
|
|
1685
|
+
const reconciled = reconcilePersistedChatRun(persisted, { cancelStale: true });
|
|
1686
|
+
if (reconciled.stale_finalized) {
|
|
1687
|
+
return {
|
|
1688
|
+
status: "CANCEL_REQUESTED",
|
|
1689
|
+
run: reconciled.run,
|
|
1690
|
+
message: "该 run 的后台进程已经不在,已将磁盘状态安全标记为 canceled 并生成结果文件。",
|
|
1691
|
+
persistence: {
|
|
1692
|
+
source: "disk",
|
|
1693
|
+
active_control_available: false,
|
|
1694
|
+
stale_finalized: true,
|
|
1695
|
+
artifacts_repaired: reconciled.artifacts_repaired === true
|
|
1696
|
+
},
|
|
1697
|
+
runtime_evaluate_used: false,
|
|
1698
|
+
method_summary: {},
|
|
1699
|
+
method_log: [],
|
|
1700
|
+
chrome: null
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1471
1704
|
return getBossChatRunTool({ args });
|
|
1472
1705
|
}
|
|
1473
1706
|
}
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import {
|
|
9
9
|
assertNoForbiddenCdpCalls,
|
|
10
|
+
buildBossChromeLaunchArgs,
|
|
10
11
|
bringPageToFront,
|
|
11
12
|
connectToChromeTarget,
|
|
12
13
|
enableDomains,
|
|
@@ -1994,14 +1995,7 @@ async function launchChrome(options = {}) {
|
|
|
1994
1995
|
}
|
|
1995
1996
|
|
|
1996
1997
|
const userDataDir = getChromeUserDataDir(port);
|
|
1997
|
-
const args =
|
|
1998
|
-
`--remote-debugging-port=${port}`,
|
|
1999
|
-
`--user-data-dir=${userDataDir}`,
|
|
2000
|
-
"--no-first-run",
|
|
2001
|
-
"--no-default-browser-check",
|
|
2002
|
-
"--new-window",
|
|
2003
|
-
bossUrl
|
|
2004
|
-
];
|
|
1998
|
+
const args = buildBossChromeLaunchArgs({ port, userDataDir, url: bossUrl });
|
|
2005
1999
|
const child = spawn(chromePath, args, {
|
|
2006
2000
|
detached: true,
|
|
2007
2001
|
stdio: "ignore",
|
|
@@ -7,6 +7,12 @@ import CDP from "chrome-remote-interface";
|
|
|
7
7
|
export const DEFAULT_CHROME_HOST = "127.0.0.1";
|
|
8
8
|
export const DEFAULT_CHROME_PORT = 9222;
|
|
9
9
|
export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
10
|
+
export const LID_CLOSED_SAFE_CHROME_ARGS = [
|
|
11
|
+
"--disable-backgrounding-occluded-windows",
|
|
12
|
+
"--disable-background-timer-throttling",
|
|
13
|
+
"--disable-renderer-backgrounding",
|
|
14
|
+
"--disable-features=CalculateNativeWinOcclusion"
|
|
15
|
+
];
|
|
10
16
|
|
|
11
17
|
export const ALLOWED_CDP_DOMAINS = new Set([
|
|
12
18
|
"Accessibility",
|
|
@@ -208,6 +214,33 @@ export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
|
|
|
208
214
|
return sharedPath;
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
function parseExtraChromeArgs(value = "") {
|
|
218
|
+
return String(value || "")
|
|
219
|
+
.split(/\s+/)
|
|
220
|
+
.map((item) => item.trim())
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function buildBossChromeLaunchArgs({
|
|
225
|
+
port = DEFAULT_CHROME_PORT,
|
|
226
|
+
userDataDir = "",
|
|
227
|
+
url = "about:blank",
|
|
228
|
+
extraArgs = []
|
|
229
|
+
} = {}) {
|
|
230
|
+
const args = [
|
|
231
|
+
`--remote-debugging-port=${port}`,
|
|
232
|
+
`--user-data-dir=${userDataDir}`,
|
|
233
|
+
"--no-first-run",
|
|
234
|
+
"--no-default-browser-check",
|
|
235
|
+
...LID_CLOSED_SAFE_CHROME_ARGS,
|
|
236
|
+
...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
|
|
237
|
+
...extraArgs,
|
|
238
|
+
"--new-window",
|
|
239
|
+
url
|
|
240
|
+
];
|
|
241
|
+
return Array.from(new Set(args.filter(Boolean)));
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
export async function waitForChromeDebugPort({
|
|
212
245
|
host = DEFAULT_CHROME_HOST,
|
|
213
246
|
port = DEFAULT_CHROME_PORT,
|
|
@@ -250,14 +283,7 @@ export async function launchChromeDebugInstance({
|
|
|
250
283
|
throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
|
|
251
284
|
}
|
|
252
285
|
const userDataDir = getBossChromeUserDataDir(port);
|
|
253
|
-
const args =
|
|
254
|
-
`--remote-debugging-port=${port}`,
|
|
255
|
-
`--user-data-dir=${userDataDir}`,
|
|
256
|
-
"--no-first-run",
|
|
257
|
-
"--no-default-browser-check",
|
|
258
|
-
"--new-window",
|
|
259
|
-
url
|
|
260
|
-
];
|
|
286
|
+
const args = buildBossChromeLaunchArgs({ port, userDataDir, url });
|
|
261
287
|
const child = spawn(chromePath, args, {
|
|
262
288
|
detached: true,
|
|
263
289
|
stdio: "ignore",
|
|
@@ -277,6 +303,7 @@ export async function launchChromeDebugInstance({
|
|
|
277
303
|
launched: true,
|
|
278
304
|
chrome_path: chromePath,
|
|
279
305
|
user_data_dir: userDataDir,
|
|
306
|
+
launch_args: args,
|
|
280
307
|
port,
|
|
281
308
|
url,
|
|
282
309
|
readiness: {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getAttributesMap,
|
|
7
7
|
getNodeBox,
|
|
8
8
|
getOuterHTML,
|
|
9
|
+
querySelectorAll,
|
|
9
10
|
sleep
|
|
10
11
|
} from "../browser/index.js";
|
|
11
12
|
import {
|
|
@@ -158,6 +159,185 @@ function screenshotHash(buffer) {
|
|
|
158
159
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
function createCaptureTimeoutError(label, timeoutMs) {
|
|
163
|
+
const error = new Error(`Image fallback capture timed out during ${label} after ${timeoutMs}ms`);
|
|
164
|
+
error.code = "IMAGE_CAPTURE_TIMEOUT";
|
|
165
|
+
error.capture_step = label;
|
|
166
|
+
error.timeout_ms = timeoutMs;
|
|
167
|
+
return error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function withCaptureTimeout(promise, {
|
|
171
|
+
label = "capture_step",
|
|
172
|
+
timeoutMs = 0
|
|
173
|
+
} = {}) {
|
|
174
|
+
const safeTimeout = Math.max(0, Number(timeoutMs) || 0);
|
|
175
|
+
if (!safeTimeout) return promise;
|
|
176
|
+
let timer = null;
|
|
177
|
+
try {
|
|
178
|
+
return await Promise.race([
|
|
179
|
+
promise,
|
|
180
|
+
new Promise((_, reject) => {
|
|
181
|
+
timer = setTimeout(() => reject(createCaptureTimeoutError(label, safeTimeout)), safeTimeout);
|
|
182
|
+
})
|
|
183
|
+
]);
|
|
184
|
+
} finally {
|
|
185
|
+
if (timer) clearTimeout(timer);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assertCaptureTotalBudget(started, totalTimeoutMs, label) {
|
|
190
|
+
const safeTimeout = Math.max(0, Number(totalTimeoutMs) || 0);
|
|
191
|
+
if (!safeTimeout) return;
|
|
192
|
+
const elapsed = Date.now() - started;
|
|
193
|
+
if (elapsed <= safeTimeout) return;
|
|
194
|
+
const error = createCaptureTimeoutError(label, safeTimeout);
|
|
195
|
+
error.elapsed_ms = elapsed;
|
|
196
|
+
error.code = "IMAGE_CAPTURE_TOTAL_TIMEOUT";
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const DEFAULT_SCROLL_ANCHOR_SELECTOR = [
|
|
201
|
+
"h1",
|
|
202
|
+
"h2",
|
|
203
|
+
"h3",
|
|
204
|
+
"h4",
|
|
205
|
+
"h5",
|
|
206
|
+
"p",
|
|
207
|
+
"li",
|
|
208
|
+
"section",
|
|
209
|
+
"article",
|
|
210
|
+
"table",
|
|
211
|
+
"tr",
|
|
212
|
+
"dl",
|
|
213
|
+
"dt",
|
|
214
|
+
"dd",
|
|
215
|
+
"[class*='resume']",
|
|
216
|
+
"[class*='work']",
|
|
217
|
+
"[class*='project']",
|
|
218
|
+
"[class*='education']",
|
|
219
|
+
"[class*='experience']",
|
|
220
|
+
"[class*='item']",
|
|
221
|
+
"div"
|
|
222
|
+
].join(",");
|
|
223
|
+
|
|
224
|
+
function normalizeScrollMethod(value = "dom-anchor-fallback-input") {
|
|
225
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
226
|
+
if (["dom", "dom-anchor", "dom_anchor", "anchor"].includes(normalized)) return "dom-anchor";
|
|
227
|
+
if (["dom-anchor-fallback-input", "dom_anchor_fallback_input", "dom-fallback-input"].includes(normalized)) {
|
|
228
|
+
return "dom-anchor-fallback-input";
|
|
229
|
+
}
|
|
230
|
+
return "input";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function uniqueNumbers(values = []) {
|
|
234
|
+
return Array.from(new Set(values.map((value) => Number(value) || 0).filter(Boolean)));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function pickEvenly(items = [], limit = 1) {
|
|
238
|
+
const safeLimit = Math.max(1, Number(limit) || 1);
|
|
239
|
+
if (items.length <= safeLimit) return items;
|
|
240
|
+
const picked = [];
|
|
241
|
+
const last = items.length - 1;
|
|
242
|
+
for (let index = 0; index < safeLimit; index += 1) {
|
|
243
|
+
const sourceIndex = Math.round((index * last) / Math.max(1, safeLimit - 1));
|
|
244
|
+
picked.push(items[sourceIndex]);
|
|
245
|
+
}
|
|
246
|
+
return Array.from(new Map(picked.map((item) => [item.node_id, item])).values());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function collectDomScrollAnchors(client, rootNodeId, {
|
|
250
|
+
selector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
251
|
+
maxScreenshots = 6,
|
|
252
|
+
maxProbeNodes = 260,
|
|
253
|
+
minAnchorGap = 180,
|
|
254
|
+
stepTimeoutMs = 45000
|
|
255
|
+
} = {}) {
|
|
256
|
+
const started = Date.now();
|
|
257
|
+
let nodeIds = [];
|
|
258
|
+
try {
|
|
259
|
+
nodeIds = uniqueNumbers(await querySelectorAll(client, rootNodeId, selector));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
method: "dom-anchor",
|
|
264
|
+
reason: "query_selector_all_failed",
|
|
265
|
+
error: error?.message || String(error)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (!nodeIds.length) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
method: "dom-anchor",
|
|
272
|
+
reason: "no_anchor_nodes"
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const probeLimit = Math.max(1, Number(maxProbeNodes) || 260);
|
|
277
|
+
const perNodeTimeoutMs = Math.min(1200, Math.max(250, Math.floor((Number(stepTimeoutMs) || 45000) / 30)));
|
|
278
|
+
const measured = [];
|
|
279
|
+
for (const nodeId of nodeIds.slice(0, probeLimit)) {
|
|
280
|
+
try {
|
|
281
|
+
const box = await withCaptureTimeout(getNodeBox(client, nodeId), {
|
|
282
|
+
label: `anchor_box_${nodeId}`,
|
|
283
|
+
timeoutMs: perNodeTimeoutMs
|
|
284
|
+
});
|
|
285
|
+
const rect = box?.rect || {};
|
|
286
|
+
if ((Number(rect.width) || 0) < 80 || (Number(rect.height) || 0) < 8) continue;
|
|
287
|
+
measured.push({
|
|
288
|
+
node_id: nodeId,
|
|
289
|
+
y: Math.round(Number(rect.y) || 0),
|
|
290
|
+
height: Math.round(Number(rect.height) || 0)
|
|
291
|
+
});
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let anchors = [];
|
|
296
|
+
if (measured.length) {
|
|
297
|
+
const sorted = measured.sort((a, b) => a.y - b.y);
|
|
298
|
+
for (const item of sorted) {
|
|
299
|
+
const last = anchors[anchors.length - 1];
|
|
300
|
+
if (!last || Math.abs(item.y - last.y) >= Math.max(40, Number(minAnchorGap) || 180)) {
|
|
301
|
+
anchors.push(item);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (anchors.length < 2) {
|
|
307
|
+
anchors = nodeIds.slice(0, probeLimit).map((nodeId, index) => ({
|
|
308
|
+
node_id: nodeId,
|
|
309
|
+
y: null,
|
|
310
|
+
height: null,
|
|
311
|
+
document_order: index
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
anchors = pickEvenly(anchors, Math.max(1, Number(maxScreenshots) || 1));
|
|
316
|
+
return {
|
|
317
|
+
ok: anchors.length > 0,
|
|
318
|
+
method: "dom-anchor",
|
|
319
|
+
elapsed_ms: Date.now() - started,
|
|
320
|
+
selector,
|
|
321
|
+
discovered_node_count: nodeIds.length,
|
|
322
|
+
measured_node_count: measured.length,
|
|
323
|
+
anchor_count: anchors.length,
|
|
324
|
+
anchors
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function scrollDomAnchorIntoView(client, nodeId, {
|
|
329
|
+
timeoutMs = 10000,
|
|
330
|
+
label = "dom_scroll_anchor"
|
|
331
|
+
} = {}) {
|
|
332
|
+
if (client.DOM && typeof client.DOM.scrollIntoViewIfNeeded === "function") {
|
|
333
|
+
return withCaptureTimeout(client.DOM.scrollIntoViewIfNeeded({ nodeId }), { label, timeoutMs });
|
|
334
|
+
}
|
|
335
|
+
if (typeof client.send === "function") {
|
|
336
|
+
return withCaptureTimeout(client.send("DOM.scrollIntoViewIfNeeded", { nodeId }), { label, timeoutMs });
|
|
337
|
+
}
|
|
338
|
+
throw new Error("CDP client does not expose DOM.scrollIntoViewIfNeeded");
|
|
339
|
+
}
|
|
340
|
+
|
|
161
341
|
async function optimizeScreenshotBuffer(buffer, {
|
|
162
342
|
enabled = false,
|
|
163
343
|
format = "png",
|
|
@@ -339,20 +519,83 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
339
519
|
llmPagesPerImage = 3,
|
|
340
520
|
llmResizeMaxWidth = 1100,
|
|
341
521
|
llmQuality = 72,
|
|
522
|
+
stepTimeoutMs = 45000,
|
|
523
|
+
totalTimeoutMs = 90000,
|
|
524
|
+
scrollMethod = "dom-anchor-fallback-input",
|
|
525
|
+
scrollAnchorSelector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
|
|
526
|
+
scrollAnchorMaxProbeNodes = 260,
|
|
527
|
+
scrollAnchorMinGap = 180,
|
|
342
528
|
metadata = {}
|
|
343
529
|
} = {}) {
|
|
344
530
|
if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
|
|
345
531
|
const sequenceStarted = Date.now();
|
|
532
|
+
const normalizedScrollMethod = normalizeScrollMethod(scrollMethod);
|
|
533
|
+
const maxScreenshotCount = Math.max(1, Number(maxScreenshots) || 1);
|
|
534
|
+
const anchorPlan = normalizedScrollMethod !== "input"
|
|
535
|
+
? await collectDomScrollAnchors(client, nodeId, {
|
|
536
|
+
selector: scrollAnchorSelector,
|
|
537
|
+
maxScreenshots: maxScreenshotCount,
|
|
538
|
+
maxProbeNodes: scrollAnchorMaxProbeNodes,
|
|
539
|
+
minAnchorGap: scrollAnchorMinGap,
|
|
540
|
+
stepTimeoutMs
|
|
541
|
+
})
|
|
542
|
+
: null;
|
|
346
543
|
const screenshots = [];
|
|
347
544
|
let consecutiveDuplicates = 0;
|
|
348
545
|
let previousHash = "";
|
|
349
546
|
let captureCount = 0;
|
|
350
547
|
let droppedDuplicateCount = 0;
|
|
548
|
+
let currentScrollMetadata = {
|
|
549
|
+
before_capture: "initial",
|
|
550
|
+
method: normalizedScrollMethod,
|
|
551
|
+
anchor_plan: anchorPlan
|
|
552
|
+
? {
|
|
553
|
+
ok: Boolean(anchorPlan.ok),
|
|
554
|
+
reason: anchorPlan.reason || null,
|
|
555
|
+
discovered_node_count: anchorPlan.discovered_node_count || 0,
|
|
556
|
+
measured_node_count: anchorPlan.measured_node_count || 0,
|
|
557
|
+
anchor_count: anchorPlan.anchor_count || 0,
|
|
558
|
+
elapsed_ms: anchorPlan.elapsed_ms || 0
|
|
559
|
+
}
|
|
560
|
+
: null
|
|
561
|
+
};
|
|
351
562
|
|
|
352
|
-
|
|
563
|
+
if (anchorPlan?.anchors?.[0]?.node_id && normalizedScrollMethod !== "input") {
|
|
564
|
+
try {
|
|
565
|
+
await scrollDomAnchorIntoView(client, anchorPlan.anchors[0].node_id, {
|
|
566
|
+
label: "scroll_dom_anchor_initial",
|
|
567
|
+
timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
|
|
568
|
+
});
|
|
569
|
+
currentScrollMetadata = {
|
|
570
|
+
before_capture: "dom_anchor_initial",
|
|
571
|
+
method: "DOM.scrollIntoViewIfNeeded",
|
|
572
|
+
anchor_node_id: anchorPlan.anchors[0].node_id,
|
|
573
|
+
anchor_y: anchorPlan.anchors[0].y,
|
|
574
|
+
anchor_height: anchorPlan.anchors[0].height,
|
|
575
|
+
anchor_plan: currentScrollMetadata.anchor_plan
|
|
576
|
+
};
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (normalizedScrollMethod === "dom-anchor") {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
currentScrollMetadata = {
|
|
582
|
+
before_capture: "dom_anchor_initial_failed",
|
|
583
|
+
method: "DOM.scrollIntoViewIfNeeded",
|
|
584
|
+
anchor_node_id: anchorPlan.anchors[0].node_id,
|
|
585
|
+
error: error?.message || String(error),
|
|
586
|
+
anchor_plan: currentScrollMetadata.anchor_plan
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
for (let index = 0; index < maxScreenshotCount; index += 1) {
|
|
592
|
+
assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `capture_page_${index + 1}`);
|
|
353
593
|
captureCount += 1;
|
|
354
594
|
const captureStarted = Date.now();
|
|
355
|
-
const box = await getNodeBox(client, nodeId)
|
|
595
|
+
const box = await withCaptureTimeout(getNodeBox(client, nodeId), {
|
|
596
|
+
label: `get_box_${index + 1}`,
|
|
597
|
+
timeoutMs: stepTimeoutMs
|
|
598
|
+
});
|
|
356
599
|
const clip = withPadding(box.rect, padding);
|
|
357
600
|
const captureOptions = captureViewport ? {
|
|
358
601
|
format,
|
|
@@ -367,13 +610,19 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
367
610
|
if (quality != null) {
|
|
368
611
|
captureOptions.quality = quality;
|
|
369
612
|
}
|
|
370
|
-
const screenshot = await client.Page.captureScreenshot(captureOptions)
|
|
613
|
+
const screenshot = await withCaptureTimeout(client.Page.captureScreenshot(captureOptions), {
|
|
614
|
+
label: `capture_screenshot_${index + 1}`,
|
|
615
|
+
timeoutMs: stepTimeoutMs
|
|
616
|
+
});
|
|
371
617
|
const originalBuffer = Buffer.from(screenshot.data || "", "base64");
|
|
372
|
-
const optimized = await optimizeScreenshotBuffer(originalBuffer, {
|
|
618
|
+
const optimized = await withCaptureTimeout(optimizeScreenshotBuffer(originalBuffer, {
|
|
373
619
|
enabled: optimize,
|
|
374
620
|
format,
|
|
375
621
|
quality,
|
|
376
622
|
resizeMaxWidth
|
|
623
|
+
}), {
|
|
624
|
+
label: `optimize_screenshot_${index + 1}`,
|
|
625
|
+
timeoutMs: stepTimeoutMs
|
|
377
626
|
});
|
|
378
627
|
const buffer = optimized.buffer;
|
|
379
628
|
const hash = screenshotHash(buffer);
|
|
@@ -412,9 +661,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
412
661
|
clip,
|
|
413
662
|
capture_viewport: Boolean(captureViewport),
|
|
414
663
|
node_rect: box.rect,
|
|
415
|
-
scroll:
|
|
416
|
-
? { before_capture: "initial" }
|
|
417
|
-
: { before_capture: `wheel_down_${index}` },
|
|
664
|
+
scroll: currentScrollMetadata,
|
|
418
665
|
metadata
|
|
419
666
|
});
|
|
420
667
|
}
|
|
@@ -424,27 +671,75 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
424
671
|
break;
|
|
425
672
|
}
|
|
426
673
|
|
|
427
|
-
if (index <
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
674
|
+
if (index < maxScreenshotCount - 1) {
|
|
675
|
+
assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `scroll_after_page_${index + 1}`);
|
|
676
|
+
let scrolledByDomAnchor = false;
|
|
677
|
+
const nextAnchor = anchorPlan?.anchors?.[index + 1] || null;
|
|
678
|
+
if (nextAnchor?.node_id && normalizedScrollMethod !== "input") {
|
|
679
|
+
try {
|
|
680
|
+
await scrollDomAnchorIntoView(client, nextAnchor.node_id, {
|
|
681
|
+
label: `scroll_dom_anchor_${index + 1}`,
|
|
682
|
+
timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
|
|
683
|
+
});
|
|
684
|
+
scrolledByDomAnchor = true;
|
|
685
|
+
currentScrollMetadata = {
|
|
686
|
+
before_capture: `dom_anchor_${index + 1}`,
|
|
687
|
+
method: "DOM.scrollIntoViewIfNeeded",
|
|
688
|
+
anchor_node_id: nextAnchor.node_id,
|
|
689
|
+
anchor_y: nextAnchor.y,
|
|
690
|
+
anchor_height: nextAnchor.height
|
|
691
|
+
};
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (normalizedScrollMethod === "dom-anchor") {
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
currentScrollMetadata = {
|
|
697
|
+
before_capture: `dom_anchor_${index + 1}_failed`,
|
|
698
|
+
method: "DOM.scrollIntoViewIfNeeded",
|
|
699
|
+
anchor_node_id: nextAnchor.node_id,
|
|
700
|
+
error: error?.message || String(error)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
} else if (normalizedScrollMethod === "dom-anchor") {
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (!scrolledByDomAnchor && normalizedScrollMethod !== "dom-anchor") {
|
|
708
|
+
const x = box.center.x;
|
|
709
|
+
const y = box.center.y;
|
|
710
|
+
await withCaptureTimeout(client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" }), {
|
|
711
|
+
label: `scroll_mouse_move_${index + 1}`,
|
|
712
|
+
timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
|
|
713
|
+
});
|
|
714
|
+
await withCaptureTimeout(client.Input.dispatchMouseEvent({
|
|
715
|
+
type: "mouseWheel",
|
|
716
|
+
x,
|
|
717
|
+
y,
|
|
718
|
+
deltaX: 0,
|
|
719
|
+
deltaY: Math.max(1, Number(wheelDeltaY) || 650)
|
|
720
|
+
}), {
|
|
721
|
+
label: `scroll_wheel_${index + 1}`,
|
|
722
|
+
timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
|
|
723
|
+
});
|
|
724
|
+
currentScrollMetadata = {
|
|
725
|
+
before_capture: `wheel_down_${index + 1}`,
|
|
726
|
+
method: "Input.dispatchMouseEvent",
|
|
727
|
+
fallback_from_dom_anchor: Boolean(anchorPlan && normalizedScrollMethod === "dom-anchor-fallback-input")
|
|
728
|
+
};
|
|
729
|
+
}
|
|
438
730
|
if (settleMs > 0) await sleep(settleMs);
|
|
439
731
|
}
|
|
440
732
|
}
|
|
441
733
|
|
|
442
734
|
const llmComposition = composeForLlm
|
|
443
|
-
? await composeScreenshotsForLlm(screenshots, {
|
|
735
|
+
? await withCaptureTimeout(composeScreenshotsForLlm(screenshots, {
|
|
444
736
|
basePath: filePath,
|
|
445
737
|
pagesPerImage: llmPagesPerImage,
|
|
446
738
|
resizeMaxWidth: llmResizeMaxWidth,
|
|
447
739
|
quality: llmQuality
|
|
740
|
+
}), {
|
|
741
|
+
label: "compose_llm_screenshots",
|
|
742
|
+
timeoutMs: stepTimeoutMs
|
|
448
743
|
})
|
|
449
744
|
: {
|
|
450
745
|
llm_file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
@@ -456,6 +751,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
456
751
|
|
|
457
752
|
return {
|
|
458
753
|
schema_version: 1,
|
|
754
|
+
ok: true,
|
|
459
755
|
source: "image-scroll-sequence",
|
|
460
756
|
captured_at: nowIso(),
|
|
461
757
|
node_id: nodeId,
|
|
@@ -482,8 +778,15 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
482
778
|
llm_compose_enabled: Boolean(composeForLlm),
|
|
483
779
|
llm_pages_per_image: Math.max(1, Math.min(5, Number(llmPagesPerImage) || 3)),
|
|
484
780
|
llm_resize_max_width: Math.max(0, Number(llmResizeMaxWidth) || 0),
|
|
485
|
-
llm_quality: llmQuality ?? null
|
|
781
|
+
llm_quality: llmQuality ?? null,
|
|
782
|
+
step_timeout_ms: Math.max(0, Number(stepTimeoutMs) || 0),
|
|
783
|
+
total_timeout_ms: Math.max(0, Number(totalTimeoutMs) || 0),
|
|
784
|
+
scroll_method: normalizedScrollMethod,
|
|
785
|
+
scroll_anchor_selector: scrollAnchorSelector,
|
|
786
|
+
scroll_anchor_max_probe_nodes: Math.max(1, Number(scrollAnchorMaxProbeNodes) || 260),
|
|
787
|
+
scroll_anchor_min_gap: Math.max(0, Number(scrollAnchorMinGap) || 0)
|
|
486
788
|
},
|
|
789
|
+
scroll_anchor_plan: anchorPlan,
|
|
487
790
|
file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
488
791
|
screenshots,
|
|
489
792
|
metadata
|
|
@@ -124,6 +124,7 @@ export function hasParsedNetworkProfile(detailResult = {}) {
|
|
|
124
124
|
export function summarizeImageEvidence(imageEvidence = null) {
|
|
125
125
|
if (!imageEvidence) return null;
|
|
126
126
|
return {
|
|
127
|
+
ok: imageEvidence.ok !== false,
|
|
127
128
|
source: imageEvidence.source || "",
|
|
128
129
|
elapsed_ms: imageEvidence.elapsed_ms || 0,
|
|
129
130
|
capture_count: imageEvidence.capture_count || imageEvidence.screenshot_count || 0,
|
|
@@ -137,6 +138,8 @@ export function summarizeImageEvidence(imageEvidence = null) {
|
|
|
137
138
|
llm_original_total_byte_length: imageEvidence.llm_original_total_byte_length || 0,
|
|
138
139
|
llm_composition_error: imageEvidence.llm_composition_error || null,
|
|
139
140
|
optimization: imageEvidence.optimization || null,
|
|
141
|
+
error_code: imageEvidence.error_code || imageEvidence.code || null,
|
|
142
|
+
error: imageEvidence.error || null,
|
|
140
143
|
file_paths: imageEvidence.file_paths || [],
|
|
141
144
|
llm_file_paths: imageEvidence.llm_file_paths || [],
|
|
142
145
|
first_clip: imageEvidence.screenshots?.[0]?.clip || imageEvidence.clip || null
|
package/src/domains/chat/jobs.js
CHANGED
|
@@ -223,6 +223,15 @@ function matchJobOption(option, jobLabel = "") {
|
|
|
223
223
|
));
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
function activeMatchingJobOption(options = [], jobLabel = "") {
|
|
227
|
+
return (options || []).find((option) => option.active && matchJobOption(option, jobLabel)) || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function selectedLabelMatches(label = "", jobLabel = "") {
|
|
231
|
+
const normalized = normalizeJobText(label);
|
|
232
|
+
return Boolean(normalized && matchJobOption({ label: normalized, value: normalized, title: normalized }, jobLabel));
|
|
233
|
+
}
|
|
234
|
+
|
|
226
235
|
async function clickFirstVisible(client, rootNodeId, selectors = []) {
|
|
227
236
|
for (const selector of selectors) {
|
|
228
237
|
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
@@ -247,6 +256,62 @@ async function clickFirstVisible(client, rootNodeId, selectors = []) {
|
|
|
247
256
|
};
|
|
248
257
|
}
|
|
249
258
|
|
|
259
|
+
async function waitForChatJobOptions(client, rootNodeId, {
|
|
260
|
+
timeoutMs = 12000,
|
|
261
|
+
intervalMs = 300,
|
|
262
|
+
requireVisible = false
|
|
263
|
+
} = {}) {
|
|
264
|
+
const started = Date.now();
|
|
265
|
+
let latest = null;
|
|
266
|
+
while (Date.now() - started <= timeoutMs) {
|
|
267
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
268
|
+
latest = await readChatJobOptions(client, currentRootNodeId, {
|
|
269
|
+
timeoutMs: Math.min(intervalMs, 300),
|
|
270
|
+
intervalMs
|
|
271
|
+
});
|
|
272
|
+
const options = latest.job_options || [];
|
|
273
|
+
if (options.length && (!requireVisible || options.some((option) => option.visible))) {
|
|
274
|
+
return latest;
|
|
275
|
+
}
|
|
276
|
+
await sleep(intervalMs);
|
|
277
|
+
}
|
|
278
|
+
return latest || {
|
|
279
|
+
selector: "",
|
|
280
|
+
source: "chat-job-list",
|
|
281
|
+
selected_label: "",
|
|
282
|
+
job_options: []
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function waitForSelectedChatJob(client, rootNodeId, jobLabel = "", {
|
|
287
|
+
timeoutMs = 5000,
|
|
288
|
+
intervalMs = 300
|
|
289
|
+
} = {}) {
|
|
290
|
+
const started = Date.now();
|
|
291
|
+
let latest = null;
|
|
292
|
+
while (Date.now() - started <= timeoutMs) {
|
|
293
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
294
|
+
latest = await readChatJobOptions(client, currentRootNodeId, {
|
|
295
|
+
timeoutMs: Math.min(intervalMs, 300),
|
|
296
|
+
intervalMs
|
|
297
|
+
});
|
|
298
|
+
if (
|
|
299
|
+
selectedLabelMatches(latest.selected_label, jobLabel)
|
|
300
|
+
|| activeMatchingJobOption(latest.job_options || [], jobLabel)
|
|
301
|
+
) {
|
|
302
|
+
return {
|
|
303
|
+
verified: true,
|
|
304
|
+
result: latest
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
await sleep(intervalMs);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
verified: false,
|
|
311
|
+
result: latest
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
250
315
|
export async function selectChatJob(client, rootNodeId, {
|
|
251
316
|
jobLabel = "",
|
|
252
317
|
timeoutMs = 12000,
|
|
@@ -267,14 +332,34 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
267
332
|
intervalMs
|
|
268
333
|
});
|
|
269
334
|
let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
335
|
+
if (
|
|
336
|
+
matched
|
|
337
|
+
&& (
|
|
338
|
+
matched.active
|
|
339
|
+
|| selectedLabelMatches(optionsResult.selected_label, matched.label)
|
|
340
|
+
|| selectedLabelMatches(optionsResult.selected_label, requested)
|
|
341
|
+
)
|
|
342
|
+
) {
|
|
343
|
+
return {
|
|
344
|
+
selected: true,
|
|
345
|
+
verified: true,
|
|
346
|
+
already_current: true,
|
|
347
|
+
requested,
|
|
348
|
+
selected_option: matched,
|
|
349
|
+
options: optionsResult.job_options || [],
|
|
350
|
+
selected_label: optionsResult.selected_label || matched.label
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
270
354
|
if (!matched || !matched.visible) {
|
|
271
355
|
const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
272
356
|
const trigger = await clickFirstVisible(client, triggerRootNodeId, CHAT_JOB_TRIGGER_SELECTORS);
|
|
273
357
|
if (settleMs > 0) await sleep(settleMs);
|
|
274
358
|
currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
|
|
275
|
-
optionsResult = await
|
|
359
|
+
optionsResult = await waitForChatJobOptions(client, currentRootNodeId, {
|
|
276
360
|
timeoutMs,
|
|
277
|
-
intervalMs
|
|
361
|
+
intervalMs,
|
|
362
|
+
requireVisible: true
|
|
278
363
|
});
|
|
279
364
|
matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
280
365
|
if (!matched || !matched.visible) {
|
|
@@ -292,6 +377,7 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
292
377
|
if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
|
|
293
378
|
return {
|
|
294
379
|
selected: true,
|
|
380
|
+
verified: true,
|
|
295
381
|
already_current: true,
|
|
296
382
|
requested,
|
|
297
383
|
selected_option: matched,
|
|
@@ -310,22 +396,27 @@ export async function selectChatJob(client, rootNodeId, {
|
|
|
310
396
|
if (settleMs > 0) await sleep(settleMs);
|
|
311
397
|
|
|
312
398
|
const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
313
|
-
const
|
|
314
|
-
timeoutMs: Math.min(timeoutMs,
|
|
399
|
+
const verification = await waitForSelectedChatJob(client, afterRootNodeId, matched.label, {
|
|
400
|
+
timeoutMs: Math.min(timeoutMs, 5000),
|
|
315
401
|
intervalMs
|
|
316
402
|
});
|
|
403
|
+
const after = verification.result || {
|
|
404
|
+
selected_label: "",
|
|
405
|
+
job_options: []
|
|
406
|
+
};
|
|
317
407
|
const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
|
|
318
|
-
const selectedLabel = normalizeJobText(after.selected_label ||
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
: true;
|
|
408
|
+
const selectedLabel = normalizeJobText(after.selected_label || "");
|
|
409
|
+
const activeMatch = activeMatchingJobOption(after.job_options || [], matched.label);
|
|
410
|
+
const verified = Boolean(verification.verified || selectedLabelMatches(selectedLabel, matched.label) || activeMatch);
|
|
322
411
|
|
|
323
412
|
return {
|
|
324
|
-
selected:
|
|
413
|
+
selected: verified,
|
|
325
414
|
verified,
|
|
326
415
|
already_current: false,
|
|
416
|
+
reason: verified ? "verified" : "job_selection_not_verified",
|
|
327
417
|
requested,
|
|
328
418
|
selected_option: afterMatch,
|
|
419
|
+
active_option: activeMatch,
|
|
329
420
|
options: after.job_options || optionsResult.job_options || [],
|
|
330
421
|
selected_label: selectedLabel,
|
|
331
422
|
before: optionsResult,
|
|
@@ -306,6 +306,9 @@ async function setupChatRunContext(client, {
|
|
|
306
306
|
if (normalizeText(job) && !jobSelection.selected) {
|
|
307
307
|
throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
|
|
308
308
|
}
|
|
309
|
+
if (normalizeText(job) && jobSelection.verified !== true) {
|
|
310
|
+
throw new Error(`Chat job selection was not verified: requested=${jobSelection.requested || job}; selected=${jobSelection.selected_label || "unknown"}`);
|
|
311
|
+
}
|
|
309
312
|
rootState = await getChatRoots(client);
|
|
310
313
|
if (ensureViewport) {
|
|
311
314
|
rootState = await ensureViewport(rootState, "context_job");
|
|
@@ -838,6 +841,9 @@ export async function runChatWorkflow({
|
|
|
838
841
|
maxScreenshots: maxImagePages,
|
|
839
842
|
wheelDeltaY: imageWheelDeltaY,
|
|
840
843
|
settleMs: 350,
|
|
844
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
845
|
+
stepTimeoutMs: 45000,
|
|
846
|
+
totalTimeoutMs: 90000,
|
|
841
847
|
duplicateStopCount: 1,
|
|
842
848
|
skipDuplicateScreenshots: true,
|
|
843
849
|
composeForLlm: true,
|
|
@@ -1174,6 +1180,8 @@ export function createChatRunService({
|
|
|
1174
1180
|
detail_limit: normalizedDetailLimit,
|
|
1175
1181
|
detail_source: normalizedDetailSource,
|
|
1176
1182
|
close_resume: closeResume,
|
|
1183
|
+
request_resume_for_passed: Boolean(requestResumeForPassed),
|
|
1184
|
+
dry_run_request_cv: Boolean(dryRunRequestCv),
|
|
1177
1185
|
cv_acquisition_mode: cvAcquisitionMode,
|
|
1178
1186
|
call_llm_on_image: Boolean(callLlmOnImage),
|
|
1179
1187
|
screening_mode: normalizedScreeningMode,
|
|
@@ -361,6 +361,18 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
361
361
|
};
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
function countPassedResults(results = []) {
|
|
365
|
+
return results.filter((item) => item?.screening?.passed).length;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
369
|
+
if (!error) return null;
|
|
370
|
+
return {
|
|
371
|
+
code: error.code || fallbackCode,
|
|
372
|
+
message: error.message || String(error)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
364
376
|
export async function runRecommendWorkflow({
|
|
365
377
|
client,
|
|
366
378
|
targetUrl = "",
|
|
@@ -407,9 +419,9 @@ export async function runRecommendWorkflow({
|
|
|
407
419
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
408
420
|
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
409
421
|
const postActionEnabled = normalizedPostAction !== "none";
|
|
410
|
-
const
|
|
411
|
-
const detailCountLimit = detailLimit == null ?
|
|
412
|
-
const effectiveDetailLimit = postActionEnabled ?
|
|
422
|
+
const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
|
|
423
|
+
const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
|
|
424
|
+
const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
|
|
413
425
|
const networkRecorder = effectiveDetailLimit > 0
|
|
414
426
|
? createRecommendDetailNetworkRecorder(client)
|
|
415
427
|
: null;
|
|
@@ -522,7 +534,8 @@ export async function runRecommendWorkflow({
|
|
|
522
534
|
|
|
523
535
|
runControl.updateProgress({
|
|
524
536
|
card_count: cardNodeIds.length,
|
|
525
|
-
target_count:
|
|
537
|
+
target_count: targetPassCount,
|
|
538
|
+
target_count_semantics: "passed_candidates",
|
|
526
539
|
processed: 0,
|
|
527
540
|
screened: 0,
|
|
528
541
|
detail_opened: 0,
|
|
@@ -539,7 +552,7 @@ export async function runRecommendWorkflow({
|
|
|
539
552
|
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
540
553
|
});
|
|
541
554
|
|
|
542
|
-
while (results
|
|
555
|
+
while (countPassedResults(results) < targetPassCount) {
|
|
543
556
|
const candidateStarted = Date.now();
|
|
544
557
|
const timings = {};
|
|
545
558
|
await runControl.waitIfPaused();
|
|
@@ -587,7 +600,7 @@ export async function runRecommendWorkflow({
|
|
|
587
600
|
if (
|
|
588
601
|
(nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
|
|
589
602
|
&& refreshOnEnd
|
|
590
|
-
&& results
|
|
603
|
+
&& countPassedResults(results) < targetPassCount
|
|
591
604
|
&& refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
|
|
592
605
|
) {
|
|
593
606
|
await runControl.waitIfPaused();
|
|
@@ -613,7 +626,8 @@ export async function runRecommendWorkflow({
|
|
|
613
626
|
});
|
|
614
627
|
runControl.updateProgress({
|
|
615
628
|
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
616
|
-
target_count:
|
|
629
|
+
target_count: targetPassCount,
|
|
630
|
+
target_count_semantics: "passed_candidates",
|
|
617
631
|
processed: results.length,
|
|
618
632
|
screened: results.length,
|
|
619
633
|
detail_opened: results.filter((item) => item.detail).length,
|
|
@@ -734,6 +748,9 @@ export async function runRecommendWorkflow({
|
|
|
734
748
|
maxScreenshots: maxImagePages,
|
|
735
749
|
wheelDeltaY: imageWheelDeltaY,
|
|
736
750
|
settleMs: 350,
|
|
751
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
752
|
+
stepTimeoutMs: 45000,
|
|
753
|
+
totalTimeoutMs: 90000,
|
|
737
754
|
duplicateStopCount: 1,
|
|
738
755
|
skipDuplicateScreenshots: true,
|
|
739
756
|
composeForLlm: true,
|
|
@@ -859,7 +876,8 @@ export async function runRecommendWorkflow({
|
|
|
859
876
|
|
|
860
877
|
runControl.updateProgress({
|
|
861
878
|
card_count: cardNodeIds.length,
|
|
862
|
-
target_count:
|
|
879
|
+
target_count: targetPassCount,
|
|
880
|
+
target_count_semantics: "passed_candidates",
|
|
863
881
|
processed: results.length,
|
|
864
882
|
screened: results.length,
|
|
865
883
|
detail_opened: results.filter((item) => item.detail).length,
|
|
@@ -989,7 +1007,7 @@ export function createRecommendRunService({
|
|
|
989
1007
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
990
1008
|
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
991
1009
|
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
992
|
-
const normalizedDetailLimit = detailLimit == null ?
|
|
1010
|
+
const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
|
|
993
1011
|
return manager.startRun({
|
|
994
1012
|
name,
|
|
995
1013
|
context: {
|
|
@@ -1001,6 +1019,7 @@ export function createRecommendRunService({
|
|
|
1001
1019
|
fallback_page_scope: normalizedFallbackPageScope,
|
|
1002
1020
|
filter: normalizedFilter,
|
|
1003
1021
|
max_candidates: maxCandidates,
|
|
1022
|
+
max_candidates_semantics: "passed_candidates",
|
|
1004
1023
|
detail_limit: normalizedDetailLimit,
|
|
1005
1024
|
close_detail: closeDetail,
|
|
1006
1025
|
cv_acquisition_mode: cvAcquisitionMode,
|
|
@@ -1029,6 +1048,7 @@ export function createRecommendRunService({
|
|
|
1029
1048
|
progress: {
|
|
1030
1049
|
card_count: 0,
|
|
1031
1050
|
target_count: candidateLimit,
|
|
1051
|
+
target_count_semantics: "passed_candidates",
|
|
1032
1052
|
processed: 0,
|
|
1033
1053
|
screened: 0,
|
|
1034
1054
|
detail_opened: 0,
|
|
@@ -450,6 +450,9 @@ export async function runRecruitWorkflow({
|
|
|
450
450
|
maxScreenshots: maxImagePages,
|
|
451
451
|
wheelDeltaY: imageWheelDeltaY,
|
|
452
452
|
settleMs: 350,
|
|
453
|
+
scrollMethod: "dom-anchor-fallback-input",
|
|
454
|
+
stepTimeoutMs: 45000,
|
|
455
|
+
totalTimeoutMs: 90000,
|
|
453
456
|
duplicateStopCount: 1,
|
|
454
457
|
skipDuplicateScreenshots: true,
|
|
455
458
|
composeForLlm: true,
|