@reconcrap/boss-recommend-mcp 1.3.30 → 1.3.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.
@@ -18,7 +18,14 @@ import { __testables as indexTestables } from "./index.js";
18
18
  import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
19
19
  import { __testables as vendorCliTestables } from "../vendor/boss-chat-cli/src/cli.js";
20
20
  import { BossChatPage } from "../vendor/boss-chat-cli/src/browser/chat-page.js";
21
- import { LlmClient, parseLlmJson } from "../vendor/boss-chat-cli/src/services/llm.js";
21
+ import { LlmClient, parseLlmJson, __testables as llmTestables } from "../vendor/boss-chat-cli/src/services/llm.js";
22
+ import { ReportStore } from "../vendor/boss-chat-cli/src/services/report-store.js";
23
+ import {
24
+ NETWORK_RESUME_IMAGE_MODE_GRACE_MS,
25
+ NETWORK_RESUME_RETRY_WAIT_MS,
26
+ NETWORK_RESUME_WAIT_MS,
27
+ ResumeNetworkTracker,
28
+ } from "../vendor/boss-chat-cli/src/services/resume-network.js";
22
29
 
23
30
  const { handleRequest } = indexTestables;
24
31
 
@@ -29,6 +36,7 @@ const TOOL_BOSS_CHAT_GET_RUN = "get_boss_chat_run";
29
36
  const TOOL_BOSS_CHAT_PAUSE_RUN = "pause_boss_chat_run";
30
37
  const TOOL_BOSS_CHAT_RESUME_RUN = "resume_boss_chat_run";
31
38
  const TOOL_BOSS_CHAT_CANCEL_RUN = "cancel_boss_chat_run";
39
+ const { extractCompletionReasoningText, extractResponsesReasoningText } = llmTestables;
32
40
 
33
41
  function makeToolCall(id, name, args = {}) {
34
42
  return {
@@ -58,6 +66,8 @@ function createBossChatTestWorkspace() {
58
66
  baseUrl: "https://api.example.com/v1",
59
67
  apiKey: "sk-test-key",
60
68
  model: "gpt-4.1-mini",
69
+ llmTimeoutMs: 65000,
70
+ llmMaxRetries: 4,
61
71
  debugPort: 9666
62
72
  }, null, 2));
63
73
 
@@ -254,6 +264,8 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
254
264
  assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
255
265
  assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
256
266
  assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
267
+ assert.equal(stateAfterPrepare.last_prepare_args["llm-timeout-ms"], "65000");
268
+ assert.equal(stateAfterPrepare.last_prepare_args["llm-max-retries"], "4");
257
269
 
258
270
  const started = await startBossChatRun({
259
271
  workspaceRoot,
@@ -278,6 +290,8 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
278
290
  assert.equal(stateAfterStart.last_start_args.apikey, "sk-test-key");
279
291
  assert.equal(stateAfterStart.last_start_args.model, "gpt-4.1-mini");
280
292
  assert.equal(stateAfterStart.last_start_args.port, "9666");
293
+ assert.equal(stateAfterStart.last_start_args["llm-timeout-ms"], "65000");
294
+ assert.equal(stateAfterStart.last_start_args["llm-max-retries"], "4");
281
295
 
282
296
  const startedAll = await startBossChatRun({
283
297
  workspaceRoot,
@@ -897,41 +911,87 @@ function testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig() {
897
911
  assert.equal(launchConfig.args[0], "-y");
898
912
  }
899
913
 
900
- function testBossChatLlmEvidenceGateShouldDemoteMissingEvidence() {
914
+ function testVendorBossChatCliShouldParseSharedLlmTransportArgs() {
915
+ const parsed = vendorCliTestables.parseArgs([
916
+ "start-run",
917
+ "--llm-timeout-ms",
918
+ "70000",
919
+ "--llm-max-retries",
920
+ "5",
921
+ ]);
922
+ assert.equal(parsed.command, "start-run");
923
+ assert.equal(parsed.overrides.llm.timeoutMs, 70000);
924
+ assert.equal(parsed.overrides.llm.maxRetries, 5);
925
+ }
926
+
927
+ function testBossChatLlmParserShouldAcceptMinimalDecisionJson() {
901
928
  const parsed = parseLlmJson(
902
929
  JSON.stringify({
903
930
  passed: true,
904
- reason: "命中标准",
905
- summary: "命中",
906
- evidence: [],
907
931
  }),
908
- {
909
- evidenceCorpus: "南京大学 机器学习 项目经历",
910
- },
911
932
  );
912
- assert.equal(parsed.rawPassed, true);
933
+ assert.equal(parsed.passed, true);
934
+ assert.equal(parsed.rawOutputText.includes('"passed":true'), true);
935
+ }
936
+
937
+ function testBossChatLlmParserShouldAcceptPlainPassFailText() {
938
+ const passed = parseLlmJson("PASS");
939
+ assert.equal(passed.passed, true);
940
+ const failed = parseLlmJson("false");
941
+ assert.equal(failed.passed, false);
942
+ }
943
+
944
+ function testBossChatLlmParserShouldAcceptDecisionField() {
945
+ const parsed = parseLlmJson(
946
+ JSON.stringify({
947
+ decision: "fail",
948
+ }),
949
+ );
913
950
  assert.equal(parsed.passed, false);
914
- assert.equal(parsed.evidenceGateDemoted, true);
915
- assert.equal(parsed.evidenceMatchedCount, 0);
916
951
  }
917
952
 
918
- function testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence() {
953
+ function testBossChatLlmParserShouldPreserveReasoningFields() {
919
954
  const parsed = parseLlmJson(
920
955
  JSON.stringify({
921
956
  passed: true,
922
- reason: "命中标准",
923
- summary: "命中",
924
- evidence: ["十年金融风控投研经验"],
957
+ reason: "候选人具备 2 段 AI Agent 项目经验",
958
+ summary: "符合筛选要求",
959
+ evidence: ["AI Agent", "MCP"],
925
960
  }),
926
961
  {
927
- evidenceCorpus: "南京大学 机器学习 项目经历",
962
+ reasoningText: "先检查项目经历,再核对技能栈,结论为通过。",
928
963
  },
929
964
  );
930
- assert.equal(parsed.rawPassed, true);
931
- assert.equal(parsed.passed, false);
932
- assert.equal(parsed.evidenceGateDemoted, true);
933
- assert.equal(parsed.evidenceRawCount, 1);
934
- assert.equal(parsed.evidenceMatchedCount, 0);
965
+ assert.equal(parsed.passed, true);
966
+ assert.equal(parsed.reason, "候选人具备 2 段 AI Agent 项目经验");
967
+ assert.equal(parsed.summary, "符合筛选要求");
968
+ assert.equal(parsed.cot, "先检查项目经历,再核对技能栈,结论为通过。");
969
+ assert.deepEqual(parsed.evidence, ["AI Agent", "MCP"]);
970
+ assert.equal(parsed.rawReasoningText, "先检查项目经历,再核对技能栈,结论为通过。");
971
+ }
972
+
973
+ function testBossChatLlmExtractorsShouldReadProviderReasoningFields() {
974
+ const completionReasoning = extractCompletionReasoningText({
975
+ choices: [
976
+ {
977
+ message: {
978
+ content: [{ type: "text", text: "{\"passed\":true}" }],
979
+ reasoning_content: [{ text: "先核对教育背景,再核对项目经历。" }],
980
+ },
981
+ },
982
+ ],
983
+ });
984
+ assert.equal(completionReasoning.includes("教育背景"), true);
985
+
986
+ const responsesReasoning = extractResponsesReasoningText({
987
+ output: [
988
+ {
989
+ type: "reasoning",
990
+ summary: [{ text: "根据项目经历与技能关键词判断为通过。" }],
991
+ },
992
+ ],
993
+ });
994
+ assert.equal(responsesReasoning.includes("技能关键词"), true);
935
995
  }
936
996
 
937
997
  async function testBossChatLlmTextChunkFallbackShouldWork() {
@@ -962,39 +1022,21 @@ async function testBossChatLlmTextChunkFallbackShouldWork() {
962
1022
  if (payload.chunkIndex === 2) {
963
1023
  return {
964
1024
  passed: true,
965
- rawPassed: true,
966
- reason: "命中分段证据",
967
- summary: "命中",
968
- evidence: ["PASS_MARKER_ABC"],
969
- evidenceRawCount: 1,
970
- evidenceMatchedCount: 1,
971
- evidenceGateDemoted: false,
1025
+ rawOutputText: '{"passed":true}',
972
1026
  chunkIndex: payload.chunkIndex,
973
1027
  chunkTotal: payload.chunkTotal,
974
1028
  };
975
1029
  }
976
1030
  return {
977
1031
  passed: false,
978
- rawPassed: false,
979
- reason: "本段证据不足",
980
- summary: "不足",
981
- evidence: [],
982
- evidenceRawCount: 0,
983
- evidenceMatchedCount: 0,
984
- evidenceGateDemoted: false,
1032
+ rawOutputText: '{"passed":false}',
985
1033
  chunkIndex: payload.chunkIndex,
986
1034
  chunkTotal: payload.chunkTotal,
987
1035
  };
988
1036
  }
989
1037
  return {
990
1038
  passed: false,
991
- rawPassed: false,
992
- reason: "unexpected",
993
- summary: "unexpected",
994
- evidence: [],
995
- evidenceRawCount: 0,
996
- evidenceMatchedCount: 0,
997
- evidenceGateDemoted: false,
1039
+ rawOutputText: '{"passed":false}',
998
1040
  chunkIndex: 1,
999
1041
  chunkTotal: 1,
1000
1042
  };
@@ -1115,6 +1157,70 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
1115
1157
  assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
1116
1158
  }
1117
1159
 
1160
+ async function testBossChatLlmShouldSendAllImageChunksInSingleRequest() {
1161
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-image-chunks-"));
1162
+ const firstImage = path.join(tempDir, "chunk-1.png");
1163
+ const secondImage = path.join(tempDir, "chunk-2.png");
1164
+ fs.writeFileSync(
1165
+ firstImage,
1166
+ Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1167
+ );
1168
+ fs.writeFileSync(
1169
+ secondImage,
1170
+ Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1171
+ );
1172
+
1173
+ let completionPayload = null;
1174
+ const client = new LlmClient({
1175
+ baseUrl: "https://api.openai.com/v1",
1176
+ apiKey: "sk-test",
1177
+ model: "gpt-test",
1178
+ }, {
1179
+ fetchImpl: async (_url, options = {}) => {
1180
+ completionPayload = JSON.parse(String(options.body || "{}"));
1181
+ return {
1182
+ ok: true,
1183
+ status: 200,
1184
+ async json() {
1185
+ return {
1186
+ choices: [
1187
+ {
1188
+ message: {
1189
+ content: "{\"passed\":true}",
1190
+ },
1191
+ },
1192
+ ],
1193
+ };
1194
+ },
1195
+ };
1196
+ },
1197
+ });
1198
+
1199
+ try {
1200
+ const result = await client.evaluateResume({
1201
+ screeningCriteria: "有 AI 项目经验",
1202
+ candidate: {
1203
+ name: "候选人A",
1204
+ sourceJob: "算法工程师",
1205
+ resumeText: "",
1206
+ evidenceCorpus: "",
1207
+ },
1208
+ imagePaths: [firstImage, secondImage],
1209
+ });
1210
+
1211
+ assert.equal(result.passed, true);
1212
+ assert.equal(result.evaluationMode, "image-multi-chunk");
1213
+ assert.equal(result.imageCount, 2);
1214
+ assert.equal(Array.isArray(completionPayload.messages?.[0]?.content), true);
1215
+ assert.equal(
1216
+ completionPayload.messages[0].content.filter((item) => item.type === "image_url").length,
1217
+ 2,
1218
+ );
1219
+ } finally {
1220
+ fs.rmSync(tempDir, { recursive: true, force: true });
1221
+ }
1222
+ }
1223
+
1118
1224
  async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1119
1225
  const calls = [];
1120
1226
  const page = {
@@ -1599,6 +1705,431 @@ async function testBossChatAppShouldWaitForCandidateListBeforePriming() {
1599
1705
  assert.equal(summary.skipped, 1);
1600
1706
  }
1601
1707
 
1708
+ function createProcessCustomerHarness({
1709
+ llmEvaluate,
1710
+ captureResume,
1711
+ tracker,
1712
+ pageOverrides = {},
1713
+ } = {}) {
1714
+ const recorded = [];
1715
+ const page = {
1716
+ async closeResumeModalDomOnce() {
1717
+ recorded.push("closeResumeModalDomOnce");
1718
+ return {
1719
+ closed: true,
1720
+ method: "dom",
1721
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1722
+ };
1723
+ },
1724
+ async waitForConversationReady() {
1725
+ recorded.push("waitForConversationReady");
1726
+ return {
1727
+ hasOnlineResume: true,
1728
+ hasAskResume: true,
1729
+ hasAttachmentResume: false,
1730
+ attachmentResumeEnabled: false,
1731
+ };
1732
+ },
1733
+ async openOnlineResume() {
1734
+ recorded.push("openOnlineResume");
1735
+ return { clicked: true, detectedOpen: true, by: "dom" };
1736
+ },
1737
+ async getResumeRateLimitWarning() {
1738
+ return { hit: false, text: "" };
1739
+ },
1740
+ async getResumeModalState() {
1741
+ return { open: true, iframeCount: 1, scopeCount: 1, closeCount: 1 };
1742
+ },
1743
+ async waitForCandidateActivated() {
1744
+ recorded.push("waitForCandidateActivated");
1745
+ return { matched: true };
1746
+ },
1747
+ async activateCandidate() {
1748
+ recorded.push("activateCandidate");
1749
+ return { ok: true };
1750
+ },
1751
+ ...pageOverrides,
1752
+ };
1753
+ const llmCalls = [];
1754
+ const llmClient = {
1755
+ async evaluateResume(payload) {
1756
+ llmCalls.push(payload);
1757
+ return llmEvaluate(payload);
1758
+ },
1759
+ };
1760
+ const resumeCaptureService = {
1761
+ async captureResume(payload) {
1762
+ recorded.push("captureResume");
1763
+ return captureResume(payload);
1764
+ },
1765
+ };
1766
+ const stateStore = {
1767
+ async record(_key, result) {
1768
+ recorded.push(`record:${result.decision}`);
1769
+ },
1770
+ };
1771
+ const app = new BossChatApp({
1772
+ page,
1773
+ llmClient,
1774
+ interaction: {
1775
+ async sleepRange() {},
1776
+ async clickRect() {},
1777
+ },
1778
+ resumeCaptureService,
1779
+ resumeNetworkTracker: tracker || null,
1780
+ stateStore,
1781
+ reportStore: { async write() { return ""; } },
1782
+ dryRun: true,
1783
+ artifactRootDir: os.tmpdir(),
1784
+ resumeOpenCooldownMs: 0,
1785
+ logger: { log() {} },
1786
+ });
1787
+ app.waitResumeOpenCooldown = async () => {};
1788
+ return { app, llmCalls, recorded };
1789
+ }
1790
+
1791
+ async function testBossChatResumeTrackerShouldRetryInitialNetworkWait() {
1792
+ const tracker = new ResumeNetworkTracker({
1793
+ chromeClient: { Network: null },
1794
+ logger: { log() {} },
1795
+ });
1796
+ const waits = [];
1797
+ let callCount = 0;
1798
+ tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
1799
+ waits.push(timeoutMs);
1800
+ callCount += 1;
1801
+ if (callCount === 2) {
1802
+ return {
1803
+ candidateInfo: { resumeText: "network resume" },
1804
+ source: "geek_id_map",
1805
+ waitedMs: 80,
1806
+ };
1807
+ }
1808
+ return null;
1809
+ };
1810
+ const result = await tracker.waitForResumeNetworkByMode({ customerId: "1001" });
1811
+ assert.deepEqual(waits, [NETWORK_RESUME_WAIT_MS, NETWORK_RESUME_RETRY_WAIT_MS]);
1812
+ assert.equal(result.acquisitionReason, "network_retry_hit");
1813
+ }
1814
+
1815
+ async function testBossChatResumeTrackerShouldUseImageModeGraceWindow() {
1816
+ const tracker = new ResumeNetworkTracker({
1817
+ chromeClient: { Network: null },
1818
+ logger: { log() {} },
1819
+ });
1820
+ tracker.setResumeAcquisitionMode("image", "previous_image_fallback");
1821
+ const waits = [];
1822
+ tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
1823
+ waits.push(timeoutMs);
1824
+ return null;
1825
+ };
1826
+ const result = await tracker.waitForResumeNetworkByMode({ customerId: "1002" });
1827
+ assert.deepEqual(waits, [NETWORK_RESUME_IMAGE_MODE_GRACE_MS]);
1828
+ assert.equal(result.initialWaitMs >= 0, true);
1829
+ assert.equal(result.retryWaitMs, 0);
1830
+ }
1831
+
1832
+ async function testBossChatAppShouldUseNetworkBeforeImageFallback() {
1833
+ const tracker = {
1834
+ resumeNetworkDiagnostics: [],
1835
+ getResumeAcquisitionState() {
1836
+ return { mode: "network", reason: "initial_network_hit" };
1837
+ },
1838
+ async waitForResumeNetworkByMode() {
1839
+ return {
1840
+ candidateInfo: {
1841
+ name: "候选人A",
1842
+ school: "清华大学",
1843
+ major: "计算机",
1844
+ company: "OpenAI",
1845
+ position: "工程师",
1846
+ resumeText: "清华大学 计算机 OpenAI",
1847
+ evidenceCorpus: "清华大学 计算机 OpenAI",
1848
+ },
1849
+ acquisitionReason: "initial_network_hit",
1850
+ initialWaitMs: 12,
1851
+ retryWaitMs: 0,
1852
+ };
1853
+ },
1854
+ async waitForLateNetworkResumeCandidateInfo() {
1855
+ throw new Error("late network retry should not run");
1856
+ },
1857
+ };
1858
+ const { app, llmCalls, recorded } = createProcessCustomerHarness({
1859
+ tracker,
1860
+ llmEvaluate: async () => ({
1861
+ passed: true,
1862
+ rawOutputText: '{"passed":true}',
1863
+ evaluationMode: "text",
1864
+ chunkIndex: 1,
1865
+ chunkTotal: 1,
1866
+ }),
1867
+ captureResume: async () => {
1868
+ throw new Error("image capture should not run");
1869
+ },
1870
+ });
1871
+
1872
+ const result = await app.processCustomer(
1873
+ {
1874
+ customerKey: "candidate-network",
1875
+ name: "候选人A",
1876
+ sourceJob: "算法工程师",
1877
+ domIndex: 0,
1878
+ customerId: "1001",
1879
+ textSnippet: "",
1880
+ },
1881
+ { screeningCriteria: "有 AI 项目经验" },
1882
+ "run-network",
1883
+ { skipCardClick: true },
1884
+ );
1885
+
1886
+ assert.equal(result.artifacts.resumeAcquisitionMode, "network");
1887
+ assert.equal(result.artifacts.resumeAcquisitionReason, "initial_network_hit");
1888
+ assert.equal(llmCalls.length, 1);
1889
+ assert.equal(llmCalls[0].candidate.resumeText.includes("清华大学"), true);
1890
+ assert.equal(Array.isArray(llmCalls[0].imagePaths), false);
1891
+ assert.equal(recorded.includes("captureResume"), false);
1892
+ }
1893
+
1894
+ async function testBossChatAppShouldFallbackToImageAfterNetworkMiss() {
1895
+ const tracker = {
1896
+ resumeNetworkDiagnostics: [],
1897
+ setResumeAcquisitionMode(mode, reason) {
1898
+ this.state = { mode, reason };
1899
+ },
1900
+ getResumeAcquisitionState() {
1901
+ return this.state || { mode: "image", reason: "image_capture_success" };
1902
+ },
1903
+ async waitForResumeNetworkByMode() {
1904
+ return {
1905
+ candidateInfo: null,
1906
+ acquisitionReason: "",
1907
+ initialWaitMs: 10,
1908
+ retryWaitMs: 20,
1909
+ };
1910
+ },
1911
+ async waitForLateNetworkResumeCandidateInfo() {
1912
+ return {
1913
+ candidateInfo: null,
1914
+ acquisitionReason: "",
1915
+ lateRetryMs: 0,
1916
+ };
1917
+ },
1918
+ };
1919
+ const { app, llmCalls } = createProcessCustomerHarness({
1920
+ tracker,
1921
+ llmEvaluate: async () => ({
1922
+ passed: false,
1923
+ rawOutputText: '{"passed":false}',
1924
+ evaluationMode: "image-multi-chunk",
1925
+ imageCount: 2,
1926
+ chunkIndex: 1,
1927
+ chunkTotal: 1,
1928
+ }),
1929
+ captureResume: async ({ artifactDir }) => ({
1930
+ metadataFile: path.join(artifactDir, "chunks.json"),
1931
+ chunkDir: path.join(artifactDir, "chunks"),
1932
+ chunkCount: 2,
1933
+ modelImagePaths: [
1934
+ path.join(artifactDir, "chunks", "chunk_000.png"),
1935
+ path.join(artifactDir, "chunks", "chunk_001.png"),
1936
+ ],
1937
+ stitchedImage: "",
1938
+ quality: { likelyBlank: false },
1939
+ }),
1940
+ });
1941
+
1942
+ const result = await app.processCustomer(
1943
+ {
1944
+ customerKey: "candidate-image",
1945
+ name: "候选人B",
1946
+ sourceJob: "算法工程师",
1947
+ domIndex: 0,
1948
+ customerId: "1002",
1949
+ textSnippet: "",
1950
+ },
1951
+ { screeningCriteria: "有 AI 项目经验" },
1952
+ "run-image",
1953
+ { skipCardClick: true },
1954
+ );
1955
+
1956
+ assert.equal(result.artifacts.resumeAcquisitionMode, "image_fallback");
1957
+ assert.equal(result.artifacts.resumeAcquisitionReason, "image_capture_success");
1958
+ assert.equal(Array.isArray(llmCalls[0].imagePaths), true);
1959
+ assert.equal(llmCalls[0].imagePaths.length, 2);
1960
+ }
1961
+
1962
+ async function testBossChatAppShouldRetryLateNetworkBeforeDomFallback() {
1963
+ const tracker = {
1964
+ resumeNetworkDiagnostics: [],
1965
+ getResumeAcquisitionState() {
1966
+ return { mode: "network", reason: "late_network_hit" };
1967
+ },
1968
+ setResumeAcquisitionMode() {},
1969
+ async waitForResumeNetworkByMode() {
1970
+ return {
1971
+ candidateInfo: null,
1972
+ acquisitionReason: "",
1973
+ initialWaitMs: 10,
1974
+ retryWaitMs: 20,
1975
+ };
1976
+ },
1977
+ async waitForLateNetworkResumeCandidateInfo() {
1978
+ return {
1979
+ candidateInfo: {
1980
+ name: "候选人C",
1981
+ school: "上海交大",
1982
+ major: "软件工程",
1983
+ resumeText: "上海交大 软件工程",
1984
+ evidenceCorpus: "上海交大 软件工程",
1985
+ },
1986
+ acquisitionReason: "late_network_hit",
1987
+ lateRetryMs: 30,
1988
+ };
1989
+ },
1990
+ };
1991
+ let imageAttempt = 0;
1992
+ const { app, llmCalls } = createProcessCustomerHarness({
1993
+ tracker,
1994
+ llmEvaluate: async (payload) => {
1995
+ imageAttempt += 1;
1996
+ if (Array.isArray(payload.imagePaths) && payload.imagePaths.length > 0) {
1997
+ throw new Error("VISION_MODEL_FAILED");
1998
+ }
1999
+ return {
2000
+ passed: true,
2001
+ rawOutputText: '{"passed":true}',
2002
+ evaluationMode: "text",
2003
+ chunkIndex: 1,
2004
+ chunkTotal: 1,
2005
+ };
2006
+ },
2007
+ captureResume: async ({ artifactDir }) => ({
2008
+ metadataFile: path.join(artifactDir, "chunks.json"),
2009
+ chunkDir: path.join(artifactDir, "chunks"),
2010
+ chunkCount: 1,
2011
+ modelImagePaths: [path.join(artifactDir, "chunks", "chunk_000.png")],
2012
+ stitchedImage: "",
2013
+ quality: { likelyBlank: false },
2014
+ }),
2015
+ });
2016
+
2017
+ const result = await app.processCustomer(
2018
+ {
2019
+ customerKey: "candidate-late-network",
2020
+ name: "候选人C",
2021
+ sourceJob: "算法工程师",
2022
+ domIndex: 0,
2023
+ customerId: "1003",
2024
+ textSnippet: "",
2025
+ },
2026
+ { screeningCriteria: "有 AI 项目经验" },
2027
+ "run-late-network",
2028
+ { skipCardClick: true },
2029
+ );
2030
+
2031
+ assert.equal(imageAttempt >= 2, true);
2032
+ assert.equal(result.artifacts.resumeAcquisitionMode, "network");
2033
+ assert.equal(result.artifacts.resumeAcquisitionReason, "late_network_hit");
2034
+ assert.equal(llmCalls[llmCalls.length - 1].candidate.resumeText.includes("上海交大"), true);
2035
+ }
2036
+
2037
+ async function testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail() {
2038
+ let domReadCount = 0;
2039
+ const tracker = {
2040
+ resumeNetworkDiagnostics: [],
2041
+ getResumeAcquisitionState() {
2042
+ return { mode: "image", reason: "image_capture_success" };
2043
+ },
2044
+ setResumeAcquisitionMode() {},
2045
+ async waitForResumeNetworkByMode() {
2046
+ return {
2047
+ candidateInfo: null,
2048
+ acquisitionReason: "",
2049
+ initialWaitMs: 10,
2050
+ retryWaitMs: 20,
2051
+ };
2052
+ },
2053
+ async waitForLateNetworkResumeCandidateInfo() {
2054
+ return {
2055
+ candidateInfo: null,
2056
+ acquisitionReason: "",
2057
+ lateRetryMs: 15,
2058
+ };
2059
+ },
2060
+ async waitForNetworkResumeCandidateInfo() {
2061
+ return null;
2062
+ },
2063
+ };
2064
+ const { app, recorded } = createProcessCustomerHarness({
2065
+ tracker,
2066
+ llmEvaluate: async (payload) => ({
2067
+ passed: false,
2068
+ rawOutputText: '{"passed":false}',
2069
+ evaluationMode: "text",
2070
+ chunkIndex: 1,
2071
+ chunkTotal: 1,
2072
+ imageCount: 0,
2073
+ }),
2074
+ captureResume: async () => {
2075
+ throw new Error("IMAGE_CAPTURE_FAILED");
2076
+ },
2077
+ pageOverrides: {
2078
+ async getResumeProfileFromDom() {
2079
+ domReadCount += 1;
2080
+ if (domReadCount === 1) {
2081
+ return {
2082
+ ok: true,
2083
+ name: "李同学",
2084
+ primarySchool: "北京大学",
2085
+ schools: ["北京大学"],
2086
+ major: "数学",
2087
+ majors: ["数学"],
2088
+ company: "",
2089
+ position: "",
2090
+ resumeText: "北京大学 数学",
2091
+ evidenceCorpus: "北京大学 数学",
2092
+ };
2093
+ }
2094
+ return {
2095
+ ok: true,
2096
+ name: "候选人D",
2097
+ primarySchool: "浙江大学",
2098
+ schools: ["浙江大学"],
2099
+ major: "计算机",
2100
+ majors: ["计算机"],
2101
+ company: "",
2102
+ position: "",
2103
+ resumeText: "浙江大学 计算机",
2104
+ evidenceCorpus: "浙江大学 计算机",
2105
+ };
2106
+ },
2107
+ },
2108
+ });
2109
+
2110
+ const result = await app.processCustomer(
2111
+ {
2112
+ customerKey: "candidate-dom",
2113
+ name: "候选人D",
2114
+ school: "浙江大学",
2115
+ major: "计算机",
2116
+ sourceJob: "算法工程师",
2117
+ domIndex: 0,
2118
+ customerId: "1004",
2119
+ textSnippet: "",
2120
+ },
2121
+ { screeningCriteria: "有 AI 项目经验" },
2122
+ "run-dom",
2123
+ { skipCardClick: true },
2124
+ );
2125
+
2126
+ assert.equal(result.artifacts.resumeAcquisitionMode, "dom_fallback");
2127
+ assert.equal(result.artifacts.resumeAcquisitionReason, "dom_retry_hit");
2128
+ assert.equal(domReadCount, 2);
2129
+ assert.equal(recorded.includes("activateCandidate"), true);
2130
+ assert.equal(recorded.includes("openOnlineResume"), true);
2131
+ }
2132
+
1602
2133
  async function testBossChatAppShouldPersistEvidenceArtifacts() {
1603
2134
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
1604
2135
  await mkdir(tempDir, { recursive: true });
@@ -1646,14 +2177,14 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
1646
2177
  async evaluateResume() {
1647
2178
  return {
1648
2179
  passed: false,
1649
- rawPassed: true,
1650
- reason: "模型未给出可在简历原文中校验的证据,按安全策略判为不通过。",
1651
- summary: "降级",
1652
- evidence: [],
1653
- evidenceRawCount: 1,
1654
- evidenceMatchedCount: 0,
1655
- evidenceGateDemoted: true,
1656
- evaluationMode: "text",
2180
+ rawOutputText: '{"passed":false}',
2181
+ rawReasoningText: "先看项目经验,再看技能,结论不通过。",
2182
+ cot: "先看项目经验,再看技能,结论不通过。",
2183
+ reason: "项目经验与岗位要求不符",
2184
+ summary: "不符合要求",
2185
+ evidence: ["Python"],
2186
+ evaluationMode: "image-multi-chunk",
2187
+ imageCount: 3,
1657
2188
  chunkIndex: 1,
1658
2189
  chunkTotal: 1,
1659
2190
  };
@@ -1666,10 +2197,15 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
1666
2197
  const resumeCaptureService = {
1667
2198
  async captureResume({ artifactDir }) {
1668
2199
  return {
1669
- stitchedImage: path.join(artifactDir, "resume.png"),
1670
2200
  metadataFile: path.join(artifactDir, "chunks.json"),
1671
2201
  chunkDir: path.join(artifactDir, "chunks"),
1672
2202
  chunkCount: 1,
2203
+ modelImagePaths: [
2204
+ path.join(artifactDir, "chunks", "chunk_000.png"),
2205
+ path.join(artifactDir, "chunks", "chunk_001.png"),
2206
+ path.join(artifactDir, "chunks", "chunk_002.png"),
2207
+ ],
2208
+ stitchedImage: "",
1673
2209
  quality: { likelyBlank: false },
1674
2210
  };
1675
2211
  },
@@ -1710,16 +2246,129 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
1710
2246
  );
1711
2247
 
1712
2248
  assert.equal(result.passed, false);
1713
- assert.equal(result.artifacts.rawPassed, true);
1714
2249
  assert.equal(result.artifacts.finalPassed, false);
1715
- assert.equal(result.artifacts.evidenceRawCount, 1);
1716
- assert.equal(result.artifacts.evidenceMatchedCount, 0);
1717
- assert.equal(result.artifacts.evidenceGateDemoted, true);
1718
- assert.equal(result.artifacts.evaluationMode, "text");
2250
+ assert.equal(result.reason, "项目经验与岗位要求不符");
2251
+ assert.equal(result.artifacts.evaluationMode, "image-multi-chunk");
2252
+ assert.equal(result.artifacts.evaluationImageCount, 3);
2253
+ assert.equal(result.artifacts.llmReason, "项目经验与岗位要求不符");
2254
+ assert.equal(result.artifacts.llmSummary, "不符合要求");
2255
+ assert.equal(result.artifacts.llmCot, "先看项目经验,再看技能,结论不通过。");
2256
+ assert.deepEqual(result.artifacts.llmEvidence, ["Python"]);
2257
+ assert.equal(result.artifacts.llmRawReasoning, "先看项目经验,再看技能,结论不通过。");
2258
+ assert.equal(result.artifacts.llmRawOutput, '{"passed":false}');
2259
+ assert.equal(Array.isArray(result.artifacts.modelImagePaths), true);
2260
+ assert.equal(result.artifacts.modelImagePaths.length, 3);
1719
2261
  assert.equal(Array.isArray(records), true);
1720
2262
  assert.equal(records.length, 1);
1721
2263
  }
1722
2264
 
2265
+ async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
2266
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-report-store-"));
2267
+ const reportStore = new ReportStore(tempDir);
2268
+ const summary = {
2269
+ startedAt: "2026-04-17T10:00:00.000Z",
2270
+ finishedAt: "2026-04-17T10:01:05.000Z",
2271
+ dryRun: true,
2272
+ profile: {
2273
+ targetCount: 5,
2274
+ screeningCriteria: "有 AI 项目经验",
2275
+ },
2276
+ inspected: 2,
2277
+ passed: 1,
2278
+ requested: 0,
2279
+ skipped: 1,
2280
+ errors: 0,
2281
+ exhausted: false,
2282
+ stopped: false,
2283
+ stopReason: "",
2284
+ results: [
2285
+ {
2286
+ name: "候选人A",
2287
+ sourceJob: "算法工程师",
2288
+ decision: "passed",
2289
+ passed: true,
2290
+ requested: false,
2291
+ reason: "符合要求",
2292
+ error: "",
2293
+ artifacts: {
2294
+ resumeAcquisitionMode: "network",
2295
+ resumeAcquisitionReason: "initial_hit",
2296
+ textModelMs: 18234,
2297
+ initialNetworkWaitMs: 4200,
2298
+ evaluationMode: "text",
2299
+ llmSummary: "教育与项目经历匹配",
2300
+ llmCot: "先看教育背景,再看项目经历,结论通过。",
2301
+ llmEvidence: ["AI Agent", "MCP"],
2302
+ llmRawReasoning: "先看教育背景,再看项目经历,结论通过。",
2303
+ llmRawOutput: '{"passed":true}',
2304
+ },
2305
+ },
2306
+ {
2307
+ name: "候选人B",
2308
+ sourceJob: "大模型算法",
2309
+ decision: "skipped",
2310
+ passed: false,
2311
+ requested: false,
2312
+ reason: "LLM判定不通过",
2313
+ error: "",
2314
+ artifacts: {
2315
+ resumeAcquisitionMode: "image_fallback",
2316
+ resumeAcquisitionReason: "late_network_miss",
2317
+ imageCaptureMs: 2300,
2318
+ imageModelMs: 19500,
2319
+ lateNetworkRetryMs: 3000,
2320
+ evaluationMode: "image-multi-chunk",
2321
+ evaluationImageCount: 3,
2322
+ llmSummary: "项目经历不足",
2323
+ llmCot: "先看项目经历,再看实习时长,结论不通过。",
2324
+ llmEvidence: ["数据分析"],
2325
+ llmRawReasoning: "先看项目经历,再看实习时长,结论不通过。",
2326
+ llmRawOutput: '{"passed":false}',
2327
+ },
2328
+ },
2329
+ ],
2330
+ reportPath: null,
2331
+ };
2332
+
2333
+ const jsonPath = await reportStore.write(summary);
2334
+ const markdownPath = summary.reportMarkdownPath;
2335
+ const csvPath = summary.reportCsvPath;
2336
+ const jsonContent = fs.readFileSync(jsonPath, "utf8");
2337
+ const markdownContent = fs.readFileSync(markdownPath, "utf8");
2338
+ const csvContent = fs.readFileSync(csvPath, "utf8");
2339
+
2340
+ assert.equal(path.extname(jsonPath), ".json");
2341
+ assert.equal(path.extname(markdownPath), ".md");
2342
+ assert.equal(path.extname(csvPath), ".csv");
2343
+ assert.equal(summary.reportPath, jsonPath);
2344
+ assert.equal(typeof summary.reportArtifacts, "object");
2345
+ assert.equal(summary.reportArtifacts.markdownPath, markdownPath);
2346
+ assert.equal(summary.reportArtifacts.csvPath, csvPath);
2347
+
2348
+ const parsedJson = JSON.parse(jsonContent);
2349
+ assert.equal(parsedJson.reportPath, jsonPath);
2350
+ assert.equal(parsedJson.reportMarkdownPath, markdownPath);
2351
+ assert.equal(parsedJson.reportCsvPath, csvPath);
2352
+
2353
+ assert.match(markdownContent, /# Boss Chat 运行报告/);
2354
+ assert.match(markdownContent, /Resume Acquisition 汇总/);
2355
+ assert.match(markdownContent, /Timing 汇总/);
2356
+ assert.match(markdownContent, /候选人A/);
2357
+ assert.match(markdownContent, /image_fallback/);
2358
+ assert.match(markdownContent, /图片模型 19500ms/);
2359
+
2360
+ assert.match(csvContent, /resume_acquisition_mode/);
2361
+ assert.match(csvContent, /initial_network_wait_ms/);
2362
+ assert.match(csvContent, /late_network_retry_ms/);
2363
+ assert.match(csvContent, /llm_summary/);
2364
+ assert.match(csvContent, /llm_cot/);
2365
+ assert.match(csvContent, /llm_raw_reasoning/);
2366
+ assert.match(csvContent, /llm_raw_output/);
2367
+ assert.match(csvContent, /候选人B/);
2368
+ assert.match(csvContent, /image-multi-chunk/);
2369
+ assert.match(csvContent, /先看项目经历,再看实习时长/);
2370
+ }
2371
+
1723
2372
  async function main() {
1724
2373
  await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
1725
2374
  await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
@@ -1731,15 +2380,27 @@ async function main() {
1731
2380
  await testVendorBossChatCliShouldWaitForHydratedChatShell();
1732
2381
  await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
1733
2382
  testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
1734
- testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
1735
- testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
2383
+ testVendorBossChatCliShouldParseSharedLlmTransportArgs();
2384
+ testBossChatLlmParserShouldAcceptMinimalDecisionJson();
2385
+ testBossChatLlmParserShouldAcceptPlainPassFailText();
2386
+ testBossChatLlmParserShouldAcceptDecisionField();
2387
+ testBossChatLlmParserShouldPreserveReasoningFields();
2388
+ testBossChatLlmExtractorsShouldReadProviderReasoningFields();
1736
2389
  await testBossChatLlmTextChunkFallbackShouldWork();
1737
2390
  await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
2391
+ await testBossChatLlmShouldSendAllImageChunksInSingleRequest();
1738
2392
  await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
1739
2393
  await testBossChatAppShouldCloseCandidateDetailDuringRunCleanup();
1740
2394
  await testBossChatAppShouldRestoreListContextAfterRecovery();
1741
2395
  await testBossChatAppShouldWaitForCandidateListBeforePriming();
2396
+ await testBossChatResumeTrackerShouldRetryInitialNetworkWait();
2397
+ await testBossChatResumeTrackerShouldUseImageModeGraceWindow();
2398
+ await testBossChatAppShouldUseNetworkBeforeImageFallback();
2399
+ await testBossChatAppShouldFallbackToImageAfterNetworkMiss();
2400
+ await testBossChatAppShouldRetryLateNetworkBeforeDomFallback();
2401
+ await testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail();
1742
2402
  await testBossChatAppShouldPersistEvidenceArtifacts();
2403
+ await testBossChatReportStoreShouldWriteReadableMarkdownAndCsv();
1743
2404
  console.log("boss-chat tests passed");
1744
2405
  }
1745
2406