@reconcrap/boss-recommend-mcp 1.3.30 → 1.3.31

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.
@@ -19,6 +19,13 @@ 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
21
  import { LlmClient, parseLlmJson } 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
 
@@ -58,6 +65,8 @@ function createBossChatTestWorkspace() {
58
65
  baseUrl: "https://api.example.com/v1",
59
66
  apiKey: "sk-test-key",
60
67
  model: "gpt-4.1-mini",
68
+ llmTimeoutMs: 65000,
69
+ llmMaxRetries: 4,
61
70
  debugPort: 9666
62
71
  }, null, 2));
63
72
 
@@ -254,6 +263,8 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
254
263
  assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
255
264
  assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
256
265
  assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
266
+ assert.equal(stateAfterPrepare.last_prepare_args["llm-timeout-ms"], "65000");
267
+ assert.equal(stateAfterPrepare.last_prepare_args["llm-max-retries"], "4");
257
268
 
258
269
  const started = await startBossChatRun({
259
270
  workspaceRoot,
@@ -278,6 +289,8 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
278
289
  assert.equal(stateAfterStart.last_start_args.apikey, "sk-test-key");
279
290
  assert.equal(stateAfterStart.last_start_args.model, "gpt-4.1-mini");
280
291
  assert.equal(stateAfterStart.last_start_args.port, "9666");
292
+ assert.equal(stateAfterStart.last_start_args["llm-timeout-ms"], "65000");
293
+ assert.equal(stateAfterStart.last_start_args["llm-max-retries"], "4");
281
294
 
282
295
  const startedAll = await startBossChatRun({
283
296
  workspaceRoot,
@@ -897,41 +910,43 @@ function testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig() {
897
910
  assert.equal(launchConfig.args[0], "-y");
898
911
  }
899
912
 
900
- function testBossChatLlmEvidenceGateShouldDemoteMissingEvidence() {
913
+ function testVendorBossChatCliShouldParseSharedLlmTransportArgs() {
914
+ const parsed = vendorCliTestables.parseArgs([
915
+ "start-run",
916
+ "--llm-timeout-ms",
917
+ "70000",
918
+ "--llm-max-retries",
919
+ "5",
920
+ ]);
921
+ assert.equal(parsed.command, "start-run");
922
+ assert.equal(parsed.overrides.llm.timeoutMs, 70000);
923
+ assert.equal(parsed.overrides.llm.maxRetries, 5);
924
+ }
925
+
926
+ function testBossChatLlmParserShouldAcceptMinimalDecisionJson() {
901
927
  const parsed = parseLlmJson(
902
928
  JSON.stringify({
903
929
  passed: true,
904
- reason: "命中标准",
905
- summary: "命中",
906
- evidence: [],
907
930
  }),
908
- {
909
- evidenceCorpus: "南京大学 机器学习 项目经历",
910
- },
911
931
  );
912
- assert.equal(parsed.rawPassed, true);
913
- assert.equal(parsed.passed, false);
914
- assert.equal(parsed.evidenceGateDemoted, true);
915
- assert.equal(parsed.evidenceMatchedCount, 0);
932
+ assert.equal(parsed.passed, true);
933
+ assert.equal(parsed.rawOutputText.includes('"passed":true'), true);
934
+ }
935
+
936
+ function testBossChatLlmParserShouldAcceptPlainPassFailText() {
937
+ const passed = parseLlmJson("PASS");
938
+ assert.equal(passed.passed, true);
939
+ const failed = parseLlmJson("false");
940
+ assert.equal(failed.passed, false);
916
941
  }
917
942
 
918
- function testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence() {
943
+ function testBossChatLlmParserShouldAcceptDecisionField() {
919
944
  const parsed = parseLlmJson(
920
945
  JSON.stringify({
921
- passed: true,
922
- reason: "命中标准",
923
- summary: "命中",
924
- evidence: ["十年金融风控投研经验"],
946
+ decision: "fail",
925
947
  }),
926
- {
927
- evidenceCorpus: "南京大学 机器学习 项目经历",
928
- },
929
948
  );
930
- assert.equal(parsed.rawPassed, true);
931
949
  assert.equal(parsed.passed, false);
932
- assert.equal(parsed.evidenceGateDemoted, true);
933
- assert.equal(parsed.evidenceRawCount, 1);
934
- assert.equal(parsed.evidenceMatchedCount, 0);
935
950
  }
936
951
 
937
952
  async function testBossChatLlmTextChunkFallbackShouldWork() {
@@ -962,39 +977,21 @@ async function testBossChatLlmTextChunkFallbackShouldWork() {
962
977
  if (payload.chunkIndex === 2) {
963
978
  return {
964
979
  passed: true,
965
- rawPassed: true,
966
- reason: "命中分段证据",
967
- summary: "命中",
968
- evidence: ["PASS_MARKER_ABC"],
969
- evidenceRawCount: 1,
970
- evidenceMatchedCount: 1,
971
- evidenceGateDemoted: false,
980
+ rawOutputText: '{"passed":true}',
972
981
  chunkIndex: payload.chunkIndex,
973
982
  chunkTotal: payload.chunkTotal,
974
983
  };
975
984
  }
976
985
  return {
977
986
  passed: false,
978
- rawPassed: false,
979
- reason: "本段证据不足",
980
- summary: "不足",
981
- evidence: [],
982
- evidenceRawCount: 0,
983
- evidenceMatchedCount: 0,
984
- evidenceGateDemoted: false,
987
+ rawOutputText: '{"passed":false}',
985
988
  chunkIndex: payload.chunkIndex,
986
989
  chunkTotal: payload.chunkTotal,
987
990
  };
988
991
  }
989
992
  return {
990
993
  passed: false,
991
- rawPassed: false,
992
- reason: "unexpected",
993
- summary: "unexpected",
994
- evidence: [],
995
- evidenceRawCount: 0,
996
- evidenceMatchedCount: 0,
997
- evidenceGateDemoted: false,
994
+ rawOutputText: '{"passed":false}',
998
995
  chunkIndex: 1,
999
996
  chunkTotal: 1,
1000
997
  };
@@ -1115,6 +1112,70 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
1115
1112
  assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
1116
1113
  }
1117
1114
 
1115
+ async function testBossChatLlmShouldSendAllImageChunksInSingleRequest() {
1116
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-image-chunks-"));
1117
+ const firstImage = path.join(tempDir, "chunk-1.png");
1118
+ const secondImage = path.join(tempDir, "chunk-2.png");
1119
+ fs.writeFileSync(
1120
+ firstImage,
1121
+ Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1122
+ );
1123
+ fs.writeFileSync(
1124
+ secondImage,
1125
+ Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN4QAAAAASUVORK5CYII=", "base64"),
1126
+ );
1127
+
1128
+ let completionPayload = null;
1129
+ const client = new LlmClient({
1130
+ baseUrl: "https://api.openai.com/v1",
1131
+ apiKey: "sk-test",
1132
+ model: "gpt-test",
1133
+ }, {
1134
+ fetchImpl: async (_url, options = {}) => {
1135
+ completionPayload = JSON.parse(String(options.body || "{}"));
1136
+ return {
1137
+ ok: true,
1138
+ status: 200,
1139
+ async json() {
1140
+ return {
1141
+ choices: [
1142
+ {
1143
+ message: {
1144
+ content: "{\"passed\":true}",
1145
+ },
1146
+ },
1147
+ ],
1148
+ };
1149
+ },
1150
+ };
1151
+ },
1152
+ });
1153
+
1154
+ try {
1155
+ const result = await client.evaluateResume({
1156
+ screeningCriteria: "有 AI 项目经验",
1157
+ candidate: {
1158
+ name: "候选人A",
1159
+ sourceJob: "算法工程师",
1160
+ resumeText: "",
1161
+ evidenceCorpus: "",
1162
+ },
1163
+ imagePaths: [firstImage, secondImage],
1164
+ });
1165
+
1166
+ assert.equal(result.passed, true);
1167
+ assert.equal(result.evaluationMode, "image-multi-chunk");
1168
+ assert.equal(result.imageCount, 2);
1169
+ assert.equal(Array.isArray(completionPayload.messages?.[0]?.content), true);
1170
+ assert.equal(
1171
+ completionPayload.messages[0].content.filter((item) => item.type === "image_url").length,
1172
+ 2,
1173
+ );
1174
+ } finally {
1175
+ fs.rmSync(tempDir, { recursive: true, force: true });
1176
+ }
1177
+ }
1178
+
1118
1179
  async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
1119
1180
  const calls = [];
1120
1181
  const page = {
@@ -1599,6 +1660,431 @@ async function testBossChatAppShouldWaitForCandidateListBeforePriming() {
1599
1660
  assert.equal(summary.skipped, 1);
1600
1661
  }
1601
1662
 
1663
+ function createProcessCustomerHarness({
1664
+ llmEvaluate,
1665
+ captureResume,
1666
+ tracker,
1667
+ pageOverrides = {},
1668
+ } = {}) {
1669
+ const recorded = [];
1670
+ const page = {
1671
+ async closeResumeModalDomOnce() {
1672
+ recorded.push("closeResumeModalDomOnce");
1673
+ return {
1674
+ closed: true,
1675
+ method: "dom",
1676
+ finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
1677
+ };
1678
+ },
1679
+ async waitForConversationReady() {
1680
+ recorded.push("waitForConversationReady");
1681
+ return {
1682
+ hasOnlineResume: true,
1683
+ hasAskResume: true,
1684
+ hasAttachmentResume: false,
1685
+ attachmentResumeEnabled: false,
1686
+ };
1687
+ },
1688
+ async openOnlineResume() {
1689
+ recorded.push("openOnlineResume");
1690
+ return { clicked: true, detectedOpen: true, by: "dom" };
1691
+ },
1692
+ async getResumeRateLimitWarning() {
1693
+ return { hit: false, text: "" };
1694
+ },
1695
+ async getResumeModalState() {
1696
+ return { open: true, iframeCount: 1, scopeCount: 1, closeCount: 1 };
1697
+ },
1698
+ async waitForCandidateActivated() {
1699
+ recorded.push("waitForCandidateActivated");
1700
+ return { matched: true };
1701
+ },
1702
+ async activateCandidate() {
1703
+ recorded.push("activateCandidate");
1704
+ return { ok: true };
1705
+ },
1706
+ ...pageOverrides,
1707
+ };
1708
+ const llmCalls = [];
1709
+ const llmClient = {
1710
+ async evaluateResume(payload) {
1711
+ llmCalls.push(payload);
1712
+ return llmEvaluate(payload);
1713
+ },
1714
+ };
1715
+ const resumeCaptureService = {
1716
+ async captureResume(payload) {
1717
+ recorded.push("captureResume");
1718
+ return captureResume(payload);
1719
+ },
1720
+ };
1721
+ const stateStore = {
1722
+ async record(_key, result) {
1723
+ recorded.push(`record:${result.decision}`);
1724
+ },
1725
+ };
1726
+ const app = new BossChatApp({
1727
+ page,
1728
+ llmClient,
1729
+ interaction: {
1730
+ async sleepRange() {},
1731
+ async clickRect() {},
1732
+ },
1733
+ resumeCaptureService,
1734
+ resumeNetworkTracker: tracker || null,
1735
+ stateStore,
1736
+ reportStore: { async write() { return ""; } },
1737
+ dryRun: true,
1738
+ artifactRootDir: os.tmpdir(),
1739
+ resumeOpenCooldownMs: 0,
1740
+ logger: { log() {} },
1741
+ });
1742
+ app.waitResumeOpenCooldown = async () => {};
1743
+ return { app, llmCalls, recorded };
1744
+ }
1745
+
1746
+ async function testBossChatResumeTrackerShouldRetryInitialNetworkWait() {
1747
+ const tracker = new ResumeNetworkTracker({
1748
+ chromeClient: { Network: null },
1749
+ logger: { log() {} },
1750
+ });
1751
+ const waits = [];
1752
+ let callCount = 0;
1753
+ tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
1754
+ waits.push(timeoutMs);
1755
+ callCount += 1;
1756
+ if (callCount === 2) {
1757
+ return {
1758
+ candidateInfo: { resumeText: "network resume" },
1759
+ source: "geek_id_map",
1760
+ waitedMs: 80,
1761
+ };
1762
+ }
1763
+ return null;
1764
+ };
1765
+ const result = await tracker.waitForResumeNetworkByMode({ customerId: "1001" });
1766
+ assert.deepEqual(waits, [NETWORK_RESUME_WAIT_MS, NETWORK_RESUME_RETRY_WAIT_MS]);
1767
+ assert.equal(result.acquisitionReason, "network_retry_hit");
1768
+ }
1769
+
1770
+ async function testBossChatResumeTrackerShouldUseImageModeGraceWindow() {
1771
+ const tracker = new ResumeNetworkTracker({
1772
+ chromeClient: { Network: null },
1773
+ logger: { log() {} },
1774
+ });
1775
+ tracker.setResumeAcquisitionMode("image", "previous_image_fallback");
1776
+ const waits = [];
1777
+ tracker.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
1778
+ waits.push(timeoutMs);
1779
+ return null;
1780
+ };
1781
+ const result = await tracker.waitForResumeNetworkByMode({ customerId: "1002" });
1782
+ assert.deepEqual(waits, [NETWORK_RESUME_IMAGE_MODE_GRACE_MS]);
1783
+ assert.equal(result.initialWaitMs >= 0, true);
1784
+ assert.equal(result.retryWaitMs, 0);
1785
+ }
1786
+
1787
+ async function testBossChatAppShouldUseNetworkBeforeImageFallback() {
1788
+ const tracker = {
1789
+ resumeNetworkDiagnostics: [],
1790
+ getResumeAcquisitionState() {
1791
+ return { mode: "network", reason: "initial_network_hit" };
1792
+ },
1793
+ async waitForResumeNetworkByMode() {
1794
+ return {
1795
+ candidateInfo: {
1796
+ name: "候选人A",
1797
+ school: "清华大学",
1798
+ major: "计算机",
1799
+ company: "OpenAI",
1800
+ position: "工程师",
1801
+ resumeText: "清华大学 计算机 OpenAI",
1802
+ evidenceCorpus: "清华大学 计算机 OpenAI",
1803
+ },
1804
+ acquisitionReason: "initial_network_hit",
1805
+ initialWaitMs: 12,
1806
+ retryWaitMs: 0,
1807
+ };
1808
+ },
1809
+ async waitForLateNetworkResumeCandidateInfo() {
1810
+ throw new Error("late network retry should not run");
1811
+ },
1812
+ };
1813
+ const { app, llmCalls, recorded } = createProcessCustomerHarness({
1814
+ tracker,
1815
+ llmEvaluate: async () => ({
1816
+ passed: true,
1817
+ rawOutputText: '{"passed":true}',
1818
+ evaluationMode: "text",
1819
+ chunkIndex: 1,
1820
+ chunkTotal: 1,
1821
+ }),
1822
+ captureResume: async () => {
1823
+ throw new Error("image capture should not run");
1824
+ },
1825
+ });
1826
+
1827
+ const result = await app.processCustomer(
1828
+ {
1829
+ customerKey: "candidate-network",
1830
+ name: "候选人A",
1831
+ sourceJob: "算法工程师",
1832
+ domIndex: 0,
1833
+ customerId: "1001",
1834
+ textSnippet: "",
1835
+ },
1836
+ { screeningCriteria: "有 AI 项目经验" },
1837
+ "run-network",
1838
+ { skipCardClick: true },
1839
+ );
1840
+
1841
+ assert.equal(result.artifacts.resumeAcquisitionMode, "network");
1842
+ assert.equal(result.artifacts.resumeAcquisitionReason, "initial_network_hit");
1843
+ assert.equal(llmCalls.length, 1);
1844
+ assert.equal(llmCalls[0].candidate.resumeText.includes("清华大学"), true);
1845
+ assert.equal(Array.isArray(llmCalls[0].imagePaths), false);
1846
+ assert.equal(recorded.includes("captureResume"), false);
1847
+ }
1848
+
1849
+ async function testBossChatAppShouldFallbackToImageAfterNetworkMiss() {
1850
+ const tracker = {
1851
+ resumeNetworkDiagnostics: [],
1852
+ setResumeAcquisitionMode(mode, reason) {
1853
+ this.state = { mode, reason };
1854
+ },
1855
+ getResumeAcquisitionState() {
1856
+ return this.state || { mode: "image", reason: "image_capture_success" };
1857
+ },
1858
+ async waitForResumeNetworkByMode() {
1859
+ return {
1860
+ candidateInfo: null,
1861
+ acquisitionReason: "",
1862
+ initialWaitMs: 10,
1863
+ retryWaitMs: 20,
1864
+ };
1865
+ },
1866
+ async waitForLateNetworkResumeCandidateInfo() {
1867
+ return {
1868
+ candidateInfo: null,
1869
+ acquisitionReason: "",
1870
+ lateRetryMs: 0,
1871
+ };
1872
+ },
1873
+ };
1874
+ const { app, llmCalls } = createProcessCustomerHarness({
1875
+ tracker,
1876
+ llmEvaluate: async () => ({
1877
+ passed: false,
1878
+ rawOutputText: '{"passed":false}',
1879
+ evaluationMode: "image-multi-chunk",
1880
+ imageCount: 2,
1881
+ chunkIndex: 1,
1882
+ chunkTotal: 1,
1883
+ }),
1884
+ captureResume: async ({ artifactDir }) => ({
1885
+ metadataFile: path.join(artifactDir, "chunks.json"),
1886
+ chunkDir: path.join(artifactDir, "chunks"),
1887
+ chunkCount: 2,
1888
+ modelImagePaths: [
1889
+ path.join(artifactDir, "chunks", "chunk_000.png"),
1890
+ path.join(artifactDir, "chunks", "chunk_001.png"),
1891
+ ],
1892
+ stitchedImage: "",
1893
+ quality: { likelyBlank: false },
1894
+ }),
1895
+ });
1896
+
1897
+ const result = await app.processCustomer(
1898
+ {
1899
+ customerKey: "candidate-image",
1900
+ name: "候选人B",
1901
+ sourceJob: "算法工程师",
1902
+ domIndex: 0,
1903
+ customerId: "1002",
1904
+ textSnippet: "",
1905
+ },
1906
+ { screeningCriteria: "有 AI 项目经验" },
1907
+ "run-image",
1908
+ { skipCardClick: true },
1909
+ );
1910
+
1911
+ assert.equal(result.artifacts.resumeAcquisitionMode, "image_fallback");
1912
+ assert.equal(result.artifacts.resumeAcquisitionReason, "image_capture_success");
1913
+ assert.equal(Array.isArray(llmCalls[0].imagePaths), true);
1914
+ assert.equal(llmCalls[0].imagePaths.length, 2);
1915
+ }
1916
+
1917
+ async function testBossChatAppShouldRetryLateNetworkBeforeDomFallback() {
1918
+ const tracker = {
1919
+ resumeNetworkDiagnostics: [],
1920
+ getResumeAcquisitionState() {
1921
+ return { mode: "network", reason: "late_network_hit" };
1922
+ },
1923
+ setResumeAcquisitionMode() {},
1924
+ async waitForResumeNetworkByMode() {
1925
+ return {
1926
+ candidateInfo: null,
1927
+ acquisitionReason: "",
1928
+ initialWaitMs: 10,
1929
+ retryWaitMs: 20,
1930
+ };
1931
+ },
1932
+ async waitForLateNetworkResumeCandidateInfo() {
1933
+ return {
1934
+ candidateInfo: {
1935
+ name: "候选人C",
1936
+ school: "上海交大",
1937
+ major: "软件工程",
1938
+ resumeText: "上海交大 软件工程",
1939
+ evidenceCorpus: "上海交大 软件工程",
1940
+ },
1941
+ acquisitionReason: "late_network_hit",
1942
+ lateRetryMs: 30,
1943
+ };
1944
+ },
1945
+ };
1946
+ let imageAttempt = 0;
1947
+ const { app, llmCalls } = createProcessCustomerHarness({
1948
+ tracker,
1949
+ llmEvaluate: async (payload) => {
1950
+ imageAttempt += 1;
1951
+ if (Array.isArray(payload.imagePaths) && payload.imagePaths.length > 0) {
1952
+ throw new Error("VISION_MODEL_FAILED");
1953
+ }
1954
+ return {
1955
+ passed: true,
1956
+ rawOutputText: '{"passed":true}',
1957
+ evaluationMode: "text",
1958
+ chunkIndex: 1,
1959
+ chunkTotal: 1,
1960
+ };
1961
+ },
1962
+ captureResume: async ({ artifactDir }) => ({
1963
+ metadataFile: path.join(artifactDir, "chunks.json"),
1964
+ chunkDir: path.join(artifactDir, "chunks"),
1965
+ chunkCount: 1,
1966
+ modelImagePaths: [path.join(artifactDir, "chunks", "chunk_000.png")],
1967
+ stitchedImage: "",
1968
+ quality: { likelyBlank: false },
1969
+ }),
1970
+ });
1971
+
1972
+ const result = await app.processCustomer(
1973
+ {
1974
+ customerKey: "candidate-late-network",
1975
+ name: "候选人C",
1976
+ sourceJob: "算法工程师",
1977
+ domIndex: 0,
1978
+ customerId: "1003",
1979
+ textSnippet: "",
1980
+ },
1981
+ { screeningCriteria: "有 AI 项目经验" },
1982
+ "run-late-network",
1983
+ { skipCardClick: true },
1984
+ );
1985
+
1986
+ assert.equal(imageAttempt >= 2, true);
1987
+ assert.equal(result.artifacts.resumeAcquisitionMode, "network");
1988
+ assert.equal(result.artifacts.resumeAcquisitionReason, "late_network_hit");
1989
+ assert.equal(llmCalls[llmCalls.length - 1].candidate.resumeText.includes("上海交大"), true);
1990
+ }
1991
+
1992
+ async function testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail() {
1993
+ let domReadCount = 0;
1994
+ const tracker = {
1995
+ resumeNetworkDiagnostics: [],
1996
+ getResumeAcquisitionState() {
1997
+ return { mode: "image", reason: "image_capture_success" };
1998
+ },
1999
+ setResumeAcquisitionMode() {},
2000
+ async waitForResumeNetworkByMode() {
2001
+ return {
2002
+ candidateInfo: null,
2003
+ acquisitionReason: "",
2004
+ initialWaitMs: 10,
2005
+ retryWaitMs: 20,
2006
+ };
2007
+ },
2008
+ async waitForLateNetworkResumeCandidateInfo() {
2009
+ return {
2010
+ candidateInfo: null,
2011
+ acquisitionReason: "",
2012
+ lateRetryMs: 15,
2013
+ };
2014
+ },
2015
+ async waitForNetworkResumeCandidateInfo() {
2016
+ return null;
2017
+ },
2018
+ };
2019
+ const { app, recorded } = createProcessCustomerHarness({
2020
+ tracker,
2021
+ llmEvaluate: async (payload) => ({
2022
+ passed: false,
2023
+ rawOutputText: '{"passed":false}',
2024
+ evaluationMode: "text",
2025
+ chunkIndex: 1,
2026
+ chunkTotal: 1,
2027
+ imageCount: 0,
2028
+ }),
2029
+ captureResume: async () => {
2030
+ throw new Error("IMAGE_CAPTURE_FAILED");
2031
+ },
2032
+ pageOverrides: {
2033
+ async getResumeProfileFromDom() {
2034
+ domReadCount += 1;
2035
+ if (domReadCount === 1) {
2036
+ return {
2037
+ ok: true,
2038
+ name: "李同学",
2039
+ primarySchool: "北京大学",
2040
+ schools: ["北京大学"],
2041
+ major: "数学",
2042
+ majors: ["数学"],
2043
+ company: "",
2044
+ position: "",
2045
+ resumeText: "北京大学 数学",
2046
+ evidenceCorpus: "北京大学 数学",
2047
+ };
2048
+ }
2049
+ return {
2050
+ ok: true,
2051
+ name: "候选人D",
2052
+ primarySchool: "浙江大学",
2053
+ schools: ["浙江大学"],
2054
+ major: "计算机",
2055
+ majors: ["计算机"],
2056
+ company: "",
2057
+ position: "",
2058
+ resumeText: "浙江大学 计算机",
2059
+ evidenceCorpus: "浙江大学 计算机",
2060
+ };
2061
+ },
2062
+ },
2063
+ });
2064
+
2065
+ const result = await app.processCustomer(
2066
+ {
2067
+ customerKey: "candidate-dom",
2068
+ name: "候选人D",
2069
+ school: "浙江大学",
2070
+ major: "计算机",
2071
+ sourceJob: "算法工程师",
2072
+ domIndex: 0,
2073
+ customerId: "1004",
2074
+ textSnippet: "",
2075
+ },
2076
+ { screeningCriteria: "有 AI 项目经验" },
2077
+ "run-dom",
2078
+ { skipCardClick: true },
2079
+ );
2080
+
2081
+ assert.equal(result.artifacts.resumeAcquisitionMode, "dom_fallback");
2082
+ assert.equal(result.artifacts.resumeAcquisitionReason, "dom_retry_hit");
2083
+ assert.equal(domReadCount, 2);
2084
+ assert.equal(recorded.includes("activateCandidate"), true);
2085
+ assert.equal(recorded.includes("openOnlineResume"), true);
2086
+ }
2087
+
1602
2088
  async function testBossChatAppShouldPersistEvidenceArtifacts() {
1603
2089
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
1604
2090
  await mkdir(tempDir, { recursive: true });
@@ -1646,14 +2132,9 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
1646
2132
  async evaluateResume() {
1647
2133
  return {
1648
2134
  passed: false,
1649
- rawPassed: true,
1650
- reason: "模型未给出可在简历原文中校验的证据,按安全策略判为不通过。",
1651
- summary: "降级",
1652
- evidence: [],
1653
- evidenceRawCount: 1,
1654
- evidenceMatchedCount: 0,
1655
- evidenceGateDemoted: true,
1656
- evaluationMode: "text",
2135
+ rawOutputText: '{"passed":false}',
2136
+ evaluationMode: "image-multi-chunk",
2137
+ imageCount: 3,
1657
2138
  chunkIndex: 1,
1658
2139
  chunkTotal: 1,
1659
2140
  };
@@ -1666,10 +2147,15 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
1666
2147
  const resumeCaptureService = {
1667
2148
  async captureResume({ artifactDir }) {
1668
2149
  return {
1669
- stitchedImage: path.join(artifactDir, "resume.png"),
1670
2150
  metadataFile: path.join(artifactDir, "chunks.json"),
1671
2151
  chunkDir: path.join(artifactDir, "chunks"),
1672
2152
  chunkCount: 1,
2153
+ modelImagePaths: [
2154
+ path.join(artifactDir, "chunks", "chunk_000.png"),
2155
+ path.join(artifactDir, "chunks", "chunk_001.png"),
2156
+ path.join(artifactDir, "chunks", "chunk_002.png"),
2157
+ ],
2158
+ stitchedImage: "",
1673
2159
  quality: { likelyBlank: false },
1674
2160
  };
1675
2161
  },
@@ -1710,16 +2196,111 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
1710
2196
  );
1711
2197
 
1712
2198
  assert.equal(result.passed, false);
1713
- assert.equal(result.artifacts.rawPassed, true);
1714
2199
  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");
2200
+ assert.equal(result.reason, "LLM判定不通过");
2201
+ assert.equal(result.artifacts.evaluationMode, "image-multi-chunk");
2202
+ assert.equal(result.artifacts.evaluationImageCount, 3);
2203
+ assert.equal(result.artifacts.llmRawOutput, '{"passed":false}');
2204
+ assert.equal(Array.isArray(result.artifacts.modelImagePaths), true);
2205
+ assert.equal(result.artifacts.modelImagePaths.length, 3);
1719
2206
  assert.equal(Array.isArray(records), true);
1720
2207
  assert.equal(records.length, 1);
1721
2208
  }
1722
2209
 
2210
+ async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
2211
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-report-store-"));
2212
+ const reportStore = new ReportStore(tempDir);
2213
+ const summary = {
2214
+ startedAt: "2026-04-17T10:00:00.000Z",
2215
+ finishedAt: "2026-04-17T10:01:05.000Z",
2216
+ dryRun: true,
2217
+ profile: {
2218
+ targetCount: 5,
2219
+ screeningCriteria: "有 AI 项目经验",
2220
+ },
2221
+ inspected: 2,
2222
+ passed: 1,
2223
+ requested: 0,
2224
+ skipped: 1,
2225
+ errors: 0,
2226
+ exhausted: false,
2227
+ stopped: false,
2228
+ stopReason: "",
2229
+ results: [
2230
+ {
2231
+ name: "候选人A",
2232
+ sourceJob: "算法工程师",
2233
+ decision: "passed",
2234
+ passed: true,
2235
+ requested: false,
2236
+ reason: "符合要求",
2237
+ error: "",
2238
+ artifacts: {
2239
+ resumeAcquisitionMode: "network",
2240
+ resumeAcquisitionReason: "initial_hit",
2241
+ textModelMs: 18234,
2242
+ initialNetworkWaitMs: 4200,
2243
+ evaluationMode: "text",
2244
+ llmRawOutput: '{"passed":true}',
2245
+ },
2246
+ },
2247
+ {
2248
+ name: "候选人B",
2249
+ sourceJob: "大模型算法",
2250
+ decision: "skipped",
2251
+ passed: false,
2252
+ requested: false,
2253
+ reason: "LLM判定不通过",
2254
+ error: "",
2255
+ artifacts: {
2256
+ resumeAcquisitionMode: "image_fallback",
2257
+ resumeAcquisitionReason: "late_network_miss",
2258
+ imageCaptureMs: 2300,
2259
+ imageModelMs: 19500,
2260
+ lateNetworkRetryMs: 3000,
2261
+ evaluationMode: "image-multi-chunk",
2262
+ evaluationImageCount: 3,
2263
+ llmRawOutput: '{"passed":false}',
2264
+ },
2265
+ },
2266
+ ],
2267
+ reportPath: null,
2268
+ };
2269
+
2270
+ const jsonPath = await reportStore.write(summary);
2271
+ const markdownPath = summary.reportMarkdownPath;
2272
+ const csvPath = summary.reportCsvPath;
2273
+ const jsonContent = fs.readFileSync(jsonPath, "utf8");
2274
+ const markdownContent = fs.readFileSync(markdownPath, "utf8");
2275
+ const csvContent = fs.readFileSync(csvPath, "utf8");
2276
+
2277
+ assert.equal(path.extname(jsonPath), ".json");
2278
+ assert.equal(path.extname(markdownPath), ".md");
2279
+ assert.equal(path.extname(csvPath), ".csv");
2280
+ assert.equal(summary.reportPath, jsonPath);
2281
+ assert.equal(typeof summary.reportArtifacts, "object");
2282
+ assert.equal(summary.reportArtifacts.markdownPath, markdownPath);
2283
+ assert.equal(summary.reportArtifacts.csvPath, csvPath);
2284
+
2285
+ const parsedJson = JSON.parse(jsonContent);
2286
+ assert.equal(parsedJson.reportPath, jsonPath);
2287
+ assert.equal(parsedJson.reportMarkdownPath, markdownPath);
2288
+ assert.equal(parsedJson.reportCsvPath, csvPath);
2289
+
2290
+ assert.match(markdownContent, /# Boss Chat 运行报告/);
2291
+ assert.match(markdownContent, /Resume Acquisition 汇总/);
2292
+ assert.match(markdownContent, /Timing 汇总/);
2293
+ assert.match(markdownContent, /候选人A/);
2294
+ assert.match(markdownContent, /image_fallback/);
2295
+ assert.match(markdownContent, /图片模型 19500ms/);
2296
+
2297
+ assert.match(csvContent, /resume_acquisition_mode/);
2298
+ assert.match(csvContent, /initial_network_wait_ms/);
2299
+ assert.match(csvContent, /late_network_retry_ms/);
2300
+ assert.match(csvContent, /候选人B/);
2301
+ assert.match(csvContent, /image-multi-chunk/);
2302
+ }
2303
+
1723
2304
  async function main() {
1724
2305
  await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
1725
2306
  await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
@@ -1731,15 +2312,25 @@ async function main() {
1731
2312
  await testVendorBossChatCliShouldWaitForHydratedChatShell();
1732
2313
  await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
1733
2314
  testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
1734
- testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
1735
- testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
2315
+ testVendorBossChatCliShouldParseSharedLlmTransportArgs();
2316
+ testBossChatLlmParserShouldAcceptMinimalDecisionJson();
2317
+ testBossChatLlmParserShouldAcceptPlainPassFailText();
2318
+ testBossChatLlmParserShouldAcceptDecisionField();
1736
2319
  await testBossChatLlmTextChunkFallbackShouldWork();
1737
2320
  await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
2321
+ await testBossChatLlmShouldSendAllImageChunksInSingleRequest();
1738
2322
  await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
1739
2323
  await testBossChatAppShouldCloseCandidateDetailDuringRunCleanup();
1740
2324
  await testBossChatAppShouldRestoreListContextAfterRecovery();
1741
2325
  await testBossChatAppShouldWaitForCandidateListBeforePriming();
2326
+ await testBossChatResumeTrackerShouldRetryInitialNetworkWait();
2327
+ await testBossChatResumeTrackerShouldUseImageModeGraceWindow();
2328
+ await testBossChatAppShouldUseNetworkBeforeImageFallback();
2329
+ await testBossChatAppShouldFallbackToImageAfterNetworkMiss();
2330
+ await testBossChatAppShouldRetryLateNetworkBeforeDomFallback();
2331
+ await testBossChatAppShouldUseDomOnlyAfterHigherPriorityPathsFail();
1742
2332
  await testBossChatAppShouldPersistEvidenceArtifacts();
2333
+ await testBossChatReportStoreShouldWriteReadableMarkdownAndCsv();
1743
2334
  console.log("boss-chat tests passed");
1744
2335
  }
1745
2336