@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

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.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,266 +1,438 @@
1
- import { sleep } from "../../core/browser/index.js";
2
- import {
3
- buildRecommendSelfHealConfig,
4
- resolveRecommendSelfHealRoots
5
- } from "../../core/self-heal/index.js";
6
- import {
7
- createRecoverySettleError,
8
- waitForMiniFreshStartSettle
9
- } from "../common/recovery-settle.js";
10
- import {
11
- clickRecommendEndRefreshButton,
12
- waitForRecommendCardNodeIds
13
- } from "./cards.js";
1
+ import { sleep } from "../../core/browser/index.js";
2
+ import {
3
+ buildRecommendSelfHealConfig,
4
+ resolveRecommendSelfHealRoots
5
+ } from "../../core/self-heal/index.js";
6
+ import {
7
+ createRecoverySettleError,
8
+ waitForMiniFreshStartSettle
9
+ } from "../common/recovery-settle.js";
10
+ import {
11
+ clickRecommendEndRefreshButton,
12
+ waitForRecommendCardNodeIds
13
+ } from "./cards.js";
14
14
  import {
15
15
  RECOMMEND_RECENT_NOT_VIEW_LABEL,
16
16
  RECOMMEND_TARGET_URL
17
17
  } from "./constants.js";
18
18
  import { selectAndConfirmFirstSafeFilter } from "./filters.js";
19
+ import { ensureRecommendCurrentCityOnly } from "./location.js";
19
20
  import {
20
21
  selectRecommendJob,
21
22
  verifyRecommendJobSelection
22
23
  } from "./jobs.js";
23
24
  import { selectRecommendPageScope } from "./scopes.js";
24
- import {
25
- getRecommendRoots,
26
- waitForRecommendRoots
27
- } from "./roots.js";
28
- import { isStaleRecommendNodeError } from "./detail.js";
29
-
30
- function normalizeLabels(labels = []) {
31
- return labels.map((label) => String(label || "").trim()).filter(Boolean);
32
- }
25
+ import {
26
+ getRecommendRoots,
27
+ waitForRecommendRoots
28
+ } from "./roots.js";
29
+ import { isStaleRecommendNodeError } from "./detail.js";
30
+
31
+ function normalizeLabels(labels = []) {
32
+ return labels.map((label) => String(label || "").trim()).filter(Boolean);
33
+ }
33
34
 
34
35
  function normalizeFilterGroup(spec = {}) {
35
36
  return {
36
37
  group: String(spec.group || "").trim(),
37
38
  labels: normalizeLabels(spec.labels || spec.filterLabels || []),
38
- selectAllLabels: spec.selectAllLabels !== false
39
+ selectAllLabels: spec.selectAllLabels !== false,
40
+ allowUnlimited: spec.allowUnlimited === true,
41
+ verifySticky: spec.verifySticky === true
39
42
  };
40
43
  }
41
44
 
42
45
  export function buildRecommendFilterGroups(filter = {}, {
43
46
  forceRecentNotView = false
44
- } = {}) {
45
- const groups = [];
46
- const sourceGroups = Array.isArray(filter.filterGroups)
47
- ? filter.filterGroups
48
- : Array.isArray(filter.groups)
49
- ? filter.groups
50
- : [];
51
-
52
- for (const spec of sourceGroups) {
53
- const group = normalizeFilterGroup(spec);
54
- if (group.group || group.labels.length) groups.push(group);
55
- }
56
-
57
- const rootGroup = normalizeFilterGroup(filter);
58
- if ((rootGroup.group || rootGroup.labels.length) && !groups.length) {
59
- groups.push(rootGroup);
60
- }
61
-
62
- if (forceRecentNotView) {
63
- const recentGroup = groups.find((item) => item.group === "recentNotView");
64
- if (recentGroup) {
65
- if (!recentGroup.labels.some((label) => label.replace(/\s+/g, "") === RECOMMEND_RECENT_NOT_VIEW_LABEL)) {
66
- recentGroup.labels.push(RECOMMEND_RECENT_NOT_VIEW_LABEL);
67
- }
47
+ } = {}) {
48
+ const groups = [];
49
+ const sourceGroups = Array.isArray(filter.filterGroups)
50
+ ? filter.filterGroups
51
+ : Array.isArray(filter.groups)
52
+ ? filter.groups
53
+ : [];
54
+
55
+ for (const spec of sourceGroups) {
56
+ const group = normalizeFilterGroup(spec);
57
+ if (group.group || group.labels.length) groups.push(group);
58
+ }
59
+
60
+ const rootGroup = normalizeFilterGroup(filter);
61
+ if ((rootGroup.group || rootGroup.labels.length) && !groups.length) {
62
+ groups.push(rootGroup);
63
+ }
64
+
65
+ if (forceRecentNotView) {
66
+ const recentGroup = groups.find((item) => item.group === "recentNotView");
67
+ if (recentGroup) {
68
+ if (!recentGroup.labels.some((label) => label.replace(/\s+/g, "") === RECOMMEND_RECENT_NOT_VIEW_LABEL)) {
69
+ recentGroup.labels.push(RECOMMEND_RECENT_NOT_VIEW_LABEL);
70
+ }
68
71
  recentGroup.selectAllLabels = true;
69
72
  } else {
70
73
  groups.unshift({
71
74
  group: "recentNotView",
72
75
  labels: [RECOMMEND_RECENT_NOT_VIEW_LABEL],
73
- selectAllLabels: true
76
+ selectAllLabels: true,
77
+ allowUnlimited: false,
78
+ verifySticky: false
74
79
  });
75
80
  }
76
81
  }
77
82
 
78
83
  return groups;
79
- }
80
-
81
- export function buildRecommendFilterSelectionOptions(filter = {}, {
82
- forceRecentNotView = false
83
- } = {}) {
84
- const filterGroups = buildRecommendFilterGroups(filter, { forceRecentNotView });
85
- if (filterGroups.length > 1 || forceRecentNotView || Array.isArray(filter.filterGroups) || Array.isArray(filter.groups)) {
86
- return { filterGroups };
87
- }
84
+ }
85
+
86
+ export function buildRecommendFilterSelectionOptions(filter = {}, {
87
+ forceRecentNotView = false
88
+ } = {}) {
89
+ const filterGroups = buildRecommendFilterGroups(filter, { forceRecentNotView });
90
+ if (filterGroups.length > 1 || forceRecentNotView || Array.isArray(filter.filterGroups) || Array.isArray(filter.groups)) {
91
+ return { filterGroups };
92
+ }
88
93
  const [singleGroup] = filterGroups;
89
94
  if (singleGroup) {
90
95
  return {
91
96
  group: singleGroup.group,
92
97
  labels: singleGroup.labels,
93
- selectAllLabels: singleGroup.selectAllLabels
98
+ selectAllLabels: singleGroup.selectAllLabels,
99
+ allowUnlimited: singleGroup.allowUnlimited,
100
+ verifySticky: singleGroup.verifySticky
94
101
  };
95
102
  }
96
103
  return {
97
104
  group: filter.group || "",
98
105
  labels: normalizeLabels(filter.labels || filter.filterLabels || []),
99
- selectAllLabels: filter.selectAllLabels !== false
106
+ selectAllLabels: filter.selectAllLabels !== false,
107
+ allowUnlimited: filter.allowUnlimited === true,
108
+ verifySticky: filter.verifySticky === true
100
109
  };
101
110
  }
102
111
 
103
- function refreshFailureReason(method = "") {
104
- return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
112
+ export async function applyRecommendFilterEnvelopeStages(filter = {}, {
113
+ applyCurrentCityOnly,
114
+ applyFilterPanel
115
+ } = {}) {
116
+ if (filter.enabled === false) {
117
+ return {
118
+ applied: false,
119
+ skipped: true,
120
+ current_city_only: null,
121
+ filter: null
122
+ };
123
+ }
124
+ if (typeof applyCurrentCityOnly !== "function" || typeof applyFilterPanel !== "function") {
125
+ throw new Error("Recommend native filter stages require location and filter-panel callbacks");
126
+ }
127
+ const currentCityOnlyResult = await applyCurrentCityOnly();
128
+ const filterResult = await applyFilterPanel();
129
+ return {
130
+ applied: true,
131
+ skipped: false,
132
+ current_city_only: currentCityOnlyResult,
133
+ filter: filterResult
134
+ };
105
135
  }
106
136
 
107
- export function isRetryableRecommendFilterReapplyError(error) {
108
- const message = String(error?.message || error || "");
109
- return /Recommend filter panel did not open|Recommend filter trigger was not found|Recommend filter confirm button was not found|No matching recommend filter option/i.test(message);
137
+ function refreshFailureReason(method = "") {
138
+ return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
110
139
  }
111
140
 
112
- function compactFilterReapplyError(error) {
113
- return error?.message || String(error || "Recommend filter reapply failed");
141
+ function safeDiagnosticText(value, maxLength = 4000) {
142
+ const text = String(value ?? "");
143
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
114
144
  }
115
145
 
116
- export function isRetryableRecommendJobSelectionError(error) {
117
- if (isStaleRecommendNodeError(error)) return true;
118
- const message = String(error?.message || error || "");
119
- return /Recommend job trigger was not found|Recommend job dropdown did not mount options|Recommend job dropdown did not expose visible options|Matched recommend job has no clickable center|Matched recommend job has no visible clickable option|Recommend job selection was not sticky|Recommend job dropdown remained open after sticky verification/i.test(message);
146
+ function safeDiagnosticStack(stack) {
147
+ if (!stack) return null;
148
+ return safeDiagnosticText(stack, 8000)
149
+ .split(/\r?\n/)
150
+ .slice(0, 12)
151
+ .join("\n");
120
152
  }
121
153
 
122
- function compactJobSelectionAttempt({
123
- ok = false,
124
- attempt = 0,
125
- iframeDocumentNodeId = 0,
126
- error = null,
127
- selection = null
154
+ function buildErrorDiagnostic(error, {
155
+ depth = 0,
156
+ seen = new Set()
128
157
  } = {}) {
129
- return {
130
- ok: Boolean(ok),
131
- method: "job_select",
132
- reason: error ? "job_select_failed" : null,
133
- error: error ? (error?.message || String(error)) : null,
134
- attempt,
135
- iframe_document_node_id: iframeDocumentNodeId || 0,
136
- selected: Boolean(selection?.selected),
137
- selection_reason: selection?.reason || null,
138
- sticky_verified: selection?.sticky_verification?.verified ?? null,
139
- sticky_current_label: selection?.sticky_verification?.current_label_without_salary
140
- || selection?.sticky_verification?.current_label
141
- || null,
142
- sticky_menu_closed: selection?.sticky_verification?.menu_close?.ok ?? null
158
+ if (!error) return null;
159
+ const isRecord = typeof error === "object" || typeof error === "function";
160
+ if (isRecord && seen.has(error)) {
161
+ return {
162
+ name: error?.name || "Error",
163
+ message: safeDiagnosticText(error?.message || String(error)),
164
+ circular: true
165
+ };
166
+ }
167
+ if (isRecord) seen.add(error);
168
+ const diagnostic = {
169
+ name: error?.name || "Error",
170
+ message: safeDiagnosticText(error?.message || String(error))
143
171
  };
172
+ if (error?.code !== undefined && error?.code !== null && error.code !== "") {
173
+ diagnostic.code = typeof error.code === "number"
174
+ ? error.code
175
+ : safeDiagnosticText(error.code, 300);
176
+ }
177
+ if (error?.phase) diagnostic.phase = safeDiagnosticText(error.phase, 300);
178
+ if (error?.cdp_method) diagnostic.cdp_method = safeDiagnosticText(error.cdp_method, 300);
179
+ if (error?.cdp_at) diagnostic.cdp_at = safeDiagnosticText(error.cdp_at, 100);
180
+ if (Number.isInteger(error?.cdp_node_id)) diagnostic.cdp_node_id = error.cdp_node_id;
181
+ if (Number.isInteger(error?.cdp_backend_node_id)) {
182
+ diagnostic.cdp_backend_node_id = error.cdp_backend_node_id;
183
+ }
184
+ if (Number.isInteger(error?.node_id)) diagnostic.node_id = error.node_id;
185
+ if (Number.isInteger(error?.backend_node_id)) diagnostic.backend_node_id = error.backend_node_id;
186
+ if (error?.cdp_search_id) diagnostic.cdp_search_id = safeDiagnosticText(error.cdp_search_id, 300);
187
+ if (Array.isArray(error?.cdp_param_keys)) {
188
+ diagnostic.cdp_param_keys = error.cdp_param_keys
189
+ .slice(0, 20)
190
+ .map((key) => safeDiagnosticText(key, 100));
191
+ }
192
+ const stack = safeDiagnosticStack(error?.stack);
193
+ if (stack) diagnostic.stack = stack;
194
+ if (depth < 2 && error?.cause && error.cause !== error) {
195
+ diagnostic.cause = buildErrorDiagnostic(error.cause, {
196
+ depth: depth + 1,
197
+ seen
198
+ });
199
+ }
200
+ return diagnostic;
144
201
  }
145
202
 
146
- async function waitForRecommendRecoverySettle(client, {
147
- reloadSettleMs = 8000,
148
- timeoutMs = 90000
149
- } = {}) {
150
- return waitForMiniFreshStartSettle(client, {
151
- domain: "recommend",
152
- timeoutMs,
153
- intervalMs: reloadSettleMs > 10000 ? 1200 : 800,
154
- settleMs: Math.max(0, Math.min(reloadSettleMs || 0, 5000)),
155
- selfHealConfig: buildRecommendSelfHealConfig(),
156
- resolveSelfHealRoots: resolveRecommendSelfHealRoots
157
- });
158
- }
159
-
160
- async function waitForFreshRecommendRoots(client, {
161
- timeoutMs = 10000,
162
- intervalMs = 500
163
- } = {}) {
164
- const rootState = await waitForRecommendRoots(client, {
165
- timeoutMs,
166
- intervalMs
167
- });
168
- return rootState?.iframe?.documentNodeId ? rootState : null;
203
+ export function compactRecommendRefreshErrorDiagnostic(error) {
204
+ return buildErrorDiagnostic(error);
169
205
  }
170
206
 
171
- export async function selectRecommendJobWithRootRefresh(client, rootState, {
172
- jobLabel = "",
173
- settleMs = 6000,
174
- dropdownTimeoutMs = 4000,
175
- totalTimeoutMs = 30000,
176
- retryDelayMs = 1000
177
- } = {}) {
178
- const started = Date.now();
179
- const attempts = [];
180
- let currentRootState = rootState || null;
181
- let lastError = null;
182
- let attempt = 0;
183
-
184
- while (Date.now() - started <= totalTimeoutMs) {
185
- attempt += 1;
186
- if (!currentRootState?.iframe?.documentNodeId) {
187
- currentRootState = await waitForFreshRecommendRoots(client, {
188
- timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
189
- intervalMs: 500
190
- });
191
- }
192
- const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
193
- try {
194
- const selection = await selectRecommendJob(client, iframeDocumentNodeId, {
195
- jobLabel,
196
- settleMs,
197
- dropdownTimeoutMs
198
- });
199
- if (selection.selected) {
200
- const stickyRootState = await waitForFreshRecommendRoots(client, {
201
- timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
202
- intervalMs: 500
203
- }) || currentRootState;
204
- const stickyFrameNodeId = stickyRootState?.iframe?.documentNodeId || iframeDocumentNodeId;
205
- const stickyVerification = await verifyRecommendJobSelection(client, stickyFrameNodeId, {
206
- jobLabel,
207
- delayMs: 2000,
208
- dropdownTimeoutMs,
209
- closeSettleMs: 300
210
- });
211
- selection.sticky_verification = stickyVerification;
212
- currentRootState = stickyRootState || currentRootState;
213
- if (!stickyVerification.verified) {
214
- const stickyError = new Error(`Recommend job selection was not sticky after 2s: requested=${jobLabel}; current=${stickyVerification.current_label_without_salary || stickyVerification.current_label || "unknown"}`);
215
- stickyError.sticky_verification = stickyVerification;
216
- throw stickyError;
217
- }
218
- if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
219
- const closeError = new Error(`Recommend job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
220
- closeError.sticky_verification = stickyVerification;
221
- throw closeError;
222
- }
223
- }
224
- attempts.push(compactJobSelectionAttempt({
225
- ok: true,
226
- attempt,
227
- iframeDocumentNodeId,
228
- selection
229
- }));
230
- return {
231
- job_selection: {
232
- ...selection,
233
- refresh_attempts: attempts
234
- },
235
- root_state: currentRootState,
236
- attempts
237
- };
238
- } catch (error) {
239
- lastError = error;
240
- attempts.push(compactJobSelectionAttempt({
241
- ok: false,
242
- attempt,
243
- iframeDocumentNodeId,
244
- error
245
- }));
246
- if (!isRetryableRecommendJobSelectionError(error) || Date.now() - started >= totalTimeoutMs) {
247
- break;
248
- }
249
- if (retryDelayMs > 0) await sleep(retryDelayMs);
250
- currentRootState = await waitForFreshRecommendRoots(client, {
251
- timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
252
- intervalMs: 500
253
- });
254
- }
207
+ export function isRetryableRecommendFilterReapplyError(error) {
208
+ const messages = [];
209
+ const seen = new Set();
210
+ let current = error;
211
+ for (let depth = 0; current && depth < 4; depth += 1) {
212
+ if ((typeof current === "object" || typeof current === "function") && seen.has(current)) break;
213
+ if (typeof current === "object" || typeof current === "function") seen.add(current);
214
+ if (isStaleRecommendNodeError(current)) return true;
215
+ messages.push(String(current?.message || current || ""));
216
+ current = current?.cause;
255
217
  }
256
-
257
- const wrapped = new Error(lastError?.message || "Recommend job selection failed after refresh reload");
218
+ const message = messages.join("\n");
219
+ return /Recommend filter panel did not open|Recommend filter trigger was not found|Recommend filter confirm button was not found|No matching recommend filter option|Invalid (?:backend )?node(?:\s*id)?|Node with given id does not exist|No node found for given backend id/i.test(message);
220
+ }
221
+
222
+ function compactFilterReapplyError(error) {
223
+ return error?.message || String(error || "Recommend filter reapply failed");
224
+ }
225
+
226
+ export function isRetryableRecommendJobSelectionError(error) {
227
+ if (isStaleRecommendNodeError(error)) return true;
228
+ const message = String(error?.message || error || "");
229
+ return /Recommend job trigger was not found|Recommend job dropdown did not mount options|Recommend job dropdown did not expose visible options|Matched recommend job has no clickable center|Matched recommend job has no visible clickable option|Recommend job selection was not sticky|Recommend job dropdown remained open after sticky verification/i.test(message);
230
+ }
231
+
232
+ function compactJobSelectionAttempt({
233
+ ok = false,
234
+ attempt = 0,
235
+ iframeDocumentNodeId = 0,
236
+ error = null,
237
+ selection = null
238
+ } = {}) {
239
+ return {
240
+ ok: Boolean(ok),
241
+ method: "job_select",
242
+ reason: error ? "job_select_failed" : null,
243
+ error: error ? (error?.message || String(error)) : null,
244
+ error_diagnostic: error ? compactRecommendRefreshErrorDiagnostic(error) : null,
245
+ attempt,
246
+ iframe_document_node_id: iframeDocumentNodeId || 0,
247
+ selected: Boolean(selection?.selected),
248
+ selection_reason: selection?.reason || null,
249
+ sticky_verified: selection?.sticky_verification?.verified ?? null,
250
+ sticky_current_label: selection?.sticky_verification?.current_label_without_salary
251
+ || selection?.sticky_verification?.current_label
252
+ || null,
253
+ sticky_menu_closed: selection?.sticky_verification?.menu_close?.ok ?? null
254
+ };
255
+ }
256
+
257
+ async function waitForRecommendRecoverySettle(client, {
258
+ reloadSettleMs = 8000,
259
+ timeoutMs = 90000
260
+ } = {}) {
261
+ return waitForMiniFreshStartSettle(client, {
262
+ domain: "recommend",
263
+ timeoutMs,
264
+ intervalMs: reloadSettleMs > 10000 ? 1200 : 800,
265
+ settleMs: Math.max(0, Math.min(reloadSettleMs || 0, 5000)),
266
+ selfHealConfig: buildRecommendSelfHealConfig(),
267
+ resolveSelfHealRoots: resolveRecommendSelfHealRoots
268
+ });
269
+ }
270
+
271
+ async function waitForFreshRecommendRoots(client, {
272
+ timeoutMs = 10000,
273
+ intervalMs = 500
274
+ } = {}) {
275
+ const rootState = await waitForRecommendRoots(client, {
276
+ timeoutMs,
277
+ intervalMs
278
+ });
279
+ return rootState?.iframe?.documentNodeId ? rootState : null;
280
+ }
281
+
282
+ export async function selectRecommendJobWithRootRefresh(client, rootState, {
283
+ jobLabel = "",
284
+ settleMs = 6000,
285
+ dropdownTimeoutMs = 4000,
286
+ totalTimeoutMs = 30000,
287
+ retryDelayMs = 1000
288
+ } = {}) {
289
+ const started = Date.now();
290
+ const attempts = [];
291
+ let currentRootState = rootState || null;
292
+ let lastError = null;
293
+ let attempt = 0;
294
+
295
+ while (Date.now() - started <= totalTimeoutMs) {
296
+ attempt += 1;
297
+ if (!currentRootState?.iframe?.documentNodeId) {
298
+ currentRootState = await waitForFreshRecommendRoots(client, {
299
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
300
+ intervalMs: 500
301
+ });
302
+ }
303
+ const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
304
+ try {
305
+ const selection = await selectRecommendJob(client, iframeDocumentNodeId, {
306
+ jobLabel,
307
+ settleMs,
308
+ dropdownTimeoutMs
309
+ });
310
+ if (selection.selected) {
311
+ const stickyRootState = await waitForFreshRecommendRoots(client, {
312
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
313
+ intervalMs: 500
314
+ }) || currentRootState;
315
+ const stickyFrameNodeId = stickyRootState?.iframe?.documentNodeId || iframeDocumentNodeId;
316
+ const stickyVerification = await verifyRecommendJobSelection(client, stickyFrameNodeId, {
317
+ jobLabel,
318
+ delayMs: 2000,
319
+ dropdownTimeoutMs,
320
+ closeSettleMs: 300
321
+ });
322
+ selection.sticky_verification = stickyVerification;
323
+ currentRootState = stickyRootState || currentRootState;
324
+ if (!stickyVerification.verified) {
325
+ const stickyError = new Error(`Recommend job selection was not sticky after 2s: requested=${jobLabel}; current=${stickyVerification.current_label_without_salary || stickyVerification.current_label || "unknown"}`);
326
+ stickyError.sticky_verification = stickyVerification;
327
+ throw stickyError;
328
+ }
329
+ if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
330
+ const closeError = new Error(`Recommend job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
331
+ closeError.sticky_verification = stickyVerification;
332
+ throw closeError;
333
+ }
334
+ }
335
+ attempts.push(compactJobSelectionAttempt({
336
+ ok: true,
337
+ attempt,
338
+ iframeDocumentNodeId,
339
+ selection
340
+ }));
341
+ return {
342
+ job_selection: {
343
+ ...selection,
344
+ refresh_attempts: attempts
345
+ },
346
+ root_state: currentRootState,
347
+ attempts
348
+ };
349
+ } catch (error) {
350
+ lastError = error;
351
+ attempts.push(compactJobSelectionAttempt({
352
+ ok: false,
353
+ attempt,
354
+ iframeDocumentNodeId,
355
+ error
356
+ }));
357
+ if (!isRetryableRecommendJobSelectionError(error) || Date.now() - started >= totalTimeoutMs) {
358
+ break;
359
+ }
360
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
361
+ currentRootState = await waitForFreshRecommendRoots(client, {
362
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
363
+ intervalMs: 500
364
+ });
365
+ }
366
+ }
367
+
368
+ const wrapped = new Error(lastError?.message || "Recommend job selection failed after refresh reload");
369
+ wrapped.cause = lastError;
370
+ wrapped.job_selection_attempts = attempts;
371
+ throw wrapped;
372
+ }
373
+
374
+ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
375
+ maxAttempts = 3,
376
+ retryDelayMs = 1500
377
+ } = {}) {
378
+ const attempts = [];
379
+ let currentRootState = rootState;
380
+ let lastError = null;
381
+
382
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
383
+ try {
384
+ const filter = await selectAndConfirmFirstSafeFilter(
385
+ client,
386
+ currentRootState.iframe.documentNodeId,
387
+ filterOptions
388
+ );
389
+ attempts.push({
390
+ ok: true,
391
+ method: "filter_reapply",
392
+ attempt
393
+ });
394
+ return {
395
+ filter,
396
+ root_state: currentRootState,
397
+ attempts
398
+ };
399
+ } catch (error) {
400
+ lastError = error;
401
+ attempts.push({
402
+ ok: false,
403
+ method: "filter_reapply",
404
+ reason: "filter_reapply_failed",
405
+ error: compactFilterReapplyError(error),
406
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
407
+ attempt
408
+ });
409
+ if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
410
+ break;
411
+ }
412
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
413
+ currentRootState = await getRecommendRoots(client);
414
+ }
415
+ }
416
+
417
+ const wrapped = new Error(compactFilterReapplyError(lastError));
258
418
  wrapped.cause = lastError;
259
- wrapped.job_selection_attempts = attempts;
419
+ wrapped.filter_reapply_attempts = attempts;
260
420
  throw wrapped;
261
421
  }
262
422
 
263
- async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
423
+ function compactCurrentCityOnlyReapplyError(error) {
424
+ return error?.message || String(error || "Recommend current-city-only reapply failed");
425
+ }
426
+
427
+ function isRetryableRecommendCurrentCityOnlyReapplyError(error) {
428
+ if (isStaleRecommendNodeError(error)) return true;
429
+ const message = compactCurrentCityOnlyReapplyError(error);
430
+ return /current-city|current city|location|popover|trigger|checkbox|stale|did not (?:open|mount|close)/i.test(message)
431
+ && !/unavailable/i.test(message);
432
+ }
433
+
434
+ async function ensureRefreshCurrentCityOnly(client, rootState, {
435
+ enabled = false,
264
436
  maxAttempts = 3,
265
437
  retryDelayMs = 1500
266
438
  } = {}) {
@@ -270,18 +442,19 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
270
442
 
271
443
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
272
444
  try {
273
- const filter = await selectAndConfirmFirstSafeFilter(
445
+ const result = await ensureRecommendCurrentCityOnly(
274
446
  client,
275
447
  currentRootState.iframe.documentNodeId,
276
- filterOptions
448
+ { enabled }
277
449
  );
278
450
  attempts.push({
279
451
  ok: true,
280
- method: "filter_reapply",
281
- attempt
452
+ method: "current_city_only_reapply",
453
+ attempt,
454
+ result
282
455
  });
283
456
  return {
284
- filter,
457
+ current_city_only: result,
285
458
  root_state: currentRootState,
286
459
  attempts
287
460
  };
@@ -289,12 +462,13 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
289
462
  lastError = error;
290
463
  attempts.push({
291
464
  ok: false,
292
- method: "filter_reapply",
293
- reason: "filter_reapply_failed",
294
- error: compactFilterReapplyError(error),
465
+ method: "current_city_only_reapply",
466
+ reason: "current_city_only_reapply_failed",
467
+ error: compactCurrentCityOnlyReapplyError(error),
468
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
295
469
  attempt
296
470
  });
297
- if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
471
+ if (attempt >= maxAttempts || !isRetryableRecommendCurrentCityOnlyReapplyError(error)) {
298
472
  break;
299
473
  }
300
474
  if (retryDelayMs > 0) await sleep(retryDelayMs);
@@ -302,9 +476,9 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
302
476
  }
303
477
  }
304
478
 
305
- const wrapped = new Error(compactFilterReapplyError(lastError));
479
+ const wrapped = new Error(compactCurrentCityOnlyReapplyError(lastError));
306
480
  wrapped.cause = lastError;
307
- wrapped.filter_reapply_attempts = attempts;
481
+ wrapped.current_city_only_attempts = attempts;
308
482
  throw wrapped;
309
483
  }
310
484
 
@@ -313,174 +487,215 @@ async function applyRefreshMethod(client, method, {
313
487
  pageScope = "recommend",
314
488
  fallbackPageScope = "recommend",
315
489
  filter = {},
316
- targetUrl = RECOMMEND_TARGET_URL,
317
- forceRecentNotView = true,
318
- cardTimeoutMs = 30000,
319
- reloadSettleMs = 8000
320
- } = {}) {
490
+ targetUrl = RECOMMEND_TARGET_URL,
491
+ forceRecentNotView = true,
492
+ cardTimeoutMs = 30000,
493
+ reloadSettleMs = 8000
494
+ } = {}) {
321
495
  const started = Date.now();
322
496
  let currentRootState = null;
323
497
  let jobSelection = null;
324
498
  let jobSelectionAttempts = [];
325
499
  let pageScopeResult = null;
500
+ let currentCityOnlyResult = null;
501
+ let currentCityOnlyAttempts = [];
326
502
  let filterResult = null;
327
503
  let filterReapplyAttempts = [];
328
504
  let recoverySettle = null;
329
505
  try {
330
506
  if (method === "page_navigate") {
331
- await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
332
- } else {
333
- await client.Page.reload({ ignoreCache: true });
334
- }
335
- recoverySettle = await waitForRecommendRecoverySettle(client, {
336
- reloadSettleMs,
337
- timeoutMs: Math.max(45000, reloadSettleMs * 6)
338
- });
339
- if (!recoverySettle.ok) {
340
- throw createRecoverySettleError("recommend", recoverySettle);
341
- }
342
- currentRootState = await waitForRecommendRoots(client, {
343
- timeoutMs: Math.max(45000, reloadSettleMs * 6),
344
- intervalMs: 500
345
- });
346
- if (!currentRootState?.iframe?.documentNodeId) {
347
- throw new Error("Recommend iframe was not ready after refresh reload");
348
- }
349
- if (jobLabel) {
350
- const jobSelectionResult = await selectRecommendJobWithRootRefresh(client, currentRootState, {
351
- jobLabel,
352
- settleMs: reloadSettleMs > 10000 ? 12000 : 6000,
353
- dropdownTimeoutMs: 4000,
354
- totalTimeoutMs: reloadSettleMs > 10000 ? 45000 : 30000,
355
- retryDelayMs: 1200
356
- });
357
- jobSelection = jobSelectionResult.job_selection;
358
- jobSelectionAttempts = jobSelectionResult.attempts;
359
- if (!jobSelection.selected) {
360
- throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
361
- }
362
- currentRootState = jobSelectionResult.root_state || await getRecommendRoots(client);
363
- }
364
- pageScopeResult = await selectRecommendPageScope(
365
- client,
366
- currentRootState.iframe.documentNodeId,
367
- {
368
- pageScope,
369
- fallbackScope: fallbackPageScope,
370
- settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
371
- timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
372
- }
507
+ await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
508
+ } else {
509
+ await client.Page.reload({ ignoreCache: true });
510
+ }
511
+ recoverySettle = await waitForRecommendRecoverySettle(client, {
512
+ reloadSettleMs,
513
+ timeoutMs: Math.max(45000, reloadSettleMs * 6)
514
+ });
515
+ if (!recoverySettle.ok) {
516
+ throw createRecoverySettleError("recommend", recoverySettle);
517
+ }
518
+ currentRootState = await waitForRecommendRoots(client, {
519
+ timeoutMs: Math.max(45000, reloadSettleMs * 6),
520
+ intervalMs: 500
521
+ });
522
+ if (!currentRootState?.iframe?.documentNodeId) {
523
+ throw new Error("Recommend iframe was not ready after refresh reload");
524
+ }
525
+ if (jobLabel) {
526
+ const jobSelectionResult = await selectRecommendJobWithRootRefresh(client, currentRootState, {
527
+ jobLabel,
528
+ settleMs: reloadSettleMs > 10000 ? 12000 : 6000,
529
+ dropdownTimeoutMs: 4000,
530
+ totalTimeoutMs: reloadSettleMs > 10000 ? 45000 : 30000,
531
+ retryDelayMs: 1200
532
+ });
533
+ jobSelection = jobSelectionResult.job_selection;
534
+ jobSelectionAttempts = jobSelectionResult.attempts;
535
+ if (!jobSelection.selected) {
536
+ throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
537
+ }
538
+ currentRootState = jobSelectionResult.root_state || await getRecommendRoots(client);
539
+ }
540
+ pageScopeResult = await selectRecommendPageScope(
541
+ client,
542
+ currentRootState.iframe.documentNodeId,
543
+ {
544
+ pageScope,
545
+ fallbackScope: fallbackPageScope,
546
+ settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
547
+ timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
548
+ }
373
549
  );
374
550
  if (!pageScopeResult.selected) {
375
551
  throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
376
552
  }
377
553
  currentRootState = await getRecommendRoots(client);
378
- const filterSelection = await selectAndConfirmRefreshFilter(
379
- client,
380
- currentRootState,
381
- buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
382
- {
383
- retryDelayMs: Math.max(1200, Math.min(5000, Math.floor((reloadSettleMs || 8000) / 2)))
554
+ const retryDelayMs = Math.max(1200, Math.min(5000, Math.floor((reloadSettleMs || 8000) / 2)));
555
+ const filterStages = await applyRecommendFilterEnvelopeStages(filter, {
556
+ applyCurrentCityOnly: async () => {
557
+ const selection = await ensureRefreshCurrentCityOnly(client, currentRootState, {
558
+ enabled: filter.currentCityOnly === true || filter.current_city_only === true,
559
+ retryDelayMs
560
+ });
561
+ currentCityOnlyAttempts = selection.attempts;
562
+ currentRootState = await getRecommendRoots(client);
563
+ return selection.current_city_only;
564
+ },
565
+ applyFilterPanel: async () => {
566
+ const selection = await selectAndConfirmRefreshFilter(
567
+ client,
568
+ currentRootState,
569
+ buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
570
+ { retryDelayMs }
571
+ );
572
+ filterReapplyAttempts = selection.attempts;
573
+ currentRootState = await getRecommendRoots(client);
574
+ return selection.filter;
384
575
  }
385
- );
386
- filterResult = filterSelection.filter;
387
- filterReapplyAttempts = filterSelection.attempts;
388
- currentRootState = await getRecommendRoots(client);
576
+ });
577
+ currentCityOnlyResult = filterStages.current_city_only;
578
+ filterResult = filterStages.filter;
389
579
  const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
390
580
  timeoutMs: cardTimeoutMs,
391
581
  intervalMs: 500
392
582
  });
393
583
  if (!cardNodeIds.length) {
394
- throw new Error("No recommend candidate cards were found after refresh reload");
395
- }
396
- return {
397
- ok: true,
398
- method,
584
+ throw new Error("No recommend candidate cards were found after refresh reload");
585
+ }
586
+ return {
587
+ ok: true,
588
+ method,
399
589
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
400
590
  job_selection: jobSelection,
401
591
  job_selection_attempts: jobSelectionAttempts,
402
592
  recovery_settle: recoverySettle,
403
593
  page_scope: pageScopeResult,
594
+ current_city_only: currentCityOnlyResult,
595
+ current_city_only_attempts: currentCityOnlyAttempts,
404
596
  filter: filterResult,
405
597
  filter_reapply_attempts: filterReapplyAttempts,
406
598
  card_count: cardNodeIds.length,
407
599
  root_state: currentRootState,
408
- forced_recent_not_view: forceRecentNotView,
600
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
409
601
  elapsed_ms: Date.now() - started
410
602
  };
411
603
  } catch (error) {
412
604
  return {
413
605
  ok: false,
414
- method,
606
+ method,
415
607
  reason: refreshFailureReason(method),
416
608
  error: error?.message || String(error),
609
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
417
610
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
418
611
  job_selection: jobSelection,
419
612
  job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
420
613
  recovery_settle: error?.recovery_settle || recoverySettle,
421
614
  page_scope: pageScopeResult,
615
+ current_city_only: currentCityOnlyResult,
616
+ current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
422
617
  filter: filterResult,
423
618
  filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
424
619
  card_count: 0,
425
620
  root_state: currentRootState,
426
- forced_recent_not_view: forceRecentNotView,
621
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
427
622
  elapsed_ms: Date.now() - started
428
623
  };
429
624
  }
430
625
  }
431
626
 
432
- export async function refreshRecommendListAtEnd(client, {
433
- rootState = null,
434
- jobLabel = "",
435
- pageScope = "recommend",
436
- fallbackPageScope = "recommend",
437
- filter = {},
438
- preferEndRefreshButton = true,
439
- forceNavigate = false,
440
- targetUrl = RECOMMEND_TARGET_URL,
441
- forceRecentNotView = true,
442
- cardTimeoutMs = 30000,
443
- buttonSettleMs = 8000,
444
- reloadSettleMs = 8000
445
- } = {}) {
446
- const attempts = [];
447
- let currentRootState = rootState || null;
448
-
449
- if (preferEndRefreshButton) {
450
- currentRootState = currentRootState || await getRecommendRoots(client);
451
- const buttonResult = await clickRecommendEndRefreshButton(
452
- client,
627
+ export async function refreshRecommendListAtEnd(client, {
628
+ rootState = null,
629
+ jobLabel = "",
630
+ pageScope = "recommend",
631
+ fallbackPageScope = "recommend",
632
+ filter = {},
633
+ preferEndRefreshButton = true,
634
+ forceNavigate = false,
635
+ targetUrl = RECOMMEND_TARGET_URL,
636
+ forceRecentNotView = true,
637
+ cardTimeoutMs = 30000,
638
+ buttonSettleMs = 8000,
639
+ reloadSettleMs = 8000
640
+ } = {}) {
641
+ const attempts = [];
642
+ let currentRootState = rootState || null;
643
+
644
+ if (preferEndRefreshButton) {
645
+ currentRootState = currentRootState || await getRecommendRoots(client);
646
+ const buttonResult = await clickRecommendEndRefreshButton(
647
+ client,
453
648
  currentRootState.iframe.documentNodeId,
454
649
  { settleMs: buttonSettleMs }
455
650
  );
456
651
  attempts.push(buttonResult);
457
652
  if (buttonResult.ok) {
653
+ let pageScopeResult = null;
654
+ let currentCityOnlyResult = null;
655
+ let currentCityOnlyAttempts = [];
656
+ let filterResult = null;
657
+ let filterReapplyAttempts = [];
458
658
  try {
459
659
  currentRootState = await getRecommendRoots(client);
460
- const pageScopeResult = await selectRecommendPageScope(
660
+ pageScopeResult = await selectRecommendPageScope(
461
661
  client,
462
662
  currentRootState.iframe.documentNodeId,
463
663
  {
464
664
  pageScope,
465
665
  fallbackScope: fallbackPageScope,
466
- settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
467
- timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
468
- }
666
+ settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
667
+ timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
668
+ }
469
669
  );
470
670
  if (!pageScopeResult.selected) {
471
671
  throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
472
672
  }
473
673
  currentRootState = await getRecommendRoots(client);
474
- const filterSelection = await selectAndConfirmRefreshFilter(
475
- client,
476
- currentRootState,
477
- buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
478
- {
479
- retryDelayMs: Math.max(1200, Math.min(5000, Math.floor((buttonSettleMs || 8000) / 2)))
674
+ const retryDelayMs = Math.max(1200, Math.min(5000, Math.floor((buttonSettleMs || 8000) / 2)));
675
+ const filterStages = await applyRecommendFilterEnvelopeStages(filter, {
676
+ applyCurrentCityOnly: async () => {
677
+ const selection = await ensureRefreshCurrentCityOnly(client, currentRootState, {
678
+ enabled: filter.currentCityOnly === true || filter.current_city_only === true,
679
+ retryDelayMs
680
+ });
681
+ currentCityOnlyAttempts = selection.attempts;
682
+ currentRootState = await getRecommendRoots(client);
683
+ return selection.current_city_only;
684
+ },
685
+ applyFilterPanel: async () => {
686
+ const selection = await selectAndConfirmRefreshFilter(
687
+ client,
688
+ currentRootState,
689
+ buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
690
+ { retryDelayMs }
691
+ );
692
+ filterReapplyAttempts = selection.attempts;
693
+ currentRootState = await getRecommendRoots(client);
694
+ return selection.filter;
480
695
  }
481
- );
482
- const filterResult = filterSelection.filter;
483
- currentRootState = await getRecommendRoots(client);
696
+ });
697
+ currentCityOnlyResult = filterStages.current_city_only;
698
+ filterResult = filterStages.filter;
484
699
  const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
485
700
  timeoutMs: cardTimeoutMs,
486
701
  intervalMs: 500
@@ -490,65 +705,74 @@ export async function refreshRecommendListAtEnd(client, {
490
705
  method: "end_refresh_button",
491
706
  attempts,
492
707
  page_scope: pageScopeResult,
708
+ current_city_only: currentCityOnlyResult,
709
+ current_city_only_attempts: currentCityOnlyAttempts,
493
710
  filter: filterResult,
494
- filter_reapply_attempts: filterSelection.attempts,
711
+ filter_reapply_attempts: filterReapplyAttempts,
495
712
  card_count: cardNodeIds.length,
496
713
  root_state: currentRootState,
497
- forced_recent_not_view: forceRecentNotView
714
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
498
715
  };
499
716
  } catch (error) {
500
717
  attempts.push({
501
718
  ok: false,
502
719
  method: "end_refresh_button_after_click",
503
720
  reason: "end_refresh_reapply_failed",
504
- error: error?.message || String(error)
721
+ error: error?.message || String(error),
722
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
723
+ page_scope: pageScopeResult,
724
+ current_city_only: currentCityOnlyResult,
725
+ current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
726
+ filter: filterResult,
727
+ filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
728
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
505
729
  });
506
730
  }
507
731
  }
508
732
  }
509
733
 
510
- const fallbackMethods = [];
511
- if (forceNavigate && typeof client?.Page?.navigate === "function") {
512
- fallbackMethods.push("page_navigate");
513
- }
514
- if (typeof client?.Page?.reload === "function") {
515
- fallbackMethods.push("page_reload");
516
- }
517
- if (!fallbackMethods.length) {
518
- fallbackMethods.push("page_reload");
519
- }
520
-
521
- let lastRefreshResult = null;
522
- for (const method of fallbackMethods) {
523
- const refreshResult = await applyRefreshMethod(client, method, {
524
- jobLabel,
525
- pageScope,
526
- fallbackPageScope,
527
- filter,
528
- targetUrl,
529
- forceRecentNotView,
530
- cardTimeoutMs,
531
- reloadSettleMs
532
- });
533
- if (refreshResult.ok) {
534
- return {
535
- ...refreshResult,
536
- attempts
537
- };
538
- }
539
- attempts.push(refreshResult);
540
- lastRefreshResult = refreshResult;
541
- }
542
-
543
- return {
544
- ...(lastRefreshResult || {
545
- ok: false,
734
+ const fallbackMethods = [];
735
+ if (forceNavigate && typeof client?.Page?.navigate === "function") {
736
+ fallbackMethods.push("page_navigate");
737
+ }
738
+ if (typeof client?.Page?.reload === "function") {
739
+ fallbackMethods.push("page_reload");
740
+ }
741
+ if (!fallbackMethods.length) {
742
+ fallbackMethods.push("page_reload");
743
+ }
744
+
745
+ let lastRefreshResult = null;
746
+ for (const method of fallbackMethods) {
747
+ const refreshResult = await applyRefreshMethod(client, method, {
748
+ jobLabel,
749
+ pageScope,
750
+ fallbackPageScope,
751
+ filter,
752
+ targetUrl,
753
+ forceRecentNotView,
754
+ cardTimeoutMs,
755
+ reloadSettleMs
756
+ });
757
+ if (refreshResult.ok) {
758
+ return {
759
+ ...refreshResult,
760
+ attempts
761
+ };
762
+ }
763
+ attempts.push(refreshResult);
764
+ lastRefreshResult = refreshResult;
765
+ }
766
+
767
+ return {
768
+ ...(lastRefreshResult || {
769
+ ok: false,
546
770
  method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
547
771
  reason: "refresh_failed",
548
772
  error: "Recommend refresh did not run",
549
773
  card_count: 0,
550
774
  root_state: currentRootState,
551
- forced_recent_not_view: forceRecentNotView
775
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
552
776
  }),
553
777
  attempts
554
778
  };