@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/core/kernel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { SnowLumaEvent } from '@snowluma/sdk'
|
|
1
2
|
import type { BotClient } from './client.js'
|
|
2
3
|
import type { AppConfig } from './config/schema.js'
|
|
3
|
-
import { createBotApi } from './api.js'
|
|
4
|
+
import { createBotApi, type BotApi } from './api.js'
|
|
4
5
|
import { Router } from './dispatch/router.js'
|
|
5
6
|
import { PerKeyTaskQueue } from './dispatch/task-queue.js'
|
|
6
7
|
import { Lifecycle, type LifecycleState } from './lifecycle/lifecycle.js'
|
|
@@ -28,10 +29,13 @@ export interface KernelOptions {
|
|
|
28
29
|
plugins?: AnyPluginDefinition[] | undefined
|
|
29
30
|
installer?: PluginInstaller | undefined
|
|
30
31
|
onRestart?: (() => void) | undefined
|
|
32
|
+
onEvent?: ((event: SnowLumaEvent) => void) | undefined
|
|
31
33
|
drainTimeoutMs?: number | undefined
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export class Kernel {
|
|
37
|
+
readonly api: BotApi
|
|
38
|
+
|
|
35
39
|
private readonly config: AppConfig
|
|
36
40
|
private readonly client: BotClient
|
|
37
41
|
private readonly logger: Logger
|
|
@@ -62,12 +66,16 @@ export class Kernel {
|
|
|
62
66
|
const tasks = new PerKeyTaskQueue(options.logger)
|
|
63
67
|
const api = createBotApi(options.client, sender)
|
|
64
68
|
|
|
69
|
+
this.api = api
|
|
65
70
|
this.tasks = tasks
|
|
66
71
|
this.router = new Router({
|
|
67
72
|
sender,
|
|
68
73
|
roles,
|
|
69
74
|
gate,
|
|
70
75
|
logger: options.logger,
|
|
76
|
+
...(options.onEvent !== undefined
|
|
77
|
+
? { onEvent: options.onEvent }
|
|
78
|
+
: {}),
|
|
71
79
|
})
|
|
72
80
|
|
|
73
81
|
let host: PluginHost
|
|
@@ -131,6 +139,22 @@ export class Kernel {
|
|
|
131
139
|
return this.lifecycle.state
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
get uptimeMs(): number {
|
|
143
|
+
return this.startedAt === null ? 0 : Date.now() - this.startedAt
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get isConnected(): boolean {
|
|
147
|
+
return this.client.isConnected
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
listPlugins() {
|
|
151
|
+
return this.host.listStatus()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
listCommands() {
|
|
155
|
+
return this.router.listCommands()
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
start(): Promise<void> {
|
|
135
159
|
return this.lifecycle.start()
|
|
136
160
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatValue,
|
|
3
|
+
logLevelWeight,
|
|
4
|
+
type LogLevel,
|
|
5
|
+
type LogRecord,
|
|
6
|
+
} from './logger.js'
|
|
7
|
+
|
|
8
|
+
export interface LogEntry {
|
|
9
|
+
time: number
|
|
10
|
+
level: LogLevel
|
|
11
|
+
scope: string
|
|
12
|
+
message: string
|
|
13
|
+
args: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LogFilter {
|
|
17
|
+
level?: LogLevel | undefined
|
|
18
|
+
scope?: string | undefined
|
|
19
|
+
query?: string | undefined
|
|
20
|
+
limit?: number | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function matchesLogFilter(entry: LogEntry, filter: LogFilter): boolean {
|
|
24
|
+
if (
|
|
25
|
+
filter.level !== undefined &&
|
|
26
|
+
logLevelWeight(entry.level) < logLevelWeight(filter.level)
|
|
27
|
+
) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
filter.scope !== undefined &&
|
|
33
|
+
filter.scope.length > 0 &&
|
|
34
|
+
entry.scope !== filter.scope
|
|
35
|
+
) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (filter.query !== undefined && filter.query.length > 0) {
|
|
40
|
+
const query = filter.query.toLowerCase()
|
|
41
|
+
const haystack = [entry.scope, entry.message, ...entry.args]
|
|
42
|
+
.join(' ')
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
|
|
45
|
+
if (!haystack.includes(query)) return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class LogBuffer {
|
|
52
|
+
private readonly entries: LogEntry[] = []
|
|
53
|
+
private readonly listeners = new Set<(entry: LogEntry) => void>()
|
|
54
|
+
|
|
55
|
+
constructor(private readonly capacity = 2_000) {}
|
|
56
|
+
|
|
57
|
+
get size(): number {
|
|
58
|
+
return this.entries.length
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
push(record: LogRecord): void {
|
|
62
|
+
const entry: LogEntry = {
|
|
63
|
+
time: record.time.getTime(),
|
|
64
|
+
level: record.level,
|
|
65
|
+
scope: record.scope,
|
|
66
|
+
message: record.message,
|
|
67
|
+
args: record.args.map(formatValue),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.entries.push(entry)
|
|
71
|
+
|
|
72
|
+
if (this.entries.length > this.capacity) {
|
|
73
|
+
this.entries.splice(0, this.entries.length - this.capacity)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const listener of this.listeners) {
|
|
77
|
+
try {
|
|
78
|
+
listener(entry)
|
|
79
|
+
} catch {
|
|
80
|
+
// Log consumers must never break logging.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
list(filter: LogFilter = {}): LogEntry[] {
|
|
86
|
+
const matched = this.entries.filter((entry) =>
|
|
87
|
+
matchesLogFilter(entry, filter)
|
|
88
|
+
)
|
|
89
|
+
const limit = filter.limit
|
|
90
|
+
|
|
91
|
+
if (limit === undefined || limit < 0 || limit >= matched.length) {
|
|
92
|
+
return matched
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return matched.slice(-limit)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
clear(): void {
|
|
99
|
+
this.entries.length = 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
subscribe(listener: (entry: LogEntry) => void): () => void {
|
|
103
|
+
this.listeners.add(listener)
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
this.listeners.delete(listener)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -34,7 +34,7 @@ export interface Logger {
|
|
|
34
34
|
child(scope: string): Logger
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function formatValue(value: unknown): string {
|
|
37
|
+
export function formatValue(value: unknown): string {
|
|
38
38
|
if (value instanceof Error) {
|
|
39
39
|
return value.stack ?? `${value.name}: ${value.message}`
|
|
40
40
|
}
|
|
@@ -49,7 +49,7 @@ function formatValue(value: unknown): string {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
export const defaultLogSink: LogSink = (record) => {
|
|
53
53
|
const time = record.time.toISOString()
|
|
54
54
|
const scope = record.scope ? ` [${record.scope}]` : ''
|
|
55
55
|
const args =
|
|
@@ -92,10 +92,14 @@ export function parseLogLevel(
|
|
|
92
92
|
return normalized
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export function logLevelWeight(level: LogLevel): number {
|
|
96
|
+
return LEVEL_WEIGHT[level]
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
export function createLogger(options: LoggerOptions = {}): Logger {
|
|
96
100
|
const threshold = options.level ?? 'info'
|
|
97
101
|
const scope = options.scope ?? 'bot'
|
|
98
|
-
const sink = options.sink ??
|
|
102
|
+
const sink = options.sink ?? defaultLogSink
|
|
99
103
|
|
|
100
104
|
const emit = (level: LogLevel, message: string, args: unknown[]): void => {
|
|
101
105
|
if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[threshold]) return
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { parseSegments } from '@snowluma/sdk'
|
|
2
|
+
|
|
1
3
|
export type CommandParamType =
|
|
2
|
-
'string' | 'text' | 'number' | 'integer' | 'boolean'
|
|
4
|
+
'string' | 'text' | 'number' | 'integer' | 'boolean' | 'at'
|
|
3
5
|
|
|
4
6
|
export interface ParsedCommandParams {
|
|
5
7
|
args: unknown[]
|
|
@@ -48,6 +50,7 @@ function parseType(value: string): CommandParamType {
|
|
|
48
50
|
case 'number':
|
|
49
51
|
case 'integer':
|
|
50
52
|
case 'boolean':
|
|
53
|
+
case 'at':
|
|
51
54
|
return value
|
|
52
55
|
default:
|
|
53
56
|
throw declarationError(`未知的参数类型 "${value}"`)
|
|
@@ -177,6 +180,35 @@ function parseBoolean(value: string): boolean | null {
|
|
|
177
180
|
return null
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
function parseAt(value: string, name: string, usage: string): number {
|
|
184
|
+
const raw = value.trim()
|
|
185
|
+
const segments = parseSegments(raw)
|
|
186
|
+
const segment = segments[0]
|
|
187
|
+
|
|
188
|
+
if (segments.length === 1 && segment?.type === 'at') {
|
|
189
|
+
const qq = segment.data.qq
|
|
190
|
+
|
|
191
|
+
if (qq !== 'all') {
|
|
192
|
+
const parsed = Number(qq)
|
|
193
|
+
|
|
194
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) {
|
|
195
|
+
return parsed
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (/^\d+$/.test(raw)) {
|
|
199
|
+
const parsed = Number(raw)
|
|
200
|
+
|
|
201
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) {
|
|
202
|
+
return parsed
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new CommandParamsError(
|
|
207
|
+
`参数 ${name} 需要 @ 一个用户或填写 QQ 号`,
|
|
208
|
+
usage
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
180
212
|
function parseDefaultValue(
|
|
181
213
|
type: CommandParamType,
|
|
182
214
|
raw: string,
|
|
@@ -215,6 +247,8 @@ function parseDefaultValue(
|
|
|
215
247
|
|
|
216
248
|
return boolean
|
|
217
249
|
}
|
|
250
|
+
case 'at':
|
|
251
|
+
throw declarationError(`参数 ${name} 不支持默认值`)
|
|
218
252
|
}
|
|
219
253
|
}
|
|
220
254
|
|
|
@@ -405,6 +439,16 @@ function tokenizeInput(input: string, usage: string): InputToken[] {
|
|
|
405
439
|
|
|
406
440
|
if (/\s/.test(current)) break
|
|
407
441
|
|
|
442
|
+
if (current === '[' && input.startsWith('[CQ:', index)) {
|
|
443
|
+
const close = input.indexOf(']', index + 4)
|
|
444
|
+
|
|
445
|
+
if (close >= 0) {
|
|
446
|
+
value += input.slice(index, close + 1)
|
|
447
|
+
index = close + 1
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
408
452
|
if (current === '"' || current === "'") {
|
|
409
453
|
quote = current
|
|
410
454
|
index += 1
|
|
@@ -480,6 +524,8 @@ function parseValue(spec: ParamSpec, raw: string, usage: string): unknown {
|
|
|
480
524
|
|
|
481
525
|
return value
|
|
482
526
|
}
|
|
527
|
+
case 'at':
|
|
528
|
+
return parseAt(raw, spec.name, usage)
|
|
483
529
|
}
|
|
484
530
|
}
|
|
485
531
|
|
|
@@ -607,7 +653,7 @@ type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
|
|
|
607
653
|
type ParamValue<Type extends string> =
|
|
608
654
|
Trim<Type> extends 'string' | 'text'
|
|
609
655
|
? string
|
|
610
|
-
: Trim<Type> extends 'number' | 'integer'
|
|
656
|
+
: Trim<Type> extends 'number' | 'integer' | 'at'
|
|
611
657
|
? number
|
|
612
658
|
: Trim<Type> extends 'boolean'
|
|
613
659
|
? boolean
|
|
@@ -26,6 +26,21 @@ export interface SessionStore {
|
|
|
26
26
|
clear(sessionId: string): Promise<void>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface SessionSummary {
|
|
30
|
+
sessionId: string
|
|
31
|
+
provider?: string | undefined
|
|
32
|
+
model?: string | undefined
|
|
33
|
+
persona?: string | undefined
|
|
34
|
+
messageCount: number
|
|
35
|
+
updatedAt: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SessionListOptions {
|
|
39
|
+
limit?: number | undefined
|
|
40
|
+
offset?: number | undefined
|
|
41
|
+
query?: string | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
interface SessionRow {
|
|
30
45
|
session_id: string
|
|
31
46
|
provider: string | null
|
|
@@ -35,6 +50,15 @@ interface SessionRow {
|
|
|
35
50
|
updated_at: number
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
interface SessionSummaryRow {
|
|
54
|
+
session_id: string
|
|
55
|
+
provider: string | null
|
|
56
|
+
model: string | null
|
|
57
|
+
persona: string | null
|
|
58
|
+
message_count: number
|
|
59
|
+
updated_at: number
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
function isSessionRole(value: unknown): value is SessionRole {
|
|
39
63
|
return value === 'system' || value === 'user' || value === 'assistant'
|
|
40
64
|
}
|
|
@@ -79,6 +103,29 @@ function toState(row: SessionRow): SessionState {
|
|
|
79
103
|
}
|
|
80
104
|
}
|
|
81
105
|
|
|
106
|
+
function toSummary(row: SessionSummaryRow): SessionSummary {
|
|
107
|
+
return {
|
|
108
|
+
sessionId: row.session_id,
|
|
109
|
+
...(row.provider !== null ? { provider: row.provider } : {}),
|
|
110
|
+
...(row.model !== null ? { model: row.model } : {}),
|
|
111
|
+
...(row.persona !== null ? { persona: row.persona } : {}),
|
|
112
|
+
messageCount: row.message_count,
|
|
113
|
+
updatedAt: row.updated_at,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clampLimit(value: number | undefined, fallback: number): number {
|
|
118
|
+
if (value === undefined || !Number.isFinite(value)) return fallback
|
|
119
|
+
|
|
120
|
+
return Math.min(Math.max(Math.trunc(value), 1), 500)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function clampOffset(value: number | undefined): number {
|
|
124
|
+
if (value === undefined || !Number.isFinite(value)) return 0
|
|
125
|
+
|
|
126
|
+
return Math.max(Math.trunc(value), 0)
|
|
127
|
+
}
|
|
128
|
+
|
|
82
129
|
export class SqliteSessionStore implements SessionStore {
|
|
83
130
|
constructor(private readonly database: Database) {}
|
|
84
131
|
|
|
@@ -167,4 +214,52 @@ export class SqliteSessionStore implements SessionStore {
|
|
|
167
214
|
.prepare('DELETE FROM sessions WHERE session_id = ?')
|
|
168
215
|
.run(sessionId)
|
|
169
216
|
}
|
|
217
|
+
|
|
218
|
+
async list(options: SessionListOptions = {}): Promise<SessionSummary[]> {
|
|
219
|
+
const limit = clampLimit(options.limit, 50)
|
|
220
|
+
const offset = clampOffset(options.offset)
|
|
221
|
+
const query = options.query?.trim()
|
|
222
|
+
|
|
223
|
+
const rows = (query
|
|
224
|
+
? this.database.connection
|
|
225
|
+
.prepare(
|
|
226
|
+
`SELECT session_id, provider, model, persona,
|
|
227
|
+
json_array_length(messages) AS message_count,
|
|
228
|
+
updated_at
|
|
229
|
+
FROM sessions
|
|
230
|
+
WHERE session_id LIKE ?
|
|
231
|
+
ORDER BY updated_at DESC
|
|
232
|
+
LIMIT ? OFFSET ?`
|
|
233
|
+
)
|
|
234
|
+
.all(`%${query}%`, limit, offset)
|
|
235
|
+
: this.database.connection
|
|
236
|
+
.prepare(
|
|
237
|
+
`SELECT session_id, provider, model, persona,
|
|
238
|
+
json_array_length(messages) AS message_count,
|
|
239
|
+
updated_at
|
|
240
|
+
FROM sessions
|
|
241
|
+
ORDER BY updated_at DESC
|
|
242
|
+
LIMIT ? OFFSET ?`
|
|
243
|
+
)
|
|
244
|
+
.all(limit, offset)) as unknown as SessionSummaryRow[]
|
|
245
|
+
|
|
246
|
+
return rows.map(toSummary)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async count(query?: string): Promise<number> {
|
|
250
|
+
const value = query?.trim()
|
|
251
|
+
const row = (
|
|
252
|
+
value
|
|
253
|
+
? this.database.connection
|
|
254
|
+
.prepare(
|
|
255
|
+
'SELECT COUNT(*) AS count FROM sessions WHERE session_id LIKE ?'
|
|
256
|
+
)
|
|
257
|
+
.get(`%${value}%`)
|
|
258
|
+
: this.database.connection
|
|
259
|
+
.prepare('SELECT COUNT(*) AS count FROM sessions')
|
|
260
|
+
.get()
|
|
261
|
+
) as { count: number } | undefined
|
|
262
|
+
|
|
263
|
+
return row?.count ?? 0
|
|
264
|
+
}
|
|
170
265
|
}
|