@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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 +8 -8
  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 +2419 -2415
  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 +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  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 +650 -650
  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 +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  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 +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,266 +1,360 @@
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";
105
- }
106
-
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);
110
- }
111
-
112
- function compactFilterReapplyError(error) {
113
- return error?.message || String(error || "Recommend filter reapply failed");
114
- }
115
-
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);
120
- }
121
-
122
- function compactJobSelectionAttempt({
123
- ok = false,
124
- attempt = 0,
125
- iframeDocumentNodeId = 0,
126
- error = null,
127
- selection = null
112
+ export async function applyRecommendFilterEnvelopeStages(filter = {}, {
113
+ applyCurrentCityOnly,
114
+ applyFilterPanel
128
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
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
130
+ applied: true,
131
+ skipped: false,
132
+ current_city_only: currentCityOnlyResult,
133
+ filter: filterResult
143
134
  };
144
135
  }
145
136
 
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
- });
137
+ function refreshFailureReason(method = "") {
138
+ return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
139
+ }
140
+
141
+ export function isRetryableRecommendFilterReapplyError(error) {
142
+ const message = String(error?.message || error || "");
143
+ 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);
144
+ }
145
+
146
+ function compactFilterReapplyError(error) {
147
+ return error?.message || String(error || "Recommend filter reapply failed");
148
+ }
149
+
150
+ export function isRetryableRecommendJobSelectionError(error) {
151
+ if (isStaleRecommendNodeError(error)) return true;
152
+ const message = String(error?.message || error || "");
153
+ 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);
154
+ }
155
+
156
+ function compactJobSelectionAttempt({
157
+ ok = false,
158
+ attempt = 0,
159
+ iframeDocumentNodeId = 0,
160
+ error = null,
161
+ selection = null
162
+ } = {}) {
163
+ return {
164
+ ok: Boolean(ok),
165
+ method: "job_select",
166
+ reason: error ? "job_select_failed" : null,
167
+ error: error ? (error?.message || String(error)) : null,
168
+ attempt,
169
+ iframe_document_node_id: iframeDocumentNodeId || 0,
170
+ selected: Boolean(selection?.selected),
171
+ selection_reason: selection?.reason || null,
172
+ sticky_verified: selection?.sticky_verification?.verified ?? null,
173
+ sticky_current_label: selection?.sticky_verification?.current_label_without_salary
174
+ || selection?.sticky_verification?.current_label
175
+ || null,
176
+ sticky_menu_closed: selection?.sticky_verification?.menu_close?.ok ?? null
177
+ };
178
+ }
179
+
180
+ async function waitForRecommendRecoverySettle(client, {
181
+ reloadSettleMs = 8000,
182
+ timeoutMs = 90000
183
+ } = {}) {
184
+ return waitForMiniFreshStartSettle(client, {
185
+ domain: "recommend",
186
+ timeoutMs,
187
+ intervalMs: reloadSettleMs > 10000 ? 1200 : 800,
188
+ settleMs: Math.max(0, Math.min(reloadSettleMs || 0, 5000)),
189
+ selfHealConfig: buildRecommendSelfHealConfig(),
190
+ resolveSelfHealRoots: resolveRecommendSelfHealRoots
191
+ });
192
+ }
193
+
194
+ async function waitForFreshRecommendRoots(client, {
195
+ timeoutMs = 10000,
196
+ intervalMs = 500
197
+ } = {}) {
198
+ const rootState = await waitForRecommendRoots(client, {
199
+ timeoutMs,
200
+ intervalMs
201
+ });
202
+ return rootState?.iframe?.documentNodeId ? rootState : null;
203
+ }
204
+
205
+ export async function selectRecommendJobWithRootRefresh(client, rootState, {
206
+ jobLabel = "",
207
+ settleMs = 6000,
208
+ dropdownTimeoutMs = 4000,
209
+ totalTimeoutMs = 30000,
210
+ retryDelayMs = 1000
211
+ } = {}) {
212
+ const started = Date.now();
213
+ const attempts = [];
214
+ let currentRootState = rootState || null;
215
+ let lastError = null;
216
+ let attempt = 0;
217
+
218
+ while (Date.now() - started <= totalTimeoutMs) {
219
+ attempt += 1;
220
+ if (!currentRootState?.iframe?.documentNodeId) {
221
+ currentRootState = await waitForFreshRecommendRoots(client, {
222
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
223
+ intervalMs: 500
224
+ });
225
+ }
226
+ const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
227
+ try {
228
+ const selection = await selectRecommendJob(client, iframeDocumentNodeId, {
229
+ jobLabel,
230
+ settleMs,
231
+ dropdownTimeoutMs
232
+ });
233
+ if (selection.selected) {
234
+ const stickyRootState = await waitForFreshRecommendRoots(client, {
235
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
236
+ intervalMs: 500
237
+ }) || currentRootState;
238
+ const stickyFrameNodeId = stickyRootState?.iframe?.documentNodeId || iframeDocumentNodeId;
239
+ const stickyVerification = await verifyRecommendJobSelection(client, stickyFrameNodeId, {
240
+ jobLabel,
241
+ delayMs: 2000,
242
+ dropdownTimeoutMs,
243
+ closeSettleMs: 300
244
+ });
245
+ selection.sticky_verification = stickyVerification;
246
+ currentRootState = stickyRootState || currentRootState;
247
+ if (!stickyVerification.verified) {
248
+ const stickyError = new Error(`Recommend job selection was not sticky after 2s: requested=${jobLabel}; current=${stickyVerification.current_label_without_salary || stickyVerification.current_label || "unknown"}`);
249
+ stickyError.sticky_verification = stickyVerification;
250
+ throw stickyError;
251
+ }
252
+ if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
253
+ const closeError = new Error(`Recommend job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
254
+ closeError.sticky_verification = stickyVerification;
255
+ throw closeError;
256
+ }
257
+ }
258
+ attempts.push(compactJobSelectionAttempt({
259
+ ok: true,
260
+ attempt,
261
+ iframeDocumentNodeId,
262
+ selection
263
+ }));
264
+ return {
265
+ job_selection: {
266
+ ...selection,
267
+ refresh_attempts: attempts
268
+ },
269
+ root_state: currentRootState,
270
+ attempts
271
+ };
272
+ } catch (error) {
273
+ lastError = error;
274
+ attempts.push(compactJobSelectionAttempt({
275
+ ok: false,
276
+ attempt,
277
+ iframeDocumentNodeId,
278
+ error
279
+ }));
280
+ if (!isRetryableRecommendJobSelectionError(error) || Date.now() - started >= totalTimeoutMs) {
281
+ break;
282
+ }
283
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
284
+ currentRootState = await waitForFreshRecommendRoots(client, {
285
+ timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
286
+ intervalMs: 500
287
+ });
288
+ }
289
+ }
290
+
291
+ const wrapped = new Error(lastError?.message || "Recommend job selection failed after refresh reload");
292
+ wrapped.cause = lastError;
293
+ wrapped.job_selection_attempts = attempts;
294
+ throw wrapped;
295
+ }
296
+
297
+ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
298
+ maxAttempts = 3,
299
+ retryDelayMs = 1500
300
+ } = {}) {
301
+ const attempts = [];
302
+ let currentRootState = rootState;
303
+ let lastError = null;
304
+
305
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
306
+ try {
307
+ const filter = await selectAndConfirmFirstSafeFilter(
308
+ client,
309
+ currentRootState.iframe.documentNodeId,
310
+ filterOptions
311
+ );
312
+ attempts.push({
313
+ ok: true,
314
+ method: "filter_reapply",
315
+ attempt
316
+ });
317
+ return {
318
+ filter,
319
+ root_state: currentRootState,
320
+ attempts
321
+ };
322
+ } catch (error) {
323
+ lastError = error;
324
+ attempts.push({
325
+ ok: false,
326
+ method: "filter_reapply",
327
+ reason: "filter_reapply_failed",
328
+ error: compactFilterReapplyError(error),
329
+ attempt
330
+ });
331
+ if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
332
+ break;
333
+ }
334
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
335
+ currentRootState = await getRecommendRoots(client);
336
+ }
337
+ }
338
+
339
+ const wrapped = new Error(compactFilterReapplyError(lastError));
340
+ wrapped.cause = lastError;
341
+ wrapped.filter_reapply_attempts = attempts;
342
+ throw wrapped;
158
343
  }
159
344
 
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;
345
+ function compactCurrentCityOnlyReapplyError(error) {
346
+ return error?.message || String(error || "Recommend current-city-only reapply failed");
169
347
  }
170
348
 
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
- }
255
- }
256
-
257
- const wrapped = new Error(lastError?.message || "Recommend job selection failed after refresh reload");
258
- wrapped.cause = lastError;
259
- wrapped.job_selection_attempts = attempts;
260
- throw wrapped;
349
+ function isRetryableRecommendCurrentCityOnlyReapplyError(error) {
350
+ if (isStaleRecommendNodeError(error)) return true;
351
+ const message = compactCurrentCityOnlyReapplyError(error);
352
+ return /current-city|current city|location|popover|trigger|checkbox|stale|did not (?:open|mount|close)/i.test(message)
353
+ && !/unavailable/i.test(message);
261
354
  }
262
355
 
263
- async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
356
+ async function ensureRefreshCurrentCityOnly(client, rootState, {
357
+ enabled = false,
264
358
  maxAttempts = 3,
265
359
  retryDelayMs = 1500
266
360
  } = {}) {
@@ -270,18 +364,19 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
270
364
 
271
365
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
272
366
  try {
273
- const filter = await selectAndConfirmFirstSafeFilter(
367
+ const result = await ensureRecommendCurrentCityOnly(
274
368
  client,
275
369
  currentRootState.iframe.documentNodeId,
276
- filterOptions
370
+ { enabled }
277
371
  );
278
372
  attempts.push({
279
373
  ok: true,
280
- method: "filter_reapply",
281
- attempt
374
+ method: "current_city_only_reapply",
375
+ attempt,
376
+ result
282
377
  });
283
378
  return {
284
- filter,
379
+ current_city_only: result,
285
380
  root_state: currentRootState,
286
381
  attempts
287
382
  };
@@ -289,12 +384,12 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
289
384
  lastError = error;
290
385
  attempts.push({
291
386
  ok: false,
292
- method: "filter_reapply",
293
- reason: "filter_reapply_failed",
294
- error: compactFilterReapplyError(error),
387
+ method: "current_city_only_reapply",
388
+ reason: "current_city_only_reapply_failed",
389
+ error: compactCurrentCityOnlyReapplyError(error),
295
390
  attempt
296
391
  });
297
- if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
392
+ if (attempt >= maxAttempts || !isRetryableRecommendCurrentCityOnlyReapplyError(error)) {
298
393
  break;
299
394
  }
300
395
  if (retryDelayMs > 0) await sleep(retryDelayMs);
@@ -302,9 +397,9 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
302
397
  }
303
398
  }
304
399
 
305
- const wrapped = new Error(compactFilterReapplyError(lastError));
400
+ const wrapped = new Error(compactCurrentCityOnlyReapplyError(lastError));
306
401
  wrapped.cause = lastError;
307
- wrapped.filter_reapply_attempts = attempts;
402
+ wrapped.current_city_only_attempts = attempts;
308
403
  throw wrapped;
309
404
  }
310
405
 
@@ -313,174 +408,214 @@ async function applyRefreshMethod(client, method, {
313
408
  pageScope = "recommend",
314
409
  fallbackPageScope = "recommend",
315
410
  filter = {},
316
- targetUrl = RECOMMEND_TARGET_URL,
317
- forceRecentNotView = true,
318
- cardTimeoutMs = 30000,
319
- reloadSettleMs = 8000
320
- } = {}) {
411
+ targetUrl = RECOMMEND_TARGET_URL,
412
+ forceRecentNotView = true,
413
+ cardTimeoutMs = 30000,
414
+ reloadSettleMs = 8000
415
+ } = {}) {
321
416
  const started = Date.now();
322
417
  let currentRootState = null;
323
418
  let jobSelection = null;
324
419
  let jobSelectionAttempts = [];
325
420
  let pageScopeResult = null;
421
+ let currentCityOnlyResult = null;
422
+ let currentCityOnlyAttempts = [];
326
423
  let filterResult = null;
327
424
  let filterReapplyAttempts = [];
328
425
  let recoverySettle = null;
329
426
  try {
330
427
  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
- }
428
+ await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
429
+ } else {
430
+ await client.Page.reload({ ignoreCache: true });
431
+ }
432
+ recoverySettle = await waitForRecommendRecoverySettle(client, {
433
+ reloadSettleMs,
434
+ timeoutMs: Math.max(45000, reloadSettleMs * 6)
435
+ });
436
+ if (!recoverySettle.ok) {
437
+ throw createRecoverySettleError("recommend", recoverySettle);
438
+ }
439
+ currentRootState = await waitForRecommendRoots(client, {
440
+ timeoutMs: Math.max(45000, reloadSettleMs * 6),
441
+ intervalMs: 500
442
+ });
443
+ if (!currentRootState?.iframe?.documentNodeId) {
444
+ throw new Error("Recommend iframe was not ready after refresh reload");
445
+ }
446
+ if (jobLabel) {
447
+ const jobSelectionResult = await selectRecommendJobWithRootRefresh(client, currentRootState, {
448
+ jobLabel,
449
+ settleMs: reloadSettleMs > 10000 ? 12000 : 6000,
450
+ dropdownTimeoutMs: 4000,
451
+ totalTimeoutMs: reloadSettleMs > 10000 ? 45000 : 30000,
452
+ retryDelayMs: 1200
453
+ });
454
+ jobSelection = jobSelectionResult.job_selection;
455
+ jobSelectionAttempts = jobSelectionResult.attempts;
456
+ if (!jobSelection.selected) {
457
+ throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
458
+ }
459
+ currentRootState = jobSelectionResult.root_state || await getRecommendRoots(client);
460
+ }
461
+ pageScopeResult = await selectRecommendPageScope(
462
+ client,
463
+ currentRootState.iframe.documentNodeId,
464
+ {
465
+ pageScope,
466
+ fallbackScope: fallbackPageScope,
467
+ settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
468
+ timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
469
+ }
373
470
  );
374
471
  if (!pageScopeResult.selected) {
375
472
  throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
376
473
  }
377
474
  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)))
475
+ const retryDelayMs = Math.max(1200, Math.min(5000, Math.floor((reloadSettleMs || 8000) / 2)));
476
+ const filterStages = await applyRecommendFilterEnvelopeStages(filter, {
477
+ applyCurrentCityOnly: async () => {
478
+ const selection = await ensureRefreshCurrentCityOnly(client, currentRootState, {
479
+ enabled: filter.currentCityOnly === true || filter.current_city_only === true,
480
+ retryDelayMs
481
+ });
482
+ currentCityOnlyAttempts = selection.attempts;
483
+ currentRootState = await getRecommendRoots(client);
484
+ return selection.current_city_only;
485
+ },
486
+ applyFilterPanel: async () => {
487
+ const selection = await selectAndConfirmRefreshFilter(
488
+ client,
489
+ currentRootState,
490
+ buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
491
+ { retryDelayMs }
492
+ );
493
+ filterReapplyAttempts = selection.attempts;
494
+ currentRootState = await getRecommendRoots(client);
495
+ return selection.filter;
384
496
  }
385
- );
386
- filterResult = filterSelection.filter;
387
- filterReapplyAttempts = filterSelection.attempts;
388
- currentRootState = await getRecommendRoots(client);
497
+ });
498
+ currentCityOnlyResult = filterStages.current_city_only;
499
+ filterResult = filterStages.filter;
389
500
  const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
390
501
  timeoutMs: cardTimeoutMs,
391
502
  intervalMs: 500
392
503
  });
393
504
  if (!cardNodeIds.length) {
394
- throw new Error("No recommend candidate cards were found after refresh reload");
395
- }
396
- return {
397
- ok: true,
398
- method,
505
+ throw new Error("No recommend candidate cards were found after refresh reload");
506
+ }
507
+ return {
508
+ ok: true,
509
+ method,
399
510
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
400
511
  job_selection: jobSelection,
401
512
  job_selection_attempts: jobSelectionAttempts,
402
513
  recovery_settle: recoverySettle,
403
514
  page_scope: pageScopeResult,
515
+ current_city_only: currentCityOnlyResult,
516
+ current_city_only_attempts: currentCityOnlyAttempts,
404
517
  filter: filterResult,
405
518
  filter_reapply_attempts: filterReapplyAttempts,
406
519
  card_count: cardNodeIds.length,
407
520
  root_state: currentRootState,
408
- forced_recent_not_view: forceRecentNotView,
521
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
409
522
  elapsed_ms: Date.now() - started
410
523
  };
411
524
  } catch (error) {
412
525
  return {
413
526
  ok: false,
414
- method,
415
- reason: refreshFailureReason(method),
416
- error: error?.message || String(error),
527
+ method,
528
+ reason: refreshFailureReason(method),
529
+ error: error?.message || String(error),
417
530
  target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
418
531
  job_selection: jobSelection,
419
532
  job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
420
533
  recovery_settle: error?.recovery_settle || recoverySettle,
421
534
  page_scope: pageScopeResult,
535
+ current_city_only: currentCityOnlyResult,
536
+ current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
422
537
  filter: filterResult,
423
538
  filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
424
539
  card_count: 0,
425
540
  root_state: currentRootState,
426
- forced_recent_not_view: forceRecentNotView,
541
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
427
542
  elapsed_ms: Date.now() - started
428
543
  };
429
544
  }
430
545
  }
431
546
 
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,
547
+ export async function refreshRecommendListAtEnd(client, {
548
+ rootState = null,
549
+ jobLabel = "",
550
+ pageScope = "recommend",
551
+ fallbackPageScope = "recommend",
552
+ filter = {},
553
+ preferEndRefreshButton = true,
554
+ forceNavigate = false,
555
+ targetUrl = RECOMMEND_TARGET_URL,
556
+ forceRecentNotView = true,
557
+ cardTimeoutMs = 30000,
558
+ buttonSettleMs = 8000,
559
+ reloadSettleMs = 8000
560
+ } = {}) {
561
+ const attempts = [];
562
+ let currentRootState = rootState || null;
563
+
564
+ if (preferEndRefreshButton) {
565
+ currentRootState = currentRootState || await getRecommendRoots(client);
566
+ const buttonResult = await clickRecommendEndRefreshButton(
567
+ client,
453
568
  currentRootState.iframe.documentNodeId,
454
569
  { settleMs: buttonSettleMs }
455
570
  );
456
571
  attempts.push(buttonResult);
457
572
  if (buttonResult.ok) {
573
+ let pageScopeResult = null;
574
+ let currentCityOnlyResult = null;
575
+ let currentCityOnlyAttempts = [];
576
+ let filterResult = null;
577
+ let filterReapplyAttempts = [];
458
578
  try {
459
579
  currentRootState = await getRecommendRoots(client);
460
- const pageScopeResult = await selectRecommendPageScope(
580
+ pageScopeResult = await selectRecommendPageScope(
461
581
  client,
462
582
  currentRootState.iframe.documentNodeId,
463
583
  {
464
584
  pageScope,
465
585
  fallbackScope: fallbackPageScope,
466
- settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
467
- timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
468
- }
586
+ settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
587
+ timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
588
+ }
469
589
  );
470
590
  if (!pageScopeResult.selected) {
471
591
  throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
472
592
  }
473
593
  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)))
594
+ const retryDelayMs = Math.max(1200, Math.min(5000, Math.floor((buttonSettleMs || 8000) / 2)));
595
+ const filterStages = await applyRecommendFilterEnvelopeStages(filter, {
596
+ applyCurrentCityOnly: async () => {
597
+ const selection = await ensureRefreshCurrentCityOnly(client, currentRootState, {
598
+ enabled: filter.currentCityOnly === true || filter.current_city_only === true,
599
+ retryDelayMs
600
+ });
601
+ currentCityOnlyAttempts = selection.attempts;
602
+ currentRootState = await getRecommendRoots(client);
603
+ return selection.current_city_only;
604
+ },
605
+ applyFilterPanel: async () => {
606
+ const selection = await selectAndConfirmRefreshFilter(
607
+ client,
608
+ currentRootState,
609
+ buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
610
+ { retryDelayMs }
611
+ );
612
+ filterReapplyAttempts = selection.attempts;
613
+ currentRootState = await getRecommendRoots(client);
614
+ return selection.filter;
480
615
  }
481
- );
482
- const filterResult = filterSelection.filter;
483
- currentRootState = await getRecommendRoots(client);
616
+ });
617
+ currentCityOnlyResult = filterStages.current_city_only;
618
+ filterResult = filterStages.filter;
484
619
  const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
485
620
  timeoutMs: cardTimeoutMs,
486
621
  intervalMs: 500
@@ -490,65 +625,73 @@ export async function refreshRecommendListAtEnd(client, {
490
625
  method: "end_refresh_button",
491
626
  attempts,
492
627
  page_scope: pageScopeResult,
628
+ current_city_only: currentCityOnlyResult,
629
+ current_city_only_attempts: currentCityOnlyAttempts,
493
630
  filter: filterResult,
494
- filter_reapply_attempts: filterSelection.attempts,
631
+ filter_reapply_attempts: filterReapplyAttempts,
495
632
  card_count: cardNodeIds.length,
496
633
  root_state: currentRootState,
497
- forced_recent_not_view: forceRecentNotView
634
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
498
635
  };
499
636
  } catch (error) {
500
637
  attempts.push({
501
638
  ok: false,
502
639
  method: "end_refresh_button_after_click",
503
640
  reason: "end_refresh_reapply_failed",
504
- error: error?.message || String(error)
641
+ error: error?.message || String(error),
642
+ page_scope: pageScopeResult,
643
+ current_city_only: currentCityOnlyResult,
644
+ current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
645
+ filter: filterResult,
646
+ filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
647
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
505
648
  });
506
649
  }
507
650
  }
508
651
  }
509
652
 
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,
653
+ const fallbackMethods = [];
654
+ if (forceNavigate && typeof client?.Page?.navigate === "function") {
655
+ fallbackMethods.push("page_navigate");
656
+ }
657
+ if (typeof client?.Page?.reload === "function") {
658
+ fallbackMethods.push("page_reload");
659
+ }
660
+ if (!fallbackMethods.length) {
661
+ fallbackMethods.push("page_reload");
662
+ }
663
+
664
+ let lastRefreshResult = null;
665
+ for (const method of fallbackMethods) {
666
+ const refreshResult = await applyRefreshMethod(client, method, {
667
+ jobLabel,
668
+ pageScope,
669
+ fallbackPageScope,
670
+ filter,
671
+ targetUrl,
672
+ forceRecentNotView,
673
+ cardTimeoutMs,
674
+ reloadSettleMs
675
+ });
676
+ if (refreshResult.ok) {
677
+ return {
678
+ ...refreshResult,
679
+ attempts
680
+ };
681
+ }
682
+ attempts.push(refreshResult);
683
+ lastRefreshResult = refreshResult;
684
+ }
685
+
686
+ return {
687
+ ...(lastRefreshResult || {
688
+ ok: false,
546
689
  method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
547
690
  reason: "refresh_failed",
548
691
  error: "Recommend refresh did not run",
549
692
  card_count: 0,
550
693
  root_state: currentRootState,
551
- forced_recent_not_view: forceRecentNotView
694
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
552
695
  }),
553
696
  attempts
554
697
  };