@reconcrap/boss-recommend-mcp 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +195 -195
- package/src/adapters.js +1876 -1806
- package/src/index.js +1254 -1254
- package/src/pipeline.js +918 -793
- package/src/run-state.js +351 -351
- package/src/test-adapters-runtime.js +163 -163
- package/src/test-index-async.js +236 -236
- package/src/test-pipeline.js +103 -0
- package/src/test-run-state.js +152 -152
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +100 -14
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +508 -452
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +245 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +811 -811
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +201 -201
package/src/adapters.js
CHANGED
|
@@ -3,1838 +3,1908 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|| normalized.endsWith("/windows/
|
|
80
|
-
|| normalized.endsWith("/
|
|
81
|
-
|| normalized.endsWith("/program files
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|| normalized
|
|
88
|
-
|| normalized
|
|
89
|
-
|| normalized
|
|
90
|
-
|| normalized
|
|
91
|
-
|| normalized
|
|
92
|
-
|| normalized
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|| root
|
|
103
|
-
||
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (normalized.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
candidateMap.
|
|
170
|
-
|
|
171
|
-
candidateMap.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (!
|
|
262
|
-
if (!
|
|
263
|
-
if (missing.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
&&
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
path.join(process.env.
|
|
304
|
-
path.join(process.env
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
"/usr/bin/google-chrome
|
|
316
|
-
"/usr/bin/
|
|
317
|
-
"/usr/bin/chromium",
|
|
318
|
-
"/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
`--
|
|
373
|
-
|
|
374
|
-
"--no-
|
|
375
|
-
"--
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
path.join(screenDir, "boss-recommend-screen-cli.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
path.join(searchDir, "src", "cli.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
let
|
|
456
|
-
let
|
|
457
|
-
let
|
|
458
|
-
let
|
|
459
|
-
let
|
|
460
|
-
let
|
|
461
|
-
let
|
|
462
|
-
let
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
let
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
settled
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
emitLine("
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
tracker.
|
|
862
|
-
tracker.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
nextTracker.
|
|
891
|
-
nextTracker.
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
nextTracker.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
nextTracker.
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (result.
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
checks,
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
.
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
"
|
|
1067
|
-
"
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
.
|
|
1072
|
-
.
|
|
1073
|
-
.
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|| failed.has("
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
.
|
|
1196
|
-
.
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
||
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
let
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
|
|
6
|
+
import CDP from "chrome-remote-interface";
|
|
7
|
+
|
|
8
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
9
|
+
const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
|
|
10
|
+
const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
|
|
11
|
+
const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
12
|
+
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
13
|
+
const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
14
|
+
const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
15
|
+
const screenConfigTemplateDefaults = {
|
|
16
|
+
baseUrl: "https://api.openai.com/v1",
|
|
17
|
+
apiKey: "replace-with-openai-api-key",
|
|
18
|
+
model: "gpt-4.1-mini"
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
function getCodexHome() {
|
|
23
|
+
return process.env.CODEX_HOME
|
|
24
|
+
? path.resolve(process.env.CODEX_HOME)
|
|
25
|
+
: path.join(os.homedir(), ".codex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getStateHome() {
|
|
29
|
+
return process.env.BOSS_RECOMMEND_HOME
|
|
30
|
+
? path.resolve(process.env.BOSS_RECOMMEND_HOME)
|
|
31
|
+
: path.join(os.homedir(), ".boss-recommend-mcp");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getUserConfigPath() {
|
|
35
|
+
return path.join(getStateHome(), "screening-config.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getLegacyUserConfigPath() {
|
|
39
|
+
return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getDesktopDir() {
|
|
43
|
+
return path.join(os.homedir(), "Desktop");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureDir(targetPath) {
|
|
47
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pathExists(targetPath) {
|
|
51
|
+
try {
|
|
52
|
+
return fs.existsSync(targetPath);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parsePositiveInteger(raw) {
|
|
59
|
+
const value = Number.parseInt(String(raw || ""), 10);
|
|
60
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeText(value) {
|
|
64
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRootDirectory(targetPath) {
|
|
68
|
+
const resolved = path.resolve(String(targetPath || ""));
|
|
69
|
+
const parsed = path.parse(resolved);
|
|
70
|
+
return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
|
|
74
|
+
const root = path.resolve(String(workspaceRoot || ""));
|
|
75
|
+
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
76
|
+
if (process.platform === "win32") {
|
|
77
|
+
return (
|
|
78
|
+
normalized.endsWith("/windows")
|
|
79
|
+
|| normalized.endsWith("/windows/system32")
|
|
80
|
+
|| normalized.endsWith("/windows/syswow64")
|
|
81
|
+
|| normalized.endsWith("/program files")
|
|
82
|
+
|| normalized.endsWith("/program files (x86)")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
normalized === "/system"
|
|
87
|
+
|| normalized.startsWith("/system/")
|
|
88
|
+
|| normalized === "/usr"
|
|
89
|
+
|| normalized.startsWith("/usr/")
|
|
90
|
+
|| normalized === "/bin"
|
|
91
|
+
|| normalized.startsWith("/bin/")
|
|
92
|
+
|| normalized === "/sbin"
|
|
93
|
+
|| normalized.startsWith("/sbin/")
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function shouldIgnoreWorkspaceConfigRoot(workspaceRoot) {
|
|
98
|
+
const root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
99
|
+
const home = path.resolve(os.homedir());
|
|
100
|
+
return (
|
|
101
|
+
isEphemeralNpxWorkspaceRoot(root)
|
|
102
|
+
|| isRootDirectory(root)
|
|
103
|
+
|| root.toLowerCase() === home.toLowerCase()
|
|
104
|
+
|| isSystemDirectoryWorkspaceRoot(root)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveWorkspaceConfigCandidates(workspaceRoot) {
|
|
109
|
+
const root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
110
|
+
if (shouldIgnoreWorkspaceConfigRoot(root)) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const directPath = path.join(root, "config", "screening-config.json");
|
|
114
|
+
const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
|
|
115
|
+
const candidates = [directPath];
|
|
116
|
+
if (path.basename(root).toLowerCase() !== "boss-recommend-mcp") {
|
|
117
|
+
candidates.push(nestedPath);
|
|
118
|
+
}
|
|
119
|
+
return Array.from(new Set(candidates));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function serializeDegreeSelection(value) {
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
125
|
+
return normalized.length ? normalized.join(",") : "不限";
|
|
126
|
+
}
|
|
127
|
+
const normalized = String(value || "").trim();
|
|
128
|
+
return normalized || "不限";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function serializeSchoolTagSelection(value) {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
134
|
+
if (!normalized.length) return "不限";
|
|
135
|
+
if (normalized.includes("不限")) {
|
|
136
|
+
return normalized.length === 1
|
|
137
|
+
? "不限"
|
|
138
|
+
: normalized.filter((item) => item !== "不限").join(",");
|
|
139
|
+
}
|
|
140
|
+
return normalized.join(",");
|
|
141
|
+
}
|
|
142
|
+
const normalized = String(value || "").trim();
|
|
143
|
+
return normalized || "不限";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
|
|
147
|
+
const root = path.resolve(String(workspaceRoot || ""));
|
|
148
|
+
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
149
|
+
return (
|
|
150
|
+
normalized.includes("/appdata/local/npm-cache/_npx/")
|
|
151
|
+
|| normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildScreenConfigCandidateMap(workspaceRoot) {
|
|
156
|
+
return {
|
|
157
|
+
env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
|
|
158
|
+
? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
|
|
159
|
+
: null,
|
|
160
|
+
workspace_paths: resolveWorkspaceConfigCandidates(workspaceRoot),
|
|
161
|
+
user_path: getUserConfigPath(),
|
|
162
|
+
legacy_path: getLegacyUserConfigPath()
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveScreenConfigCandidates(workspaceRoot) {
|
|
167
|
+
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
168
|
+
return [
|
|
169
|
+
candidateMap.env_path,
|
|
170
|
+
candidateMap.user_path,
|
|
171
|
+
...candidateMap.workspace_paths,
|
|
172
|
+
candidateMap.legacy_path
|
|
173
|
+
].filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function canWriteDirectory(targetDir) {
|
|
177
|
+
try {
|
|
178
|
+
ensureDir(targetDir);
|
|
179
|
+
fs.accessSync(targetDir, fs.constants.W_OK);
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveWritableScreenConfigPath(workspaceRoot) {
|
|
187
|
+
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
188
|
+
const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
|
|
189
|
+
if (candidateMap.env_path) {
|
|
190
|
+
return candidateMap.env_path;
|
|
191
|
+
}
|
|
192
|
+
if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
|
|
193
|
+
return candidateMap.user_path;
|
|
194
|
+
}
|
|
195
|
+
if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
|
|
196
|
+
return workspacePreferred;
|
|
197
|
+
}
|
|
198
|
+
if (workspacePreferred) {
|
|
199
|
+
return workspacePreferred;
|
|
200
|
+
}
|
|
201
|
+
return candidateMap.user_path || candidateMap.legacy_path;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveScreenConfigPath(workspaceRoot) {
|
|
205
|
+
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
206
|
+
if (candidateMap.env_path) {
|
|
207
|
+
return candidateMap.env_path;
|
|
208
|
+
}
|
|
209
|
+
if (candidateMap.user_path && pathExists(candidateMap.user_path)) {
|
|
210
|
+
return candidateMap.user_path;
|
|
211
|
+
}
|
|
212
|
+
const existingWorkspacePath = candidateMap.workspace_paths.find((item) => pathExists(item));
|
|
213
|
+
if (existingWorkspacePath) {
|
|
214
|
+
return existingWorkspacePath;
|
|
215
|
+
}
|
|
216
|
+
const writablePath = resolveWritableScreenConfigPath(workspaceRoot);
|
|
217
|
+
if (writablePath) {
|
|
218
|
+
return writablePath;
|
|
219
|
+
}
|
|
220
|
+
return candidateMap.legacy_path;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function getScreenConfigResolution(workspaceRoot) {
|
|
224
|
+
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
225
|
+
const candidate_paths = resolveScreenConfigCandidates(workspaceRoot);
|
|
226
|
+
const resolved_path = resolveScreenConfigPath(workspaceRoot) || null;
|
|
227
|
+
const workspace_root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
228
|
+
return {
|
|
229
|
+
resolved_path,
|
|
230
|
+
candidate_paths,
|
|
231
|
+
workspace_root,
|
|
232
|
+
workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRoot),
|
|
233
|
+
workspace_ignored_for_config: shouldIgnoreWorkspaceConfigRoot(workspace_root),
|
|
234
|
+
writable_path: resolveWritableScreenConfigPath(workspaceRoot),
|
|
235
|
+
legacy_path: candidateMap.legacy_path
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function readJsonFile(filePath) {
|
|
240
|
+
if (!filePath || !pathExists(filePath)) return null;
|
|
241
|
+
try {
|
|
242
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
243
|
+
return JSON.parse(raw);
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function validateScreenConfig(config) {
|
|
250
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
reason: "INVALID_OR_MISSING_CONFIG",
|
|
254
|
+
message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const baseUrl = String(config.baseUrl || "").trim();
|
|
258
|
+
const apiKey = String(config.apiKey || "").trim();
|
|
259
|
+
const model = String(config.model || "").trim();
|
|
260
|
+
const missing = [];
|
|
261
|
+
if (!baseUrl) missing.push("baseUrl");
|
|
262
|
+
if (!apiKey) missing.push("apiKey");
|
|
263
|
+
if (!model) missing.push("model");
|
|
264
|
+
if (missing.length > 0) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
reason: "MISSING_REQUIRED_FIELDS",
|
|
268
|
+
message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
reason: "PLACEHOLDER_API_KEY",
|
|
275
|
+
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
baseUrl === screenConfigTemplateDefaults.baseUrl
|
|
280
|
+
&& apiKey === screenConfigTemplateDefaults.apiKey
|
|
281
|
+
&& model === screenConfigTemplateDefaults.model
|
|
282
|
+
) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
reason: "PLACEHOLDER_TEMPLATE_VALUES",
|
|
286
|
+
message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resolveWorkspaceDebugPort(workspaceRoot) {
|
|
293
|
+
const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
|
|
294
|
+
if (fromEnv) return fromEnv;
|
|
295
|
+
const config = readJsonFile(resolveScreenConfigPath(workspaceRoot));
|
|
296
|
+
return parsePositiveInteger(config?.debugPort) || 9222;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getDefaultChromeExecutableCandidates() {
|
|
300
|
+
const candidates = [process.env.BOSS_RECOMMEND_CHROME_PATH].filter(Boolean);
|
|
301
|
+
if (process.platform === "win32") {
|
|
302
|
+
candidates.push(
|
|
303
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
304
|
+
path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
305
|
+
path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
|
|
306
|
+
);
|
|
307
|
+
} else if (process.platform === "darwin") {
|
|
308
|
+
candidates.push(
|
|
309
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
310
|
+
path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
|
|
311
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
candidates.push(
|
|
315
|
+
"/usr/bin/google-chrome",
|
|
316
|
+
"/usr/bin/google-chrome-stable",
|
|
317
|
+
"/usr/bin/chromium-browser",
|
|
318
|
+
"/usr/bin/chromium",
|
|
319
|
+
"/snap/bin/chromium"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return Array.from(new Set(candidates.filter(Boolean)));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getChromeExecutable() {
|
|
326
|
+
const candidates = getDefaultChromeExecutableCandidates();
|
|
327
|
+
return candidates.find((candidate) => pathExists(candidate)) || null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getChromeUserDataDir(port) {
|
|
331
|
+
const profileDir = resolveDefaultChromeUserDataDir(port);
|
|
332
|
+
ensureDir(profileDir);
|
|
333
|
+
return profileDir;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getSharedChromeUserDataDir(port) {
|
|
337
|
+
return path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getLegacyRecruitChromeUserDataDir(port) {
|
|
341
|
+
return path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getLegacyRecommendChromeUserDataDir(port) {
|
|
345
|
+
return path.join(getStateHome(), `chrome-profile-${port}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveDefaultChromeUserDataDir(port) {
|
|
349
|
+
const sharedPath = getSharedChromeUserDataDir(port);
|
|
350
|
+
if (pathExists(sharedPath)) {
|
|
351
|
+
return sharedPath;
|
|
352
|
+
}
|
|
353
|
+
const legacyPaths = [
|
|
354
|
+
getLegacyRecruitChromeUserDataDir(port),
|
|
355
|
+
getLegacyRecommendChromeUserDataDir(port)
|
|
356
|
+
];
|
|
357
|
+
const legacyExisting = legacyPaths.find((candidate) => pathExists(candidate));
|
|
358
|
+
return legacyExisting || sharedPath;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function launchChromeWithDebugPort(port) {
|
|
362
|
+
const chromePath = getChromeExecutable();
|
|
363
|
+
if (!chromePath) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
code: "CHROME_EXECUTABLE_NOT_FOUND",
|
|
367
|
+
message: "未找到 Chrome 可执行文件,请安装 Chrome 或设置 BOSS_RECOMMEND_CHROME_PATH。"
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const userDataDir = getChromeUserDataDir(port);
|
|
371
|
+
const args = [
|
|
372
|
+
`--remote-debugging-port=${port}`,
|
|
373
|
+
`--user-data-dir=${userDataDir}`,
|
|
374
|
+
"--no-first-run",
|
|
375
|
+
"--no-default-browser-check",
|
|
376
|
+
"--new-window",
|
|
377
|
+
bossRecommendUrl
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const child = spawn(chromePath, args, {
|
|
382
|
+
detached: true,
|
|
383
|
+
stdio: "ignore",
|
|
384
|
+
windowsHide: false
|
|
385
|
+
});
|
|
386
|
+
child.unref();
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
code: "CHROME_LAUNCHED",
|
|
390
|
+
chrome_path: chromePath,
|
|
391
|
+
user_data_dir: userDataDir
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
code: "CHROME_LAUNCH_FAILED",
|
|
397
|
+
message: error.message || "Chrome 启动失败。"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveRecommendSearchCliDir(workspaceRoot) {
|
|
403
|
+
const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
|
|
404
|
+
if (pathExists(localDir)) return localDir;
|
|
405
|
+
const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-search-cli");
|
|
406
|
+
if (pathExists(vendoredDir)) return vendoredDir;
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function resolveRecommendScreenCliDir(workspaceRoot) {
|
|
411
|
+
const localDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
|
|
412
|
+
if (pathExists(localDir)) return localDir;
|
|
413
|
+
const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-screen-cli");
|
|
414
|
+
if (pathExists(vendoredDir)) return vendoredDir;
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function resolveRecommendScreenCliEntry(screenDir) {
|
|
419
|
+
const candidates = [
|
|
420
|
+
path.join(screenDir, "boss-recommend-screen-cli.cjs"),
|
|
421
|
+
path.join(screenDir, "boss-recommend-screen-cli.js")
|
|
422
|
+
];
|
|
423
|
+
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function resolveRecommendSearchCliEntry(searchDir) {
|
|
427
|
+
const candidates = [
|
|
428
|
+
path.join(searchDir, "src", "cli.js"),
|
|
429
|
+
path.join(searchDir, "src", "cli.cjs")
|
|
430
|
+
];
|
|
431
|
+
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function safeInvokeCallback(callback, payload) {
|
|
435
|
+
if (typeof callback !== "function") return;
|
|
436
|
+
try {
|
|
437
|
+
callback(payload);
|
|
438
|
+
} catch {
|
|
439
|
+
// Ignore callback errors to keep pipeline runtime stable.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function runProcess({
|
|
444
|
+
command,
|
|
445
|
+
args,
|
|
446
|
+
cwd,
|
|
447
|
+
timeoutMs,
|
|
448
|
+
onOutput,
|
|
449
|
+
onLine,
|
|
450
|
+
onHeartbeat,
|
|
451
|
+
heartbeatIntervalMs = 10_000,
|
|
452
|
+
signal
|
|
453
|
+
}) {
|
|
454
|
+
return new Promise((resolve) => {
|
|
455
|
+
let stdout = "";
|
|
456
|
+
let stderr = "";
|
|
457
|
+
let stdoutLineBuffer = "";
|
|
458
|
+
let stderrLineBuffer = "";
|
|
459
|
+
let settled = false;
|
|
460
|
+
let timer = null;
|
|
461
|
+
let heartbeatTimer = null;
|
|
462
|
+
let abortedBySignal = Boolean(signal?.aborted);
|
|
463
|
+
let abortListener = null;
|
|
464
|
+
|
|
465
|
+
function notifyHeartbeat(source) {
|
|
466
|
+
safeInvokeCallback(onHeartbeat, {
|
|
467
|
+
source,
|
|
468
|
+
command,
|
|
469
|
+
args,
|
|
470
|
+
cwd,
|
|
471
|
+
at: new Date().toISOString()
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function emitLine(stream, line) {
|
|
476
|
+
const normalized = String(line ?? "").replace(/\r$/, "");
|
|
477
|
+
if (!normalized) return;
|
|
478
|
+
safeInvokeCallback(onLine, {
|
|
479
|
+
stream,
|
|
480
|
+
line: normalized,
|
|
481
|
+
at: new Date().toISOString()
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function pushLineBuffer(stream, chunkText) {
|
|
486
|
+
if (stream === "stdout") {
|
|
487
|
+
stdoutLineBuffer += chunkText;
|
|
488
|
+
} else {
|
|
489
|
+
stderrLineBuffer += chunkText;
|
|
490
|
+
}
|
|
491
|
+
let buffer = stream === "stdout" ? stdoutLineBuffer : stderrLineBuffer;
|
|
492
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
493
|
+
while (newlineIndex !== -1) {
|
|
494
|
+
emitLine(stream, buffer.slice(0, newlineIndex));
|
|
495
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
496
|
+
newlineIndex = buffer.indexOf("\n");
|
|
497
|
+
}
|
|
498
|
+
if (stream === "stdout") {
|
|
499
|
+
stdoutLineBuffer = buffer;
|
|
500
|
+
} else {
|
|
501
|
+
stderrLineBuffer = buffer;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function finish(payload) {
|
|
506
|
+
if (settled) return;
|
|
507
|
+
settled = true;
|
|
508
|
+
if (timer) clearTimeout(timer);
|
|
509
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
510
|
+
if (signal && typeof signal.removeEventListener === "function" && abortListener) {
|
|
511
|
+
signal.removeEventListener("abort", abortListener);
|
|
512
|
+
}
|
|
513
|
+
emitLine("stdout", stdoutLineBuffer);
|
|
514
|
+
emitLine("stderr", stderrLineBuffer);
|
|
515
|
+
stdoutLineBuffer = "";
|
|
516
|
+
stderrLineBuffer = "";
|
|
517
|
+
resolve(payload);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (abortedBySignal) {
|
|
521
|
+
finish({
|
|
522
|
+
code: -1,
|
|
523
|
+
stdout,
|
|
524
|
+
stderr: "Process aborted before spawn",
|
|
525
|
+
error_code: "ABORTED"
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let child;
|
|
531
|
+
try {
|
|
532
|
+
child = spawn(command, args, {
|
|
533
|
+
cwd,
|
|
534
|
+
windowsHide: true,
|
|
535
|
+
shell: false,
|
|
536
|
+
env: process.env
|
|
537
|
+
});
|
|
538
|
+
} catch (error) {
|
|
539
|
+
finish({
|
|
540
|
+
code: -1,
|
|
541
|
+
stdout,
|
|
542
|
+
stderr: error.message,
|
|
543
|
+
error_code: error.code || "SPAWN_FAILED"
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (signal && typeof signal.addEventListener === "function") {
|
|
549
|
+
abortListener = () => {
|
|
550
|
+
abortedBySignal = true;
|
|
551
|
+
try {
|
|
552
|
+
child.kill();
|
|
553
|
+
} catch {}
|
|
554
|
+
};
|
|
555
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
559
|
+
timer = setTimeout(() => {
|
|
560
|
+
try {
|
|
561
|
+
child.kill();
|
|
562
|
+
} catch {}
|
|
563
|
+
finish({
|
|
564
|
+
code: -1,
|
|
565
|
+
stdout,
|
|
566
|
+
stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`.trim(),
|
|
567
|
+
error_code: "TIMEOUT"
|
|
568
|
+
});
|
|
569
|
+
}, timeoutMs);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (Number.isFinite(heartbeatIntervalMs) && heartbeatIntervalMs > 0) {
|
|
573
|
+
heartbeatTimer = setInterval(() => {
|
|
574
|
+
notifyHeartbeat("timer");
|
|
575
|
+
}, heartbeatIntervalMs);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
child.stdout.on("data", (chunk) => {
|
|
579
|
+
const text = chunk.toString();
|
|
580
|
+
stdout += text;
|
|
581
|
+
pushLineBuffer("stdout", text);
|
|
582
|
+
safeInvokeCallback(onOutput, {
|
|
583
|
+
stream: "stdout",
|
|
584
|
+
text,
|
|
585
|
+
at: new Date().toISOString()
|
|
586
|
+
});
|
|
587
|
+
notifyHeartbeat("stdout");
|
|
588
|
+
});
|
|
589
|
+
child.stderr.on("data", (chunk) => {
|
|
590
|
+
const text = chunk.toString();
|
|
591
|
+
stderr += text;
|
|
592
|
+
pushLineBuffer("stderr", text);
|
|
593
|
+
safeInvokeCallback(onOutput, {
|
|
594
|
+
stream: "stderr",
|
|
595
|
+
text,
|
|
596
|
+
at: new Date().toISOString()
|
|
597
|
+
});
|
|
598
|
+
notifyHeartbeat("stderr");
|
|
599
|
+
});
|
|
600
|
+
child.on("close", (code) => {
|
|
601
|
+
if (abortedBySignal) {
|
|
602
|
+
finish({
|
|
603
|
+
code: -1,
|
|
604
|
+
stdout,
|
|
605
|
+
stderr: `${stderr}\nProcess aborted by signal`.trim(),
|
|
606
|
+
error_code: "ABORTED"
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
finish({ code, stdout, stderr });
|
|
611
|
+
});
|
|
612
|
+
child.on("error", (error) => {
|
|
613
|
+
finish({
|
|
614
|
+
code: -1,
|
|
615
|
+
stdout,
|
|
616
|
+
stderr: `${stderr}\n${error.message}`.trim(),
|
|
617
|
+
error_code: error.code || "SPAWN_FAILED"
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function runProcessSync({ command, args, cwd }) {
|
|
624
|
+
try {
|
|
625
|
+
const result = spawnSync(command, args, {
|
|
626
|
+
cwd,
|
|
627
|
+
windowsHide: true,
|
|
628
|
+
shell: false,
|
|
629
|
+
env: process.env,
|
|
630
|
+
encoding: "utf8"
|
|
631
|
+
});
|
|
632
|
+
const stdout = String(result.stdout || "").trim();
|
|
633
|
+
const stderr = String(result.stderr || "").trim();
|
|
634
|
+
return {
|
|
635
|
+
ok: result.status === 0,
|
|
636
|
+
status: result.status,
|
|
637
|
+
stdout,
|
|
638
|
+
stderr,
|
|
639
|
+
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
|
640
|
+
error_code: result.error?.code || null,
|
|
641
|
+
error_message: result.error?.message || null
|
|
642
|
+
};
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return {
|
|
645
|
+
ok: false,
|
|
646
|
+
status: -1,
|
|
647
|
+
stdout: "",
|
|
648
|
+
stderr: "",
|
|
649
|
+
output: "",
|
|
650
|
+
error_code: error.code || "SPAWN_FAILED",
|
|
651
|
+
error_message: error.message || String(error)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function parseMajorVersion(raw) {
|
|
657
|
+
const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
|
|
658
|
+
if (!match) return null;
|
|
659
|
+
const major = Number.parseInt(match[1], 10);
|
|
660
|
+
return Number.isFinite(major) ? major : null;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function buildNodeCommandCheck() {
|
|
664
|
+
const probe = runProcessSync({
|
|
665
|
+
command: "node",
|
|
666
|
+
args: ["--version"]
|
|
667
|
+
});
|
|
668
|
+
const major = parseMajorVersion(probe.output);
|
|
669
|
+
const versionOk = Number.isInteger(major) && major >= 18;
|
|
670
|
+
return {
|
|
671
|
+
key: "node_cli",
|
|
672
|
+
ok: probe.ok && versionOk,
|
|
673
|
+
path: "node --version",
|
|
674
|
+
message: probe.ok
|
|
675
|
+
? (versionOk
|
|
676
|
+
? `Node 命令可用 (${probe.output || "unknown version"})`
|
|
677
|
+
: `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
|
|
678
|
+
: `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function detectPythonCommand() {
|
|
683
|
+
const python = runProcessSync({
|
|
684
|
+
command: "python",
|
|
685
|
+
args: ["--version"]
|
|
686
|
+
});
|
|
687
|
+
if (python.ok) {
|
|
688
|
+
return {
|
|
689
|
+
ok: true,
|
|
690
|
+
command: "python",
|
|
691
|
+
probe: python
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const python3 = runProcessSync({
|
|
695
|
+
command: "python3",
|
|
696
|
+
args: ["--version"]
|
|
697
|
+
});
|
|
698
|
+
if (python3.ok) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
command: null,
|
|
702
|
+
probe: python,
|
|
703
|
+
fallback: python3
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
ok: false,
|
|
708
|
+
command: null,
|
|
709
|
+
probe: python,
|
|
710
|
+
fallback: null
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function buildPythonCommandCheck() {
|
|
715
|
+
const detected = detectPythonCommand();
|
|
716
|
+
if (detected.ok) {
|
|
717
|
+
return {
|
|
718
|
+
key: "python_cli",
|
|
719
|
+
ok: true,
|
|
720
|
+
path: "python --version",
|
|
721
|
+
message: `Python 命令可用 (${detected.probe.output || "unknown version"})`
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (detected.fallback) {
|
|
725
|
+
return {
|
|
726
|
+
key: "python_cli",
|
|
727
|
+
ok: false,
|
|
728
|
+
path: "python --version",
|
|
729
|
+
message: `检测到 ${detected.fallback.output || "python3"},但当前流程依赖 python 命令;请创建 python 别名后重试。`
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
key: "python_cli",
|
|
734
|
+
ok: false,
|
|
735
|
+
path: "python --version",
|
|
736
|
+
message: "未找到 python 命令,请安装 Python 并确保 python 在 PATH 中。"
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function buildPillowCheck() {
|
|
741
|
+
const detected = detectPythonCommand();
|
|
742
|
+
if (!detected.ok || !detected.command) {
|
|
743
|
+
return {
|
|
744
|
+
key: "python_pillow",
|
|
745
|
+
ok: false,
|
|
746
|
+
path: "python -c \"import PIL\"",
|
|
747
|
+
message: "无法校验 Pillow:python 命令不可用。"
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const probe = runProcessSync({
|
|
751
|
+
command: detected.command,
|
|
752
|
+
args: ["-c", "import PIL, PIL.Image; print(PIL.__version__)"]
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
key: "python_pillow",
|
|
756
|
+
ok: probe.ok,
|
|
757
|
+
path: `${detected.command} -c "import PIL"`,
|
|
758
|
+
message: probe.ok
|
|
759
|
+
? `Pillow 可用 (${probe.output || "version unknown"})`
|
|
760
|
+
: "Pillow 未安装。请执行 `python -m pip install pillow`。"
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function buildNodePackageCheck({ key, moduleName, cwd, missingMessage }) {
|
|
765
|
+
if (!cwd || !pathExists(cwd)) {
|
|
766
|
+
return {
|
|
767
|
+
key,
|
|
768
|
+
ok: false,
|
|
769
|
+
path: moduleName,
|
|
770
|
+
module: moduleName,
|
|
771
|
+
install_cwd: null,
|
|
772
|
+
message: missingMessage
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const probe = runProcessSync({
|
|
776
|
+
command: "node",
|
|
777
|
+
args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
|
|
778
|
+
cwd
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
key,
|
|
782
|
+
ok: probe.ok,
|
|
783
|
+
path: moduleName,
|
|
784
|
+
module: moduleName,
|
|
785
|
+
install_cwd: cwd,
|
|
786
|
+
message: probe.ok
|
|
787
|
+
? `${moduleName} npm 依赖可用`
|
|
788
|
+
: `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function buildRuntimeDependencyChecks({ searchDir, screenDir }) {
|
|
793
|
+
return [
|
|
794
|
+
buildNodeCommandCheck(),
|
|
795
|
+
buildPythonCommandCheck(),
|
|
796
|
+
buildPillowCheck(),
|
|
797
|
+
buildNodePackageCheck({
|
|
798
|
+
key: "npm_dep_chrome_remote_interface_search",
|
|
799
|
+
moduleName: "chrome-remote-interface",
|
|
800
|
+
cwd: searchDir,
|
|
801
|
+
missingMessage: "无法校验 chrome-remote-interface:boss-recommend-search-cli 目录不存在。"
|
|
802
|
+
}),
|
|
803
|
+
buildNodePackageCheck({
|
|
804
|
+
key: "npm_dep_chrome_remote_interface_screen",
|
|
805
|
+
moduleName: "chrome-remote-interface",
|
|
806
|
+
cwd: screenDir,
|
|
807
|
+
missingMessage: "无法校验 chrome-remote-interface:boss-recommend-screen-cli 目录不存在。"
|
|
808
|
+
}),
|
|
809
|
+
buildNodePackageCheck({
|
|
810
|
+
key: "npm_dep_ws",
|
|
811
|
+
moduleName: "ws",
|
|
812
|
+
cwd: screenDir,
|
|
813
|
+
missingMessage: "无法校验 ws:boss-recommend-screen-cli 目录不存在。"
|
|
814
|
+
})
|
|
815
|
+
];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function parseJsonOutput(text) {
|
|
819
|
+
const trimmed = String(text || "").trim();
|
|
820
|
+
if (!trimmed) return null;
|
|
821
|
+
try {
|
|
822
|
+
return JSON.parse(trimmed);
|
|
823
|
+
} catch {}
|
|
824
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
825
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
826
|
+
try {
|
|
827
|
+
return JSON.parse(lines[index]);
|
|
828
|
+
} catch {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function createScreenProgressTracker(currentTracker = {}) {
|
|
836
|
+
const outcome = String(currentTracker.outcome || "").trim();
|
|
837
|
+
return {
|
|
838
|
+
candidate_index: Number.isInteger(currentTracker.candidate_index) ? currentTracker.candidate_index : null,
|
|
839
|
+
outcome: outcome === "pass" || outcome === "skip" ? outcome : null,
|
|
840
|
+
action_failed: currentTracker.action_failed === true
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function finalizeCandidateProgress(progress, tracker) {
|
|
845
|
+
if (!Number.isInteger(tracker.candidate_index)) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
let changed = false;
|
|
850
|
+
if (tracker.action_failed === true) {
|
|
851
|
+
progress.skipped += 1;
|
|
852
|
+
changed = true;
|
|
853
|
+
} else if (tracker.outcome === "pass") {
|
|
854
|
+
progress.passed += 1;
|
|
855
|
+
changed = true;
|
|
856
|
+
} else if (tracker.outcome === "skip") {
|
|
857
|
+
progress.skipped += 1;
|
|
858
|
+
changed = true;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
tracker.candidate_index = null;
|
|
862
|
+
tracker.outcome = null;
|
|
863
|
+
tracker.action_failed = false;
|
|
864
|
+
return changed;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function parseScreenProgressLine(line, currentProgress = {}, currentTracker = {}) {
|
|
868
|
+
const normalizedLine = String(line || "").replace(/\s+/g, " ").trim();
|
|
869
|
+
if (!normalizedLine) return null;
|
|
870
|
+
|
|
871
|
+
const nextProgress = {
|
|
872
|
+
processed: Number.isInteger(currentProgress.processed) ? currentProgress.processed : 0,
|
|
873
|
+
passed: Number.isInteger(currentProgress.passed) ? currentProgress.passed : 0,
|
|
874
|
+
skipped: Number.isInteger(currentProgress.skipped) ? currentProgress.skipped : 0,
|
|
875
|
+
greet_count: Number.isInteger(currentProgress.greet_count) ? currentProgress.greet_count : 0
|
|
876
|
+
};
|
|
877
|
+
const nextTracker = createScreenProgressTracker(currentTracker);
|
|
878
|
+
|
|
879
|
+
let changed = false;
|
|
880
|
+
const processedMatch = normalizedLine.match(/处理第\s*(\d+)\s*位候选人/u);
|
|
881
|
+
if (processedMatch) {
|
|
882
|
+
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
883
|
+
changed = true;
|
|
884
|
+
}
|
|
885
|
+
const processed = Number.parseInt(processedMatch[1], 10);
|
|
886
|
+
if (Number.isInteger(processed) && processed >= 0 && processed !== nextProgress.processed) {
|
|
887
|
+
nextProgress.processed = processed;
|
|
888
|
+
changed = true;
|
|
889
|
+
}
|
|
890
|
+
nextTracker.candidate_index = processed;
|
|
891
|
+
nextTracker.outcome = null;
|
|
892
|
+
nextTracker.action_failed = false;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (/筛选结果:\s*通过/u.test(normalizedLine)) {
|
|
896
|
+
if (nextTracker.outcome !== "pass" || nextTracker.action_failed) {
|
|
897
|
+
changed = true;
|
|
898
|
+
}
|
|
899
|
+
nextTracker.outcome = "pass";
|
|
900
|
+
nextTracker.action_failed = false;
|
|
901
|
+
} else if (/筛选结果:\s*不通过/u.test(normalizedLine)) {
|
|
902
|
+
if (nextTracker.outcome !== "skip" || nextTracker.action_failed) {
|
|
903
|
+
changed = true;
|
|
904
|
+
}
|
|
905
|
+
nextTracker.outcome = "skip";
|
|
906
|
+
nextTracker.action_failed = false;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (/候选人处理失败\s*:/u.test(normalizedLine)) {
|
|
910
|
+
if (!nextTracker.action_failed) {
|
|
911
|
+
changed = true;
|
|
912
|
+
}
|
|
913
|
+
nextTracker.action_failed = true;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (/^\[关闭详情\].*成功/u.test(normalizedLine)) {
|
|
917
|
+
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
918
|
+
changed = true;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const finalStateLine = /Process timed out after|status"\s*:\s*"(?:COMPLETED|PAUSED|FAILED)"/iu.test(normalizedLine);
|
|
923
|
+
if (finalStateLine) {
|
|
924
|
+
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
925
|
+
changed = true;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const greetMatch = normalizedLine.match(/greet[_\s-]*count\s*[:=]\s*(\d+)/iu);
|
|
930
|
+
if (greetMatch) {
|
|
931
|
+
const greetCount = Number.parseInt(greetMatch[1], 10);
|
|
932
|
+
if (Number.isInteger(greetCount) && greetCount >= 0 && greetCount !== nextProgress.greet_count) {
|
|
933
|
+
nextProgress.greet_count = greetCount;
|
|
934
|
+
changed = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (!changed) return null;
|
|
939
|
+
return {
|
|
940
|
+
line: normalizedLine,
|
|
941
|
+
progress: nextProgress,
|
|
942
|
+
tracker: nextTracker
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function resolveRecommendScreenTimeoutMs(runtime = null) {
|
|
947
|
+
const runtimeTimeoutMs = parsePositiveInteger(runtime?.timeoutMs);
|
|
948
|
+
const envTimeoutMs = parsePositiveInteger(process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS);
|
|
949
|
+
return runtimeTimeoutMs || envTimeoutMs || DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function buildRecommendScreenProcessError(result, screenTimeoutMs) {
|
|
953
|
+
if (result.code === 0) return null;
|
|
954
|
+
if (result.error_code === "TIMEOUT") {
|
|
955
|
+
return {
|
|
956
|
+
code: "TIMEOUT",
|
|
957
|
+
message: `推荐页筛选命令执行超时(${screenTimeoutMs}ms)。`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (result.error_code === "ABORTED") {
|
|
961
|
+
return {
|
|
962
|
+
code: "PROCESS_ABORTED",
|
|
963
|
+
message: "推荐页筛选命令已取消。"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
code: "RECOMMEND_SCREEN_FAILED",
|
|
968
|
+
message: "推荐页筛选命令执行失败。"
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function loadScreenConfig(configPath) {
|
|
973
|
+
const parsed = readJsonFile(configPath);
|
|
974
|
+
const validation = validateScreenConfig(parsed);
|
|
975
|
+
if (!validation.ok) {
|
|
976
|
+
return {
|
|
977
|
+
ok: false,
|
|
978
|
+
error: `${validation.message} (path: ${configPath})`
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
return { ok: true, config: parsed };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function localDirHint(workspaceRoot, dirName) {
|
|
985
|
+
return path.join(workspaceRoot, dirName);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function runPipelinePreflight(workspaceRoot) {
|
|
989
|
+
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
990
|
+
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
991
|
+
const searchDirExists = Boolean(searchDir && pathExists(searchDir));
|
|
992
|
+
const searchEntryPath = searchDir
|
|
993
|
+
? resolveRecommendSearchCliEntry(searchDir)
|
|
994
|
+
: path.join(localDirHint(workspaceRoot, "boss-recommend-search-cli"), "src", "cli.js");
|
|
995
|
+
const searchEntryExists = Boolean(searchDir && pathExists(searchEntryPath));
|
|
996
|
+
const screenDirExists = Boolean(screenDir && pathExists(screenDir));
|
|
997
|
+
const screenEntryPath = screenDir
|
|
998
|
+
? resolveRecommendScreenCliEntry(screenDir)
|
|
999
|
+
: path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs");
|
|
1000
|
+
const screenEntryExists = Boolean(screenDir && pathExists(screenEntryPath));
|
|
1001
|
+
const configResolution = getScreenConfigResolution(workspaceRoot);
|
|
1002
|
+
const screenConfigPath = configResolution.resolved_path;
|
|
1003
|
+
const screenConfigParsed = readJsonFile(screenConfigPath);
|
|
1004
|
+
const screenConfigValidation = validateScreenConfig(screenConfigParsed);
|
|
1005
|
+
const checks = [
|
|
1006
|
+
{
|
|
1007
|
+
key: "recommend_search_cli_dir",
|
|
1008
|
+
ok: searchDirExists,
|
|
1009
|
+
path: searchDir || localDirHint(workspaceRoot, "boss-recommend-search-cli"),
|
|
1010
|
+
message: searchDirExists
|
|
1011
|
+
? "boss-recommend-search-cli 目录可用"
|
|
1012
|
+
: "boss-recommend-search-cli 目录不存在"
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
key: "recommend_search_cli_entry",
|
|
1016
|
+
ok: searchEntryExists,
|
|
1017
|
+
path: searchEntryPath,
|
|
1018
|
+
message: searchEntryExists
|
|
1019
|
+
? "boss-recommend-search-cli 入口文件可用"
|
|
1020
|
+
: "boss-recommend-search-cli 入口文件缺失"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
key: "recommend_screen_cli_dir",
|
|
1024
|
+
ok: screenDirExists,
|
|
1025
|
+
path: screenDir || localDirHint(workspaceRoot, "boss-recommend-screen-cli"),
|
|
1026
|
+
message: screenDirExists
|
|
1027
|
+
? "boss-recommend-screen-cli 目录可用"
|
|
1028
|
+
: "boss-recommend-screen-cli 目录不存在"
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
key: "recommend_screen_cli_entry",
|
|
1032
|
+
ok: screenEntryExists,
|
|
1033
|
+
path: screenEntryPath,
|
|
1034
|
+
message: screenEntryExists
|
|
1035
|
+
? "boss-recommend-screen-cli 入口文件可用"
|
|
1036
|
+
: "boss-recommend-screen-cli 入口文件缺失"
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
key: "screen_config",
|
|
1040
|
+
ok: screenConfigValidation.ok,
|
|
1041
|
+
path: screenConfigPath,
|
|
1042
|
+
reason: screenConfigValidation.reason || null,
|
|
1043
|
+
message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
|
|
1044
|
+
}
|
|
1045
|
+
];
|
|
1046
|
+
checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
ok: checks.every((item) => item.ok),
|
|
1050
|
+
checks,
|
|
1051
|
+
debug_port: resolveWorkspaceDebugPort(workspaceRoot),
|
|
1052
|
+
config_resolution: configResolution
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function collectFailedCheckKeys(checks = []) {
|
|
1057
|
+
return new Set(
|
|
1058
|
+
checks
|
|
1059
|
+
.filter((item) => item && item.ok === false && typeof item.key === "string")
|
|
1060
|
+
.map((item) => item.key)
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function collectNpmInstallDirsFromChecks(checks = [], workspaceRoot) {
|
|
1065
|
+
const npmKeys = new Set([
|
|
1066
|
+
"npm_dep_chrome_remote_interface_search",
|
|
1067
|
+
"npm_dep_chrome_remote_interface_screen",
|
|
1068
|
+
"npm_dep_ws"
|
|
1069
|
+
]);
|
|
1070
|
+
const dirs = checks
|
|
1071
|
+
.filter((item) => item && item.ok === false && npmKeys.has(item.key))
|
|
1072
|
+
.map((item) => item.install_cwd)
|
|
1073
|
+
.filter((item) => typeof item === "string" && item.trim())
|
|
1074
|
+
.map((item) => path.resolve(item));
|
|
1075
|
+
if (dirs.length > 0) {
|
|
1076
|
+
return [...new Set(dirs)];
|
|
1077
|
+
}
|
|
1078
|
+
return [path.resolve(workspaceRoot)];
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function installNpmDependencies(checks, workspaceRoot) {
|
|
1082
|
+
const dirs = collectNpmInstallDirsFromChecks(checks, workspaceRoot);
|
|
1083
|
+
const commandResults = [];
|
|
1084
|
+
let allOk = true;
|
|
1085
|
+
for (const cwd of dirs) {
|
|
1086
|
+
const result = runProcessSync({
|
|
1087
|
+
command: "npm",
|
|
1088
|
+
args: ["install"],
|
|
1089
|
+
cwd
|
|
1090
|
+
});
|
|
1091
|
+
commandResults.push({
|
|
1092
|
+
cwd,
|
|
1093
|
+
ok: result.ok,
|
|
1094
|
+
output: result.output || result.error_message || ""
|
|
1095
|
+
});
|
|
1096
|
+
if (!result.ok) allOk = false;
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
ok: allOk,
|
|
1100
|
+
action: "install_npm_dependencies",
|
|
1101
|
+
changed: true,
|
|
1102
|
+
command_results: commandResults,
|
|
1103
|
+
message: allOk ? "npm 依赖自动安装完成。" : "npm 依赖自动安装失败。"
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function installPillowIfPossible() {
|
|
1108
|
+
const detected = detectPythonCommand();
|
|
1109
|
+
if (!detected.ok || !detected.command) {
|
|
1110
|
+
return {
|
|
1111
|
+
ok: false,
|
|
1112
|
+
action: "install_pillow",
|
|
1113
|
+
changed: false,
|
|
1114
|
+
message: "未检测到可用 python 命令,无法自动安装 Pillow。"
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const install = runProcessSync({
|
|
1118
|
+
command: detected.command,
|
|
1119
|
+
args: ["-m", "pip", "install", "pillow"]
|
|
1120
|
+
});
|
|
1121
|
+
return {
|
|
1122
|
+
ok: install.ok,
|
|
1123
|
+
action: "install_pillow",
|
|
1124
|
+
changed: install.ok,
|
|
1125
|
+
message: install.ok ? "Pillow 自动安装完成。" : `Pillow 自动安装失败:${install.output || install.error_message || "unknown"}`
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
|
|
1130
|
+
const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
|
|
1131
|
+
const failed = collectFailedCheckKeys(checks);
|
|
1132
|
+
const actions = [];
|
|
1133
|
+
|
|
1134
|
+
if (
|
|
1135
|
+
failed.has("npm_dep_chrome_remote_interface_search")
|
|
1136
|
+
|| failed.has("npm_dep_chrome_remote_interface_screen")
|
|
1137
|
+
|| failed.has("npm_dep_ws")
|
|
1138
|
+
) {
|
|
1139
|
+
if (!failed.has("node_cli")) {
|
|
1140
|
+
actions.push(installNpmDependencies(checks, workspaceRoot));
|
|
1141
|
+
} else {
|
|
1142
|
+
actions.push({
|
|
1143
|
+
ok: false,
|
|
1144
|
+
action: "install_npm_dependencies",
|
|
1145
|
+
changed: false,
|
|
1146
|
+
message: "Node 命令不可用,跳过 npm 自动安装。"
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (failed.has("python_pillow")) {
|
|
1152
|
+
if (!failed.has("python_cli")) {
|
|
1153
|
+
actions.push(installPillowIfPossible());
|
|
1154
|
+
} else {
|
|
1155
|
+
actions.push({
|
|
1156
|
+
ok: false,
|
|
1157
|
+
action: "install_pillow",
|
|
1158
|
+
changed: false,
|
|
1159
|
+
message: "python 命令不可用,跳过 Pillow 自动安装。"
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const attempted = actions.length > 0;
|
|
1165
|
+
const nextPreflight = runPipelinePreflight(workspaceRoot);
|
|
1166
|
+
return {
|
|
1167
|
+
attempted,
|
|
1168
|
+
actions,
|
|
1169
|
+
preflight: nextPreflight
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function sleep(ms) {
|
|
1174
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async function listChromeTabs(port) {
|
|
1178
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
1179
|
+
if (!response.ok) {
|
|
1180
|
+
throw new Error(`DevTools endpoint returned ${response.status}`);
|
|
1181
|
+
}
|
|
1182
|
+
const data = await response.json();
|
|
1183
|
+
return Array.isArray(data) ? data : [];
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function buildBossPageState(payload) {
|
|
1187
|
+
return {
|
|
1188
|
+
key: "boss_page_state",
|
|
1189
|
+
...payload
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function extractSampleUrls(tabs, limit = 5) {
|
|
1194
|
+
return tabs
|
|
1195
|
+
.map((tab) => tab?.url)
|
|
1196
|
+
.filter(Boolean)
|
|
1197
|
+
.slice(0, limit);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function findChromeOnboardingUrl(tabs) {
|
|
1201
|
+
for (const tab of tabs) {
|
|
1202
|
+
if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
|
|
1203
|
+
return tab.url;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function isBossLoginTab(tab) {
|
|
1210
|
+
const url = String(tab?.url || "");
|
|
1211
|
+
const title = String(tab?.title || "");
|
|
1212
|
+
return (
|
|
1213
|
+
url === bossLoginUrl
|
|
1214
|
+
|| bossLoginUrlPattern.test(url)
|
|
1215
|
+
|| bossLoginTitlePattern.test(title)
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
export async function inspectBossRecommendPageState(port, options = {}) {
|
|
1220
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
|
|
1221
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
|
|
1222
|
+
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
1223
|
+
const deadline = Date.now() + timeoutMs;
|
|
1224
|
+
let lastError = null;
|
|
1225
|
+
let lastTabs = [];
|
|
1226
|
+
|
|
1227
|
+
while (Date.now() < deadline) {
|
|
1228
|
+
try {
|
|
1229
|
+
const tabs = await listChromeTabs(port);
|
|
1230
|
+
lastTabs = tabs;
|
|
1231
|
+
const exactTab = tabs.find(
|
|
1232
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
1233
|
+
);
|
|
1234
|
+
if (exactTab) {
|
|
1235
|
+
if (isBossLoginTab(exactTab)) {
|
|
1236
|
+
return buildBossPageState({
|
|
1237
|
+
ok: false,
|
|
1238
|
+
state: "LOGIN_REQUIRED",
|
|
1239
|
+
path: exactTab.url || bossLoginUrl,
|
|
1240
|
+
current_url: exactTab.url || bossLoginUrl,
|
|
1241
|
+
title: exactTab.title || null,
|
|
1242
|
+
requires_login: true,
|
|
1243
|
+
expected_url: expectedUrl,
|
|
1244
|
+
login_url: bossLoginUrl,
|
|
1245
|
+
message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
return buildBossPageState({
|
|
1249
|
+
ok: true,
|
|
1250
|
+
state: "RECOMMEND_READY",
|
|
1251
|
+
path: exactTab.url,
|
|
1252
|
+
current_url: exactTab.url,
|
|
1253
|
+
title: exactTab.title || null,
|
|
1254
|
+
requires_login: false,
|
|
1255
|
+
message: "Boss 推荐页已打开,且当前仍停留在 recommend 页面。"
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const loginTab = tabs.find((tab) => isBossLoginTab(tab));
|
|
1260
|
+
if (loginTab) {
|
|
1261
|
+
return buildBossPageState({
|
|
1262
|
+
ok: false,
|
|
1263
|
+
state: "LOGIN_REQUIRED",
|
|
1264
|
+
path: loginTab.url || bossLoginUrl,
|
|
1265
|
+
current_url: loginTab.url || bossLoginUrl,
|
|
1266
|
+
title: loginTab.title || null,
|
|
1267
|
+
requires_login: true,
|
|
1268
|
+
expected_url: expectedUrl,
|
|
1269
|
+
login_url: bossLoginUrl,
|
|
1270
|
+
message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const bossTab = tabs.find(
|
|
1275
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1276
|
+
);
|
|
1277
|
+
if (bossTab) {
|
|
1278
|
+
const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
|
|
1279
|
+
return buildBossPageState({
|
|
1280
|
+
ok: false,
|
|
1281
|
+
state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
|
|
1282
|
+
path: bossTab.url,
|
|
1283
|
+
current_url: bossTab.url,
|
|
1284
|
+
title: bossTab.title || null,
|
|
1285
|
+
requires_login: requiresLogin,
|
|
1286
|
+
expected_url: expectedUrl,
|
|
1287
|
+
message: requiresLogin
|
|
1288
|
+
? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
1289
|
+
: "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
lastError = error;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
await sleep(pollMs);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (lastError) {
|
|
1300
|
+
return buildBossPageState({
|
|
1301
|
+
ok: false,
|
|
1302
|
+
state: "DEBUG_PORT_UNREACHABLE",
|
|
1303
|
+
path: `http://127.0.0.1:${port}`,
|
|
1304
|
+
current_url: null,
|
|
1305
|
+
title: null,
|
|
1306
|
+
requires_login: false,
|
|
1307
|
+
expected_url: expectedUrl,
|
|
1308
|
+
message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
|
|
1309
|
+
error: lastError.message
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const onboardingUrl = findChromeOnboardingUrl(lastTabs);
|
|
1314
|
+
if (onboardingUrl) {
|
|
1315
|
+
return buildBossPageState({
|
|
1316
|
+
ok: false,
|
|
1317
|
+
state: "CHROME_ONBOARDING_INTERCEPTED",
|
|
1318
|
+
path: onboardingUrl,
|
|
1319
|
+
current_url: onboardingUrl,
|
|
1320
|
+
title: null,
|
|
1321
|
+
requires_login: false,
|
|
1322
|
+
expected_url: expectedUrl,
|
|
1323
|
+
message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
|
|
1324
|
+
sample_urls: extractSampleUrls(lastTabs)
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
return buildBossPageState({
|
|
1329
|
+
ok: false,
|
|
1330
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
1331
|
+
path: expectedUrl,
|
|
1332
|
+
current_url: null,
|
|
1333
|
+
title: null,
|
|
1334
|
+
requires_login: false,
|
|
1335
|
+
expected_url,
|
|
1336
|
+
message: "未检测到 Boss 推荐页标签页。",
|
|
1337
|
+
sample_urls: extractSampleUrls(lastTabs)
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
async function openBossRecommendTab(port) {
|
|
1342
|
+
const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossRecommendUrl)}`;
|
|
1343
|
+
const attempts = ["PUT", "GET"];
|
|
1344
|
+
let lastError = null;
|
|
1345
|
+
|
|
1346
|
+
for (const method of attempts) {
|
|
1347
|
+
try {
|
|
1348
|
+
const response = await fetch(endpoint, { method });
|
|
1349
|
+
if (response.ok) {
|
|
1350
|
+
return { ok: true, method };
|
|
1351
|
+
}
|
|
1352
|
+
lastError = new Error(`DevTools /json/new returned ${response.status}`);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
lastError = error;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
error: lastError?.message || "Failed to open Boss recommend tab via DevTools /json/new"
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1363
1364
|
async function verifyRecommendPageStable(port, options = {}) {
|
|
1364
1365
|
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
|
|
1365
1366
|
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
|
|
1366
1367
|
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1367
|
-
|
|
1368
|
-
await sleep(settleMs);
|
|
1369
|
-
const recheck = await inspectBossRecommendPageState(port, {
|
|
1370
|
-
timeoutMs: recheckTimeoutMs,
|
|
1371
|
-
pollMs
|
|
1372
|
-
});
|
|
1373
|
-
if (recheck.state === "RECOMMEND_READY") {
|
|
1374
|
-
return recheck;
|
|
1375
|
-
}
|
|
1376
|
-
if (recheck.state === "LOGIN_REQUIRED") {
|
|
1377
|
-
return buildBossPageState({
|
|
1378
|
-
...recheck,
|
|
1379
|
-
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
1380
|
-
message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1368
|
+
|
|
1369
|
+
await sleep(settleMs);
|
|
1370
|
+
const recheck = await inspectBossRecommendPageState(port, {
|
|
1371
|
+
timeoutMs: recheckTimeoutMs,
|
|
1372
|
+
pollMs
|
|
1373
|
+
});
|
|
1374
|
+
if (recheck.state === "RECOMMEND_READY") {
|
|
1375
|
+
return recheck;
|
|
1376
|
+
}
|
|
1377
|
+
if (recheck.state === "LOGIN_REQUIRED") {
|
|
1378
|
+
return buildBossPageState({
|
|
1379
|
+
...recheck,
|
|
1380
|
+
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
1381
|
+
message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1383
1384
|
return recheck;
|
|
1384
1385
|
}
|
|
1385
1386
|
|
|
1386
|
-
|
|
1387
|
+
function pickBossRecommendReloadTarget(tabs = []) {
|
|
1388
|
+
return tabs.find(
|
|
1389
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
1390
|
+
) || tabs.find(
|
|
1391
|
+
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1392
|
+
) || null;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
export async function reloadBossRecommendPage(workspaceRoot, options = {}) {
|
|
1387
1396
|
const debugPort = Number.isFinite(options.port)
|
|
1388
1397
|
? options.port
|
|
1389
1398
|
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1392
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs :
|
|
1393
|
-
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 800;
|
|
1394
|
-
|
|
1395
|
-
let pageState = await inspectBossRecommendPageState(debugPort, {
|
|
1396
|
-
timeoutMs: inspectTimeoutMs,
|
|
1397
|
-
pollMs
|
|
1398
|
-
});
|
|
1399
|
-
if (pageState.state === "RECOMMEND_READY") {
|
|
1400
|
-
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
1401
|
-
return {
|
|
1402
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
1403
|
-
debug_port: debugPort,
|
|
1404
|
-
state: stableState.state,
|
|
1405
|
-
page_state: stableState
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1399
|
+
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1200;
|
|
1400
|
+
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 4000;
|
|
1401
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1408
1402
|
|
|
1409
|
-
let
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
state: pageState.state,
|
|
1415
|
-
page_state: {
|
|
1416
|
-
...pageState,
|
|
1417
|
-
launch_attempt: launchAttempt
|
|
1418
|
-
}
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
|
|
1422
|
-
launchAttempt = launchChromeWithDebugPort(debugPort);
|
|
1423
|
-
if (launchAttempt.ok) {
|
|
1424
|
-
await sleep(settleMs + 1200);
|
|
1425
|
-
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
1426
|
-
timeoutMs: inspectTimeoutMs,
|
|
1427
|
-
pollMs
|
|
1428
|
-
});
|
|
1429
|
-
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
1430
|
-
return {
|
|
1431
|
-
ok: false,
|
|
1432
|
-
debug_port: debugPort,
|
|
1433
|
-
state: pageState.state,
|
|
1434
|
-
page_state: {
|
|
1435
|
-
...pageState,
|
|
1436
|
-
launch_attempt: launchAttempt
|
|
1437
|
-
}
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
if (pageState.state === "RECOMMEND_READY") {
|
|
1441
|
-
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
1442
|
-
return {
|
|
1443
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
1444
|
-
debug_port: debugPort,
|
|
1445
|
-
state: stableState.state,
|
|
1446
|
-
page_state: {
|
|
1447
|
-
...stableState,
|
|
1448
|
-
launch_attempt: launchAttempt
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
} else {
|
|
1403
|
+
let client = null;
|
|
1404
|
+
try {
|
|
1405
|
+
const tabs = await listChromeTabs(debugPort);
|
|
1406
|
+
const target = pickBossRecommendReloadTarget(tabs);
|
|
1407
|
+
if (!target) {
|
|
1453
1408
|
return {
|
|
1454
1409
|
ok: false,
|
|
1455
1410
|
debug_port: debugPort,
|
|
1456
|
-
state:
|
|
1457
|
-
page_state:
|
|
1458
|
-
|
|
1459
|
-
launch_attempt: launchAttempt
|
|
1460
|
-
}
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1466
|
-
if (
|
|
1467
|
-
pageState.state === "DEBUG_PORT_UNREACHABLE"
|
|
1468
|
-
|| pageState.state === "LOGIN_REQUIRED"
|
|
1469
|
-
|| pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
1470
|
-
) {
|
|
1471
|
-
break;
|
|
1472
|
-
}
|
|
1473
|
-
await openBossRecommendTab(debugPort);
|
|
1474
|
-
await sleep(settleMs);
|
|
1475
|
-
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
1476
|
-
timeoutMs: inspectTimeoutMs,
|
|
1477
|
-
pollMs
|
|
1478
|
-
});
|
|
1479
|
-
if (pageState.state === "RECOMMEND_READY") {
|
|
1480
|
-
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
1481
|
-
return {
|
|
1482
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
1483
|
-
debug_port: debugPort,
|
|
1484
|
-
state: stableState.state,
|
|
1485
|
-
page_state: {
|
|
1486
|
-
...stableState,
|
|
1487
|
-
launch_attempt: launchAttempt
|
|
1488
|
-
}
|
|
1411
|
+
state: "BOSS_TAB_NOT_FOUND",
|
|
1412
|
+
page_state: null,
|
|
1413
|
+
message: "未找到可刷新的 Boss 标签页。"
|
|
1489
1414
|
};
|
|
1490
1415
|
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
return {
|
|
1494
|
-
ok: false,
|
|
1495
|
-
debug_port: debugPort,
|
|
1496
|
-
state: pageState.state || "UNKNOWN",
|
|
1497
|
-
page_state: {
|
|
1498
|
-
...pageState,
|
|
1499
|
-
launch_attempt: launchAttempt
|
|
1500
|
-
}
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
1416
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
ok: false,
|
|
1509
|
-
stdout: "",
|
|
1510
|
-
stderr: "boss-recommend-search-cli package not found",
|
|
1511
|
-
error: {
|
|
1512
|
-
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
1513
|
-
message: "boss-recommend-search-cli 目录不存在。"
|
|
1514
|
-
}
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
1518
|
-
const args = [
|
|
1519
|
-
cliPath,
|
|
1520
|
-
"--list-jobs",
|
|
1521
|
-
"--port",
|
|
1522
|
-
String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
|
|
1523
|
-
];
|
|
1524
|
-
const result = await runProcess({
|
|
1525
|
-
command: "node",
|
|
1526
|
-
args,
|
|
1527
|
-
cwd: searchDir,
|
|
1528
|
-
timeoutMs: 180000,
|
|
1529
|
-
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1530
|
-
signal: runtime?.signal,
|
|
1531
|
-
onOutput: (event) => {
|
|
1532
|
-
safeInvokeCallback(runtime?.onOutput, event);
|
|
1533
|
-
},
|
|
1534
|
-
onHeartbeat: (event) => {
|
|
1535
|
-
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
1417
|
+
client = await CDP({ port: debugPort, target });
|
|
1418
|
+
const { Page } = client;
|
|
1419
|
+
if (Page && typeof Page.enable === "function") {
|
|
1420
|
+
await Page.enable();
|
|
1536
1421
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const jobs = Array.isArray(structured?.result?.jobs) ? structured.result.jobs : [];
|
|
1540
|
-
const missingOutputError = result.code === 0 && !structured
|
|
1541
|
-
? {
|
|
1542
|
-
code: "RECOMMEND_JOB_LIST_NO_OUTPUT",
|
|
1543
|
-
message: "岗位列表读取完成但未返回可解析结果。"
|
|
1544
|
-
}
|
|
1545
|
-
: null;
|
|
1546
|
-
return {
|
|
1547
|
-
ok: result.code === 0 && structured?.status === "COMPLETED" && jobs.length > 0,
|
|
1548
|
-
stdout: result.stdout,
|
|
1549
|
-
stderr: result.stderr,
|
|
1550
|
-
structured,
|
|
1551
|
-
jobs,
|
|
1552
|
-
error: structured?.error || missingOutputError || (
|
|
1553
|
-
result.code === 0
|
|
1554
|
-
? {
|
|
1555
|
-
code: "RECOMMEND_JOB_LIST_EMPTY",
|
|
1556
|
-
message: "未读取到可选岗位。"
|
|
1557
|
-
}
|
|
1558
|
-
: result.error_code === "ABORTED"
|
|
1559
|
-
? {
|
|
1560
|
-
code: "PROCESS_ABORTED",
|
|
1561
|
-
message: "岗位列表读取已取消。"
|
|
1562
|
-
}
|
|
1563
|
-
: {
|
|
1564
|
-
code: "RECOMMEND_JOB_LIST_FAILED",
|
|
1565
|
-
message: "岗位列表读取失败。"
|
|
1566
|
-
}
|
|
1567
|
-
)
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
export async function runRecommendSearchCli({ workspaceRoot, searchParams, selectedJob, runtime = null }) {
|
|
1572
|
-
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
1573
|
-
if (!searchDir) {
|
|
1574
|
-
return {
|
|
1575
|
-
ok: false,
|
|
1576
|
-
stdout: "",
|
|
1577
|
-
stderr: "boss-recommend-search-cli package not found",
|
|
1578
|
-
error: {
|
|
1579
|
-
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
1580
|
-
message: "boss-recommend-search-cli 目录不存在。"
|
|
1581
|
-
}
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
1585
|
-
const args = [
|
|
1586
|
-
cliPath,
|
|
1587
|
-
"--school-tag",
|
|
1588
|
-
serializeSchoolTagSelection(searchParams.school_tag),
|
|
1589
|
-
"--degree",
|
|
1590
|
-
serializeDegreeSelection(searchParams.degree),
|
|
1591
|
-
"--gender",
|
|
1592
|
-
searchParams.gender,
|
|
1593
|
-
"--recent-not-view",
|
|
1594
|
-
searchParams.recent_not_view,
|
|
1595
|
-
"--port",
|
|
1596
|
-
String(resolveWorkspaceDebugPort(workspaceRoot))
|
|
1597
|
-
];
|
|
1598
|
-
const normalizedSelectedJob = String(selectedJob || "").trim();
|
|
1599
|
-
if (normalizedSelectedJob) {
|
|
1600
|
-
args.push("--job", normalizedSelectedJob);
|
|
1601
|
-
}
|
|
1602
|
-
const result = await runProcess({
|
|
1603
|
-
command: "node",
|
|
1604
|
-
args,
|
|
1605
|
-
cwd: searchDir,
|
|
1606
|
-
timeoutMs: 180000,
|
|
1607
|
-
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1608
|
-
signal: runtime?.signal,
|
|
1609
|
-
onOutput: (event) => {
|
|
1610
|
-
safeInvokeCallback(runtime?.onOutput, event);
|
|
1611
|
-
},
|
|
1612
|
-
onHeartbeat: (event) => {
|
|
1613
|
-
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
1422
|
+
if (Page && typeof Page.bringToFront === "function") {
|
|
1423
|
+
await Page.bringToFront();
|
|
1614
1424
|
}
|
|
1615
|
-
|
|
1616
|
-
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1617
|
-
const missingOutputError = result.code === 0 && !structured
|
|
1618
|
-
? {
|
|
1619
|
-
code: "RECOMMEND_SEARCH_NO_OUTPUT",
|
|
1620
|
-
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
1621
|
-
}
|
|
1622
|
-
: null;
|
|
1623
|
-
return {
|
|
1624
|
-
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
1625
|
-
stdout: result.stdout,
|
|
1626
|
-
stderr: result.stderr,
|
|
1627
|
-
structured,
|
|
1628
|
-
summary: structured?.result || null,
|
|
1629
|
-
error: structured?.error || missingOutputError || (
|
|
1630
|
-
result.code === 0
|
|
1631
|
-
? null
|
|
1632
|
-
: result.error_code === "ABORTED"
|
|
1633
|
-
? {
|
|
1634
|
-
code: "PROCESS_ABORTED",
|
|
1635
|
-
message: "推荐页筛选命令已取消。"
|
|
1636
|
-
}
|
|
1637
|
-
: {
|
|
1638
|
-
code: "RECOMMEND_SEARCH_FAILED",
|
|
1639
|
-
message: "推荐页筛选命令执行失败。"
|
|
1640
|
-
}
|
|
1641
|
-
)
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1425
|
+
await Page.reload({ ignoreCache: true });
|
|
1644
1426
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1427
|
+
const stableState = await verifyRecommendPageStable(debugPort, {
|
|
1428
|
+
settleMs,
|
|
1429
|
+
recheckTimeoutMs,
|
|
1430
|
+
pollMs
|
|
1431
|
+
});
|
|
1648
1432
|
return {
|
|
1649
|
-
ok:
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
message: "boss-recommend-screen-cli 目录不存在。"
|
|
1655
|
-
}
|
|
1433
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
1434
|
+
debug_port: debugPort,
|
|
1435
|
+
state: stableState.state,
|
|
1436
|
+
page_state: stableState,
|
|
1437
|
+
reloaded_url: target.url || null
|
|
1656
1438
|
};
|
|
1657
|
-
}
|
|
1658
|
-
const configPath = resolveScreenConfigPath(workspaceRoot);
|
|
1659
|
-
const loaded = loadScreenConfig(configPath);
|
|
1660
|
-
if (!loaded.ok) {
|
|
1439
|
+
} catch (error) {
|
|
1661
1440
|
return {
|
|
1662
1441
|
ok: false,
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
message: loaded.error
|
|
1668
|
-
}
|
|
1442
|
+
debug_port: debugPort,
|
|
1443
|
+
state: "RELOAD_FAILED",
|
|
1444
|
+
page_state: null,
|
|
1445
|
+
message: error?.message || "刷新 Boss recommend 页面失败。"
|
|
1669
1446
|
};
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
if (!fixedOutput) {
|
|
1676
|
-
if (loaded.config.outputDir) {
|
|
1677
|
-
const resolvedOutputDir = path.resolve(path.dirname(configPath), loaded.config.outputDir);
|
|
1678
|
-
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
1679
|
-
outputPath = path.join(resolvedOutputDir, outputName);
|
|
1680
|
-
} else {
|
|
1681
|
-
const desktopDir = getDesktopDir();
|
|
1682
|
-
fs.mkdirSync(desktopDir, { recursive: true });
|
|
1683
|
-
outputPath = path.join(desktopDir, outputName);
|
|
1447
|
+
} finally {
|
|
1448
|
+
if (client) {
|
|
1449
|
+
try {
|
|
1450
|
+
await client.close();
|
|
1451
|
+
} catch {}
|
|
1684
1452
|
}
|
|
1685
|
-
} else {
|
|
1686
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1687
1453
|
}
|
|
1688
|
-
|
|
1689
|
-
const checkpointPath = normalizeText(resume?.checkpoint_path || "")
|
|
1690
|
-
? path.resolve(String(resume.checkpoint_path))
|
|
1691
|
-
: null;
|
|
1692
|
-
const pauseControlPath = normalizeText(resume?.pause_control_path || "")
|
|
1693
|
-
? path.resolve(String(resume.pause_control_path))
|
|
1694
|
-
: null;
|
|
1695
|
-
const resumeRequested = resume?.resume === true;
|
|
1696
|
-
const requireCheckpoint = resume?.require_checkpoint === true;
|
|
1697
|
-
if (resumeRequested && requireCheckpoint) {
|
|
1698
|
-
if (!checkpointPath) {
|
|
1699
|
-
return {
|
|
1700
|
-
ok: false,
|
|
1701
|
-
paused: false,
|
|
1702
|
-
stdout: "",
|
|
1703
|
-
stderr: "",
|
|
1704
|
-
structured: null,
|
|
1705
|
-
summary: null,
|
|
1706
|
-
error: {
|
|
1707
|
-
code: "RESUME_CHECKPOINT_MISSING",
|
|
1708
|
-
message: "恢复执行缺少 checkpoint_path,无法从上次进度继续。"
|
|
1709
|
-
}
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
if (!fs.existsSync(checkpointPath)) {
|
|
1713
|
-
return {
|
|
1714
|
-
ok: false,
|
|
1715
|
-
paused: false,
|
|
1716
|
-
stdout: "",
|
|
1717
|
-
stderr: "",
|
|
1718
|
-
structured: null,
|
|
1719
|
-
summary: null,
|
|
1720
|
-
error: {
|
|
1721
|
-
code: "RESUME_CHECKPOINT_MISSING",
|
|
1722
|
-
message: `恢复执行未找到 checkpoint 文件:${checkpointPath}`
|
|
1723
|
-
}
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
const cliPath = resolveRecommendScreenCliEntry(screenDir);
|
|
1729
|
-
const args = [
|
|
1730
|
-
cliPath,
|
|
1731
|
-
"--baseurl",
|
|
1732
|
-
loaded.config.baseUrl,
|
|
1733
|
-
"--apikey",
|
|
1734
|
-
loaded.config.apiKey,
|
|
1735
|
-
"--model",
|
|
1736
|
-
loaded.config.model,
|
|
1737
|
-
"--port",
|
|
1738
|
-
String(resolveWorkspaceDebugPort(workspaceRoot)),
|
|
1739
|
-
"--criteria",
|
|
1740
|
-
screenParams.criteria,
|
|
1741
|
-
"--post-action",
|
|
1742
|
-
screenParams.post_action,
|
|
1743
|
-
"--post-action-confirmed",
|
|
1744
|
-
"true",
|
|
1745
|
-
"--output",
|
|
1746
|
-
outputPath
|
|
1747
|
-
];
|
|
1748
|
-
|
|
1749
|
-
if (loaded.config.openaiOrganization) {
|
|
1750
|
-
args.push("--openai-organization", loaded.config.openaiOrganization);
|
|
1751
|
-
}
|
|
1752
|
-
if (loaded.config.openaiProject) {
|
|
1753
|
-
args.push("--openai-project", loaded.config.openaiProject);
|
|
1754
|
-
}
|
|
1755
|
-
if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
|
|
1756
|
-
args.push("--targetCount", String(screenParams.target_count));
|
|
1757
|
-
}
|
|
1758
|
-
if (screenParams.post_action === "greet"
|
|
1759
|
-
&& Number.isInteger(screenParams.max_greet_count)
|
|
1760
|
-
&& screenParams.max_greet_count > 0) {
|
|
1761
|
-
args.push("--max-greet-count", String(screenParams.max_greet_count));
|
|
1762
|
-
}
|
|
1763
|
-
if (checkpointPath) {
|
|
1764
|
-
args.push("--checkpoint-path", checkpointPath);
|
|
1765
|
-
}
|
|
1766
|
-
if (pauseControlPath) {
|
|
1767
|
-
args.push("--pause-control-path", pauseControlPath);
|
|
1768
|
-
}
|
|
1769
|
-
if (resumeRequested) {
|
|
1770
|
-
args.push("--resume");
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
let inferredProgress = {
|
|
1774
|
-
processed: 0,
|
|
1775
|
-
passed: 0,
|
|
1776
|
-
skipped: 0,
|
|
1777
|
-
greet_count: 0
|
|
1778
|
-
};
|
|
1779
|
-
let inferredTracker = createScreenProgressTracker();
|
|
1780
|
-
const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
|
|
1781
|
-
|
|
1782
|
-
const result = await runProcess({
|
|
1783
|
-
command: "node",
|
|
1784
|
-
args,
|
|
1785
|
-
cwd: screenDir,
|
|
1786
|
-
timeoutMs: screenTimeoutMs,
|
|
1787
|
-
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1788
|
-
signal: runtime?.signal,
|
|
1789
|
-
onOutput: (event) => {
|
|
1790
|
-
safeInvokeCallback(runtime?.onOutput, event);
|
|
1791
|
-
},
|
|
1792
|
-
onLine: (event) => {
|
|
1793
|
-
const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
|
|
1794
|
-
if (!parsed) return;
|
|
1795
|
-
inferredProgress = parsed.progress;
|
|
1796
|
-
inferredTracker = parsed.tracker;
|
|
1797
|
-
safeInvokeCallback(runtime?.onProgress, {
|
|
1798
|
-
...inferredProgress,
|
|
1799
|
-
line: parsed.line
|
|
1800
|
-
});
|
|
1801
|
-
},
|
|
1802
|
-
onHeartbeat: (event) => {
|
|
1803
|
-
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
1804
|
-
}
|
|
1805
|
-
});
|
|
1806
|
-
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1807
|
-
const status = normalizeText(structured?.status || "").toUpperCase();
|
|
1808
|
-
const summary = structured?.result || null;
|
|
1809
|
-
if (summary) {
|
|
1810
|
-
safeInvokeCallback(runtime?.onProgress, {
|
|
1811
|
-
processed: Number.isInteger(summary.processed_count) ? summary.processed_count : inferredProgress.processed,
|
|
1812
|
-
passed: Number.isInteger(summary.passed_count) ? summary.passed_count : inferredProgress.passed,
|
|
1813
|
-
skipped: Number.isInteger(summary.skipped_count) ? summary.skipped_count : inferredProgress.skipped,
|
|
1814
|
-
greet_count: Number.isInteger(summary.greet_count) ? summary.greet_count : inferredProgress.greet_count
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
const missingOutputError = result.code === 0 && !structured
|
|
1818
|
-
? {
|
|
1819
|
-
code: "RECOMMEND_SCREEN_NO_OUTPUT",
|
|
1820
|
-
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
1821
|
-
}
|
|
1822
|
-
: null;
|
|
1823
|
-
return {
|
|
1824
|
-
ok: result.code === 0 && status === "COMPLETED",
|
|
1825
|
-
paused: result.code === 0 && status === "PAUSED",
|
|
1826
|
-
stdout: result.stdout,
|
|
1827
|
-
stderr: result.stderr,
|
|
1828
|
-
structured,
|
|
1829
|
-
summary,
|
|
1830
|
-
error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
|
|
1831
|
-
};
|
|
1832
1454
|
}
|
|
1833
1455
|
|
|
1834
|
-
export
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1456
|
+
export async function ensureBossRecommendPageReady(workspaceRoot, options = {}) {
|
|
1457
|
+
const debugPort = Number.isFinite(options.port)
|
|
1458
|
+
? options.port
|
|
1459
|
+
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
1460
|
+
const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
|
|
1461
|
+
const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
|
|
1462
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
|
|
1463
|
+
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 800;
|
|
1464
|
+
|
|
1465
|
+
let pageState = await inspectBossRecommendPageState(debugPort, {
|
|
1466
|
+
timeoutMs: inspectTimeoutMs,
|
|
1467
|
+
pollMs
|
|
1468
|
+
});
|
|
1469
|
+
if (pageState.state === "RECOMMEND_READY") {
|
|
1470
|
+
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
1471
|
+
return {
|
|
1472
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
1473
|
+
debug_port: debugPort,
|
|
1474
|
+
state: stableState.state,
|
|
1475
|
+
page_state: stableState
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
let launchAttempt = null;
|
|
1480
|
+
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
1481
|
+
return {
|
|
1482
|
+
ok: false,
|
|
1483
|
+
debug_port: debugPort,
|
|
1484
|
+
state: pageState.state,
|
|
1485
|
+
page_state: {
|
|
1486
|
+
...pageState,
|
|
1487
|
+
launch_attempt: launchAttempt
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
|
|
1492
|
+
launchAttempt = launchChromeWithDebugPort(debugPort);
|
|
1493
|
+
if (launchAttempt.ok) {
|
|
1494
|
+
await sleep(settleMs + 1200);
|
|
1495
|
+
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
1496
|
+
timeoutMs: inspectTimeoutMs,
|
|
1497
|
+
pollMs
|
|
1498
|
+
});
|
|
1499
|
+
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
1500
|
+
return {
|
|
1501
|
+
ok: false,
|
|
1502
|
+
debug_port: debugPort,
|
|
1503
|
+
state: pageState.state,
|
|
1504
|
+
page_state: {
|
|
1505
|
+
...pageState,
|
|
1506
|
+
launch_attempt: launchAttempt
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (pageState.state === "RECOMMEND_READY") {
|
|
1511
|
+
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
1512
|
+
return {
|
|
1513
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
1514
|
+
debug_port: debugPort,
|
|
1515
|
+
state: stableState.state,
|
|
1516
|
+
page_state: {
|
|
1517
|
+
...stableState,
|
|
1518
|
+
launch_attempt: launchAttempt
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
} else {
|
|
1523
|
+
return {
|
|
1524
|
+
ok: false,
|
|
1525
|
+
debug_port: debugPort,
|
|
1526
|
+
state: pageState.state,
|
|
1527
|
+
page_state: {
|
|
1528
|
+
...pageState,
|
|
1529
|
+
launch_attempt: launchAttempt
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1536
|
+
if (
|
|
1537
|
+
pageState.state === "DEBUG_PORT_UNREACHABLE"
|
|
1538
|
+
|| pageState.state === "LOGIN_REQUIRED"
|
|
1539
|
+
|| pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
1540
|
+
) {
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
await openBossRecommendTab(debugPort);
|
|
1544
|
+
await sleep(settleMs);
|
|
1545
|
+
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
1546
|
+
timeoutMs: inspectTimeoutMs,
|
|
1547
|
+
pollMs
|
|
1548
|
+
});
|
|
1549
|
+
if (pageState.state === "RECOMMEND_READY") {
|
|
1550
|
+
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
1551
|
+
return {
|
|
1552
|
+
ok: stableState.state === "RECOMMEND_READY",
|
|
1553
|
+
debug_port: debugPort,
|
|
1554
|
+
state: stableState.state,
|
|
1555
|
+
page_state: {
|
|
1556
|
+
...stableState,
|
|
1557
|
+
launch_attempt: launchAttempt
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return {
|
|
1564
|
+
ok: false,
|
|
1565
|
+
debug_port: debugPort,
|
|
1566
|
+
state: pageState.state || "UNKNOWN",
|
|
1567
|
+
page_state: {
|
|
1568
|
+
...pageState,
|
|
1569
|
+
launch_attempt: launchAttempt
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export async function listRecommendJobs({ workspaceRoot, port, runtime = null }) {
|
|
1575
|
+
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
1576
|
+
if (!searchDir) {
|
|
1577
|
+
return {
|
|
1578
|
+
ok: false,
|
|
1579
|
+
stdout: "",
|
|
1580
|
+
stderr: "boss-recommend-search-cli package not found",
|
|
1581
|
+
error: {
|
|
1582
|
+
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
1583
|
+
message: "boss-recommend-search-cli 目录不存在。"
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
1588
|
+
const args = [
|
|
1589
|
+
cliPath,
|
|
1590
|
+
"--list-jobs",
|
|
1591
|
+
"--port",
|
|
1592
|
+
String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
|
|
1593
|
+
];
|
|
1594
|
+
const result = await runProcess({
|
|
1595
|
+
command: "node",
|
|
1596
|
+
args,
|
|
1597
|
+
cwd: searchDir,
|
|
1598
|
+
timeoutMs: 180000,
|
|
1599
|
+
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1600
|
+
signal: runtime?.signal,
|
|
1601
|
+
onOutput: (event) => {
|
|
1602
|
+
safeInvokeCallback(runtime?.onOutput, event);
|
|
1603
|
+
},
|
|
1604
|
+
onHeartbeat: (event) => {
|
|
1605
|
+
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1609
|
+
const jobs = Array.isArray(structured?.result?.jobs) ? structured.result.jobs : [];
|
|
1610
|
+
const missingOutputError = result.code === 0 && !structured
|
|
1611
|
+
? {
|
|
1612
|
+
code: "RECOMMEND_JOB_LIST_NO_OUTPUT",
|
|
1613
|
+
message: "岗位列表读取完成但未返回可解析结果。"
|
|
1614
|
+
}
|
|
1615
|
+
: null;
|
|
1616
|
+
return {
|
|
1617
|
+
ok: result.code === 0 && structured?.status === "COMPLETED" && jobs.length > 0,
|
|
1618
|
+
stdout: result.stdout,
|
|
1619
|
+
stderr: result.stderr,
|
|
1620
|
+
structured,
|
|
1621
|
+
jobs,
|
|
1622
|
+
error: structured?.error || missingOutputError || (
|
|
1623
|
+
result.code === 0
|
|
1624
|
+
? {
|
|
1625
|
+
code: "RECOMMEND_JOB_LIST_EMPTY",
|
|
1626
|
+
message: "未读取到可选岗位。"
|
|
1627
|
+
}
|
|
1628
|
+
: result.error_code === "ABORTED"
|
|
1629
|
+
? {
|
|
1630
|
+
code: "PROCESS_ABORTED",
|
|
1631
|
+
message: "岗位列表读取已取消。"
|
|
1632
|
+
}
|
|
1633
|
+
: {
|
|
1634
|
+
code: "RECOMMEND_JOB_LIST_FAILED",
|
|
1635
|
+
message: "岗位列表读取失败。"
|
|
1636
|
+
}
|
|
1637
|
+
)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
export async function runRecommendSearchCli({ workspaceRoot, searchParams, selectedJob, runtime = null }) {
|
|
1642
|
+
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
1643
|
+
if (!searchDir) {
|
|
1644
|
+
return {
|
|
1645
|
+
ok: false,
|
|
1646
|
+
stdout: "",
|
|
1647
|
+
stderr: "boss-recommend-search-cli package not found",
|
|
1648
|
+
error: {
|
|
1649
|
+
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
1650
|
+
message: "boss-recommend-search-cli 目录不存在。"
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
1655
|
+
const args = [
|
|
1656
|
+
cliPath,
|
|
1657
|
+
"--school-tag",
|
|
1658
|
+
serializeSchoolTagSelection(searchParams.school_tag),
|
|
1659
|
+
"--degree",
|
|
1660
|
+
serializeDegreeSelection(searchParams.degree),
|
|
1661
|
+
"--gender",
|
|
1662
|
+
searchParams.gender,
|
|
1663
|
+
"--recent-not-view",
|
|
1664
|
+
searchParams.recent_not_view,
|
|
1665
|
+
"--port",
|
|
1666
|
+
String(resolveWorkspaceDebugPort(workspaceRoot))
|
|
1667
|
+
];
|
|
1668
|
+
const normalizedSelectedJob = String(selectedJob || "").trim();
|
|
1669
|
+
if (normalizedSelectedJob) {
|
|
1670
|
+
args.push("--job", normalizedSelectedJob);
|
|
1671
|
+
}
|
|
1672
|
+
const result = await runProcess({
|
|
1673
|
+
command: "node",
|
|
1674
|
+
args,
|
|
1675
|
+
cwd: searchDir,
|
|
1676
|
+
timeoutMs: 180000,
|
|
1677
|
+
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1678
|
+
signal: runtime?.signal,
|
|
1679
|
+
onOutput: (event) => {
|
|
1680
|
+
safeInvokeCallback(runtime?.onOutput, event);
|
|
1681
|
+
},
|
|
1682
|
+
onHeartbeat: (event) => {
|
|
1683
|
+
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1687
|
+
const missingOutputError = result.code === 0 && !structured
|
|
1688
|
+
? {
|
|
1689
|
+
code: "RECOMMEND_SEARCH_NO_OUTPUT",
|
|
1690
|
+
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
1691
|
+
}
|
|
1692
|
+
: null;
|
|
1693
|
+
return {
|
|
1694
|
+
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
1695
|
+
stdout: result.stdout,
|
|
1696
|
+
stderr: result.stderr,
|
|
1697
|
+
structured,
|
|
1698
|
+
summary: structured?.result || null,
|
|
1699
|
+
error: structured?.error || missingOutputError || (
|
|
1700
|
+
result.code === 0
|
|
1701
|
+
? null
|
|
1702
|
+
: result.error_code === "ABORTED"
|
|
1703
|
+
? {
|
|
1704
|
+
code: "PROCESS_ABORTED",
|
|
1705
|
+
message: "推荐页筛选命令已取消。"
|
|
1706
|
+
}
|
|
1707
|
+
: {
|
|
1708
|
+
code: "RECOMMEND_SEARCH_FAILED",
|
|
1709
|
+
message: "推荐页筛选命令执行失败。"
|
|
1710
|
+
}
|
|
1711
|
+
)
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
export async function runRecommendScreenCli({ workspaceRoot, screenParams, resume = null, runtime = null }) {
|
|
1716
|
+
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
1717
|
+
if (!screenDir) {
|
|
1718
|
+
return {
|
|
1719
|
+
ok: false,
|
|
1720
|
+
stdout: "",
|
|
1721
|
+
stderr: "boss-recommend-screen-cli package not found",
|
|
1722
|
+
error: {
|
|
1723
|
+
code: "RECOMMEND_SCREEN_CLI_MISSING",
|
|
1724
|
+
message: "boss-recommend-screen-cli 目录不存在。"
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const configPath = resolveScreenConfigPath(workspaceRoot);
|
|
1729
|
+
const loaded = loadScreenConfig(configPath);
|
|
1730
|
+
if (!loaded.ok) {
|
|
1731
|
+
return {
|
|
1732
|
+
ok: false,
|
|
1733
|
+
stdout: "",
|
|
1734
|
+
stderr: loaded.error,
|
|
1735
|
+
error: {
|
|
1736
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
1737
|
+
message: loaded.error
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const fixedOutput = normalizeText(resume?.output_csv || "");
|
|
1743
|
+
const outputName = `recommend_screen_result_${Date.now()}.csv`;
|
|
1744
|
+
let outputPath = fixedOutput ? path.resolve(fixedOutput) : outputName;
|
|
1745
|
+
if (!fixedOutput) {
|
|
1746
|
+
if (loaded.config.outputDir) {
|
|
1747
|
+
const resolvedOutputDir = path.resolve(path.dirname(configPath), loaded.config.outputDir);
|
|
1748
|
+
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
1749
|
+
outputPath = path.join(resolvedOutputDir, outputName);
|
|
1750
|
+
} else {
|
|
1751
|
+
const desktopDir = getDesktopDir();
|
|
1752
|
+
fs.mkdirSync(desktopDir, { recursive: true });
|
|
1753
|
+
outputPath = path.join(desktopDir, outputName);
|
|
1754
|
+
}
|
|
1755
|
+
} else {
|
|
1756
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const checkpointPath = normalizeText(resume?.checkpoint_path || "")
|
|
1760
|
+
? path.resolve(String(resume.checkpoint_path))
|
|
1761
|
+
: null;
|
|
1762
|
+
const pauseControlPath = normalizeText(resume?.pause_control_path || "")
|
|
1763
|
+
? path.resolve(String(resume.pause_control_path))
|
|
1764
|
+
: null;
|
|
1765
|
+
const resumeRequested = resume?.resume === true;
|
|
1766
|
+
const requireCheckpoint = resume?.require_checkpoint === true;
|
|
1767
|
+
if (resumeRequested && requireCheckpoint) {
|
|
1768
|
+
if (!checkpointPath) {
|
|
1769
|
+
return {
|
|
1770
|
+
ok: false,
|
|
1771
|
+
paused: false,
|
|
1772
|
+
stdout: "",
|
|
1773
|
+
stderr: "",
|
|
1774
|
+
structured: null,
|
|
1775
|
+
summary: null,
|
|
1776
|
+
error: {
|
|
1777
|
+
code: "RESUME_CHECKPOINT_MISSING",
|
|
1778
|
+
message: "恢复执行缺少 checkpoint_path,无法从上次进度继续。"
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
if (!fs.existsSync(checkpointPath)) {
|
|
1783
|
+
return {
|
|
1784
|
+
ok: false,
|
|
1785
|
+
paused: false,
|
|
1786
|
+
stdout: "",
|
|
1787
|
+
stderr: "",
|
|
1788
|
+
structured: null,
|
|
1789
|
+
summary: null,
|
|
1790
|
+
error: {
|
|
1791
|
+
code: "RESUME_CHECKPOINT_MISSING",
|
|
1792
|
+
message: `恢复执行未找到 checkpoint 文件:${checkpointPath}`
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const cliPath = resolveRecommendScreenCliEntry(screenDir);
|
|
1799
|
+
const args = [
|
|
1800
|
+
cliPath,
|
|
1801
|
+
"--baseurl",
|
|
1802
|
+
loaded.config.baseUrl,
|
|
1803
|
+
"--apikey",
|
|
1804
|
+
loaded.config.apiKey,
|
|
1805
|
+
"--model",
|
|
1806
|
+
loaded.config.model,
|
|
1807
|
+
"--port",
|
|
1808
|
+
String(resolveWorkspaceDebugPort(workspaceRoot)),
|
|
1809
|
+
"--criteria",
|
|
1810
|
+
screenParams.criteria,
|
|
1811
|
+
"--post-action",
|
|
1812
|
+
screenParams.post_action,
|
|
1813
|
+
"--post-action-confirmed",
|
|
1814
|
+
"true",
|
|
1815
|
+
"--output",
|
|
1816
|
+
outputPath
|
|
1817
|
+
];
|
|
1818
|
+
|
|
1819
|
+
if (loaded.config.openaiOrganization) {
|
|
1820
|
+
args.push("--openai-organization", loaded.config.openaiOrganization);
|
|
1821
|
+
}
|
|
1822
|
+
if (loaded.config.openaiProject) {
|
|
1823
|
+
args.push("--openai-project", loaded.config.openaiProject);
|
|
1824
|
+
}
|
|
1825
|
+
if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
|
|
1826
|
+
args.push("--targetCount", String(screenParams.target_count));
|
|
1827
|
+
}
|
|
1828
|
+
if (screenParams.post_action === "greet"
|
|
1829
|
+
&& Number.isInteger(screenParams.max_greet_count)
|
|
1830
|
+
&& screenParams.max_greet_count > 0) {
|
|
1831
|
+
args.push("--max-greet-count", String(screenParams.max_greet_count));
|
|
1832
|
+
}
|
|
1833
|
+
if (checkpointPath) {
|
|
1834
|
+
args.push("--checkpoint-path", checkpointPath);
|
|
1835
|
+
}
|
|
1836
|
+
if (pauseControlPath) {
|
|
1837
|
+
args.push("--pause-control-path", pauseControlPath);
|
|
1838
|
+
}
|
|
1839
|
+
if (resumeRequested) {
|
|
1840
|
+
args.push("--resume");
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
let inferredProgress = {
|
|
1844
|
+
processed: 0,
|
|
1845
|
+
passed: 0,
|
|
1846
|
+
skipped: 0,
|
|
1847
|
+
greet_count: 0
|
|
1848
|
+
};
|
|
1849
|
+
let inferredTracker = createScreenProgressTracker();
|
|
1850
|
+
const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
|
|
1851
|
+
|
|
1852
|
+
const result = await runProcess({
|
|
1853
|
+
command: "node",
|
|
1854
|
+
args,
|
|
1855
|
+
cwd: screenDir,
|
|
1856
|
+
timeoutMs: screenTimeoutMs,
|
|
1857
|
+
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
1858
|
+
signal: runtime?.signal,
|
|
1859
|
+
onOutput: (event) => {
|
|
1860
|
+
safeInvokeCallback(runtime?.onOutput, event);
|
|
1861
|
+
},
|
|
1862
|
+
onLine: (event) => {
|
|
1863
|
+
const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
|
|
1864
|
+
if (!parsed) return;
|
|
1865
|
+
inferredProgress = parsed.progress;
|
|
1866
|
+
inferredTracker = parsed.tracker;
|
|
1867
|
+
safeInvokeCallback(runtime?.onProgress, {
|
|
1868
|
+
...inferredProgress,
|
|
1869
|
+
line: parsed.line
|
|
1870
|
+
});
|
|
1871
|
+
},
|
|
1872
|
+
onHeartbeat: (event) => {
|
|
1873
|
+
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1877
|
+
const status = normalizeText(structured?.status || "").toUpperCase();
|
|
1878
|
+
const summary = structured?.result || null;
|
|
1879
|
+
if (summary) {
|
|
1880
|
+
safeInvokeCallback(runtime?.onProgress, {
|
|
1881
|
+
processed: Number.isInteger(summary.processed_count) ? summary.processed_count : inferredProgress.processed,
|
|
1882
|
+
passed: Number.isInteger(summary.passed_count) ? summary.passed_count : inferredProgress.passed,
|
|
1883
|
+
skipped: Number.isInteger(summary.skipped_count) ? summary.skipped_count : inferredProgress.skipped,
|
|
1884
|
+
greet_count: Number.isInteger(summary.greet_count) ? summary.greet_count : inferredProgress.greet_count
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const missingOutputError = result.code === 0 && !structured
|
|
1888
|
+
? {
|
|
1889
|
+
code: "RECOMMEND_SCREEN_NO_OUTPUT",
|
|
1890
|
+
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
1891
|
+
}
|
|
1892
|
+
: null;
|
|
1893
|
+
return {
|
|
1894
|
+
ok: result.code === 0 && status === "COMPLETED",
|
|
1895
|
+
paused: result.code === 0 && status === "PAUSED",
|
|
1896
|
+
stdout: result.stdout,
|
|
1897
|
+
stderr: result.stderr,
|
|
1898
|
+
structured,
|
|
1899
|
+
summary,
|
|
1900
|
+
error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
export const __testables = {
|
|
1905
|
+
runProcess,
|
|
1906
|
+
parseJsonOutput,
|
|
1907
|
+
parseScreenProgressLine,
|
|
1908
|
+
resolveRecommendScreenTimeoutMs,
|
|
1909
|
+
buildRecommendScreenProcessError
|
|
1910
|
+
};
|