@reconcrap/boss-recommend-mcp 1.0.13 → 1.0.15
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
|
@@ -9,7 +9,6 @@ Boss 推荐页自动化流水线 MCP(stdio)服务。
|
|
|
9
9
|
|
|
10
10
|
MCP 工具:
|
|
11
11
|
|
|
12
|
-
- `run_recommend_pipeline`(默认异步,但会先执行与同步一致的前置门禁;仅门禁通过后返回 run_id)
|
|
13
12
|
- `start_recommend_pipeline_run`(异步启动;同样先经过前置门禁,通过后返回 run_id)
|
|
14
13
|
- `get_recommend_pipeline_run`(轮询 run_id 状态)
|
|
15
14
|
- `cancel_recommend_pipeline_run`(取消运行中任务)
|
|
@@ -143,14 +142,14 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
|
|
|
143
142
|
|
|
144
143
|
当宿主 agent 对“长时间无回包”敏感(容易误判失败)时,建议改用异步工具:
|
|
145
144
|
|
|
146
|
-
1.
|
|
145
|
+
1. 调用 `start_recommend_pipeline_run`。
|
|
147
146
|
2. 若返回 `NEED_INPUT/NEED_CONFIRMATION/FAILED`,按同步流程先补齐前置条件(登录、页面就绪、岗位确认、最终确认)。
|
|
148
147
|
3. 仅当门禁通过时,接口才会返回 `ACCEPTED + run_id`;随后每 5~15 秒调用一次 `get_recommend_pipeline_run` 轮询。
|
|
149
148
|
4. 若需终止,调用 `cancel_recommend_pipeline_run`。
|
|
150
149
|
|
|
151
150
|
说明:
|
|
152
151
|
|
|
153
|
-
- `
|
|
152
|
+
- `start_recommend_pipeline_run` 为异步入口,但不会跳过同步确认流程。
|
|
154
153
|
- 定时心跳默认 120 秒一次;`updated_at` 仍会在阶段或进度变化时刷新。
|
|
155
154
|
- 每个 run 会持久化到 `~/.boss-recommend-mcp/runs/<run_id>.json`(可通过 `BOSS_RECOMMEND_HOME` 覆盖)。
|
|
156
155
|
- 轮询期间不要重复 `start`,优先复用已有 `run_id`,避免重复筛选。
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
9
9
|
|
|
10
|
-
当用户希望在 Boss 推荐页按条件筛选候选人时,优先调用 MCP 工具 `
|
|
10
|
+
当用户希望在 Boss 推荐页按条件筛选候选人时,优先调用 MCP 工具 `start_recommend_pipeline_run` 完成端到端任务:
|
|
11
11
|
|
|
12
12
|
1. 解析推荐页筛选指令
|
|
13
13
|
2. 第一阶段仅确认非岗位参数(filters / criteria / target_count / post_action / max_greet_count)
|
|
@@ -75,14 +75,22 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
75
75
|
`post_action` 的确认是**单次运行级别**的:
|
|
76
76
|
|
|
77
77
|
- 若用户确认 `favorite`,则本次运行中所有通过人选都统一收藏
|
|
78
|
-
- 若用户确认 `greet`,则本次运行中先按 `max_greet_count` 执行打招呼,超出上限后自动改为收藏
|
|
79
|
-
- 不要在每位候选人通过后再次逐个确认
|
|
80
|
-
|
|
78
|
+
- 若用户确认 `greet`,则本次运行中先按 `max_greet_count` 执行打招呼,超出上限后自动改为收藏
|
|
79
|
+
- 不要在每位候选人通过后再次逐个确认
|
|
80
|
+
|
|
81
|
+
## Instruction 原文锁定与执行前回显校验(必须遵守)
|
|
82
|
+
|
|
83
|
+
- 第一次收到用户自然语言需求时,必须把该条 `instruction` 原文锁定为 `locked_instruction_raw`。
|
|
84
|
+
- 后续所有调用(包括二轮确认、最终执行、重试)都必须复用同一条 `locked_instruction_raw`,禁止改写、扩写、摘要、同义替换、翻译。
|
|
85
|
+
- 未经用户明确要求,禁止 agent 自行生成新的 `instruction` 文案。
|
|
86
|
+
- 最终执行前(即准备提交 `job_confirmed=true` 与 `final_confirmed=true` 的那次调用),必须先向用户逐字回显本次将提交的 `instruction`,并明确提示“将按以下原文执行”。
|
|
87
|
+
- 回显校验规则:若当前待提交 `instruction` 与 `locked_instruction_raw` 不一致(按原样字符串比对),必须停止调用工具,先修正为原文后再执行。
|
|
88
|
+
- 仅当用户明确要求修改 `instruction` 时,才允许更新 `locked_instruction_raw`;更新后仍需再次逐字回显并确认。
|
|
89
|
+
|
|
81
90
|
## Tool Contract
|
|
82
91
|
|
|
83
|
-
- Tool name
|
|
92
|
+
- Tool name: `start_recommend_pipeline_run`
|
|
84
93
|
- Input:
|
|
85
|
-
- `execution_mode`(可选:`async|sync`,默认 `async`)
|
|
86
94
|
- `instruction` (required)
|
|
87
95
|
- `confirmation`
|
|
88
96
|
- `filters_confirmed`
|
|
@@ -117,7 +125,8 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
117
125
|
|
|
118
126
|
长耗时宿主兼容(推荐):
|
|
119
127
|
|
|
120
|
-
- `
|
|
128
|
+
- 默认调用 `start_recommend_pipeline_run` 启动异步流程,并配合 `get_recommend_pipeline_run` 轮询。
|
|
129
|
+
- `start_recommend_pipeline_run` 会先走同步一致的前置门禁(登录/页面就绪/岗位确认/最终确认)。
|
|
121
130
|
- 只有门禁通过后才会返回 `ACCEPTED + run_id`;否则会先返回 `NEED_INPUT/NEED_CONFIRMATION/FAILED`,必须先按提示补齐。
|
|
122
131
|
- 若宿主要显式拆成三步,也可使用:
|
|
123
132
|
- `start_recommend_pipeline_run`
|
|
@@ -194,7 +203,7 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
194
203
|
2) 若任意 npm_dep_* 失败:再安装 npm 依赖(chrome-remote-interface / ws)。
|
|
195
204
|
3) 若 python_cli 失败:安装 Python,并确保 python 命令可用。
|
|
196
205
|
4) 若 python_pillow 失败:最后执行 python -m pip install pillow。
|
|
197
|
-
每一步完成后重新运行 doctor,全部通过后再重试
|
|
206
|
+
每一步完成后重新运行 doctor,全部通过后再重试 start_recommend_pipeline_run。
|
|
198
207
|
```
|
|
199
208
|
|
|
200
209
|
安装顺序约束(必须遵守):
|
package/src/cli.js
CHANGED
|
@@ -880,7 +880,7 @@ function inspectAgentIntegration(agentRaw) {
|
|
|
880
880
|
|
|
881
881
|
const hasRecommendIntent = (content) => /(recommend|推荐页|boss recommend|recommend page)/i.test(content);
|
|
882
882
|
const hasSearchIntent = (content) => /(search|搜索页|boss search|search page)/i.test(content);
|
|
883
|
-
const hasRecommendPipelineRoute = (content) => /(boss-recommend-pipeline|
|
|
883
|
+
const hasRecommendPipelineRoute = (content) => /(boss-recommend-pipeline|start_recommend_pipeline_run)/i.test(content);
|
|
884
884
|
const hasRecruitPipelineRoute = (content) => /(boss-recruit-pipeline|run_recruit_pipeline)/i.test(content);
|
|
885
885
|
|
|
886
886
|
const skill_checks = skill_bases.map((baseDir) => {
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { runRecommendPipeline } from "./pipeline.js";
|
|
6
6
|
import {
|
|
7
7
|
RUN_MODE_ASYNC,
|
|
8
|
-
RUN_MODE_SYNC,
|
|
9
8
|
RUN_STAGE_PREFLIGHT,
|
|
10
9
|
RUN_STATE_CANCELED,
|
|
11
10
|
RUN_STATE_COMPLETED,
|
|
@@ -25,7 +24,6 @@ import {
|
|
|
25
24
|
const require = createRequire(import.meta.url);
|
|
26
25
|
const { version: SERVER_VERSION } = require("../package.json");
|
|
27
26
|
|
|
28
|
-
const TOOL_RUN_PIPELINE = "run_recommend_pipeline";
|
|
29
27
|
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
30
28
|
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
31
29
|
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
@@ -78,11 +76,6 @@ function createRunInputSchema() {
|
|
|
78
76
|
type: "string",
|
|
79
77
|
description: "用户自然语言推荐筛选指令"
|
|
80
78
|
},
|
|
81
|
-
execution_mode: {
|
|
82
|
-
type: "string",
|
|
83
|
-
enum: [RUN_MODE_ASYNC, RUN_MODE_SYNC],
|
|
84
|
-
description: "执行模式;默认 async。"
|
|
85
|
-
},
|
|
86
79
|
confirmation: {
|
|
87
80
|
type: "object",
|
|
88
81
|
properties: {
|
|
@@ -219,11 +212,6 @@ function createRunInputSchema() {
|
|
|
219
212
|
|
|
220
213
|
function createToolsSchema() {
|
|
221
214
|
return [
|
|
222
|
-
{
|
|
223
|
-
name: TOOL_RUN_PIPELINE,
|
|
224
|
-
description: "Boss 推荐页流水线:默认异步,但会先走与同步一致的前置确认/页面就绪门禁;仅在门禁通过后返回 run_id。传 execution_mode=sync 可改为全程同步执行。",
|
|
225
|
-
inputSchema: createRunInputSchema()
|
|
226
|
-
},
|
|
227
215
|
{
|
|
228
216
|
name: TOOL_START_RUN,
|
|
229
217
|
description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
|
|
@@ -291,12 +279,6 @@ function getLastOutputLine(text) {
|
|
|
291
279
|
return lines.length > 0 ? lines[lines.length - 1] : null;
|
|
292
280
|
}
|
|
293
281
|
|
|
294
|
-
function normalizeExecutionMode(value) {
|
|
295
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
296
|
-
if (normalized === RUN_MODE_SYNC) return RUN_MODE_SYNC;
|
|
297
|
-
return RUN_MODE_ASYNC;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
282
|
function normalizeRequiredConfirmations(value) {
|
|
301
283
|
if (!Array.isArray(value)) return [];
|
|
302
284
|
return value
|
|
@@ -413,31 +395,6 @@ function createRuntimeCallbacks(runId, heartbeatIntervalMs) {
|
|
|
413
395
|
};
|
|
414
396
|
}
|
|
415
397
|
|
|
416
|
-
function attachSyncRunMetadata(result, runId, lastStage) {
|
|
417
|
-
if (!result || typeof result !== "object") return result;
|
|
418
|
-
if (result.status === "COMPLETED") {
|
|
419
|
-
const nextResult = result.result && typeof result.result === "object" ? result.result : {};
|
|
420
|
-
return {
|
|
421
|
-
...result,
|
|
422
|
-
result: {
|
|
423
|
-
...nextResult,
|
|
424
|
-
run_id: runId
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
if (result.status === "FAILED") {
|
|
429
|
-
const diagnostics = result.diagnostics && typeof result.diagnostics === "object" ? result.diagnostics : {};
|
|
430
|
-
return {
|
|
431
|
-
...result,
|
|
432
|
-
diagnostics: {
|
|
433
|
-
...diagnostics,
|
|
434
|
-
run_id: runId,
|
|
435
|
-
last_stage: lastStage || RUN_STAGE_PREFLIGHT
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
return result;
|
|
440
|
-
}
|
|
441
398
|
|
|
442
399
|
async function executeTrackedPipeline({
|
|
443
400
|
runId,
|
|
@@ -552,31 +509,6 @@ function initializeRunStateOrThrow(runId, mode) {
|
|
|
552
509
|
return writeRunState(snapshot);
|
|
553
510
|
}
|
|
554
511
|
|
|
555
|
-
async function handleSyncRunTool({ workspaceRoot, args }) {
|
|
556
|
-
cleanupExpiredRuns();
|
|
557
|
-
const runId = createRunId();
|
|
558
|
-
try {
|
|
559
|
-
initializeRunStateOrThrow(runId, RUN_MODE_SYNC);
|
|
560
|
-
} catch (error) {
|
|
561
|
-
return {
|
|
562
|
-
status: "FAILED",
|
|
563
|
-
error: {
|
|
564
|
-
code: "RUN_STATE_IO_ERROR",
|
|
565
|
-
message: `无法写入运行状态目录:${error.message || "unknown"}`,
|
|
566
|
-
retryable: false
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
const tracked = await executeTrackedPipeline({
|
|
571
|
-
runId,
|
|
572
|
-
mode: RUN_MODE_SYNC,
|
|
573
|
-
workspaceRoot,
|
|
574
|
-
args,
|
|
575
|
-
signal: null
|
|
576
|
-
});
|
|
577
|
-
return attachSyncRunMetadata(tracked.result, runId, tracked.lastStage);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
512
|
async function handleStartRunTool({ workspaceRoot, args }) {
|
|
581
513
|
const precheckArgs = buildAsyncPrecheckArgs(args);
|
|
582
514
|
let precheckResult;
|
|
@@ -768,7 +700,7 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
768
700
|
const toolName = params?.name;
|
|
769
701
|
const args = params?.arguments || {};
|
|
770
702
|
|
|
771
|
-
if (
|
|
703
|
+
if (toolName === TOOL_START_RUN) {
|
|
772
704
|
const inputError = validateRunArgs(args);
|
|
773
705
|
if (inputError) {
|
|
774
706
|
return createJsonRpcError(id, -32602, inputError);
|
|
@@ -783,12 +715,7 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
783
715
|
|
|
784
716
|
try {
|
|
785
717
|
let payload;
|
|
786
|
-
if (toolName ===
|
|
787
|
-
const executionMode = normalizeExecutionMode(args.execution_mode);
|
|
788
|
-
payload = executionMode === RUN_MODE_SYNC
|
|
789
|
-
? await handleSyncRunTool({ workspaceRoot, args })
|
|
790
|
-
: await handleStartRunTool({ workspaceRoot, args });
|
|
791
|
-
} else if (toolName === TOOL_START_RUN) {
|
|
718
|
+
if (toolName === TOOL_START_RUN) {
|
|
792
719
|
payload = await handleStartRunTool({ workspaceRoot, args });
|
|
793
720
|
} else if (toolName === TOOL_GET_RUN) {
|
|
794
721
|
payload = handleGetRunTool(args);
|
package/src/test-index-async.js
CHANGED
|
@@ -47,7 +47,7 @@ async function waitForTerminalRunState(runId, timeoutMs = 4000) {
|
|
|
47
47
|
throw new Error(`Timed out waiting terminal run state for run_id=${runId}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async function
|
|
50
|
+
async function testAsyncStartStatusAndCancel() {
|
|
51
51
|
const previousHome = process.env.BOSS_RECOMMEND_HOME;
|
|
52
52
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
|
|
53
53
|
|
|
@@ -130,6 +130,21 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
130
130
|
assert.deepEqual(gatedStartPayload.required_confirmations, ["job"]);
|
|
131
131
|
assert.equal(gatedStartPayload.run_id, undefined);
|
|
132
132
|
|
|
133
|
+
const finalReviewGateResponse = await handleRequest(
|
|
134
|
+
makeToolCall(11, "start_recommend_pipeline_run", {
|
|
135
|
+
instruction: "need final review gate",
|
|
136
|
+
confirmation: {
|
|
137
|
+
job_confirmed: true,
|
|
138
|
+
job_value: "mock job",
|
|
139
|
+
final_confirmed: false
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
process.cwd()
|
|
143
|
+
);
|
|
144
|
+
const finalReviewGatePayload = await readToolPayload(finalReviewGateResponse);
|
|
145
|
+
assert.equal(finalReviewGatePayload.status, "NEED_CONFIRMATION");
|
|
146
|
+
assert.deepEqual(finalReviewGatePayload.required_confirmations, ["final_review"]);
|
|
147
|
+
|
|
133
148
|
const startResponse = await handleRequest(
|
|
134
149
|
makeToolCall(2, "start_recommend_pipeline_run", {
|
|
135
150
|
instruction: "slow task for cancel",
|
|
@@ -164,64 +179,14 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
164
179
|
const canceledRun = await waitForTerminalRunState(started.run_id);
|
|
165
180
|
assert.equal(canceledRun.state, "canceled");
|
|
166
181
|
|
|
167
|
-
const
|
|
168
|
-
makeToolCall(
|
|
169
|
-
|
|
170
|
-
);
|
|
171
|
-
const defaultAsyncGatedPayload = await readToolPayload(defaultAsyncGatedResponse);
|
|
172
|
-
assert.equal(defaultAsyncGatedPayload.status, "NEED_CONFIRMATION");
|
|
173
|
-
assert.deepEqual(defaultAsyncGatedPayload.required_confirmations, ["job"]);
|
|
174
|
-
|
|
175
|
-
const defaultAsyncResponse = await handleRequest(
|
|
176
|
-
makeToolCall(6, "run_recommend_pipeline", {
|
|
177
|
-
instruction: "fast async accepted run",
|
|
178
|
-
confirmation: {
|
|
179
|
-
job_confirmed: true,
|
|
180
|
-
job_value: "mock job",
|
|
181
|
-
final_confirmed: true
|
|
182
|
-
}
|
|
183
|
-
}),
|
|
184
|
-
process.cwd()
|
|
185
|
-
);
|
|
186
|
-
const defaultAsyncPayload = await readToolPayload(defaultAsyncResponse);
|
|
187
|
-
assert.equal(defaultAsyncPayload.status, "ACCEPTED");
|
|
188
|
-
assert.equal(typeof defaultAsyncPayload.run_id, "string");
|
|
189
|
-
const completedDefaultAsyncRun = await waitForTerminalRunState(defaultAsyncPayload.run_id);
|
|
190
|
-
assert.equal(completedDefaultAsyncRun.state, "completed");
|
|
191
|
-
|
|
192
|
-
const syncResponse = await handleRequest(
|
|
193
|
-
makeToolCall(7, "run_recommend_pipeline", {
|
|
194
|
-
instruction: "fast forced sync run",
|
|
195
|
-
execution_mode: "sync",
|
|
196
|
-
confirmation: {
|
|
197
|
-
job_confirmed: true,
|
|
198
|
-
job_value: "mock job",
|
|
199
|
-
final_confirmed: true
|
|
200
|
-
}
|
|
201
|
-
}),
|
|
202
|
-
process.cwd()
|
|
203
|
-
);
|
|
204
|
-
const syncPayload = await readToolPayload(syncResponse);
|
|
205
|
-
assert.equal(syncPayload.status, "COMPLETED");
|
|
206
|
-
assert.equal(typeof syncPayload.result.run_id, "string");
|
|
207
|
-
assert.equal(syncPayload.result.processed_count, 40);
|
|
208
|
-
|
|
209
|
-
const failedSyncResponse = await handleRequest(
|
|
210
|
-
makeToolCall(8, "run_recommend_pipeline", {
|
|
211
|
-
instruction: "force fail",
|
|
212
|
-
execution_mode: "sync",
|
|
213
|
-
confirmation: {
|
|
214
|
-
job_confirmed: true,
|
|
215
|
-
job_value: "mock job",
|
|
216
|
-
final_confirmed: true
|
|
217
|
-
}
|
|
182
|
+
const unknownToolResponse = await handleRequest(
|
|
183
|
+
makeToolCall(12, "run_recommend_pipeline", {
|
|
184
|
+
instruction: "legacy tool should be removed"
|
|
218
185
|
}),
|
|
219
186
|
process.cwd()
|
|
220
187
|
);
|
|
221
|
-
|
|
222
|
-
assert.
|
|
223
|
-
assert.equal(typeof syncFailedPayload.diagnostics?.run_id, "string");
|
|
224
|
-
assert.equal(typeof syncFailedPayload.diagnostics?.last_stage, "string");
|
|
188
|
+
assert.equal(unknownToolResponse?.error?.code, -32602);
|
|
189
|
+
assert.match(String(unknownToolResponse?.error?.message || ""), /Unknown tool/i);
|
|
225
190
|
|
|
226
191
|
assert.equal(activeAsyncRuns.size >= 0, true);
|
|
227
192
|
} finally {
|
|
@@ -236,7 +201,7 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
236
201
|
}
|
|
237
202
|
|
|
238
203
|
async function main() {
|
|
239
|
-
await
|
|
204
|
+
await testAsyncStartStatusAndCancel();
|
|
240
205
|
console.log("index async tests passed");
|
|
241
206
|
}
|
|
242
207
|
|
|
@@ -1008,23 +1008,7 @@ const jsClickCloseFallback = `(() => {
|
|
|
1008
1008
|
})()`;
|
|
1009
1009
|
|
|
1010
1010
|
const jsReloadRecommendFrame = `(() => {
|
|
1011
|
-
|
|
1012
|
-
|| document.querySelector('iframe[src*="/web/frame/recommend/"]')
|
|
1013
|
-
|| document.querySelector('iframe');
|
|
1014
|
-
if (!frame || !frame.contentWindow) {
|
|
1015
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1016
|
-
}
|
|
1017
|
-
try {
|
|
1018
|
-
const current = String(frame.contentWindow.location.href || '');
|
|
1019
|
-
if (current.includes('/web/frame/recommend/')) {
|
|
1020
|
-
frame.contentWindow.location.reload();
|
|
1021
|
-
} else {
|
|
1022
|
-
frame.contentWindow.location.href = 'https://www.zhipin.com/web/frame/recommend/';
|
|
1023
|
-
}
|
|
1024
|
-
return { ok: true };
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
return { ok: false, error: String(error && error.message ? error.message : error) };
|
|
1027
|
-
}
|
|
1011
|
+
return { ok: false, error: 'RELOAD_DISABLED_BY_POLICY' };
|
|
1028
1012
|
})()`;
|
|
1029
1013
|
|
|
1030
1014
|
class RecommendScreenCli {
|
|
@@ -1201,7 +1185,20 @@ class RecommendScreenCli {
|
|
|
1201
1185
|
);
|
|
1202
1186
|
}
|
|
1203
1187
|
|
|
1204
|
-
async
|
|
1188
|
+
async getCurrentWindowState() {
|
|
1189
|
+
if (!this.Browser || !this.windowId || typeof this.Browser.getWindowBounds !== "function") {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
const info = await this.Browser.getWindowBounds({ windowId: this.windowId });
|
|
1194
|
+
const state = String(info?.bounds?.windowState || "").toLowerCase();
|
|
1195
|
+
return state || null;
|
|
1196
|
+
} catch {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async setWindowStateIfPossible(windowState, reason = "unknown") {
|
|
1205
1202
|
if (!this.Browser || !this.windowId || typeof this.Browser.setWindowBounds !== "function") {
|
|
1206
1203
|
return false;
|
|
1207
1204
|
}
|
|
@@ -1209,17 +1206,38 @@ class RecommendScreenCli {
|
|
|
1209
1206
|
await this.Browser.setWindowBounds({
|
|
1210
1207
|
windowId: this.windowId,
|
|
1211
1208
|
bounds: {
|
|
1212
|
-
windowState
|
|
1209
|
+
windowState
|
|
1213
1210
|
}
|
|
1214
1211
|
});
|
|
1215
|
-
log(`[视口恢复]
|
|
1212
|
+
log(`[视口恢复] 已设置窗口状态为 ${windowState},原因: ${reason}`);
|
|
1216
1213
|
return true;
|
|
1217
1214
|
} catch (error) {
|
|
1218
|
-
log(`[视口恢复]
|
|
1215
|
+
log(`[视口恢复] 设置窗口状态 ${windowState} 失败: ${error.message || error}`);
|
|
1219
1216
|
return false;
|
|
1220
1217
|
}
|
|
1221
1218
|
}
|
|
1222
1219
|
|
|
1220
|
+
async toggleWindowStateForViewportRecovery(reason = "unknown") {
|
|
1221
|
+
const currentState = await this.getCurrentWindowState();
|
|
1222
|
+
const sequence = currentState === "normal"
|
|
1223
|
+
? ["maximized", "normal"]
|
|
1224
|
+
: ["normal", "maximized"];
|
|
1225
|
+
let applied = false;
|
|
1226
|
+
for (const state of sequence) {
|
|
1227
|
+
const ok = await this.setWindowStateIfPossible(state, reason);
|
|
1228
|
+
if (ok) {
|
|
1229
|
+
applied = true;
|
|
1230
|
+
await sleep(humanDelay(520, 80));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (applied && this.Page && typeof this.Page.bringToFront === "function") {
|
|
1234
|
+
try {
|
|
1235
|
+
await this.Page.bringToFront();
|
|
1236
|
+
} catch {}
|
|
1237
|
+
}
|
|
1238
|
+
return applied;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1223
1241
|
async ensureHealthyListViewport(reason = "unknown") {
|
|
1224
1242
|
let state = await this.getListState();
|
|
1225
1243
|
if (!this.isListViewportCollapsed(state)) {
|
|
@@ -1227,24 +1245,13 @@ class RecommendScreenCli {
|
|
|
1227
1245
|
}
|
|
1228
1246
|
|
|
1229
1247
|
log(`[视口恢复] 检测到推荐列表视口异常缩小,尝试自动恢复。原因: ${reason}`);
|
|
1230
|
-
await this.
|
|
1231
|
-
await sleep(humanDelay(
|
|
1248
|
+
await this.toggleWindowStateForViewportRecovery(reason);
|
|
1249
|
+
await sleep(humanDelay(900, 130));
|
|
1232
1250
|
state = await this.getListState();
|
|
1233
1251
|
if (!this.isListViewportCollapsed(state)) {
|
|
1234
1252
|
return { ok: true, recovered: true, state };
|
|
1235
1253
|
}
|
|
1236
1254
|
|
|
1237
|
-
const reloaded = await this.evaluate(jsReloadRecommendFrame);
|
|
1238
|
-
if (reloaded?.ok) {
|
|
1239
|
-
log("[视口恢复] 已触发 recommendFrame reload。");
|
|
1240
|
-
await this.waitForListReady(45);
|
|
1241
|
-
await sleep(humanDelay(900, 150));
|
|
1242
|
-
state = await this.getListState();
|
|
1243
|
-
if (!this.isListViewportCollapsed(state)) {
|
|
1244
|
-
return { ok: true, recovered: true, state };
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
1255
|
return {
|
|
1249
1256
|
ok: false,
|
|
1250
1257
|
recovered: false,
|
|
@@ -1597,35 +1604,21 @@ class RecommendScreenCli {
|
|
|
1597
1604
|
}
|
|
1598
1605
|
}
|
|
1599
1606
|
|
|
1600
|
-
log(`[关闭详情]
|
|
1607
|
+
log(`[关闭详情] 常规关闭失败,进入额外 ESC 重试(禁用刷新/跳转)。最后状态: ${state?.reason || "unknown"}`);
|
|
1601
1608
|
for (let retry = 0; retry < 2; retry += 1) {
|
|
1609
|
+
await sleep(2000);
|
|
1602
1610
|
await this.pressEsc();
|
|
1603
|
-
await sleep(
|
|
1611
|
+
await sleep(humanDelay(260, 60));
|
|
1604
1612
|
state = await this.getDetailClosedState();
|
|
1605
1613
|
if (state?.closed) {
|
|
1606
|
-
log(`[关闭详情]
|
|
1614
|
+
log(`[关闭详情] 额外ESC成功: ${state.reason || "closed"}`);
|
|
1607
1615
|
return true;
|
|
1608
1616
|
}
|
|
1609
1617
|
}
|
|
1610
1618
|
|
|
1611
|
-
const reloaded = await this.evaluate(jsReloadRecommendFrame);
|
|
1612
|
-
if (reloaded?.ok) {
|
|
1613
|
-
log("[关闭详情] 已触发 recommendFrame reload,等待列表恢复。");
|
|
1614
|
-
const listReady = await this.waitForListReady(45);
|
|
1615
|
-
if (listReady) {
|
|
1616
|
-
state = await this.getDetailClosedState();
|
|
1617
|
-
if (state?.closed) {
|
|
1618
|
-
log(`[关闭详情] reload 后恢复成功: ${state.reason || "closed"}`);
|
|
1619
|
-
return true;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
} else {
|
|
1623
|
-
log(`[关闭详情] recommendFrame reload 失败: ${reloaded?.error || "unknown"}`);
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
1619
|
state = await this.getDetailClosedState();
|
|
1627
|
-
log(`[关闭详情]
|
|
1628
|
-
return
|
|
1620
|
+
log(`[关闭详情] 连续 ESC 后仍未确认关闭(${state?.reason || "unknown"}),按策略视为检测误差并继续下一位。`);
|
|
1621
|
+
return true;
|
|
1629
1622
|
}
|
|
1630
1623
|
|
|
1631
1624
|
async waitForListReady(maxRounds = 30) {
|