@reconcrap/boss-recommend-mcp 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -221,10 +221,10 @@ config/screening-config.example.json
221
221
 
222
222
  - `openaiOrganization`
223
223
  - `openaiProject`
224
- - `debugPort`
225
- - `outputDir`
224
+ - `debugPort`:未显式传 `port` 时,recommend / search / chat CDP-only MCP run 和健康检查默认连接这个 Chrome 调试端口。
225
+ - `outputDir`:recommend / search / chat 完成后的最终 CSV 与 report JSON 会写入这里;run state / checkpoint 仍保留在各自状态目录,方便 pause/resume/cancel。
226
226
  - `llmThinkingLevel`:默认 `low`。可设为 `off/minimal/low/medium/high/auto/current`,用于控制 OpenAI-compatible LLM 的 thinking/reasoning 强度。
227
- - `humanRestEnabled`:默认 `false`。`false` recommend-screen 随机休息/批次休息与 boss-chat 批次休息均为 `0ms`;`true` 时恢复随机休息节奏。
227
+ - `humanRestEnabled`:默认 `false`。当前 CDP-only recommend / search / chat run 尚未实现随机休息层,因此会读取并保留该字段但不改变节奏;如后续重新加入 human rest,应以此字段为默认值。
228
228
 
229
229
  ## 常用命令
230
230
 
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "baseUrl": "https://api.openai.com/v1",
3
3
  "apiKey": "replace-with-openai-api-key",
4
- "model": "gpt-4.1-mini",
5
- "llmThinkingLevel": "low",
6
- "llmTimeoutMs": 60000,
7
- "llmMaxRetries": 3,
8
- "humanRestEnabled": false,
9
- "openaiOrganization": "optional-org-id",
10
- "openaiProject": "optional-project-id"
11
- }
4
+ "model": "gpt-4.1-mini",
5
+ "llmThinkingLevel": "low",
6
+ "llmTimeoutMs": 60000,
7
+ "llmMaxRetries": 3,
8
+ "debugPort": 9222,
9
+ "outputDir": "",
10
+ "humanRestEnabled": false,
11
+ "openaiOrganization": "optional-org-id",
12
+ "openaiProject": "optional-project-id"
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/chat-mcp.js CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  getBossChatDataDir,
46
46
  getBossChatTargetCountValue,
47
47
  normalizeTargetCountInput,
48
+ resolveBossConfiguredOutputDir,
48
49
  resolveBossChatRuntimeLayout,
49
50
  resolveBossScreeningConfig
50
51
  } from "./chat-runtime-config.js";
@@ -123,12 +124,14 @@ function getChatRunArtifacts(runId) {
123
124
  const normalized = normalizeRunId(runId);
124
125
  if (!normalized) return null;
125
126
  const runsDir = getChatRunsDir();
127
+ const outputDir = resolveBossConfiguredOutputDir("", runsDir);
126
128
  return {
127
129
  runs_dir: runsDir,
130
+ output_dir: outputDir,
128
131
  run_state_path: path.join(runsDir, `${normalized}.json`),
129
132
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
130
- output_csv: path.join(runsDir, `${normalized}.results.csv`),
131
- report_json: path.join(runsDir, `${normalized}.report.json`)
133
+ output_csv: path.join(outputDir, `${normalized}.results.csv`),
134
+ report_json: path.join(outputDir, `${normalized}.report.json`)
132
135
  };
133
136
  }
134
137
 
@@ -491,7 +494,8 @@ async function waitForHealthyChat(client, config, {
491
494
  domain: "chat",
492
495
  roots: roots.roots,
493
496
  selectorProbes: config.selectorProbes,
494
- accessibilityProbes: config.accessibilityProbes
497
+ accessibilityProbes: config.accessibilityProbes,
498
+ viewportProbes: config.viewportProbes
495
499
  });
496
500
  if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
497
501
  await sleep(intervalMs);
@@ -633,7 +637,7 @@ async function readChatJobOptionsFromSession(session) {
633
637
  return readChatJobOptions(session.client, roots.rootNodes.top);
634
638
  }
635
639
 
636
- function normalizeChatStartInput(args = {}) {
640
+ function normalizeChatStartInput(args = {}, configResolution = null) {
637
641
  const target = normalizeTargetCountInput(getBossChatTargetCountValue(args));
638
642
  return {
639
643
  profile: normalizeText(args.profile) || "default",
@@ -645,7 +649,10 @@ function normalizeChatStartInput(args = {}) {
645
649
  targetCount: target.targetCount,
646
650
  publicTargetCount: target.publicValue,
647
651
  host: normalizeText(args.host) || DEFAULT_CHAT_HOST,
648
- port: parsePositiveInteger(args.port, DEFAULT_CHAT_PORT),
652
+ port: parsePositiveInteger(
653
+ args.port,
654
+ configResolution?.ok ? configResolution.config.debugPort : DEFAULT_CHAT_PORT
655
+ ),
649
656
  targetUrlIncludes: normalizeText(args.target_url_includes) || CHAT_TARGET_URL,
650
657
  allowNavigate: args.allow_navigate !== false,
651
658
  slowLive: args.slow_live === true
@@ -800,7 +807,6 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "" } = {}) {
800
807
  const shouldRequestResume = shouldRequestChatResume(args);
801
808
  const useLlm = shouldUseChatLlm(args, shouldRequestResume);
802
809
  const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false };
803
- const configFile = configResolution.ok ? readJsonFile(configResolution.config_path) : null;
804
810
  return {
805
811
  client: session.client,
806
812
  targetUrl: CHAT_TARGET_URL,
@@ -827,8 +833,7 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "" } = {}) {
827
833
  maxImagePages: parsePositiveInteger(args.max_image_pages, 8),
828
834
  imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
829
835
  llmConfig: configResolution.ok ? {
830
- ...configResolution.config,
831
- apiKey: configFile?.apiKey || ""
836
+ ...configResolution.config
832
837
  } : null,
833
838
  llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
834
839
  llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
@@ -888,7 +893,8 @@ function trackChatRun(runId) {
888
893
  }
889
894
 
890
895
  async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
891
- const normalized = normalizeChatStartInput(args);
896
+ const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
897
+ const normalized = normalizeChatStartInput(args, defaultConfigResolution);
892
898
  const missingFields = getMissingChatStartFields(args, normalized);
893
899
  if (missingFields.length) {
894
900
  return buildNeedInputResponse({
@@ -987,7 +993,8 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {})
987
993
  }
988
994
 
989
995
  export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
990
- const normalized = normalizeChatStartInput(args);
996
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
997
+ const normalized = normalizeChatStartInput(args, configResolution);
991
998
  let session;
992
999
  try {
993
1000
  session = await chatConnectorImpl({
@@ -1129,6 +1136,7 @@ export async function bossChatHealthCheckTool({ workspaceRoot = "", args = {} }
1129
1136
  cli_path: null,
1130
1137
  config_path: configResolution.config_path || null,
1131
1138
  config_dir: configResolution.config_dir || null,
1139
+ output_dir: configResolution.ok ? configResolution.config.outputDir || null : null,
1132
1140
  debug_port: port,
1133
1141
  shared_llm_config: configResolution.ok === true,
1134
1142
  data_dir: runtimeLayout.data_dir,
@@ -223,6 +223,22 @@ function parsePositiveInteger(raw, fallback = null) {
223
223
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
224
224
  }
225
225
 
226
+ function parseConfigBoolean(raw, fallback = false) {
227
+ if (typeof raw === "boolean") return raw;
228
+ const normalized = normalizeText(raw).toLowerCase();
229
+ if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
230
+ if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
231
+ return fallback;
232
+ }
233
+
234
+ function resolveConfigPathValue(raw, configDir) {
235
+ const normalized = normalizeText(raw);
236
+ if (!normalized) return "";
237
+ return path.isAbsolute(normalized)
238
+ ? path.resolve(normalized)
239
+ : path.resolve(configDir || process.cwd(), normalized);
240
+ }
241
+
226
242
  function validateScreeningConfig(config) {
227
243
  if (!config || typeof config !== "object" || Array.isArray(config)) {
228
244
  return {
@@ -369,8 +385,12 @@ export function resolveBossScreeningConfig(workspaceRoot) {
369
385
  ok: true,
370
386
  config: {
371
387
  baseUrl: normalizeText(parsed.baseUrl).replace(/\/+$/, ""),
388
+ apiKey: normalizeText(parsed.apiKey),
372
389
  model: normalizeText(parsed.model),
373
- debugPort: parsePositiveInteger(parsed.debugPort, 9222)
390
+ debugPort: parsePositiveInteger(parsed.debugPort, 9222),
391
+ llmThinkingLevel: normalizeText(parsed.llmThinkingLevel || parsed.thinkingLevel || parsed.reasoningEffort),
392
+ outputDir: resolveConfigPathValue(parsed.outputDir, configDir),
393
+ humanRestEnabled: parseConfigBoolean(parsed.humanRestEnabled, false)
374
394
  },
375
395
  config_path: configPath,
376
396
  config_dir: configDir,
@@ -378,6 +398,13 @@ export function resolveBossScreeningConfig(workspaceRoot) {
378
398
  };
379
399
  }
380
400
 
401
+ export function resolveBossConfiguredOutputDir(workspaceRoot, fallbackDir = "") {
402
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
403
+ const configuredDir = configResolution.ok ? normalizeText(configResolution.config.outputDir) : "";
404
+ if (configuredDir) return configuredDir;
405
+ return fallbackDir ? path.resolve(fallbackDir) : "";
406
+ }
407
+
381
408
  function isUnlimitedTargetCountToken(value) {
382
409
  const token = normalizeText(value).toLowerCase();
383
410
  if (!token) return false;
@@ -10,6 +10,7 @@ export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
10
 
11
11
  export const ALLOWED_CDP_DOMAINS = new Set([
12
12
  "Accessibility",
13
+ "Browser",
13
14
  "DOM",
14
15
  "Input",
15
16
  "Network",
@@ -37,7 +37,7 @@ const GENDER_CODE_MAP = {
37
37
  2: "女"
38
38
  };
39
39
 
40
- const LLM_THINKING_LEVELS = new Set(["off", "low", "medium", "high", "current"]);
40
+ const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
41
41
 
42
42
  function nowIso() {
43
43
  return new Date().toISOString();
@@ -64,9 +64,9 @@ function isVolcengineModel(baseUrl, model) {
64
64
 
65
65
  function applyChatCompletionThinking(payload, { baseUrl = "", model = "", thinkingLevel = "" } = {}) {
66
66
  const level = normalizeLlmThinkingLevel(thinkingLevel);
67
- if (!level || level === "current") return payload;
67
+ if (!level || level === "current" || level === "auto") return payload;
68
68
  if (isVolcengineModel(baseUrl, model)) {
69
- if (level === "off") {
69
+ if (level === "off" || level === "minimal") {
70
70
  payload.thinking = { type: "disabled" };
71
71
  } else {
72
72
  payload.thinking = { type: "enabled" };
@@ -5,6 +5,26 @@ import {
5
5
  querySelectorAll,
6
6
  sleep
7
7
  } from "../browser/index.js";
8
+ import {
9
+ compactViewportHealthResult,
10
+ ensureHealthyViewport
11
+ } from "./viewport.js";
12
+
13
+ export {
14
+ buildViewportHealthDiagnostics,
15
+ compactViewportHealthResult,
16
+ compactViewportState,
17
+ createViewportRunGuard,
18
+ ensureHealthyViewport,
19
+ getCurrentWindowInfo,
20
+ isListViewportCollapsed,
21
+ readViewportState,
22
+ setWindowStateIfPossible,
23
+ toggleWindowStateForViewportRecovery,
24
+ VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
25
+ VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO,
26
+ VIEWPORT_COLLAPSE_RATIO_THRESHOLD
27
+ } from "./viewport.js";
8
28
 
9
29
  export const PROBE_STATUS = Object.freeze({
10
30
  PASS: "pass",
@@ -245,6 +265,26 @@ export function createNetworkProbe({
245
265
  };
246
266
  }
247
267
 
268
+ export function createViewportCollapseProbe({
269
+ id = "viewport_collapse",
270
+ root = "frame",
271
+ frameOwnerRoot = "frameOwner",
272
+ required = true,
273
+ repair = true,
274
+ description = ""
275
+ } = {}) {
276
+ if (!id) throw new Error("Viewport collapse probe requires an id");
277
+ return {
278
+ type: "viewport",
279
+ id,
280
+ root,
281
+ frameOwnerRoot,
282
+ required: Boolean(required),
283
+ repair: Boolean(repair),
284
+ description
285
+ };
286
+ }
287
+
248
288
  export async function runSelectorProbe(client, roots, probe) {
249
289
  const nodeId = rootNodeId(roots, probe.root);
250
290
  if (!nodeId) {
@@ -338,6 +378,52 @@ export function runNetworkProbe(networkEvents = [], probe) {
338
378
  };
339
379
  }
340
380
 
381
+ export async function runViewportCollapseProbe(client, roots, probe) {
382
+ const nodeId = rootNodeId(roots, probe.root);
383
+ if (!nodeId) {
384
+ return {
385
+ ...probe,
386
+ ok: !probe.required,
387
+ status: probe.required ? PROBE_STATUS.BLOCKED : PROBE_STATUS.OPTIONAL_ABSENT,
388
+ count: 0,
389
+ collapsed: false,
390
+ recovered: false,
391
+ error: `Root not found: ${probe.root}`
392
+ };
393
+ }
394
+
395
+ try {
396
+ const health = await ensureHealthyViewport(client, {
397
+ roots,
398
+ root: probe.root,
399
+ frameOwnerRoot: probe.frameOwnerRoot,
400
+ reason: probe.id,
401
+ repair: probe.repair
402
+ });
403
+ const ok = Boolean(health.ok);
404
+ return {
405
+ ...probe,
406
+ ok: probe.required ? ok : true,
407
+ status: ok ? PROBE_STATUS.PASS : probe.required ? PROBE_STATUS.FAIL : PROBE_STATUS.OPTIONAL_ABSENT,
408
+ count: ok ? 1 : 0,
409
+ collapsed: Boolean(health.collapsed),
410
+ recovered: Boolean(health.recovered),
411
+ viewport_health: compactViewportHealthResult(health),
412
+ error: health.error || null
413
+ };
414
+ } catch (error) {
415
+ return {
416
+ ...probe,
417
+ ok: !probe.required,
418
+ status: probe.required ? PROBE_STATUS.ERROR : PROBE_STATUS.OPTIONAL_ABSENT,
419
+ count: 0,
420
+ collapsed: false,
421
+ recovered: false,
422
+ error: error?.message || String(error)
423
+ };
424
+ }
425
+ }
426
+
341
427
  export function summarizeProbeResults(probes = []) {
342
428
  const required = probes.filter((probe) => probe.required);
343
429
  const blocked = required.filter((probe) => probe.status === PROBE_STATUS.BLOCKED);
@@ -370,6 +456,7 @@ export function buildDriftReport(probes = []) {
370
456
  expected_min_count: probe.minCount,
371
457
  observed_count: probe.count || 0,
372
458
  selectors: probe.selectors || [],
459
+ viewport_health: probe.viewport_health || undefined,
373
460
  error: probe.error || null
374
461
  }));
375
462
  }
@@ -380,6 +467,7 @@ export async function runSelfHealCheck({
380
467
  roots = {},
381
468
  selectorProbes = [],
382
469
  accessibilityProbes = [],
470
+ viewportProbes = [],
383
471
  networkProbes = [],
384
472
  networkEvents = []
385
473
  } = {}) {
@@ -393,8 +481,13 @@ export async function runSelfHealCheck({
393
481
  accessibilityResults.push(await runAccessibilityProbe(client, probe));
394
482
  }
395
483
 
484
+ const viewportResults = [];
485
+ for (const probe of viewportProbes) {
486
+ viewportResults.push(await runViewportCollapseProbe(client, roots, probe));
487
+ }
488
+
396
489
  const networkResults = networkProbes.map((probe) => runNetworkProbe(networkEvents, probe));
397
- const probes = [...selectorResults, ...accessibilityResults, ...networkResults];
490
+ const probes = [...selectorResults, ...accessibilityResults, ...viewportResults, ...networkResults];
398
491
  const summary = summarizeProbeResults(probes);
399
492
 
400
493
  return {
@@ -507,6 +600,16 @@ export function buildRecommendSelfHealConfig(rules = {}) {
507
600
  description: "Candidate detail popup may mount inside the recommend frame"
508
601
  })
509
602
  ],
603
+ viewportProbes: [
604
+ createViewportCollapseProbe({
605
+ id: "recommend_viewport_collapse",
606
+ root: "frame",
607
+ frameOwnerRoot: "frameOwner",
608
+ required: true,
609
+ repair: true,
610
+ description: "Recommend frame/list viewport has not collapsed relative to the Chrome window"
611
+ })
612
+ ],
510
613
  accessibilityProbes: [
511
614
  createAccessibilityProbe({
512
615
  id: "accessibility_tree",
@@ -610,6 +713,16 @@ export function buildRecruitSelfHealConfig(rules = {}) {
610
713
  description: "Candidate detail popup may mount inside the search frame"
611
714
  })
612
715
  ],
716
+ viewportProbes: [
717
+ createViewportCollapseProbe({
718
+ id: "recruit_viewport_collapse",
719
+ root: "frame",
720
+ frameOwnerRoot: "frameOwner",
721
+ required: true,
722
+ repair: true,
723
+ description: "Search frame/list viewport has not collapsed relative to the Chrome window"
724
+ })
725
+ ],
613
726
  accessibilityProbes: [
614
727
  createAccessibilityProbe({
615
728
  id: "accessibility_tree",
@@ -711,6 +824,16 @@ export function buildChatSelfHealConfig(rules = {}) {
711
824
  description: "Resume iframe appears after the online resume is opened"
712
825
  })
713
826
  ],
827
+ viewportProbes: [
828
+ createViewportCollapseProbe({
829
+ id: "chat_viewport_collapse",
830
+ root: "top",
831
+ frameOwnerRoot: "top",
832
+ required: true,
833
+ repair: true,
834
+ description: "Chat list viewport has not collapsed relative to the Chrome window"
835
+ })
836
+ ],
714
837
  accessibilityProbes: [
715
838
  createAccessibilityProbe({
716
839
  id: "accessibility_tree",
@@ -756,7 +879,8 @@ export async function resolveRecommendSelfHealRoots(client, config = buildRecomm
756
879
  return {
757
880
  roots: {
758
881
  top: topRoot.nodeId,
759
- frame: iframe.documentNodeId
882
+ frame: iframe.documentNodeId,
883
+ frameOwner: iframe.nodeId
760
884
  },
761
885
  topRoot,
762
886
  iframe
@@ -779,7 +903,8 @@ export async function resolveRecruitSelfHealRoots(client, config = buildRecruitS
779
903
  return {
780
904
  roots: {
781
905
  top: topRoot.nodeId,
782
- frame: iframe.documentNodeId
906
+ frame: iframe.documentNodeId,
907
+ frameOwner: iframe.nodeId
783
908
  },
784
909
  topRoot,
785
910
  iframe