@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.24

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 (66) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/core/boss-cards/index.js +199 -199
  14. package/src/core/browser/index.js +286 -136
  15. package/src/core/capture/index.js +2930 -1201
  16. package/src/core/cv-acquisition/index.js +512 -238
  17. package/src/core/cv-capture-target/index.js +513 -299
  18. package/src/core/greet-quota/index.js +71 -71
  19. package/src/core/infinite-list/index.js +31 -31
  20. package/src/core/reporting/legacy-csv.js +12 -12
  21. package/src/core/run/detached-launcher.js +305 -0
  22. package/src/core/run/index.js +105 -52
  23. package/src/core/run/timing.js +33 -33
  24. package/src/core/run/windows-detached-worker.ps1 +106 -0
  25. package/src/core/screening/index.js +2135 -2135
  26. package/src/core/self-heal/index.js +989 -973
  27. package/src/core/self-heal/viewport.js +1505 -564
  28. package/src/detached-worker.js +99 -99
  29. package/src/domains/chat/action-journal.js +443 -0
  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 +1684 -401
  33. package/src/domains/chat/index.js +8 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +157 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +1801 -760
  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 +515 -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 +92 -92
  44. package/src/domains/recommend/detail.js +12 -12
  45. package/src/domains/recommend/filters.js +611 -611
  46. package/src/domains/recommend/index.js +9 -9
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -736
  49. package/src/domains/recommend/refresh.js +422 -422
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1791 -1205
  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 +1817 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +16 -1
  64. package/src/parser.js +1296 -1296
  65. package/src/recommend-mcp.js +551 -362
  66. package/src/recommend-scheduler.js +75 -75
@@ -11,17 +11,17 @@ import {
11
11
  clickRecommendEndRefreshButton,
12
12
  waitForRecommendCardNodeIds
13
13
  } from "./cards.js";
14
- import {
15
- RECOMMEND_RECENT_NOT_VIEW_LABEL,
16
- RECOMMEND_TARGET_URL
17
- } from "./constants.js";
18
- import { selectAndConfirmFirstSafeFilter } from "./filters.js";
19
- import { ensureRecommendCurrentCityOnly } from "./location.js";
20
- import {
21
- selectRecommendJob,
22
- verifyRecommendJobSelection
23
- } from "./jobs.js";
24
- import { selectRecommendPageScope } from "./scopes.js";
14
+ import {
15
+ RECOMMEND_RECENT_NOT_VIEW_LABEL,
16
+ RECOMMEND_TARGET_URL
17
+ } from "./constants.js";
18
+ import { selectAndConfirmFirstSafeFilter } from "./filters.js";
19
+ import { ensureRecommendCurrentCityOnly } from "./location.js";
20
+ import {
21
+ selectRecommendJob,
22
+ verifyRecommendJobSelection
23
+ } from "./jobs.js";
24
+ import { selectRecommendPageScope } from "./scopes.js";
25
25
  import {
26
26
  getRecommendRoots,
27
27
  waitForRecommendRoots
@@ -31,19 +31,19 @@ import { isStaleRecommendNodeError } from "./detail.js";
31
31
  function normalizeLabels(labels = []) {
32
32
  return labels.map((label) => String(label || "").trim()).filter(Boolean);
33
33
  }
34
-
35
- function normalizeFilterGroup(spec = {}) {
36
- return {
37
- group: String(spec.group || "").trim(),
38
- labels: normalizeLabels(spec.labels || spec.filterLabels || []),
39
- selectAllLabels: spec.selectAllLabels !== false,
40
- allowUnlimited: spec.allowUnlimited === true,
41
- verifySticky: spec.verifySticky === true
42
- };
43
- }
44
-
45
- export function buildRecommendFilterGroups(filter = {}, {
46
- forceRecentNotView = false
34
+
35
+ function normalizeFilterGroup(spec = {}) {
36
+ return {
37
+ group: String(spec.group || "").trim(),
38
+ labels: normalizeLabels(spec.labels || spec.filterLabels || []),
39
+ selectAllLabels: spec.selectAllLabels !== false,
40
+ allowUnlimited: spec.allowUnlimited === true,
41
+ verifySticky: spec.verifySticky === true
42
+ };
43
+ }
44
+
45
+ export function buildRecommendFilterGroups(filter = {}, {
46
+ forceRecentNotView = false
47
47
  } = {}) {
48
48
  const groups = [];
49
49
  const sourceGroups = Array.isArray(filter.filterGroups)
@@ -68,19 +68,19 @@ export function buildRecommendFilterGroups(filter = {}, {
68
68
  if (!recentGroup.labels.some((label) => label.replace(/\s+/g, "") === RECOMMEND_RECENT_NOT_VIEW_LABEL)) {
69
69
  recentGroup.labels.push(RECOMMEND_RECENT_NOT_VIEW_LABEL);
70
70
  }
71
- recentGroup.selectAllLabels = true;
72
- } else {
73
- groups.unshift({
74
- group: "recentNotView",
75
- labels: [RECOMMEND_RECENT_NOT_VIEW_LABEL],
76
- selectAllLabels: true,
77
- allowUnlimited: false,
78
- verifySticky: false
79
- });
80
- }
81
- }
82
-
83
- return groups;
71
+ recentGroup.selectAllLabels = true;
72
+ } else {
73
+ groups.unshift({
74
+ group: "recentNotView",
75
+ labels: [RECOMMEND_RECENT_NOT_VIEW_LABEL],
76
+ selectAllLabels: true,
77
+ allowUnlimited: false,
78
+ verifySticky: false
79
+ });
80
+ }
81
+ }
82
+
83
+ return groups;
84
84
  }
85
85
 
86
86
  export function buildRecommendFilterSelectionOptions(filter = {}, {
@@ -90,134 +90,134 @@ export function buildRecommendFilterSelectionOptions(filter = {}, {
90
90
  if (filterGroups.length > 1 || forceRecentNotView || Array.isArray(filter.filterGroups) || Array.isArray(filter.groups)) {
91
91
  return { filterGroups };
92
92
  }
93
- const [singleGroup] = filterGroups;
94
- if (singleGroup) {
95
- return {
96
- group: singleGroup.group,
97
- labels: singleGroup.labels,
98
- selectAllLabels: singleGroup.selectAllLabels,
99
- allowUnlimited: singleGroup.allowUnlimited,
100
- verifySticky: singleGroup.verifySticky
101
- };
102
- }
103
- return {
104
- group: filter.group || "",
105
- labels: normalizeLabels(filter.labels || filter.filterLabels || []),
106
- selectAllLabels: filter.selectAllLabels !== false,
107
- allowUnlimited: filter.allowUnlimited === true,
108
- verifySticky: filter.verifySticky === true
109
- };
110
- }
111
-
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
- };
135
- }
136
-
137
- function refreshFailureReason(method = "") {
138
- return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
139
- }
140
-
141
- function safeDiagnosticText(value, maxLength = 4000) {
142
- const text = String(value ?? "");
143
- return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
144
- }
145
-
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");
152
- }
153
-
154
- function buildErrorDiagnostic(error, {
155
- depth = 0,
156
- seen = new Set()
157
- } = {}) {
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))
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;
201
- }
202
-
203
- export function compactRecommendRefreshErrorDiagnostic(error) {
204
- return buildErrorDiagnostic(error);
205
- }
206
-
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;
217
- }
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
- }
93
+ const [singleGroup] = filterGroups;
94
+ if (singleGroup) {
95
+ return {
96
+ group: singleGroup.group,
97
+ labels: singleGroup.labels,
98
+ selectAllLabels: singleGroup.selectAllLabels,
99
+ allowUnlimited: singleGroup.allowUnlimited,
100
+ verifySticky: singleGroup.verifySticky
101
+ };
102
+ }
103
+ return {
104
+ group: filter.group || "",
105
+ labels: normalizeLabels(filter.labels || filter.filterLabels || []),
106
+ selectAllLabels: filter.selectAllLabels !== false,
107
+ allowUnlimited: filter.allowUnlimited === true,
108
+ verifySticky: filter.verifySticky === true
109
+ };
110
+ }
111
+
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
+ };
135
+ }
136
+
137
+ function refreshFailureReason(method = "") {
138
+ return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
139
+ }
140
+
141
+ function safeDiagnosticText(value, maxLength = 4000) {
142
+ const text = String(value ?? "");
143
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
144
+ }
145
+
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");
152
+ }
153
+
154
+ function buildErrorDiagnostic(error, {
155
+ depth = 0,
156
+ seen = new Set()
157
+ } = {}) {
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))
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;
201
+ }
202
+
203
+ export function compactRecommendRefreshErrorDiagnostic(error) {
204
+ return buildErrorDiagnostic(error);
205
+ }
206
+
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;
217
+ }
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
221
 
222
222
  function compactFilterReapplyError(error) {
223
223
  return error?.message || String(error || "Recommend filter reapply failed");
@@ -239,10 +239,10 @@ function compactJobSelectionAttempt({
239
239
  return {
240
240
  ok: Boolean(ok),
241
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,
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
246
  iframe_document_node_id: iframeDocumentNodeId || 0,
247
247
  selected: Boolean(selection?.selected),
248
248
  selection_reason: selection?.reason || null,
@@ -401,10 +401,10 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
401
401
  attempts.push({
402
402
  ok: false,
403
403
  method: "filter_reapply",
404
- reason: "filter_reapply_failed",
405
- error: compactFilterReapplyError(error),
406
- error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
407
- attempt
404
+ reason: "filter_reapply_failed",
405
+ error: compactFilterReapplyError(error),
406
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
407
+ attempt
408
408
  });
409
409
  if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
410
410
  break;
@@ -415,95 +415,95 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
415
415
  }
416
416
 
417
417
  const wrapped = new Error(compactFilterReapplyError(lastError));
418
- wrapped.cause = lastError;
419
- wrapped.filter_reapply_attempts = attempts;
420
- throw wrapped;
421
- }
422
-
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,
436
- maxAttempts = 3,
437
- retryDelayMs = 1500
438
- } = {}) {
439
- const attempts = [];
440
- let currentRootState = rootState;
441
- let lastError = null;
442
-
443
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
444
- try {
445
- const result = await ensureRecommendCurrentCityOnly(
446
- client,
447
- currentRootState.iframe.documentNodeId,
448
- { enabled }
449
- );
450
- attempts.push({
451
- ok: true,
452
- method: "current_city_only_reapply",
453
- attempt,
454
- result
455
- });
456
- return {
457
- current_city_only: result,
458
- root_state: currentRootState,
459
- attempts
460
- };
461
- } catch (error) {
462
- lastError = error;
463
- attempts.push({
464
- ok: false,
465
- method: "current_city_only_reapply",
466
- reason: "current_city_only_reapply_failed",
467
- error: compactCurrentCityOnlyReapplyError(error),
468
- error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
469
- attempt
470
- });
471
- if (attempt >= maxAttempts || !isRetryableRecommendCurrentCityOnlyReapplyError(error)) {
472
- break;
473
- }
474
- if (retryDelayMs > 0) await sleep(retryDelayMs);
475
- currentRootState = await getRecommendRoots(client);
476
- }
477
- }
478
-
479
- const wrapped = new Error(compactCurrentCityOnlyReapplyError(lastError));
480
- wrapped.cause = lastError;
481
- wrapped.current_city_only_attempts = attempts;
482
- throw wrapped;
483
- }
484
-
485
- async function applyRefreshMethod(client, method, {
486
- jobLabel = "",
487
- pageScope = "recommend",
488
- fallbackPageScope = "recommend",
489
- filter = {},
418
+ wrapped.cause = lastError;
419
+ wrapped.filter_reapply_attempts = attempts;
420
+ throw wrapped;
421
+ }
422
+
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,
436
+ maxAttempts = 3,
437
+ retryDelayMs = 1500
438
+ } = {}) {
439
+ const attempts = [];
440
+ let currentRootState = rootState;
441
+ let lastError = null;
442
+
443
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
444
+ try {
445
+ const result = await ensureRecommendCurrentCityOnly(
446
+ client,
447
+ currentRootState.iframe.documentNodeId,
448
+ { enabled }
449
+ );
450
+ attempts.push({
451
+ ok: true,
452
+ method: "current_city_only_reapply",
453
+ attempt,
454
+ result
455
+ });
456
+ return {
457
+ current_city_only: result,
458
+ root_state: currentRootState,
459
+ attempts
460
+ };
461
+ } catch (error) {
462
+ lastError = error;
463
+ attempts.push({
464
+ ok: false,
465
+ method: "current_city_only_reapply",
466
+ reason: "current_city_only_reapply_failed",
467
+ error: compactCurrentCityOnlyReapplyError(error),
468
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
469
+ attempt
470
+ });
471
+ if (attempt >= maxAttempts || !isRetryableRecommendCurrentCityOnlyReapplyError(error)) {
472
+ break;
473
+ }
474
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
475
+ currentRootState = await getRecommendRoots(client);
476
+ }
477
+ }
478
+
479
+ const wrapped = new Error(compactCurrentCityOnlyReapplyError(lastError));
480
+ wrapped.cause = lastError;
481
+ wrapped.current_city_only_attempts = attempts;
482
+ throw wrapped;
483
+ }
484
+
485
+ async function applyRefreshMethod(client, method, {
486
+ jobLabel = "",
487
+ pageScope = "recommend",
488
+ fallbackPageScope = "recommend",
489
+ filter = {},
490
490
  targetUrl = RECOMMEND_TARGET_URL,
491
491
  forceRecentNotView = true,
492
492
  cardTimeoutMs = 30000,
493
493
  reloadSettleMs = 8000
494
494
  } = {}) {
495
- const started = Date.now();
496
- let currentRootState = null;
497
- let jobSelection = null;
498
- let jobSelectionAttempts = [];
499
- let pageScopeResult = null;
500
- let currentCityOnlyResult = null;
501
- let currentCityOnlyAttempts = [];
502
- let filterResult = null;
503
- let filterReapplyAttempts = [];
504
- let recoverySettle = null;
505
- try {
506
- if (method === "page_navigate") {
495
+ const started = Date.now();
496
+ let currentRootState = null;
497
+ let jobSelection = null;
498
+ let jobSelectionAttempts = [];
499
+ let pageScopeResult = null;
500
+ let currentCityOnlyResult = null;
501
+ let currentCityOnlyAttempts = [];
502
+ let filterResult = null;
503
+ let filterReapplyAttempts = [];
504
+ let recoverySettle = null;
505
+ try {
506
+ if (method === "page_navigate") {
507
507
  await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
508
508
  } else {
509
509
  await client.Page.reload({ ignoreCache: true });
@@ -546,84 +546,84 @@ async function applyRefreshMethod(client, method, {
546
546
  settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
547
547
  timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
548
548
  }
549
- );
550
- if (!pageScopeResult.selected) {
551
- throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
552
- }
553
- currentRootState = await getRecommendRoots(client);
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;
575
- }
576
- });
577
- currentCityOnlyResult = filterStages.current_city_only;
578
- filterResult = filterStages.filter;
579
- const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
580
- timeoutMs: cardTimeoutMs,
581
- intervalMs: 500
582
- });
583
- if (!cardNodeIds.length) {
549
+ );
550
+ if (!pageScopeResult.selected) {
551
+ throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
552
+ }
553
+ currentRootState = await getRecommendRoots(client);
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;
575
+ }
576
+ });
577
+ currentCityOnlyResult = filterStages.current_city_only;
578
+ filterResult = filterStages.filter;
579
+ const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
580
+ timeoutMs: cardTimeoutMs,
581
+ intervalMs: 500
582
+ });
583
+ if (!cardNodeIds.length) {
584
584
  throw new Error("No recommend candidate cards were found after refresh reload");
585
585
  }
586
586
  return {
587
587
  ok: true,
588
588
  method,
589
- target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
590
- job_selection: jobSelection,
591
- job_selection_attempts: jobSelectionAttempts,
592
- recovery_settle: recoverySettle,
593
- page_scope: pageScopeResult,
594
- current_city_only: currentCityOnlyResult,
595
- current_city_only_attempts: currentCityOnlyAttempts,
596
- filter: filterResult,
597
- filter_reapply_attempts: filterReapplyAttempts,
598
- card_count: cardNodeIds.length,
599
- root_state: currentRootState,
600
- forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
601
- elapsed_ms: Date.now() - started
602
- };
603
- } catch (error) {
604
- return {
605
- ok: false,
589
+ target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
590
+ job_selection: jobSelection,
591
+ job_selection_attempts: jobSelectionAttempts,
592
+ recovery_settle: recoverySettle,
593
+ page_scope: pageScopeResult,
594
+ current_city_only: currentCityOnlyResult,
595
+ current_city_only_attempts: currentCityOnlyAttempts,
596
+ filter: filterResult,
597
+ filter_reapply_attempts: filterReapplyAttempts,
598
+ card_count: cardNodeIds.length,
599
+ root_state: currentRootState,
600
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
601
+ elapsed_ms: Date.now() - started
602
+ };
603
+ } catch (error) {
604
+ return {
605
+ ok: false,
606
606
  method,
607
- reason: refreshFailureReason(method),
608
- error: error?.message || String(error),
609
- error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
610
- target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
611
- job_selection: jobSelection,
612
- job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
613
- recovery_settle: error?.recovery_settle || recoverySettle,
614
- page_scope: pageScopeResult,
615
- current_city_only: currentCityOnlyResult,
616
- current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
617
- filter: filterResult,
618
- filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
619
- card_count: 0,
620
- root_state: currentRootState,
621
- forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
622
- elapsed_ms: Date.now() - started
623
- };
624
- }
625
- }
626
-
607
+ reason: refreshFailureReason(method),
608
+ error: error?.message || String(error),
609
+ error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
610
+ target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
611
+ job_selection: jobSelection,
612
+ job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
613
+ recovery_settle: error?.recovery_settle || recoverySettle,
614
+ page_scope: pageScopeResult,
615
+ current_city_only: currentCityOnlyResult,
616
+ current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
617
+ filter: filterResult,
618
+ filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
619
+ card_count: 0,
620
+ root_state: currentRootState,
621
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
622
+ elapsed_ms: Date.now() - started
623
+ };
624
+ }
625
+ }
626
+
627
627
  export async function refreshRecommendListAtEnd(client, {
628
628
  rootState = null,
629
629
  jobLabel = "",
@@ -645,92 +645,92 @@ export async function refreshRecommendListAtEnd(client, {
645
645
  currentRootState = currentRootState || await getRecommendRoots(client);
646
646
  const buttonResult = await clickRecommendEndRefreshButton(
647
647
  client,
648
- currentRootState.iframe.documentNodeId,
649
- { settleMs: buttonSettleMs }
650
- );
651
- attempts.push(buttonResult);
652
- if (buttonResult.ok) {
653
- let pageScopeResult = null;
654
- let currentCityOnlyResult = null;
655
- let currentCityOnlyAttempts = [];
656
- let filterResult = null;
657
- let filterReapplyAttempts = [];
658
- try {
659
- currentRootState = await getRecommendRoots(client);
660
- pageScopeResult = await selectRecommendPageScope(
661
- client,
662
- currentRootState.iframe.documentNodeId,
663
- {
664
- pageScope,
665
- fallbackScope: fallbackPageScope,
648
+ currentRootState.iframe.documentNodeId,
649
+ { settleMs: buttonSettleMs }
650
+ );
651
+ attempts.push(buttonResult);
652
+ if (buttonResult.ok) {
653
+ let pageScopeResult = null;
654
+ let currentCityOnlyResult = null;
655
+ let currentCityOnlyAttempts = [];
656
+ let filterResult = null;
657
+ let filterReapplyAttempts = [];
658
+ try {
659
+ currentRootState = await getRecommendRoots(client);
660
+ pageScopeResult = await selectRecommendPageScope(
661
+ client,
662
+ currentRootState.iframe.documentNodeId,
663
+ {
664
+ pageScope,
665
+ fallbackScope: fallbackPageScope,
666
666
  settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
667
667
  timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
668
668
  }
669
- );
670
- if (!pageScopeResult.selected) {
671
- throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
672
- }
673
- currentRootState = await getRecommendRoots(client);
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;
695
- }
696
- });
697
- currentCityOnlyResult = filterStages.current_city_only;
698
- filterResult = filterStages.filter;
699
- const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
700
- timeoutMs: cardTimeoutMs,
701
- intervalMs: 500
702
- });
703
- return {
704
- ok: cardNodeIds.length > 0,
705
- method: "end_refresh_button",
706
- attempts,
707
- page_scope: pageScopeResult,
708
- current_city_only: currentCityOnlyResult,
709
- current_city_only_attempts: currentCityOnlyAttempts,
710
- filter: filterResult,
711
- filter_reapply_attempts: filterReapplyAttempts,
712
- card_count: cardNodeIds.length,
713
- root_state: currentRootState,
714
- forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
715
- };
716
- } catch (error) {
717
- attempts.push({
718
- ok: false,
719
- method: "end_refresh_button_after_click",
720
- reason: "end_refresh_reapply_failed",
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)
729
- });
730
- }
731
- }
732
- }
733
-
669
+ );
670
+ if (!pageScopeResult.selected) {
671
+ throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
672
+ }
673
+ currentRootState = await getRecommendRoots(client);
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;
695
+ }
696
+ });
697
+ currentCityOnlyResult = filterStages.current_city_only;
698
+ filterResult = filterStages.filter;
699
+ const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
700
+ timeoutMs: cardTimeoutMs,
701
+ intervalMs: 500
702
+ });
703
+ return {
704
+ ok: cardNodeIds.length > 0,
705
+ method: "end_refresh_button",
706
+ attempts,
707
+ page_scope: pageScopeResult,
708
+ current_city_only: currentCityOnlyResult,
709
+ current_city_only_attempts: currentCityOnlyAttempts,
710
+ filter: filterResult,
711
+ filter_reapply_attempts: filterReapplyAttempts,
712
+ card_count: cardNodeIds.length,
713
+ root_state: currentRootState,
714
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
715
+ };
716
+ } catch (error) {
717
+ attempts.push({
718
+ ok: false,
719
+ method: "end_refresh_button_after_click",
720
+ reason: "end_refresh_reapply_failed",
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)
729
+ });
730
+ }
731
+ }
732
+ }
733
+
734
734
  const fallbackMethods = [];
735
735
  if (forceNavigate && typeof client?.Page?.navigate === "function") {
736
736
  fallbackMethods.push("page_navigate");
@@ -767,13 +767,13 @@ export async function refreshRecommendListAtEnd(client, {
767
767
  return {
768
768
  ...(lastRefreshResult || {
769
769
  ok: false,
770
- method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
771
- reason: "refresh_failed",
772
- error: "Recommend refresh did not run",
773
- card_count: 0,
774
- root_state: currentRootState,
775
- forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
776
- }),
777
- attempts
778
- };
779
- }
770
+ method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
771
+ reason: "refresh_failed",
772
+ error: "Recommend refresh did not run",
773
+ card_count: 0,
774
+ root_state: currentRootState,
775
+ forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
776
+ }),
777
+ attempts
778
+ };
779
+ }