@reconcrap/boss-recommend-mcp 1.0.7 → 1.0.9
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
|
@@ -27,6 +27,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
27
27
|
- 当用户请求中出现 “recommend / 推荐页 / boss recommend / recommend page” 语义时,只能走 `boss-recommend-pipeline`
|
|
28
28
|
- 当用户请求中出现 “search / 搜索页 / boss search / search page” 语义时,不能走本技能,必须转交 `boss-recruit-pipeline` 并调用 `run_recruit_pipeline`
|
|
29
29
|
- 不要调用 `boss-recruit-pipeline`,也不要调用 `run_recruit_pipeline`,除非上一条(search/搜索页语义)命中
|
|
30
|
+
- 当 recommend 流程返回任何错误(包括 `JOB_TRIGGER_NOT_FOUND` / `NO_RECOMMEND_IFRAME` / `BOSS_LOGIN_REQUIRED`)时,禁止把 recommend 请求降级到 recruit 流程;必须先修复 recommend 的页面就绪或登录态问题
|
|
30
31
|
|
|
31
32
|
路由示例(中英文都要兼容):
|
|
32
33
|
|
package/src/index.js
CHANGED
|
@@ -33,7 +33,7 @@ function createJsonRpcError(id, code, message) {
|
|
|
33
33
|
function createToolSchema() {
|
|
34
34
|
return {
|
|
35
35
|
name: TOOL_NAME,
|
|
36
|
-
description: "Boss 推荐页流水线:解析推荐筛选指令、确认岗位/筛选条件/post_action(greet 时确认 max_greet_count),先点击确认岗位,再执行 recommend filter 与 recommend screen
|
|
36
|
+
description: "Boss 推荐页流水线:解析推荐筛选指令、确认岗位/筛选条件/post_action(greet 时确认 max_greet_count),先点击确认岗位,再执行 recommend filter 与 recommend screen 并返回摘要;也支持通过 instruction 直接发送“暂停 / 停止 / 继续”控制流程状态。",
|
|
37
37
|
inputSchema: {
|
|
38
38
|
type: "object",
|
|
39
39
|
properties: {
|
package/src/pipeline.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
1
3
|
import path from "node:path";
|
|
2
4
|
import { parseRecommendInstruction } from "./parser.js";
|
|
3
5
|
import {
|
|
@@ -379,6 +381,122 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
|
|
|
379
381
|
};
|
|
380
382
|
}
|
|
381
383
|
|
|
384
|
+
const PIPELINE_CONTROL_STATE_RUNNING = "running";
|
|
385
|
+
const PIPELINE_CONTROL_STATE_PAUSED = "paused";
|
|
386
|
+
const PIPELINE_CONTROL_STATE_STOPPED = "stopped";
|
|
387
|
+
const PIPELINE_CONTROL_ACTION_PAUSE = "pause";
|
|
388
|
+
const PIPELINE_CONTROL_ACTION_STOP = "stop";
|
|
389
|
+
const PIPELINE_CONTROL_ACTION_CONTINUE = "continue";
|
|
390
|
+
|
|
391
|
+
function getStateHome() {
|
|
392
|
+
return process.env.BOSS_RECOMMEND_HOME
|
|
393
|
+
? path.resolve(process.env.BOSS_RECOMMEND_HOME)
|
|
394
|
+
: path.join(os.homedir(), ".boss-recommend-mcp");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getPipelineControlStatePath() {
|
|
398
|
+
return path.join(getStateHome(), "pipeline-control-state.json");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizePipelineControlState(value) {
|
|
402
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
403
|
+
if (normalized === PIPELINE_CONTROL_STATE_PAUSED) return PIPELINE_CONTROL_STATE_PAUSED;
|
|
404
|
+
if (normalized === PIPELINE_CONTROL_STATE_STOPPED) return PIPELINE_CONTROL_STATE_STOPPED;
|
|
405
|
+
return PIPELINE_CONTROL_STATE_RUNNING;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function readPipelineControlFile() {
|
|
409
|
+
const filePath = getPipelineControlStatePath();
|
|
410
|
+
try {
|
|
411
|
+
if (!fs.existsSync(filePath)) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
415
|
+
return JSON.parse(raw);
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function defaultGetPipelineControlState() {
|
|
422
|
+
const payload = readPipelineControlFile();
|
|
423
|
+
const state = normalizePipelineControlState(payload?.state);
|
|
424
|
+
return {
|
|
425
|
+
state,
|
|
426
|
+
action: normalizeText(payload?.action || "") || null,
|
|
427
|
+
updated_at: normalizeText(payload?.updated_at || "") || null
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function defaultSetPipelineControlState(nextState, metadata = {}) {
|
|
432
|
+
const state = normalizePipelineControlState(nextState);
|
|
433
|
+
const payload = {
|
|
434
|
+
state,
|
|
435
|
+
action: normalizeText(metadata?.action || "") || null,
|
|
436
|
+
updated_at: new Date().toISOString()
|
|
437
|
+
};
|
|
438
|
+
const filePath = getPipelineControlStatePath();
|
|
439
|
+
try {
|
|
440
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
441
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
442
|
+
} catch {
|
|
443
|
+
// Keep in-memory response even if persistence fails.
|
|
444
|
+
}
|
|
445
|
+
return payload;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function formatPipelineControlState(statePayload) {
|
|
449
|
+
const normalized = statePayload && typeof statePayload === "object"
|
|
450
|
+
? statePayload
|
|
451
|
+
: {};
|
|
452
|
+
return {
|
|
453
|
+
state: normalizePipelineControlState(normalized.state),
|
|
454
|
+
action: normalizeText(normalized.action || "") || null,
|
|
455
|
+
updated_at: normalizeText(normalized.updated_at || "") || null
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function normalizeControlInstructionText(instruction) {
|
|
460
|
+
return normalizeText(instruction).replace(/[。!!??;;,,]+$/g, "").trim().toLowerCase();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function detectPipelineControlAction(instruction) {
|
|
464
|
+
const text = normalizeControlInstructionText(instruction);
|
|
465
|
+
if (!text) return null;
|
|
466
|
+
|
|
467
|
+
if (/^(?:请|麻烦|帮我)?\s*(?:先)?\s*(?:暂停|暂停一下|先暂停|pause)\s*$/.test(text)) {
|
|
468
|
+
return PIPELINE_CONTROL_ACTION_PAUSE;
|
|
469
|
+
}
|
|
470
|
+
if (/^(?:请|麻烦|帮我)?\s*(?:先)?\s*(?:停止|终止|结束|stop)\s*$/.test(text)) {
|
|
471
|
+
return PIPELINE_CONTROL_ACTION_STOP;
|
|
472
|
+
}
|
|
473
|
+
if (/^(?:请|麻烦|帮我)?\s*(?:继续|恢复|resume|continue)(?:执行)?\s*$/.test(text)) {
|
|
474
|
+
return PIPELINE_CONTROL_ACTION_CONTINUE;
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function buildPipelinePausedResponse(controlState) {
|
|
480
|
+
return {
|
|
481
|
+
status: "PAUSED",
|
|
482
|
+
error: {
|
|
483
|
+
code: "PIPELINE_PAUSED",
|
|
484
|
+
message: "Recommend 流水线当前处于暂停状态。请先发送“继续”恢复,或发送“停止”结束暂停。",
|
|
485
|
+
retryable: true
|
|
486
|
+
},
|
|
487
|
+
required_user_action: "continue_or_stop_pipeline",
|
|
488
|
+
control_state: formatPipelineControlState(controlState)
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function buildPipelineControlCommandResponse(status, message, controlState) {
|
|
493
|
+
return {
|
|
494
|
+
status,
|
|
495
|
+
message,
|
|
496
|
+
control_state: formatPipelineControlState(controlState)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
382
500
|
const defaultDependencies = {
|
|
383
501
|
attemptPipelineAutoRepair,
|
|
384
502
|
parseRecommendInstruction,
|
|
@@ -386,7 +504,9 @@ const defaultDependencies = {
|
|
|
386
504
|
listRecommendJobs,
|
|
387
505
|
runPipelinePreflight,
|
|
388
506
|
runRecommendSearchCli,
|
|
389
|
-
runRecommendScreenCli
|
|
507
|
+
runRecommendScreenCli,
|
|
508
|
+
getPipelineControlState: defaultGetPipelineControlState,
|
|
509
|
+
setPipelineControlState: defaultSetPipelineControlState
|
|
390
510
|
};
|
|
391
511
|
|
|
392
512
|
export async function runRecommendPipeline(
|
|
@@ -402,10 +522,82 @@ export async function runRecommendPipeline(
|
|
|
402
522
|
listRecommendJobs: listJobs,
|
|
403
523
|
runPipelinePreflight: runPreflight,
|
|
404
524
|
runRecommendSearchCli: searchCli,
|
|
405
|
-
runRecommendScreenCli: screenCli
|
|
525
|
+
runRecommendScreenCli: screenCli,
|
|
526
|
+
getPipelineControlState: getControlState,
|
|
527
|
+
setPipelineControlState: setControlState
|
|
406
528
|
} = resolvedDependencies;
|
|
407
|
-
|
|
408
|
-
const
|
|
529
|
+
|
|
530
|
+
const controlAction = detectPipelineControlAction(instruction);
|
|
531
|
+
const currentControlState = formatPipelineControlState(
|
|
532
|
+
typeof getControlState === "function"
|
|
533
|
+
? getControlState(workspaceRoot)
|
|
534
|
+
: defaultGetPipelineControlState()
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (controlAction === PIPELINE_CONTROL_ACTION_PAUSE) {
|
|
538
|
+
const pausedState = formatPipelineControlState(
|
|
539
|
+
typeof setControlState === "function"
|
|
540
|
+
? setControlState(PIPELINE_CONTROL_STATE_PAUSED, { action: PIPELINE_CONTROL_ACTION_PAUSE })
|
|
541
|
+
: defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_PAUSED, { action: PIPELINE_CONTROL_ACTION_PAUSE })
|
|
542
|
+
);
|
|
543
|
+
return buildPipelineControlCommandResponse(
|
|
544
|
+
"PAUSED",
|
|
545
|
+
"已暂停 Recommend 流水线。后续如需恢复请发送“继续”,如需终止暂停请发送“停止”。",
|
|
546
|
+
pausedState
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (controlAction === PIPELINE_CONTROL_ACTION_STOP) {
|
|
551
|
+
const stoppedState = formatPipelineControlState(
|
|
552
|
+
typeof setControlState === "function"
|
|
553
|
+
? setControlState(PIPELINE_CONTROL_STATE_STOPPED, { action: PIPELINE_CONTROL_ACTION_STOP })
|
|
554
|
+
: defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_STOPPED, { action: PIPELINE_CONTROL_ACTION_STOP })
|
|
555
|
+
);
|
|
556
|
+
return buildPipelineControlCommandResponse(
|
|
557
|
+
"STOPPED",
|
|
558
|
+
"已停止 Recommend 流水线。后续可直接发送新的筛选指令重新开始。",
|
|
559
|
+
stoppedState
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (controlAction === PIPELINE_CONTROL_ACTION_CONTINUE) {
|
|
564
|
+
if (currentControlState.state !== PIPELINE_CONTROL_STATE_PAUSED) {
|
|
565
|
+
return buildFailedResponse(
|
|
566
|
+
"PIPELINE_CONTINUE_REQUIRES_PAUSED",
|
|
567
|
+
"当前流程未处于暂停状态,无法执行“继续”。请先发送“暂停”,或直接发送新的筛选指令。",
|
|
568
|
+
{
|
|
569
|
+
required_user_action: "pause_before_continue",
|
|
570
|
+
control_state: currentControlState
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const resumedState = formatPipelineControlState(
|
|
575
|
+
typeof setControlState === "function"
|
|
576
|
+
? setControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: PIPELINE_CONTROL_ACTION_CONTINUE })
|
|
577
|
+
: defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: PIPELINE_CONTROL_ACTION_CONTINUE })
|
|
578
|
+
);
|
|
579
|
+
return buildPipelineControlCommandResponse(
|
|
580
|
+
"RESUMED",
|
|
581
|
+
"已恢复 Recommend 流水线。请继续发送筛选指令,我会按确认流程继续执行。",
|
|
582
|
+
resumedState
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (currentControlState.state === PIPELINE_CONTROL_STATE_PAUSED) {
|
|
587
|
+
return buildPipelinePausedResponse(currentControlState);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (currentControlState.state !== PIPELINE_CONTROL_STATE_RUNNING) {
|
|
591
|
+
const nextActiveState = formatPipelineControlState(
|
|
592
|
+
typeof setControlState === "function"
|
|
593
|
+
? setControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: "run" })
|
|
594
|
+
: defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: "run" })
|
|
595
|
+
);
|
|
596
|
+
Object.assign(currentControlState, nextActiveState);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const startedAt = Date.now();
|
|
600
|
+
const parsed = parseInstruction({ instruction, confirmation, overrides });
|
|
409
601
|
|
|
410
602
|
if (parsed.missing_fields.length > 0) {
|
|
411
603
|
return buildNeedInputResponse(parsed);
|
|
@@ -534,6 +726,54 @@ export async function runRecommendPipeline(
|
|
|
534
726
|
port: preflight.debug_port
|
|
535
727
|
});
|
|
536
728
|
if (!jobListResult.ok) {
|
|
729
|
+
const jobListErrorCode = String(jobListResult.error?.code || "");
|
|
730
|
+
const jobListErrorMessage = String(jobListResult.error?.message || "");
|
|
731
|
+
const pageReadinessFailure = (
|
|
732
|
+
jobListErrorCode === "JOB_TRIGGER_NOT_FOUND"
|
|
733
|
+
|| jobListErrorCode === "NO_RECOMMEND_IFRAME"
|
|
734
|
+
|| jobListErrorCode === "LOGIN_REQUIRED"
|
|
735
|
+
|| jobListErrorMessage.includes("JOB_TRIGGER_NOT_FOUND")
|
|
736
|
+
|| jobListErrorMessage.includes("NO_RECOMMEND_IFRAME")
|
|
737
|
+
|| jobListErrorMessage.includes("LOGIN_REQUIRED")
|
|
738
|
+
);
|
|
739
|
+
if (pageReadinessFailure) {
|
|
740
|
+
const recheck = await ensureRecommendPageReady(workspaceRoot, {
|
|
741
|
+
port: preflight.debug_port
|
|
742
|
+
});
|
|
743
|
+
const loginRelated = new Set(["LOGIN_REQUIRED", "LOGIN_REQUIRED_AFTER_REDIRECT"]);
|
|
744
|
+
const connectivityRelated = new Set(["DEBUG_PORT_UNREACHABLE"]);
|
|
745
|
+
const guidance = buildChromeSetupGuidance({
|
|
746
|
+
debugPort: preflight.debug_port,
|
|
747
|
+
pageState: recheck.page_state
|
|
748
|
+
});
|
|
749
|
+
if (!recheck.ok || loginRelated.has(recheck.state) || connectivityRelated.has(recheck.state)) {
|
|
750
|
+
return buildFailedResponse(
|
|
751
|
+
connectivityRelated.has(recheck.state)
|
|
752
|
+
? "BOSS_CHROME_NOT_CONNECTED"
|
|
753
|
+
: loginRelated.has(recheck.state)
|
|
754
|
+
? "BOSS_LOGIN_REQUIRED"
|
|
755
|
+
: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
756
|
+
loginRelated.has(recheck.state)
|
|
757
|
+
? `检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket`
|
|
758
|
+
: connectivityRelated.has(recheck.state)
|
|
759
|
+
? `读取岗位列表前需要先连接到端口 ${preflight.debug_port} 的 Chrome 远程调试实例。`
|
|
760
|
+
: `读取岗位列表前,请先在端口 ${preflight.debug_port} 的 Chrome 停留在 Boss recommend 页面。`,
|
|
761
|
+
{
|
|
762
|
+
search_params: parsed.searchParams,
|
|
763
|
+
screen_params: parsed.screenParams,
|
|
764
|
+
required_user_action: "prepare_boss_recommend_page",
|
|
765
|
+
guidance,
|
|
766
|
+
diagnostics: {
|
|
767
|
+
debug_port: preflight.debug_port,
|
|
768
|
+
page_state: recheck.page_state,
|
|
769
|
+
stdout: jobListResult.stdout?.slice(-1000),
|
|
770
|
+
stderr: jobListResult.stderr?.slice(-1000),
|
|
771
|
+
result: jobListResult.structured || null
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
537
777
|
return buildFailedResponse(
|
|
538
778
|
jobListResult.error?.code || "RECOMMEND_JOB_LIST_FAILED",
|
|
539
779
|
jobListResult.error?.message || "读取推荐岗位列表失败,无法开始筛选。",
|
package/src/test-pipeline.js
CHANGED
|
@@ -78,6 +78,152 @@ function createParsed(overrides = {}) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function createControlStateStore(initialState = "running") {
|
|
82
|
+
let state = {
|
|
83
|
+
state: initialState,
|
|
84
|
+
action: null,
|
|
85
|
+
updated_at: null
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
getPipelineControlState: () => ({ ...state }),
|
|
89
|
+
setPipelineControlState: (nextState, metadata = {}) => {
|
|
90
|
+
state = {
|
|
91
|
+
state: nextState,
|
|
92
|
+
action: metadata.action || null,
|
|
93
|
+
updated_at: "2026-01-01T00:00:00.000Z"
|
|
94
|
+
};
|
|
95
|
+
return { ...state };
|
|
96
|
+
},
|
|
97
|
+
snapshot: () => ({ ...state })
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function testPauseCommandShouldPausePipeline() {
|
|
102
|
+
const controlStore = createControlStateStore("running");
|
|
103
|
+
let parseCalled = false;
|
|
104
|
+
const result = await runRecommendPipeline(
|
|
105
|
+
{
|
|
106
|
+
workspaceRoot: process.cwd(),
|
|
107
|
+
instruction: "暂停",
|
|
108
|
+
confirmation: {},
|
|
109
|
+
overrides: {}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
getPipelineControlState: controlStore.getPipelineControlState,
|
|
113
|
+
setPipelineControlState: controlStore.setPipelineControlState,
|
|
114
|
+
parseRecommendInstruction: () => {
|
|
115
|
+
parseCalled = true;
|
|
116
|
+
return createParsed();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
assert.equal(result.status, "PAUSED");
|
|
122
|
+
assert.equal(result.control_state.state, "paused");
|
|
123
|
+
assert.equal(parseCalled, false);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function testContinueCommandRequiresPausedState() {
|
|
127
|
+
const controlStore = createControlStateStore("running");
|
|
128
|
+
const result = await runRecommendPipeline(
|
|
129
|
+
{
|
|
130
|
+
workspaceRoot: process.cwd(),
|
|
131
|
+
instruction: "继续",
|
|
132
|
+
confirmation: {},
|
|
133
|
+
overrides: {}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
getPipelineControlState: controlStore.getPipelineControlState,
|
|
137
|
+
setPipelineControlState: controlStore.setPipelineControlState
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
assert.equal(result.status, "FAILED");
|
|
142
|
+
assert.equal(result.error.code, "PIPELINE_CONTINUE_REQUIRES_PAUSED");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function testContinueCommandCanResumeFromPausedState() {
|
|
146
|
+
const controlStore = createControlStateStore("running");
|
|
147
|
+
await runRecommendPipeline(
|
|
148
|
+
{
|
|
149
|
+
workspaceRoot: process.cwd(),
|
|
150
|
+
instruction: "暂停",
|
|
151
|
+
confirmation: {},
|
|
152
|
+
overrides: {}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
getPipelineControlState: controlStore.getPipelineControlState,
|
|
156
|
+
setPipelineControlState: controlStore.setPipelineControlState
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const resumed = await runRecommendPipeline(
|
|
161
|
+
{
|
|
162
|
+
workspaceRoot: process.cwd(),
|
|
163
|
+
instruction: "继续",
|
|
164
|
+
confirmation: {},
|
|
165
|
+
overrides: {}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
getPipelineControlState: controlStore.getPipelineControlState,
|
|
169
|
+
setPipelineControlState: controlStore.setPipelineControlState
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
assert.equal(resumed.status, "RESUMED");
|
|
174
|
+
assert.equal(resumed.control_state.state, "running");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function testPipelineShouldBlockWhenPaused() {
|
|
178
|
+
const controlStore = createControlStateStore("paused");
|
|
179
|
+
let parseCalled = false;
|
|
180
|
+
let preflightCalled = false;
|
|
181
|
+
const result = await runRecommendPipeline(
|
|
182
|
+
{
|
|
183
|
+
workspaceRoot: process.cwd(),
|
|
184
|
+
instruction: "推荐页筛选985男生",
|
|
185
|
+
confirmation: {},
|
|
186
|
+
overrides: {}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
getPipelineControlState: controlStore.getPipelineControlState,
|
|
190
|
+
setPipelineControlState: controlStore.setPipelineControlState,
|
|
191
|
+
parseRecommendInstruction: () => {
|
|
192
|
+
parseCalled = true;
|
|
193
|
+
return createParsed();
|
|
194
|
+
},
|
|
195
|
+
runPipelinePreflight: () => {
|
|
196
|
+
preflightCalled = true;
|
|
197
|
+
return { ok: true, checks: [], debug_port: 9222 };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
assert.equal(result.status, "PAUSED");
|
|
203
|
+
assert.equal(result.error.code, "PIPELINE_PAUSED");
|
|
204
|
+
assert.equal(parseCalled, false);
|
|
205
|
+
assert.equal(preflightCalled, false);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function testStopCommandShouldExitPausedState() {
|
|
209
|
+
const controlStore = createControlStateStore("paused");
|
|
210
|
+
const result = await runRecommendPipeline(
|
|
211
|
+
{
|
|
212
|
+
workspaceRoot: process.cwd(),
|
|
213
|
+
instruction: "停止",
|
|
214
|
+
confirmation: {},
|
|
215
|
+
overrides: {}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
getPipelineControlState: controlStore.getPipelineControlState,
|
|
219
|
+
setPipelineControlState: controlStore.setPipelineControlState
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
assert.equal(result.status, "STOPPED");
|
|
224
|
+
assert.equal(result.control_state.state, "stopped");
|
|
225
|
+
}
|
|
226
|
+
|
|
81
227
|
async function testNeedConfirmationGate() {
|
|
82
228
|
let preflightCalled = false;
|
|
83
229
|
const result = await runRecommendPipeline(
|
|
@@ -344,6 +490,50 @@ async function testSearchNoIframeWithLoginShouldReturnLoginRequired() {
|
|
|
344
490
|
assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
|
|
345
491
|
}
|
|
346
492
|
|
|
493
|
+
async function testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin() {
|
|
494
|
+
const result = await runRecommendPipeline(
|
|
495
|
+
{
|
|
496
|
+
workspaceRoot: process.cwd(),
|
|
497
|
+
instruction: "test",
|
|
498
|
+
confirmation: {},
|
|
499
|
+
overrides: {}
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
parseRecommendInstruction: () => createParsed(),
|
|
503
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
504
|
+
ensureBossRecommendPageReady: async () => ({
|
|
505
|
+
ok: false,
|
|
506
|
+
debug_port: 9222,
|
|
507
|
+
state: "LOGIN_REQUIRED",
|
|
508
|
+
page_state: {
|
|
509
|
+
state: "LOGIN_REQUIRED",
|
|
510
|
+
expected_url: "https://www.zhipin.com/web/chat/recommend",
|
|
511
|
+
current_url: "https://www.zhipin.com/web/user/?ka=bticket",
|
|
512
|
+
login_url: "https://www.zhipin.com/web/user/?ka=bticket"
|
|
513
|
+
}
|
|
514
|
+
}),
|
|
515
|
+
listRecommendJobs: async () => ({
|
|
516
|
+
ok: false,
|
|
517
|
+
stdout: "",
|
|
518
|
+
stderr: "",
|
|
519
|
+
structured: null,
|
|
520
|
+
jobs: [],
|
|
521
|
+
error: {
|
|
522
|
+
code: "JOB_TRIGGER_NOT_FOUND",
|
|
523
|
+
message: "JOB_TRIGGER_NOT_FOUND"
|
|
524
|
+
}
|
|
525
|
+
}),
|
|
526
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
527
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
assert.equal(result.status, "FAILED");
|
|
532
|
+
assert.equal(result.error.code, "BOSS_LOGIN_REQUIRED");
|
|
533
|
+
assert.equal(result.required_user_action, "prepare_boss_recommend_page");
|
|
534
|
+
assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
|
|
535
|
+
}
|
|
536
|
+
|
|
347
537
|
async function testNeedJobConfirmationGate() {
|
|
348
538
|
const result = await runRecommendPipeline(
|
|
349
539
|
{
|
|
@@ -711,6 +901,11 @@ async function testScreenConfigRecoveryStepShouldBeFirst() {
|
|
|
711
901
|
}
|
|
712
902
|
|
|
713
903
|
async function main() {
|
|
904
|
+
await testPauseCommandShouldPausePipeline();
|
|
905
|
+
await testContinueCommandRequiresPausedState();
|
|
906
|
+
await testContinueCommandCanResumeFromPausedState();
|
|
907
|
+
await testPipelineShouldBlockWhenPaused();
|
|
908
|
+
await testStopCommandShouldExitPausedState();
|
|
714
909
|
await testNeedConfirmationGate();
|
|
715
910
|
await testNeedSchoolTagConfirmationGate();
|
|
716
911
|
await testNeedTargetCountConfirmationGate();
|
|
@@ -721,6 +916,7 @@ async function main() {
|
|
|
721
916
|
await testCompletedPipeline();
|
|
722
917
|
await testSearchFailure();
|
|
723
918
|
await testSearchNoIframeWithLoginShouldReturnLoginRequired();
|
|
919
|
+
await testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin();
|
|
724
920
|
await testLoginRequiredShouldReturnGuidance();
|
|
725
921
|
await testDebugPortUnreachableShouldReturnConnectionCode();
|
|
726
922
|
await testPreflightRecoveryPlanOrder();
|
|
@@ -10,10 +10,10 @@ const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
|
10
10
|
const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
11
11
|
const BOSS_LOGIN_TITLE_PATTERN = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
12
12
|
const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
|
|
13
|
-
const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
14
|
-
const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
15
|
-
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
16
|
-
const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
|
|
13
|
+
const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
14
|
+
const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
|
|
15
|
+
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
16
|
+
const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
|
|
17
17
|
|
|
18
18
|
function normalizeText(value) {
|
|
19
19
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
@@ -107,7 +107,7 @@ function expandDegreeAtOrAbove(value) {
|
|
|
107
107
|
return DEGREE_ORDER.slice(index);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function parseDegreeSelection(raw) {
|
|
110
|
+
function parseDegreeSelection(raw) {
|
|
111
111
|
const text = normalizeText(raw);
|
|
112
112
|
if (!text) return null;
|
|
113
113
|
if (text === "不限") return ["不限"];
|
|
@@ -132,9 +132,9 @@ function parseDegreeSelection(raw) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const normalized = sortDegreeSelection(selected);
|
|
136
|
-
return normalized.length ? normalized : null;
|
|
137
|
-
}
|
|
135
|
+
const normalized = sortDegreeSelection(selected);
|
|
136
|
+
return normalized.length ? normalized : null;
|
|
137
|
+
}
|
|
138
138
|
|
|
139
139
|
function parseArgs(argv) {
|
|
140
140
|
const args = {
|
|
@@ -296,14 +296,14 @@ function generateBezierPath(start, end, steps = 18) {
|
|
|
296
296
|
return path;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
class RecommendSearchCli {
|
|
300
|
-
constructor(args) {
|
|
301
|
-
this.args = args;
|
|
302
|
-
this.client = null;
|
|
303
|
-
this.Runtime = null;
|
|
304
|
-
this.Input = null;
|
|
305
|
-
this.target = null;
|
|
306
|
-
}
|
|
299
|
+
class RecommendSearchCli {
|
|
300
|
+
constructor(args) {
|
|
301
|
+
this.args = args;
|
|
302
|
+
this.client = null;
|
|
303
|
+
this.Runtime = null;
|
|
304
|
+
this.Input = null;
|
|
305
|
+
this.target = null;
|
|
306
|
+
}
|
|
307
307
|
|
|
308
308
|
async connect() {
|
|
309
309
|
const targets = await CDP.List({ port: this.args.port });
|
|
@@ -792,7 +792,7 @@ class RecommendSearchCli {
|
|
|
792
792
|
})()`);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
-
async openFilterPanel() {
|
|
795
|
+
async openFilterPanel() {
|
|
796
796
|
if (await this.isFilterPanelVisible()) return;
|
|
797
797
|
let lastError = 'FILTER_PANEL_UNAVAILABLE';
|
|
798
798
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -821,8 +821,8 @@ class RecommendSearchCli {
|
|
|
821
821
|
lastError = fallback?.error || lastError;
|
|
822
822
|
}
|
|
823
823
|
}
|
|
824
|
-
throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
|
|
825
|
-
}
|
|
824
|
+
throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
|
|
825
|
+
}
|
|
826
826
|
|
|
827
827
|
async closeFilterPanel() {
|
|
828
828
|
if (!(await this.isFilterPanelVisible())) {
|
|
@@ -1480,10 +1480,10 @@ class RecommendSearchCli {
|
|
|
1480
1480
|
if (!Array.isArray(this.args.schoolTag) || this.args.schoolTag.length === 0) {
|
|
1481
1481
|
throw new Error("INVALID_SCHOOL_TAG_INPUT");
|
|
1482
1482
|
}
|
|
1483
|
-
if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
|
|
1484
|
-
throw new Error("INVALID_DEGREE_INPUT");
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1483
|
+
if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
|
|
1484
|
+
throw new Error("INVALID_DEGREE_INPUT");
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
1487
|
await this.connect();
|
|
1488
1488
|
try {
|
|
1489
1489
|
const frameState = await this.getFrameState();
|