@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0

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 (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,1101 @@
1
+ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
2
+ import {
3
+ clickPoint,
4
+ getNodeBox,
5
+ scrollNodeIntoView,
6
+ sleep
7
+ } from "../../core/browser/index.js";
8
+ import {
9
+ compactCvAcquisitionState,
10
+ countParsedNetworkProfiles,
11
+ createCvAcquisitionState,
12
+ getCvNetworkWaitPlan,
13
+ recordCvImageFallback,
14
+ recordCvNetworkHit,
15
+ recordCvNetworkMiss,
16
+ summarizeImageEvidence,
17
+ waitForCvNetworkEvents
18
+ } from "../../core/cv-acquisition/index.js";
19
+ import {
20
+ compactInfiniteListState,
21
+ createInfiniteListState,
22
+ getNextInfiniteListCandidate,
23
+ markInfiniteListCandidateProcessed
24
+ } from "../../core/infinite-list/index.js";
25
+ import { createRunLifecycleManager } from "../../core/run/index.js";
26
+ import {
27
+ callScreeningLlm,
28
+ normalizeText,
29
+ screenCandidate
30
+ } from "../../core/screening/index.js";
31
+ import { CHAT_TARGET_URL } from "./constants.js";
32
+ import {
33
+ chatCandidateKeyFromProfile,
34
+ findChatCandidateNodeIdById,
35
+ readChatCardCandidate,
36
+ waitForChatCandidateNodeIds
37
+ } from "./cards.js";
38
+ import {
39
+ closeChatResumeModal,
40
+ createChatProfileNetworkRecorder,
41
+ extractChatProfileCandidate,
42
+ openChatOnlineResume,
43
+ readChatConversationReadyState,
44
+ requestChatResumeForPassedCandidate,
45
+ selectChatMessageFilter,
46
+ selectChatPrimaryLabel,
47
+ waitForChatOnlineResumeButton,
48
+ waitForChatProfileNetworkEvents,
49
+ waitForChatResumeContent
50
+ } from "./detail.js";
51
+ import { selectChatJob } from "./jobs.js";
52
+ import {
53
+ getChatTopLevelState,
54
+ isForbiddenChatResumeNavigationError,
55
+ makeForbiddenChatResumeNavigationError,
56
+ recoverChatShell
57
+ } from "./page-guard.js";
58
+ import { getChatRoots } from "./roots.js";
59
+
60
+ const DETAIL_SOURCES = new Set(["cascade", "network", "dom", "image"]);
61
+
62
+ function normalizeDetailSource(value) {
63
+ const normalized = String(value || "").trim().toLowerCase();
64
+ return DETAIL_SOURCES.has(normalized) ? normalized : "cascade";
65
+ }
66
+
67
+ function compactScreening(screening) {
68
+ return {
69
+ status: screening.status,
70
+ passed: screening.passed,
71
+ score: screening.score,
72
+ reasons: screening.reasons,
73
+ candidate: {
74
+ domain: screening.candidate?.domain || "chat",
75
+ source: screening.candidate?.source || "",
76
+ id: screening.candidate?.id || null,
77
+ identity: screening.candidate?.identity || {}
78
+ }
79
+ };
80
+ }
81
+
82
+ function compactLlmResult(llmResult) {
83
+ if (!llmResult) return null;
84
+ return {
85
+ ok: Boolean(llmResult.ok),
86
+ provider: llmResult.provider || null,
87
+ passed: llmResult.passed,
88
+ cot: llmResult.cot || llmResult.decision_cot || "",
89
+ reasoning_content: llmResult.reasoning_content || "",
90
+ raw_model_output: llmResult.raw_model_output || "",
91
+ evidence_count: llmResult.evidence?.length || 0,
92
+ usage: llmResult.usage || null,
93
+ finish_reason: llmResult.finish_reason || null,
94
+ image_input_count: llmResult.image_input_count || 0,
95
+ error: llmResult.error || null
96
+ };
97
+ }
98
+
99
+ function compactCandidate(candidate) {
100
+ return {
101
+ id: candidate?.id || null,
102
+ identity: candidate?.identity || {},
103
+ text_length: candidate?.text?.raw?.length || 0,
104
+ tag_count: candidate?.tags?.length || 0
105
+ };
106
+ }
107
+
108
+ function compactDetail(detailResult) {
109
+ if (!detailResult) return null;
110
+ return {
111
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
112
+ content_text_length: detailResult.detail?.content_text?.length || 0,
113
+ resume_iframe_text_length: detailResult.detail?.resume_iframe_text?.length || 0,
114
+ network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
115
+ parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
116
+ cv_acquisition: detailResult.cv_acquisition || null,
117
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
118
+ llm_screening: compactLlmResult(detailResult.llm_result),
119
+ close_result: detailResult.close_result
120
+ };
121
+ }
122
+
123
+ function resultOpenedDetail(result) {
124
+ return Boolean(result?.detail && !result.detail?.cv_acquisition?.skipped);
125
+ }
126
+
127
+ function llmToScreening(llmResult, candidate) {
128
+ return {
129
+ status: llmResult?.passed ? "pass" : "fail",
130
+ passed: Boolean(llmResult?.passed),
131
+ score: llmResult?.passed ? 100 : 0,
132
+ reasons: llmResult?.error ? ["llm_invalid_response"] : [],
133
+ candidate
134
+ };
135
+ }
136
+
137
+ function captureNodeIdFromResumeState(resumeState) {
138
+ return resumeState?.content?.node_id
139
+ || resumeState?.popup?.node_id
140
+ || resumeState?.resumeIframe?.node_id
141
+ || null;
142
+ }
143
+
144
+ function isRecoverableCdpNodeError(error) {
145
+ return /(?:Could not find node|No node with given id|Cannot find node|Could not compute box model)/i
146
+ .test(String(error?.message || error || ""));
147
+ }
148
+
149
+ function isRecoverableLlmScreeningError(error) {
150
+ return /(?:LLM response missing boolean passed decision|LLM response was not valid JSON)/i
151
+ .test(String(error?.message || error || ""));
152
+ }
153
+
154
+ function createFailedLlmResult(error) {
155
+ return {
156
+ ok: false,
157
+ passed: false,
158
+ reason: "",
159
+ evidence: [],
160
+ cot: "",
161
+ decision_cot: "",
162
+ reasoning_content: "",
163
+ raw_model_output: "",
164
+ error: error?.message || String(error || "unknown"),
165
+ screened_at: new Date().toISOString()
166
+ };
167
+ }
168
+
169
+ function createSkippedDetailResult(cardCandidate, reason, error = null) {
170
+ return {
171
+ candidate: cardCandidate,
172
+ parsed_network_profiles: [],
173
+ network_bodies: [],
174
+ detail: {},
175
+ cv_acquisition: {
176
+ source: reason,
177
+ skipped: true,
178
+ error: error?.message || null
179
+ },
180
+ close_result: null
181
+ };
182
+ }
183
+
184
+ async function resolveFreshChatCardNodeId(client, {
185
+ fallbackNodeId,
186
+ candidate,
187
+ rootNodeId = null
188
+ } = {}) {
189
+ const candidateId = candidate?.id || "";
190
+ if (!candidateId) return fallbackNodeId;
191
+ let currentRootNodeId = rootNodeId;
192
+ if (!currentRootNodeId) {
193
+ const rootState = await getChatRoots(client);
194
+ currentRootNodeId = rootState.rootNodes.top;
195
+ }
196
+ const freshNodeId = await findChatCandidateNodeIdById(client, currentRootNodeId, candidateId);
197
+ return freshNodeId || fallbackNodeId;
198
+ }
199
+
200
+ async function selectFreshChatCandidate(client, {
201
+ cardNodeId,
202
+ candidate,
203
+ timeoutMs,
204
+ settleMs = 1200
205
+ } = {}) {
206
+ let lastError = null;
207
+ for (let attempt = 0; attempt < 3; attempt += 1) {
208
+ const rootState = await getChatRoots(client);
209
+ const freshNodeId = await resolveFreshChatCardNodeId(client, {
210
+ fallbackNodeId: cardNodeId,
211
+ candidate,
212
+ rootNodeId: rootState.rootNodes.top
213
+ });
214
+ try {
215
+ await scrollNodeIntoView(client, freshNodeId);
216
+ await sleep(250);
217
+ const box = await getNodeBox(client, freshNodeId);
218
+ await clickPoint(client, box.center.x, box.center.y);
219
+ if (settleMs > 0) await sleep(settleMs);
220
+ const ready = await waitForChatOnlineResumeButton(client, {
221
+ timeoutMs,
222
+ expectedCandidateId: candidate?.id || ""
223
+ });
224
+ return {
225
+ card_box: box,
226
+ ready,
227
+ card_node_id: freshNodeId,
228
+ refreshed_node: freshNodeId !== cardNodeId,
229
+ attempt: attempt + 1
230
+ };
231
+ } catch (error) {
232
+ lastError = error;
233
+ if (!isRecoverableCdpNodeError(error)) throw error;
234
+ await sleep(350);
235
+ }
236
+ }
237
+ throw lastError || new Error("Chat candidate selection failed");
238
+ }
239
+
240
+ function selectedDetailNetworkEvents(detailSource, selectionEvents, resumeEvents) {
241
+ if (detailSource !== "network" && detailSource !== "cascade") return [];
242
+ return [
243
+ ...(selectionEvents || []),
244
+ ...(resumeEvents || [])
245
+ ];
246
+ }
247
+
248
+ async function setupChatRunContext(client, {
249
+ job,
250
+ normalizedStartFrom,
251
+ readyTimeoutMs,
252
+ listSettleMs,
253
+ runControl
254
+ } = {}) {
255
+ const rootState = await getChatRoots(client);
256
+ runControl.checkpoint({
257
+ top_document_node_id: rootState.rootNodes.top
258
+ });
259
+
260
+ const primaryLabel = await selectChatPrimaryLabel(client, {
261
+ label: "全部",
262
+ timeoutMs: readyTimeoutMs,
263
+ settleMs: listSettleMs
264
+ });
265
+ runControl.checkpoint({
266
+ chat_context_step: "primary_label",
267
+ primary_label: primaryLabel
268
+ });
269
+
270
+ const jobSelection = normalizeText(job)
271
+ ? await selectChatJob(client, rootState.rootNodes.top, {
272
+ jobLabel: job,
273
+ timeoutMs: readyTimeoutMs,
274
+ settleMs: listSettleMs
275
+ })
276
+ : {
277
+ selected: false,
278
+ reason: "job_not_requested"
279
+ };
280
+ if (normalizeText(job) && !jobSelection.selected) {
281
+ throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
282
+ }
283
+ runControl.checkpoint({
284
+ chat_context_step: "job_selection",
285
+ primary_label: primaryLabel,
286
+ job_selection: jobSelection
287
+ });
288
+
289
+ const startFilter = await selectChatMessageFilter(client, {
290
+ startFrom: normalizedStartFrom,
291
+ timeoutMs: readyTimeoutMs,
292
+ settleMs: listSettleMs
293
+ });
294
+ if (!startFilter.ok) {
295
+ throw new Error(`Chat start filter selection failed: ${startFilter.error || "unknown"}`);
296
+ }
297
+ runControl.checkpoint({
298
+ chat_context_step: "start_filter",
299
+ primary_label: primaryLabel,
300
+ job_selection: jobSelection,
301
+ start_filter: startFilter
302
+ });
303
+
304
+ return {
305
+ rootState,
306
+ contextSetup: {
307
+ primary_label: primaryLabel,
308
+ job_selection: jobSelection,
309
+ start_filter: startFilter,
310
+ requested_start_from: normalizedStartFrom
311
+ }
312
+ };
313
+ }
314
+
315
+ export async function runChatWorkflow({
316
+ client,
317
+ targetUrl = CHAT_TARGET_URL,
318
+ job = "",
319
+ startFrom = "all",
320
+ criteria = "",
321
+ maxCandidates = 5,
322
+ targetPassCount = null,
323
+ processUntilListEnd = false,
324
+ detailLimit = 0,
325
+ detailSource = "cascade",
326
+ closeResume = true,
327
+ requestResumeForPassed = false,
328
+ dryRunRequestCv = false,
329
+ greetingText = "Hi同学,能麻烦发下简历吗?",
330
+ delayMs = 0,
331
+ cardTimeoutMs = 90000,
332
+ readyTimeoutMs = 60000,
333
+ onlineResumeButtonTimeoutMs = 30000,
334
+ resumeDomTimeoutMs = 60000,
335
+ maxImagePages = 8,
336
+ imageWheelDeltaY = 650,
337
+ cvAcquisitionMode = "unknown",
338
+ callLlmOnImage = false,
339
+ llmConfig = null,
340
+ llmTimeoutMs = 120000,
341
+ llmImageLimit = 8,
342
+ llmImageDetail = "high",
343
+ listMaxScrolls = 20,
344
+ listStableSignatureLimit = 2,
345
+ listWheelDeltaY = 850,
346
+ listSettleMs = 1200,
347
+ listFallbackPoint = null
348
+ } = {}, runControl) {
349
+ if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
350
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
351
+ const processedLimit = Math.max(1, Number(maxCandidates) || 1);
352
+ const passTarget = Number.isFinite(Number(targetPassCount)) && Number(targetPassCount) > 0
353
+ ? Number(targetPassCount)
354
+ : null;
355
+ const normalizedStartFrom = normalizeText(startFrom).toLowerCase() === "unread" ? "unread" : "all";
356
+ const detailCountLimit = Math.max(0, Number(detailLimit) || 0);
357
+ const networkRecorder = detailCountLimit > 0
358
+ ? createChatProfileNetworkRecorder(client)
359
+ : null;
360
+ const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
361
+ const listState = createInfiniteListState({
362
+ domain: "chat",
363
+ listName: "chat-candidates"
364
+ });
365
+ const results = [];
366
+ let cardNodeIds = [];
367
+ let listEndReason = "";
368
+ let requestedCount = 0;
369
+ let requestSatisfiedCount = 0;
370
+ let requestSkippedCount = 0;
371
+ let contextSetup = {};
372
+
373
+ runControl.setPhase("chat:cleanup");
374
+ let initialTopLevelState = await getChatTopLevelState(client);
375
+ if (!initialTopLevelState.is_chat_shell) {
376
+ const recovery = await recoverChatShell(client, {
377
+ targetUrl,
378
+ timeoutMs: readyTimeoutMs
379
+ });
380
+ runControl.checkpoint({
381
+ chat_shell_recovery: {
382
+ reason: "initial_non_chat_shell",
383
+ ...recovery
384
+ }
385
+ });
386
+ if (!recovery.recovered) {
387
+ throw new Error(`Chat shell recovery failed before run setup: ${recovery.after?.url || recovery.before?.url || "unknown"}`);
388
+ }
389
+ initialTopLevelState = recovery.after;
390
+ }
391
+ await closeChatResumeModal(client, { attemptsLimit: 2 });
392
+
393
+ await runControl.waitIfPaused();
394
+ runControl.throwIfCanceled();
395
+ runControl.setPhase("chat:context");
396
+ const setup = await setupChatRunContext(client, {
397
+ job,
398
+ normalizedStartFrom,
399
+ readyTimeoutMs,
400
+ listSettleMs,
401
+ runControl
402
+ });
403
+ let rootState = setup.rootState;
404
+ contextSetup = {
405
+ ...setup.contextSetup,
406
+ initial_top_level_state: initialTopLevelState
407
+ };
408
+ runControl.checkpoint({
409
+ chat_context: contextSetup
410
+ });
411
+
412
+ async function recoverAndReapplyChatContext(reason, error = null) {
413
+ runControl.setPhase("chat:recover_shell");
414
+ const recovery = await recoverChatShell(client, {
415
+ targetUrl,
416
+ timeoutMs: readyTimeoutMs
417
+ });
418
+ runControl.checkpoint({
419
+ chat_shell_recovery: {
420
+ reason,
421
+ error: error?.message || null,
422
+ ...recovery
423
+ }
424
+ });
425
+ if (!recovery.recovered && !recovery.after?.is_chat_shell) {
426
+ throw new Error(`Chat shell recovery failed after ${reason}: ${recovery.after?.url || recovery.before?.url || "unknown"}`);
427
+ }
428
+ await closeChatResumeModal(client, { attemptsLimit: 2 });
429
+ const recoveredSetup = await setupChatRunContext(client, {
430
+ job,
431
+ normalizedStartFrom,
432
+ readyTimeoutMs,
433
+ listSettleMs,
434
+ runControl
435
+ });
436
+ rootState = recoveredSetup.rootState;
437
+ contextSetup = {
438
+ ...recoveredSetup.contextSetup,
439
+ recovered_from: reason,
440
+ recovery,
441
+ previous_context: contextSetup
442
+ };
443
+ runControl.checkpoint({
444
+ chat_context: contextSetup
445
+ });
446
+ return recovery;
447
+ }
448
+
449
+ await runControl.waitIfPaused();
450
+ runControl.throwIfCanceled();
451
+ runControl.setPhase("chat:cards");
452
+ const cardRootState = await getChatRoots(client);
453
+ const initialCards = await waitForChatCandidateNodeIds(client, cardRootState.rootNodes.top, {
454
+ timeoutMs: cardTimeoutMs,
455
+ intervalMs: 500
456
+ });
457
+ cardNodeIds = initialCards.nodeIds || [];
458
+ if (!cardNodeIds.length) {
459
+ runControl.checkpoint({
460
+ empty_list_state: {
461
+ method: "cdp_dom_selector_count",
462
+ candidate_count: 0,
463
+ requested_start_from: normalizedStartFrom
464
+ }
465
+ });
466
+ listEndReason = "no_chat_candidates_found";
467
+ runControl.updateProgress({
468
+ card_count: 0,
469
+ target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
470
+ target_pass_count: passTarget,
471
+ processed_limit: processedLimit,
472
+ processed: 0,
473
+ screened: 0,
474
+ detail_opened: 0,
475
+ llm_screened: 0,
476
+ passed: 0,
477
+ requested: 0,
478
+ request_satisfied: 0,
479
+ request_skipped: 0,
480
+ unique_seen: compactInfiniteListState(listState).seen_count,
481
+ scroll_count: compactInfiniteListState(listState).scroll_count,
482
+ list_end_reason: listEndReason
483
+ });
484
+ runControl.setPhase("chat:done");
485
+ return {
486
+ domain: "chat",
487
+ target_url: targetUrl,
488
+ card_count: 0,
489
+ context_setup: contextSetup,
490
+ empty_list_state: {
491
+ method: "cdp_dom_selector_count",
492
+ candidate_count: 0,
493
+ requested_start_from: normalizedStartFrom
494
+ },
495
+ candidate_list: compactInfiniteListState(listState),
496
+ list_end_reason: listEndReason,
497
+ target_pass_count: passTarget,
498
+ process_until_list_end: Boolean(processUntilListEnd),
499
+ processed_limit: processedLimit,
500
+ detail_source: normalizedDetailSource,
501
+ processed: 0,
502
+ screened: 0,
503
+ detail_opened: 0,
504
+ llm_screened: 0,
505
+ passed: 0,
506
+ requested: requestedCount,
507
+ request_satisfied: requestSatisfiedCount,
508
+ request_skipped: requestSkippedCount,
509
+ results
510
+ };
511
+ }
512
+
513
+ runControl.updateProgress({
514
+ card_count: cardNodeIds.length,
515
+ target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
516
+ target_pass_count: passTarget,
517
+ processed_limit: processedLimit,
518
+ processed: 0,
519
+ screened: 0,
520
+ detail_opened: 0,
521
+ llm_screened: 0,
522
+ passed: 0,
523
+ requested: 0,
524
+ request_satisfied: 0,
525
+ request_skipped: 0,
526
+ unique_seen: compactInfiniteListState(listState).seen_count,
527
+ scroll_count: 0
528
+ });
529
+
530
+ while (
531
+ results.length < processedLimit
532
+ && (
533
+ !passTarget
534
+ || results.filter((item) => item.screening?.passed).length < passTarget
535
+ )
536
+ ) {
537
+ await runControl.waitIfPaused();
538
+ runControl.throwIfCanceled();
539
+ runControl.setPhase("chat:candidate");
540
+ const loopTopLevelState = await getChatTopLevelState(client);
541
+ if (!loopTopLevelState.is_chat_shell) {
542
+ await recoverAndReapplyChatContext("candidate_loop_non_chat_shell", {
543
+ message: `Unexpected chat top-level URL: ${loopTopLevelState.url}`
544
+ });
545
+ continue;
546
+ }
547
+
548
+ const nextCandidateResult = await getNextInfiniteListCandidate({
549
+ client,
550
+ state: listState,
551
+ maxScrolls: listMaxScrolls,
552
+ stableSignatureLimit: listStableSignatureLimit,
553
+ wheelDeltaY: listWheelDeltaY,
554
+ settleMs: listSettleMs,
555
+ fallbackPoint: listFallbackPoint,
556
+ findNodeIds: async () => {
557
+ const currentRootState = await getChatRoots(client);
558
+ const currentCards = await waitForChatCandidateNodeIds(client, currentRootState.rootNodes.top, {
559
+ timeoutMs: Math.min(cardTimeoutMs, 8000),
560
+ intervalMs: 500
561
+ });
562
+ cardNodeIds = currentCards.nodeIds || [];
563
+ return cardNodeIds;
564
+ },
565
+ keyForCandidate: chatCandidateKeyFromProfile,
566
+ readCandidate: async (nodeId, { visibleIndex }) => readChatCardCandidate(client, nodeId, {
567
+ targetUrl,
568
+ source: "chat-run-card",
569
+ metadata: {
570
+ run_candidate_index: results.length,
571
+ visible_index: visibleIndex
572
+ }
573
+ })
574
+ });
575
+ if (!nextCandidateResult.ok) {
576
+ const endTopLevelState = await getChatTopLevelState(client);
577
+ if (!endTopLevelState.is_chat_shell) {
578
+ await recoverAndReapplyChatContext("candidate_list_end_non_chat_shell", {
579
+ message: `Unexpected chat top-level URL at list end: ${endTopLevelState.url}`
580
+ });
581
+ continue;
582
+ }
583
+ if (nextCandidateResult.reason === "empty_visible_list") {
584
+ runControl.checkpoint({
585
+ terminal_empty_list_state: {
586
+ method: "cdp_dom_selector_count",
587
+ reason: nextCandidateResult.reason,
588
+ requested_start_from: normalizedStartFrom
589
+ }
590
+ });
591
+ }
592
+ listEndReason = nextCandidateResult.reason || "list_exhausted";
593
+ break;
594
+ }
595
+
596
+ const index = results.length;
597
+ const cardNodeId = nextCandidateResult.item.node_id;
598
+ let effectiveCardNodeId = cardNodeId;
599
+ const candidateKey = nextCandidateResult.item.key;
600
+ const cardCandidate = nextCandidateResult.item.candidate;
601
+
602
+ let screeningCandidate = cardCandidate;
603
+ let detailResult = null;
604
+ let preActionState = null;
605
+ let detailUnavailableReason = "";
606
+ if (index < detailCountLimit) {
607
+ let detailStep = "start";
608
+ try {
609
+ await runControl.waitIfPaused();
610
+ runControl.throwIfCanceled();
611
+ runControl.setPhase("chat:detail");
612
+
613
+ detailStep = "select_candidate";
614
+ networkRecorder.clear();
615
+ const selected = await selectFreshChatCandidate(client, {
616
+ cardNodeId,
617
+ candidate: cardCandidate,
618
+ timeoutMs: onlineResumeButtonTimeoutMs
619
+ });
620
+ if (selected.ready?.forbidden_top_level_navigation) {
621
+ throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
622
+ }
623
+ effectiveCardNodeId = selected.card_node_id || cardNodeId;
624
+ const selectionNetworkEvents = networkRecorder.events.slice();
625
+ if (!selected.ready?.ok) {
626
+ if (selected.ready?.reason === "active_candidate_mismatch") {
627
+ detailUnavailableReason = "active_candidate_mismatch";
628
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
629
+ detailResult.cv_acquisition.selection_ready_state = selected.ready;
630
+ } else {
631
+ detailStep = "read_conversation_ready_state";
632
+ preActionState = await readChatConversationReadyState(client);
633
+ if (preActionState.attachment_resume_enabled) {
634
+ detailUnavailableReason = "attachment_resume_already_available";
635
+ detailResult = createSkippedDetailResult(cardCandidate, "attachment_resume_already_available");
636
+ } else {
637
+ detailUnavailableReason = "online_resume_button_unavailable";
638
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason);
639
+ }
640
+ }
641
+ }
642
+
643
+ if (!detailResult) {
644
+ detailStep = "open_online_resume";
645
+ networkRecorder.clear();
646
+ const openedResume = await openChatOnlineResume(client, {
647
+ timeoutMs: readyTimeoutMs
648
+ });
649
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
650
+ detailStep = "wait_network";
651
+ const networkWait = ["network", "cascade"].includes(normalizedDetailSource)
652
+ ? await waitForCvNetworkEvents(
653
+ waitForChatProfileNetworkEvents,
654
+ networkRecorder,
655
+ {
656
+ waitPlan,
657
+ minCount: 1,
658
+ requireLoaded: true,
659
+ intervalMs: 200
660
+ }
661
+ )
662
+ : null;
663
+ let contentWait = {
664
+ ok: false,
665
+ skipped: false,
666
+ reason: "not_started",
667
+ elapsed_ms: 0,
668
+ text_length: 0
669
+ };
670
+ let resumeState = openedResume.resume_state;
671
+ let resumeHtml = null;
672
+ let resumeNetworkEvents = networkRecorder.events.slice();
673
+ let parsedNetworkProfileCount = 0;
674
+
675
+ if (
676
+ ["network", "cascade"].includes(normalizedDetailSource)
677
+ && networkWait?.count > 0
678
+ ) {
679
+ detailStep = "extract_network_profile";
680
+ detailResult = await extractChatProfileCandidate(client, {
681
+ cardCandidate,
682
+ cardNodeId: effectiveCardNodeId,
683
+ resumeState,
684
+ resumeHtml,
685
+ networkEvents: selectedDetailNetworkEvents(
686
+ normalizedDetailSource,
687
+ selectionNetworkEvents,
688
+ resumeNetworkEvents
689
+ ),
690
+ targetUrl,
691
+ closeResume: false
692
+ });
693
+ parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
694
+ if (parsedNetworkProfileCount > 0) {
695
+ contentWait = {
696
+ ok: true,
697
+ skipped: true,
698
+ reason: "network_profile_parsed_before_dom_wait",
699
+ elapsed_ms: 0,
700
+ text_length: 0
701
+ };
702
+ } else {
703
+ detailResult = null;
704
+ }
705
+ }
706
+
707
+ if (!detailResult) {
708
+ detailStep = "wait_resume_content";
709
+ contentWait = await waitForChatResumeContent(client, {
710
+ timeoutMs: resumeDomTimeoutMs,
711
+ intervalMs: 300
712
+ });
713
+ resumeState = contentWait.resume_state || openedResume.resume_state;
714
+ resumeHtml = contentWait.resume_html || null;
715
+ resumeNetworkEvents = networkRecorder.events.slice();
716
+ detailStep = "extract_resume_content";
717
+ detailResult = await extractChatProfileCandidate(client, {
718
+ cardCandidate,
719
+ cardNodeId: effectiveCardNodeId,
720
+ resumeState,
721
+ resumeHtml,
722
+ networkEvents: selectedDetailNetworkEvents(
723
+ normalizedDetailSource,
724
+ selectionNetworkEvents,
725
+ resumeNetworkEvents
726
+ ),
727
+ targetUrl,
728
+ closeResume: false
729
+ });
730
+ parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
731
+ }
732
+
733
+ let source = normalizedDetailSource === "dom" ? "dom" : "network";
734
+ let imageEvidence = null;
735
+ let llmResult = null;
736
+ const captureNodeId = captureNodeIdFromResumeState(resumeState);
737
+ const shouldCaptureImage = normalizedDetailSource === "image"
738
+ || (normalizedDetailSource === "cascade" && parsedNetworkProfileCount < 1);
739
+ if (shouldCaptureImage) {
740
+ if (captureNodeId) {
741
+ detailStep = "capture_image_fallback";
742
+ imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
743
+ padding: 8,
744
+ maxScreenshots: maxImagePages,
745
+ wheelDeltaY: imageWheelDeltaY,
746
+ settleMs: 1200,
747
+ metadata: {
748
+ domain: "chat",
749
+ capture_mode: "scroll_sequence",
750
+ acquisition_reason: normalizedDetailSource === "image"
751
+ ? "forced_image"
752
+ : "network_miss_image_fallback",
753
+ run_candidate_index: index,
754
+ candidate_key: candidateKey
755
+ }
756
+ });
757
+ source = "image";
758
+ recordCvImageFallback(cvAcquisitionState, {
759
+ parsedNetworkProfileCount,
760
+ waitResult: networkWait,
761
+ imageEvidence
762
+ });
763
+ if (callLlmOnImage) {
764
+ if (!llmConfig) throw new Error("callLlmOnImage requires llmConfig");
765
+ detailStep = "llm_image_screening";
766
+ try {
767
+ llmResult = await callScreeningLlm({
768
+ candidate: detailResult.candidate,
769
+ criteria,
770
+ config: llmConfig,
771
+ timeoutMs: llmTimeoutMs,
772
+ imageEvidence,
773
+ maxImages: llmImageLimit,
774
+ imageDetail: llmImageDetail
775
+ });
776
+ } catch (error) {
777
+ if (!isRecoverableLlmScreeningError(error)) throw error;
778
+ llmResult = createFailedLlmResult(error);
779
+ }
780
+ }
781
+ } else {
782
+ source = "missing_capture_node";
783
+ recordCvNetworkMiss(cvAcquisitionState, {
784
+ reason: "network_miss_no_capture_node",
785
+ parsedNetworkProfileCount,
786
+ waitResult: networkWait
787
+ });
788
+ }
789
+ } else if (parsedNetworkProfileCount > 0) {
790
+ recordCvNetworkHit(cvAcquisitionState, {
791
+ parsedNetworkProfileCount,
792
+ waitResult: networkWait
793
+ });
794
+ } else if (normalizedDetailSource !== "dom") {
795
+ source = "network_miss";
796
+ recordCvNetworkMiss(cvAcquisitionState, {
797
+ reason: "network_miss_without_image_fallback",
798
+ parsedNetworkProfileCount,
799
+ waitResult: networkWait
800
+ });
801
+ }
802
+
803
+ if (llmConfig && !llmResult) {
804
+ detailStep = "llm_screening";
805
+ try {
806
+ llmResult = await callScreeningLlm({
807
+ candidate: detailResult.candidate,
808
+ criteria,
809
+ config: llmConfig,
810
+ timeoutMs: llmTimeoutMs,
811
+ imageEvidence,
812
+ maxImages: llmImageLimit,
813
+ imageDetail: llmImageDetail
814
+ });
815
+ } catch (error) {
816
+ if (!isRecoverableLlmScreeningError(error)) throw error;
817
+ llmResult = createFailedLlmResult(error);
818
+ }
819
+ }
820
+
821
+ let closeResult = null;
822
+ if (closeResume) {
823
+ detailStep = "close_resume_modal";
824
+ closeResult = await closeChatResumeModal(client);
825
+ }
826
+ detailResult.close_result = closeResult;
827
+ detailResult.image_evidence = imageEvidence;
828
+ detailResult.llm_result = llmResult;
829
+ detailResult.cv_acquisition = {
830
+ source,
831
+ mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
832
+ wait_plan: waitPlan,
833
+ network_wait: networkWait,
834
+ selection_network_event_count: selectionNetworkEvents.length,
835
+ resume_network_event_count: resumeNetworkEvents.length,
836
+ content_wait: {
837
+ ok: contentWait.ok,
838
+ skipped: Boolean(contentWait.skipped),
839
+ reason: contentWait.reason || null,
840
+ elapsed_ms: contentWait.elapsed_ms,
841
+ text_length: contentWait.text_length
842
+ },
843
+ parsed_network_profile_count: parsedNetworkProfileCount,
844
+ image_evidence: summarizeImageEvidence(imageEvidence)
845
+ };
846
+ }
847
+ } catch (error) {
848
+ if (isForbiddenChatResumeNavigationError(error)) {
849
+ detailUnavailableReason = "forbidden_top_level_resume_navigation";
850
+ const recovery = await recoverAndReapplyChatContext(detailUnavailableReason, error);
851
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
852
+ detailResult.cv_acquisition.recovery = recovery;
853
+ } else {
854
+ if (!isRecoverableCdpNodeError(error)) throw error;
855
+ detailUnavailableReason = `recoverable_cdp_node_stale:${detailStep}`;
856
+ detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
857
+ await closeChatResumeModal(client, { attemptsLimit: 2 });
858
+ }
859
+ }
860
+ screeningCandidate = detailResult.candidate;
861
+ }
862
+
863
+ await runControl.waitIfPaused();
864
+ runControl.throwIfCanceled();
865
+ runControl.setPhase("chat:screening");
866
+ const screening = detailUnavailableReason
867
+ ? {
868
+ status: "skip",
869
+ passed: false,
870
+ score: 0,
871
+ reasons: [detailUnavailableReason],
872
+ candidate: screeningCandidate
873
+ }
874
+ : detailResult?.llm_result
875
+ ? llmToScreening(detailResult.llm_result, screeningCandidate)
876
+ : screenCandidate(screeningCandidate, { criteria });
877
+ let postAction = null;
878
+ if (requestResumeForPassed && screening.passed) {
879
+ postAction = await requestChatResumeForPassedCandidate(client, {
880
+ greetingText,
881
+ dryRun: dryRunRequestCv
882
+ });
883
+ if (postAction?.requested) requestSatisfiedCount += 1;
884
+ if (postAction?.skipped) requestSkippedCount += 1;
885
+ if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
886
+ if (!postAction?.requested && !postAction?.skipped && !dryRunRequestCv) {
887
+ throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
888
+ }
889
+ }
890
+ const compactResult = {
891
+ index,
892
+ candidate_key: candidateKey,
893
+ card_node_id: effectiveCardNodeId,
894
+ candidate: compactCandidate(screeningCandidate),
895
+ detail: compactDetail(detailResult),
896
+ screening: compactScreening(screening),
897
+ post_action: postAction,
898
+ pre_action_state: preActionState
899
+ };
900
+ results.push(compactResult);
901
+ markInfiniteListCandidateProcessed(listState, candidateKey, {
902
+ metadata: {
903
+ result_index: index,
904
+ candidate_id: screeningCandidate.id || null
905
+ }
906
+ });
907
+
908
+ runControl.updateProgress({
909
+ card_count: cardNodeIds.length,
910
+ target_count: passTarget || (processUntilListEnd ? "all" : processedLimit),
911
+ target_pass_count: passTarget,
912
+ processed_limit: processedLimit,
913
+ processed: results.length,
914
+ screened: results.length,
915
+ detail_opened: results.filter(resultOpenedDetail).length,
916
+ llm_screened: results.filter((item) => item.detail?.llm_screening).length,
917
+ passed: results.filter((item) => item.screening.passed).length,
918
+ requested: requestedCount,
919
+ request_satisfied: requestSatisfiedCount,
920
+ request_skipped: requestSkippedCount,
921
+ unique_seen: compactInfiniteListState(listState).seen_count,
922
+ scroll_count: compactInfiniteListState(listState).scroll_count,
923
+ list_end_reason: listEndReason || null,
924
+ last_candidate_id: screeningCandidate.id || null,
925
+ last_candidate_key: candidateKey,
926
+ last_score: screening.score
927
+ });
928
+ runControl.checkpoint({
929
+ last_candidate: {
930
+ id: screeningCandidate.id || null,
931
+ key: candidateKey,
932
+ identity: screeningCandidate.identity || {},
933
+ screening: {
934
+ status: screening.status,
935
+ passed: screening.passed,
936
+ score: screening.score
937
+ }
938
+ }
939
+ });
940
+
941
+ if (delayMs > 0) {
942
+ await runControl.sleep(delayMs);
943
+ }
944
+ }
945
+
946
+ runControl.setPhase("chat:done");
947
+ return {
948
+ domain: "chat",
949
+ target_url: targetUrl,
950
+ card_count: cardNodeIds.length,
951
+ context_setup: contextSetup,
952
+ candidate_list: compactInfiniteListState(listState),
953
+ list_end_reason: listEndReason || null,
954
+ target_pass_count: passTarget,
955
+ process_until_list_end: Boolean(processUntilListEnd),
956
+ processed_limit: processedLimit,
957
+ detail_source: normalizedDetailSource,
958
+ processed: results.length,
959
+ screened: results.length,
960
+ detail_opened: results.filter(resultOpenedDetail).length,
961
+ llm_screened: results.filter((item) => item.detail?.llm_screening).length,
962
+ passed: results.filter((item) => item.screening.passed).length,
963
+ requested: requestedCount,
964
+ request_satisfied: requestSatisfiedCount,
965
+ request_skipped: requestSkippedCount,
966
+ results
967
+ };
968
+ }
969
+
970
+ export function createChatRunService({
971
+ lifecycle,
972
+ idPrefix = "chat",
973
+ workflow = runChatWorkflow
974
+ } = {}) {
975
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix });
976
+
977
+ function startChatRun({
978
+ client,
979
+ targetUrl = CHAT_TARGET_URL,
980
+ job = "",
981
+ startFrom = "all",
982
+ criteria = "",
983
+ maxCandidates = 5,
984
+ targetPassCount = null,
985
+ processUntilListEnd = false,
986
+ detailLimit = 0,
987
+ detailSource = "cascade",
988
+ closeResume = true,
989
+ requestResumeForPassed = false,
990
+ dryRunRequestCv = false,
991
+ greetingText = "Hi同学,能麻烦发下简历吗?",
992
+ delayMs = 0,
993
+ cardTimeoutMs = 90000,
994
+ readyTimeoutMs = 60000,
995
+ onlineResumeButtonTimeoutMs = 30000,
996
+ resumeDomTimeoutMs = 60000,
997
+ maxImagePages = 8,
998
+ imageWheelDeltaY = 650,
999
+ cvAcquisitionMode = "unknown",
1000
+ callLlmOnImage = false,
1001
+ llmConfig = null,
1002
+ llmTimeoutMs = 120000,
1003
+ llmImageLimit = 8,
1004
+ llmImageDetail = "high",
1005
+ listMaxScrolls = 20,
1006
+ listStableSignatureLimit = 2,
1007
+ listWheelDeltaY = 850,
1008
+ listSettleMs = 1200,
1009
+ listFallbackPoint = null,
1010
+ name = "chat-domain-run"
1011
+ } = {}) {
1012
+ if (!client) throw new Error("startChatRun requires a guarded CDP client");
1013
+ const normalizedDetailSource = normalizeDetailSource(detailSource);
1014
+ return manager.startRun({
1015
+ name,
1016
+ context: {
1017
+ domain: "chat",
1018
+ target_url: targetUrl,
1019
+ criteria_present: Boolean(criteria),
1020
+ job,
1021
+ start_from: startFrom,
1022
+ max_candidates: maxCandidates,
1023
+ target_pass_count: targetPassCount,
1024
+ process_until_list_end: Boolean(processUntilListEnd),
1025
+ detail_limit: detailLimit,
1026
+ detail_source: normalizedDetailSource,
1027
+ close_resume: closeResume,
1028
+ cv_acquisition_mode: cvAcquisitionMode,
1029
+ call_llm_on_image: Boolean(callLlmOnImage),
1030
+ max_image_pages: maxImagePages,
1031
+ image_wheel_delta_y: imageWheelDeltaY,
1032
+ list_max_scrolls: listMaxScrolls,
1033
+ list_stable_signature_limit: listStableSignatureLimit,
1034
+ list_wheel_delta_y: listWheelDeltaY,
1035
+ list_settle_ms: listSettleMs,
1036
+ list_fallback_point: listFallbackPoint,
1037
+ online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs
1038
+ },
1039
+ progress: {
1040
+ card_count: 0,
1041
+ target_count: targetPassCount || (processUntilListEnd ? "all" : Math.max(1, Number(maxCandidates) || 1)),
1042
+ target_pass_count: targetPassCount,
1043
+ processed_limit: Math.max(1, Number(maxCandidates) || 1),
1044
+ processed: 0,
1045
+ screened: 0,
1046
+ detail_opened: 0,
1047
+ llm_screened: 0,
1048
+ passed: 0,
1049
+ requested: 0,
1050
+ request_satisfied: 0,
1051
+ request_skipped: 0
1052
+ },
1053
+ checkpoint: {},
1054
+ task: (runControl) => workflow({
1055
+ client,
1056
+ targetUrl,
1057
+ job,
1058
+ startFrom,
1059
+ criteria,
1060
+ maxCandidates,
1061
+ targetPassCount,
1062
+ processUntilListEnd,
1063
+ detailLimit,
1064
+ detailSource: normalizedDetailSource,
1065
+ closeResume,
1066
+ requestResumeForPassed,
1067
+ dryRunRequestCv,
1068
+ greetingText,
1069
+ delayMs,
1070
+ cardTimeoutMs,
1071
+ readyTimeoutMs,
1072
+ onlineResumeButtonTimeoutMs,
1073
+ resumeDomTimeoutMs,
1074
+ maxImagePages,
1075
+ imageWheelDeltaY,
1076
+ cvAcquisitionMode,
1077
+ callLlmOnImage,
1078
+ llmConfig,
1079
+ llmTimeoutMs,
1080
+ llmImageLimit,
1081
+ llmImageDetail,
1082
+ listMaxScrolls,
1083
+ listStableSignatureLimit,
1084
+ listWheelDeltaY,
1085
+ listSettleMs,
1086
+ listFallbackPoint
1087
+ }, runControl)
1088
+ });
1089
+ }
1090
+
1091
+ return {
1092
+ startChatRun,
1093
+ getChatRun: manager.getRun,
1094
+ pauseChatRun: manager.pauseRun,
1095
+ resumeChatRun: manager.resumeRun,
1096
+ cancelChatRun: manager.cancelRun,
1097
+ waitForChatRun: manager.waitForRun,
1098
+ listChatRuns: manager.listRuns,
1099
+ manager
1100
+ };
1101
+ }