@reconcrap/boss-recommend-mcp 2.0.30 → 2.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -3
- package/config/screening-config.example.json +4 -2
- package/package.json +3 -1
- package/src/chat-mcp.js +5 -2
- package/src/chat-runtime-config.js +81 -24
- package/src/cli.js +14 -1
- package/src/core/cv-acquisition/index.js +1 -0
- package/src/core/cv-capture-target/index.js +299 -0
- package/src/core/screening/index.js +208 -73
- package/src/domains/chat/run-service.js +26 -6
- package/src/domains/recommend/detail.js +28 -4
- package/src/domains/recommend/run-service.js +22 -10
- package/src/domains/recruit/detail.js +28 -4
- package/src/domains/recruit/run-service.js +21 -9
- package/src/index.js +1 -1
- package/src/recommend-mcp.js +2 -1
- package/src/recruit-mcp.js +2 -0
|
@@ -58,6 +58,74 @@ function buildChatCompletionsUrl(baseUrl) {
|
|
|
58
58
|
return `${normalized}/chat/completions`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function redactBaseUrl(baseUrl) {
|
|
62
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
63
|
+
return normalized ? normalized.replace(/\/\/[^/]+/, "//[redacted-host]") : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function firstConfiguredValue(...values) {
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
if (value === undefined || value === null) continue;
|
|
69
|
+
if (typeof value === "string" && !value.trim()) continue;
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeLlmProviderEntry(rawEntry, inherited = {}, index = 0) {
|
|
76
|
+
const entry = typeof rawEntry === "string"
|
|
77
|
+
? { model: rawEntry }
|
|
78
|
+
: (rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {});
|
|
79
|
+
const providerName = firstConfiguredValue(
|
|
80
|
+
entry.name,
|
|
81
|
+
entry.label,
|
|
82
|
+
entry.id,
|
|
83
|
+
entry.providerName,
|
|
84
|
+
entry.provider,
|
|
85
|
+
""
|
|
86
|
+
);
|
|
87
|
+
const next = {
|
|
88
|
+
...inherited,
|
|
89
|
+
...entry,
|
|
90
|
+
baseUrl: firstConfiguredValue(entry.baseUrl, entry.base_url, inherited.baseUrl, inherited.base_url),
|
|
91
|
+
apiKey: firstConfiguredValue(entry.apiKey, entry.api_key, inherited.apiKey, inherited.api_key),
|
|
92
|
+
model: firstConfiguredValue(entry.model, entry.modelName, entry.model_name, typeof rawEntry === "string" ? rawEntry : "", inherited.model),
|
|
93
|
+
openaiOrganization: firstConfiguredValue(entry.openaiOrganization, entry.organization, inherited.openaiOrganization, inherited.organization),
|
|
94
|
+
openaiProject: firstConfiguredValue(entry.openaiProject, entry.project, inherited.openaiProject, inherited.project),
|
|
95
|
+
topP: firstConfiguredValue(entry.topP, entry.top_p, inherited.topP, inherited.top_p),
|
|
96
|
+
llmProviderName: normalizeText(providerName),
|
|
97
|
+
llmProviderIndex: index
|
|
98
|
+
};
|
|
99
|
+
delete next.llmModels;
|
|
100
|
+
delete next.models;
|
|
101
|
+
return next;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeLlmProviderConfigs(config = {}) {
|
|
105
|
+
if (Array.isArray(config)) {
|
|
106
|
+
return config.map((entry, index) => normalizeLlmProviderEntry(entry, {}, index));
|
|
107
|
+
}
|
|
108
|
+
const inherited = config && typeof config === "object" && !Array.isArray(config) ? { ...config } : {};
|
|
109
|
+
const rawProviders = Array.isArray(inherited.llmModels) && inherited.llmModels.length > 0
|
|
110
|
+
? inherited.llmModels
|
|
111
|
+
: (Array.isArray(inherited.models) && inherited.models.length > 0 ? inherited.models : [inherited]);
|
|
112
|
+
delete inherited.llmModels;
|
|
113
|
+
delete inherited.models;
|
|
114
|
+
return rawProviders.map((entry, index) => normalizeLlmProviderEntry(entry, inherited, index));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compactLlmProviderFailure(error, providerConfig = {}, providerIndex = 0) {
|
|
118
|
+
return {
|
|
119
|
+
index: providerIndex + 1,
|
|
120
|
+
name: normalizeText(providerConfig.llmProviderName || providerConfig.name || providerConfig.label || providerConfig.id) || null,
|
|
121
|
+
baseUrl: redactBaseUrl(providerConfig.baseUrl),
|
|
122
|
+
model: normalizeText(providerConfig.model) || null,
|
|
123
|
+
status: Number.isFinite(Number(error?.status)) ? Number(error.status) : null,
|
|
124
|
+
attempts: Number(error?.llm_attempt_count) || 0,
|
|
125
|
+
message: String(error?.message || error || "").slice(0, 500)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
function isVolcengineModel(baseUrl, model) {
|
|
62
130
|
return /volces|volcengine|ark\.cn|doubao|seed/i.test(`${baseUrl || ""} ${model || ""}`);
|
|
63
131
|
}
|
|
@@ -1325,6 +1393,8 @@ export function compactScreeningLlmResult(llmResult) {
|
|
|
1325
1393
|
finish_reason: llmResult.finish_reason || null,
|
|
1326
1394
|
image_input_count: llmResult.image_input_count || 0,
|
|
1327
1395
|
attempt_count: llmResult.attempt_count || 0,
|
|
1396
|
+
fallback_count: llmResult.fallback_count || 0,
|
|
1397
|
+
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
1328
1398
|
error: llmResult.error || null,
|
|
1329
1399
|
screened_at: llmResult.screened_at || null
|
|
1330
1400
|
};
|
|
@@ -1358,6 +1428,8 @@ export function createFailedLlmScreeningResult(error) {
|
|
|
1358
1428
|
image_input_count: Number(error?.image_input_count) || 0,
|
|
1359
1429
|
image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
|
|
1360
1430
|
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
1431
|
+
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
1432
|
+
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
1361
1433
|
error: error?.message || String(error || "unknown"),
|
|
1362
1434
|
screened_at: nowIso()
|
|
1363
1435
|
};
|
|
@@ -1435,7 +1507,7 @@ function sleepMs(ms) {
|
|
|
1435
1507
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
1436
1508
|
}
|
|
1437
1509
|
|
|
1438
|
-
|
|
1510
|
+
async function callScreeningLlmWithProvider({
|
|
1439
1511
|
candidate,
|
|
1440
1512
|
criteria,
|
|
1441
1513
|
config = {},
|
|
@@ -1488,81 +1560,89 @@ export async function callScreeningLlm({
|
|
|
1488
1560
|
thinkingLevel
|
|
1489
1561
|
});
|
|
1490
1562
|
|
|
1563
|
+
const effectiveTimeoutMs = parsePositiveNumber(config.llmTimeoutMs ?? config.timeoutMs, timeoutMs) || timeoutMs;
|
|
1491
1564
|
const maxRetries = normalizeLlmMaxRetries(config.llmMaxRetries ?? config.maxRetries);
|
|
1492
1565
|
const maxAttempts = maxRetries + 1;
|
|
1493
1566
|
let lastError = null;
|
|
1494
1567
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1495
1568
|
const controller = new AbortController();
|
|
1496
|
-
const timer = setTimeout(() => controller.abort(),
|
|
1569
|
+
const timer = setTimeout(() => controller.abort(), effectiveTimeoutMs);
|
|
1497
1570
|
try {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1571
|
+
const headers = {
|
|
1572
|
+
"Content-Type": "application/json",
|
|
1573
|
+
Authorization: `Bearer ${apiKey}`
|
|
1574
|
+
};
|
|
1575
|
+
if (config.openaiOrganization) headers["OpenAI-Organization"] = config.openaiOrganization;
|
|
1576
|
+
if (config.openaiProject) headers["OpenAI-Project"] = config.openaiProject;
|
|
1577
|
+
|
|
1578
|
+
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
1579
|
+
method: "POST",
|
|
1580
|
+
headers,
|
|
1581
|
+
body: JSON.stringify(payload),
|
|
1582
|
+
signal: controller.signal
|
|
1583
|
+
});
|
|
1584
|
+
const responseText = await response.text();
|
|
1585
|
+
if (!response.ok) {
|
|
1586
|
+
const error = new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
|
|
1587
|
+
error.status = response.status;
|
|
1588
|
+
throw error;
|
|
1589
|
+
}
|
|
1590
|
+
const json = tryParseJson(responseText);
|
|
1591
|
+
if (!json) {
|
|
1592
|
+
throw new Error("LLM response was not valid JSON");
|
|
1593
|
+
}
|
|
1594
|
+
const choice = json?.choices?.[0] || {};
|
|
1595
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
1596
|
+
const reasoningContent = collectLlmReasoningText(choice);
|
|
1597
|
+
const parsed = tryExtractJsonObject(content) || tryExtractJsonObject(reasoningContent);
|
|
1598
|
+
const passed = parsePassedDecision(parsed?.passed);
|
|
1599
|
+
if (passed === null) {
|
|
1600
|
+
throw new Error(`LLM response missing boolean passed decision: ${content.slice(0, 240)}`);
|
|
1601
|
+
}
|
|
1602
|
+
const evidence = Array.isArray(parsed?.evidence)
|
|
1603
|
+
? parsed.evidence.map(normalizeText).filter(Boolean)
|
|
1604
|
+
: [];
|
|
1605
|
+
const decisionCot = firstUsefulLine([
|
|
1606
|
+
parsed?.cot,
|
|
1607
|
+
parsed?.decision_cot,
|
|
1608
|
+
parsed?.reasoning,
|
|
1609
|
+
parsed?.chain_of_thought,
|
|
1610
|
+
reasoningContent
|
|
1611
|
+
].map(normalizeBlockText).filter(Boolean)) || reasoningContent;
|
|
1612
|
+
const providerName = normalizeText(config.llmProviderName || config.name || config.label || config.id);
|
|
1613
|
+
const providerIndex = Number.isFinite(Number(config.llmProviderIndex)) ? Number(config.llmProviderIndex) : 0;
|
|
1614
|
+
const providerCount = Number.isFinite(Number(config.llmProviderCount)) ? Number(config.llmProviderCount) : 1;
|
|
1615
|
+
return {
|
|
1616
|
+
ok: true,
|
|
1617
|
+
provider: {
|
|
1618
|
+
baseUrl: redactBaseUrl(baseUrl),
|
|
1619
|
+
model,
|
|
1620
|
+
name: providerName || null,
|
|
1621
|
+
index: providerIndex + 1,
|
|
1622
|
+
total: providerCount,
|
|
1623
|
+
thinking_level: normalizeLlmThinkingLevel(thinkingLevel) || "low",
|
|
1624
|
+
thinking: payload.thinking || null,
|
|
1625
|
+
reasoning_effort: payload.reasoning_effort || null,
|
|
1626
|
+
max_tokens: payload.max_tokens,
|
|
1627
|
+
max_completion_tokens: payload.max_completion_tokens || null
|
|
1628
|
+
},
|
|
1629
|
+
passed,
|
|
1630
|
+
reason: "",
|
|
1631
|
+
evidence,
|
|
1632
|
+
cot: decisionCot,
|
|
1633
|
+
decision_cot: decisionCot,
|
|
1634
|
+
reasoning_content: reasoningContent,
|
|
1635
|
+
raw_model_output: content,
|
|
1636
|
+
usage: json.usage || null,
|
|
1637
|
+
finish_reason: choice.finish_reason || null,
|
|
1638
|
+
raw_content_length: content.length,
|
|
1639
|
+
image_input_count: imageInputs.length,
|
|
1640
|
+
image_inputs: summarizeLlmImageInputs(imageInputs),
|
|
1641
|
+
attempt_count: attempt,
|
|
1642
|
+
provider_attempt_count: attempt,
|
|
1643
|
+
screened_at: nowIso()
|
|
1644
|
+
};
|
|
1645
|
+
} catch (error) {
|
|
1566
1646
|
lastError = error;
|
|
1567
1647
|
if (attempt >= maxAttempts || !isRetryableLlmRequestError(error)) {
|
|
1568
1648
|
error.image_input_count = imageInputs.length;
|
|
@@ -1571,9 +1651,9 @@ export async function callScreeningLlm({
|
|
|
1571
1651
|
throw error;
|
|
1572
1652
|
}
|
|
1573
1653
|
await sleepMs(Math.min(2500, 500 * attempt));
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1654
|
+
} finally {
|
|
1655
|
+
clearTimeout(timer);
|
|
1656
|
+
}
|
|
1577
1657
|
}
|
|
1578
1658
|
lastError = lastError || new Error("LLM request failed without response");
|
|
1579
1659
|
lastError.image_input_count = imageInputs.length;
|
|
@@ -1581,3 +1661,58 @@ export async function callScreeningLlm({
|
|
|
1581
1661
|
lastError.llm_attempt_count = maxAttempts;
|
|
1582
1662
|
throw lastError;
|
|
1583
1663
|
}
|
|
1664
|
+
|
|
1665
|
+
export async function callScreeningLlm(args = {}) {
|
|
1666
|
+
const providers = normalizeLlmProviderConfigs(args.config || {});
|
|
1667
|
+
if (providers.length <= 1) {
|
|
1668
|
+
return callScreeningLlmWithProvider({
|
|
1669
|
+
...args,
|
|
1670
|
+
config: {
|
|
1671
|
+
...(providers[0] || args.config || {}),
|
|
1672
|
+
llmProviderCount: 1
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const providerFailures = [];
|
|
1678
|
+
let lastError = null;
|
|
1679
|
+
for (let index = 0; index < providers.length; index += 1) {
|
|
1680
|
+
const providerConfig = {
|
|
1681
|
+
...providers[index],
|
|
1682
|
+
llmProviderIndex: index,
|
|
1683
|
+
llmProviderCount: providers.length
|
|
1684
|
+
};
|
|
1685
|
+
try {
|
|
1686
|
+
const previousAttempts = providerFailures.reduce((sum, item) => sum + (Number(item.attempts) || 0), 0);
|
|
1687
|
+
const result = await callScreeningLlmWithProvider({
|
|
1688
|
+
...args,
|
|
1689
|
+
config: providerConfig
|
|
1690
|
+
});
|
|
1691
|
+
const providerAttempts = Number(result.provider_attempt_count ?? result.attempt_count) || 0;
|
|
1692
|
+
return {
|
|
1693
|
+
...result,
|
|
1694
|
+
attempt_count: previousAttempts + providerAttempts,
|
|
1695
|
+
llm_model_failures: providerFailures,
|
|
1696
|
+
fallback_count: providerFailures.length
|
|
1697
|
+
};
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
lastError = error;
|
|
1700
|
+
providerFailures.push(compactLlmProviderFailure(error, providerConfig, index));
|
|
1701
|
+
if (index < providers.length - 1) {
|
|
1702
|
+
await sleepMs(Math.min(1500, 250 * (index + 1)));
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const totalAttempts = providerFailures.reduce((sum, item) => sum + (Number(item.attempts) || 0), 0);
|
|
1708
|
+
const finalError = new Error(
|
|
1709
|
+
`All configured LLM models failed (${providers.length}); last error: ${lastError?.message || "unknown error"}`
|
|
1710
|
+
);
|
|
1711
|
+
finalError.cause = lastError || null;
|
|
1712
|
+
finalError.llm_provider_failures = providerFailures;
|
|
1713
|
+
finalError.llm_model_failures = providerFailures;
|
|
1714
|
+
finalError.llm_attempt_count = totalAttempts;
|
|
1715
|
+
finalError.image_input_count = lastError?.image_input_count || 0;
|
|
1716
|
+
finalError.image_inputs = lastError?.image_inputs || [];
|
|
1717
|
+
throw finalError;
|
|
1718
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
2
|
+
import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
|
|
2
3
|
import {
|
|
3
4
|
clickPoint,
|
|
4
5
|
getNodeBox,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
compactCvAcquisitionState,
|
|
10
11
|
countParsedNetworkProfiles,
|
|
11
12
|
createCvAcquisitionState,
|
|
13
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
12
14
|
getCvNetworkWaitPlan,
|
|
13
15
|
recordCvImageFallback,
|
|
14
16
|
recordCvNetworkHit,
|
|
@@ -109,6 +111,8 @@ function compactLlmResult(llmResult) {
|
|
|
109
111
|
finish_reason: llmResult.finish_reason || null,
|
|
110
112
|
image_input_count: llmResult.image_input_count || 0,
|
|
111
113
|
attempt_count: llmResult.attempt_count || 0,
|
|
114
|
+
fallback_count: llmResult.fallback_count || 0,
|
|
115
|
+
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
112
116
|
error: llmResult.error || null
|
|
113
117
|
};
|
|
114
118
|
}
|
|
@@ -203,9 +207,9 @@ function llmToScreening(llmResult, candidate) {
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
export function captureNodeIdFromResumeState(resumeState) {
|
|
206
|
-
return resumeState?.
|
|
207
|
-
|| resumeState?.content?.node_id
|
|
210
|
+
return resumeState?.content?.node_id
|
|
208
211
|
|| resumeState?.resumeIframe?.node_id
|
|
212
|
+
|| resumeState?.popup?.node_id
|
|
209
213
|
|| null;
|
|
210
214
|
}
|
|
211
215
|
|
|
@@ -287,6 +291,9 @@ function createFailedLlmResult(error) {
|
|
|
287
291
|
decision_cot: "",
|
|
288
292
|
reasoning_content: "",
|
|
289
293
|
raw_model_output: "",
|
|
294
|
+
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
295
|
+
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
296
|
+
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
290
297
|
error: error?.message || String(error || "unknown"),
|
|
291
298
|
screened_at: new Date().toISOString()
|
|
292
299
|
};
|
|
@@ -636,7 +643,7 @@ export async function runChatWorkflow({
|
|
|
636
643
|
readyTimeoutMs = 60000,
|
|
637
644
|
onlineResumeButtonTimeoutMs = 30000,
|
|
638
645
|
resumeDomTimeoutMs = 60000,
|
|
639
|
-
maxImagePages =
|
|
646
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
640
647
|
imageWheelDeltaY = 650,
|
|
641
648
|
cvAcquisitionMode = "unknown",
|
|
642
649
|
callLlmOnImage = false,
|
|
@@ -1226,11 +1233,19 @@ export async function runChatWorkflow({
|
|
|
1226
1233
|
let source = normalizedDetailSource === "dom" ? "dom" : "network";
|
|
1227
1234
|
let imageEvidence = null;
|
|
1228
1235
|
let llmResult = null;
|
|
1229
|
-
|
|
1236
|
+
let captureTarget = null;
|
|
1237
|
+
let captureTargetWait = null;
|
|
1230
1238
|
let fullCvEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
|
|
1231
1239
|
const shouldCaptureImage = normalizedDetailSource === "image"
|
|
1232
1240
|
|| (normalizedDetailSource === "cascade" && !fullCvEvidence.full_cv_acquired);
|
|
1233
1241
|
if (shouldCaptureImage) {
|
|
1242
|
+
captureTargetWait = await waitForCvCaptureTarget(client, resumeState, {
|
|
1243
|
+
domain: "chat",
|
|
1244
|
+
timeoutMs: 6000,
|
|
1245
|
+
intervalMs: 250
|
|
1246
|
+
});
|
|
1247
|
+
captureTarget = captureTargetWait.target || null;
|
|
1248
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
1234
1249
|
if (captureNodeId) {
|
|
1235
1250
|
detailStep = "capture_image_fallback";
|
|
1236
1251
|
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
@@ -1272,7 +1287,9 @@ export async function runChatWorkflow({
|
|
|
1272
1287
|
? "forced_image"
|
|
1273
1288
|
: "network_miss_image_fallback",
|
|
1274
1289
|
run_candidate_index: index,
|
|
1275
|
-
candidate_key: candidateKey
|
|
1290
|
+
candidate_key: candidateKey,
|
|
1291
|
+
capture_target: captureTarget,
|
|
1292
|
+
capture_target_wait: captureTargetWait
|
|
1276
1293
|
}
|
|
1277
1294
|
}));
|
|
1278
1295
|
source = "image";
|
|
@@ -1416,6 +1433,8 @@ export async function runChatWorkflow({
|
|
|
1416
1433
|
},
|
|
1417
1434
|
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
1418
1435
|
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
1436
|
+
capture_target: captureTarget || null,
|
|
1437
|
+
capture_target_wait: captureTargetWait,
|
|
1419
1438
|
full_cv_evidence: fullCvEvidence
|
|
1420
1439
|
};
|
|
1421
1440
|
}
|
|
@@ -1600,7 +1619,7 @@ export function createChatRunService({
|
|
|
1600
1619
|
readyTimeoutMs = 60000,
|
|
1601
1620
|
onlineResumeButtonTimeoutMs = 30000,
|
|
1602
1621
|
resumeDomTimeoutMs = 60000,
|
|
1603
|
-
maxImagePages =
|
|
1622
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1604
1623
|
imageWheelDeltaY = 650,
|
|
1605
1624
|
cvAcquisitionMode = "unknown",
|
|
1606
1625
|
callLlmOnImage = false,
|
|
@@ -1638,6 +1657,7 @@ export function createChatRunService({
|
|
|
1638
1657
|
close_resume: closeResume,
|
|
1639
1658
|
request_resume_for_passed: Boolean(requestResumeForPassed),
|
|
1640
1659
|
dry_run_request_cv: Boolean(dryRunRequestCv),
|
|
1660
|
+
greeting_text: greetingText,
|
|
1641
1661
|
cv_acquisition_mode: cvAcquisitionMode,
|
|
1642
1662
|
call_llm_on_image: Boolean(callLlmOnImage),
|
|
1643
1663
|
screening_mode: normalizedScreeningMode,
|
|
@@ -20,8 +20,7 @@ import {
|
|
|
20
20
|
DETAIL_RESUME_IFRAME_SELECTORS
|
|
21
21
|
} from "./constants.js";
|
|
22
22
|
import {
|
|
23
|
-
getRecommendRoots
|
|
24
|
-
queryFirstAcrossRoots
|
|
23
|
+
getRecommendRoots
|
|
25
24
|
} from "./roots.js";
|
|
26
25
|
import {
|
|
27
26
|
findRecommendCardNodeIds,
|
|
@@ -133,8 +132,8 @@ export async function waitForRecommendDetail(client, {
|
|
|
133
132
|
let lastState = null;
|
|
134
133
|
while (Date.now() - started <= timeoutMs) {
|
|
135
134
|
const rootState = await getRecommendRoots(client);
|
|
136
|
-
const popup = await
|
|
137
|
-
const resumeIframe = await
|
|
135
|
+
const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
|
|
136
|
+
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
|
|
138
137
|
lastState = {
|
|
139
138
|
iframe: rootState.iframe,
|
|
140
139
|
roots: rootState.roots,
|
|
@@ -147,6 +146,31 @@ export async function waitForRecommendDetail(client, {
|
|
|
147
146
|
return lastState;
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
async function findVisibleDetailTarget(client, roots, selectors) {
|
|
150
|
+
for (const root of roots) {
|
|
151
|
+
if (!root?.nodeId) continue;
|
|
152
|
+
for (const selector of selectors) {
|
|
153
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
154
|
+
for (const nodeId of nodeIds) {
|
|
155
|
+
try {
|
|
156
|
+
const box = await getNodeBox(client, nodeId);
|
|
157
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
158
|
+
return {
|
|
159
|
+
root: root.name,
|
|
160
|
+
root_node_id: root.nodeId,
|
|
161
|
+
selector,
|
|
162
|
+
node_id: nodeId,
|
|
163
|
+
center: box.center,
|
|
164
|
+
rect: box.rect
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
export async function readRecommendDetailHtml(client, detailState) {
|
|
151
175
|
let popupHTML = "";
|
|
152
176
|
let resumeHTML = "";
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
measureTiming
|
|
8
8
|
} from "../../core/run/timing.js";
|
|
9
9
|
import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
|
|
10
|
+
import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
|
|
10
11
|
import { sleep } from "../../core/browser/index.js";
|
|
11
12
|
import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
|
|
12
13
|
import {
|
|
13
14
|
compactCvAcquisitionState,
|
|
14
15
|
countParsedNetworkProfiles,
|
|
15
16
|
createCvAcquisitionState,
|
|
17
|
+
DEFAULT_MAX_IMAGE_PAGES,
|
|
16
18
|
getCvNetworkWaitPlan,
|
|
17
19
|
recordCvImageFallback,
|
|
18
20
|
recordCvNetworkHit,
|
|
@@ -404,7 +406,7 @@ export function createRecoverableImageCaptureEvidence(error, {
|
|
|
404
406
|
elapsedMs = 0,
|
|
405
407
|
filePath = "",
|
|
406
408
|
extension = "jpg",
|
|
407
|
-
maxScreenshots =
|
|
409
|
+
maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
|
|
408
410
|
} = {}) {
|
|
409
411
|
const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
|
|
410
412
|
return {
|
|
@@ -474,7 +476,7 @@ export async function runRecommendWorkflow({
|
|
|
474
476
|
closeDetail = true,
|
|
475
477
|
delayMs = 0,
|
|
476
478
|
cardTimeoutMs = 10000,
|
|
477
|
-
maxImagePages =
|
|
479
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
478
480
|
imageWheelDeltaY = 650,
|
|
479
481
|
cvAcquisitionMode = "unknown",
|
|
480
482
|
listMaxScrolls = 20,
|
|
@@ -820,15 +822,21 @@ export async function runRecommendWorkflow({
|
|
|
820
822
|
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
821
823
|
let source = "network";
|
|
822
824
|
let imageEvidence = null;
|
|
825
|
+
let captureTarget = null;
|
|
826
|
+
let captureTargetWait = null;
|
|
823
827
|
if (parsedNetworkProfileCount > 0) {
|
|
824
828
|
recordCvNetworkHit(cvAcquisitionState, {
|
|
825
829
|
parsedNetworkProfileCount,
|
|
826
830
|
waitResult: networkWait
|
|
827
831
|
});
|
|
828
832
|
} else {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
833
|
+
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
834
|
+
domain: "recommend",
|
|
835
|
+
timeoutMs: 6000,
|
|
836
|
+
intervalMs: 250
|
|
837
|
+
});
|
|
838
|
+
captureTarget = captureTargetWait.target || null;
|
|
839
|
+
const captureNodeId = captureTarget?.node_id || null;
|
|
832
840
|
if (captureNodeId) {
|
|
833
841
|
const imageEvidencePath = imageEvidenceFilePath({
|
|
834
842
|
imageOutputDir,
|
|
@@ -844,8 +852,8 @@ export async function runRecommendWorkflow({
|
|
|
844
852
|
quality: 72,
|
|
845
853
|
optimize: true,
|
|
846
854
|
resizeMaxWidth: 1100,
|
|
847
|
-
captureViewport:
|
|
848
|
-
padding:
|
|
855
|
+
captureViewport: false,
|
|
856
|
+
padding: 0,
|
|
849
857
|
maxScreenshots: maxImagePages,
|
|
850
858
|
wheelDeltaY: imageWheelDeltaY,
|
|
851
859
|
settleMs: 350,
|
|
@@ -863,7 +871,9 @@ export async function runRecommendWorkflow({
|
|
|
863
871
|
capture_mode: "scroll_sequence",
|
|
864
872
|
acquisition_reason: "network_miss_image_fallback",
|
|
865
873
|
run_candidate_index: index,
|
|
866
|
-
candidate_key: candidateKey
|
|
874
|
+
candidate_key: candidateKey,
|
|
875
|
+
capture_target: captureTarget,
|
|
876
|
+
capture_target_wait: captureTargetWait
|
|
867
877
|
}
|
|
868
878
|
}));
|
|
869
879
|
source = "image";
|
|
@@ -902,7 +912,9 @@ export async function runRecommendWorkflow({
|
|
|
902
912
|
wait_plan: waitPlan,
|
|
903
913
|
network_wait: networkWait,
|
|
904
914
|
parsed_network_profile_count: parsedNetworkProfileCount,
|
|
905
|
-
image_evidence: summarizeImageEvidence(imageEvidence)
|
|
915
|
+
image_evidence: summarizeImageEvidence(imageEvidence),
|
|
916
|
+
capture_target: captureTarget || null,
|
|
917
|
+
capture_target_wait: captureTargetWait
|
|
906
918
|
};
|
|
907
919
|
screeningCandidate = detailResult.candidate;
|
|
908
920
|
} catch (error) {
|
|
@@ -1120,7 +1132,7 @@ export function createRecommendRunService({
|
|
|
1120
1132
|
closeDetail = true,
|
|
1121
1133
|
delayMs = 0,
|
|
1122
1134
|
cardTimeoutMs = 10000,
|
|
1123
|
-
maxImagePages =
|
|
1135
|
+
maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
|
|
1124
1136
|
imageWheelDeltaY = 650,
|
|
1125
1137
|
cvAcquisitionMode = "unknown",
|
|
1126
1138
|
listMaxScrolls = 20,
|
|
@@ -19,8 +19,7 @@ import {
|
|
|
19
19
|
RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
|
|
20
20
|
} from "./constants.js";
|
|
21
21
|
import {
|
|
22
|
-
getRecruitRoots
|
|
23
|
-
queryFirstAcrossRoots
|
|
22
|
+
getRecruitRoots
|
|
24
23
|
} from "./roots.js";
|
|
25
24
|
|
|
26
25
|
export function matchesRecruitDetailNetwork(url) {
|
|
@@ -128,8 +127,8 @@ export async function waitForRecruitDetail(client, {
|
|
|
128
127
|
let lastState = null;
|
|
129
128
|
while (Date.now() - started <= timeoutMs) {
|
|
130
129
|
const rootState = await getRecruitRoots(client);
|
|
131
|
-
const popup = await
|
|
132
|
-
const resumeIframe = await
|
|
130
|
+
const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
|
|
131
|
+
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
|
|
133
132
|
lastState = {
|
|
134
133
|
iframe: rootState.iframe,
|
|
135
134
|
roots: rootState.roots,
|
|
@@ -142,6 +141,31 @@ export async function waitForRecruitDetail(client, {
|
|
|
142
141
|
return lastState;
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
async function findVisibleDetailTarget(client, roots, selectors) {
|
|
145
|
+
for (const root of roots) {
|
|
146
|
+
if (!root?.nodeId) continue;
|
|
147
|
+
for (const selector of selectors) {
|
|
148
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
149
|
+
for (const nodeId of nodeIds) {
|
|
150
|
+
try {
|
|
151
|
+
const box = await getNodeBox(client, nodeId);
|
|
152
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
153
|
+
return {
|
|
154
|
+
root: root.name,
|
|
155
|
+
root_node_id: root.nodeId,
|
|
156
|
+
selector,
|
|
157
|
+
node_id: nodeId,
|
|
158
|
+
center: box.center,
|
|
159
|
+
rect: box.rect
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
export async function readRecruitDetailHtml(client, detailState) {
|
|
146
170
|
let popupHTML = "";
|
|
147
171
|
let resumeHTML = "";
|