@reconcrap/boss-recommend-mcp 1.0.12 → 1.0.14
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,8 +9,8 @@ Boss 推荐页自动化流水线 MCP(stdio)服务。
|
|
|
9
9
|
|
|
10
10
|
MCP 工具:
|
|
11
11
|
|
|
12
|
-
- `run_recommend_pipeline
|
|
13
|
-
- `start_recommend_pipeline_run
|
|
12
|
+
- `run_recommend_pipeline`(默认异步,但会先执行与同步一致的前置门禁;仅门禁通过后返回 run_id)
|
|
13
|
+
- `start_recommend_pipeline_run`(异步启动;同样先经过前置门禁,通过后返回 run_id)
|
|
14
14
|
- `get_recommend_pipeline_run`(轮询 run_id 状态)
|
|
15
15
|
- `cancel_recommend_pipeline_run`(取消运行中任务)
|
|
16
16
|
|
|
@@ -143,13 +143,14 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
|
|
|
143
143
|
|
|
144
144
|
当宿主 agent 对“长时间无回包”敏感(容易误判失败)时,建议改用异步工具:
|
|
145
145
|
|
|
146
|
-
1.
|
|
147
|
-
2.
|
|
148
|
-
3.
|
|
146
|
+
1. 优先调用 `run_recommend_pipeline`(默认异步)。
|
|
147
|
+
2. 若返回 `NEED_INPUT/NEED_CONFIRMATION/FAILED`,按同步流程先补齐前置条件(登录、页面就绪、岗位确认、最终确认)。
|
|
148
|
+
3. 仅当门禁通过时,接口才会返回 `ACCEPTED + run_id`;随后每 5~15 秒调用一次 `get_recommend_pipeline_run` 轮询。
|
|
149
|
+
4. 若需终止,调用 `cancel_recommend_pipeline_run`。
|
|
149
150
|
|
|
150
151
|
说明:
|
|
151
152
|
|
|
152
|
-
- `run_recommend_pipeline`
|
|
153
|
+
- `run_recommend_pipeline` 默认异步,但不会跳过同步确认流程;若确实需要阻塞式返回,可传 `execution_mode=sync`。
|
|
153
154
|
- 定时心跳默认 120 秒一次;`updated_at` 仍会在阶段或进度变化时刷新。
|
|
154
155
|
- 每个 run 会持久化到 `~/.boss-recommend-mcp/runs/<run_id>.json`(可通过 `BOSS_RECOMMEND_HOME` 覆盖)。
|
|
155
156
|
- 轮询期间不要重复 `start`,优先复用已有 `run_id`,避免重复筛选。
|
package/package.json
CHANGED
|
@@ -117,7 +117,8 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
117
117
|
|
|
118
118
|
长耗时宿主兼容(推荐):
|
|
119
119
|
|
|
120
|
-
- `run_recommend_pipeline`
|
|
120
|
+
- `run_recommend_pipeline` 默认异步,但开始前会先走与同步一致的前置门禁(登录/页面就绪/岗位确认/最终确认)。
|
|
121
|
+
- 只有门禁通过后才会返回 `ACCEPTED + run_id`;否则会先返回 `NEED_INPUT/NEED_CONFIRMATION/FAILED`,必须先按提示补齐。
|
|
121
122
|
- 若宿主要显式拆成三步,也可使用:
|
|
122
123
|
- `start_recommend_pipeline_run`
|
|
123
124
|
- `get_recommend_pipeline_run`
|
package/src/index.js
CHANGED
|
@@ -221,12 +221,12 @@ function createToolsSchema() {
|
|
|
221
221
|
return [
|
|
222
222
|
{
|
|
223
223
|
name: TOOL_RUN_PIPELINE,
|
|
224
|
-
description: "Boss
|
|
224
|
+
description: "Boss 推荐页流水线:默认异步,但会先走与同步一致的前置确认/页面就绪门禁;仅在门禁通过后返回 run_id。传 execution_mode=sync 可改为全程同步执行。",
|
|
225
225
|
inputSchema: createRunInputSchema()
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
name: TOOL_START_RUN,
|
|
229
|
-
description: "异步启动 Boss
|
|
229
|
+
description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
|
|
230
230
|
inputSchema: createRunInputSchema()
|
|
231
231
|
},
|
|
232
232
|
{
|
|
@@ -297,6 +297,43 @@ function normalizeExecutionMode(value) {
|
|
|
297
297
|
return RUN_MODE_ASYNC;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
function normalizeRequiredConfirmations(value) {
|
|
301
|
+
if (!Array.isArray(value)) return [];
|
|
302
|
+
return value
|
|
303
|
+
.map((item) => normalizeText(item))
|
|
304
|
+
.filter(Boolean);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function hasExplicitFinalConfirmation(args) {
|
|
308
|
+
return args?.confirmation?.final_confirmed === true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildAsyncPrecheckConfirmation(confirmation) {
|
|
312
|
+
if (!confirmation || typeof confirmation !== "object") {
|
|
313
|
+
return {
|
|
314
|
+
final_confirmed: false
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...confirmation,
|
|
319
|
+
final_confirmed: false
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildAsyncPrecheckArgs(args) {
|
|
324
|
+
return {
|
|
325
|
+
instruction: args.instruction,
|
|
326
|
+
confirmation: buildAsyncPrecheckConfirmation(args.confirmation),
|
|
327
|
+
overrides: args.overrides
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isFinalReviewOnlyConfirmation(result) {
|
|
332
|
+
if (result?.status !== "NEED_CONFIRMATION") return false;
|
|
333
|
+
const required = normalizeRequiredConfirmations(result.required_confirmations);
|
|
334
|
+
return required.length > 0 && required.every((item) => item === "final_review");
|
|
335
|
+
}
|
|
336
|
+
|
|
300
337
|
function safeUpdateRunState(runId, updater) {
|
|
301
338
|
try {
|
|
302
339
|
return updateRunState(runId, updater);
|
|
@@ -540,7 +577,38 @@ async function handleSyncRunTool({ workspaceRoot, args }) {
|
|
|
540
577
|
return attachSyncRunMetadata(tracked.result, runId, tracked.lastStage);
|
|
541
578
|
}
|
|
542
579
|
|
|
543
|
-
function handleStartRunTool({ workspaceRoot, args }) {
|
|
580
|
+
async function handleStartRunTool({ workspaceRoot, args }) {
|
|
581
|
+
const precheckArgs = buildAsyncPrecheckArgs(args);
|
|
582
|
+
let precheckResult;
|
|
583
|
+
try {
|
|
584
|
+
precheckResult = await runPipelineImpl(
|
|
585
|
+
{
|
|
586
|
+
workspaceRoot,
|
|
587
|
+
instruction: precheckArgs.instruction,
|
|
588
|
+
confirmation: precheckArgs.confirmation,
|
|
589
|
+
overrides: precheckArgs.overrides
|
|
590
|
+
},
|
|
591
|
+
undefined,
|
|
592
|
+
null
|
|
593
|
+
);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
precheckResult = {
|
|
596
|
+
status: "FAILED",
|
|
597
|
+
error: {
|
|
598
|
+
code: "UNEXPECTED_ERROR",
|
|
599
|
+
message: error?.message || "Unexpected error",
|
|
600
|
+
retryable: true
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (precheckResult?.status !== "NEED_CONFIRMATION") {
|
|
606
|
+
return precheckResult;
|
|
607
|
+
}
|
|
608
|
+
if (!hasExplicitFinalConfirmation(args) || !isFinalReviewOnlyConfirmation(precheckResult)) {
|
|
609
|
+
return precheckResult;
|
|
610
|
+
}
|
|
611
|
+
|
|
544
612
|
cleanupExpiredRuns();
|
|
545
613
|
const runId = createRunId();
|
|
546
614
|
try {
|
|
@@ -719,9 +787,9 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
719
787
|
const executionMode = normalizeExecutionMode(args.execution_mode);
|
|
720
788
|
payload = executionMode === RUN_MODE_SYNC
|
|
721
789
|
? await handleSyncRunTool({ workspaceRoot, args })
|
|
722
|
-
: handleStartRunTool({ workspaceRoot, args });
|
|
790
|
+
: await handleStartRunTool({ workspaceRoot, args });
|
|
723
791
|
} else if (toolName === TOOL_START_RUN) {
|
|
724
|
-
payload = handleStartRunTool({ workspaceRoot, args });
|
|
792
|
+
payload = await handleStartRunTool({ workspaceRoot, args });
|
|
725
793
|
} else if (toolName === TOOL_GET_RUN) {
|
|
726
794
|
payload = handleGetRunTool(args);
|
|
727
795
|
} else if (toolName === TOOL_CANCEL_RUN) {
|
package/src/test-index-async.js
CHANGED
|
@@ -52,6 +52,31 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
52
52
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-index-async-"));
|
|
53
53
|
|
|
54
54
|
setRunPipelineImplForTests(async (input, _deps, runtime) => {
|
|
55
|
+
if (input.confirmation?.job_confirmed !== true) {
|
|
56
|
+
return {
|
|
57
|
+
status: "NEED_CONFIRMATION",
|
|
58
|
+
required_confirmations: ["job"],
|
|
59
|
+
pending_questions: [
|
|
60
|
+
{
|
|
61
|
+
field: "job",
|
|
62
|
+
question: "请先确认岗位"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (input.confirmation?.final_confirmed !== true) {
|
|
68
|
+
return {
|
|
69
|
+
status: "NEED_CONFIRMATION",
|
|
70
|
+
required_confirmations: ["final_review"],
|
|
71
|
+
pending_questions: [
|
|
72
|
+
{
|
|
73
|
+
field: "final_review",
|
|
74
|
+
question: "请做最终确认"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
runtime?.onStage?.({ stage: "preflight", message: "preflight started" });
|
|
56
81
|
await sleep(50);
|
|
57
82
|
if (input.instruction.includes("fail")) {
|
|
@@ -96,17 +121,33 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
96
121
|
process.env.BOSS_RECOMMEND_HOME = tempHome;
|
|
97
122
|
|
|
98
123
|
try {
|
|
99
|
-
const
|
|
124
|
+
const gatedStartResponse = await handleRequest(
|
|
100
125
|
makeToolCall(1, "start_recommend_pipeline_run", { instruction: "slow task for cancel" }),
|
|
101
126
|
process.cwd()
|
|
102
127
|
);
|
|
128
|
+
const gatedStartPayload = await readToolPayload(gatedStartResponse);
|
|
129
|
+
assert.equal(gatedStartPayload.status, "NEED_CONFIRMATION");
|
|
130
|
+
assert.deepEqual(gatedStartPayload.required_confirmations, ["job"]);
|
|
131
|
+
assert.equal(gatedStartPayload.run_id, undefined);
|
|
132
|
+
|
|
133
|
+
const startResponse = await handleRequest(
|
|
134
|
+
makeToolCall(2, "start_recommend_pipeline_run", {
|
|
135
|
+
instruction: "slow task for cancel",
|
|
136
|
+
confirmation: {
|
|
137
|
+
job_confirmed: true,
|
|
138
|
+
job_value: "mock job",
|
|
139
|
+
final_confirmed: true
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
process.cwd()
|
|
143
|
+
);
|
|
103
144
|
const started = await readToolPayload(startResponse);
|
|
104
145
|
assert.equal(started.status, "ACCEPTED");
|
|
105
146
|
assert.equal(typeof started.run_id, "string");
|
|
106
147
|
assert.equal(started.poll_after_sec >= 5 && started.poll_after_sec <= 15, true);
|
|
107
148
|
|
|
108
149
|
const statusResponse = await handleRequest(
|
|
109
|
-
makeToolCall(
|
|
150
|
+
makeToolCall(3, "get_recommend_pipeline_run", { run_id: started.run_id }),
|
|
110
151
|
process.cwd()
|
|
111
152
|
);
|
|
112
153
|
const initialStatus = await readToolPayload(statusResponse);
|
|
@@ -114,7 +155,7 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
114
155
|
assert.equal(["queued", "running"].includes(initialStatus.run.state), true);
|
|
115
156
|
|
|
116
157
|
const cancelResponse = await handleRequest(
|
|
117
|
-
makeToolCall(
|
|
158
|
+
makeToolCall(4, "cancel_recommend_pipeline_run", { run_id: started.run_id }),
|
|
118
159
|
process.cwd()
|
|
119
160
|
);
|
|
120
161
|
const canceled = await readToolPayload(cancelResponse);
|
|
@@ -123,8 +164,23 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
123
164
|
const canceledRun = await waitForTerminalRunState(started.run_id);
|
|
124
165
|
assert.equal(canceledRun.state, "canceled");
|
|
125
166
|
|
|
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
|
+
|
|
126
175
|
const defaultAsyncResponse = await handleRequest(
|
|
127
|
-
makeToolCall(
|
|
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
|
+
}),
|
|
128
184
|
process.cwd()
|
|
129
185
|
);
|
|
130
186
|
const defaultAsyncPayload = await readToolPayload(defaultAsyncResponse);
|
|
@@ -133,22 +189,32 @@ async function testAsyncStartStatusCancelAndSyncCompatibility() {
|
|
|
133
189
|
const completedDefaultAsyncRun = await waitForTerminalRunState(defaultAsyncPayload.run_id);
|
|
134
190
|
assert.equal(completedDefaultAsyncRun.state, "completed");
|
|
135
191
|
|
|
136
|
-
const
|
|
137
|
-
makeToolCall(
|
|
192
|
+
const syncResponse = await handleRequest(
|
|
193
|
+
makeToolCall(7, "run_recommend_pipeline", {
|
|
138
194
|
instruction: "fast forced sync run",
|
|
139
|
-
execution_mode: "sync"
|
|
195
|
+
execution_mode: "sync",
|
|
196
|
+
confirmation: {
|
|
197
|
+
job_confirmed: true,
|
|
198
|
+
job_value: "mock job",
|
|
199
|
+
final_confirmed: true
|
|
200
|
+
}
|
|
140
201
|
}),
|
|
141
202
|
process.cwd()
|
|
142
203
|
);
|
|
143
|
-
const syncPayload = await readToolPayload(
|
|
204
|
+
const syncPayload = await readToolPayload(syncResponse);
|
|
144
205
|
assert.equal(syncPayload.status, "COMPLETED");
|
|
145
206
|
assert.equal(typeof syncPayload.result.run_id, "string");
|
|
146
207
|
assert.equal(syncPayload.result.processed_count, 40);
|
|
147
208
|
|
|
148
209
|
const failedSyncResponse = await handleRequest(
|
|
149
|
-
makeToolCall(
|
|
210
|
+
makeToolCall(8, "run_recommend_pipeline", {
|
|
150
211
|
instruction: "force fail",
|
|
151
|
-
execution_mode: "sync"
|
|
212
|
+
execution_mode: "sync",
|
|
213
|
+
confirmation: {
|
|
214
|
+
job_confirmed: true,
|
|
215
|
+
job_value: "mock job",
|
|
216
|
+
final_confirmed: true
|
|
217
|
+
}
|
|
152
218
|
}),
|
|
153
219
|
process.cwd()
|
|
154
220
|
);
|
|
@@ -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) {
|