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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const crypto = require('node:crypto')
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CAPACITY = 128
|
|
4
|
+
const DEFAULT_TOTAL_BYTES = 32 * 1024 * 1024
|
|
5
|
+
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024
|
|
6
|
+
const SENSITIVE_KEY = /^(?:api_token|authorization|cookie|credentials?|loginData|password|secret|ticket)$/iu
|
|
7
|
+
|
|
8
|
+
function createPoiApiResponses(options = {}) {
|
|
9
|
+
const now = options.now || (() => new Date())
|
|
10
|
+
const capacity = boundedInteger(
|
|
11
|
+
options.capacity == null ? DEFAULT_CAPACITY : options.capacity,
|
|
12
|
+
1,
|
|
13
|
+
512,
|
|
14
|
+
'capacity',
|
|
15
|
+
)
|
|
16
|
+
const maxTotalBytes = boundedInteger(
|
|
17
|
+
options.maxTotalBytes == null ? DEFAULT_TOTAL_BYTES : options.maxTotalBytes,
|
|
18
|
+
1024,
|
|
19
|
+
128 * 1024 * 1024,
|
|
20
|
+
'maxTotalBytes',
|
|
21
|
+
)
|
|
22
|
+
const sessionId = String(options.sessionId || crypto.randomUUID())
|
|
23
|
+
const responses = []
|
|
24
|
+
let latestGeneration = 0
|
|
25
|
+
let totalBytes = 0
|
|
26
|
+
|
|
27
|
+
function capture(detail, capturedAt) {
|
|
28
|
+
if (
|
|
29
|
+
!detail ||
|
|
30
|
+
typeof detail !== 'object' ||
|
|
31
|
+
Array.isArray(detail) ||
|
|
32
|
+
typeof detail.path !== 'string' ||
|
|
33
|
+
!detail.path.startsWith('/kcsapi/')
|
|
34
|
+
) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
const body = sanitize(detail.body)
|
|
38
|
+
const postBody = sanitize(detail.postBody)
|
|
39
|
+
const apiResult = readApiResult(detail)
|
|
40
|
+
latestGeneration += 1
|
|
41
|
+
const base = {
|
|
42
|
+
generation: latestGeneration,
|
|
43
|
+
capturedAt: capturedAt == null ? timestamp(now()) : timestamp(capturedAt),
|
|
44
|
+
path: detail.path,
|
|
45
|
+
apiResult,
|
|
46
|
+
postBody: isObject(postBody) ? postBody : {},
|
|
47
|
+
}
|
|
48
|
+
let responseBody = body
|
|
49
|
+
let truncated = false
|
|
50
|
+
let bytes = encodedBytes({ ...base, responseBody })
|
|
51
|
+
if (bytes > MAX_RESPONSE_BYTES) {
|
|
52
|
+
responseBody = null
|
|
53
|
+
truncated = true
|
|
54
|
+
bytes = encodedBytes({ ...base, responseBody, truncated })
|
|
55
|
+
}
|
|
56
|
+
const entry = Object.freeze({
|
|
57
|
+
...base,
|
|
58
|
+
responseBody,
|
|
59
|
+
truncated,
|
|
60
|
+
bytes,
|
|
61
|
+
})
|
|
62
|
+
responses.push(entry)
|
|
63
|
+
totalBytes += bytes
|
|
64
|
+
while (responses.length > capacity || totalBytes > maxTotalBytes) {
|
|
65
|
+
const removed = responses.shift()
|
|
66
|
+
totalBytes -= removed.bytes
|
|
67
|
+
}
|
|
68
|
+
return entry
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function read(options = {}) {
|
|
72
|
+
const after = options.after == null
|
|
73
|
+
? 0
|
|
74
|
+
: boundedInteger(options.after, 0, Number.MAX_SAFE_INTEGER, 'after')
|
|
75
|
+
const limit = options.limit == null
|
|
76
|
+
? 64
|
|
77
|
+
: boundedInteger(options.limit, 1, 256, 'limit')
|
|
78
|
+
const apiPath = options.path == null ? null : String(options.path)
|
|
79
|
+
return {
|
|
80
|
+
available: true,
|
|
81
|
+
sessionId,
|
|
82
|
+
earliestGeneration: responses.length === 0 ? 0 : responses[0].generation,
|
|
83
|
+
latestGeneration,
|
|
84
|
+
retainedBytes: totalBytes,
|
|
85
|
+
responses: responses
|
|
86
|
+
.filter((response) =>
|
|
87
|
+
response.generation > after &&
|
|
88
|
+
(apiPath === null || response.path === apiPath))
|
|
89
|
+
.slice(0, limit)
|
|
90
|
+
.map(({ bytes, ...response }) => response),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.freeze({ capture, read })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sanitize(value, depth = 0, seen = new WeakSet()) {
|
|
98
|
+
if (
|
|
99
|
+
value === null ||
|
|
100
|
+
typeof value === 'boolean' ||
|
|
101
|
+
(typeof value === 'number' && Number.isFinite(value))
|
|
102
|
+
) {
|
|
103
|
+
return value
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === 'string') return value.slice(0, 512 * 1024)
|
|
106
|
+
if (typeof value !== 'object') return undefined
|
|
107
|
+
if (seen.has(value)) return '[Circular]'
|
|
108
|
+
if (depth >= 32) return '[MaxDepth]'
|
|
109
|
+
seen.add(value)
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return value.slice(0, 100_000).map((item) => sanitize(item, depth + 1, seen))
|
|
112
|
+
}
|
|
113
|
+
const output = {}
|
|
114
|
+
for (const [key, item] of Object.entries(value).slice(0, 100_000)) {
|
|
115
|
+
if (SENSITIVE_KEY.test(key)) {
|
|
116
|
+
output[key] = '[REDACTED]'
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const sanitized = sanitize(item, depth + 1, seen)
|
|
120
|
+
if (sanitized !== undefined) output[key] = sanitized
|
|
121
|
+
}
|
|
122
|
+
return output
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readApiResult(detail) {
|
|
126
|
+
const candidates = [
|
|
127
|
+
detail.apiResult,
|
|
128
|
+
detail.api_result,
|
|
129
|
+
detail.result,
|
|
130
|
+
detail.body && detail.body.api_result,
|
|
131
|
+
]
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
if (candidate == null || candidate === '') continue
|
|
134
|
+
const parsed = Number(candidate)
|
|
135
|
+
if (Number.isInteger(parsed)) return parsed
|
|
136
|
+
}
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function encodedBytes(value) {
|
|
141
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function boundedInteger(value, minimum, maximum, name) {
|
|
145
|
+
const parsed = Number(value)
|
|
146
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
147
|
+
throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`)
|
|
148
|
+
}
|
|
149
|
+
return parsed
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function timestamp(value) {
|
|
153
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
154
|
+
if (!Number.isFinite(date.getTime())) throw new Error('now must return a valid date')
|
|
155
|
+
return date.toISOString()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isObject(value) {
|
|
159
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
MAX_RESPONSE_BYTES,
|
|
164
|
+
createPoiApiResponses,
|
|
165
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
const { createPoiScreenshotProvider } = require('./poi-screenshot')
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ATTACH_INTERVAL_MS = 1000
|
|
4
|
+
const DEFAULT_IDLE_SESSION_MS = 5 * 60 * 1000
|
|
5
|
+
const MAX_BUFFERED_MOUSE_EVENTS = 256
|
|
6
|
+
const MAX_BUFFERED_RESPONSES = 128
|
|
7
|
+
|
|
8
|
+
function createPoiAutoInteractionRecorder(options = {}) {
|
|
9
|
+
const getStore = options.getStore || defaultGetStore
|
|
10
|
+
const loadElectronRemote = options.loadElectronRemote ||
|
|
11
|
+
(() => require('@electron/remote'))
|
|
12
|
+
const resolveWebContents = options.resolveWebContents || ((webContentsId) => {
|
|
13
|
+
const remote = loadElectronRemote()
|
|
14
|
+
return remote.webContents.fromId(webContentsId)
|
|
15
|
+
})
|
|
16
|
+
let captureScreenshot = options.captureScreenshot || null
|
|
17
|
+
const createSessionRecorder = options.createSessionRecorder
|
|
18
|
+
const eventTarget = options.eventTarget === undefined
|
|
19
|
+
? defaultEventTarget()
|
|
20
|
+
: options.eventTarget
|
|
21
|
+
const logger = options.logger || console
|
|
22
|
+
const now = options.now || (() => new Date())
|
|
23
|
+
const nowMs = options.nowMs || (() => Date.now())
|
|
24
|
+
const setIntervalFn = options.setInterval || setInterval
|
|
25
|
+
const clearIntervalFn = options.clearInterval || clearInterval
|
|
26
|
+
const setTimeoutFn = options.setTimeout || setTimeout
|
|
27
|
+
const clearTimeoutFn = options.clearTimeout || clearTimeout
|
|
28
|
+
const attachIntervalMs = positiveInteger(
|
|
29
|
+
options.attachIntervalMs,
|
|
30
|
+
DEFAULT_ATTACH_INTERVAL_MS,
|
|
31
|
+
'attachIntervalMs',
|
|
32
|
+
)
|
|
33
|
+
const idleSessionMs = positiveInteger(
|
|
34
|
+
options.idleSessionMs,
|
|
35
|
+
DEFAULT_IDLE_SESSION_MS,
|
|
36
|
+
'idleSessionMs',
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
let armed = false
|
|
40
|
+
let attachedWebContents = null
|
|
41
|
+
let attachTimer = null
|
|
42
|
+
let idleTimer = null
|
|
43
|
+
let sessionRecorder = null
|
|
44
|
+
let starting = false
|
|
45
|
+
let pending = Promise.resolve()
|
|
46
|
+
let lastSessionStatus = null
|
|
47
|
+
let limitReached = null
|
|
48
|
+
let closing = false
|
|
49
|
+
let startAfterClose = false
|
|
50
|
+
let bufferedGesture = false
|
|
51
|
+
let bufferedMouseEvents = []
|
|
52
|
+
let bufferedResponses = []
|
|
53
|
+
let sessionStopError = null
|
|
54
|
+
|
|
55
|
+
function enqueue(action) {
|
|
56
|
+
const result = pending.then(action, action)
|
|
57
|
+
pending = result.catch((error) => {
|
|
58
|
+
logger.error(`[poi-plugin-mcp] Automatic recorder transition failed: ${error.message}`)
|
|
59
|
+
})
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function currentWebContents() {
|
|
64
|
+
const layout = getStore('layout.webview')
|
|
65
|
+
if (!layout || !layout.ref) return null
|
|
66
|
+
if (typeof layout.ref.getWebContents === 'function') {
|
|
67
|
+
return layout.ref.getWebContents()
|
|
68
|
+
}
|
|
69
|
+
if (typeof layout.ref.getWebContentsId === 'function') {
|
|
70
|
+
const id = layout.ref.getWebContentsId()
|
|
71
|
+
if (Number.isInteger(id) && id > 0) return resolveWebContents(id)
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tryAttach() {
|
|
77
|
+
if (!armed) return false
|
|
78
|
+
let nextWebContents = null
|
|
79
|
+
try {
|
|
80
|
+
nextWebContents = currentWebContents()
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error(`[poi-plugin-mcp] Automatic recorder WebView lookup failed: ${error.message}`)
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
if (!nextWebContents || typeof nextWebContents.on !== 'function') {
|
|
86
|
+
detachWebContents()
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
if (nextWebContents === attachedWebContents) return true
|
|
90
|
+
detachWebContents()
|
|
91
|
+
attachedWebContents = nextWebContents
|
|
92
|
+
attachedWebContents.on('before-mouse-event', handleMouseEvent)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleMouseEvent(_event, mouse) {
|
|
97
|
+
if (
|
|
98
|
+
!armed ||
|
|
99
|
+
!mouse ||
|
|
100
|
+
typeof mouse !== 'object' ||
|
|
101
|
+
!['mouseMove', 'mouseDown', 'mouseUp'].includes(mouse.type) ||
|
|
102
|
+
!Number.isFinite(mouse.x) ||
|
|
103
|
+
!Number.isFinite(mouse.y)
|
|
104
|
+
) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (limitReached) return
|
|
108
|
+
if (closing) {
|
|
109
|
+
startAfterClose = true
|
|
110
|
+
bufferMouseEvent(mouse)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (!sessionRecorder && !starting) beginSession()
|
|
114
|
+
if (starting) bufferMouseEvent(mouse)
|
|
115
|
+
if (sessionRecorder) scheduleIdleStop()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleGameResponse(event) {
|
|
119
|
+
if (
|
|
120
|
+
!armed ||
|
|
121
|
+
limitReached ||
|
|
122
|
+
(!starting && !(closing && startAfterClose))
|
|
123
|
+
) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (bufferedResponses.length >= MAX_BUFFERED_RESPONSES) return
|
|
127
|
+
bufferedResponses.push({
|
|
128
|
+
detail: event && event.detail,
|
|
129
|
+
observedAt: now().toISOString(),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function bufferMouseEvent(mouse) {
|
|
134
|
+
if (mouse.type === 'mouseDown') bufferedGesture = true
|
|
135
|
+
if (!bufferedGesture && mouse.type !== 'mouseDown') return
|
|
136
|
+
if (bufferedMouseEvents.length >= MAX_BUFFERED_MOUSE_EVENTS) return
|
|
137
|
+
const observation = {
|
|
138
|
+
mouse: { ...mouse },
|
|
139
|
+
observedAt: now().toISOString(),
|
|
140
|
+
observedAtMs: nowMs(),
|
|
141
|
+
screenshot: null,
|
|
142
|
+
}
|
|
143
|
+
if (mouse.type === 'mouseDown') {
|
|
144
|
+
try {
|
|
145
|
+
if (!captureScreenshot) {
|
|
146
|
+
captureScreenshot = createPoiScreenshotProvider({ getStore })
|
|
147
|
+
}
|
|
148
|
+
observation.screenshot = Promise.resolve(captureScreenshot())
|
|
149
|
+
} catch (error) {
|
|
150
|
+
observation.screenshot = Promise.reject(error)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
bufferedMouseEvents.push(observation)
|
|
154
|
+
if (mouse.type === 'mouseUp') bufferedGesture = false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function beginSession() {
|
|
158
|
+
if (!armed || sessionRecorder || starting) return
|
|
159
|
+
starting = true
|
|
160
|
+
enqueue(async () => {
|
|
161
|
+
try {
|
|
162
|
+
if (!armed) return
|
|
163
|
+
const nextRecorder = createSessionRecorder()
|
|
164
|
+
sessionRecorder = nextRecorder
|
|
165
|
+
await nextRecorder.start()
|
|
166
|
+
lastSessionStatus = nextRecorder.getStatus()
|
|
167
|
+
limitReached = lastSessionStatus.limitReached || null
|
|
168
|
+
if (typeof nextRecorder.observeMouseEvent === 'function') {
|
|
169
|
+
for (const observation of bufferedMouseEvents) {
|
|
170
|
+
nextRecorder.observeMouseEvent(observation)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (typeof nextRecorder.observeGameResponse === 'function') {
|
|
174
|
+
for (const event of bufferedResponses) {
|
|
175
|
+
nextRecorder.observeGameResponse(event)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
bufferedMouseEvents = []
|
|
179
|
+
bufferedResponses = []
|
|
180
|
+
bufferedGesture = false
|
|
181
|
+
if (armed && lastSessionStatus.running) scheduleIdleStop()
|
|
182
|
+
} catch (error) {
|
|
183
|
+
sessionRecorder = null
|
|
184
|
+
bufferedMouseEvents = []
|
|
185
|
+
bufferedResponses = []
|
|
186
|
+
bufferedGesture = false
|
|
187
|
+
limitReached = {
|
|
188
|
+
reason: 'session-start-failed',
|
|
189
|
+
message: error.message,
|
|
190
|
+
}
|
|
191
|
+
throw error
|
|
192
|
+
} finally {
|
|
193
|
+
starting = false
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function scheduleIdleStop() {
|
|
199
|
+
if (idleTimer != null) clearTimeoutFn(idleTimer)
|
|
200
|
+
idleTimer = setTimeoutFn(() => {
|
|
201
|
+
idleTimer = null
|
|
202
|
+
endCurrentSession()
|
|
203
|
+
}, idleSessionMs)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function endCurrentSession() {
|
|
207
|
+
if (closing) return
|
|
208
|
+
closing = true
|
|
209
|
+
enqueue(async () => {
|
|
210
|
+
let stopFailed = false
|
|
211
|
+
try {
|
|
212
|
+
if (!sessionRecorder) return
|
|
213
|
+
const endingRecorder = sessionRecorder
|
|
214
|
+
try {
|
|
215
|
+
await endingRecorder.stop()
|
|
216
|
+
lastSessionStatus = endingRecorder.getStatus()
|
|
217
|
+
limitReached = lastSessionStatus.limitReached || limitReached
|
|
218
|
+
} catch (error) {
|
|
219
|
+
stopFailed = true
|
|
220
|
+
sessionStopError = error
|
|
221
|
+
try {
|
|
222
|
+
lastSessionStatus = endingRecorder.getStatus()
|
|
223
|
+
} catch (_statusError) {
|
|
224
|
+
// Preserve the original stop failure.
|
|
225
|
+
}
|
|
226
|
+
limitReached = {
|
|
227
|
+
reason: 'session-stop-failed',
|
|
228
|
+
message: error.message,
|
|
229
|
+
}
|
|
230
|
+
startAfterClose = false
|
|
231
|
+
bufferedMouseEvents = []
|
|
232
|
+
bufferedResponses = []
|
|
233
|
+
bufferedGesture = false
|
|
234
|
+
throw error
|
|
235
|
+
} finally {
|
|
236
|
+
if (sessionRecorder === endingRecorder) sessionRecorder = null
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
closing = false
|
|
240
|
+
if (armed && startAfterClose && !stopFailed && !limitReached) {
|
|
241
|
+
startAfterClose = false
|
|
242
|
+
beginSession()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function clearIdleTimer() {
|
|
249
|
+
if (idleTimer == null) return
|
|
250
|
+
clearTimeoutFn(idleTimer)
|
|
251
|
+
idleTimer = null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function detachWebContents() {
|
|
255
|
+
if (!attachedWebContents) return
|
|
256
|
+
if (typeof attachedWebContents.off === 'function') {
|
|
257
|
+
attachedWebContents.off('before-mouse-event', handleMouseEvent)
|
|
258
|
+
} else if (typeof attachedWebContents.removeListener === 'function') {
|
|
259
|
+
attachedWebContents.removeListener('before-mouse-event', handleMouseEvent)
|
|
260
|
+
}
|
|
261
|
+
attachedWebContents = null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function start() {
|
|
265
|
+
if (armed) return getStatus()
|
|
266
|
+
limitReached = null
|
|
267
|
+
sessionStopError = null
|
|
268
|
+
armed = true
|
|
269
|
+
try {
|
|
270
|
+
if (eventTarget && typeof eventTarget.addEventListener === 'function') {
|
|
271
|
+
eventTarget.addEventListener('game.response', handleGameResponse)
|
|
272
|
+
}
|
|
273
|
+
tryAttach()
|
|
274
|
+
attachTimer = setIntervalFn(tryAttach, attachIntervalMs)
|
|
275
|
+
return getStatus()
|
|
276
|
+
} catch (error) {
|
|
277
|
+
armed = false
|
|
278
|
+
if (attachTimer != null) {
|
|
279
|
+
clearIntervalFn(attachTimer)
|
|
280
|
+
attachTimer = null
|
|
281
|
+
}
|
|
282
|
+
if (eventTarget && typeof eventTarget.removeEventListener === 'function') {
|
|
283
|
+
eventTarget.removeEventListener('game.response', handleGameResponse)
|
|
284
|
+
}
|
|
285
|
+
detachWebContents()
|
|
286
|
+
throw error
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function stop() {
|
|
291
|
+
armed = false
|
|
292
|
+
startAfterClose = false
|
|
293
|
+
bufferedMouseEvents = []
|
|
294
|
+
bufferedResponses = []
|
|
295
|
+
bufferedGesture = false
|
|
296
|
+
clearIdleTimer()
|
|
297
|
+
if (attachTimer != null) {
|
|
298
|
+
clearIntervalFn(attachTimer)
|
|
299
|
+
attachTimer = null
|
|
300
|
+
}
|
|
301
|
+
if (eventTarget && typeof eventTarget.removeEventListener === 'function') {
|
|
302
|
+
eventTarget.removeEventListener('game.response', handleGameResponse)
|
|
303
|
+
}
|
|
304
|
+
detachWebContents()
|
|
305
|
+
let stopError = null
|
|
306
|
+
try {
|
|
307
|
+
await pending
|
|
308
|
+
if (sessionRecorder) {
|
|
309
|
+
const endingRecorder = sessionRecorder
|
|
310
|
+
try {
|
|
311
|
+
await endingRecorder.stop()
|
|
312
|
+
} catch (error) {
|
|
313
|
+
stopError = error
|
|
314
|
+
} finally {
|
|
315
|
+
try {
|
|
316
|
+
lastSessionStatus = endingRecorder.getStatus()
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (!stopError) stopError = error
|
|
319
|
+
}
|
|
320
|
+
if (sessionRecorder === endingRecorder) sessionRecorder = null
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!stopError && sessionStopError) stopError = sessionStopError
|
|
324
|
+
} finally {
|
|
325
|
+
detachWebContents()
|
|
326
|
+
}
|
|
327
|
+
if (stopError) throw stopError
|
|
328
|
+
return getStatus()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getStatus() {
|
|
332
|
+
const sessionStatus = sessionRecorder
|
|
333
|
+
? sessionRecorder.getStatus()
|
|
334
|
+
: lastSessionStatus || {}
|
|
335
|
+
return {
|
|
336
|
+
armed,
|
|
337
|
+
starting,
|
|
338
|
+
running: sessionStatus.running === true,
|
|
339
|
+
acceptingEvents: sessionStatus.acceptingEvents === true,
|
|
340
|
+
attached: armed && (
|
|
341
|
+
sessionStatus.attached === true ||
|
|
342
|
+
attachedWebContents !== null
|
|
343
|
+
),
|
|
344
|
+
sessionId: sessionStatus.sessionId || null,
|
|
345
|
+
sessionDir: sessionStatus.sessionDir || null,
|
|
346
|
+
sessionBytes: Number.isFinite(sessionStatus.sessionBytes)
|
|
347
|
+
? sessionStatus.sessionBytes
|
|
348
|
+
: 0,
|
|
349
|
+
limitReached: sessionStatus.limitReached || limitReached,
|
|
350
|
+
idleSessionMs,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function flush() {
|
|
355
|
+
while (true) {
|
|
356
|
+
const currentPending = pending
|
|
357
|
+
await currentPending
|
|
358
|
+
if (currentPending === pending) break
|
|
359
|
+
}
|
|
360
|
+
if (sessionRecorder && typeof sessionRecorder.flush === 'function') {
|
|
361
|
+
await sessionRecorder.flush()
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (typeof createSessionRecorder !== 'function') {
|
|
366
|
+
throw new TypeError('createSessionRecorder must be a function')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
flush,
|
|
371
|
+
getStatus,
|
|
372
|
+
start,
|
|
373
|
+
stop,
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function positiveInteger(value, fallback, field) {
|
|
378
|
+
const next = value === undefined ? fallback : value
|
|
379
|
+
if (!Number.isSafeInteger(next) || next <= 0) {
|
|
380
|
+
throw new TypeError(`${field} must be a positive integer`)
|
|
381
|
+
}
|
|
382
|
+
return next
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function defaultEventTarget() {
|
|
386
|
+
return typeof window !== 'undefined' ? window : null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function defaultGetStore(path) {
|
|
390
|
+
if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
|
|
391
|
+
return window.getStore(path)
|
|
392
|
+
}
|
|
393
|
+
return undefined
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
DEFAULT_IDLE_SESSION_MS,
|
|
398
|
+
createPoiAutoInteractionRecorder,
|
|
399
|
+
}
|