@reconcrap/boss-recommend-mcp 1.0.6 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -155,6 +155,7 @@ CLI fallback 的状态机与 MCP 保持一致:
155
155
  - 检查 Chrome DevTools 端口是否可连接
156
156
  - 检查 Boss 是否已登录
157
157
  - 检查当前页面是否已停留在 recommend 页面
158
+ - 若检测到当前 URL 为 `https://www.zhipin.com/web/user/?ka=bticket`(或同类登录页 URL),立即判定为“未登录”,只提示用户登录;不要继续做页面脚本修改
158
159
  - 若端口不可连接:先自动尝试启动 Chrome,并且必须使用 `--remote-debugging-port=<port>` + `--user-data-dir=<profile>`
159
160
  - 若检测到 Boss 已登录但不在 recommend 页面:先自动 navigate 到 `https://www.zhipin.com/web/chat/recommend`
160
161
  - 若检测到 Boss 未登录:提示用户先登录;用户登录后先 navigate 到 recommend 页面再继续
@@ -196,5 +197,6 @@ CLI fallback 的状态机与 MCP 保持一致:
196
197
  - 对 `school_tag/degree/gender/recent_not_view` 必须逐项提问并逐项确认,不可合并成一句“filters已确认”
197
198
  - 询问 `criteria` 时必须使用开放式文本输入,不要提供“严格执行/宽松执行”等枚举选项
198
199
  - 当页面就绪检查失败时,提示文案里必须包含 `debug_port` 和 recommend 页面 URL
200
+ - 若失败原因是未登录,提示文案必须明确给出登录 URL:`https://www.zhipin.com/web/user/?ka=bticket`
199
201
  - 不要跳过 `post_action` 的首轮确认
200
202
  - 不要把 recommend 流程说成 search 流程
package/src/pipeline.js CHANGED
@@ -629,9 +629,46 @@ export async function runRecommendPipeline(
629
629
  selectedJob: selectedJobToken
630
630
  });
631
631
  if (!searchResult.ok) {
632
+ const searchErrorCode = String(searchResult.error?.code || "");
633
+ const searchErrorMessage = String(searchResult.error?.message || "");
634
+ const loginRelatedSearchFailure = (
635
+ searchErrorCode === "LOGIN_REQUIRED"
636
+ || searchErrorCode === "NO_RECOMMEND_IFRAME"
637
+ || searchErrorMessage.includes("LOGIN_REQUIRED")
638
+ || searchErrorMessage.includes("NO_RECOMMEND_IFRAME")
639
+ );
640
+ if (loginRelatedSearchFailure) {
641
+ const recheck = await ensureRecommendPageReady(workspaceRoot, {
642
+ port: preflight.debug_port
643
+ });
644
+ if (recheck.state === "LOGIN_REQUIRED" || recheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
645
+ const guidance = buildChromeSetupGuidance({
646
+ debugPort: preflight.debug_port,
647
+ pageState: recheck.page_state
648
+ });
649
+ return buildFailedResponse(
650
+ "BOSS_LOGIN_REQUIRED",
651
+ "检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket",
652
+ {
653
+ search_params: parsed.searchParams,
654
+ screen_params: parsed.screenParams,
655
+ selected_job: selectedJob,
656
+ required_user_action: "prepare_boss_recommend_page",
657
+ guidance,
658
+ diagnostics: {
659
+ debug_port: preflight.debug_port,
660
+ page_state: recheck.page_state,
661
+ stdout: searchResult.stdout?.slice(-1000),
662
+ stderr: searchResult.stderr?.slice(-1000),
663
+ result: searchResult.structured || null
664
+ }
665
+ }
666
+ );
667
+ }
668
+ }
632
669
  return buildFailedResponse(
633
- searchResult.error?.code || "RECOMMEND_SEARCH_FAILED",
634
- searchResult.error?.message || "推荐页筛选执行失败。",
670
+ searchResult.error?.code || "RECOMMEND_SEARCH_FAILED",
671
+ searchResult.error?.message || "推荐页筛选执行失败。",
635
672
  {
636
673
  search_params: parsed.searchParams,
637
674
  screen_params: parsed.screenParams,
@@ -301,6 +301,49 @@ async function testSearchFailure() {
301
301
  assert.equal(result.error.code, "RECOMMEND_FILTER_PANEL_UNAVAILABLE");
302
302
  }
303
303
 
304
+ async function testSearchNoIframeWithLoginShouldReturnLoginRequired() {
305
+ const result = await runRecommendPipeline(
306
+ {
307
+ workspaceRoot: process.cwd(),
308
+ instruction: "test",
309
+ confirmation: createJobConfirmedConfirmation(),
310
+ overrides: {}
311
+ },
312
+ {
313
+ parseRecommendInstruction: () => createParsed(),
314
+ runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
315
+ ensureBossRecommendPageReady: async () => ({
316
+ ok: false,
317
+ debug_port: 9222,
318
+ state: "LOGIN_REQUIRED",
319
+ page_state: {
320
+ state: "LOGIN_REQUIRED",
321
+ expected_url: "https://www.zhipin.com/web/chat/recommend",
322
+ current_url: "https://www.zhipin.com/web/user/?ka=bticket",
323
+ login_url: "https://www.zhipin.com/web/user/?ka=bticket"
324
+ }
325
+ }),
326
+ listRecommendJobs: async () => createJobListResult(),
327
+ runRecommendSearchCli: async () => ({
328
+ ok: false,
329
+ stdout: "",
330
+ stderr: "NO_RECOMMEND_IFRAME",
331
+ structured: null,
332
+ error: {
333
+ code: "NO_RECOMMEND_IFRAME",
334
+ message: "NO_RECOMMEND_IFRAME"
335
+ }
336
+ }),
337
+ runRecommendScreenCli: async () => ({ ok: true, summary: {} })
338
+ }
339
+ );
340
+
341
+ assert.equal(result.status, "FAILED");
342
+ assert.equal(result.error.code, "BOSS_LOGIN_REQUIRED");
343
+ assert.equal(result.required_user_action, "prepare_boss_recommend_page");
344
+ assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
345
+ }
346
+
304
347
  async function testNeedJobConfirmationGate() {
305
348
  const result = await runRecommendPipeline(
306
349
  {
@@ -677,6 +720,7 @@ async function main() {
677
720
  await testNeedFinalReviewConfirmationGate();
678
721
  await testCompletedPipeline();
679
722
  await testSearchFailure();
723
+ await testSearchNoIframeWithLoginShouldReturnLoginRequired();
680
724
  await testLoginRequiredShouldReturnGuidance();
681
725
  await testDebugPortUnreachableShouldReturnConnectionCode();
682
726
  await testPreflightRecoveryPlanOrder();
@@ -4,9 +4,12 @@ import readline from "node:readline";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import CDP from "chrome-remote-interface";
6
6
 
7
- const DEFAULT_PORT = 9222;
8
- const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
9
- const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
7
+ const DEFAULT_PORT = 9222;
8
+ const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
9
+ const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
+ const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
11
+ const BOSS_LOGIN_TITLE_PATTERN = /登录|signin|扫码登录|BOSS直聘登录/i;
12
+ const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
10
13
  const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
11
14
  const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
12
15
  const GENDER_OPTIONS = ["不限", "男", "女"];
@@ -383,21 +386,39 @@ class RecommendSearchCli {
383
386
  });
384
387
  }
385
388
 
386
- async getFrameState() {
387
- return this.evaluate(`(() => {
388
- const frame = document.querySelector('iframe[name="recommendFrame"]')
389
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
390
- || document.querySelector('iframe');
391
- if (!frame || !frame.contentDocument) {
392
- return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
393
- }
394
- return {
395
- ok: true,
396
- frameUrl: (() => {
397
- try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
398
- })()
399
- };
400
- })()`);
389
+ async getFrameState() {
390
+ return this.evaluate(`(() => {
391
+ const currentUrl = (() => {
392
+ try { return String(window.location.href || ''); } catch { return ''; }
393
+ })();
394
+ const title = (() => {
395
+ try { return String(document.title || ''); } catch { return ''; }
396
+ })();
397
+ const isLogin = ${BOSS_LOGIN_URL_PATTERN}.test(currentUrl)
398
+ || ${BOSS_LOGIN_TITLE_PATTERN}.test(title);
399
+ if (isLogin) {
400
+ return {
401
+ ok: false,
402
+ error: 'LOGIN_REQUIRED',
403
+ currentUrl: currentUrl || ${JSON.stringify(BOSS_LOGIN_URL)},
404
+ title
405
+ };
406
+ }
407
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
408
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
409
+ || document.querySelector('iframe');
410
+ if (!frame || !frame.contentDocument) {
411
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME', currentUrl, title };
412
+ }
413
+ return {
414
+ ok: true,
415
+ currentUrl,
416
+ title,
417
+ frameUrl: (() => {
418
+ try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
419
+ })()
420
+ };
421
+ })()`);
401
422
  }
402
423
 
403
424
  async getFilterEntryPoint() {
@@ -1467,6 +1488,9 @@ class RecommendSearchCli {
1467
1488
  try {
1468
1489
  const frameState = await this.getFrameState();
1469
1490
  if (!frameState?.ok) {
1491
+ if (frameState?.error === "LOGIN_REQUIRED") {
1492
+ throw new Error("LOGIN_REQUIRED");
1493
+ }
1470
1494
  throw new Error(frameState?.error || 'NO_RECOMMEND_IFRAME');
1471
1495
  }
1472
1496