@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1

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.
Files changed (88) hide show
  1. package/README.md +86 -33
  2. package/package.json +62 -9
  3. package/skills/boss-chat/SKILL.md +5 -4
  4. package/skills/boss-recommend-pipeline/SKILL.md +21 -31
  5. package/skills/boss-recruit-pipeline/README.md +17 -0
  6. package/skills/boss-recruit-pipeline/SKILL.md +55 -0
  7. package/src/chat-mcp.js +1333 -0
  8. package/src/chat-runtime-config.js +559 -0
  9. package/src/cli.js +1254 -225
  10. package/src/core/browser/index.js +378 -0
  11. package/src/core/capture/index.js +298 -0
  12. package/src/core/cv-acquisition/index.js +219 -0
  13. package/src/core/greet-quota/index.js +54 -0
  14. package/src/core/infinite-list/index.js +459 -0
  15. package/src/core/reporting/legacy-csv.js +332 -0
  16. package/src/core/run/index.js +286 -0
  17. package/src/core/screening/index.js +1166 -0
  18. package/src/core/self-heal/index.js +848 -0
  19. package/src/domains/chat/cards.js +129 -0
  20. package/src/domains/chat/constants.js +183 -0
  21. package/src/domains/chat/detail.js +1369 -0
  22. package/src/domains/chat/index.js +7 -0
  23. package/src/domains/chat/jobs.js +334 -0
  24. package/src/domains/chat/page-guard.js +88 -0
  25. package/src/domains/chat/roots.js +56 -0
  26. package/src/domains/chat/run-service.js +1101 -0
  27. package/src/domains/recommend/actions.js +457 -0
  28. package/src/domains/recommend/cards.js +228 -0
  29. package/src/domains/recommend/constants.js +141 -0
  30. package/src/domains/recommend/detail.js +341 -0
  31. package/src/domains/recommend/filters.js +581 -0
  32. package/src/domains/recommend/index.js +10 -0
  33. package/src/domains/recommend/jobs.js +232 -0
  34. package/src/domains/recommend/refresh.js +204 -0
  35. package/src/domains/recommend/roots.js +78 -0
  36. package/src/domains/recommend/run-service.js +903 -0
  37. package/src/domains/recommend/scopes.js +245 -0
  38. package/src/domains/recruit/actions.js +277 -0
  39. package/src/domains/recruit/cards.js +66 -0
  40. package/src/domains/recruit/constants.js +130 -0
  41. package/src/domains/recruit/detail.js +414 -0
  42. package/src/domains/recruit/index.js +9 -0
  43. package/src/domains/recruit/instruction-parser.js +451 -0
  44. package/src/domains/recruit/refresh.js +40 -0
  45. package/src/domains/recruit/roots.js +67 -0
  46. package/src/domains/recruit/run-service.js +580 -0
  47. package/src/domains/recruit/search.js +1149 -0
  48. package/src/index.js +578 -419
  49. package/src/recommend-mcp.js +1257 -0
  50. package/src/recruit-mcp.js +1035 -0
  51. package/src/adapters.js +0 -3079
  52. package/src/boss-chat.js +0 -1037
  53. package/src/pipeline.js +0 -2249
  54. package/src/recommend-healing-config.js +0 -131
  55. package/src/recommend-healing-rules.json +0 -261
  56. package/src/self-heal.js +0 -2237
  57. package/src/test-adapters-runtime.js +0 -628
  58. package/src/test-boss-chat.js +0 -3196
  59. package/src/test-index-async.js +0 -498
  60. package/src/test-parser.js +0 -742
  61. package/src/test-pipeline.js +0 -2703
  62. package/src/test-run-state.js +0 -152
  63. package/src/test-self-heal.js +0 -224
  64. package/vendor/boss-chat-cli/README.md +0 -134
  65. package/vendor/boss-chat-cli/package.json +0 -53
  66. package/vendor/boss-chat-cli/src/app.js +0 -1501
  67. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  68. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  69. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  70. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  71. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  72. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  73. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  74. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  75. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  76. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  77. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  78. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  79. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  80. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  81. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  82. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  83. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  84. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  85. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  86. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  87. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  88. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,1035 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ assertNoForbiddenCdpCalls,
6
+ bringPageToFront,
7
+ connectToChromeTarget,
8
+ enableDomains,
9
+ sleep
10
+ } from "./core/browser/index.js";
11
+ import {
12
+ RUN_STATUS_CANCELING,
13
+ RUN_STATUS_CANCELED,
14
+ RUN_STATUS_COMPLETED,
15
+ RUN_STATUS_FAILED,
16
+ RUN_STATUS_PAUSED
17
+ } from "./core/run/index.js";
18
+ import {
19
+ buildLegacyScreenInputRows,
20
+ cloneReportInput,
21
+ writeLegacyScreenCsv
22
+ } from "./core/reporting/legacy-csv.js";
23
+ import {
24
+ createRecruitRunService,
25
+ parseRecruitInstruction,
26
+ RECRUIT_TARGET_URL,
27
+ runRecruitWorkflow,
28
+ waitForRecruitSearchControls
29
+ } from "./domains/recruit/index.js";
30
+
31
+ const RUN_MODE_ASYNC = "async";
32
+ const RUN_MODE_SYNC = "sync";
33
+ const DEFAULT_RECRUIT_POLL_AFTER_SEC = 10;
34
+ const DEFAULT_RECRUIT_HOST = "127.0.0.1";
35
+ const DEFAULT_RECRUIT_PORT = 9222;
36
+ const TARGET_COUNT_SEMANTICS = "target_count means processed candidate count, not passed candidate count";
37
+ const DEFAULT_RECRUIT_HOME_DIR = ".boss-recruit-mcp";
38
+
39
+ const TERMINAL_STATUSES = new Set([
40
+ RUN_STATUS_COMPLETED,
41
+ RUN_STATUS_FAILED,
42
+ RUN_STATUS_CANCELED
43
+ ]);
44
+
45
+ let recruitWorkflowImpl = runRecruitWorkflow;
46
+ let recruitConnectorImpl = connectRecruitChromeSession;
47
+ let recruitRunService = createRecruitRunService({
48
+ idPrefix: "mcp_recruit",
49
+ workflow: (...args) => recruitWorkflowImpl(...args)
50
+ });
51
+ const recruitRunMeta = new Map();
52
+
53
+ function normalizeText(value) {
54
+ return String(value || "").replace(/\s+/g, " ").trim();
55
+ }
56
+
57
+ function parsePositiveInteger(raw, fallback) {
58
+ const parsed = Number.parseInt(String(raw || ""), 10);
59
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
60
+ }
61
+
62
+ function parseNonNegativeInteger(raw, fallback) {
63
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
64
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
65
+ }
66
+
67
+ function methodSummary(methodLog = []) {
68
+ const summary = {};
69
+ for (const entry of methodLog || []) {
70
+ summary[entry.method] = (summary[entry.method] || 0) + 1;
71
+ }
72
+ return summary;
73
+ }
74
+
75
+ function normalizeExecutionMode(value) {
76
+ return normalizeText(value).toLowerCase() === RUN_MODE_SYNC ? RUN_MODE_SYNC : RUN_MODE_ASYNC;
77
+ }
78
+
79
+ function clonePlain(value, fallback = null) {
80
+ try {
81
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
82
+ } catch {
83
+ return fallback;
84
+ }
85
+ }
86
+
87
+ function normalizeRunId(runId) {
88
+ const normalized = normalizeText(runId);
89
+ if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
90
+ return normalized;
91
+ }
92
+
93
+ function getRecruitStateHome() {
94
+ const fromEnv = normalizeText(globalThis.process?.env?.BOSS_RECRUIT_HOME || "");
95
+ return fromEnv ? path.resolve(fromEnv) : path.join(os.homedir(), DEFAULT_RECRUIT_HOME_DIR);
96
+ }
97
+
98
+ function getRecruitRunsDir() {
99
+ return path.join(getRecruitStateHome(), "runs");
100
+ }
101
+
102
+ function getRecruitRunArtifacts(runId) {
103
+ const normalized = normalizeRunId(runId);
104
+ if (!normalized) return null;
105
+ const runsDir = getRecruitRunsDir();
106
+ return {
107
+ runs_dir: runsDir,
108
+ run_state_path: path.join(runsDir, `${normalized}.json`),
109
+ checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
110
+ output_csv: path.join(runsDir, `${normalized}.results.csv`),
111
+ report_json: path.join(runsDir, `${normalized}.report.json`)
112
+ };
113
+ }
114
+
115
+ function ensureDirectory(dirPath) {
116
+ fs.mkdirSync(dirPath, { recursive: true });
117
+ }
118
+
119
+ function writeJsonAtomic(filePath, payload) {
120
+ ensureDirectory(path.dirname(filePath));
121
+ const tempPath = `${filePath}.tmp`;
122
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
123
+ fs.renameSync(tempPath, filePath);
124
+ }
125
+
126
+ function readJsonFile(filePath) {
127
+ try {
128
+ if (!fs.existsSync(filePath)) return null;
129
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
130
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ function selectedRecruitJobForCsv(meta = {}) {
137
+ const keyword = normalizeText(
138
+ meta.parsed?.proposed_keyword
139
+ || meta.parsed?.searchParams?.keyword
140
+ || meta.args?.confirmation?.keyword_value
141
+ || meta.args?.overrides?.keyword
142
+ || ""
143
+ );
144
+ return {
145
+ value: keyword,
146
+ title: keyword,
147
+ label: keyword
148
+ };
149
+ }
150
+
151
+ function buildRecruitCsvInputRows(snapshot = {}, meta = {}) {
152
+ const searchParams = meta.parsed?.searchParams || snapshot.summary?.search_params || {};
153
+ const screenParams = meta.parsed?.screenParams || {};
154
+ return buildLegacyScreenInputRows({
155
+ instruction: meta.args?.instruction || "",
156
+ selectedPage: "search",
157
+ selectedJob: selectedRecruitJobForCsv(meta),
158
+ userSearchParams: cloneReportInput(searchParams, {}),
159
+ effectiveSearchParams: cloneReportInput(searchParams, {}),
160
+ screenParams: {
161
+ criteria: screenParams.criteria || "",
162
+ target_count: screenParams.target_count || snapshot.progress?.target_count || snapshot.context?.max_candidates || "",
163
+ post_action: screenParams.post_action || "none",
164
+ max_greet_count: screenParams.max_greet_count ?? ""
165
+ },
166
+ followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || null
167
+ });
168
+ }
169
+
170
+ function writeRecruitLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
171
+ writeLegacyScreenCsv(filePath, {
172
+ inputRows: buildRecruitCsvInputRows(snapshot, meta),
173
+ results: rows
174
+ });
175
+ }
176
+
177
+ function readRecruitRunState(runId) {
178
+ const artifacts = getRecruitRunArtifacts(runId);
179
+ if (!artifacts) return null;
180
+ return readJsonFile(artifacts.run_state_path);
181
+ }
182
+
183
+ function ensureRecruitRunArtifacts(snapshot) {
184
+ const artifacts = getRecruitRunArtifacts(snapshot?.runId || snapshot?.run_id);
185
+ if (!artifacts) return null;
186
+
187
+ const meta = getRecruitRunMeta(snapshot?.runId || snapshot?.run_id);
188
+ const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
189
+ ? snapshot.checkpoint
190
+ : {};
191
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
192
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
193
+
194
+ const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
195
+ if (summary) {
196
+ const rows = Array.isArray(summary.results) ? summary.results : [];
197
+ writeRecruitLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
198
+ writeJsonAtomic(artifacts.report_json, {
199
+ run_id: snapshot.runId || snapshot.run_id,
200
+ status: snapshot.status || snapshot.state,
201
+ phase: snapshot.phase || snapshot.stage,
202
+ progress: snapshot.progress || {},
203
+ context: snapshot.context || {},
204
+ checkpoint,
205
+ summary,
206
+ generated_at: new Date().toISOString()
207
+ });
208
+ if (meta) {
209
+ meta.outputCsvPath = artifacts.output_csv;
210
+ meta.reportJsonPath = artifacts.report_json;
211
+ }
212
+ }
213
+
214
+ return artifacts;
215
+ }
216
+
217
+ function toIsoOrNull(value) {
218
+ const normalized = normalizeText(value);
219
+ return normalized || null;
220
+ }
221
+
222
+ function secondsBetween(startedAt, endedAt) {
223
+ const startMs = Date.parse(startedAt || "");
224
+ const endMs = Date.parse(endedAt || "") || Date.now();
225
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
226
+ return Math.max(1, Math.round((endMs - startMs) / 1000));
227
+ }
228
+
229
+ function normalizeLegacyProgress(progress = {}, summary = null) {
230
+ const processed = Number.isInteger(progress.processed)
231
+ ? progress.processed
232
+ : Number.isInteger(summary?.processed)
233
+ ? summary.processed
234
+ : 0;
235
+ const passed = Number.isInteger(progress.passed)
236
+ ? progress.passed
237
+ : Number.isInteger(summary?.passed)
238
+ ? summary.passed
239
+ : 0;
240
+ return {
241
+ ...progress,
242
+ processed,
243
+ passed,
244
+ skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
245
+ greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0
246
+ };
247
+ }
248
+
249
+ function completionReason(status) {
250
+ if (status === RUN_STATUS_COMPLETED) return "completed";
251
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
252
+ if (status === RUN_STATUS_FAILED) return "failed";
253
+ if (status === RUN_STATUS_PAUSED) return "paused";
254
+ return null;
255
+ }
256
+
257
+ function buildLegacyRunResult(snapshot) {
258
+ if (!snapshot) return null;
259
+ const artifacts = ensureRecruitRunArtifacts(snapshot);
260
+ const meta = getRecruitRunMeta(snapshot.runId);
261
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
262
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
263
+ const targetCount = Number.isInteger(progress.target_count)
264
+ ? progress.target_count
265
+ : Number.isInteger(snapshot.context?.max_candidates)
266
+ ? snapshot.context.max_candidates
267
+ : null;
268
+ return {
269
+ target_count: targetCount,
270
+ processed_count: progress.processed,
271
+ passed_count: progress.passed,
272
+ screened_count: Number.isInteger(progress.screened)
273
+ ? progress.screened
274
+ : Number.isInteger(summary?.screened)
275
+ ? summary.screened
276
+ : progress.processed,
277
+ detail_opened: Number.isInteger(progress.detail_opened)
278
+ ? progress.detail_opened
279
+ : Number.isInteger(summary?.detail_opened)
280
+ ? summary.detail_opened
281
+ : 0,
282
+ duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt || snapshot.updatedAt),
283
+ output_csv: summary?.output_csv || meta.outputCsvPath || artifacts?.output_csv || null,
284
+ report_json: summary?.report_json || meta.reportJsonPath || artifacts?.report_json || null,
285
+ round_count: 1,
286
+ current_round_index: 1,
287
+ checkpoint_path: snapshot.checkpoint?.checkpoint_path
288
+ || snapshot.checkpoint?.path
289
+ || meta.checkpointPath
290
+ || artifacts?.checkpoint_path
291
+ || null,
292
+ completion_reason: completionReason(snapshot.status),
293
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
294
+ run_id: snapshot.runId
295
+ };
296
+ }
297
+
298
+ function createTargetCountSchema(description) {
299
+ return {
300
+ oneOf: [
301
+ { type: "integer", minimum: 1 },
302
+ { type: "string", pattern: "^[1-9][0-9]*$" }
303
+ ],
304
+ description
305
+ };
306
+ }
307
+
308
+ export function createRecruitPipelineInputSchema() {
309
+ return {
310
+ type: "object",
311
+ properties: {
312
+ instruction: {
313
+ type: "string",
314
+ description: "用户自然语言招聘指令"
315
+ },
316
+ execution_mode: {
317
+ type: "string",
318
+ enum: [RUN_MODE_ASYNC, RUN_MODE_SYNC],
319
+ description: "执行模式;默认 async。"
320
+ },
321
+ confirmation: {
322
+ type: "object",
323
+ properties: {
324
+ keyword_confirmed: { type: "boolean" },
325
+ keyword_value: { type: "string" },
326
+ search_params_confirmed: { type: "boolean" },
327
+ criteria_confirmed: { type: "boolean" },
328
+ use_default_for_missing: { type: "boolean" }
329
+ },
330
+ additionalProperties: false
331
+ },
332
+ overrides: {
333
+ type: "object",
334
+ properties: {
335
+ city: { type: "string" },
336
+ degree: { type: "string" },
337
+ filter_recent_viewed: { type: "boolean" },
338
+ schools: {
339
+ anyOf: [
340
+ { type: "array", items: { type: "string" } },
341
+ { type: "string" }
342
+ ]
343
+ },
344
+ keyword: { type: "string" },
345
+ target_count: { type: "integer", minimum: 1 },
346
+ criteria: { type: "string" }
347
+ },
348
+ additionalProperties: false
349
+ },
350
+ host: {
351
+ type: "string",
352
+ description: "可选,Chrome 调试 host;默认 127.0.0.1"
353
+ },
354
+ port: {
355
+ type: "integer",
356
+ minimum: 1,
357
+ description: "可选,Chrome 调试端口;默认 9222"
358
+ },
359
+ target_url_includes: {
360
+ type: "string",
361
+ description: "可选,Chrome target URL 匹配片段;默认 Boss search 页"
362
+ },
363
+ allow_navigate: {
364
+ type: "boolean",
365
+ description: "找不到 search target 时,是否允许复用 Boss chat target 并导航到 search;默认 true"
366
+ },
367
+ reset_search: {
368
+ type: "boolean",
369
+ description: "执行前是否重置 Boss search frame;默认 true"
370
+ },
371
+ slow_live: {
372
+ type: "boolean",
373
+ description: "VPN/慢页面模式:放宽 live DOM 等待时间"
374
+ },
375
+ max_candidates: createTargetCountSchema("本次最多处理候选人数;默认使用解析出的 target_count"),
376
+ detail_limit: {
377
+ type: "integer",
378
+ minimum: 0,
379
+ description: "打开详情的人数上限;默认 1,0 表示只用卡片信息"
380
+ },
381
+ delay_ms: {
382
+ type: "integer",
383
+ minimum: 0,
384
+ description: "候选人之间的延迟;live pause/resume 测试可增大它"
385
+ }
386
+ },
387
+ required: ["instruction"],
388
+ additionalProperties: false
389
+ };
390
+ }
391
+
392
+ export function createRecruitRunIdInputSchema() {
393
+ return {
394
+ type: "object",
395
+ properties: {
396
+ run_id: { type: "string" }
397
+ },
398
+ required: ["run_id"],
399
+ additionalProperties: false
400
+ };
401
+ }
402
+
403
+ export function validateRecruitPipelineArgs(args) {
404
+ if (!args || typeof args !== "object") return "arguments must be an object";
405
+ if (!args.instruction || typeof args.instruction !== "string") {
406
+ return "instruction is required and must be a string";
407
+ }
408
+ return null;
409
+ }
410
+
411
+ function buildRequiredConfirmations(parsedResult) {
412
+ const confirmations = [];
413
+ if (parsedResult.needs_search_params_confirmation) confirmations.push("search_params");
414
+ if (parsedResult.needs_keyword_confirmation) confirmations.push("keyword");
415
+ if (parsedResult.needs_recent_viewed_filter_confirmation) confirmations.push("filter_recent_viewed");
416
+ if (parsedResult.needs_criteria_confirmation) confirmations.push("criteria");
417
+ if (parsedResult.has_unresolved_missing_fields) confirmations.push("missing_fields_or_defaults");
418
+ if ((parsedResult.suspicious_fields || []).length) confirmations.push("suspicious_fields");
419
+ return confirmations;
420
+ }
421
+
422
+ function buildNeedInputResponse(parsedResult) {
423
+ return {
424
+ status: "NEED_INPUT",
425
+ missing_fields: parsedResult.missing_fields,
426
+ proposed_keyword: parsedResult.proposed_keyword,
427
+ required_confirmations: buildRequiredConfirmations(parsedResult),
428
+ search_params: parsedResult.searchParams,
429
+ screen_params: parsedResult.screenParams,
430
+ pending_questions: parsedResult.pending_questions,
431
+ review: parsedResult.review,
432
+ error: {
433
+ code: "MISSING_REQUIRED_FIELDS",
434
+ message: "缺少必要字段。请先补齐缺失项;若要按默认值继续,必须先明确确认默认值及其风险。",
435
+ retryable: true
436
+ }
437
+ };
438
+ }
439
+
440
+ function buildNeedConfirmationResponse(parsedResult) {
441
+ return {
442
+ status: "NEED_CONFIRMATION",
443
+ proposed_keyword: parsedResult.proposed_keyword,
444
+ required_confirmations: buildRequiredConfirmations(parsedResult),
445
+ search_params: {
446
+ ...parsedResult.searchParams,
447
+ keyword: parsedResult.proposed_keyword || parsedResult.searchParams.keyword
448
+ },
449
+ screen_params: parsedResult.screenParams,
450
+ pending_questions: parsedResult.pending_questions,
451
+ review: parsedResult.review
452
+ };
453
+ }
454
+
455
+ function parseRecruitPipelineRequest(args = {}) {
456
+ const parsed = parseRecruitInstruction({
457
+ instruction: args.instruction,
458
+ confirmation: args.confirmation,
459
+ overrides: args.overrides
460
+ });
461
+ const criteriaOverride = normalizeText(args.overrides?.criteria || "");
462
+ if (criteriaOverride) {
463
+ parsed.screenParams = {
464
+ ...parsed.screenParams,
465
+ criteria: criteriaOverride
466
+ };
467
+ parsed.review = {
468
+ ...parsed.review,
469
+ current_screen_params: {
470
+ ...(parsed.review?.current_screen_params || {}),
471
+ criteria: criteriaOverride
472
+ }
473
+ };
474
+ }
475
+ return parsed;
476
+ }
477
+
478
+ function evaluateRecruitPipelineGate(parsed) {
479
+ if (parsed.has_unresolved_missing_fields) return buildNeedInputResponse(parsed);
480
+ if (
481
+ parsed.needs_keyword_confirmation
482
+ || parsed.needs_recent_viewed_filter_confirmation
483
+ || parsed.needs_criteria_confirmation
484
+ || parsed.needs_search_params_confirmation
485
+ || (parsed.suspicious_fields || []).length > 0
486
+ ) {
487
+ return buildNeedConfirmationResponse(parsed);
488
+ }
489
+ return null;
490
+ }
491
+
492
+ function normalizeRunSnapshot(snapshot) {
493
+ if (!snapshot) return null;
494
+ const meta = getRecruitRunMeta(snapshot.runId);
495
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
496
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
497
+ const legacyResult = (
498
+ TERMINAL_STATUSES.has(snapshot.status)
499
+ || snapshot.status === RUN_STATUS_PAUSED
500
+ ) ? buildLegacyRunResult({ ...snapshot, progress }) : null;
501
+ const oldContext = {
502
+ workspace_root: meta.workspaceRoot || null,
503
+ instruction: meta.args?.instruction || "",
504
+ confirmation: clonePlain(meta.args?.confirmation || {}, {}),
505
+ overrides: clonePlain(meta.args?.overrides || {}, {}),
506
+ rounds: []
507
+ };
508
+ return {
509
+ ...snapshot,
510
+ progress,
511
+ run_id: snapshot.runId,
512
+ mode: meta.mode || RUN_MODE_ASYNC,
513
+ state: snapshot.status,
514
+ stage: snapshot.phase,
515
+ started_at: snapshot.startedAt,
516
+ updated_at: snapshot.updatedAt,
517
+ completed_at: toIsoOrNull(snapshot.completedAt),
518
+ heartbeat_at: snapshot.updatedAt,
519
+ pid: globalThis.process?.pid || null,
520
+ last_message: snapshot.error?.message || snapshot.phase || null,
521
+ context: {
522
+ ...(snapshot.context || {}),
523
+ ...oldContext,
524
+ shared_run_context: snapshot.context || {}
525
+ },
526
+ control: {
527
+ pause_requested: snapshot.status === RUN_STATUS_PAUSED,
528
+ pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
529
+ pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_recruit_pipeline_run" : null,
530
+ cancel_requested: snapshot.status === RUN_STATUS_CANCELING
531
+ },
532
+ resume: {
533
+ checkpoint_path: legacyResult?.checkpoint_path || null,
534
+ pause_control_path: getRecruitRunArtifacts(snapshot.runId)?.run_state_path || null,
535
+ output_csv: legacyResult?.output_csv || null,
536
+ resume_count: meta.resumeCount || 0,
537
+ last_resumed_at: meta.lastResumedAt || null,
538
+ last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
539
+ },
540
+ result: legacyResult,
541
+ artifacts: getRecruitRunArtifacts(snapshot.runId)
542
+ };
543
+ }
544
+
545
+ function persistRecruitRunSnapshot(snapshot) {
546
+ const normalized = normalizeRunSnapshot(snapshot);
547
+ if (!normalized?.run_id) return normalized;
548
+ const artifacts = getRecruitRunArtifacts(normalized.run_id);
549
+ if (!artifacts) return normalized;
550
+ const payload = {
551
+ run_id: normalized.run_id,
552
+ mode: normalized.mode,
553
+ state: normalized.state,
554
+ stage: normalized.stage,
555
+ started_at: normalized.started_at,
556
+ updated_at: normalized.updated_at,
557
+ heartbeat_at: normalized.heartbeat_at,
558
+ completed_at: normalized.completed_at,
559
+ pid: normalized.pid,
560
+ progress: normalized.progress,
561
+ last_message: normalized.last_message,
562
+ context: normalized.context,
563
+ control: normalized.control,
564
+ resume: normalized.resume,
565
+ error: normalized.error,
566
+ result: normalized.result,
567
+ summary: normalized.summary,
568
+ artifacts: normalized.artifacts
569
+ };
570
+ writeJsonAtomic(artifacts.run_state_path, payload);
571
+ return normalized;
572
+ }
573
+
574
+ function getRecruitRunMeta(runId) {
575
+ return recruitRunMeta.get(runId) || {};
576
+ }
577
+
578
+ function attachMethodEvidence(payload, runId) {
579
+ const meta = getRecruitRunMeta(runId);
580
+ return {
581
+ ...payload,
582
+ runtime_evaluate_used: false,
583
+ method_summary: methodSummary(meta.methodLog || []),
584
+ method_log: meta.methodLog || [],
585
+ chrome: meta.chrome || null
586
+ };
587
+ }
588
+
589
+ async function connectRecruitChromeSession({
590
+ host = DEFAULT_RECRUIT_HOST,
591
+ port = DEFAULT_RECRUIT_PORT,
592
+ targetUrlIncludes = RECRUIT_TARGET_URL,
593
+ allowNavigate = true,
594
+ slowLive = false
595
+ } = {}) {
596
+ let session;
597
+ try {
598
+ session = await connectToChromeTarget({
599
+ host,
600
+ port,
601
+ targetUrlIncludes
602
+ });
603
+ } catch (error) {
604
+ if (!allowNavigate) throw error;
605
+ session = await connectToChromeTarget({
606
+ host,
607
+ port,
608
+ targetPredicate: (target) => (
609
+ target?.type === "page"
610
+ && String(target?.url || "").includes("zhipin.com/web/chat")
611
+ )
612
+ });
613
+ }
614
+
615
+ const { client, target } = session;
616
+ await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
617
+ if (typeof client?.Network?.setCacheDisabled === "function") {
618
+ await client.Network.setCacheDisabled({ cacheDisabled: true });
619
+ }
620
+ await bringPageToFront(client);
621
+
622
+ const targetUrl = String(target?.url || "");
623
+ if (allowNavigate && !targetUrl.includes(targetUrlIncludes)) {
624
+ await client.Page.navigate({ url: RECRUIT_TARGET_URL });
625
+ await sleep(slowLive ? 8000 : 3000);
626
+ }
627
+
628
+ const controls = await waitForRecruitSearchControls(client, {
629
+ timeoutMs: slowLive ? 180000 : 90000,
630
+ intervalMs: 300
631
+ });
632
+ if (!controls.ok) {
633
+ throw new Error("Boss recruit search page did not expose ready search controls");
634
+ }
635
+
636
+ return {
637
+ ...session,
638
+ controls
639
+ };
640
+ }
641
+
642
+ function getRunOptions(args, parsed, session) {
643
+ const slowLive = args.slow_live === true;
644
+ const targetCount = parsePositiveInteger(args.max_candidates, parsed.screenParams.target_count || 10);
645
+ return {
646
+ client: session.client,
647
+ targetUrl: RECRUIT_TARGET_URL,
648
+ criteria: parsed.screenParams.criteria,
649
+ searchParams: parsed.searchParams,
650
+ maxCandidates: targetCount,
651
+ detailLimit: parseNonNegativeInteger(args.detail_limit, 1),
652
+ closeDetail: true,
653
+ delayMs: Math.max(0, parsePositiveInteger(args.delay_ms, 0)),
654
+ cardTimeoutMs: slowLive ? 180000 : 90000,
655
+ resetBeforeSearch: args.reset_search !== false,
656
+ resetTimeoutMs: slowLive ? 300000 : 180000,
657
+ cityOptionTimeoutMs: slowLive ? 60000 : 30000,
658
+ name: "mcp-recruit-pipeline-run"
659
+ };
660
+ }
661
+
662
+ async function closeRecruitRunSession(runId) {
663
+ const meta = recruitRunMeta.get(runId);
664
+ if (!meta || meta.closed) return;
665
+ try {
666
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
667
+ } finally {
668
+ meta.closed = true;
669
+ try {
670
+ await meta.session?.close?.();
671
+ } catch {
672
+ // Nothing actionable for the caller once the run has settled.
673
+ }
674
+ }
675
+ }
676
+
677
+ async function waitForRecruitRunTerminal(runId) {
678
+ while (true) {
679
+ try {
680
+ const snapshot = recruitRunService.getRecruitRun(runId);
681
+ if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
682
+ } catch {
683
+ return null;
684
+ }
685
+ await sleep(1000);
686
+ }
687
+ }
688
+
689
+ function trackRecruitRun(runId) {
690
+ waitForRecruitRunTerminal(runId)
691
+ .then((terminal) => {
692
+ if (terminal) persistRecruitRunSnapshot(terminal);
693
+ })
694
+ .catch(() => null)
695
+ .finally(() => {
696
+ closeRecruitRunSession(runId).catch(() => {});
697
+ });
698
+ }
699
+
700
+ async function startRecruitPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
701
+ const parsed = parseRecruitPipelineRequest(args);
702
+ const gate = evaluateRecruitPipelineGate(parsed);
703
+ if (gate) return gate;
704
+
705
+ let session;
706
+ try {
707
+ session = await recruitConnectorImpl({
708
+ host: normalizeText(args.host) || DEFAULT_RECRUIT_HOST,
709
+ port: parsePositiveInteger(args.port, DEFAULT_RECRUIT_PORT),
710
+ targetUrlIncludes: normalizeText(args.target_url_includes) || RECRUIT_TARGET_URL,
711
+ allowNavigate: args.allow_navigate !== false,
712
+ slowLive: args.slow_live === true
713
+ });
714
+ } catch (error) {
715
+ return {
716
+ status: "FAILED",
717
+ error: {
718
+ code: "BOSS_SEARCH_PAGE_NOT_READY",
719
+ message: error?.message || "Boss recruit search page is not ready",
720
+ retryable: true
721
+ }
722
+ };
723
+ }
724
+
725
+ let started;
726
+ try {
727
+ started = recruitRunService.startRecruitRun(getRunOptions(args, parsed, session));
728
+ } catch (error) {
729
+ await session.close?.();
730
+ return {
731
+ status: "FAILED",
732
+ error: {
733
+ code: "RECRUIT_RUN_START_FAILED",
734
+ message: error?.message || "Failed to start recruit run",
735
+ retryable: true
736
+ }
737
+ };
738
+ }
739
+
740
+ recruitRunMeta.set(started.runId, {
741
+ session,
742
+ methodLog: session.methodLog || [],
743
+ mode: normalizeExecutionMode(args.execution_mode),
744
+ workspaceRoot: normalizeText(workspaceRoot) || globalThis.process?.cwd?.() || "",
745
+ args: clonePlain(args, {}),
746
+ chrome: {
747
+ host: normalizeText(args.host) || DEFAULT_RECRUIT_HOST,
748
+ port: parsePositiveInteger(args.port, DEFAULT_RECRUIT_PORT),
749
+ target_url: session.target?.url || RECRUIT_TARGET_URL,
750
+ target_id: session.target?.id || null
751
+ },
752
+ parsed
753
+ });
754
+ trackRecruitRun(started.runId);
755
+ const persistedStarted = persistRecruitRunSnapshot(started);
756
+
757
+ return {
758
+ status: "ACCEPTED",
759
+ run_id: persistedStarted.run_id,
760
+ state: persistedStarted.state,
761
+ run: persistedStarted,
762
+ poll_after_sec: DEFAULT_RECRUIT_POLL_AFTER_SEC,
763
+ review: parsed.review,
764
+ message: "Recruit pipeline run started through shared CDP-only recruit service."
765
+ };
766
+ }
767
+
768
+ export async function runRecruitPipelineTool({ workspaceRoot = "", args = {} } = {}) {
769
+ const mode = normalizeExecutionMode(args.execution_mode);
770
+ const started = await startRecruitPipelineRunInternal({
771
+ ...args,
772
+ execution_mode: mode
773
+ }, { workspaceRoot });
774
+ if (started.status !== "ACCEPTED") return started;
775
+ if (mode !== RUN_MODE_SYNC) return attachMethodEvidence(started, started.run_id);
776
+
777
+ const final = await waitForRecruitRunTerminal(started.run_id);
778
+ await closeRecruitRunSession(started.run_id);
779
+ const normalizedFinal = persistRecruitRunSnapshot(final);
780
+ const legacyResult = normalizedFinal?.result || buildLegacyRunResult(final);
781
+ const finalStatus = final?.status === RUN_STATUS_COMPLETED
782
+ ? "COMPLETED"
783
+ : final?.status === RUN_STATUS_CANCELED
784
+ ? "CANCELED"
785
+ : "FAILED";
786
+ return attachMethodEvidence({
787
+ status: finalStatus,
788
+ run_id: started.run_id,
789
+ run: normalizedFinal,
790
+ result: legacyResult,
791
+ partial_result: finalStatus === "CANCELED" ? legacyResult : undefined,
792
+ diagnostics: finalStatus === "FAILED"
793
+ ? {
794
+ run_id: started.run_id,
795
+ last_stage: normalizedFinal?.stage || "recruit:unknown"
796
+ }
797
+ : undefined,
798
+ summary: final?.summary || null,
799
+ error: finalStatus === "CANCELED"
800
+ ? {
801
+ code: "PIPELINE_CANCELED",
802
+ message: "流水线已取消。",
803
+ retryable: true
804
+ }
805
+ : final?.error || null
806
+ }, started.run_id);
807
+ }
808
+
809
+ export async function startRecruitPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
810
+ const started = await startRecruitPipelineRunInternal({
811
+ ...args,
812
+ execution_mode: RUN_MODE_ASYNC
813
+ }, { workspaceRoot });
814
+ if (started.status !== "ACCEPTED") return started;
815
+ return attachMethodEvidence(started, started.run_id);
816
+ }
817
+
818
+ export function getRecruitPipelineRunTool({ args = {} } = {}) {
819
+ const runId = normalizeText(args.run_id);
820
+ if (!runId) {
821
+ return {
822
+ status: "FAILED",
823
+ error: {
824
+ code: "INVALID_RUN_ID",
825
+ message: "run_id is required",
826
+ retryable: false
827
+ }
828
+ };
829
+ }
830
+ try {
831
+ const run = recruitRunService.getRecruitRun(runId);
832
+ const normalizedRun = persistRecruitRunSnapshot(run);
833
+ return attachMethodEvidence({
834
+ status: "RUN_STATUS",
835
+ run: normalizedRun
836
+ }, runId);
837
+ } catch {
838
+ const persisted = readRecruitRunState(runId);
839
+ if (persisted) {
840
+ return {
841
+ status: "RUN_STATUS",
842
+ run: persisted,
843
+ persistence: {
844
+ source: "disk",
845
+ active_control_available: false
846
+ },
847
+ runtime_evaluate_used: false,
848
+ method_summary: {},
849
+ method_log: [],
850
+ chrome: null
851
+ };
852
+ }
853
+ return {
854
+ status: "FAILED",
855
+ error: {
856
+ code: "RUN_NOT_FOUND",
857
+ message: `No recruit run found for run_id=${runId}`,
858
+ retryable: false
859
+ }
860
+ };
861
+ }
862
+ }
863
+
864
+ export function pauseRecruitPipelineRunTool({ args = {} } = {}) {
865
+ const runId = normalizeText(args.run_id);
866
+ try {
867
+ const before = recruitRunService.getRecruitRun(runId);
868
+ if (TERMINAL_STATUSES.has(before.status)) {
869
+ const normalizedBefore = persistRecruitRunSnapshot(before);
870
+ return attachMethodEvidence({
871
+ status: "PAUSE_IGNORED",
872
+ run: normalizedBefore,
873
+ message: "目标任务已结束,无需暂停。"
874
+ }, runId);
875
+ }
876
+ if (before.status === RUN_STATUS_PAUSED) {
877
+ const normalizedBefore = persistRecruitRunSnapshot(before);
878
+ return attachMethodEvidence({
879
+ status: "PAUSE_IGNORED",
880
+ run: normalizedBefore,
881
+ message: "目标任务已经处于 paused 状态。"
882
+ }, runId);
883
+ }
884
+ const run = recruitRunService.pauseRecruitRun(runId);
885
+ const normalizedRun = persistRecruitRunSnapshot(run);
886
+ return attachMethodEvidence({
887
+ status: "PAUSE_REQUESTED",
888
+ run: normalizedRun,
889
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
890
+ }, runId);
891
+ } catch {
892
+ const persisted = readRecruitRunState(runId);
893
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
894
+ return {
895
+ status: "PAUSE_IGNORED",
896
+ run: persisted,
897
+ message: "目标任务已结束,无需暂停。",
898
+ runtime_evaluate_used: false,
899
+ method_summary: {},
900
+ method_log: [],
901
+ chrome: null
902
+ };
903
+ }
904
+ return getRecruitPipelineRunTool({ args });
905
+ }
906
+ }
907
+
908
+ export function resumeRecruitPipelineRunTool({ args = {} } = {}) {
909
+ const runId = normalizeText(args.run_id);
910
+ try {
911
+ const before = recruitRunService.getRecruitRun(runId);
912
+ if (TERMINAL_STATUSES.has(before.status)) {
913
+ const normalizedBefore = persistRecruitRunSnapshot(before);
914
+ return attachMethodEvidence({
915
+ status: "FAILED",
916
+ error: {
917
+ code: "RUN_ALREADY_TERMINATED",
918
+ message: "目标任务已结束,无法继续。",
919
+ retryable: false
920
+ },
921
+ run: normalizedBefore
922
+ }, runId);
923
+ }
924
+ if (before.status !== RUN_STATUS_PAUSED) {
925
+ const normalizedBefore = persistRecruitRunSnapshot(before);
926
+ return attachMethodEvidence({
927
+ status: "FAILED",
928
+ error: {
929
+ code: "RUN_NOT_PAUSED",
930
+ message: "仅 paused 状态的 run 才能继续。",
931
+ retryable: true
932
+ },
933
+ run: normalizedBefore
934
+ }, runId);
935
+ }
936
+ const run = recruitRunService.resumeRecruitRun(runId);
937
+ const meta = getRecruitRunMeta(runId);
938
+ if (meta) {
939
+ meta.resumeCount = (meta.resumeCount || 0) + 1;
940
+ meta.lastResumedAt = new Date().toISOString();
941
+ }
942
+ const normalizedRun = persistRecruitRunSnapshot(run);
943
+ return attachMethodEvidence({
944
+ status: "RESUME_REQUESTED",
945
+ run: normalizedRun,
946
+ poll_after_sec: DEFAULT_RECRUIT_POLL_AFTER_SEC,
947
+ message: "已恢复 Boss 招聘流水线,请使用 get_recruit_pipeline_run 按需轮询。"
948
+ }, runId);
949
+ } catch {
950
+ const persisted = readRecruitRunState(runId);
951
+ if (persisted) {
952
+ return {
953
+ status: TERMINAL_STATUSES.has(persisted.state) ? "FAILED" : "FAILED",
954
+ error: {
955
+ code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
956
+ message: TERMINAL_STATUSES.has(persisted.state)
957
+ ? "目标任务已结束,无法继续。"
958
+ : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
959
+ retryable: !TERMINAL_STATUSES.has(persisted.state)
960
+ },
961
+ run: persisted,
962
+ persistence: {
963
+ source: "disk",
964
+ active_control_available: false
965
+ },
966
+ runtime_evaluate_used: false,
967
+ method_summary: {},
968
+ method_log: [],
969
+ chrome: null
970
+ };
971
+ }
972
+ return getRecruitPipelineRunTool({ args });
973
+ }
974
+ }
975
+
976
+ export function cancelRecruitPipelineRunTool({ args = {} } = {}) {
977
+ const runId = normalizeText(args.run_id);
978
+ try {
979
+ const before = recruitRunService.getRecruitRun(runId);
980
+ if (TERMINAL_STATUSES.has(before.status)) {
981
+ const normalizedBefore = persistRecruitRunSnapshot(before);
982
+ return attachMethodEvidence({
983
+ status: "CANCEL_IGNORED",
984
+ run: normalizedBefore,
985
+ message: "目标任务已结束,无需取消。"
986
+ }, runId);
987
+ }
988
+ const run = recruitRunService.cancelRecruitRun(runId);
989
+ const normalizedRun = persistRecruitRunSnapshot(run);
990
+ return attachMethodEvidence({
991
+ status: "CANCEL_REQUESTED",
992
+ run: normalizedRun,
993
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
994
+ }, runId);
995
+ } catch {
996
+ const persisted = readRecruitRunState(runId);
997
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
998
+ return {
999
+ status: "CANCEL_IGNORED",
1000
+ run: persisted,
1001
+ message: "目标任务已结束,无需取消。",
1002
+ runtime_evaluate_used: false,
1003
+ method_summary: {},
1004
+ method_log: [],
1005
+ chrome: null
1006
+ };
1007
+ }
1008
+ return getRecruitPipelineRunTool({ args });
1009
+ }
1010
+ }
1011
+
1012
+ export function __setRecruitMcpConnectorForTests(nextConnector) {
1013
+ recruitConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectRecruitChromeSession;
1014
+ }
1015
+
1016
+ export function __setRecruitMcpWorkflowForTests(nextWorkflow) {
1017
+ recruitWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecruitWorkflow;
1018
+ recruitRunService = createRecruitRunService({
1019
+ idPrefix: "mcp_recruit",
1020
+ workflow: (...args) => recruitWorkflowImpl(...args)
1021
+ });
1022
+ }
1023
+
1024
+ export function __resetRecruitMcpStateForTests() {
1025
+ for (const meta of recruitRunMeta.values()) {
1026
+ try {
1027
+ meta.session?.close?.();
1028
+ } catch {
1029
+ // Best-effort test cleanup.
1030
+ }
1031
+ }
1032
+ recruitRunMeta.clear();
1033
+ __setRecruitMcpConnectorForTests(null);
1034
+ __setRecruitMcpWorkflowForTests(null);
1035
+ }