poi-plugin-mcp 0.2.28 → 0.2.29

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.
@@ -197,7 +197,9 @@ function createPixiHitLedger(options = {}) {
197
197
  ? { x: operation.x, y: operation.y }
198
198
  : operation.operation === 'drag'
199
199
  ? { x: operation.fromX, y: operation.fromY }
200
- : null
200
+ : operation.operation === 'scroll'
201
+ ? { x: operation.x, y: operation.y }
202
+ : null
201
203
  if (point === null) return Object.freeze({ available: false, reason: 'not_pointer' })
202
204
  const record = {
203
205
  at: capturedAt,
@@ -16,16 +16,21 @@ const EXACT_PATHS = new Set([
16
16
  '/kcsapi/api_start2/getData',
17
17
  '/kcsapi/api_port/port',
18
18
  '/kcsapi/api_get_member/base_air_corps',
19
+ '/kcsapi/api_get_member/chart_additional_info',
19
20
  '/kcsapi/api_get_member/deck',
21
+ '/kcsapi/api_get_member/furniture',
20
22
  '/kcsapi/api_get_member/kdock',
21
23
  '/kcsapi/api_get_member/mapinfo',
22
24
  '/kcsapi/api_get_member/material',
25
+ '/kcsapi/api_get_member/mission',
23
26
  '/kcsapi/api_get_member/ndock',
24
27
  '/kcsapi/api_get_member/practice',
28
+ '/kcsapi/api_get_member/preset_deck',
25
29
  '/kcsapi/api_get_member/questlist',
26
30
  '/kcsapi/api_get_member/require_info',
27
31
  '/kcsapi/api_get_member/ship2',
28
32
  '/kcsapi/api_get_member/ship3',
33
+ '/kcsapi/api_get_member/ship_deck',
29
34
  '/kcsapi/api_get_member/slot_item',
30
35
  '/kcsapi/api_get_member/unsetslot',
31
36
  '/kcsapi/api_get_member/useitem',
@@ -39,11 +44,13 @@ const EXACT_PATHS = new Set([
39
44
  '/kcsapi/api_req_hensei/combined',
40
45
  '/kcsapi/api_req_hensei/preset_select',
41
46
  '/kcsapi/api_req_hokyu/charge',
47
+ '/kcsapi/api_req_kaisou/can_preset_slot_select',
42
48
  '/kcsapi/api_req_kaisou/powerup',
43
49
  '/kcsapi/api_req_kaisou/slot_select',
44
50
  '/kcsapi/api_req_kaisou/slot_deprive',
45
51
  '/kcsapi/api_req_kaisou/slotset',
46
52
  '/kcsapi/api_req_kaisou/slotset_ex',
53
+ '/kcsapi/api_req_kaisou/unsetslot',
47
54
  '/kcsapi/api_req_kaisou/unsetslot_all',
48
55
  '/kcsapi/api_req_kousyou/createitem',
49
56
  '/kcsapi/api_req_kousyou/createship',
@@ -57,6 +64,7 @@ const EXACT_PATHS = new Set([
57
64
  '/kcsapi/api_req_map/start',
58
65
  '/kcsapi/api_req_map/start_air_base',
59
66
  '/kcsapi/api_req_member/get_practice_enemyinfo',
67
+ '/kcsapi/api_req_member/set_oss_condition',
60
68
  '/kcsapi/api_req_mission/result',
61
69
  '/kcsapi/api_req_mission/start',
62
70
  '/kcsapi/api_req_nyukyo/speedchange',
@@ -76,6 +84,7 @@ const ALLOWED_PREFIXES = [
76
84
  ]
77
85
 
78
86
  const SHARED_TELEMETRY_TIMESTAMP_PATHS = new Set([
87
+ '/kcsapi/api_get_member/mission',
79
88
  '/kcsapi/api_get_member/questlist',
80
89
  '/kcsapi/api_get_member/unsetslot',
81
90
  '/kcsapi/api_req_quest/start',
@@ -290,7 +290,9 @@ function createPoiDataBridge(options = {}) {
290
290
  let hit
291
291
  if (
292
292
  debugEvalEnabled === true &&
293
- (operation.operation === 'click' || operation.operation === 'drag')
293
+ (operation.operation === 'click' ||
294
+ operation.operation === 'drag' ||
295
+ operation.operation === 'scroll')
294
296
  ) {
295
297
  hit = await currentHitLedger().capture(operation, {
296
298
  leaseId: claim.leaseId,
@@ -832,6 +834,33 @@ function createPoiDataBridge(options = {}) {
832
834
  return
833
835
  }
834
836
 
837
+ if (endpoint === '/api-responses') {
838
+ if (req.method !== 'GET') {
839
+ drainRequest(req)
840
+ sendJson(res, 405, {
841
+ error: 'API responses endpoint only accepts GET requests.',
842
+ })
843
+ return
844
+ }
845
+ const pathFilter = requestUrl.searchParams.get('path')
846
+ sendJson(res, 200, getApiResponses({
847
+ after: clampedQueryInteger(
848
+ requestUrl.searchParams.get('after'),
849
+ 0,
850
+ 0,
851
+ Number.MAX_SAFE_INTEGER,
852
+ ),
853
+ limit: clampedQueryInteger(
854
+ requestUrl.searchParams.get('limit'),
855
+ DEFAULT_ACTION_EVENT_LIMIT,
856
+ 1,
857
+ MAX_ACTION_EVENT_LIMIT,
858
+ ),
859
+ ...(pathFilter === null ? {} : { path: pathFilter }),
860
+ }))
861
+ return
862
+ }
863
+
835
864
  const store = readStore()
836
865
  const info = store.info
837
866
 
package/lib/poi-input.js CHANGED
@@ -2,7 +2,19 @@ const CANONICAL_WIDTH = 1200
2
2
  const CANONICAL_HEIGHT = 720
3
3
  const DRAG_MOVE_STEPS = 6
4
4
  const DRAG_RELEASE_DELAY_MS = 120
5
+ const HOVER_MOVE_STEPS = 10
6
+ const HOVER_MOVE_STEP_DELAY_MS = 45
7
+ const HOVER_ENTRY_OFFSET_X = 44
5
8
  const MAX_TEXT_LENGTH = 256
9
+ // One wheel detent (a physical mouse-wheel notch) is WHEEL_DELTA = 120 in
10
+ // Win32 terms; Chromium reports ~100-120 px of deltaY per notch on Windows
11
+ // default scroll settings. 120 per event is the Puppeteer/Playwright
12
+ // convention for "one notch" and matches what a real wheel delivers here.
13
+ const SCROLL_NOTCH_DELTA = 120
14
+ const DEFAULT_SCROLL_INTERVAL_MS = 40
15
+ const MIN_SCROLL_INTERVAL_MS = 20
16
+ const MAX_SCROLL_INTERVAL_MS = 200
17
+ const MAX_SCROLL_NOTCHES = 50
6
18
 
7
19
  const SUPPORTED_BUTTONS = new Set(['left', 'middle', 'right'])
8
20
  const SUPPORTED_KEY_EVENTS = new Set(['keyDown', 'keyUp'])
@@ -44,7 +56,7 @@ function createPoiInputProvider(options = {}) {
44
56
  switch (operation.operation) {
45
57
  case 'move':
46
58
  validateMove(operation)
47
- sendMove(layout, operation)
59
+ await sendMove(layout, operation, delay)
48
60
  return 'move'
49
61
  case 'click':
50
62
  validateClick(operation)
@@ -54,6 +66,10 @@ function createPoiInputProvider(options = {}) {
54
66
  validateDrag(operation)
55
67
  await sendDrag(layout, operation, delay)
56
68
  return 'drag'
69
+ case 'scroll':
70
+ validateScroll(operation)
71
+ await sendScroll(layout, operation, delay)
72
+ return 'scroll'
57
73
  case 'key':
58
74
  validateKey(operation)
59
75
  await layout.webContents.sendInputEvent({
@@ -164,15 +180,37 @@ function validateClick(operation) {
164
180
  }
165
181
 
166
182
  function validateMove(operation) {
167
- assertExactFields(operation, ['operation', 'x', 'y'])
168
- if (!Number.isFinite(operation.x) || !Number.isFinite(operation.y)) {
183
+ const hasOriginX = operation.fromX !== undefined
184
+ const hasOriginY = operation.fromY !== undefined
185
+ if (hasOriginX !== hasOriginY) {
186
+ throw new Error('Move origin coordinates must be provided together')
187
+ }
188
+ assertExactFields(
189
+ operation,
190
+ hasOriginX
191
+ ? ['operation', 'x', 'y', 'fromX', 'fromY']
192
+ : ['operation', 'x', 'y'],
193
+ )
194
+ const xCoordinates = hasOriginX
195
+ ? [operation.x, operation.fromX]
196
+ : [operation.x]
197
+ const yCoordinates = hasOriginY
198
+ ? [operation.y, operation.fromY]
199
+ : [operation.y]
200
+ if (
201
+ xCoordinates
202
+ .concat(yCoordinates)
203
+ .some((coordinate) => !Number.isFinite(coordinate))
204
+ ) {
169
205
  throw new Error('Move coordinates must be finite numbers')
170
206
  }
171
207
  if (
172
- operation.x < 0 ||
173
- operation.x >= CANONICAL_WIDTH ||
174
- operation.y < 0 ||
175
- operation.y >= CANONICAL_HEIGHT
208
+ xCoordinates.some(
209
+ (coordinate) => coordinate < 0 || coordinate >= CANONICAL_WIDTH,
210
+ ) ||
211
+ yCoordinates.some(
212
+ (coordinate) => coordinate < 0 || coordinate >= CANONICAL_HEIGHT,
213
+ )
176
214
  ) {
177
215
  throw new Error('Move coordinates must be within canonical bounds')
178
216
  }
@@ -220,6 +258,45 @@ function validateDrag(operation) {
220
258
  }
221
259
  }
222
260
 
261
+ function validateScroll(operation) {
262
+ const hasInterval = operation.intervalMs !== undefined
263
+ assertExactFields(
264
+ operation,
265
+ hasInterval
266
+ ? ['operation', 'x', 'y', 'notches', 'intervalMs']
267
+ : ['operation', 'x', 'y', 'notches'],
268
+ )
269
+ if (!Number.isFinite(operation.x) || !Number.isFinite(operation.y)) {
270
+ throw new Error('Scroll coordinates must be finite numbers')
271
+ }
272
+ if (
273
+ operation.x < 0 ||
274
+ operation.x >= CANONICAL_WIDTH ||
275
+ operation.y < 0 ||
276
+ operation.y >= CANONICAL_HEIGHT
277
+ ) {
278
+ throw new Error('Scroll coordinates must be within canonical bounds')
279
+ }
280
+ if (!Number.isInteger(operation.notches) || operation.notches === 0) {
281
+ throw new Error('Scroll notches must be a non-zero integer')
282
+ }
283
+ if (Math.abs(operation.notches) > MAX_SCROLL_NOTCHES) {
284
+ throw new Error(
285
+ `Scroll notches must be from -${MAX_SCROLL_NOTCHES} to ${MAX_SCROLL_NOTCHES}`,
286
+ )
287
+ }
288
+ if (
289
+ hasInterval &&
290
+ (!Number.isInteger(operation.intervalMs) ||
291
+ operation.intervalMs < MIN_SCROLL_INTERVAL_MS ||
292
+ operation.intervalMs > MAX_SCROLL_INTERVAL_MS)
293
+ ) {
294
+ throw new Error(
295
+ `Scroll intervalMs must be an integer from ${MIN_SCROLL_INTERVAL_MS} to ${MAX_SCROLL_INTERVAL_MS}`,
296
+ )
297
+ }
298
+ }
299
+
223
300
  function validateKey(operation) {
224
301
  assertExactFields(operation, ['operation', 'event', 'key'])
225
302
  if (!SUPPORTED_KEY_EVENTS.has(operation.event)) {
@@ -256,11 +333,47 @@ function assertExactFields(operation, allowedFields) {
256
333
  }
257
334
  }
258
335
 
259
- function sendMove(layout, operation) {
260
- sendPointerEnterAndMove(layout.webContents, {
336
+ async function sendMove(layout, operation, delay) {
337
+ const destination = {
261
338
  x: Math.floor((operation.x * layout.width) / CANONICAL_WIDTH),
262
339
  y: Math.floor((operation.y * layout.height) / CANONICAL_HEIGHT),
263
- })
340
+ }
341
+ // poi-plugin-kancolle-assistant's sendFocusedMouseMove (poi mode) feeds
342
+ // PIXI's interaction state machine a pointermove sequence arriving across
343
+ // frames; a single mouseEnter + mouseMove teleport never promotes hover
344
+ // (the 0921 equipment-filter calibration hit exactly that). Glide in from
345
+ // an explicit origin — or ~44 px left of the target — then park on it: a
346
+ // hover must persist, so no (0, 0) reset afterwards.
347
+ const originX =
348
+ operation.fromX !== undefined
349
+ ? operation.fromX
350
+ : Math.max(0, operation.x - HOVER_ENTRY_OFFSET_X)
351
+ const originY = operation.fromY !== undefined ? operation.fromY : operation.y
352
+ const start = {
353
+ x: Math.floor((originX * layout.width) / CANONICAL_WIDTH),
354
+ y: Math.floor((originY * layout.height) / CANONICAL_HEIGHT),
355
+ }
356
+ layout.webContents.sendInputEvent({ type: 'mouseEnter', x: start.x, y: start.y })
357
+ let previousX = start.x
358
+ let previousY = start.y
359
+ for (let step = 1; step <= HOVER_MOVE_STEPS; step += 1) {
360
+ await delay(HOVER_MOVE_STEP_DELAY_MS)
361
+ const x = Math.round(
362
+ start.x + ((destination.x - start.x) * step) / HOVER_MOVE_STEPS,
363
+ )
364
+ const y = Math.round(
365
+ start.y + ((destination.y - start.y) * step) / HOVER_MOVE_STEPS,
366
+ )
367
+ await layout.webContents.sendInputEvent({
368
+ type: 'mouseMove',
369
+ x,
370
+ y,
371
+ movementX: x - previousX,
372
+ movementY: y - previousY,
373
+ })
374
+ previousX = x
375
+ previousY = y
376
+ }
264
377
  }
265
378
 
266
379
  function sendClick(layout, operation) {
@@ -350,6 +463,35 @@ function sendPointerEnterAndMove(webContents, point) {
350
463
  webContents.sendInputEvent({ type: 'mouseMove', x: point.x, y: point.y })
351
464
  }
352
465
 
466
+ async function sendScroll(layout, operation, delay) {
467
+ const point = {
468
+ x: Math.floor((operation.x * layout.width) / CANONICAL_WIDTH),
469
+ y: Math.floor((operation.y * layout.height) / CANONICAL_HEIGHT),
470
+ }
471
+ const direction = operation.notches > 0 ? 1 : -1
472
+ const intervalMs =
473
+ operation.intervalMs === undefined
474
+ ? DEFAULT_SCROLL_INTERVAL_MS
475
+ : operation.intervalMs
476
+ // Wheel targets hit-test at the pointer, so park the cursor on the point
477
+ // first (the same enter+move the drag gesture uses); one mouseWheel event
478
+ // per notch keeps each detent a discrete event for handlers that quantize
479
+ // per event — a single notches*120 mega-delta can be clamped or swallowed.
480
+ sendPointerEnterAndMove(layout.webContents, point)
481
+ for (let index = 0; index < Math.abs(operation.notches); index += 1) {
482
+ if (index > 0) {
483
+ await delay(intervalMs)
484
+ }
485
+ await layout.webContents.sendInputEvent({
486
+ type: 'mouseWheel',
487
+ x: point.x,
488
+ y: point.y,
489
+ deltaX: 0,
490
+ deltaY: direction * SCROLL_NOTCH_DELTA,
491
+ })
492
+ }
493
+ }
494
+
353
495
  function defaultDelay(milliseconds) {
354
496
  return new Promise((resolve) => setTimeout(resolve, milliseconds))
355
497
  }
@@ -276,10 +276,17 @@ ${PIXI_ROOT_RESOLVER_SNIPPET}
276
276
  const maxDepth = boundedInteger(request.maxDepth, 6, 0, 12, 'maxDepth')
277
277
  const maxNodes = boundedInteger(request.maxNodes, 20_000, 1, 50_000, 'maxNodes')
278
278
  const maxMatches = boundedInteger(request.maxMatches, 16, 1, 256, 'maxMatches')
279
+ // Visibility pruning by default: a node that fails visible/renderable/
280
+ // worldAlpha also stops the descent into its subtree — a hidden parent
281
+ // leaves children with stale own flags (worldAlpha 1, on-screen transform),
282
+ // so per-node checks alone let dismissed-dialog ghosts leak into matches.
283
+ // includeHidden restores the raw walk for forensics only.
284
+ const includeHidden = request.includeHidden === true
279
285
  if (projection === 'pixi.interactive') {
280
286
  const capturedAt = timestamp(now())
281
287
  const expression = pixiInteractiveExpression({
282
288
  anyRequiredKeys,
289
+ includeHidden,
283
290
  maxDepth,
284
291
  maxMatches,
285
292
  maxNodes,
@@ -471,6 +478,7 @@ ${PIXI_ROOT_RESOLVER_SNIPPET}
471
478
 
472
479
  function pixiInteractiveExpression({
473
480
  anyRequiredKeys,
481
+ includeHidden,
474
482
  maxDepth,
475
483
  maxMatches,
476
484
  maxNodes,
@@ -488,6 +496,7 @@ function pixiInteractiveExpression({
488
496
  const maxDepth = ${maxDepth};
489
497
  const maxNodes = ${maxNodes};
490
498
  const maxMatches = ${maxMatches};
499
+ const includeHidden = ${includeHidden ? 'true' : 'false'};
491
500
  const isObject = (value) =>
492
501
  (typeof value === 'object' || typeof value === 'function') && value !== null;
493
502
  const ownDescriptor = (object, key) => {
@@ -732,6 +741,7 @@ ${PIXI_ROOT_RESOLVER_SNIPPET}
732
741
  let cursor = 0;
733
742
  let visited = 0;
734
743
  let inspectedChildren = 0;
744
+ let prunedSubtrees = 0;
735
745
  let depthLimited = false;
736
746
  let childScanLimited = false;
737
747
  let truncatedBy = null;
@@ -745,17 +755,25 @@ ${PIXI_ROOT_RESOLVER_SNIPPET}
745
755
  }
746
756
  seen.add(current.node);
747
757
  visited += 1;
758
+ const nodeShown = shown(current.node);
748
759
  const entryIndex = entries.length;
749
760
  entries.push({
750
761
  node: current.node,
751
762
  path: current.path,
752
763
  depth: current.depth,
753
764
  parent: current.parent,
754
- ownWorldBounds: shown(current.node)
765
+ ownWorldBounds: nodeShown
755
766
  ? transformedBounds(localBounds(current.node), worldTransform(current.node))
756
767
  : null,
757
768
  subtreeWorldBounds: null,
758
769
  });
770
+ if (!nodeShown && !includeHidden) {
771
+ // Hidden parent: children keep stale own flags (visible=true,
772
+ // worldAlpha=1, on-screen transform) because PIXI skips
773
+ // updateTransform for invisible subtrees — do not descend.
774
+ prunedSubtrees += 1;
775
+ continue;
776
+ }
759
777
 
760
778
  const children = ownData(current.node, 'children');
761
779
  if (!Array.isArray(children)) continue;
@@ -845,6 +863,7 @@ ${PIXI_ROOT_RESOLVER_SNIPPET}
845
863
  visited,
846
864
  exhausted: truncatedBy === null,
847
865
  truncatedBy,
866
+ prunedSubtrees,
848
867
  matches,
849
868
  };
850
869
  })()`
@@ -935,6 +954,10 @@ function finalizePixiInteractiveProjection(value, options) {
935
954
  ? value.truncatedBy
936
955
  : null
937
956
  const exhausted = available && value.exhausted === true && truncatedBy === null
957
+ const prunedSubtrees = available &&
958
+ Number.isInteger(value.prunedSubtrees) && value.prunedSubtrees >= 0
959
+ ? value.prunedSubtrees
960
+ : 0
938
961
  const snapshotDigest = sha256Token({
939
962
  kind: 'poi.webview.pixi.snapshot.v1',
940
963
  available,
@@ -957,6 +980,7 @@ function finalizePixiInteractiveProjection(value, options) {
957
980
  visited,
958
981
  exhausted,
959
982
  truncatedBy,
983
+ prunedSubtrees,
960
984
  matches: identifiedMatches,
961
985
  }
962
986
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
4
4
  "description": "Poi data, WebView capture, and opt-in authenticated input bridge for local KanColle tools.",
5
5
  "main": "index.js",
6
6
  "keywords": [