@jsbot-tellus/core 2.3.0 → 2.5.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/command.d.ts.map +1 -1
- package/dist/core/dispatch/command.js +5 -1
- package/dist/core/dispatch/command.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 +14 -1
- 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 +4 -3
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/command-params.js +122 -3
- 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 +2 -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/command.ts +7 -1
- package/src/core/dispatch/router.ts +17 -1
- 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 +165 -7
- 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,7 +1,14 @@
|
|
|
1
|
-
import { parseSegments } from '@snowluma/sdk'
|
|
1
|
+
import { parseSegments, type OneBotMessageEvent } from '@snowluma/sdk'
|
|
2
2
|
|
|
3
3
|
export type CommandParamType =
|
|
4
|
-
|
|
4
|
+
| 'string'
|
|
5
|
+
| 'text'
|
|
6
|
+
| 'number'
|
|
7
|
+
| 'integer'
|
|
8
|
+
| 'boolean'
|
|
9
|
+
| 'at'
|
|
10
|
+
| 'reply'
|
|
11
|
+
| 'time'
|
|
5
12
|
|
|
6
13
|
export interface ParsedCommandParams {
|
|
7
14
|
args: unknown[]
|
|
@@ -10,7 +17,7 @@ export interface ParsedCommandParams {
|
|
|
10
17
|
|
|
11
18
|
export interface CompiledCommandParams {
|
|
12
19
|
usage: string
|
|
13
|
-
parse(input: string): ParsedCommandParams
|
|
20
|
+
parse(input: string, event?: OneBotMessageEvent): ParsedCommandParams
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export class CommandParamsError extends Error {
|
|
@@ -51,6 +58,8 @@ function parseType(value: string): CommandParamType {
|
|
|
51
58
|
case 'integer':
|
|
52
59
|
case 'boolean':
|
|
53
60
|
case 'at':
|
|
61
|
+
case 'reply':
|
|
62
|
+
case 'time':
|
|
54
63
|
return value
|
|
55
64
|
default:
|
|
56
65
|
throw declarationError(`未知的参数类型 "${value}"`)
|
|
@@ -209,6 +218,127 @@ function parseAt(value: string, name: string, usage: string): number {
|
|
|
209
218
|
)
|
|
210
219
|
}
|
|
211
220
|
|
|
221
|
+
const TIME_UNITS: Record<string, number> = {
|
|
222
|
+
s: 1,
|
|
223
|
+
秒: 1,
|
|
224
|
+
m: 60,
|
|
225
|
+
分: 60,
|
|
226
|
+
分钟: 60,
|
|
227
|
+
h: 3600,
|
|
228
|
+
时: 3600,
|
|
229
|
+
小时: 3600,
|
|
230
|
+
d: 86400,
|
|
231
|
+
天: 86400,
|
|
232
|
+
w: 604800,
|
|
233
|
+
周: 604800,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const TIME_PART = /^(\d+(?:\.\d+)?)(分钟|小时|秒|分|时|天|周|[smhdw])/i
|
|
237
|
+
|
|
238
|
+
function parseTimeValue(value: string): number | null {
|
|
239
|
+
const raw = value.trim()
|
|
240
|
+
|
|
241
|
+
if (/^\d+$/.test(raw)) {
|
|
242
|
+
const seconds = Number(raw)
|
|
243
|
+
|
|
244
|
+
return Number.isSafeInteger(seconds) ? seconds : null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let index = 0
|
|
248
|
+
let total = 0
|
|
249
|
+
let parsed = false
|
|
250
|
+
|
|
251
|
+
while (index < raw.length) {
|
|
252
|
+
const match = TIME_PART.exec(raw.slice(index))
|
|
253
|
+
|
|
254
|
+
if (!match) return null
|
|
255
|
+
|
|
256
|
+
const unit = TIME_UNITS[match[2]?.toLowerCase() ?? '']
|
|
257
|
+
const amount = Number(match[1])
|
|
258
|
+
|
|
259
|
+
if (unit === undefined) return null
|
|
260
|
+
|
|
261
|
+
total += amount * unit
|
|
262
|
+
index += match[0].length
|
|
263
|
+
parsed = true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!parsed || !Number.isSafeInteger(total) || total < 0) {
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return total
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseTime(value: string, name: string, usage: string): number {
|
|
274
|
+
const seconds = parseTimeValue(value)
|
|
275
|
+
|
|
276
|
+
if (seconds === null) {
|
|
277
|
+
throw new CommandParamsError(
|
|
278
|
+
`参数 ${name} 需要是时间,例如 10s、5m、1h30m`,
|
|
279
|
+
usage
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return seconds
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
287
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function toMessageId(value: unknown): number | undefined {
|
|
291
|
+
const raw = typeof value === 'string' ? value.trim() : value
|
|
292
|
+
|
|
293
|
+
if (typeof raw !== 'number' && typeof raw !== 'string') {
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const parsed =
|
|
298
|
+
typeof raw === 'string'
|
|
299
|
+
? /^-?\d+$/.test(raw)
|
|
300
|
+
? Number(raw)
|
|
301
|
+
: Number.NaN
|
|
302
|
+
: raw
|
|
303
|
+
|
|
304
|
+
return Number.isSafeInteger(parsed) && parsed !== 0 ? parsed : undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function replyIdIn(segments: readonly unknown[]): number | undefined {
|
|
308
|
+
for (const segment of segments) {
|
|
309
|
+
if (!isRecord(segment) || segment.type !== 'reply') continue
|
|
310
|
+
|
|
311
|
+
const data = isRecord(segment.data) ? segment.data : undefined
|
|
312
|
+
const id = toMessageId(data?.id)
|
|
313
|
+
|
|
314
|
+
if (id !== undefined) return id
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function quoteIdOf(event: OneBotMessageEvent | undefined): number | undefined {
|
|
321
|
+
if (!event) return undefined
|
|
322
|
+
|
|
323
|
+
const message = event.message
|
|
324
|
+
|
|
325
|
+
if (typeof message === 'string') {
|
|
326
|
+
const id = replyIdIn(parseSegments(message))
|
|
327
|
+
|
|
328
|
+
if (id !== undefined) return id
|
|
329
|
+
} else if (Array.isArray(message)) {
|
|
330
|
+
const id = replyIdIn(message)
|
|
331
|
+
|
|
332
|
+
if (id !== undefined) return id
|
|
333
|
+
} else if (isRecord(message)) {
|
|
334
|
+
const id = replyIdIn([message])
|
|
335
|
+
|
|
336
|
+
if (id !== undefined) return id
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return replyIdIn(parseSegments(event.raw_message))
|
|
340
|
+
}
|
|
341
|
+
|
|
212
342
|
function parseDefaultValue(
|
|
213
343
|
type: CommandParamType,
|
|
214
344
|
raw: string,
|
|
@@ -248,7 +378,17 @@ function parseDefaultValue(
|
|
|
248
378
|
return boolean
|
|
249
379
|
}
|
|
250
380
|
case 'at':
|
|
381
|
+
case 'reply':
|
|
251
382
|
throw declarationError(`参数 ${name} 不支持默认值`)
|
|
383
|
+
case 'time': {
|
|
384
|
+
const seconds = parseTimeValue(value)
|
|
385
|
+
|
|
386
|
+
if (seconds === null) {
|
|
387
|
+
throw declarationError(`参数 ${name} 的默认值不是时间`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return seconds
|
|
391
|
+
}
|
|
252
392
|
}
|
|
253
393
|
}
|
|
254
394
|
|
|
@@ -526,19 +666,37 @@ function parseValue(spec: ParamSpec, raw: string, usage: string): unknown {
|
|
|
526
666
|
}
|
|
527
667
|
case 'at':
|
|
528
668
|
return parseAt(raw, spec.name, usage)
|
|
669
|
+
case 'time':
|
|
670
|
+
return parseTime(raw, spec.name, usage)
|
|
529
671
|
}
|
|
530
672
|
}
|
|
531
673
|
|
|
532
674
|
function parseInput(
|
|
533
675
|
specs: ParamSpec[],
|
|
534
676
|
input: string,
|
|
535
|
-
usage: string
|
|
677
|
+
usage: string,
|
|
678
|
+
quoteId: number | undefined
|
|
536
679
|
): ParsedCommandParams {
|
|
537
680
|
const tokens = tokenizeInput(input, usage)
|
|
538
681
|
const args: unknown[] = []
|
|
539
682
|
let index = 0
|
|
540
683
|
|
|
541
684
|
for (const spec of specs) {
|
|
685
|
+
if (spec.type === 'reply') {
|
|
686
|
+
if (quoteId !== undefined) {
|
|
687
|
+
args.push(quoteId)
|
|
688
|
+
} else if (spec.required) {
|
|
689
|
+
throw new CommandParamsError(
|
|
690
|
+
`参数 ${spec.name} 需要引用一条消息`,
|
|
691
|
+
usage
|
|
692
|
+
)
|
|
693
|
+
} else {
|
|
694
|
+
args.push(undefined)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
continue
|
|
698
|
+
}
|
|
699
|
+
|
|
542
700
|
if (spec.rest) {
|
|
543
701
|
for (
|
|
544
702
|
let tokenIndex = index;
|
|
@@ -634,8 +792,8 @@ export function compileCommandParams(
|
|
|
634
792
|
|
|
635
793
|
return {
|
|
636
794
|
usage,
|
|
637
|
-
parse(input: string): ParsedCommandParams {
|
|
638
|
-
return parseInput(specs, input, usage)
|
|
795
|
+
parse(input: string, event?: OneBotMessageEvent): ParsedCommandParams {
|
|
796
|
+
return parseInput(specs, input, usage, quoteIdOf(event))
|
|
639
797
|
},
|
|
640
798
|
}
|
|
641
799
|
}
|
|
@@ -653,7 +811,7 @@ type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
|
|
|
653
811
|
type ParamValue<Type extends string> =
|
|
654
812
|
Trim<Type> extends 'string' | 'text'
|
|
655
813
|
? string
|
|
656
|
-
: Trim<Type> extends 'number' | 'integer' | 'at'
|
|
814
|
+
: Trim<Type> extends 'number' | 'integer' | 'at' | 'reply' | 'time'
|
|
657
815
|
? number
|
|
658
816
|
: Trim<Type> extends 'boolean'
|
|
659
817
|
? 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
|
}
|