poi-plugin-mcp 0.2.16 → 0.2.21
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/README.md +290 -201
- package/index.js +3 -0
- package/lib/bridge-controller.js +75 -1
- package/lib/fleet-metrics.js +249 -0
- package/lib/poi-action-events.js +405 -292
- package/lib/poi-api-responses.js +165 -0
- package/lib/poi-auto-interaction-recorder.js +399 -0
- package/lib/poi-data-query.js +254 -0
- package/lib/poi-http-bridge.js +1673 -1320
- package/lib/poi-input.js +366 -302
- package/lib/poi-interaction-recorder.js +1693 -0
- package/lib/poi-telemetry.js +515 -439
- package/lib/poi-webview-runtime.js +1181 -0
- package/lib/settings-view.js +112 -0
- package/lib/settings.js +8 -0
- package/mcp-server.js +542 -456
- package/package.json +3 -3
package/lib/poi-http-bridge.js
CHANGED
|
@@ -1,1320 +1,1673 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
|
-
const http = require('http')
|
|
3
|
-
const crypto = require('crypto')
|
|
4
|
-
const os = require('os')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const packageJson = require('../package.json')
|
|
7
|
-
const { loadOrCreateInputToken } = require('./input-token')
|
|
8
|
-
const { createPoiInputProvider } = require('./poi-input')
|
|
9
|
-
const { createPoiInputLease } = require('./poi-input-lease')
|
|
10
|
-
const { createPoiScreenshotProvider } = require('./poi-screenshot')
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
sendInputJson(res,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
sendInputJson(res,
|
|
186
|
-
return
|
|
187
|
-
}
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
return
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function
|
|
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
|
-
|
|
862
|
-
|
|
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
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
: {
|
|
1004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const http = require('http')
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const os = require('os')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const packageJson = require('../package.json')
|
|
7
|
+
const { loadOrCreateInputToken } = require('./input-token')
|
|
8
|
+
const { createPoiInputProvider } = require('./poi-input')
|
|
9
|
+
const { createPoiInputLease } = require('./poi-input-lease')
|
|
10
|
+
const { createPoiScreenshotProvider } = require('./poi-screenshot')
|
|
11
|
+
const {
|
|
12
|
+
MAX_QUERY_BODY_BYTES,
|
|
13
|
+
createPoiDataQuery,
|
|
14
|
+
} = require('./poi-data-query')
|
|
15
|
+
const {
|
|
16
|
+
collectFleetMetricShips,
|
|
17
|
+
inspectFleetMetrics,
|
|
18
|
+
moraleMeaning,
|
|
19
|
+
speedFromRaw,
|
|
20
|
+
speedMeaning,
|
|
21
|
+
} = require('./fleet-metrics')
|
|
22
|
+
|
|
23
|
+
const DEFAULT_PORT = 17777
|
|
24
|
+
const DEFAULT_PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
|
|
25
|
+
const DEFAULT_PLANNER_FILE = path.join(
|
|
26
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
27
|
+
'poi',
|
|
28
|
+
'poi-plugin-ship-info.json',
|
|
29
|
+
)
|
|
30
|
+
const DEFAULT_MASTER_FILE = path.join(
|
|
31
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
32
|
+
'poi',
|
|
33
|
+
'navy-album',
|
|
34
|
+
'master.json',
|
|
35
|
+
)
|
|
36
|
+
const JSONRPC_VERSION = '2.0'
|
|
37
|
+
|
|
38
|
+
const POI_RESOURCE_FIELDS = Object.freeze([
|
|
39
|
+
['fuel', '燃料'],
|
|
40
|
+
['ammo', '弹药'],
|
|
41
|
+
['steel', '钢材'],
|
|
42
|
+
['bauxite', '铝土'],
|
|
43
|
+
['instantConstruction', '高速建造材(喷火)'],
|
|
44
|
+
['repairBuckets', '高速修复材(桶)'],
|
|
45
|
+
['developmentMaterials', '开发资材'],
|
|
46
|
+
['improvementMaterials', '改修资材(螺丝)'],
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
function decodePoiResources(raw) {
|
|
50
|
+
const values = Array.isArray(raw) ? raw : []
|
|
51
|
+
const named = { raw: values }
|
|
52
|
+
for (let index = 0; index < POI_RESOURCE_FIELDS.length; index += 1) {
|
|
53
|
+
const [key, label] = POI_RESOURCE_FIELDS[index]
|
|
54
|
+
named[key] = { key, label, raw: values[index] ?? null }
|
|
55
|
+
}
|
|
56
|
+
return named
|
|
57
|
+
}
|
|
58
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05'
|
|
59
|
+
const INPUT_BODY_LIMIT = 64 * 1024
|
|
60
|
+
const MASTER_FILE_LIMIT = 16 * 1024 * 1024
|
|
61
|
+
const DEFAULT_ACTION_EVENT_LIMIT = 64
|
|
62
|
+
const MAX_ACTION_EVENT_LIMIT = 256
|
|
63
|
+
const DEFAULT_ACTION_EVENT_WAIT_TIMEOUT_MS = 30_000
|
|
64
|
+
const MAX_ACTION_EVENT_WAIT_TIMEOUT_MS = 60_000
|
|
65
|
+
|
|
66
|
+
function createPoiDataBridge(options = {}) {
|
|
67
|
+
const getStore = options.getStore || defaultGetStore
|
|
68
|
+
const configuredPort = options.port == null ? DEFAULT_PORT : options.port
|
|
69
|
+
const portFile = options.portFile || DEFAULT_PORT_FILE
|
|
70
|
+
const plannerFile = options.plannerFile || DEFAULT_PLANNER_FILE
|
|
71
|
+
const masterFile = options.masterFile || DEFAULT_MASTER_FILE
|
|
72
|
+
const logger = options.logger || console
|
|
73
|
+
const getQuestList = options.getQuestList || (() => ({ available: false, generation: 0 }))
|
|
74
|
+
const getQuestAction = options.getQuestAction || (() => ({ available: false, generation: 0 }))
|
|
75
|
+
const getEquipmentAction = options.getEquipmentAction ||
|
|
76
|
+
(() => ({ available: false, generation: 0 }))
|
|
77
|
+
const getEquipmentSelection = options.getEquipmentSelection ||
|
|
78
|
+
(() => ({ available: false, generation: 0 }))
|
|
79
|
+
const getUnsetSlot = options.getUnsetSlot ||
|
|
80
|
+
(() => ({ available: false, generation: 0 }))
|
|
81
|
+
const getFleetAction = options.getFleetAction ||
|
|
82
|
+
(() => ({ available: false, generation: 0 }))
|
|
83
|
+
const getActionEvents = options.getActionEvents ||
|
|
84
|
+
(() => ({
|
|
85
|
+
available: false,
|
|
86
|
+
sessionId: null,
|
|
87
|
+
latestGeneration: 0,
|
|
88
|
+
events: [],
|
|
89
|
+
}))
|
|
90
|
+
const getActionEventsWait = options.getActionEventsWait ||
|
|
91
|
+
(typeof getActionEvents.wait === 'function'
|
|
92
|
+
? getActionEvents.wait.bind(getActionEvents)
|
|
93
|
+
: null)
|
|
94
|
+
const getApiResponses = options.getApiResponses ||
|
|
95
|
+
(() => ({
|
|
96
|
+
available: false,
|
|
97
|
+
latestGeneration: 0,
|
|
98
|
+
responses: [],
|
|
99
|
+
}))
|
|
100
|
+
const getBattleTelemetry = options.getBattleTelemetry ||
|
|
101
|
+
(() => ({ available: false, generation: 0 }))
|
|
102
|
+
const inputEnabled = options.inputEnabled === true
|
|
103
|
+
const debugEvalEnabled = options.debugEvalEnabled === true
|
|
104
|
+
const inputToken = options.inputToken || (
|
|
105
|
+
inputEnabled ? loadOrCreateInputToken(options.inputTokenFile) : null
|
|
106
|
+
)
|
|
107
|
+
const inputLease = options.inputLease || createPoiInputLease(options.inputLeaseOptions)
|
|
108
|
+
let captureScreenshot = options.captureScreenshot || null
|
|
109
|
+
let performInput = options.performInput || null
|
|
110
|
+
let dataQuery = options.dataQuery || null
|
|
111
|
+
|
|
112
|
+
let server = null
|
|
113
|
+
let actualPort = 0
|
|
114
|
+
let inputPending = Promise.resolve()
|
|
115
|
+
const pendingActionEventWaits = new Set()
|
|
116
|
+
|
|
117
|
+
function readStore() {
|
|
118
|
+
const store = getStore()
|
|
119
|
+
if (!store || !store.info) {
|
|
120
|
+
throw new Error('POI store not ready. Enter the game first.')
|
|
121
|
+
}
|
|
122
|
+
return store
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sendJson(res, statusCode, data, options = {}) {
|
|
126
|
+
const headers = {
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
}
|
|
129
|
+
if (options.allowCors !== false) {
|
|
130
|
+
headers['Access-Control-Allow-Origin'] = '*'
|
|
131
|
+
}
|
|
132
|
+
if (options.noStore) {
|
|
133
|
+
headers['Cache-Control'] = 'no-store'
|
|
134
|
+
}
|
|
135
|
+
Object.assign(headers, options.headers)
|
|
136
|
+
res.writeHead(statusCode, headers)
|
|
137
|
+
res.end(JSON.stringify(data))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sendInputJson(res, statusCode, data, options = {}) {
|
|
141
|
+
sendJson(res, statusCode, data, {
|
|
142
|
+
allowCors: false,
|
|
143
|
+
noStore: true,
|
|
144
|
+
...options,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function currentDataQuery() {
|
|
149
|
+
if (!dataQuery) {
|
|
150
|
+
dataQuery = createPoiDataQuery({
|
|
151
|
+
getStore,
|
|
152
|
+
getApiResponses,
|
|
153
|
+
resolveWebContents: options.resolveWebContents,
|
|
154
|
+
cacheRoot: options.cacheRoot,
|
|
155
|
+
logger,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
return dataQuery
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handleDataRequest(req, res, endpoint) {
|
|
162
|
+
if (req.method !== 'POST') {
|
|
163
|
+
drainRequest(req)
|
|
164
|
+
sendInputJson(res, 405, { error: `${endpoint} only accepts POST requests.` })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (!hasValidBearerToken(req.headers.authorization, inputToken)) {
|
|
168
|
+
drainRequest(req)
|
|
169
|
+
sendInputJson(
|
|
170
|
+
res,
|
|
171
|
+
401,
|
|
172
|
+
{ error: 'A valid Bearer token is required.' },
|
|
173
|
+
{ headers: { 'WWW-Authenticate': 'Bearer' } },
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const body = await readRequestBody(
|
|
179
|
+
req,
|
|
180
|
+
MAX_QUERY_BODY_BYTES,
|
|
181
|
+
'Data request body exceeds 64KB.',
|
|
182
|
+
)
|
|
183
|
+
const request = JSON.parse(body || '{}')
|
|
184
|
+
if (endpoint === '/query') {
|
|
185
|
+
sendInputJson(res, 200, await currentDataQuery().query(request))
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (!debugEvalEnabled) {
|
|
189
|
+
sendInputJson(res, 403, {
|
|
190
|
+
code: 'DEBUG_EVAL_DISABLED',
|
|
191
|
+
error: 'Dangerous WebView debug evaluation is disabled in Poi settings.',
|
|
192
|
+
})
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
sendInputJson(
|
|
196
|
+
res,
|
|
197
|
+
200,
|
|
198
|
+
await currentDataQuery().runtime.evaluate(request),
|
|
199
|
+
)
|
|
200
|
+
} catch (error) {
|
|
201
|
+
sendInputJson(
|
|
202
|
+
res,
|
|
203
|
+
error.statusCode || (error.code === 'BODY_TOO_LARGE' ? 413 : 400),
|
|
204
|
+
{
|
|
205
|
+
...(error.code ? { code: error.code } : {}),
|
|
206
|
+
...(error.details ? { details: error.details } : {}),
|
|
207
|
+
error: error.message,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function enqueueInput(operation, claim) {
|
|
214
|
+
const execute = async () => {
|
|
215
|
+
inputLease.assertClaimActive(claim)
|
|
216
|
+
if (!performInput) {
|
|
217
|
+
performInput = createPoiInputProvider({ getStore })
|
|
218
|
+
}
|
|
219
|
+
const operationName = await performInput(operation)
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
operation: operationName,
|
|
223
|
+
sequence: claim.sequence,
|
|
224
|
+
leaseId: claim.leaseId,
|
|
225
|
+
ownerSessionId: claim.ownerSessionId,
|
|
226
|
+
runId: claim.runId,
|
|
227
|
+
action: claim.action,
|
|
228
|
+
acceptedAt: claim.acceptedAt,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const result = inputPending.then(execute, execute)
|
|
232
|
+
inputPending = result.catch(() => {})
|
|
233
|
+
return result
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function handleInputRequest(req, res) {
|
|
237
|
+
if (req.method !== 'POST') {
|
|
238
|
+
drainRequest(req)
|
|
239
|
+
sendInputJson(res, 405, { error: 'Input endpoint only accepts POST requests.' })
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (!hasValidBearerToken(req.headers.authorization, inputToken)) {
|
|
243
|
+
drainRequest(req)
|
|
244
|
+
sendInputJson(
|
|
245
|
+
res,
|
|
246
|
+
401,
|
|
247
|
+
{ error: 'A valid Bearer token is required.' },
|
|
248
|
+
{ headers: { 'WWW-Authenticate': 'Bearer' } },
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
if (requestContentLength(req) > INPUT_BODY_LIMIT) {
|
|
253
|
+
drainRequest(req)
|
|
254
|
+
sendInputJson(res, 413, { error: 'Input request body exceeds 64KB.' })
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (!inputEnabled) {
|
|
258
|
+
drainRequest(req)
|
|
259
|
+
sendInputJson(res, 403, { error: 'WebView input is disabled.' })
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const body = await readRequestBody(
|
|
265
|
+
req,
|
|
266
|
+
INPUT_BODY_LIMIT,
|
|
267
|
+
'Input request body exceeds 64KB.',
|
|
268
|
+
)
|
|
269
|
+
const operation = JSON.parse(body || '{}')
|
|
270
|
+
const { input, claim } = claimInputOperation(inputLease, operation)
|
|
271
|
+
sendInputJson(res, 200, await enqueueInput(input, claim))
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const statusCode = error.statusCode || (error.code === 'BODY_TOO_LARGE'
|
|
274
|
+
? 413
|
|
275
|
+
: /WebView|dimensions/.test(error.message)
|
|
276
|
+
? 503
|
|
277
|
+
: 400)
|
|
278
|
+
sendInputJson(res, statusCode, {
|
|
279
|
+
...(error.code ? { code: error.code } : {}),
|
|
280
|
+
error: error.message,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function handleInputLeaseRequest(req, res, endpoint) {
|
|
286
|
+
if (!hasValidBearerToken(req.headers.authorization, inputToken)) {
|
|
287
|
+
drainRequest(req)
|
|
288
|
+
sendInputJson(
|
|
289
|
+
res,
|
|
290
|
+
401,
|
|
291
|
+
{ error: 'A valid Bearer token is required.' },
|
|
292
|
+
{ headers: { 'WWW-Authenticate': 'Bearer' } },
|
|
293
|
+
)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
if (endpoint === '/input/lease') {
|
|
297
|
+
if (req.method !== 'GET') {
|
|
298
|
+
drainRequest(req)
|
|
299
|
+
sendInputJson(res, 405, { error: 'Input lease status only accepts GET requests.' })
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
const lease = inputLease.status()
|
|
303
|
+
sendInputJson(res, 200, { active: lease !== null, lease })
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
if (req.method !== 'POST') {
|
|
307
|
+
drainRequest(req)
|
|
308
|
+
sendInputJson(res, 405, { error: 'Input lease changes only accept POST requests.' })
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
if (!inputEnabled) {
|
|
312
|
+
drainRequest(req)
|
|
313
|
+
sendInputJson(res, 403, { error: 'WebView input is disabled.' })
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (requestContentLength(req) > INPUT_BODY_LIMIT) {
|
|
317
|
+
drainRequest(req)
|
|
318
|
+
sendInputJson(res, 413, { error: 'Input request body exceeds 64KB.' })
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const body = await readRequestBody(
|
|
324
|
+
req,
|
|
325
|
+
INPUT_BODY_LIMIT,
|
|
326
|
+
'Input request body exceeds 64KB.',
|
|
327
|
+
)
|
|
328
|
+
const request = JSON.parse(body || '{}')
|
|
329
|
+
if (endpoint === '/input/lease/acquire') {
|
|
330
|
+
const lease = inputLease.acquire(request)
|
|
331
|
+
sendInputJson(res, 200, { active: true, lease })
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
if (endpoint === '/input/lease/renew') {
|
|
335
|
+
const lease = inputLease.renew(request)
|
|
336
|
+
sendInputJson(res, 200, { active: true, lease })
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
if (endpoint === '/input/lease/release') {
|
|
340
|
+
inputLease.release(request)
|
|
341
|
+
sendInputJson(res, 200, { active: false, released: true })
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
if (endpoint === '/input/lease/revoke') {
|
|
345
|
+
inputLease.revoke(request)
|
|
346
|
+
sendInputJson(res, 200, { active: false, revoked: true })
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
sendInputJson(res, 404, { error: `Unknown input lease endpoint: ${endpoint}` })
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const statusCode = error.statusCode || (error.code === 'BODY_TOO_LARGE' ? 413 : 400)
|
|
352
|
+
sendInputJson(res, statusCode, {
|
|
353
|
+
...(error.code ? { code: error.code } : {}),
|
|
354
|
+
error: error.message,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function handleActionEventsWait(req, res, requestUrl) {
|
|
360
|
+
if (req.method !== 'GET') {
|
|
361
|
+
drainRequest(req)
|
|
362
|
+
sendJson(res, 405, {
|
|
363
|
+
error: 'Action events wait endpoint only accepts GET requests.',
|
|
364
|
+
})
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const controller = new AbortController()
|
|
369
|
+
let responded = false
|
|
370
|
+
const abort = () => {
|
|
371
|
+
if (!responded) controller.abort()
|
|
372
|
+
}
|
|
373
|
+
const cleanup = () => {
|
|
374
|
+
req.removeListener('aborted', abort)
|
|
375
|
+
res.removeListener('close', abort)
|
|
376
|
+
pendingActionEventWaits.delete(controller)
|
|
377
|
+
}
|
|
378
|
+
req.once('aborted', abort)
|
|
379
|
+
res.once('close', abort)
|
|
380
|
+
pendingActionEventWaits.add(controller)
|
|
381
|
+
|
|
382
|
+
const options = {
|
|
383
|
+
after: clampedQueryInteger(
|
|
384
|
+
requestUrl.searchParams.get('after'),
|
|
385
|
+
0,
|
|
386
|
+
0,
|
|
387
|
+
Number.MAX_SAFE_INTEGER,
|
|
388
|
+
),
|
|
389
|
+
limit: clampedQueryInteger(
|
|
390
|
+
requestUrl.searchParams.get('limit'),
|
|
391
|
+
DEFAULT_ACTION_EVENT_LIMIT,
|
|
392
|
+
1,
|
|
393
|
+
MAX_ACTION_EVENT_LIMIT,
|
|
394
|
+
),
|
|
395
|
+
timeoutMs: clampedQueryInteger(
|
|
396
|
+
requestUrl.searchParams.get('timeoutMs'),
|
|
397
|
+
DEFAULT_ACTION_EVENT_WAIT_TIMEOUT_MS,
|
|
398
|
+
1,
|
|
399
|
+
MAX_ACTION_EVENT_WAIT_TIMEOUT_MS,
|
|
400
|
+
),
|
|
401
|
+
signal: controller.signal,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const result = await awaitActionEventsWait(options)
|
|
406
|
+
if (controller.signal.aborted || res.destroyed || res.writableEnded) return
|
|
407
|
+
responded = true
|
|
408
|
+
sendJson(res, 200, {
|
|
409
|
+
...result,
|
|
410
|
+
timedOut: result && typeof result.timedOut === 'boolean'
|
|
411
|
+
? result.timedOut
|
|
412
|
+
: false,
|
|
413
|
+
})
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (controller.signal.aborted && (res.destroyed || res.writableEnded)) return
|
|
416
|
+
if (res.destroyed || res.writableEnded) return
|
|
417
|
+
responded = true
|
|
418
|
+
sendJson(res, error.statusCode || 503, {
|
|
419
|
+
...(error.code ? { code: error.code } : {}),
|
|
420
|
+
error: error.message,
|
|
421
|
+
}, controller.signal.aborted ? { headers: { Connection: 'close' } } : {})
|
|
422
|
+
} finally {
|
|
423
|
+
cleanup()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function awaitActionEventsWait(options) {
|
|
428
|
+
const operation = () => getActionEventsWait
|
|
429
|
+
? getActionEventsWait(options)
|
|
430
|
+
: { ...getActionEvents(options), timedOut: true }
|
|
431
|
+
if (!options.signal) return Promise.resolve().then(operation)
|
|
432
|
+
if (options.signal.aborted) return Promise.reject(createActionEventAbortError())
|
|
433
|
+
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
let settled = false
|
|
436
|
+
const finish = (settlement, value) => {
|
|
437
|
+
if (settled) return
|
|
438
|
+
settled = true
|
|
439
|
+
options.signal.removeEventListener('abort', onAbort)
|
|
440
|
+
settlement(value)
|
|
441
|
+
}
|
|
442
|
+
const onAbort = () => finish(reject, createActionEventAbortError())
|
|
443
|
+
options.signal.addEventListener('abort', onAbort, { once: true })
|
|
444
|
+
let pending
|
|
445
|
+
try {
|
|
446
|
+
pending = operation()
|
|
447
|
+
} catch (error) {
|
|
448
|
+
finish(reject, error)
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
Promise.resolve(pending).then(
|
|
452
|
+
(value) => finish(resolve, value),
|
|
453
|
+
(error) => finish(reject, error),
|
|
454
|
+
)
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function handleMcpRequest(req, res) {
|
|
459
|
+
if (req.method !== 'POST') {
|
|
460
|
+
sendJson(res, 405, { error: 'MCP endpoint only accepts POST requests.' })
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
readRequestBody(req)
|
|
465
|
+
.then((body) => {
|
|
466
|
+
const message = JSON.parse(body || '{}')
|
|
467
|
+
const response = handleMcpMessage(message, readStore, plannerFile)
|
|
468
|
+
|
|
469
|
+
if (response == null) {
|
|
470
|
+
res.writeHead(202, {
|
|
471
|
+
'Access-Control-Allow-Origin': '*',
|
|
472
|
+
'Content-Type': 'application/json',
|
|
473
|
+
})
|
|
474
|
+
res.end('')
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
sendJson(res, 200, response)
|
|
479
|
+
})
|
|
480
|
+
.catch((error) => {
|
|
481
|
+
sendJson(res, 200, jsonRpcError(null, -32700, error.message))
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function handleRequest(req, res) {
|
|
486
|
+
try {
|
|
487
|
+
if (req.url === '/shutdown') {
|
|
488
|
+
sendJson(res, 200, { status: 'shutting down' })
|
|
489
|
+
stop()
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const requestUrl = new URL(
|
|
494
|
+
req.url,
|
|
495
|
+
`http://127.0.0.1:${actualPort || configuredPort}`,
|
|
496
|
+
)
|
|
497
|
+
const endpoint = requestUrl.pathname
|
|
498
|
+
|
|
499
|
+
if (endpoint === '/health') {
|
|
500
|
+
sendJson(res, 200, { status: 'ok' })
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (endpoint === '/mcp') {
|
|
505
|
+
handleMcpRequest(req, res)
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (endpoint === '/query' || endpoint === '/debug/evaluate') {
|
|
510
|
+
await handleDataRequest(req, res, endpoint)
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (endpoint === '/debug/status') {
|
|
515
|
+
if (!hasValidBearerToken(req.headers.authorization, inputToken)) {
|
|
516
|
+
drainRequest(req)
|
|
517
|
+
sendInputJson(res, 401, { error: 'A valid Bearer token is required.' })
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
sendInputJson(res, 200, {
|
|
521
|
+
enabled: debugEvalEnabled,
|
|
522
|
+
endpoint: '/debug/evaluate',
|
|
523
|
+
warning: 'Arbitrary JavaScript can read or modify the signed-in game WebView.',
|
|
524
|
+
})
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (endpoint === '/screenshot') {
|
|
529
|
+
if (req.method !== 'GET') {
|
|
530
|
+
sendJson(
|
|
531
|
+
res,
|
|
532
|
+
405,
|
|
533
|
+
{ error: 'Screenshot endpoint only accepts GET requests.' },
|
|
534
|
+
{ allowCors: false, noStore: true },
|
|
535
|
+
)
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
if (!captureScreenshot) {
|
|
540
|
+
captureScreenshot = createPoiScreenshotProvider()
|
|
541
|
+
}
|
|
542
|
+
sendJson(
|
|
543
|
+
res,
|
|
544
|
+
200,
|
|
545
|
+
await captureScreenshot(),
|
|
546
|
+
{ allowCors: false, noStore: true },
|
|
547
|
+
)
|
|
548
|
+
} catch (error) {
|
|
549
|
+
sendJson(
|
|
550
|
+
res,
|
|
551
|
+
503,
|
|
552
|
+
{ error: error.message },
|
|
553
|
+
{ allowCors: false, noStore: true },
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (endpoint === '/input/status') {
|
|
560
|
+
if (req.method !== 'GET') {
|
|
561
|
+
drainRequest(req)
|
|
562
|
+
sendInputJson(
|
|
563
|
+
res,
|
|
564
|
+
405,
|
|
565
|
+
{ error: 'Input status endpoint only accepts GET requests.' },
|
|
566
|
+
)
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
sendInputJson(res, 200, { enabled: inputEnabled })
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (
|
|
574
|
+
endpoint === '/input/lease' ||
|
|
575
|
+
endpoint === '/input/lease/acquire' ||
|
|
576
|
+
endpoint === '/input/lease/renew' ||
|
|
577
|
+
endpoint === '/input/lease/release' ||
|
|
578
|
+
endpoint === '/input/lease/revoke'
|
|
579
|
+
) {
|
|
580
|
+
await handleInputLeaseRequest(req, res, endpoint)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (endpoint === '/input') {
|
|
585
|
+
await handleInputRequest(req, res)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (endpoint === '/quest-list') {
|
|
590
|
+
sendJson(res, 200, getQuestList())
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (endpoint === '/quest-action') {
|
|
595
|
+
sendJson(res, 200, getQuestAction())
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (endpoint === '/equipment-action') {
|
|
600
|
+
sendJson(res, 200, getEquipmentAction())
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (endpoint === '/equipment-selection') {
|
|
605
|
+
sendJson(res, 200, getEquipmentSelection())
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (endpoint === '/unsetslot') {
|
|
610
|
+
sendJson(res, 200, getUnsetSlot())
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (endpoint === '/fleet-action') {
|
|
615
|
+
sendJson(res, 200, getFleetAction())
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (endpoint === '/action-events/wait') {
|
|
620
|
+
await handleActionEventsWait(req, res, requestUrl)
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (endpoint === '/action-events') {
|
|
625
|
+
if (req.method !== 'GET') {
|
|
626
|
+
drainRequest(req)
|
|
627
|
+
sendJson(res, 405, {
|
|
628
|
+
error: 'Action events endpoint only accepts GET requests.',
|
|
629
|
+
})
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
sendJson(res, 200, getActionEvents({
|
|
633
|
+
after: clampedQueryInteger(
|
|
634
|
+
requestUrl.searchParams.get('after'),
|
|
635
|
+
0,
|
|
636
|
+
0,
|
|
637
|
+
Number.MAX_SAFE_INTEGER,
|
|
638
|
+
),
|
|
639
|
+
limit: clampedQueryInteger(
|
|
640
|
+
requestUrl.searchParams.get('limit'),
|
|
641
|
+
DEFAULT_ACTION_EVENT_LIMIT,
|
|
642
|
+
1,
|
|
643
|
+
MAX_ACTION_EVENT_LIMIT,
|
|
644
|
+
),
|
|
645
|
+
}))
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const store = readStore()
|
|
650
|
+
const info = store.info
|
|
651
|
+
|
|
652
|
+
switch (endpoint) {
|
|
653
|
+
case '/basic':
|
|
654
|
+
sendJson(res, 200, info.basic || {})
|
|
655
|
+
break
|
|
656
|
+
case '/fleets':
|
|
657
|
+
sendJson(res, 200, info.fleets || [])
|
|
658
|
+
break
|
|
659
|
+
case '/ships':
|
|
660
|
+
sendJson(res, 200, info.ships || {})
|
|
661
|
+
break
|
|
662
|
+
case '/equipment':
|
|
663
|
+
sendJson(res, 200, info.equips || {})
|
|
664
|
+
break
|
|
665
|
+
case '/resources':
|
|
666
|
+
sendJson(res, 200, info.resources || [])
|
|
667
|
+
break
|
|
668
|
+
case '/quests':
|
|
669
|
+
sendJson(res, 200, {
|
|
670
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
671
|
+
records: (info.quests && info.quests.records) || {},
|
|
672
|
+
})
|
|
673
|
+
break
|
|
674
|
+
case '/airbase':
|
|
675
|
+
sendJson(res, 200, info.airbase || [])
|
|
676
|
+
break
|
|
677
|
+
case '/names':
|
|
678
|
+
sendJson(res, 200, extractNames(store))
|
|
679
|
+
break
|
|
680
|
+
case '/master':
|
|
681
|
+
sendJson(res, 200, extractMasterData(store, masterFile))
|
|
682
|
+
break
|
|
683
|
+
case '/event':
|
|
684
|
+
sendJson(res, 200, extractEventData(store))
|
|
685
|
+
break
|
|
686
|
+
case '/planner':
|
|
687
|
+
sendJson(res, 200, extractPlannerData(store, plannerFile))
|
|
688
|
+
break
|
|
689
|
+
case '/battle':
|
|
690
|
+
sendJson(res, 200, combineBattleTelemetry(
|
|
691
|
+
getBattleTelemetry(),
|
|
692
|
+
extractProphetBattle(store),
|
|
693
|
+
))
|
|
694
|
+
break
|
|
695
|
+
case '/all':
|
|
696
|
+
sendJson(res, 200, {
|
|
697
|
+
basic: info.basic || {},
|
|
698
|
+
fleets: info.fleets || [],
|
|
699
|
+
ships: info.ships || {},
|
|
700
|
+
equipment: info.equips || {},
|
|
701
|
+
resources: info.resources || [],
|
|
702
|
+
quests: {
|
|
703
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
704
|
+
records: (info.quests && info.quests.records) || {},
|
|
705
|
+
},
|
|
706
|
+
airbase: info.airbase || [],
|
|
707
|
+
repairs: info.repairs || [],
|
|
708
|
+
constructions: info.constructions || [],
|
|
709
|
+
maps: info.maps || {},
|
|
710
|
+
useitems: info.useitems || {},
|
|
711
|
+
sortie: store.sortie || {},
|
|
712
|
+
names: extractNames(store),
|
|
713
|
+
})
|
|
714
|
+
break
|
|
715
|
+
default:
|
|
716
|
+
sendJson(res, 404, { error: `Unknown endpoint: ${endpoint}` })
|
|
717
|
+
}
|
|
718
|
+
} catch (error) {
|
|
719
|
+
sendJson(res, 503, { error: error.message })
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function start() {
|
|
724
|
+
if (server) return Promise.resolve()
|
|
725
|
+
|
|
726
|
+
fs.mkdirSync(path.dirname(portFile), { recursive: true })
|
|
727
|
+
server = http.createServer(handleRequest)
|
|
728
|
+
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
const onError = (error) => {
|
|
731
|
+
cleanupPortFile(portFile)
|
|
732
|
+
server = null
|
|
733
|
+
reject(error)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
server.once('error', onError)
|
|
737
|
+
server.listen(configuredPort, '127.0.0.1', () => {
|
|
738
|
+
server.removeListener('error', onError)
|
|
739
|
+
actualPort = server.address().port
|
|
740
|
+
fs.writeFileSync(portFile, String(actualPort), 'utf8')
|
|
741
|
+
logger.log(`[poi-plugin-mcp] HTTP API started on http://127.0.0.1:${actualPort}`)
|
|
742
|
+
resolve()
|
|
743
|
+
})
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function stop() {
|
|
748
|
+
for (const controller of Array.from(pendingActionEventWaits)) controller.abort()
|
|
749
|
+
|
|
750
|
+
if (!server) {
|
|
751
|
+
cleanupPortFile(portFile)
|
|
752
|
+
actualPort = 0
|
|
753
|
+
return Promise.resolve()
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const closingServer = server
|
|
757
|
+
server = null
|
|
758
|
+
|
|
759
|
+
return new Promise((resolve) => {
|
|
760
|
+
closingServer.close(() => {
|
|
761
|
+
cleanupPortFile(portFile)
|
|
762
|
+
actualPort = 0
|
|
763
|
+
resolve()
|
|
764
|
+
})
|
|
765
|
+
})
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
start,
|
|
770
|
+
stop,
|
|
771
|
+
getPort() {
|
|
772
|
+
return actualPort
|
|
773
|
+
},
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function clampedQueryInteger(value, fallback, minimum, maximum) {
|
|
778
|
+
if (value == null || value === '') return fallback
|
|
779
|
+
const parsed = Number(value)
|
|
780
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
781
|
+
return Math.min(maximum, Math.max(minimum, Math.trunc(parsed)))
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function claimInputOperation(inputLease, request) {
|
|
785
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)) {
|
|
786
|
+
throw inputBridgeError('INPUT_LEASE_REQUIRED', 'Input lease fields are required.', 409)
|
|
787
|
+
}
|
|
788
|
+
const {
|
|
789
|
+
leaseId,
|
|
790
|
+
ownerSessionId,
|
|
791
|
+
runId,
|
|
792
|
+
action,
|
|
793
|
+
sequence,
|
|
794
|
+
...input
|
|
795
|
+
} = request
|
|
796
|
+
if (
|
|
797
|
+
typeof leaseId !== 'string' ||
|
|
798
|
+
typeof ownerSessionId !== 'string' ||
|
|
799
|
+
typeof runId !== 'string' ||
|
|
800
|
+
typeof action !== 'string' ||
|
|
801
|
+
!Number.isSafeInteger(sequence)
|
|
802
|
+
) {
|
|
803
|
+
throw inputBridgeError('INPUT_LEASE_REQUIRED', 'Input lease fields are required.', 409)
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
input,
|
|
807
|
+
claim: inputLease.consumeInput({
|
|
808
|
+
leaseId,
|
|
809
|
+
ownerSessionId,
|
|
810
|
+
runId,
|
|
811
|
+
action,
|
|
812
|
+
sequence,
|
|
813
|
+
}),
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function inputBridgeError(code, message, statusCode) {
|
|
818
|
+
const error = new Error(message)
|
|
819
|
+
error.code = code
|
|
820
|
+
error.statusCode = statusCode
|
|
821
|
+
return error
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function createActionEventAbortError() {
|
|
825
|
+
const error = new Error('Action event wait aborted.')
|
|
826
|
+
error.name = 'AbortError'
|
|
827
|
+
error.code = 'ABORT_ERR'
|
|
828
|
+
return error
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function readRequestBody(
|
|
832
|
+
req,
|
|
833
|
+
maxBytes = 1024 * 1024,
|
|
834
|
+
tooLargeMessage = 'MCP request body is too large.',
|
|
835
|
+
) {
|
|
836
|
+
return new Promise((resolve, reject) => {
|
|
837
|
+
let body = ''
|
|
838
|
+
let bodyBytes = 0
|
|
839
|
+
let tooLarge = false
|
|
840
|
+
req.setEncoding('utf8')
|
|
841
|
+
req.on('data', (chunk) => {
|
|
842
|
+
if (tooLarge) return
|
|
843
|
+
bodyBytes += Buffer.byteLength(chunk)
|
|
844
|
+
if (bodyBytes > maxBytes) {
|
|
845
|
+
tooLarge = true
|
|
846
|
+
body = ''
|
|
847
|
+
return
|
|
848
|
+
}
|
|
849
|
+
body += chunk
|
|
850
|
+
})
|
|
851
|
+
req.on('end', () => {
|
|
852
|
+
if (tooLarge) {
|
|
853
|
+
const error = new Error(tooLargeMessage)
|
|
854
|
+
error.code = 'BODY_TOO_LARGE'
|
|
855
|
+
reject(error)
|
|
856
|
+
} else {
|
|
857
|
+
resolve(body)
|
|
858
|
+
}
|
|
859
|
+
})
|
|
860
|
+
req.on('error', reject)
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function requestContentLength(req) {
|
|
865
|
+
const value = req.headers['content-length']
|
|
866
|
+
if (value == null) return 0
|
|
867
|
+
const length = Number(value)
|
|
868
|
+
return Number.isSafeInteger(length) && length >= 0 ? length : Infinity
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function drainRequest(req) {
|
|
872
|
+
req.resume()
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function hasValidBearerToken(authorization, expectedToken) {
|
|
876
|
+
if (
|
|
877
|
+
typeof authorization !== 'string' ||
|
|
878
|
+
typeof expectedToken !== 'string' ||
|
|
879
|
+
!authorization.startsWith('Bearer ')
|
|
880
|
+
) {
|
|
881
|
+
return false
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const supplied = Buffer.from(authorization.slice('Bearer '.length), 'utf8')
|
|
885
|
+
const expected = Buffer.from(expectedToken, 'utf8')
|
|
886
|
+
return supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function handleMcpMessage(message, readStore, plannerFile) {
|
|
890
|
+
const { id, method, params } = message || {}
|
|
891
|
+
|
|
892
|
+
switch (method) {
|
|
893
|
+
case 'initialize':
|
|
894
|
+
return jsonRpcResult(id, {
|
|
895
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
896
|
+
capabilities: {
|
|
897
|
+
resources: { subscribe: false },
|
|
898
|
+
tools: {},
|
|
899
|
+
},
|
|
900
|
+
serverInfo: { name: 'poi-plugin-mcp', version: packageJson.version },
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
case 'notifications/initialized':
|
|
904
|
+
case 'notifications/cancelled':
|
|
905
|
+
return null
|
|
906
|
+
|
|
907
|
+
case 'ping':
|
|
908
|
+
return jsonRpcResult(id, {})
|
|
909
|
+
|
|
910
|
+
case 'resources/list':
|
|
911
|
+
return jsonRpcResult(id, {
|
|
912
|
+
resources: MCP_RESOURCE_ENDPOINTS.map(({ uri }) => ({
|
|
913
|
+
uri,
|
|
914
|
+
name: uri.replace('poi://', ''),
|
|
915
|
+
mimeType: 'application/json',
|
|
916
|
+
})),
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
case 'resources/read': {
|
|
920
|
+
const uri = params && params.uri
|
|
921
|
+
const endpoint = MCP_RESOURCE_ENDPOINTS.find((item) => item.uri === uri)
|
|
922
|
+
if (!endpoint) return jsonRpcError(id, -32602, `Unknown resource: ${uri}`)
|
|
923
|
+
const data = readBridgeData(endpoint.path, readStore, plannerFile)
|
|
924
|
+
return jsonRpcResult(id, {
|
|
925
|
+
contents: [{
|
|
926
|
+
uri,
|
|
927
|
+
mimeType: 'application/json',
|
|
928
|
+
text: JSON.stringify(data, null, 2),
|
|
929
|
+
}],
|
|
930
|
+
})
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
case 'tools/list':
|
|
934
|
+
return jsonRpcResult(id, { tools: MCP_TOOLS })
|
|
935
|
+
|
|
936
|
+
case 'tools/call': {
|
|
937
|
+
const toolName = params && params.name
|
|
938
|
+
const toolArgs = (params && params.arguments) || {}
|
|
939
|
+
const result = callMcpTool(toolName, toolArgs, readStore, plannerFile)
|
|
940
|
+
if (result.error) return jsonRpcError(id, -32602, result.error)
|
|
941
|
+
return jsonRpcResult(id, {
|
|
942
|
+
content: [{ type: 'text', text: JSON.stringify(result.value, null, 2) }],
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
default:
|
|
947
|
+
return jsonRpcError(id, -32601, `Unknown method: ${method}`)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const MCP_RESOURCE_ENDPOINTS = [
|
|
952
|
+
{ uri: 'poi://basic', path: '/basic' },
|
|
953
|
+
{ uri: 'poi://fleets', path: '/fleets' },
|
|
954
|
+
{ uri: 'poi://ships', path: '/ships' },
|
|
955
|
+
{ uri: 'poi://equipment', path: '/equipment' },
|
|
956
|
+
{ uri: 'poi://resources', path: '/resources' },
|
|
957
|
+
{ uri: 'poi://quests', path: '/quests' },
|
|
958
|
+
{ uri: 'poi://airbase', path: '/airbase' },
|
|
959
|
+
{ uri: 'poi://names', path: '/names' },
|
|
960
|
+
{ uri: 'poi://master', path: '/master' },
|
|
961
|
+
{ uri: 'poi://event', path: '/event' },
|
|
962
|
+
{ uri: 'poi://planner', path: '/planner' },
|
|
963
|
+
{ uri: 'poi://all', path: '/all' },
|
|
964
|
+
]
|
|
965
|
+
|
|
966
|
+
const MCP_TOOLS = [
|
|
967
|
+
{
|
|
968
|
+
name: 'get_fleet_status',
|
|
969
|
+
description:
|
|
970
|
+
'Inspect one owned fleet (1-4): names, slots, expansion, speed, morale, fuel/ammo, Formula 33 LOS (Cn 1-4), and fighter power. Use this instead of get_all when freezing or reading a sortie fleet.',
|
|
971
|
+
inputSchema: {
|
|
972
|
+
type: 'object',
|
|
973
|
+
properties: { fleetId: { type: 'number', description: 'Fleet number, 1-4.' } },
|
|
974
|
+
required: ['fleetId'],
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
name: 'search_ships',
|
|
979
|
+
description: 'Search owned ship instances by level and morale.',
|
|
980
|
+
inputSchema: {
|
|
981
|
+
type: 'object',
|
|
982
|
+
properties: {
|
|
983
|
+
minLevel: { type: 'number' },
|
|
984
|
+
maxLevel: { type: 'number' },
|
|
985
|
+
minMorale: { type: 'number' },
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: 'search_equipment',
|
|
991
|
+
description: 'Search owned equipment instances by improvement level.',
|
|
992
|
+
inputSchema: {
|
|
993
|
+
type: 'object',
|
|
994
|
+
properties: {
|
|
995
|
+
minLevel: { type: 'number', description: 'Minimum improvement level.' },
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: 'get_resources',
|
|
1001
|
+
description:
|
|
1002
|
+
'Get current account resources as named fields. Poi array order is fuel, ammo, steel, bauxite, instantConstruction (喷火), repairBuckets (桶), developmentMaterials, improvementMaterials (螺丝). HTTP /resources remains the raw 8-number array.',
|
|
1003
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: 'get_all',
|
|
1007
|
+
description:
|
|
1008
|
+
'Last-resort full account dump (all ships, equipment, quests, maps). Prefer get_fleet_status, search_*, or get_resources. Do not use this to name one fleet.',
|
|
1009
|
+
inputSchema: {
|
|
1010
|
+
type: 'object',
|
|
1011
|
+
properties: {
|
|
1012
|
+
include: {
|
|
1013
|
+
type: 'array',
|
|
1014
|
+
items: { type: 'string', enum: ['master', 'event', 'planner'] },
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
]
|
|
1020
|
+
|
|
1021
|
+
function callMcpTool(toolName, args, readStore, plannerFile) {
|
|
1022
|
+
switch (toolName) {
|
|
1023
|
+
case 'get_fleet_status':
|
|
1024
|
+
return { value: buildFleetStatus(args || {}, readStore) }
|
|
1025
|
+
case 'search_ships':
|
|
1026
|
+
return { value: searchShips(args || {}, readStore) }
|
|
1027
|
+
case 'search_equipment':
|
|
1028
|
+
return { value: searchEquipment(args || {}, readStore) }
|
|
1029
|
+
case 'get_resources':
|
|
1030
|
+
return { value: decodePoiResources(readBridgeData('/resources', readStore, plannerFile)) }
|
|
1031
|
+
case 'get_all':
|
|
1032
|
+
return { value: buildAllPayload(args || {}, readStore, plannerFile) }
|
|
1033
|
+
default:
|
|
1034
|
+
return { error: `Unknown tool: ${toolName}` }
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function readBridgeData(endpoint, readStore, plannerFile) {
|
|
1039
|
+
const store = readStore()
|
|
1040
|
+
const info = store.info
|
|
1041
|
+
|
|
1042
|
+
switch (endpoint) {
|
|
1043
|
+
case '/basic':
|
|
1044
|
+
return info.basic || {}
|
|
1045
|
+
case '/fleets':
|
|
1046
|
+
return info.fleets || []
|
|
1047
|
+
case '/ships':
|
|
1048
|
+
return info.ships || {}
|
|
1049
|
+
case '/equipment':
|
|
1050
|
+
return info.equips || {}
|
|
1051
|
+
case '/resources':
|
|
1052
|
+
return info.resources || []
|
|
1053
|
+
case '/quests':
|
|
1054
|
+
return {
|
|
1055
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
1056
|
+
records: (info.quests && info.quests.records) || {},
|
|
1057
|
+
}
|
|
1058
|
+
case '/airbase':
|
|
1059
|
+
return info.airbase || []
|
|
1060
|
+
case '/names':
|
|
1061
|
+
return extractNames(store)
|
|
1062
|
+
case '/master':
|
|
1063
|
+
return extractMasterData(store)
|
|
1064
|
+
case '/event':
|
|
1065
|
+
return extractEventData(store)
|
|
1066
|
+
case '/planner':
|
|
1067
|
+
return extractPlannerData(store, plannerFile)
|
|
1068
|
+
case '/all':
|
|
1069
|
+
return {
|
|
1070
|
+
basic: info.basic || {},
|
|
1071
|
+
fleets: info.fleets || [],
|
|
1072
|
+
ships: info.ships || {},
|
|
1073
|
+
equipment: info.equips || {},
|
|
1074
|
+
resources: info.resources || [],
|
|
1075
|
+
quests: {
|
|
1076
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
1077
|
+
records: (info.quests && info.quests.records) || {},
|
|
1078
|
+
},
|
|
1079
|
+
airbase: info.airbase || [],
|
|
1080
|
+
repairs: info.repairs || [],
|
|
1081
|
+
constructions: info.constructions || [],
|
|
1082
|
+
maps: info.maps || {},
|
|
1083
|
+
useitems: info.useitems || {},
|
|
1084
|
+
sortie: store.sortie || {},
|
|
1085
|
+
names: extractNames(store),
|
|
1086
|
+
}
|
|
1087
|
+
default:
|
|
1088
|
+
throw new Error(`Unknown endpoint: ${endpoint}`)
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function buildFleetStatus(args, readStore) {
|
|
1093
|
+
const fleetId = Number(args.fleetId)
|
|
1094
|
+
const store = readStore()
|
|
1095
|
+
const info = store.info || {}
|
|
1096
|
+
const fleets = Array.isArray(info.fleets) ? info.fleets : []
|
|
1097
|
+
const fleet = fleets[fleetId - 1]
|
|
1098
|
+
if (!fleet) return { error: `Fleet #${args.fleetId} not found` }
|
|
1099
|
+
|
|
1100
|
+
const ships = info.ships || {}
|
|
1101
|
+
const equips = info.equips || {}
|
|
1102
|
+
const names = extractNames(store)
|
|
1103
|
+
const master = extractMasterData(store)
|
|
1104
|
+
const hqLevel = Number(info.basic && info.basic.api_level)
|
|
1105
|
+
const metricShips = collectFleetMetricShips(fleet, ships, equips, master)
|
|
1106
|
+
const metrics = Number.isInteger(hqLevel) && hqLevel >= 1
|
|
1107
|
+
? inspectFleetMetrics(metricShips, hqLevel)
|
|
1108
|
+
: null
|
|
1109
|
+
|
|
1110
|
+
return {
|
|
1111
|
+
id: fleet.api_id,
|
|
1112
|
+
name: fleet.api_name,
|
|
1113
|
+
mission: fleet.api_mission,
|
|
1114
|
+
metrics,
|
|
1115
|
+
ships: (fleet.api_ship || []).filter((id) => id > 0).map((shipId, index) =>
|
|
1116
|
+
projectFleetShip(ships[shipId], shipId, index + 1, equips, names, master),
|
|
1117
|
+
),
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function projectFleetShip(ship, shipId, position, equips, names, master) {
|
|
1122
|
+
if (!ship) return { id: shipId, position }
|
|
1123
|
+
|
|
1124
|
+
const masterShip = master.ships && master.ships[ship.api_ship_id]
|
|
1125
|
+
const shipType = masterShip && master.shipTypes && master.shipTypes[masterShip.api_stype]
|
|
1126
|
+
const maxHp = Number(ship.api_maxhp) || 0
|
|
1127
|
+
const speedRaw = Number(ship.api_soku ?? (masterShip && masterShip.api_soku) ?? 0)
|
|
1128
|
+
const speedKind = speedFromRaw(speedRaw)
|
|
1129
|
+
|
|
1130
|
+
return {
|
|
1131
|
+
position,
|
|
1132
|
+
id: ship.api_id,
|
|
1133
|
+
shipId: ship.api_ship_id,
|
|
1134
|
+
masterId: ship.api_ship_id,
|
|
1135
|
+
name:
|
|
1136
|
+
(names.ships && names.ships[ship.api_ship_id]) ||
|
|
1137
|
+
(masterShip && masterShip.api_name) ||
|
|
1138
|
+
'',
|
|
1139
|
+
typeName: (shipType && shipType.api_name) || '',
|
|
1140
|
+
stype: (masterShip && masterShip.api_stype) || null,
|
|
1141
|
+
level: ship.api_lv,
|
|
1142
|
+
hp: `${ship.api_nowhp}/${ship.api_maxhp}`,
|
|
1143
|
+
hpMod4: maxHp % 4,
|
|
1144
|
+
morale: ship.api_cond,
|
|
1145
|
+
moraleMeaning: moraleMeaning(ship.api_cond || 0),
|
|
1146
|
+
speed: speedRaw,
|
|
1147
|
+
speedMeaning: speedMeaning(speedKind),
|
|
1148
|
+
fuel: ship.api_fuel,
|
|
1149
|
+
ammo: ship.api_bull,
|
|
1150
|
+
locked: ship.api_locked,
|
|
1151
|
+
slotnum: ship.api_slotnum || (ship.api_slot || []).filter((id) => id !== -1).length,
|
|
1152
|
+
onslot: Array.isArray(ship.api_onslot) ? ship.api_onslot : [],
|
|
1153
|
+
sallyArea: ship.api_sally_area || 0,
|
|
1154
|
+
fire: ship.api_karyoku || null,
|
|
1155
|
+
torp: ship.api_raisou || null,
|
|
1156
|
+
aa: ship.api_taiku || null,
|
|
1157
|
+
armor: ship.api_soukou || null,
|
|
1158
|
+
luck: ship.api_lucky || null,
|
|
1159
|
+
los: ship.api_sakuteki || null,
|
|
1160
|
+
asw: ship.api_taisen || null,
|
|
1161
|
+
slotItems: (ship.api_slot || [])
|
|
1162
|
+
.filter((equipId) => equipId > 0)
|
|
1163
|
+
.map((equipId) => describeEquip(equipId, equips, names, master))
|
|
1164
|
+
.filter(Boolean),
|
|
1165
|
+
expansion: describeExpansion(ship.api_slot_ex, equips, names, master),
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function describeEquip(equipId, equips, names, master) {
|
|
1170
|
+
if (!equipId || equipId <= 0) return null
|
|
1171
|
+
const equip = equips[equipId]
|
|
1172
|
+
if (!equip) return { id: equipId, missing: true }
|
|
1173
|
+
const masterId = equip.api_slotitem_id
|
|
1174
|
+
const masterEquip = master.equipment && master.equipment[masterId]
|
|
1175
|
+
const typeIds = masterEquip && Array.isArray(masterEquip.api_type) ? masterEquip.api_type : []
|
|
1176
|
+
const typeId = typeIds[2] || typeIds[1] || typeIds[0]
|
|
1177
|
+
const equipType = typeId && master.equipmentTypes && master.equipmentTypes[typeId]
|
|
1178
|
+
return {
|
|
1179
|
+
id: equip.api_id,
|
|
1180
|
+
equipId: masterId,
|
|
1181
|
+
name:
|
|
1182
|
+
(names.equipment && names.equipment[masterId]) ||
|
|
1183
|
+
(masterEquip && masterEquip.api_name) ||
|
|
1184
|
+
'',
|
|
1185
|
+
typeName: (equipType && equipType.api_name) || '',
|
|
1186
|
+
level: equip.api_level || 0,
|
|
1187
|
+
prof: equip.api_alv || 0,
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function describeExpansion(rawEx, equips, names, master) {
|
|
1192
|
+
const raw = Number(rawEx)
|
|
1193
|
+
if (!Number.isFinite(raw) || raw === 0) {
|
|
1194
|
+
return { raw: Number.isFinite(raw) ? raw : 0, state: 'closed', meaning: '未开孔', item: null }
|
|
1195
|
+
}
|
|
1196
|
+
if (raw < 0) {
|
|
1197
|
+
return { raw, state: 'open_empty', meaning: '已开孔但为空', item: null }
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
raw,
|
|
1201
|
+
state: 'equipped',
|
|
1202
|
+
meaning: '已装备',
|
|
1203
|
+
item: describeEquip(raw, equips, names, master),
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function searchShips(args, readStore) {
|
|
1208
|
+
const store = readStore()
|
|
1209
|
+
const master = extractMasterData(store)
|
|
1210
|
+
const ships = Object.values((store.info && store.info.ships) || {}).filter((ship) => {
|
|
1211
|
+
if (!ship) return false
|
|
1212
|
+
if (args.minLevel != null && ship.api_lv < Number(args.minLevel)) return false
|
|
1213
|
+
if (args.maxLevel != null && ship.api_lv > Number(args.maxLevel)) return false
|
|
1214
|
+
if (args.minMorale != null && ship.api_cond < Number(args.minMorale)) return false
|
|
1215
|
+
return true
|
|
1216
|
+
}).map((ship) => enrichShip(ship, master))
|
|
1217
|
+
|
|
1218
|
+
return { total: ships.length, ships }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function searchEquipment(args, readStore) {
|
|
1222
|
+
const store = readStore()
|
|
1223
|
+
const master = extractMasterData(store)
|
|
1224
|
+
const equipment = Object.values((store.info && store.info.equips) || {}).filter((equip) => {
|
|
1225
|
+
if (!equip) return false
|
|
1226
|
+
if (args.minLevel != null && (equip.api_level || 0) < Number(args.minLevel)) return false
|
|
1227
|
+
return true
|
|
1228
|
+
}).map((equip) => enrichEquipment(equip, master))
|
|
1229
|
+
|
|
1230
|
+
return { total: equipment.length, equipment }
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function buildAllPayload(args, readStore, plannerFile) {
|
|
1234
|
+
const payload = readBridgeData('/all', readStore, plannerFile)
|
|
1235
|
+
const include = Array.isArray(args.include) ? new Set(args.include) : new Set()
|
|
1236
|
+
|
|
1237
|
+
if (include.has('master')) payload.master = readBridgeData('/master', readStore, plannerFile)
|
|
1238
|
+
if (include.has('event')) payload.event = readBridgeData('/event', readStore, plannerFile)
|
|
1239
|
+
if (include.has('planner')) payload.planner = readBridgeData('/planner', readStore, plannerFile)
|
|
1240
|
+
|
|
1241
|
+
return payload
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function enrichShip(ship, master) {
|
|
1245
|
+
const masterShip = master.ships && master.ships[ship.api_ship_id]
|
|
1246
|
+
const shipType = masterShip && master.shipTypes && master.shipTypes[masterShip.api_stype]
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
...ship,
|
|
1250
|
+
instanceId: ship.api_id,
|
|
1251
|
+
masterId: ship.api_ship_id,
|
|
1252
|
+
name: (masterShip && masterShip.api_name) || '',
|
|
1253
|
+
typeName: (shipType && shipType.api_name) || '',
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function enrichEquipment(equip, master) {
|
|
1258
|
+
const masterEquip = master.equipment && master.equipment[equip.api_slotitem_id]
|
|
1259
|
+
const typeIds = masterEquip && Array.isArray(masterEquip.api_type) ? masterEquip.api_type : []
|
|
1260
|
+
const typeId = typeIds[2] || typeIds[1] || typeIds[0]
|
|
1261
|
+
const equipType = typeId && master.equipmentTypes && master.equipmentTypes[typeId]
|
|
1262
|
+
|
|
1263
|
+
return {
|
|
1264
|
+
...equip,
|
|
1265
|
+
instanceId: equip.api_id,
|
|
1266
|
+
masterId: equip.api_slotitem_id,
|
|
1267
|
+
name: (masterEquip && masterEquip.api_name) || '',
|
|
1268
|
+
typeName: (equipType && equipType.api_name) || '',
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function jsonRpcResult(id, result) {
|
|
1273
|
+
return { jsonrpc: JSONRPC_VERSION, id, result }
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function jsonRpcError(id, code, message) {
|
|
1277
|
+
return { jsonrpc: JSONRPC_VERSION, id, error: { code, message } }
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function defaultGetStore(storePath) {
|
|
1281
|
+
if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
|
|
1282
|
+
return window.getStore(storePath)
|
|
1283
|
+
}
|
|
1284
|
+
return null
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function extractProphetBattle(store) {
|
|
1288
|
+
const battle = store &&
|
|
1289
|
+
store.ext &&
|
|
1290
|
+
store.ext['poi-plugin-prophet'] &&
|
|
1291
|
+
store.ext['poi-plugin-prophet']._ &&
|
|
1292
|
+
store.ext['poi-plugin-prophet']._.battle
|
|
1293
|
+
if (!battle || typeof battle !== 'object') {
|
|
1294
|
+
return {
|
|
1295
|
+
available: false,
|
|
1296
|
+
source: 'poi-plugin-prophet',
|
|
1297
|
+
engine: 'poi-lib-battle',
|
|
1298
|
+
engineVersion: '3.0.5',
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const fleets = {
|
|
1303
|
+
main: compactBattleFleet(battle.mainFleet),
|
|
1304
|
+
escort: compactBattleFleet(battle.escortFleet),
|
|
1305
|
+
enemy: compactBattleFleet(battle.enemyFleet),
|
|
1306
|
+
enemyEscort: compactBattleFleet(battle.enemyEscort),
|
|
1307
|
+
}
|
|
1308
|
+
const result = compactBattleResult(battle.result)
|
|
1309
|
+
return {
|
|
1310
|
+
available: true,
|
|
1311
|
+
source: 'poi-plugin-prophet',
|
|
1312
|
+
engine: 'poi-lib-battle',
|
|
1313
|
+
engineVersion: '3.0.5',
|
|
1314
|
+
sortieState: finiteNumber(battle.sortieState, 0),
|
|
1315
|
+
sortieStateName: sortieStateName(battle.sortieState),
|
|
1316
|
+
mapAreaId: finiteNumber(battle.mapAreaId, 0),
|
|
1317
|
+
eventId: finiteNumber(battle.eventId, 0),
|
|
1318
|
+
eventKind: finiteNumber(battle.eventKind, 0),
|
|
1319
|
+
isBaseDefense: battle.isBaseDefense === true,
|
|
1320
|
+
isHeavyBomberDefense: battle.isHeavyBomberDefense === true,
|
|
1321
|
+
smokeType: finiteNumber(battle.smokeType, 0),
|
|
1322
|
+
airControl: stringValue(battle.airControl),
|
|
1323
|
+
battleForm: stringValue(battle.battleForm),
|
|
1324
|
+
enemyFormation: stringValue(battle.eFormation),
|
|
1325
|
+
rank: typeof result.rank === 'string' ? result.rank : null,
|
|
1326
|
+
mvpIndex0Based: compactMvp(result.mvp),
|
|
1327
|
+
heavilyDamaged: findHeavilyDamaged(fleets),
|
|
1328
|
+
fleets,
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function combineBattleTelemetry(telemetry, predicted) {
|
|
1333
|
+
const current = telemetry && typeof telemetry === 'object'
|
|
1334
|
+
? telemetry
|
|
1335
|
+
: { available: false, generation: 0 }
|
|
1336
|
+
return {
|
|
1337
|
+
available: current.available === true || predicted.available === true,
|
|
1338
|
+
generation: finiteNumber(current.generation, 0),
|
|
1339
|
+
status: typeof current.status === 'string' ? current.status : 'unavailable',
|
|
1340
|
+
observed: current.observed || null,
|
|
1341
|
+
predicted,
|
|
1342
|
+
official: current.official || null,
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function compactBattleResult(result) {
|
|
1347
|
+
if (!result || typeof result !== 'object') return {}
|
|
1348
|
+
return Object.fromEntries(
|
|
1349
|
+
['rank', 'mvp', 'getShip', 'getItem']
|
|
1350
|
+
.filter((key) => result[key] !== undefined)
|
|
1351
|
+
.map((key) => [key, result[key]]),
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function compactMvp(value) {
|
|
1356
|
+
const values = Array.isArray(value) ? value : [value, null]
|
|
1357
|
+
return {
|
|
1358
|
+
main: Number.isInteger(values[0]) && values[0] >= 0 ? values[0] : null,
|
|
1359
|
+
escort: Number.isInteger(values[1]) && values[1] >= 0 ? values[1] : null,
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function findHeavilyDamaged(fleets) {
|
|
1364
|
+
return ['main', 'escort'].flatMap((fleetName) =>
|
|
1365
|
+
fleets[fleetName].flatMap((ship) => {
|
|
1366
|
+
if (
|
|
1367
|
+
!Number.isFinite(ship.currentHp) ||
|
|
1368
|
+
!Number.isFinite(ship.maxHp) ||
|
|
1369
|
+
ship.maxHp <= 0 ||
|
|
1370
|
+
ship.currentHp > ship.maxHp * 0.25
|
|
1371
|
+
) {
|
|
1372
|
+
return []
|
|
1373
|
+
}
|
|
1374
|
+
return [{
|
|
1375
|
+
fleet: fleetName,
|
|
1376
|
+
position: ship.position,
|
|
1377
|
+
instanceId: ship.instanceId,
|
|
1378
|
+
currentHp: ship.currentHp,
|
|
1379
|
+
maxHp: ship.maxHp,
|
|
1380
|
+
}]
|
|
1381
|
+
}),
|
|
1382
|
+
)
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function compactBattleFleet(fleet) {
|
|
1386
|
+
if (!Array.isArray(fleet)) return []
|
|
1387
|
+
return fleet.flatMap((ship) => {
|
|
1388
|
+
if (!ship || typeof ship !== 'object') return []
|
|
1389
|
+
const raw = ship.raw && typeof ship.raw === 'object' ? ship.raw : {}
|
|
1390
|
+
return [{
|
|
1391
|
+
id: nullableNumber(ship.id),
|
|
1392
|
+
owner: nullableNumber(ship.owner),
|
|
1393
|
+
position: nullableNumber(ship.pos),
|
|
1394
|
+
maxHp: nullableNumber(ship.maxHP),
|
|
1395
|
+
initialHp: nullableNumber(ship.initHP),
|
|
1396
|
+
currentHp: nullableNumber(ship.nowHP),
|
|
1397
|
+
lostHp: nullableNumber(ship.lostHP),
|
|
1398
|
+
damage: nullableNumber(ship.damage),
|
|
1399
|
+
items: Array.isArray(ship.items) ? [...ship.items] : [],
|
|
1400
|
+
useItem: ship.useItem == null ? null : ship.useItem,
|
|
1401
|
+
instanceId: Number.isInteger(raw.api_id) ? raw.api_id : null,
|
|
1402
|
+
masterId: Number.isInteger(raw.api_ship_id) ? raw.api_ship_id : null,
|
|
1403
|
+
}]
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function sortieStateName(value) {
|
|
1408
|
+
return ['in_port', 'navigation', 'battle', 'practice'][value] || 'unknown'
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function finiteNumber(value, fallback) {
|
|
1412
|
+
return Number.isFinite(value) ? value : fallback
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function nullableNumber(value) {
|
|
1416
|
+
return Number.isFinite(value) ? value : null
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function stringValue(value) {
|
|
1420
|
+
return typeof value === 'string' ? value : ''
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function cleanupPortFile(portFile) {
|
|
1424
|
+
try {
|
|
1425
|
+
fs.unlinkSync(portFile)
|
|
1426
|
+
} catch (_) {}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function extractNames(store) {
|
|
1430
|
+
const result = { ships: {}, equipment: {}, missions: {} }
|
|
1431
|
+
const constants = store.const || {}
|
|
1432
|
+
|
|
1433
|
+
collectApiNames(constants.$ships, result.ships)
|
|
1434
|
+
collectApiNames(constants.$equips, result.equipment)
|
|
1435
|
+
collectApiNames(constants.$missions, result.missions)
|
|
1436
|
+
|
|
1437
|
+
const wctf = store.wctf || {}
|
|
1438
|
+
if (Object.keys(result.ships).length === 0) collectSimpleNames(wctf.ships, result.ships)
|
|
1439
|
+
if (Object.keys(result.equipment).length === 0) collectSimpleNames(wctf.items, result.equipment)
|
|
1440
|
+
|
|
1441
|
+
return result
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function collectApiNames(source, target) {
|
|
1445
|
+
if (!source || typeof source !== 'object') return
|
|
1446
|
+
|
|
1447
|
+
for (const [id, value] of Object.entries(source)) {
|
|
1448
|
+
if (value && value.api_name) target[id] = value.api_name
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function collectItemBonuses(wctf) {
|
|
1453
|
+
if (!wctf || typeof wctf !== 'object') return undefined
|
|
1454
|
+
const items = wctf.items
|
|
1455
|
+
if (!items || typeof items !== 'object') return undefined
|
|
1456
|
+
const byMasterId = {}
|
|
1457
|
+
for (const [id, item] of Object.entries(items)) {
|
|
1458
|
+
if (!item || typeof item !== 'object' || !Array.isArray(item.bonus)) continue
|
|
1459
|
+
byMasterId[id] = item.bonus
|
|
1460
|
+
}
|
|
1461
|
+
return Object.keys(byMasterId).length > 0
|
|
1462
|
+
? {
|
|
1463
|
+
available: true,
|
|
1464
|
+
source: 'wctf',
|
|
1465
|
+
version: wctf.version || null,
|
|
1466
|
+
lastModified: wctf.lastModified || null,
|
|
1467
|
+
byMasterId,
|
|
1468
|
+
}
|
|
1469
|
+
: undefined
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function collectSimpleNames(source, target) {
|
|
1473
|
+
if (!source || typeof source !== 'object') return
|
|
1474
|
+
|
|
1475
|
+
for (const [id, value] of Object.entries(source)) {
|
|
1476
|
+
if (value && value.name) target[id] = value.name
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function extractMasterData(store, masterFile = DEFAULT_MASTER_FILE) {
|
|
1481
|
+
const constants = store.const || {}
|
|
1482
|
+
|
|
1483
|
+
return {
|
|
1484
|
+
ships: constants.$ships || {},
|
|
1485
|
+
equipment: constants.$equips || {},
|
|
1486
|
+
shipTypes: constants.$shipTypes || {},
|
|
1487
|
+
equipmentTypes:
|
|
1488
|
+
constants.$equipTypes ||
|
|
1489
|
+
constants.$equipmentTypes ||
|
|
1490
|
+
constants.$slotitemTypes ||
|
|
1491
|
+
constants.$slotItemTypes ||
|
|
1492
|
+
{},
|
|
1493
|
+
missions: constants.$missions || {},
|
|
1494
|
+
equipmentRules: readEquipmentRules(masterFile),
|
|
1495
|
+
itemBonuses: collectItemBonuses(store.wctf),
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function readEquipmentRules(masterFile) {
|
|
1500
|
+
try {
|
|
1501
|
+
const stat = fs.statSync(masterFile)
|
|
1502
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MASTER_FILE_LIMIT) {
|
|
1503
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
1504
|
+
}
|
|
1505
|
+
const master = JSON.parse(fs.readFileSync(masterFile, 'utf8'))
|
|
1506
|
+
const equipmentShip = objectOrEmpty(master.api_mst_equip_ship)
|
|
1507
|
+
const equipmentExslotTypes = Array.isArray(master.api_mst_equip_exslot)
|
|
1508
|
+
? master.api_mst_equip_exslot.filter(Number.isInteger)
|
|
1509
|
+
: []
|
|
1510
|
+
const equipmentExslotShip = objectOrEmpty(
|
|
1511
|
+
master.api_mst_equip_exslot_ship,
|
|
1512
|
+
)
|
|
1513
|
+
const equipmentLimitExslot = objectOrEmpty(
|
|
1514
|
+
master.api_mst_equip_limit_exslot,
|
|
1515
|
+
)
|
|
1516
|
+
if (
|
|
1517
|
+
Object.keys(equipmentShip).length === 0 ||
|
|
1518
|
+
equipmentExslotTypes.length === 0
|
|
1519
|
+
) {
|
|
1520
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
available: true,
|
|
1524
|
+
source: 'navy-album-master-cache',
|
|
1525
|
+
equipmentShip,
|
|
1526
|
+
equipmentExslotTypes,
|
|
1527
|
+
equipmentExslotShip,
|
|
1528
|
+
equipmentLimitExslot,
|
|
1529
|
+
}
|
|
1530
|
+
} catch (_) {
|
|
1531
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function objectOrEmpty(value) {
|
|
1536
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
1537
|
+
? value
|
|
1538
|
+
: {}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function extractEventData(store) {
|
|
1542
|
+
const tags = extractShipTags(store)
|
|
1543
|
+
const ships = {}
|
|
1544
|
+
|
|
1545
|
+
for (const ship of Object.values((store.info && store.info.ships) || {})) {
|
|
1546
|
+
if (!ship || typeof ship !== 'object') continue
|
|
1547
|
+
const area = ship.api_sally_area || 0
|
|
1548
|
+
const tag = tags[area - 1] || emptyTag(area)
|
|
1549
|
+
|
|
1550
|
+
ships[ship.api_id] = {
|
|
1551
|
+
instanceId: ship.api_id,
|
|
1552
|
+
masterId: ship.api_ship_id,
|
|
1553
|
+
shipId: ship.api_id,
|
|
1554
|
+
modelId: ship.api_ship_id,
|
|
1555
|
+
area,
|
|
1556
|
+
mapName: tag.mapName,
|
|
1557
|
+
fleetName: tag.fleetName,
|
|
1558
|
+
color: tag.color,
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return {
|
|
1563
|
+
tags,
|
|
1564
|
+
ships,
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function extractPlannerData(store, plannerFile = DEFAULT_PLANNER_FILE) {
|
|
1569
|
+
const tags = extractShipTags(store)
|
|
1570
|
+
const current = readPlannerCurrent(plannerFile)
|
|
1571
|
+
const length = Math.max(tags.length, current.length)
|
|
1572
|
+
const areas = []
|
|
1573
|
+
const shipMap = {}
|
|
1574
|
+
|
|
1575
|
+
for (let index = 0; index < length; index += 1) {
|
|
1576
|
+
const area = index + 1
|
|
1577
|
+
const tag = tags[index] || emptyTag(area)
|
|
1578
|
+
const shipIds = Array.isArray(current[index]) ? current[index] : []
|
|
1579
|
+
|
|
1580
|
+
areas.push({
|
|
1581
|
+
area,
|
|
1582
|
+
mapName: tag.mapName,
|
|
1583
|
+
fleetName: tag.fleetName,
|
|
1584
|
+
color: tag.color,
|
|
1585
|
+
shipIds,
|
|
1586
|
+
})
|
|
1587
|
+
|
|
1588
|
+
for (const shipId of shipIds) {
|
|
1589
|
+
shipMap[shipId] = {
|
|
1590
|
+
area,
|
|
1591
|
+
mapName: tag.mapName,
|
|
1592
|
+
fleetName: tag.fleetName,
|
|
1593
|
+
color: tag.color,
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
return {
|
|
1599
|
+
areas,
|
|
1600
|
+
shipMap,
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function readPlannerCurrent(plannerFile) {
|
|
1605
|
+
try {
|
|
1606
|
+
const data = JSON.parse(fs.readFileSync(plannerFile, 'utf8'))
|
|
1607
|
+
if (Array.isArray(data.planner)) return data.planner
|
|
1608
|
+
if (data.planner && Array.isArray(data.planner.current)) {
|
|
1609
|
+
return data.planner.current
|
|
1610
|
+
}
|
|
1611
|
+
} catch (_) {}
|
|
1612
|
+
|
|
1613
|
+
return []
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function extractShipTags(store) {
|
|
1617
|
+
const shiptag = (store.fcd && store.fcd.shiptag) || {}
|
|
1618
|
+
const mapNames = Array.isArray(shiptag.mapname) ? shiptag.mapname : []
|
|
1619
|
+
const fleetNames = selectFleetNames(shiptag.fleetname)
|
|
1620
|
+
const colors = Array.isArray(shiptag.color) ? shiptag.color : []
|
|
1621
|
+
|
|
1622
|
+
return mapNames.map((mapName, index) => ({
|
|
1623
|
+
area: index + 1,
|
|
1624
|
+
mapName,
|
|
1625
|
+
fleetName: fleetNames[index] || mapName,
|
|
1626
|
+
color: colors[index] || '',
|
|
1627
|
+
}))
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function selectFleetNames(fleetname) {
|
|
1631
|
+
if (Array.isArray(fleetname)) return fleetname
|
|
1632
|
+
if (!fleetname || typeof fleetname !== 'object') return []
|
|
1633
|
+
|
|
1634
|
+
const language = getWindowLanguage()
|
|
1635
|
+
return (
|
|
1636
|
+
fleetname[language] ||
|
|
1637
|
+
fleetname['zh-CN'] ||
|
|
1638
|
+
fleetname['zh-TW'] ||
|
|
1639
|
+
fleetname.ja ||
|
|
1640
|
+
fleetname['ja-JP'] ||
|
|
1641
|
+
fleetname['en-US'] ||
|
|
1642
|
+
Object.values(fleetname).find(Array.isArray) ||
|
|
1643
|
+
[]
|
|
1644
|
+
)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function getWindowLanguage() {
|
|
1648
|
+
if (typeof window !== 'undefined' && window.language) {
|
|
1649
|
+
return window.language
|
|
1650
|
+
}
|
|
1651
|
+
return 'zh-CN'
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function emptyTag(area) {
|
|
1655
|
+
return {
|
|
1656
|
+
area,
|
|
1657
|
+
mapName: '',
|
|
1658
|
+
fleetName: '',
|
|
1659
|
+
color: '',
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
module.exports = {
|
|
1664
|
+
createPoiDataBridge,
|
|
1665
|
+
DEFAULT_PORT,
|
|
1666
|
+
DEFAULT_PORT_FILE,
|
|
1667
|
+
DEFAULT_PLANNER_FILE,
|
|
1668
|
+
INPUT_BODY_LIMIT,
|
|
1669
|
+
extractEventData,
|
|
1670
|
+
extractMasterData,
|
|
1671
|
+
extractPlannerData,
|
|
1672
|
+
extractNames,
|
|
1673
|
+
}
|