@reconcrap/boss-recommend-mcp 2.0.7 → 2.0.8
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 +1 -0
- package/src/core/boss-cards/index.js +199 -0
- package/src/core/capture/index.js +4 -0
- package/src/core/cv-acquisition/index.js +1 -0
- package/src/core/reporting/legacy-csv.js +10 -1
- package/src/core/run/timing.js +33 -0
- package/src/core/screening/index.js +47 -5
- package/src/domains/chat/cards.js +9 -1
- package/src/domains/chat/run-service.js +55 -25
- package/src/domains/recommend/cards.js +16 -1
- package/src/domains/recommend/detail.js +11 -1
- package/src/domains/recommend/run-service.js +47 -13
- package/src/domains/recruit/cards.js +9 -1
- package/src/domains/recruit/detail.js +12 -1
- package/src/domains/recruit/run-service.js +45 -13
- package/src/recommend-mcp.js +1 -0
- package/src/recruit-mcp.js +1 -0
package/package.json
CHANGED
package/src/chat-mcp.js
CHANGED
|
@@ -872,6 +872,7 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
|
|
|
872
872
|
listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
|
|
873
873
|
listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
|
|
874
874
|
listFallbackPoint: null,
|
|
875
|
+
imageOutputDir: resolveBossConfiguredOutputDir("", getChatRunsDir()),
|
|
875
876
|
name: "mcp-boss-chat-run"
|
|
876
877
|
};
|
|
877
878
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { htmlToText, normalizeText } from "../screening/index.js";
|
|
2
|
+
|
|
3
|
+
function uniqueTexts(values = []) {
|
|
4
|
+
return Array.from(new Set(values.map((value) => normalizeText(value)).filter(Boolean)));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function classList(value = "") {
|
|
8
|
+
return String(value || "").split(/\s+/).map((item) => item.trim()).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hasAllClasses(classValue = "", requiredClasses = []) {
|
|
12
|
+
const classes = classList(classValue);
|
|
13
|
+
return requiredClasses.every((required) => classes.includes(required));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findClassAttributeIndex(html = "", requiredClasses = [], startIndex = 0) {
|
|
17
|
+
const regex = /class=(["'])(.*?)\1/gi;
|
|
18
|
+
regex.lastIndex = Math.max(0, Number(startIndex) || 0);
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = regex.exec(String(html || "")))) {
|
|
21
|
+
if (hasAllClasses(match[2], requiredClasses)) return match.index;
|
|
22
|
+
}
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sectionByClasses(html = "", startClasses = [], endClassGroups = []) {
|
|
27
|
+
const source = String(html || "");
|
|
28
|
+
const classIndex = findClassAttributeIndex(source, startClasses);
|
|
29
|
+
if (classIndex < 0) return "";
|
|
30
|
+
const start = Math.max(0, source.lastIndexOf("<", classIndex));
|
|
31
|
+
let end = source.length;
|
|
32
|
+
for (const group of endClassGroups) {
|
|
33
|
+
const found = findClassAttributeIndex(source, group, classIndex + 1);
|
|
34
|
+
if (found >= 0) {
|
|
35
|
+
const tagStart = source.lastIndexOf("<", found);
|
|
36
|
+
end = Math.min(end, tagStart >= 0 ? tagStart : found);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return source.slice(start, end);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function textFromHtmlFragment(fragment = "") {
|
|
43
|
+
return normalizeText(htmlToText(fragment).replace(/\n+/g, " "));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stripNameSuffixes(value = "") {
|
|
47
|
+
return normalizeText(value)
|
|
48
|
+
.replace(/\s*(在线|刚刚活跃|今日活跃|本周活跃|本月活跃)$/u, "")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractFirstSpanWithClass(html = "", className = "") {
|
|
53
|
+
const regex = /<span\b[^>]*class=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/span>/gi;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = regex.exec(String(html || "")))) {
|
|
56
|
+
if (classList(match[2]).includes(className)) {
|
|
57
|
+
return textFromHtmlFragment(match[3]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractSpanTexts(fragment = "") {
|
|
64
|
+
const values = [];
|
|
65
|
+
const regex = /<span\b[^>]*>([\s\S]*?)<\/span>/gi;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = regex.exec(String(fragment || "")))) {
|
|
68
|
+
values.push(textFromHtmlFragment(match[1]));
|
|
69
|
+
}
|
|
70
|
+
return uniqueTexts(values);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractDivTextsWithClasses(fragment = "", requiredClasses = []) {
|
|
74
|
+
const values = [];
|
|
75
|
+
const regex = /<div\b[^>]*class=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/div>/gi;
|
|
76
|
+
let match;
|
|
77
|
+
while ((match = regex.exec(String(fragment || "")))) {
|
|
78
|
+
if (hasAllClasses(match[2], requiredClasses)) {
|
|
79
|
+
values.push(extractSpanTexts(match[3]));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return values.filter((items) => items.length);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseAgeValue(value = "") {
|
|
86
|
+
const match = normalizeText(value).match(/^(\d{2})岁$/u);
|
|
87
|
+
if (!match) return null;
|
|
88
|
+
const age = Number.parseInt(match[1], 10);
|
|
89
|
+
return Number.isFinite(age) ? age : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseDegreeValue(value = "") {
|
|
93
|
+
const normalized = normalizeText(value);
|
|
94
|
+
const match = normalized.match(/博士|硕士|本科|大专|专科|高中|中专\/中技|中专|中技|初中及以下/u);
|
|
95
|
+
return match ? match[0] : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isSalaryLike(value = "") {
|
|
99
|
+
const normalized = normalizeText(value);
|
|
100
|
+
return Boolean(
|
|
101
|
+
/^(?:面议|薪资面议)$/i.test(normalized)
|
|
102
|
+
|| /^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?$/.test(normalized)
|
|
103
|
+
|| /^\d+\s*-\s*\d+\s*元\s*\/\s*天$/.test(normalized)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function extractSalary(html = "") {
|
|
108
|
+
const section = sectionByClasses(html, ["salary-wrap"], [
|
|
109
|
+
["name-wrap"],
|
|
110
|
+
["col-2"]
|
|
111
|
+
]);
|
|
112
|
+
return extractSpanTexts(section).find(isSalaryLike) || "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractBaseInfo(html = "") {
|
|
116
|
+
const section = sectionByClasses(html, ["base-info"], [
|
|
117
|
+
["expect-wrap"],
|
|
118
|
+
["geek-desc"],
|
|
119
|
+
["timeline-wrap"]
|
|
120
|
+
]);
|
|
121
|
+
const parts = extractSpanTexts(section);
|
|
122
|
+
return {
|
|
123
|
+
parts,
|
|
124
|
+
age: parts.map(parseAgeValue).find((value) => value != null) ?? null,
|
|
125
|
+
degree: parts.map(parseDegreeValue).find(Boolean) || ""
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractFirstTimelineContent(html = "", timelineClass = "") {
|
|
130
|
+
const section = sectionByClasses(html, ["timeline-wrap", timelineClass], [
|
|
131
|
+
timelineClass === "work-exps" ? ["timeline-wrap", "edu-exps"] : ["card-btns"],
|
|
132
|
+
["action-wrap"]
|
|
133
|
+
]);
|
|
134
|
+
const contentRows = extractDivTextsWithClasses(section, ["join-text-wrap", "content"]);
|
|
135
|
+
return contentRows[0] || [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractTagTexts(html = "") {
|
|
139
|
+
const tags = [];
|
|
140
|
+
const regex = /<span\b[^>]*class=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/span>/gi;
|
|
141
|
+
let match;
|
|
142
|
+
while ((match = regex.exec(String(html || "")))) {
|
|
143
|
+
if (classList(match[2]).includes("tag-item")) {
|
|
144
|
+
tags.push(textFromHtmlFragment(match[3]));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return uniqueTexts(tags);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseBossCandidateCardFieldsFromHtml(html = "") {
|
|
151
|
+
const name = stripNameSuffixes(extractFirstSpanWithClass(html, "name"));
|
|
152
|
+
const baseInfo = extractBaseInfo(html);
|
|
153
|
+
const work = extractFirstTimelineContent(html, "work-exps");
|
|
154
|
+
const education = extractFirstTimelineContent(html, "edu-exps");
|
|
155
|
+
const educationDegree = education.map(parseDegreeValue).find(Boolean) || "";
|
|
156
|
+
return {
|
|
157
|
+
identity: {
|
|
158
|
+
name: name && !isSalaryLike(name) ? name : "",
|
|
159
|
+
current_company: work[0] || "",
|
|
160
|
+
current_position: work[1] || "",
|
|
161
|
+
school: education[0] || "",
|
|
162
|
+
major: education[1] || "",
|
|
163
|
+
degree: educationDegree || baseInfo.degree || "",
|
|
164
|
+
age: baseInfo.age
|
|
165
|
+
},
|
|
166
|
+
salary: extractSalary(html),
|
|
167
|
+
base_info: baseInfo.parts,
|
|
168
|
+
work,
|
|
169
|
+
education,
|
|
170
|
+
tags: extractTagTexts(html)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function mergeBossCandidateCardFields(candidate, outerHTML = "", {
|
|
175
|
+
metadataKey = "boss_card_fields"
|
|
176
|
+
} = {}) {
|
|
177
|
+
const parsed = parseBossCandidateCardFieldsFromHtml(outerHTML);
|
|
178
|
+
const identity = { ...(candidate.identity || {}) };
|
|
179
|
+
for (const [key, value] of Object.entries(parsed.identity || {})) {
|
|
180
|
+
if (value !== "" && value !== null && value !== undefined) {
|
|
181
|
+
identity[key] = value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...candidate,
|
|
186
|
+
identity,
|
|
187
|
+
tags: uniqueTexts([...(candidate.tags || []), ...(parsed.tags || [])]),
|
|
188
|
+
metadata: {
|
|
189
|
+
...(candidate.metadata || {}),
|
|
190
|
+
[metadataKey]: {
|
|
191
|
+
salary: parsed.salary || "",
|
|
192
|
+
base_info: parsed.base_info || [],
|
|
193
|
+
work: parsed.work || [],
|
|
194
|
+
education: parsed.education || [],
|
|
195
|
+
tags: parsed.tags || []
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -163,11 +163,13 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
163
163
|
metadata = {}
|
|
164
164
|
} = {}) {
|
|
165
165
|
if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
|
|
166
|
+
const sequenceStarted = Date.now();
|
|
166
167
|
const screenshots = [];
|
|
167
168
|
let consecutiveDuplicates = 0;
|
|
168
169
|
let previousHash = "";
|
|
169
170
|
|
|
170
171
|
for (let index = 0; index < Math.max(1, Number(maxScreenshots) || 1); index += 1) {
|
|
172
|
+
const captureStarted = Date.now();
|
|
171
173
|
const box = await getNodeBox(client, nodeId);
|
|
172
174
|
const clip = withPadding(box.rect, padding);
|
|
173
175
|
const captureOptions = {
|
|
@@ -202,6 +204,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
202
204
|
format,
|
|
203
205
|
mime_type: `image/${format === "jpeg" ? "jpeg" : "png"}`,
|
|
204
206
|
byte_length: buffer.length,
|
|
207
|
+
elapsed_ms: Date.now() - captureStarted,
|
|
205
208
|
file_path: outputPath,
|
|
206
209
|
sha256: hash,
|
|
207
210
|
duplicate_of_previous: Boolean(duplicateOfPrevious),
|
|
@@ -238,6 +241,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
|
|
|
238
241
|
source: "image-scroll-sequence",
|
|
239
242
|
captured_at: nowIso(),
|
|
240
243
|
node_id: nodeId,
|
|
244
|
+
elapsed_ms: Date.now() - sequenceStarted,
|
|
241
245
|
screenshot_count: screenshots.length,
|
|
242
246
|
unique_screenshot_count: new Set(screenshots.map((item) => item.sha256)).size,
|
|
243
247
|
file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
|
|
@@ -125,6 +125,7 @@ export function summarizeImageEvidence(imageEvidence = null) {
|
|
|
125
125
|
if (!imageEvidence) return null;
|
|
126
126
|
return {
|
|
127
127
|
source: imageEvidence.source || "",
|
|
128
|
+
elapsed_ms: imageEvidence.elapsed_ms || 0,
|
|
128
129
|
screenshot_count: imageEvidence.screenshot_count || 0,
|
|
129
130
|
unique_screenshot_count: imageEvidence.unique_screenshot_count || 0,
|
|
130
131
|
file_paths: imageEvidence.file_paths || [],
|
|
@@ -227,8 +227,17 @@ function pickCandidate(row = {}) {
|
|
|
227
227
|
|
|
228
228
|
function timingValue(row = {}, ...keys) {
|
|
229
229
|
const timings = row.timings || row.timing || {};
|
|
230
|
+
const detail = row.detail || {};
|
|
231
|
+
const acquisition = detail.cv_acquisition || {};
|
|
232
|
+
const fallbackByKey = {
|
|
233
|
+
network_cv_wait_ms: acquisition.network_wait?.elapsed_ms,
|
|
234
|
+
screenshot_capture_ms: acquisition.image_evidence?.elapsed_ms || detail.image_evidence?.elapsed_ms,
|
|
235
|
+
dom_fallback_ms: acquisition.content_wait?.elapsed_ms,
|
|
236
|
+
close_detail_ms: detail.close_result?.elapsed_ms,
|
|
237
|
+
post_action_ms: row.post_action?.elapsed_ms
|
|
238
|
+
};
|
|
230
239
|
for (const key of keys) {
|
|
231
|
-
const value = firstDefined(row[key], timings[key]);
|
|
240
|
+
const value = firstDefined(row[key], timings[key], fallbackByKey[key]);
|
|
232
241
|
if (value !== "") return value;
|
|
233
242
|
}
|
|
234
243
|
return "";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function addTiming(timings, key, value) {
|
|
4
|
+
if (!timings || !key) return;
|
|
5
|
+
const numeric = Number(value);
|
|
6
|
+
if (!Number.isFinite(numeric) || numeric < 0) return;
|
|
7
|
+
timings[key] = (Number(timings[key]) || 0) + Math.round(numeric);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function measureTiming(timings, key, task) {
|
|
11
|
+
const started = Date.now();
|
|
12
|
+
try {
|
|
13
|
+
return await task();
|
|
14
|
+
} finally {
|
|
15
|
+
addTiming(timings, key, Date.now() - started);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function imageEvidenceFilePath({
|
|
20
|
+
imageOutputDir = "",
|
|
21
|
+
domain = "candidate",
|
|
22
|
+
runId = "",
|
|
23
|
+
index = 0,
|
|
24
|
+
extension = "png"
|
|
25
|
+
} = {}) {
|
|
26
|
+
const dir = String(imageOutputDir || "").trim();
|
|
27
|
+
if (!dir) return "";
|
|
28
|
+
const safeDomain = String(domain || "candidate").replace(/[^\w.-]+/g, "_");
|
|
29
|
+
const safeRunId = String(runId || `${safeDomain}-run`).replace(/[^\w.-]+/g, "_");
|
|
30
|
+
const safeIndex = String((Number(index) || 0) + 1).padStart(3, "0");
|
|
31
|
+
const safeExt = String(extension || "png").replace(/^\./, "") || "png";
|
|
32
|
+
return path.join(dir, safeRunId, `${safeDomain}-candidate-${safeIndex}.${safeExt}`);
|
|
33
|
+
}
|
|
@@ -206,11 +206,52 @@ function parseDateLike(value) {
|
|
|
206
206
|
return normalized;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
function isLikelySalaryLine(value = "") {
|
|
210
|
+
const normalized = normalizeText(value);
|
|
211
|
+
return Boolean(
|
|
212
|
+
/^(?:面议|薪资面议)$/i.test(normalized)
|
|
213
|
+
|| /^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?$/.test(normalized)
|
|
214
|
+
|| /^\d+\s*-\s*\d+\s*元\s*\/\s*天$/.test(normalized)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isLikelyStatusLine(value = "") {
|
|
219
|
+
const normalized = normalizeText(value);
|
|
220
|
+
return Boolean(
|
|
221
|
+
!normalized
|
|
222
|
+
|| /^沟通|^收藏|^查看|^不合适/.test(normalized)
|
|
223
|
+
|| /^(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/.test(normalized)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function stripLeadingSalaryToken(value = "") {
|
|
228
|
+
return normalizeText(value)
|
|
229
|
+
.replace(/^(?:面议|薪资面议)\s+/i, "")
|
|
230
|
+
.replace(/^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?\s+/, "")
|
|
231
|
+
.replace(/^\d+\s*-\s*\d+\s*元\s*\/\s*天\s+/, "")
|
|
232
|
+
.trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stripTrailingStatusToken(value = "") {
|
|
236
|
+
return normalizeText(value)
|
|
237
|
+
.replace(/\s*(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/u, "")
|
|
238
|
+
.trim();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cleanInferredNameLine(value = "") {
|
|
242
|
+
const withoutSalary = stripLeadingSalaryToken(value);
|
|
243
|
+
const withoutStatus = stripTrailingStatusToken(withoutSalary);
|
|
244
|
+
return withoutStatus && !isLikelyStatusLine(withoutStatus) && !isLikelySalaryLine(withoutStatus)
|
|
245
|
+
? withoutStatus
|
|
246
|
+
: "";
|
|
247
|
+
}
|
|
248
|
+
|
|
209
249
|
function firstUsefulLine(lines) {
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
}
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
const cleaned = cleanInferredNameLine(line);
|
|
252
|
+
if (cleaned) return cleaned;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
214
255
|
}
|
|
215
256
|
|
|
216
257
|
function parseNetworkBodyText(networkBody = {}) {
|
|
@@ -834,7 +875,8 @@ export function normalizeCandidateProfile(input = {}) {
|
|
|
834
875
|
|| attrs.href
|
|
835
876
|
|| ""
|
|
836
877
|
) || null;
|
|
837
|
-
const
|
|
878
|
+
const explicitName = cleanInferredNameLine(input.identity?.name || input.name || "");
|
|
879
|
+
const inferredName = explicitName || firstUsefulLine(lines) || null;
|
|
838
880
|
const fullText = collectTextParts({
|
|
839
881
|
...input,
|
|
840
882
|
text: rawText,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
querySelectorAll,
|
|
6
6
|
sleep
|
|
7
7
|
} from "../../core/browser/index.js";
|
|
8
|
+
import { mergeBossCandidateCardFields } from "../../core/boss-cards/index.js";
|
|
8
9
|
import {
|
|
9
10
|
htmlToText,
|
|
10
11
|
normalizeCandidateProfile,
|
|
@@ -24,6 +25,12 @@ function firstCandidateId(attributes = {}) {
|
|
|
24
25
|
) || null;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function mergeChatCardFields(candidate, outerHTML = "") {
|
|
29
|
+
return mergeBossCandidateCardFields(candidate, outerHTML, {
|
|
30
|
+
metadataKey: "chat_card_fields"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export async function findChatCandidateNodeIds(client, rootNodeId, {
|
|
28
35
|
selectors = CHAT_CARD_SELECTORS
|
|
29
36
|
} = {}) {
|
|
@@ -97,7 +104,7 @@ export async function readChatCardCandidate(client, cardNodeId, {
|
|
|
97
104
|
getAttributesMap(client, cardNodeId),
|
|
98
105
|
getOuterHTML(client, cardNodeId)
|
|
99
106
|
]);
|
|
100
|
-
|
|
107
|
+
const candidate = normalizeCandidateProfile({
|
|
101
108
|
domain: "chat",
|
|
102
109
|
source,
|
|
103
110
|
id: firstCandidateId(attributes),
|
|
@@ -110,6 +117,7 @@ export async function readChatCardCandidate(client, cardNodeId, {
|
|
|
110
117
|
...metadata
|
|
111
118
|
}
|
|
112
119
|
});
|
|
120
|
+
return mergeChatCardFields(candidate, outerHTML);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
export async function readFirstChatCardCandidate(client, rootNodeId, options = {}) {
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
} from "../../core/infinite-list/index.js";
|
|
25
25
|
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
26
26
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
27
|
+
import {
|
|
28
|
+
addTiming,
|
|
29
|
+
imageEvidenceFilePath,
|
|
30
|
+
measureTiming
|
|
31
|
+
} from "../../core/run/timing.js";
|
|
27
32
|
import {
|
|
28
33
|
callScreeningLlm,
|
|
29
34
|
normalizeText,
|
|
@@ -369,7 +374,8 @@ export async function runChatWorkflow({
|
|
|
369
374
|
listStableSignatureLimit = 2,
|
|
370
375
|
listWheelDeltaY = 850,
|
|
371
376
|
listSettleMs = 1200,
|
|
372
|
-
listFallbackPoint = null
|
|
377
|
+
listFallbackPoint = null,
|
|
378
|
+
imageOutputDir = ""
|
|
373
379
|
} = {}, runControl) {
|
|
374
380
|
if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
|
|
375
381
|
const normalizedDetailSource = normalizeDetailSource(detailSource);
|
|
@@ -584,6 +590,8 @@ export async function runChatWorkflow({
|
|
|
584
590
|
|| results.filter((item) => item.screening?.passed).length < passTarget
|
|
585
591
|
)
|
|
586
592
|
) {
|
|
593
|
+
const candidateStarted = Date.now();
|
|
594
|
+
const timings = {};
|
|
587
595
|
await runControl.waitIfPaused();
|
|
588
596
|
runControl.throwIfCanceled();
|
|
589
597
|
runControl.setPhase("chat:candidate");
|
|
@@ -596,7 +604,7 @@ export async function runChatWorkflow({
|
|
|
596
604
|
continue;
|
|
597
605
|
}
|
|
598
606
|
|
|
599
|
-
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
607
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
600
608
|
client,
|
|
601
609
|
state: listState,
|
|
602
610
|
maxScrolls: listMaxScrolls,
|
|
@@ -623,7 +631,7 @@ export async function runChatWorkflow({
|
|
|
623
631
|
visible_index: visibleIndex
|
|
624
632
|
}
|
|
625
633
|
})
|
|
626
|
-
});
|
|
634
|
+
}));
|
|
627
635
|
if (!nextCandidateResult.ok) {
|
|
628
636
|
const endTopLevelState = await getChatTopLevelState(client);
|
|
629
637
|
if (!endTopLevelState.is_chat_shell) {
|
|
@@ -665,11 +673,11 @@ export async function runChatWorkflow({
|
|
|
665
673
|
|
|
666
674
|
detailStep = "select_candidate";
|
|
667
675
|
networkRecorder.clear();
|
|
668
|
-
const selected = await selectFreshChatCandidate(client, {
|
|
676
|
+
const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
|
|
669
677
|
cardNodeId,
|
|
670
678
|
candidate: cardCandidate,
|
|
671
679
|
timeoutMs: onlineResumeButtonTimeoutMs
|
|
672
|
-
});
|
|
680
|
+
}));
|
|
673
681
|
if (selected.ready?.forbidden_top_level_navigation) {
|
|
674
682
|
throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
|
|
675
683
|
}
|
|
@@ -696,13 +704,13 @@ export async function runChatWorkflow({
|
|
|
696
704
|
if (!detailResult) {
|
|
697
705
|
detailStep = "open_online_resume";
|
|
698
706
|
networkRecorder.clear();
|
|
699
|
-
const openedResume = await openChatOnlineResume(client, {
|
|
707
|
+
const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
|
|
700
708
|
timeoutMs: readyTimeoutMs
|
|
701
|
-
});
|
|
709
|
+
}));
|
|
702
710
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
703
711
|
detailStep = "wait_network";
|
|
704
712
|
const networkWait = ["network", "cascade"].includes(normalizedDetailSource)
|
|
705
|
-
? await waitForCvNetworkEvents(
|
|
713
|
+
? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
706
714
|
waitForChatProfileNetworkEvents,
|
|
707
715
|
networkRecorder,
|
|
708
716
|
{
|
|
@@ -711,8 +719,11 @@ export async function runChatWorkflow({
|
|
|
711
719
|
requireLoaded: true,
|
|
712
720
|
intervalMs: 200
|
|
713
721
|
}
|
|
714
|
-
)
|
|
722
|
+
))
|
|
715
723
|
: null;
|
|
724
|
+
if (networkWait?.elapsed_ms != null) {
|
|
725
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
726
|
+
}
|
|
716
727
|
let contentWait = {
|
|
717
728
|
ok: false,
|
|
718
729
|
skipped: false,
|
|
@@ -759,10 +770,10 @@ export async function runChatWorkflow({
|
|
|
759
770
|
|
|
760
771
|
if (!detailResult) {
|
|
761
772
|
detailStep = "wait_resume_content";
|
|
762
|
-
contentWait = await waitForChatResumeContent(client, {
|
|
773
|
+
contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
|
|
763
774
|
timeoutMs: resumeDomTimeoutMs,
|
|
764
775
|
intervalMs: 300
|
|
765
|
-
});
|
|
776
|
+
}));
|
|
766
777
|
resumeState = contentWait.resume_state || openedResume.resume_state;
|
|
767
778
|
resumeHtml = contentWait.resume_html || null;
|
|
768
779
|
resumeNetworkEvents = networkRecorder.events.slice();
|
|
@@ -792,7 +803,13 @@ export async function runChatWorkflow({
|
|
|
792
803
|
if (shouldCaptureImage) {
|
|
793
804
|
if (captureNodeId) {
|
|
794
805
|
detailStep = "capture_image_fallback";
|
|
795
|
-
imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
806
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
807
|
+
filePath: imageEvidenceFilePath({
|
|
808
|
+
imageOutputDir,
|
|
809
|
+
domain: "chat",
|
|
810
|
+
runId: runControl?.runId,
|
|
811
|
+
index
|
|
812
|
+
}),
|
|
796
813
|
padding: 8,
|
|
797
814
|
maxScreenshots: maxImagePages,
|
|
798
815
|
wheelDeltaY: imageWheelDeltaY,
|
|
@@ -806,7 +823,7 @@ export async function runChatWorkflow({
|
|
|
806
823
|
run_candidate_index: index,
|
|
807
824
|
candidate_key: candidateKey
|
|
808
825
|
}
|
|
809
|
-
});
|
|
826
|
+
}));
|
|
810
827
|
source = "image";
|
|
811
828
|
recordCvImageFallback(cvAcquisitionState, {
|
|
812
829
|
parsedNetworkProfileCount,
|
|
@@ -819,7 +836,7 @@ export async function runChatWorkflow({
|
|
|
819
836
|
llmResult = createMissingLlmConfigResult();
|
|
820
837
|
} else {
|
|
821
838
|
try {
|
|
822
|
-
llmResult = await callScreeningLlm({
|
|
839
|
+
llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
|
|
823
840
|
candidate: detailResult.candidate,
|
|
824
841
|
criteria,
|
|
825
842
|
config: llmConfig,
|
|
@@ -827,7 +844,7 @@ export async function runChatWorkflow({
|
|
|
827
844
|
imageEvidence,
|
|
828
845
|
maxImages: llmImageLimit,
|
|
829
846
|
imageDetail: llmImageDetail
|
|
830
|
-
});
|
|
847
|
+
}));
|
|
831
848
|
} catch (error) {
|
|
832
849
|
llmResult = createFailedLlmResult(error);
|
|
833
850
|
}
|
|
@@ -861,7 +878,10 @@ export async function runChatWorkflow({
|
|
|
861
878
|
llmResult = createMissingLlmConfigResult();
|
|
862
879
|
} else {
|
|
863
880
|
try {
|
|
864
|
-
|
|
881
|
+
const llmTimingKey = imageEvidence?.file_paths?.length
|
|
882
|
+
? "vision_model_ms"
|
|
883
|
+
: "text_model_ms";
|
|
884
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
865
885
|
candidate: detailResult.candidate,
|
|
866
886
|
criteria,
|
|
867
887
|
config: llmConfig,
|
|
@@ -869,7 +889,7 @@ export async function runChatWorkflow({
|
|
|
869
889
|
imageEvidence,
|
|
870
890
|
maxImages: llmImageLimit,
|
|
871
891
|
imageDetail: llmImageDetail
|
|
872
|
-
});
|
|
892
|
+
}));
|
|
873
893
|
} catch (error) {
|
|
874
894
|
llmResult = createFailedLlmResult(error);
|
|
875
895
|
}
|
|
@@ -879,7 +899,7 @@ export async function runChatWorkflow({
|
|
|
879
899
|
let closeResult = null;
|
|
880
900
|
if (closeResume) {
|
|
881
901
|
detailStep = "close_resume_modal";
|
|
882
|
-
closeResult = await closeChatResumeModal(client);
|
|
902
|
+
closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
|
|
883
903
|
}
|
|
884
904
|
detailResult.close_result = closeResult;
|
|
885
905
|
detailResult.image_evidence = imageEvidence;
|
|
@@ -927,14 +947,14 @@ export async function runChatWorkflow({
|
|
|
927
947
|
cardOnlyLlmResult = createMissingLlmConfigResult();
|
|
928
948
|
} else {
|
|
929
949
|
try {
|
|
930
|
-
cardOnlyLlmResult = await callScreeningLlm({
|
|
950
|
+
cardOnlyLlmResult = await measureTiming(timings, "text_model_ms", () => callScreeningLlm({
|
|
931
951
|
candidate: screeningCandidate,
|
|
932
952
|
criteria,
|
|
933
953
|
config: llmConfig,
|
|
934
954
|
timeoutMs: llmTimeoutMs,
|
|
935
955
|
maxImages: llmImageLimit,
|
|
936
956
|
imageDetail: llmImageDetail
|
|
937
|
-
});
|
|
957
|
+
}));
|
|
938
958
|
} catch (error) {
|
|
939
959
|
cardOnlyLlmResult = createFailedLlmResult(error);
|
|
940
960
|
}
|
|
@@ -954,10 +974,10 @@ export async function runChatWorkflow({
|
|
|
954
974
|
: screenCandidate(screeningCandidate, { criteria });
|
|
955
975
|
let postAction = null;
|
|
956
976
|
if (requestResumeForPassed && screening.passed) {
|
|
957
|
-
postAction = await requestChatResumeForPassedCandidate(client, {
|
|
977
|
+
postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
|
|
958
978
|
greetingText,
|
|
959
979
|
dryRun: dryRunRequestCv
|
|
960
|
-
});
|
|
980
|
+
}));
|
|
961
981
|
if (postAction?.requested) requestSatisfiedCount += 1;
|
|
962
982
|
if (postAction?.skipped) requestSkippedCount += 1;
|
|
963
983
|
if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
|
|
@@ -965,6 +985,7 @@ export async function runChatWorkflow({
|
|
|
965
985
|
throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
|
|
966
986
|
}
|
|
967
987
|
}
|
|
988
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
968
989
|
const compactResult = {
|
|
969
990
|
index,
|
|
970
991
|
candidate_key: candidateKey,
|
|
@@ -974,7 +995,8 @@ export async function runChatWorkflow({
|
|
|
974
995
|
llm_screening: detailResult ? null : compactLlmResult(cardOnlyLlmResult),
|
|
975
996
|
screening: compactScreening(screening),
|
|
976
997
|
post_action: postAction,
|
|
977
|
-
pre_action_state: preActionState
|
|
998
|
+
pre_action_state: preActionState,
|
|
999
|
+
timings
|
|
978
1000
|
};
|
|
979
1001
|
results.push(compactResult);
|
|
980
1002
|
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
@@ -1006,6 +1028,7 @@ export async function runChatWorkflow({
|
|
|
1006
1028
|
last_candidate_key: candidateKey,
|
|
1007
1029
|
last_score: screening.score
|
|
1008
1030
|
});
|
|
1031
|
+
const checkpointStarted = Date.now();
|
|
1009
1032
|
runControl.checkpoint({
|
|
1010
1033
|
results,
|
|
1011
1034
|
last_candidate: {
|
|
@@ -1020,9 +1043,13 @@ export async function runChatWorkflow({
|
|
|
1020
1043
|
llm_screening: compactLlmResult(effectiveLlmResult)
|
|
1021
1044
|
}
|
|
1022
1045
|
});
|
|
1046
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1023
1047
|
|
|
1024
1048
|
if (delayMs > 0) {
|
|
1049
|
+
const sleepStarted = Date.now();
|
|
1025
1050
|
await runControl.sleep(delayMs);
|
|
1051
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
1052
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1026
1053
|
}
|
|
1027
1054
|
}
|
|
1028
1055
|
|
|
@@ -1095,6 +1122,7 @@ export function createChatRunService({
|
|
|
1095
1122
|
listWheelDeltaY = 850,
|
|
1096
1123
|
listSettleMs = 1200,
|
|
1097
1124
|
listFallbackPoint = null,
|
|
1125
|
+
imageOutputDir = "",
|
|
1098
1126
|
name = "chat-domain-run"
|
|
1099
1127
|
} = {}) {
|
|
1100
1128
|
if (!client) throw new Error("startChatRun requires a guarded CDP client");
|
|
@@ -1130,7 +1158,8 @@ export function createChatRunService({
|
|
|
1130
1158
|
list_wheel_delta_y: listWheelDeltaY,
|
|
1131
1159
|
list_settle_ms: listSettleMs,
|
|
1132
1160
|
list_fallback_point: listFallbackPoint,
|
|
1133
|
-
online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs
|
|
1161
|
+
online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
|
|
1162
|
+
image_output_dir: imageOutputDir || ""
|
|
1134
1163
|
},
|
|
1135
1164
|
progress: {
|
|
1136
1165
|
card_count: 0,
|
|
@@ -1180,7 +1209,8 @@ export function createChatRunService({
|
|
|
1180
1209
|
listStableSignatureLimit,
|
|
1181
1210
|
listWheelDeltaY,
|
|
1182
1211
|
listSettleMs,
|
|
1183
|
-
listFallbackPoint
|
|
1212
|
+
listFallbackPoint,
|
|
1213
|
+
imageOutputDir
|
|
1184
1214
|
}, runControl)
|
|
1185
1215
|
});
|
|
1186
1216
|
}
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
querySelectorAll,
|
|
7
7
|
sleep
|
|
8
8
|
} from "../../core/browser/index.js";
|
|
9
|
+
import {
|
|
10
|
+
mergeBossCandidateCardFields,
|
|
11
|
+
parseBossCandidateCardFieldsFromHtml
|
|
12
|
+
} from "../../core/boss-cards/index.js";
|
|
9
13
|
import {
|
|
10
14
|
htmlToText,
|
|
11
15
|
normalizeCandidateFromHtml,
|
|
@@ -24,6 +28,16 @@ function normalizeRefreshButtonLabel(outerHTML = "") {
|
|
|
24
28
|
return normalizeText(htmlToText(outerHTML)).replace(/\s+/g, "");
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export function parseRecommendCardFieldsFromHtml(html = "") {
|
|
32
|
+
return parseBossCandidateCardFieldsFromHtml(html);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function enrichRecommendCardCandidate(candidate, outerHTML = "") {
|
|
36
|
+
return mergeBossCandidateCardFields(candidate, outerHTML, {
|
|
37
|
+
metadataKey: "recommend_card_fields"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
function isRefreshButtonLabel(label = "") {
|
|
28
42
|
const normalized = String(label || "").trim();
|
|
29
43
|
if (!normalized || normalized.length > 80) return false;
|
|
@@ -91,7 +105,7 @@ export async function readRecommendCardCandidate(client, cardNodeId, {
|
|
|
91
105
|
getAttributesMap(client, cardNodeId),
|
|
92
106
|
getOuterHTML(client, cardNodeId)
|
|
93
107
|
]);
|
|
94
|
-
|
|
108
|
+
const candidate = normalizeCandidateFromHtml({
|
|
95
109
|
domain: "recommend",
|
|
96
110
|
source,
|
|
97
111
|
html: outerHTML,
|
|
@@ -102,6 +116,7 @@ export async function readRecommendCardCandidate(client, cardNodeId, {
|
|
|
102
116
|
...metadata
|
|
103
117
|
}
|
|
104
118
|
});
|
|
119
|
+
return enrichRecommendCardCandidate(candidate, outerHTML);
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
export async function readFirstRecommendCardCandidate(client, frameNodeId, options = {}) {
|
|
@@ -279,15 +279,25 @@ export async function openRecommendCardDetail(client, cardNodeId, {
|
|
|
279
279
|
timeoutMs = 12000,
|
|
280
280
|
scrollIntoView = true
|
|
281
281
|
} = {}) {
|
|
282
|
+
const started = Date.now();
|
|
283
|
+
const clickStarted = Date.now();
|
|
282
284
|
const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
|
|
285
|
+
const candidateClickMs = Date.now() - clickStarted;
|
|
286
|
+
const detailStarted = Date.now();
|
|
283
287
|
const detailState = await waitForRecommendDetail(client, { timeoutMs });
|
|
288
|
+
const detailOpenMs = Date.now() - detailStarted;
|
|
284
289
|
if (!detailState?.popup && !detailState?.resumeIframe) {
|
|
285
290
|
throw new Error("Candidate detail did not open or no known detail selectors mounted");
|
|
286
291
|
}
|
|
287
292
|
|
|
288
293
|
return {
|
|
289
294
|
card_box: cardBox,
|
|
290
|
-
detail_state: detailState
|
|
295
|
+
detail_state: detailState,
|
|
296
|
+
timings: {
|
|
297
|
+
candidate_click_ms: candidateClickMs,
|
|
298
|
+
detail_open_ms: detailOpenMs,
|
|
299
|
+
open_total_ms: Date.now() - started
|
|
300
|
+
}
|
|
291
301
|
};
|
|
292
302
|
}
|
|
293
303
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
2
|
+
import {
|
|
3
|
+
addTiming,
|
|
4
|
+
imageEvidenceFilePath,
|
|
5
|
+
measureTiming
|
|
6
|
+
} from "../../core/run/timing.js";
|
|
2
7
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
3
8
|
import { sleep } from "../../core/browser/index.js";
|
|
4
9
|
import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
|
|
@@ -377,7 +382,8 @@ export async function runRecommendWorkflow({
|
|
|
377
382
|
llmConfig = null,
|
|
378
383
|
llmTimeoutMs = 120000,
|
|
379
384
|
llmImageLimit = 8,
|
|
380
|
-
llmImageDetail = "high"
|
|
385
|
+
llmImageDetail = "high",
|
|
386
|
+
imageOutputDir = ""
|
|
381
387
|
} = {}, runControl) {
|
|
382
388
|
if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
|
|
383
389
|
const normalizedFilter = normalizeFilter(filter);
|
|
@@ -520,12 +526,14 @@ export async function runRecommendWorkflow({
|
|
|
520
526
|
});
|
|
521
527
|
|
|
522
528
|
while (results.length < limit) {
|
|
529
|
+
const candidateStarted = Date.now();
|
|
530
|
+
const timings = {};
|
|
523
531
|
await runControl.waitIfPaused();
|
|
524
532
|
runControl.throwIfCanceled();
|
|
525
533
|
runControl.setPhase("recommend:candidate");
|
|
526
534
|
rootState = await ensureRecommendViewport(rootState, "candidate_loop");
|
|
527
535
|
|
|
528
|
-
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
536
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
529
537
|
client,
|
|
530
538
|
state: listState,
|
|
531
539
|
maxScrolls: listMaxScrolls,
|
|
@@ -552,7 +560,7 @@ export async function runRecommendWorkflow({
|
|
|
552
560
|
visible_index: visibleIndex
|
|
553
561
|
}
|
|
554
562
|
})
|
|
555
|
-
});
|
|
563
|
+
}));
|
|
556
564
|
if (!nextCandidateResult.ok) {
|
|
557
565
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
558
566
|
if (
|
|
@@ -644,11 +652,13 @@ export async function runRecommendWorkflow({
|
|
|
644
652
|
targetUrl,
|
|
645
653
|
maxAttempts: 2
|
|
646
654
|
});
|
|
655
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
656
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
647
657
|
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
648
658
|
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
649
659
|
screeningCandidate = cardCandidate;
|
|
650
660
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
651
|
-
const networkWait = await waitForCvNetworkEvents(
|
|
661
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
652
662
|
waitForRecommendDetailNetworkEvents,
|
|
653
663
|
networkRecorder,
|
|
654
664
|
{
|
|
@@ -657,7 +667,10 @@ export async function runRecommendWorkflow({
|
|
|
657
667
|
requireLoaded: true,
|
|
658
668
|
intervalMs: 120
|
|
659
669
|
}
|
|
660
|
-
);
|
|
670
|
+
));
|
|
671
|
+
if (networkWait?.elapsed_ms != null) {
|
|
672
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
673
|
+
}
|
|
661
674
|
detailResult = await extractRecommendDetailCandidate(client, {
|
|
662
675
|
cardCandidate,
|
|
663
676
|
cardNodeId,
|
|
@@ -680,7 +693,13 @@ export async function runRecommendWorkflow({
|
|
|
680
693
|
|| openedDetail.detail_state?.resumeIframe?.node_id
|
|
681
694
|
|| null;
|
|
682
695
|
if (captureNodeId) {
|
|
683
|
-
imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
696
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
697
|
+
filePath: imageEvidenceFilePath({
|
|
698
|
+
imageOutputDir,
|
|
699
|
+
domain: "recommend",
|
|
700
|
+
runId: runControl?.runId,
|
|
701
|
+
index
|
|
702
|
+
}),
|
|
684
703
|
padding: 4,
|
|
685
704
|
maxScreenshots: maxImagePages,
|
|
686
705
|
wheelDeltaY: imageWheelDeltaY,
|
|
@@ -692,7 +711,7 @@ export async function runRecommendWorkflow({
|
|
|
692
711
|
run_candidate_index: index,
|
|
693
712
|
candidate_key: candidateKey
|
|
694
713
|
}
|
|
695
|
-
});
|
|
714
|
+
}));
|
|
696
715
|
source = "image";
|
|
697
716
|
recordCvImageFallback(cvAcquisitionState, {
|
|
698
717
|
parsedNetworkProfileCount,
|
|
@@ -730,7 +749,10 @@ export async function runRecommendWorkflow({
|
|
|
730
749
|
llmResult = createMissingLlmConfigResult();
|
|
731
750
|
} else {
|
|
732
751
|
try {
|
|
733
|
-
|
|
752
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
753
|
+
? "vision_model_ms"
|
|
754
|
+
: "text_model_ms";
|
|
755
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
734
756
|
candidate: screeningCandidate,
|
|
735
757
|
criteria,
|
|
736
758
|
config: llmConfig,
|
|
@@ -738,7 +760,7 @@ export async function runRecommendWorkflow({
|
|
|
738
760
|
imageEvidence: detailResult?.image_evidence || null,
|
|
739
761
|
maxImages: llmImageLimit,
|
|
740
762
|
imageDetail: llmImageDetail
|
|
741
|
-
});
|
|
763
|
+
}));
|
|
742
764
|
} catch (error) {
|
|
743
765
|
llmResult = createFailedLlmScreeningResult(error);
|
|
744
766
|
}
|
|
@@ -751,6 +773,7 @@ export async function runRecommendWorkflow({
|
|
|
751
773
|
let actionDiscovery = null;
|
|
752
774
|
let postActionResult = null;
|
|
753
775
|
if (postActionEnabled && detailResult) {
|
|
776
|
+
const postActionStarted = Date.now();
|
|
754
777
|
await runControl.waitIfPaused();
|
|
755
778
|
runControl.throwIfCanceled();
|
|
756
779
|
runControl.setPhase("recommend:post-action");
|
|
@@ -772,10 +795,12 @@ export async function runRecommendWorkflow({
|
|
|
772
795
|
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
773
796
|
greetCount += 1;
|
|
774
797
|
}
|
|
798
|
+
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
775
799
|
}
|
|
776
800
|
if (detailResult && closeDetail) {
|
|
777
|
-
detailResult.close_result = await closeRecommendDetail(client);
|
|
801
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
778
802
|
}
|
|
803
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
779
804
|
const compactResult = {
|
|
780
805
|
index,
|
|
781
806
|
candidate_key: candidateKey,
|
|
@@ -785,7 +810,8 @@ export async function runRecommendWorkflow({
|
|
|
785
810
|
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
786
811
|
screening: compactScreening(screening),
|
|
787
812
|
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
788
|
-
post_action: postActionResult
|
|
813
|
+
post_action: postActionResult,
|
|
814
|
+
timings
|
|
789
815
|
};
|
|
790
816
|
results.push(compactResult);
|
|
791
817
|
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
@@ -816,6 +842,7 @@ export async function runRecommendWorkflow({
|
|
|
816
842
|
last_candidate_key: candidateKey,
|
|
817
843
|
last_score: screening.score
|
|
818
844
|
});
|
|
845
|
+
const checkpointStarted = Date.now();
|
|
819
846
|
runControl.checkpoint({
|
|
820
847
|
results,
|
|
821
848
|
last_candidate: {
|
|
@@ -831,6 +858,7 @@ export async function runRecommendWorkflow({
|
|
|
831
858
|
post_action: postActionResult
|
|
832
859
|
}
|
|
833
860
|
});
|
|
861
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
834
862
|
|
|
835
863
|
if (postActionResult?.stop_run) {
|
|
836
864
|
listEndReason = postActionResult.reason || "post_action_stop";
|
|
@@ -838,7 +866,10 @@ export async function runRecommendWorkflow({
|
|
|
838
866
|
}
|
|
839
867
|
|
|
840
868
|
if (delayMs > 0) {
|
|
869
|
+
const sleepStarted = Date.now();
|
|
841
870
|
await runControl.sleep(delayMs);
|
|
871
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
872
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
842
873
|
}
|
|
843
874
|
}
|
|
844
875
|
|
|
@@ -912,6 +943,7 @@ export function createRecommendRunService({
|
|
|
912
943
|
llmTimeoutMs = 120000,
|
|
913
944
|
llmImageLimit = 8,
|
|
914
945
|
llmImageDetail = "high",
|
|
946
|
+
imageOutputDir = "",
|
|
915
947
|
name = "recommend-domain-run"
|
|
916
948
|
} = {}) {
|
|
917
949
|
if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
|
|
@@ -955,7 +987,8 @@ export function createRecommendRunService({
|
|
|
955
987
|
llm_configured: Boolean(llmConfig),
|
|
956
988
|
llm_timeout_ms: llmTimeoutMs,
|
|
957
989
|
llm_image_limit: llmImageLimit,
|
|
958
|
-
llm_image_detail: llmImageDetail
|
|
990
|
+
llm_image_detail: llmImageDetail,
|
|
991
|
+
image_output_dir: imageOutputDir || ""
|
|
959
992
|
},
|
|
960
993
|
progress: {
|
|
961
994
|
card_count: 0,
|
|
@@ -1004,7 +1037,8 @@ export function createRecommendRunService({
|
|
|
1004
1037
|
llmConfig,
|
|
1005
1038
|
llmTimeoutMs,
|
|
1006
1039
|
llmImageLimit,
|
|
1007
|
-
llmImageDetail
|
|
1040
|
+
llmImageDetail,
|
|
1041
|
+
imageOutputDir
|
|
1008
1042
|
}, runControl)
|
|
1009
1043
|
});
|
|
1010
1044
|
}
|
|
@@ -4,9 +4,16 @@ import {
|
|
|
4
4
|
querySelectorAll,
|
|
5
5
|
sleep
|
|
6
6
|
} from "../../core/browser/index.js";
|
|
7
|
+
import { mergeBossCandidateCardFields } from "../../core/boss-cards/index.js";
|
|
7
8
|
import { normalizeCandidateFromHtml } from "../../core/screening/index.js";
|
|
8
9
|
import { RECRUIT_CARD_SELECTOR } from "./constants.js";
|
|
9
10
|
|
|
11
|
+
function mergeRecruitCardFields(candidate, outerHTML = "") {
|
|
12
|
+
return mergeBossCandidateCardFields(candidate, outerHTML, {
|
|
13
|
+
metadataKey: "search_card_fields"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export async function findRecruitCardNodeIds(client, frameNodeId, {
|
|
11
18
|
selector = RECRUIT_CARD_SELECTOR
|
|
12
19
|
} = {}) {
|
|
@@ -37,7 +44,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
|
|
|
37
44
|
getAttributesMap(client, cardNodeId),
|
|
38
45
|
getOuterHTML(client, cardNodeId)
|
|
39
46
|
]);
|
|
40
|
-
|
|
47
|
+
const candidate = normalizeCandidateFromHtml({
|
|
41
48
|
domain: "recruit",
|
|
42
49
|
source,
|
|
43
50
|
html: outerHTML,
|
|
@@ -48,6 +55,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
|
|
|
48
55
|
...metadata
|
|
49
56
|
}
|
|
50
57
|
});
|
|
58
|
+
return mergeRecruitCardFields(candidate, outerHTML);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
export async function readFirstRecruitCardCandidate(client, frameNodeId, options = {}) {
|
|
@@ -213,17 +213,22 @@ export async function waitForRecruitDetailContent(client, {
|
|
|
213
213
|
export async function openRecruitCardDetail(client, cardNodeId, {
|
|
214
214
|
timeoutMs = 12000
|
|
215
215
|
} = {}) {
|
|
216
|
+
const openedStarted = Date.now();
|
|
216
217
|
const attempts = [];
|
|
218
|
+
const clickStarted = Date.now();
|
|
217
219
|
const cardBox = await clickNodeCenter(client, cardNodeId, {
|
|
218
220
|
scrollIntoView: true
|
|
219
221
|
});
|
|
222
|
+
let candidateClickMs = Date.now() - clickStarted;
|
|
220
223
|
attempts.push({
|
|
221
224
|
mode: "card-center",
|
|
222
225
|
center: cardBox.center
|
|
223
226
|
});
|
|
227
|
+
const detailStarted = Date.now();
|
|
224
228
|
let detailState = await waitForRecruitDetail(client, { timeoutMs });
|
|
225
229
|
|
|
226
230
|
if (!detailState?.popup && !detailState?.resumeIframe) {
|
|
231
|
+
const fallbackClickStarted = Date.now();
|
|
227
232
|
const leftTitlePoint = {
|
|
228
233
|
x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
|
|
229
234
|
y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
|
|
@@ -232,6 +237,7 @@ export async function openRecruitCardDetail(client, cardNodeId, {
|
|
|
232
237
|
clickCount: 2,
|
|
233
238
|
delayMs: 120
|
|
234
239
|
});
|
|
240
|
+
candidateClickMs += Date.now() - fallbackClickStarted;
|
|
235
241
|
attempts.push({
|
|
236
242
|
mode: "card-left-title-double-click",
|
|
237
243
|
center: leftTitlePoint
|
|
@@ -248,7 +254,12 @@ export async function openRecruitCardDetail(client, cardNodeId, {
|
|
|
248
254
|
return {
|
|
249
255
|
card_box: cardBox,
|
|
250
256
|
open_attempts: attempts,
|
|
251
|
-
detail_state: detailState
|
|
257
|
+
detail_state: detailState,
|
|
258
|
+
timings: {
|
|
259
|
+
candidate_click_ms: candidateClickMs,
|
|
260
|
+
detail_open_ms: Date.now() - detailStarted,
|
|
261
|
+
open_total_ms: Date.now() - openedStarted
|
|
262
|
+
}
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
2
|
+
import {
|
|
3
|
+
addTiming,
|
|
4
|
+
imageEvidenceFilePath,
|
|
5
|
+
measureTiming
|
|
6
|
+
} from "../../core/run/timing.js";
|
|
2
7
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
3
8
|
import {
|
|
4
9
|
compactCvAcquisitionState,
|
|
@@ -150,7 +155,8 @@ export async function runRecruitWorkflow({
|
|
|
150
155
|
llmConfig = null,
|
|
151
156
|
llmTimeoutMs = 120000,
|
|
152
157
|
llmImageLimit = 8,
|
|
153
|
-
llmImageDetail = "high"
|
|
158
|
+
llmImageDetail = "high",
|
|
159
|
+
imageOutputDir = ""
|
|
154
160
|
} = {}, runControl) {
|
|
155
161
|
if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
|
|
156
162
|
const normalizedSearchParams = normalizeSearchParams(searchParams);
|
|
@@ -258,12 +264,14 @@ export async function runRecruitWorkflow({
|
|
|
258
264
|
});
|
|
259
265
|
|
|
260
266
|
while (results.length < limit) {
|
|
267
|
+
const candidateStarted = Date.now();
|
|
268
|
+
const timings = {};
|
|
261
269
|
await runControl.waitIfPaused();
|
|
262
270
|
runControl.throwIfCanceled();
|
|
263
271
|
runControl.setPhase("recruit:candidate");
|
|
264
272
|
rootState = await ensureRecruitViewport(rootState, "candidate_loop");
|
|
265
273
|
|
|
266
|
-
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
274
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
267
275
|
client,
|
|
268
276
|
state: listState,
|
|
269
277
|
maxScrolls: listMaxScrolls,
|
|
@@ -291,7 +299,7 @@ export async function runRecruitWorkflow({
|
|
|
291
299
|
search_params: normalizedSearchParams
|
|
292
300
|
}
|
|
293
301
|
})
|
|
294
|
-
});
|
|
302
|
+
}));
|
|
295
303
|
if (!nextCandidateResult.ok) {
|
|
296
304
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
297
305
|
if (
|
|
@@ -373,8 +381,10 @@ export async function runRecruitWorkflow({
|
|
|
373
381
|
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
374
382
|
networkRecorder.clear();
|
|
375
383
|
const openedDetail = await openRecruitCardDetail(client, cardNodeId);
|
|
384
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
385
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
376
386
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
377
|
-
const networkWait = await waitForCvNetworkEvents(
|
|
387
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
378
388
|
waitForRecruitDetailNetworkEvents,
|
|
379
389
|
networkRecorder,
|
|
380
390
|
{
|
|
@@ -383,7 +393,10 @@ export async function runRecruitWorkflow({
|
|
|
383
393
|
requireLoaded: true,
|
|
384
394
|
intervalMs: 120
|
|
385
395
|
}
|
|
386
|
-
);
|
|
396
|
+
));
|
|
397
|
+
if (networkWait?.elapsed_ms != null) {
|
|
398
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
399
|
+
}
|
|
387
400
|
detailResult = await extractRecruitDetailCandidate(client, {
|
|
388
401
|
cardCandidate,
|
|
389
402
|
cardNodeId,
|
|
@@ -405,7 +418,13 @@ export async function runRecruitWorkflow({
|
|
|
405
418
|
|| openedDetail.detail_state?.resumeIframe?.node_id
|
|
406
419
|
|| null;
|
|
407
420
|
if (captureNodeId) {
|
|
408
|
-
imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
421
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
422
|
+
filePath: imageEvidenceFilePath({
|
|
423
|
+
imageOutputDir,
|
|
424
|
+
domain: "recruit",
|
|
425
|
+
runId: runControl?.runId,
|
|
426
|
+
index
|
|
427
|
+
}),
|
|
409
428
|
padding: 4,
|
|
410
429
|
maxScreenshots: maxImagePages,
|
|
411
430
|
wheelDeltaY: imageWheelDeltaY,
|
|
@@ -417,7 +436,7 @@ export async function runRecruitWorkflow({
|
|
|
417
436
|
run_candidate_index: index,
|
|
418
437
|
candidate_key: candidateKey
|
|
419
438
|
}
|
|
420
|
-
});
|
|
439
|
+
}));
|
|
421
440
|
source = "image";
|
|
422
441
|
recordCvImageFallback(cvAcquisitionState, {
|
|
423
442
|
parsedNetworkProfileCount,
|
|
@@ -436,7 +455,7 @@ export async function runRecruitWorkflow({
|
|
|
436
455
|
|
|
437
456
|
let closeResult = null;
|
|
438
457
|
if (closeDetail) {
|
|
439
|
-
closeResult = await closeRecruitDetail(client);
|
|
458
|
+
closeResult = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
|
|
440
459
|
}
|
|
441
460
|
detailResult.close_result = closeResult;
|
|
442
461
|
detailResult.image_evidence = imageEvidence;
|
|
@@ -460,7 +479,10 @@ export async function runRecruitWorkflow({
|
|
|
460
479
|
llmResult = createMissingLlmConfigResult();
|
|
461
480
|
} else {
|
|
462
481
|
try {
|
|
463
|
-
|
|
482
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
483
|
+
? "vision_model_ms"
|
|
484
|
+
: "text_model_ms";
|
|
485
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
464
486
|
candidate: screeningCandidate,
|
|
465
487
|
criteria,
|
|
466
488
|
config: llmConfig,
|
|
@@ -468,7 +490,7 @@ export async function runRecruitWorkflow({
|
|
|
468
490
|
imageEvidence: detailResult?.image_evidence || null,
|
|
469
491
|
maxImages: llmImageLimit,
|
|
470
492
|
imageDetail: llmImageDetail
|
|
471
|
-
});
|
|
493
|
+
}));
|
|
472
494
|
} catch (error) {
|
|
473
495
|
llmResult = createFailedLlmScreeningResult(error);
|
|
474
496
|
}
|
|
@@ -478,6 +500,7 @@ export async function runRecruitWorkflow({
|
|
|
478
500
|
const screening = useLlmScreening
|
|
479
501
|
? llmResultToScreening(llmResult, screeningCandidate)
|
|
480
502
|
: screenCandidate(screeningCandidate, { criteria });
|
|
503
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
481
504
|
const compactResult = {
|
|
482
505
|
index,
|
|
483
506
|
candidate_key: candidateKey,
|
|
@@ -485,7 +508,8 @@ export async function runRecruitWorkflow({
|
|
|
485
508
|
candidate: compactCandidate(screeningCandidate),
|
|
486
509
|
detail: compactDetail(detailResult),
|
|
487
510
|
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
488
|
-
screening: compactScreening(screening)
|
|
511
|
+
screening: compactScreening(screening),
|
|
512
|
+
timings
|
|
489
513
|
};
|
|
490
514
|
results.push(compactResult);
|
|
491
515
|
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
@@ -514,6 +538,7 @@ export async function runRecruitWorkflow({
|
|
|
514
538
|
last_candidate_key: candidateKey,
|
|
515
539
|
last_score: screening.score
|
|
516
540
|
});
|
|
541
|
+
const checkpointStarted = Date.now();
|
|
517
542
|
runControl.checkpoint({
|
|
518
543
|
results,
|
|
519
544
|
last_candidate: {
|
|
@@ -528,9 +553,13 @@ export async function runRecruitWorkflow({
|
|
|
528
553
|
llm_screening: compactScreeningLlmResult(llmResult)
|
|
529
554
|
}
|
|
530
555
|
});
|
|
556
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
531
557
|
|
|
532
558
|
if (delayMs > 0) {
|
|
559
|
+
const sleepStarted = Date.now();
|
|
533
560
|
await runControl.sleep(delayMs);
|
|
561
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
562
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
534
563
|
}
|
|
535
564
|
}
|
|
536
565
|
|
|
@@ -593,6 +622,7 @@ export function createRecruitRunService({
|
|
|
593
622
|
llmTimeoutMs = 120000,
|
|
594
623
|
llmImageLimit = 8,
|
|
595
624
|
llmImageDetail = "high",
|
|
625
|
+
imageOutputDir = "",
|
|
596
626
|
name = "recruit-domain-run"
|
|
597
627
|
} = {}) {
|
|
598
628
|
if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
|
|
@@ -628,7 +658,8 @@ export function createRecruitRunService({
|
|
|
628
658
|
llm_configured: Boolean(llmConfig),
|
|
629
659
|
llm_timeout_ms: llmTimeoutMs,
|
|
630
660
|
llm_image_limit: llmImageLimit,
|
|
631
|
-
llm_image_detail: llmImageDetail
|
|
661
|
+
llm_image_detail: llmImageDetail,
|
|
662
|
+
image_output_dir: imageOutputDir || ""
|
|
632
663
|
},
|
|
633
664
|
progress: {
|
|
634
665
|
card_count: 0,
|
|
@@ -668,7 +699,8 @@ export function createRecruitRunService({
|
|
|
668
699
|
llmConfig,
|
|
669
700
|
llmTimeoutMs,
|
|
670
701
|
llmImageLimit,
|
|
671
|
-
llmImageDetail
|
|
702
|
+
llmImageDetail,
|
|
703
|
+
imageOutputDir
|
|
672
704
|
}, runControl)
|
|
673
705
|
});
|
|
674
706
|
}
|
package/src/recommend-mcp.js
CHANGED
|
@@ -1048,6 +1048,7 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
|
|
|
1048
1048
|
llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
|
|
1049
1049
|
llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
|
|
1050
1050
|
llmImageDetail: normalizeText(args.llm_image_detail) || "high",
|
|
1051
|
+
imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
|
|
1051
1052
|
name: "mcp-recommend-pipeline-run",
|
|
1052
1053
|
parsed
|
|
1053
1054
|
};
|
package/src/recruit-mcp.js
CHANGED
|
@@ -837,6 +837,7 @@ function getRunOptions(args, parsed, session, configResolution = null) {
|
|
|
837
837
|
llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
|
|
838
838
|
llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
|
|
839
839
|
llmImageDetail: normalizeText(args.llm_image_detail) || "high",
|
|
840
|
+
imageOutputDir: resolveBossConfiguredOutputDir("", getRecruitRunsDir()),
|
|
840
841
|
name: "mcp-recruit-pipeline-run"
|
|
841
842
|
};
|
|
842
843
|
}
|