@jsbot-tellus/core 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/bot.config.example.json +3 -0
- package/dist/app.js +215 -98
- package/dist/app.js.map +1 -1
- package/dist/core/client.d.ts +1 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +3 -0
- package/dist/core/client.js.map +1 -1
- package/dist/core/config/schema.d.ts +19 -0
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +37 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/dispatch/router.d.ts +3 -0
- package/dist/core/dispatch/router.d.ts.map +1 -1
- package/dist/core/dispatch/router.js +13 -0
- package/dist/core/dispatch/router.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/kernel.d.ts +8 -0
- package/dist/core/kernel.d.ts.map +1 -1
- package/dist/core/kernel.js +17 -0
- package/dist/core/kernel.js.map +1 -1
- package/dist/core/logging/log-buffer.d.ts +27 -0
- package/dist/core/logging/log-buffer.d.ts.map +1 -0
- package/dist/core/logging/log-buffer.js +71 -0
- package/dist/core/logging/log-buffer.js.map +1 -0
- package/dist/core/logging/logger.d.ts +3 -0
- package/dist/core/logging/logger.d.ts.map +1 -1
- package/dist/core/logging/logger.js +6 -3
- package/dist/core/logging/logger.js.map +1 -1
- package/dist/core/plugin/command-params.d.ts +2 -2
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/command-params.js +35 -0
- package/dist/core/plugin/command-params.js.map +1 -1
- package/dist/core/storage/session.d.ts +15 -0
- package/dist/core/storage/session.d.ts.map +1 -1
- package/dist/core/storage/session.js +55 -0
- package/dist/core/storage/session.js.map +1 -1
- package/dist/core/web/api.d.ts +16 -0
- package/dist/core/web/api.d.ts.map +1 -0
- package/dist/core/web/api.js +617 -0
- package/dist/core/web/api.js.map +1 -0
- package/dist/core/web/auth.d.ts +43 -0
- package/dist/core/web/auth.d.ts.map +1 -0
- package/dist/core/web/auth.js +161 -0
- package/dist/core/web/auth.js.map +1 -0
- package/dist/core/web/config-store.d.ts +30 -0
- package/dist/core/web/config-store.d.ts.map +1 -0
- package/dist/core/web/config-store.js +216 -0
- package/dist/core/web/config-store.js.map +1 -0
- package/dist/core/web/event-feed.d.ts +26 -0
- package/dist/core/web/event-feed.d.ts.map +1 -0
- package/dist/core/web/event-feed.js +107 -0
- package/dist/core/web/event-feed.js.map +1 -0
- package/dist/core/web/http.d.ts +39 -0
- package/dist/core/web/http.d.ts.map +1 -0
- package/dist/core/web/http.js +233 -0
- package/dist/core/web/http.js.map +1 -0
- package/dist/core/web/index.d.ts +11 -0
- package/dist/core/web/index.d.ts.map +1 -0
- package/dist/core/web/index.js +6 -0
- package/dist/core/web/index.js.map +1 -0
- package/dist/core/web/server.d.ts +30 -0
- package/dist/core/web/server.d.ts.map +1 -0
- package/dist/core/web/server.js +437 -0
- package/dist/core/web/server.js.map +1 -0
- package/dist/core/web/types.d.ts +84 -0
- package/dist/core/web/types.d.ts.map +1 -0
- package/dist/core/web/types.js +2 -0
- package/dist/core/web/types.js.map +1 -0
- package/dist/webui/assets/ActionsView-BDuXBWG1.js +1 -0
- package/dist/webui/assets/ActionsView-Ciib5lC6.css +1 -0
- package/dist/webui/assets/ConfigView-B3Gv2uRG.js +1 -0
- package/dist/webui/assets/ConfigView-CrEpss2y.css +1 -0
- package/dist/webui/assets/DashboardView-DR6yCaqs.css +1 -0
- package/dist/webui/assets/DashboardView-n9LGPmQL.js +1 -0
- package/dist/webui/assets/EventsView-DhO8twWb.js +1 -0
- package/dist/webui/assets/EventsView-PMkYKxZ-.css +1 -0
- package/dist/webui/assets/KvView-BCz_dOVC.js +1 -0
- package/dist/webui/assets/KvView-CaMvbxUz.css +1 -0
- package/dist/webui/assets/LoginView-BVnFkwSW.css +1 -0
- package/dist/webui/assets/LoginView-Br3_AC5z.js +1 -0
- package/dist/webui/assets/LogsView-Ba_12frI.js +1 -0
- package/dist/webui/assets/LogsView-NR4L1-XY.css +1 -0
- package/dist/webui/assets/MainLayout-D2LTc57u.js +1 -0
- package/dist/webui/assets/MainLayout-glfVNbMz.css +1 -0
- package/dist/webui/assets/PluginsView-DL9OxGph.css +1 -0
- package/dist/webui/assets/PluginsView-vQKBSCU8.js +1 -0
- package/dist/webui/assets/SessionsView-DSl37qpo.js +1 -0
- package/dist/webui/assets/SessionsView-p-u3JGTV.css +1 -0
- package/dist/webui/assets/api-CNzxhTWe.js +3 -0
- package/dist/webui/assets/index-CUvpnClx.js +50 -0
- package/dist/webui/assets/index-CePlRZX6.css +1 -0
- package/dist/webui/index.html +110 -0
- package/docs/plugin-api.md +1 -0
- package/docs/webui.md +142 -0
- package/package.json +14 -3
- package/src/app.ts +286 -118
- package/src/core/client.ts +4 -0
- package/src/core/config/schema.ts +40 -0
- package/src/core/dispatch/router.ts +15 -0
- package/src/core/index.ts +3 -0
- package/src/core/kernel.ts +25 -1
- package/src/core/logging/log-buffer.ts +109 -0
- package/src/core/logging/logger.ts +7 -3
- package/src/core/plugin/command-params.ts +48 -2
- package/src/core/storage/session.ts +95 -0
- package/src/core/web/api.ts +932 -0
- package/src/core/web/auth.ts +245 -0
- package/src/core/web/config-store.ts +333 -0
- package/src/core/web/event-feed.ts +163 -0
- package/src/core/web/http.ts +340 -0
- package/src/core/web/index.ts +25 -0
- package/src/core/web/server.ts +621 -0
- package/src/core/web/types.ts +100 -0
package/src/app.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import type { SnowLumaEvent } from '@snowluma/sdk'
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
5
|
import { parseArgs } from 'node:util'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
4
7
|
import { createBotClient } from './core/client.js'
|
|
5
8
|
import type { AppConfig } from './core/config/schema.js'
|
|
6
9
|
import {
|
|
@@ -10,15 +13,25 @@ import {
|
|
|
10
13
|
resolveDataDir,
|
|
11
14
|
} from './core/config/loader.js'
|
|
12
15
|
import { Kernel } from './core/kernel.js'
|
|
16
|
+
import { ConfigStore } from './core/web/config-store.js'
|
|
17
|
+
import { EventFeed } from './core/web/event-feed.js'
|
|
18
|
+
import { LogBuffer } from './core/logging/log-buffer.js'
|
|
13
19
|
import {
|
|
14
20
|
createLogger,
|
|
21
|
+
defaultLogSink,
|
|
15
22
|
parseLogLevel,
|
|
23
|
+
type LogSink,
|
|
16
24
|
type Logger,
|
|
17
25
|
} from './core/logging/logger.js'
|
|
18
26
|
import { NpmPluginInstaller } from './core/plugin/installer.js'
|
|
19
27
|
import { loadLocalPlugins } from './core/plugin/loader.js'
|
|
20
|
-
import type {
|
|
21
|
-
|
|
28
|
+
import type {
|
|
29
|
+
AnyPluginDefinition,
|
|
30
|
+
PluginInstaller,
|
|
31
|
+
} from './core/plugin/types.js'
|
|
32
|
+
import { openDatabase, type Database } from './core/storage/database.js'
|
|
33
|
+
import { WebUiServer } from './core/web/server.js'
|
|
34
|
+
import type { WebUiAddress, WebUiDeps } from './core/web/types.js'
|
|
22
35
|
import { builtinPlugins } from './plugins/index.js'
|
|
23
36
|
|
|
24
37
|
const LOCAL_PLUGIN_DIR = 'plugins'
|
|
@@ -89,6 +102,13 @@ interface RuntimeSpec {
|
|
|
89
102
|
interface Runtime {
|
|
90
103
|
spec: RuntimeSpec
|
|
91
104
|
kernel: Kernel
|
|
105
|
+
database: Database
|
|
106
|
+
installer: PluginInstaller
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface StaticBinding {
|
|
110
|
+
host: string
|
|
111
|
+
port: number
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
async function loadRuntimeSpec(
|
|
@@ -130,7 +150,8 @@ async function loadRuntimeSpec(
|
|
|
130
150
|
function createRuntime(
|
|
131
151
|
spec: RuntimeSpec,
|
|
132
152
|
settings: RuntimeSettings,
|
|
133
|
-
onRestart: () => void
|
|
153
|
+
onRestart: () => void,
|
|
154
|
+
onEvent: (event: SnowLumaEvent) => void
|
|
134
155
|
): Runtime {
|
|
135
156
|
const { logger } = settings
|
|
136
157
|
const installer = new NpmPluginInstaller({
|
|
@@ -158,9 +179,51 @@ function createRuntime(
|
|
|
158
179
|
plugins: spec.plugins,
|
|
159
180
|
installer,
|
|
160
181
|
onRestart,
|
|
182
|
+
onEvent,
|
|
161
183
|
})
|
|
162
184
|
|
|
163
|
-
return { spec, kernel }
|
|
185
|
+
return { spec, kernel, database, installer }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function describeError(error: unknown): string {
|
|
189
|
+
if (error instanceof Error) {
|
|
190
|
+
return error.stack ?? `${error.name}: ${error.message}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return String(error)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function credentialSignature(config: AppConfig): string {
|
|
197
|
+
const webui = config.webui
|
|
198
|
+
|
|
199
|
+
return [webui.username ?? '', webui.password ?? ''].join(':')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveStaticDir(): string | null {
|
|
203
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
204
|
+
const candidates = [
|
|
205
|
+
join(scriptDir, 'webui'),
|
|
206
|
+
resolve(process.cwd(), 'dist/webui'),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
for (const candidate of candidates) {
|
|
210
|
+
if (existsSync(join(candidate, 'index.html'))) return candidate
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readVersion(): string {
|
|
217
|
+
try {
|
|
218
|
+
const pkgPath = new URL('../package.json', import.meta.url)
|
|
219
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
|
|
220
|
+
version?: string
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return pkg.version ?? 'unknown'
|
|
224
|
+
} catch {
|
|
225
|
+
return 'unknown'
|
|
226
|
+
}
|
|
164
227
|
}
|
|
165
228
|
|
|
166
229
|
async function main(): Promise<void> {
|
|
@@ -179,11 +242,22 @@ async function main(): Promise<void> {
|
|
|
179
242
|
return
|
|
180
243
|
}
|
|
181
244
|
|
|
245
|
+
const logs = new LogBuffer()
|
|
246
|
+
const events = new EventFeed()
|
|
247
|
+
const tapEvents = (event: SnowLumaEvent): void => {
|
|
248
|
+
events.add(event)
|
|
249
|
+
}
|
|
250
|
+
const sink: LogSink = (record) => {
|
|
251
|
+
defaultLogSink(record)
|
|
252
|
+
logs.push(record)
|
|
253
|
+
}
|
|
254
|
+
|
|
182
255
|
const logger = createLogger({
|
|
183
256
|
level: parseLogLevel(
|
|
184
257
|
pick(values['log-level']) ?? process.env.BOT_LOG_LEVEL
|
|
185
258
|
),
|
|
186
259
|
scope: 'app',
|
|
260
|
+
sink,
|
|
187
261
|
})
|
|
188
262
|
|
|
189
263
|
const configPath = resolveConfigPath({
|
|
@@ -202,48 +276,22 @@ async function main(): Promise<void> {
|
|
|
202
276
|
dataDir,
|
|
203
277
|
}
|
|
204
278
|
|
|
279
|
+
// Config file errors remain fatal: there is nothing the WebUI can restore
|
|
280
|
+
// from when the config cannot even be parsed.
|
|
281
|
+
const initialSpec = await loadRuntimeSpec(settings)
|
|
282
|
+
|
|
205
283
|
let runtime: Runtime | null = null
|
|
284
|
+
let currentConfig: AppConfig = initialSpec.config
|
|
285
|
+
let generation = 0
|
|
286
|
+
let startupError: string | null = null
|
|
206
287
|
let shuttingDown = false
|
|
207
288
|
let restarting = false
|
|
208
289
|
let restartPending = false
|
|
209
290
|
let restartTimer: NodeJS.Timeout | null = null
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
shuttingDown = true
|
|
215
|
-
|
|
216
|
-
if (restartTimer !== null) {
|
|
217
|
-
clearTimeout(restartTimer)
|
|
218
|
-
restartTimer = null
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
logger.info(`received ${signal}, shutting down`)
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
await runtime?.kernel.stop()
|
|
225
|
-
process.exit(exitCode)
|
|
226
|
-
} catch (error) {
|
|
227
|
-
logger.error('shutdown failed', error)
|
|
228
|
-
process.exit(1)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const requestRestart = (): void => {
|
|
233
|
-
if (shuttingDown) return
|
|
234
|
-
|
|
235
|
-
if (restarting) {
|
|
236
|
-
restartPending = true
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (restartTimer !== null) return
|
|
241
|
-
|
|
242
|
-
restartTimer = setTimeout(() => {
|
|
243
|
-
restartTimer = null
|
|
244
|
-
void restartRuntime()
|
|
245
|
-
}, 250)
|
|
246
|
-
}
|
|
291
|
+
let restartTail: Promise<boolean> = Promise.resolve(true)
|
|
292
|
+
let exclusiveTail: Promise<unknown> = Promise.resolve()
|
|
293
|
+
let boundWebUi: StaticBinding | null = null
|
|
294
|
+
let credentials = credentialSignature(initialSpec.config)
|
|
247
295
|
|
|
248
296
|
const restartDrainTimeout = (previous: Runtime | null): number => {
|
|
249
297
|
const installTimeout =
|
|
@@ -252,115 +300,198 @@ async function main(): Promise<void> {
|
|
|
252
300
|
return Math.max(installTimeout, 5_000) * 2 + 5_000
|
|
253
301
|
}
|
|
254
302
|
|
|
255
|
-
const
|
|
256
|
-
if (shuttingDown
|
|
303
|
+
const restartRuntimeOnce = async (): Promise<boolean> => {
|
|
304
|
+
if (shuttingDown) return false
|
|
257
305
|
|
|
258
306
|
restarting = true
|
|
259
307
|
restartPending = false
|
|
260
308
|
|
|
261
309
|
const previous = runtime
|
|
262
|
-
let stopped = false
|
|
263
|
-
let restarted = false
|
|
264
310
|
|
|
265
311
|
logger.info('restarting runtime')
|
|
266
312
|
|
|
267
313
|
try {
|
|
268
|
-
// Stop accepting new events so no command can start another install
|
|
269
|
-
// while the replacement runtime is being prepared.
|
|
270
314
|
previous?.kernel.pause()
|
|
271
315
|
|
|
272
316
|
const drainTimeout = restartDrainTimeout(previous)
|
|
273
|
-
const drained =
|
|
274
|
-
previous === null
|
|
275
|
-
? true
|
|
276
|
-
: await previous.kernel.drain(drainTimeout)
|
|
277
317
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
restartPending = false
|
|
318
|
+
if (previous !== null) {
|
|
319
|
+
const drained = await previous.kernel.drain(drainTimeout)
|
|
281
320
|
|
|
282
|
-
|
|
321
|
+
if (!drained) {
|
|
322
|
+
logger.warn('runtime restart drain timed out, cancelling')
|
|
323
|
+
}
|
|
283
324
|
|
|
284
|
-
|
|
325
|
+
restartPending = false
|
|
285
326
|
|
|
286
|
-
|
|
287
|
-
spec = await loadRuntimeSpec(settings)
|
|
327
|
+
if (shuttingDown) return false
|
|
288
328
|
|
|
289
|
-
|
|
329
|
+
await previous.kernel.stop()
|
|
330
|
+
}
|
|
290
331
|
|
|
291
|
-
|
|
292
|
-
stopped = true
|
|
293
|
-
} else {
|
|
294
|
-
logger.warn(
|
|
295
|
-
'runtime restart drain timed out, cancelling pending tasks'
|
|
296
|
-
)
|
|
332
|
+
if (shuttingDown) return false
|
|
297
333
|
|
|
298
|
-
|
|
299
|
-
stopped = true
|
|
300
|
-
|
|
301
|
-
// Kernel.stop() aborts pending tasks. Wait for their rollback
|
|
302
|
-
// to finish before the replacement runtime touches the same
|
|
303
|
-
// install directory.
|
|
304
|
-
const settled =
|
|
305
|
-
previous === null
|
|
306
|
-
? true
|
|
307
|
-
: await previous.kernel.drain(drainTimeout)
|
|
308
|
-
|
|
309
|
-
if (!settled) {
|
|
310
|
-
logger.warn(
|
|
311
|
-
'cancelled tasks are still running after restart drain'
|
|
312
|
-
)
|
|
313
|
-
}
|
|
334
|
+
const spec = await loadRuntimeSpec(settings)
|
|
314
335
|
|
|
315
|
-
|
|
336
|
+
currentConfig = spec.config
|
|
316
337
|
|
|
317
|
-
|
|
318
|
-
|
|
338
|
+
const next = createRuntime(
|
|
339
|
+
spec,
|
|
340
|
+
settings,
|
|
341
|
+
requestRestart,
|
|
342
|
+
tapEvents
|
|
343
|
+
)
|
|
319
344
|
|
|
320
|
-
|
|
345
|
+
runtime = next
|
|
346
|
+
await next.kernel.start()
|
|
321
347
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
restarted = true
|
|
325
|
-
logger.info('runtime restarted')
|
|
326
|
-
} catch (error) {
|
|
327
|
-
logger.error('runtime restart failed', error)
|
|
348
|
+
generation += 1
|
|
349
|
+
startupError = null
|
|
328
350
|
|
|
329
|
-
|
|
330
|
-
previous?.kernel.resume()
|
|
331
|
-
return
|
|
332
|
-
}
|
|
351
|
+
const signature = credentialSignature(currentConfig)
|
|
333
352
|
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
runtime = createRuntime(
|
|
338
|
-
previous.spec,
|
|
339
|
-
settings,
|
|
340
|
-
requestRestart
|
|
341
|
-
)
|
|
342
|
-
await runtime.kernel.start()
|
|
343
|
-
logger.warn('restored previous runtime')
|
|
344
|
-
return
|
|
345
|
-
} catch (fallbackError) {
|
|
346
|
-
logger.error(
|
|
347
|
-
'failed to restore previous runtime',
|
|
348
|
-
fallbackError
|
|
349
|
-
)
|
|
350
|
-
}
|
|
353
|
+
if (signature !== credentials) {
|
|
354
|
+
credentials = signature
|
|
355
|
+
server.invalidateSessions()
|
|
351
356
|
}
|
|
352
357
|
|
|
353
|
-
|
|
358
|
+
logger.info('runtime restarted')
|
|
359
|
+
|
|
360
|
+
return true
|
|
361
|
+
} catch (error) {
|
|
362
|
+
runtime = null
|
|
363
|
+
startupError = describeError(error)
|
|
364
|
+
logger.error('runtime restart failed', error)
|
|
365
|
+
|
|
366
|
+
return false
|
|
354
367
|
} finally {
|
|
355
368
|
restarting = false
|
|
356
369
|
|
|
357
|
-
if (
|
|
370
|
+
if (restartPending && !shuttingDown) {
|
|
358
371
|
restartPending = false
|
|
359
372
|
requestRestart()
|
|
360
373
|
}
|
|
361
374
|
}
|
|
362
375
|
}
|
|
363
376
|
|
|
377
|
+
const restartRuntime = (): Promise<boolean> => {
|
|
378
|
+
const run = restartTail.then(restartRuntimeOnce, restartRuntimeOnce)
|
|
379
|
+
|
|
380
|
+
restartTail = run.then(
|
|
381
|
+
() => true,
|
|
382
|
+
() => false
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return run
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const requestRestart = (): void => {
|
|
389
|
+
if (shuttingDown) return
|
|
390
|
+
|
|
391
|
+
if (restarting) {
|
|
392
|
+
restartPending = true
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (restartTimer !== null) return
|
|
397
|
+
|
|
398
|
+
restartTimer = setTimeout(() => {
|
|
399
|
+
restartTimer = null
|
|
400
|
+
void restartRuntime()
|
|
401
|
+
}, 250)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const store = new ConfigStore({
|
|
405
|
+
configPath,
|
|
406
|
+
getPlugins: () => runtime?.spec.plugins ?? [],
|
|
407
|
+
restart: () => restartRuntime(),
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
const deps: WebUiDeps = {
|
|
411
|
+
logger: logger.child('webui'),
|
|
412
|
+
paths: {
|
|
413
|
+
configPath,
|
|
414
|
+
dataDir,
|
|
415
|
+
pluginDir: initialSpec.pluginDir,
|
|
416
|
+
staticDir: resolveStaticDir(),
|
|
417
|
+
},
|
|
418
|
+
version: readVersion(),
|
|
419
|
+
logs,
|
|
420
|
+
events,
|
|
421
|
+
getWebUiConfig: () => currentConfig.webui,
|
|
422
|
+
readConfig: () => store.read(),
|
|
423
|
+
validateConfig: (value) => store.validate(value),
|
|
424
|
+
applyConfig: (value, baseHash) => store.apply(value, baseHash),
|
|
425
|
+
restart: () => restartRuntime(),
|
|
426
|
+
requestRestart,
|
|
427
|
+
pause: () => runtime?.kernel.pause(),
|
|
428
|
+
resume: () => runtime?.kernel.resume(),
|
|
429
|
+
getListening: () => server.address,
|
|
430
|
+
invalidateSessions: () => server.invalidateSessions(),
|
|
431
|
+
runExclusive: async <T>(task: () => Promise<T>): Promise<T> => {
|
|
432
|
+
const run = exclusiveTail.then(task, task)
|
|
433
|
+
|
|
434
|
+
exclusiveTail = run.then(
|
|
435
|
+
() => undefined,
|
|
436
|
+
() => undefined
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return run
|
|
440
|
+
},
|
|
441
|
+
getRuntime: () =>
|
|
442
|
+
runtime === null
|
|
443
|
+
? null
|
|
444
|
+
: {
|
|
445
|
+
generation,
|
|
446
|
+
config: runtime.spec.config,
|
|
447
|
+
kernel: runtime.kernel,
|
|
448
|
+
api: runtime.kernel.api,
|
|
449
|
+
database: runtime.database,
|
|
450
|
+
installer: runtime.installer,
|
|
451
|
+
plugins: runtime.spec.plugins,
|
|
452
|
+
},
|
|
453
|
+
isRestarting: () => restarting,
|
|
454
|
+
getStartupError: () => startupError,
|
|
455
|
+
getRequiresProcessRestart: () => {
|
|
456
|
+
const webui = currentConfig.webui
|
|
457
|
+
|
|
458
|
+
if (boundWebUi === null) return webui.enabled
|
|
459
|
+
|
|
460
|
+
if (!webui.enabled) return true
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
webui.host !== boundWebUi.host || webui.port !== boundWebUi.port
|
|
464
|
+
)
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const server = new WebUiServer({
|
|
469
|
+
config: initialSpec.config.webui,
|
|
470
|
+
deps,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
const shutdown = async (signal: string, exitCode = 0): Promise<void> => {
|
|
474
|
+
if (shuttingDown) return
|
|
475
|
+
|
|
476
|
+
shuttingDown = true
|
|
477
|
+
|
|
478
|
+
if (restartTimer !== null) {
|
|
479
|
+
clearTimeout(restartTimer)
|
|
480
|
+
restartTimer = null
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
logger.info(`received ${signal}, shutting down`)
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await runtime?.kernel.stop()
|
|
487
|
+
await server.stop()
|
|
488
|
+
process.exit(exitCode)
|
|
489
|
+
} catch (error) {
|
|
490
|
+
logger.error('shutdown failed', error)
|
|
491
|
+
process.exit(1)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
364
495
|
process.once('SIGINT', () => {
|
|
365
496
|
void shutdown('SIGINT')
|
|
366
497
|
})
|
|
@@ -369,9 +500,46 @@ async function main(): Promise<void> {
|
|
|
369
500
|
void shutdown('SIGTERM')
|
|
370
501
|
})
|
|
371
502
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
503
|
+
// Start the WebUI before the kernel so it stays online when startup fails.
|
|
504
|
+
if (initialSpec.config.webui.enabled) {
|
|
505
|
+
const address: WebUiAddress | null = await server.start(
|
|
506
|
+
initialSpec.config.webui
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if (address !== null) {
|
|
510
|
+
boundWebUi = {
|
|
511
|
+
host: address.host,
|
|
512
|
+
port: address.port,
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
runtime = createRuntime(
|
|
519
|
+
initialSpec,
|
|
520
|
+
settings,
|
|
521
|
+
requestRestart,
|
|
522
|
+
tapEvents
|
|
523
|
+
)
|
|
524
|
+
await runtime.kernel.start()
|
|
525
|
+
|
|
526
|
+
generation = 1
|
|
527
|
+
startupError = null
|
|
528
|
+
credentials = credentialSignature(currentConfig)
|
|
529
|
+
logger.info('runtime started')
|
|
530
|
+
} catch (error) {
|
|
531
|
+
runtime = null
|
|
532
|
+
startupError = describeError(error)
|
|
533
|
+
logger.error('runtime startup failed', error)
|
|
534
|
+
|
|
535
|
+
if (boundWebUi === null) {
|
|
536
|
+
await shutdown('startup failure', 1)
|
|
537
|
+
} else {
|
|
538
|
+
logger.info(
|
|
539
|
+
'webui stays online; fix the configuration on the config page and restart the runtime'
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
375
543
|
}
|
|
376
544
|
|
|
377
545
|
await main().catch((error: unknown) => {
|
package/src/core/client.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface BotClientOptions {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface BotClient {
|
|
22
|
+
readonly isConnected: boolean
|
|
22
23
|
use(middleware: EventMiddleware): () => void
|
|
23
24
|
connect(): Promise<void>
|
|
24
25
|
close(): void
|
|
@@ -74,6 +75,9 @@ export function createBotClient(options: BotClientOptions): BotClient {
|
|
|
74
75
|
})
|
|
75
76
|
|
|
76
77
|
return {
|
|
78
|
+
get isConnected() {
|
|
79
|
+
return client.isConnected
|
|
80
|
+
},
|
|
77
81
|
use: (middleware) => client.use(middleware),
|
|
78
82
|
connect: () => client.connect(),
|
|
79
83
|
close: () => client.close(),
|
|
@@ -55,12 +55,51 @@ export const installerConfigSchema = z
|
|
|
55
55
|
})
|
|
56
56
|
.strict()
|
|
57
57
|
|
|
58
|
+
export const webuiConfigSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
enabled: z.boolean().default(false),
|
|
61
|
+
host: z.string().trim().min(1).max(255).default('127.0.0.1'),
|
|
62
|
+
port: z.number().int().min(1).max(65535).default(8787),
|
|
63
|
+
username: z.string().trim().min(1).max(64).optional(),
|
|
64
|
+
password: z.string().min(8).max(256).optional(),
|
|
65
|
+
sessionTtlHours: z.number().int().min(1).max(720).default(12),
|
|
66
|
+
trustProxy: z.boolean().default(false),
|
|
67
|
+
})
|
|
68
|
+
.strict()
|
|
69
|
+
.superRefine((value, context) => {
|
|
70
|
+
if (!value.enabled) return
|
|
71
|
+
|
|
72
|
+
if (value.username === undefined) {
|
|
73
|
+
context.addIssue({
|
|
74
|
+
code: 'custom',
|
|
75
|
+
message: 'username is required when webui is enabled',
|
|
76
|
+
path: ['username'],
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value.password === undefined) {
|
|
81
|
+
context.addIssue({
|
|
82
|
+
code: 'custom',
|
|
83
|
+
message: 'password is required when webui is enabled',
|
|
84
|
+
path: ['password'],
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.default({
|
|
89
|
+
enabled: false,
|
|
90
|
+
host: '127.0.0.1',
|
|
91
|
+
port: 8787,
|
|
92
|
+
sessionTtlHours: 12,
|
|
93
|
+
trustProxy: false,
|
|
94
|
+
})
|
|
95
|
+
|
|
58
96
|
export const appConfigSchema = z
|
|
59
97
|
.object({
|
|
60
98
|
url: z.url({ protocol: /^wss?$/ }).default('ws://127.0.0.1:3001/'),
|
|
61
99
|
accessToken: z.string().min(1).optional(),
|
|
62
100
|
roles: rolesConfigSchema,
|
|
63
101
|
message: messageConfigSchema,
|
|
102
|
+
webui: webuiConfigSchema,
|
|
64
103
|
plugins: z.record(z.string(), pluginEnvelopeSchema).default({}),
|
|
65
104
|
installer: installerConfigSchema.default({
|
|
66
105
|
enabled: false,
|
|
@@ -77,6 +116,7 @@ export type PluginEnvelope = z.infer<typeof pluginEnvelopeSchema>
|
|
|
77
116
|
export type MessageConfig = z.infer<typeof messageConfigSchema>
|
|
78
117
|
export type RolesConfig = z.infer<typeof rolesConfigSchema>
|
|
79
118
|
export type InstallerConfig = z.infer<typeof installerConfigSchema>
|
|
119
|
+
export type WebUiConfig = z.infer<typeof webuiConfigSchema>
|
|
80
120
|
export type AppConfig = z.infer<typeof appConfigSchema>
|
|
81
121
|
|
|
82
122
|
export function formatConfigIssues(error: z.ZodError, prefix?: string): string {
|
|
@@ -53,6 +53,7 @@ export interface RouterDeps {
|
|
|
53
53
|
roles: RoleService
|
|
54
54
|
gate: PluginGate
|
|
55
55
|
logger: Logger
|
|
56
|
+
onEvent?: ((event: SnowLumaEvent) => void) | undefined
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
type AnyCommandHandler = CommandHandler<OneBotMessageEvent, any[]>
|
|
@@ -106,12 +107,14 @@ export class Router {
|
|
|
106
107
|
private readonly handlers = new HandlerRegistry()
|
|
107
108
|
private readonly middlewares = new MiddlewareRegistry()
|
|
108
109
|
private readonly active = new Set<Promise<void>>()
|
|
110
|
+
private readonly onEvent: ((event: SnowLumaEvent) => void) | undefined
|
|
109
111
|
|
|
110
112
|
constructor(deps: RouterDeps) {
|
|
111
113
|
this.sender = deps.sender
|
|
112
114
|
this.roles = deps.roles
|
|
113
115
|
this.gate = deps.gate
|
|
114
116
|
this.logger = deps.logger.child('router')
|
|
117
|
+
this.onEvent = deps.onEvent
|
|
115
118
|
this.refuser = new PermissionRefuser(deps.sender, this.logger)
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -122,6 +125,8 @@ export class Router {
|
|
|
122
125
|
) => {
|
|
123
126
|
if (!this.accepting) return
|
|
124
127
|
|
|
128
|
+
this.emitEvent(event)
|
|
129
|
+
|
|
125
130
|
const run = this.handleEvent(event, sdkContext, next)
|
|
126
131
|
this.active.add(run)
|
|
127
132
|
|
|
@@ -132,6 +137,16 @@ export class Router {
|
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
139
|
|
|
140
|
+
private emitEvent(event: SnowLumaEvent): void {
|
|
141
|
+
if (!this.onEvent) return
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
this.onEvent(event)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.logger.error('event tap failed', error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
135
150
|
private async handleEvent(
|
|
136
151
|
event: SnowLumaEvent,
|
|
137
152
|
sdkContext: SnowLumaEventContextWithQuote,
|
package/src/core/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type {
|
|
|
6
6
|
PluginEnvelope,
|
|
7
7
|
PluginGroups,
|
|
8
8
|
RolesConfig,
|
|
9
|
+
WebUiConfig,
|
|
9
10
|
} from './config/schema.js'
|
|
10
11
|
export type { TaskQueue } from './dispatch/task-queue.js'
|
|
11
12
|
export type { Logger, LogLevel } from './logging/logger.js'
|
|
@@ -50,8 +51,10 @@ export type {
|
|
|
50
51
|
} from './plugin/types.js'
|
|
51
52
|
export type { Permission } from './security/permission.js'
|
|
52
53
|
export type {
|
|
54
|
+
SessionListOptions,
|
|
53
55
|
SessionMessage,
|
|
54
56
|
SessionRole,
|
|
55
57
|
SessionState,
|
|
56
58
|
SessionStore,
|
|
59
|
+
SessionSummary,
|
|
57
60
|
} from './storage/session.js'
|