@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. 优先调用 `run_recommend_pipeline`(默认异步)。
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
- - `run_recommend_pipeline` 默认异步,但不会跳过同步确认流程;若确实需要阻塞式返回,可传 `execution_mode=sync`。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -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 工具 `run_recommend_pipeline` 完成端到端任务:
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(默认异步): `run_recommend_pipeline`
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
- - `run_recommend_pipeline` 默认异步,但开始前会先走与同步一致的前置门禁(登录/页面就绪/岗位确认/最终确认)。
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,全部通过后再重试 run_recommend_pipeline
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|run_recommend_pipeline)/i.test(content);
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 ([TOOL_RUN_PIPELINE, TOOL_START_RUN].includes(toolName)) {
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 === TOOL_RUN_PIPELINE) {
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);
@@ -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 testAsyncStartStatusCancelAndSyncCompatibility() {
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 defaultAsyncGatedResponse = await handleRequest(
168
- makeToolCall(5, "run_recommend_pipeline", { instruction: "fast default async gate" }),
169
- process.cwd()
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
- const syncFailedPayload = await readToolPayload(failedSyncResponse);
222
- assert.equal(syncFailedPayload.status, "FAILED");
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 testAsyncStartStatusCancelAndSyncCompatibility();
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
- const frame = document.querySelector('iframe[name="recommendFrame"]')
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 maximizeWindowIfPossible(reason = "unknown") {
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: "maximized"
1209
+ windowState
1213
1210
  }
1214
1211
  });
1215
- log(`[视口恢复] 已尝试最大化 Chrome 窗口,原因: ${reason}`);
1212
+ log(`[视口恢复] 已设置窗口状态为 ${windowState},原因: ${reason}`);
1216
1213
  return true;
1217
1214
  } catch (error) {
1218
- log(`[视口恢复] 最大化窗口失败: ${error.message || error}`);
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.maximizeWindowIfPossible(reason);
1231
- await sleep(humanDelay(800, 120));
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(`[关闭详情] 常规关闭失败,进入强制恢复。最后状态: ${state?.reason || "unknown"}`);
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(900 + retry * 250);
1611
+ await sleep(humanDelay(260, 60));
1604
1612
  state = await this.getDetailClosedState();
1605
1613
  if (state?.closed) {
1606
- log(`[关闭详情] 强制ESC成功: ${state.reason || "closed"}`);
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(`[关闭详情] 失败,当前状态: ${state?.reason || "unknown"}`);
1628
- return false;
1620
+ log(`[关闭详情] 连续 ESC 后仍未确认关闭(${state?.reason || "unknown"}),按策略视为检测误差并继续下一位。`);
1621
+ return true;
1629
1622
  }
1630
1623
 
1631
1624
  async waitForListReady(maxRounds = 30) {