@jsbot-tellus/core 2.5.0 → 2.6.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 +8 -5
- package/dist/app/cli.d.ts +10 -0
- package/dist/app/cli.d.ts.map +1 -0
- package/dist/app/cli.js +37 -0
- package/dist/app/cli.js.map +1 -0
- package/dist/app/runtime.d.ts +121 -0
- package/dist/app/runtime.d.ts.map +1 -0
- package/dist/app/runtime.js +296 -0
- package/dist/app/runtime.js.map +1 -0
- package/dist/app.js +63 -273
- package/dist/app.js.map +1 -1
- package/dist/core/plugin/command-params.d.ts +3 -37
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/command-params.js +2 -580
- package/dist/core/plugin/command-params.js.map +1 -1
- package/dist/core/plugin/install/paths.d.ts +7 -0
- package/dist/core/plugin/install/paths.d.ts.map +1 -0
- package/dist/core/plugin/install/paths.js +49 -0
- package/dist/core/plugin/install/paths.js.map +1 -0
- package/dist/core/plugin/install/state.d.ts +23 -0
- package/dist/core/plugin/install/state.d.ts.map +1 -0
- package/dist/core/plugin/install/state.js +129 -0
- package/dist/core/plugin/install/state.js.map +1 -0
- package/dist/core/plugin/install/validate-script.d.ts +3 -0
- package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
- package/dist/core/plugin/install/validate-script.js +154 -0
- package/dist/core/plugin/install/validate-script.js.map +1 -0
- package/dist/core/plugin/install-dir.d.ts +3 -28
- package/dist/core/plugin/install-dir.d.ts.map +1 -1
- package/dist/core/plugin/install-dir.js +3 -326
- package/dist/core/plugin/install-dir.js.map +1 -1
- package/dist/core/plugin/installer/checks.d.ts +6 -0
- package/dist/core/plugin/installer/checks.d.ts.map +1 -0
- package/dist/core/plugin/installer/checks.js +99 -0
- package/dist/core/plugin/installer/checks.js.map +1 -0
- package/dist/core/plugin/installer/support.d.ts +37 -0
- package/dist/core/plugin/installer/support.d.ts.map +1 -0
- package/dist/core/plugin/installer/support.js +67 -0
- package/dist/core/plugin/installer/support.js.map +1 -0
- package/dist/core/plugin/installer/warnings.d.ts +5 -0
- package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
- package/dist/core/plugin/installer/warnings.js +28 -0
- package/dist/core/plugin/installer/warnings.js.map +1 -0
- package/dist/core/plugin/installer.d.ts +4 -30
- package/dist/core/plugin/installer.d.ts.map +1 -1
- package/dist/core/plugin/installer.js +21 -202
- package/dist/core/plugin/installer.js.map +1 -1
- package/dist/core/plugin/params/compile.d.ts +3 -0
- package/dist/core/plugin/params/compile.d.ts.map +1 -0
- package/dist/core/plugin/params/compile.js +23 -0
- package/dist/core/plugin/params/compile.js.map +1 -0
- package/dist/core/plugin/params/declaration.d.ts +9 -0
- package/dist/core/plugin/params/declaration.d.ts.map +1 -0
- package/dist/core/plugin/params/declaration.js +286 -0
- package/dist/core/plugin/params/declaration.js.map +1 -0
- package/dist/core/plugin/params/input.d.ts +5 -0
- package/dist/core/plugin/params/input.d.ts.map +1 -0
- package/dist/core/plugin/params/input.js +196 -0
- package/dist/core/plugin/params/input.js.map +1 -0
- package/dist/core/plugin/params/reply.d.ts +3 -0
- package/dist/core/plugin/params/reply.d.ts.map +1 -0
- package/dist/core/plugin/params/reply.js +49 -0
- package/dist/core/plugin/params/reply.js.map +1 -0
- package/dist/core/plugin/params/type-level.d.ts +24 -0
- package/dist/core/plugin/params/type-level.d.ts.map +1 -0
- package/dist/core/plugin/params/type-level.js +2 -0
- package/dist/core/plugin/params/type-level.js.map +1 -0
- package/dist/core/plugin/params/types.d.ts +33 -0
- package/dist/core/plugin/params/types.d.ts.map +1 -0
- package/dist/core/plugin/params/types.js +12 -0
- package/dist/core/plugin/params/types.js.map +1 -0
- package/dist/core/plugin/params/values.d.ts +7 -0
- package/dist/core/plugin/params/values.d.ts.map +1 -0
- package/dist/core/plugin/params/values.js +111 -0
- package/dist/core/plugin/params/values.js.map +1 -0
- package/dist/core/web/api.d.ts +2 -14
- package/dist/core/web/api.d.ts.map +1 -1
- package/dist/core/web/api.js +8 -500
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/routes/actions.d.ts +4 -0
- package/dist/core/web/routes/actions.d.ts.map +1 -0
- package/dist/core/web/routes/actions.js +33 -0
- package/dist/core/web/routes/actions.js.map +1 -0
- package/dist/core/web/routes/config.d.ts +5 -0
- package/dist/core/web/routes/config.d.ts.map +1 -0
- package/dist/core/web/routes/config.js +30 -0
- package/dist/core/web/routes/config.js.map +1 -0
- package/dist/core/web/routes/kv.d.ts +6 -0
- package/dist/core/web/routes/kv.d.ts.map +1 -0
- package/dist/core/web/routes/kv.js +80 -0
- package/dist/core/web/routes/kv.js.map +1 -0
- package/dist/core/web/routes/plugins.d.ts +5 -0
- package/dist/core/web/routes/plugins.d.ts.map +1 -0
- package/dist/core/web/routes/plugins.js +98 -0
- package/dist/core/web/routes/plugins.js.map +1 -0
- package/dist/core/web/routes/sessions.d.ts +8 -0
- package/dist/core/web/routes/sessions.d.ts.map +1 -0
- package/dist/core/web/routes/sessions.js +130 -0
- package/dist/core/web/routes/sessions.js.map +1 -0
- package/dist/core/web/routes/shared.d.ts +18 -0
- package/dist/core/web/routes/shared.d.ts.map +1 -0
- package/dist/core/web/routes/shared.js +19 -0
- package/dist/core/web/routes/shared.js.map +1 -0
- package/dist/core/web/routes/status.d.ts +6 -0
- package/dist/core/web/routes/status.d.ts.map +1 -0
- package/dist/core/web/routes/status.js +132 -0
- package/dist/core/web/routes/status.js.map +1 -0
- package/dist/core/web/server/auth.d.ts +17 -0
- package/dist/core/web/server/auth.d.ts.map +1 -0
- package/dist/core/web/server/auth.js +68 -0
- package/dist/core/web/server/auth.js.map +1 -0
- package/dist/core/web/server/csp.d.ts +11 -0
- package/dist/core/web/server/csp.d.ts.map +1 -0
- package/dist/core/web/server/csp.js +31 -0
- package/dist/core/web/server/csp.js.map +1 -0
- package/dist/core/web/server/request.d.ts +6 -0
- package/dist/core/web/server/request.d.ts.map +1 -0
- package/dist/core/web/server/request.js +33 -0
- package/dist/core/web/server/request.js.map +1 -0
- package/dist/core/web/server/security.d.ts +4 -0
- package/dist/core/web/server/security.d.ts.map +1 -0
- package/dist/core/web/server/security.js +12 -0
- package/dist/core/web/server/security.js.map +1 -0
- package/dist/core/web/server/sse.d.ts +18 -0
- package/dist/core/web/server/sse.d.ts.map +1 -0
- package/dist/core/web/server/sse.js +49 -0
- package/dist/core/web/server/sse.js.map +1 -0
- package/dist/core/web/server/static.d.ts +16 -0
- package/dist/core/web/server/static.d.ts.map +1 -0
- package/dist/core/web/server/static.js +80 -0
- package/dist/core/web/server/static.js.map +1 -0
- package/dist/core/web/server.d.ts +0 -7
- package/dist/core/web/server.d.ts.map +1 -1
- package/dist/core/web/server.js +41 -263
- package/dist/core/web/server.js.map +1 -1
- package/docs/architecture.md +28 -11
- package/docs/configuration.md +1 -1
- package/docs/migration.md +2 -2
- package/docs/plugin-api.md +24 -11
- package/docs/plugin-development.md +1 -1
- package/package.json +19 -17
- package/src/app/cli.ts +47 -0
- package/src/app/runtime.ts +522 -0
- package/src/app.ts +87 -395
- package/src/core/plugin/command-params.ts +8 -871
- package/src/core/plugin/install/paths.ts +72 -0
- package/src/core/plugin/install/state.ts +196 -0
- package/src/core/plugin/install/validate-script.ts +154 -0
- package/src/core/plugin/install-dir.ts +19 -427
- package/src/core/plugin/installer/checks.ts +137 -0
- package/src/core/plugin/installer/support.ts +120 -0
- package/src/core/plugin/installer/warnings.ts +52 -0
- package/src/core/plugin/installer.ts +52 -301
- package/src/core/plugin/params/compile.ts +41 -0
- package/src/core/plugin/params/declaration.ts +394 -0
- package/src/core/plugin/params/input.ts +281 -0
- package/src/core/plugin/params/reply.ts +59 -0
- package/src/core/plugin/params/type-level.ts +75 -0
- package/src/core/plugin/params/types.ts +55 -0
- package/src/core/plugin/params/values.ts +150 -0
- package/src/core/web/api.ts +33 -759
- package/src/core/web/routes/actions.ts +43 -0
- package/src/core/web/routes/config.ts +42 -0
- package/src/core/web/routes/kv.ts +122 -0
- package/src/core/web/routes/plugins.ts +151 -0
- package/src/core/web/routes/sessions.ts +188 -0
- package/src/core/web/routes/shared.ts +42 -0
- package/src/core/web/routes/status.ts +191 -0
- package/src/core/web/server/auth.ts +126 -0
- package/src/core/web/server/csp.ts +33 -0
- package/src/core/web/server/request.ts +38 -0
- package/src/core/web/server/security.ts +14 -0
- package/src/core/web/server/sse.ts +74 -0
- package/src/core/web/server/static.ts +112 -0
- package/src/core/web/server.ts +41 -383
package/src/core/web/api.ts
CHANGED
|
@@ -1,764 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { Database } from '../storage/database.js'
|
|
3
|
-
import { SqlitePluginKV } from '../plugin/kv.js'
|
|
4
|
-
import { stripPluginEnvelope } from '../plugin/host.js'
|
|
1
|
+
import { HttpError } from './http.js'
|
|
5
2
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from '
|
|
11
|
-
import { logLevels, type LogLevel } from '../logging/logger.js'
|
|
12
|
-
import type { AuthSession } from './auth.js'
|
|
13
|
-
import type { FeedEventType } from './event-feed.js'
|
|
14
|
-
import { maskConfig } from './config-store.js'
|
|
15
|
-
import type { WebUiDeps, WebUiRuntime } from './types.js'
|
|
3
|
+
handleStatus,
|
|
4
|
+
handleCommands,
|
|
5
|
+
handleLogsList,
|
|
6
|
+
handleEventsList,
|
|
7
|
+
} from './routes/status.js'
|
|
16
8
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from './
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
nickname: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const loginCache = new WeakMap<
|
|
46
|
-
WebUiRuntime,
|
|
47
|
-
{ at: number; value: LoginValue | null }
|
|
48
|
-
>()
|
|
49
|
-
|
|
50
|
-
function requireRuntime(deps: WebUiDeps): WebUiRuntime {
|
|
51
|
-
const runtime = deps.getRuntime()
|
|
52
|
-
|
|
53
|
-
if (!runtime) throw new HttpError(503, '运行时不可用,请稍后再试')
|
|
54
|
-
|
|
55
|
-
return runtime
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function countKvPlugins(database: Database): number {
|
|
59
|
-
const row = database.connection
|
|
60
|
-
.prepare('SELECT COUNT(DISTINCT plugin_id) AS count FROM plugin_kv')
|
|
61
|
-
.get() as { count: number } | undefined
|
|
62
|
-
|
|
63
|
-
return row?.count ?? 0
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function loginInfo(runtime: WebUiRuntime): Promise<LoginValue | null> {
|
|
67
|
-
if (!runtime.kernel.isConnected) return null
|
|
68
|
-
|
|
69
|
-
const cached = loginCache.get(runtime)
|
|
70
|
-
|
|
71
|
-
if (cached && Date.now() - cached.at < 10_000) return cached.value
|
|
72
|
-
|
|
73
|
-
let value: LoginValue | null = null
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const info = await withTimeout(runtime.api.getLoginInfo(), 2_000)
|
|
77
|
-
|
|
78
|
-
if (typeof info.user_id === 'number') {
|
|
79
|
-
value = {
|
|
80
|
-
userId: info.user_id,
|
|
81
|
-
nickname: info.nickname || String(info.user_id),
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
value = null
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
loginCache.set(runtime, { at: Date.now(), value })
|
|
89
|
-
|
|
90
|
-
return value
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface RuntimeStatus {
|
|
94
|
-
generation: number
|
|
95
|
-
state: string
|
|
96
|
-
uptimeMs: number
|
|
97
|
-
connected: boolean
|
|
98
|
-
login: LoginValue | null
|
|
99
|
-
counts: {
|
|
100
|
-
plugins: number
|
|
101
|
-
loadedPlugins: number
|
|
102
|
-
enabledPlugins: number
|
|
103
|
-
sessions: number
|
|
104
|
-
kvPlugins: number
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function runtimeStatus(runtime: WebUiRuntime): Promise<RuntimeStatus> {
|
|
109
|
-
const statuses = runtime.kernel.listPlugins()
|
|
110
|
-
const sessions = await new SqliteSessionStore(runtime.database).count()
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
generation: runtime.generation,
|
|
114
|
-
state: runtime.kernel.state,
|
|
115
|
-
uptimeMs: runtime.kernel.uptimeMs,
|
|
116
|
-
connected: runtime.kernel.isConnected,
|
|
117
|
-
login: await loginInfo(runtime),
|
|
118
|
-
counts: {
|
|
119
|
-
plugins: statuses.length,
|
|
120
|
-
loadedPlugins: statuses.filter((entry) => entry.loaded).length,
|
|
121
|
-
enabledPlugins: statuses.filter((entry) => entry.enabled).length,
|
|
122
|
-
sessions,
|
|
123
|
-
kvPlugins: countKvPlugins(runtime.database),
|
|
124
|
-
},
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function pathsInfo(deps: WebUiDeps) {
|
|
129
|
-
return {
|
|
130
|
-
config: deps.paths.configPath,
|
|
131
|
-
data: deps.paths.dataDir,
|
|
132
|
-
plugins: deps.paths.pluginDir,
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function assertLevel(value: string): LogLevel {
|
|
137
|
-
if (!(logLevels as readonly string[]).includes(value)) {
|
|
138
|
-
throw new HttpError(400, `invalid log level: ${value}`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return value as LogLevel
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const FEED_TYPES: readonly FeedEventType[] = [
|
|
145
|
-
'message',
|
|
146
|
-
'notice',
|
|
147
|
-
'request',
|
|
148
|
-
'meta',
|
|
149
|
-
'other',
|
|
150
|
-
]
|
|
151
|
-
|
|
152
|
-
function assertFeedType(value: string): FeedEventType {
|
|
153
|
-
if (!FEED_TYPES.includes(value as FeedEventType)) {
|
|
154
|
-
throw new HttpError(400, `invalid event type: ${value}`)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return value as FeedEventType
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function clampInt(
|
|
161
|
-
value: string | null,
|
|
162
|
-
fallback: number,
|
|
163
|
-
min: number,
|
|
164
|
-
max: number
|
|
165
|
-
): number {
|
|
166
|
-
if (value === null) return fallback
|
|
167
|
-
|
|
168
|
-
const parsed = Number.parseInt(value, 10)
|
|
169
|
-
|
|
170
|
-
if (!Number.isFinite(parsed)) return fallback
|
|
171
|
-
|
|
172
|
-
return Math.min(Math.max(parsed, min), max)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
176
|
-
return isRecord(value) ? value : {}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function parseSessionMessages(value: unknown): SessionMessage[] {
|
|
180
|
-
if (!Array.isArray(value)) {
|
|
181
|
-
throw new HttpError(400, 'messages must be an array')
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (value.length > MAX_SESSION_MESSAGES) {
|
|
185
|
-
throw new HttpError(400, 'messages too long')
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return value.map((item, index) => {
|
|
189
|
-
if (!isRecord(item)) {
|
|
190
|
-
throw new HttpError(400, `invalid message at index ${index}`)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const role = item['role']
|
|
194
|
-
const content = item['content']
|
|
195
|
-
const at = item['at']
|
|
196
|
-
|
|
197
|
-
if (role !== 'system' && role !== 'user' && role !== 'assistant') {
|
|
198
|
-
throw new HttpError(400, `invalid message role at index ${index}`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (typeof content !== 'string') {
|
|
202
|
-
throw new HttpError(
|
|
203
|
-
400,
|
|
204
|
-
`invalid message content at index ${index}`
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (typeof at !== 'number' || !Number.isFinite(at)) {
|
|
209
|
-
throw new HttpError(400, `invalid message time at index ${index}`)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return { role: role as SessionRole, content, at }
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function clearableString(
|
|
217
|
-
value: unknown,
|
|
218
|
-
fallback: string | undefined
|
|
219
|
-
): string | undefined {
|
|
220
|
-
if (value === null) return undefined
|
|
221
|
-
if (value === undefined) return fallback
|
|
222
|
-
if (typeof value === 'string' && value.trim().length > 0) {
|
|
223
|
-
return value
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return undefined
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function handleStatus(ctx: ApiContext): Promise<void> {
|
|
230
|
-
const { deps, res } = ctx
|
|
231
|
-
const runtime = deps.getRuntime()
|
|
232
|
-
const webui = {
|
|
233
|
-
listening: deps.getListening(),
|
|
234
|
-
requiresProcessRestart: deps.getRequiresProcessRestart(),
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!runtime) {
|
|
238
|
-
sendData(res, 200, {
|
|
239
|
-
restarting: deps.isRestarting(),
|
|
240
|
-
startupError: deps.getStartupError(),
|
|
241
|
-
runtime: null,
|
|
242
|
-
webui,
|
|
243
|
-
version: deps.version,
|
|
244
|
-
paths: pathsInfo(deps),
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
sendData(res, 200, {
|
|
251
|
-
restarting: deps.isRestarting(),
|
|
252
|
-
startupError: deps.getStartupError(),
|
|
253
|
-
runtime: await runtimeStatus(runtime),
|
|
254
|
-
webui,
|
|
255
|
-
version: deps.version,
|
|
256
|
-
paths: pathsInfo(deps),
|
|
257
|
-
})
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function handleLogsList(ctx: ApiContext): Promise<void> {
|
|
261
|
-
const level = ctx.search.get('level')
|
|
262
|
-
|
|
263
|
-
if (level) assertLevel(level)
|
|
264
|
-
|
|
265
|
-
const scope = ctx.search.get('scope') ?? undefined
|
|
266
|
-
const query = ctx.search.get('query') ?? undefined
|
|
267
|
-
const limit = clampInt(ctx.search.get('limit'), 200, 1, 1_000)
|
|
268
|
-
|
|
269
|
-
sendData(
|
|
270
|
-
ctx.res,
|
|
271
|
-
200,
|
|
272
|
-
ctx.deps.logs.list({
|
|
273
|
-
...(level ? { level: assertLevel(level) } : {}),
|
|
274
|
-
scope,
|
|
275
|
-
query,
|
|
276
|
-
limit,
|
|
277
|
-
})
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function handleEventsList(ctx: ApiContext): Promise<void> {
|
|
282
|
-
const type = ctx.search.get('type')
|
|
283
|
-
|
|
284
|
-
if (type) assertFeedType(type)
|
|
285
|
-
|
|
286
|
-
const limit = clampInt(ctx.search.get('limit'), 100, 1, 500)
|
|
287
|
-
|
|
288
|
-
sendData(
|
|
289
|
-
ctx.res,
|
|
290
|
-
200,
|
|
291
|
-
ctx.deps.events.list({
|
|
292
|
-
...(type ? { type: assertFeedType(type) } : {}),
|
|
293
|
-
limit,
|
|
294
|
-
})
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function handleSessionList(ctx: ApiContext): Promise<void> {
|
|
299
|
-
const runtime = requireRuntime(ctx.deps)
|
|
300
|
-
const store = new SqliteSessionStore(runtime.database)
|
|
301
|
-
const limit = clampInt(ctx.search.get('limit'), 50, 1, 200)
|
|
302
|
-
const offset = clampInt(ctx.search.get('offset'), 0, 0, 10_000_000)
|
|
303
|
-
const query = ctx.search.get('query')?.slice(0, 200) || undefined
|
|
304
|
-
const items = await store.list({ limit, offset, query })
|
|
305
|
-
const total = await store.count(query)
|
|
306
|
-
|
|
307
|
-
sendData(ctx.res, 200, { items, total, limit, offset })
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function sessionIdOf(ctx: ApiContext): string {
|
|
311
|
-
const id = ctx.segments[2]
|
|
312
|
-
|
|
313
|
-
if (!id) throw new HttpError(404, 'session not found')
|
|
314
|
-
|
|
315
|
-
return id
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function requireSessionStore(ctx: ApiContext): SqliteSessionStore {
|
|
319
|
-
const runtime = requireRuntime(ctx.deps)
|
|
320
|
-
|
|
321
|
-
return new SqliteSessionStore(runtime.database)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function sessionDetail(ctx: ApiContext): Promise<SessionState> {
|
|
325
|
-
const id = sessionIdOf(ctx)
|
|
326
|
-
const state = await requireSessionStore(ctx).get(id)
|
|
327
|
-
|
|
328
|
-
if (!state) throw new HttpError(404, 'session not found')
|
|
329
|
-
|
|
330
|
-
return state
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function handleSessionGet(ctx: ApiContext): Promise<void> {
|
|
334
|
-
sendData(ctx.res, 200, await sessionDetail(ctx))
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function handleSessionPut(ctx: ApiContext): Promise<void> {
|
|
338
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
339
|
-
await sessionDetail(ctx)
|
|
340
|
-
|
|
341
|
-
const messages =
|
|
342
|
-
body['messages'] === undefined
|
|
343
|
-
? undefined
|
|
344
|
-
: parseSessionMessages(body['messages'])
|
|
345
|
-
|
|
346
|
-
const baseline = await sessionDetail(ctx)
|
|
347
|
-
const state: SessionState = {
|
|
348
|
-
sessionId: sessionIdOf(ctx),
|
|
349
|
-
provider: clearableString(body['provider'], baseline.provider),
|
|
350
|
-
model: clearableString(body['model'], baseline.model),
|
|
351
|
-
persona: clearableString(body['persona'], baseline.persona),
|
|
352
|
-
messages: messages ?? baseline.messages,
|
|
353
|
-
updatedAt: Date.now(),
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
await requireSessionStore(ctx).save(state)
|
|
357
|
-
sendData(ctx.res, 200, state)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function handleSessionMessages(ctx: ApiContext): Promise<void> {
|
|
361
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
362
|
-
const role = body['role']
|
|
363
|
-
const content = body['content']
|
|
364
|
-
|
|
365
|
-
if (role !== 'system' && role !== 'user' && role !== 'assistant') {
|
|
366
|
-
throw new HttpError(400, 'invalid message role')
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
typeof content !== 'string' ||
|
|
371
|
-
content.length === 0 ||
|
|
372
|
-
content.length > 100_000
|
|
373
|
-
) {
|
|
374
|
-
throw new HttpError(400, 'invalid message content')
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const state = await sessionDetail(ctx)
|
|
378
|
-
const message = { role: role as SessionRole, content, at: Date.now() }
|
|
379
|
-
const next: SessionState = {
|
|
380
|
-
...state,
|
|
381
|
-
messages: [...state.messages, message],
|
|
382
|
-
updatedAt: Date.now(),
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
await requireSessionStore(ctx).save(next)
|
|
386
|
-
sendData(ctx.res, 200, next)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function handleSessionSend(ctx: ApiContext): Promise<void> {
|
|
390
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
391
|
-
const id = sessionIdOf(ctx)
|
|
392
|
-
const message = body['message']
|
|
393
|
-
const autoEscape = body['autoEscape']
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
typeof message !== 'string' ||
|
|
397
|
-
message.length === 0 ||
|
|
398
|
-
message.length > 20_000
|
|
399
|
-
) {
|
|
400
|
-
throw new HttpError(400, 'invalid message')
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const runtime = requireRuntime(ctx.deps)
|
|
404
|
-
|
|
405
|
-
await sessionDetail(ctx)
|
|
406
|
-
|
|
407
|
-
const result = await runtime.api.send(
|
|
408
|
-
id,
|
|
409
|
-
message,
|
|
410
|
-
typeof autoEscape === 'boolean' ? { autoEscape } : undefined
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
sendData(ctx.res, 200, { result: result ?? null })
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async function handleSessionDelete(ctx: ApiContext): Promise<void> {
|
|
417
|
-
await requireSessionStore(ctx).clear(sessionIdOf(ctx))
|
|
418
|
-
sendData(ctx.res, 200, { cleared: true })
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async function handlePluginsList(ctx: ApiContext): Promise<void> {
|
|
422
|
-
const runtime = requireRuntime(ctx.deps)
|
|
423
|
-
const statuses = new Map(
|
|
424
|
-
runtime.kernel.listPlugins().map((entry) => [entry.id, entry])
|
|
425
|
-
)
|
|
426
|
-
const plugins = runtime.plugins.map((definition) => {
|
|
427
|
-
const id = definition.manifest.id
|
|
428
|
-
const status = statuses.get(id)
|
|
429
|
-
const envelope = asRecord(runtime.config.plugins[id]) as Record<
|
|
430
|
-
string,
|
|
431
|
-
unknown
|
|
432
|
-
>
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
id,
|
|
436
|
-
version: definition.manifest.version,
|
|
437
|
-
description: definition.manifest.description,
|
|
438
|
-
dependencies: definition.manifest.dependencies ?? [],
|
|
439
|
-
defaultEnabled: definition.manifest.defaultEnabled ?? false,
|
|
440
|
-
enabled: status?.enabled ?? false,
|
|
441
|
-
loaded: status?.loaded ?? false,
|
|
442
|
-
config: envelope,
|
|
443
|
-
options: stripPluginEnvelope(envelope as never) as Record<
|
|
444
|
-
string,
|
|
445
|
-
unknown
|
|
446
|
-
>,
|
|
447
|
-
}
|
|
448
|
-
})
|
|
449
|
-
const installed = runtime.installer.enabled
|
|
450
|
-
? await runtime.installer.list()
|
|
451
|
-
: []
|
|
452
|
-
|
|
453
|
-
sendData(ctx.res, 200, {
|
|
454
|
-
plugins,
|
|
455
|
-
installed,
|
|
456
|
-
installerEnabled: runtime.installer.enabled,
|
|
457
|
-
configHash: (await ctx.deps.readConfig()).hash,
|
|
458
|
-
})
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const ENVELOPE_KEYS = new Set(['enabled', 'private', 'groups'])
|
|
462
|
-
|
|
463
|
-
async function handlePluginConfigPut(ctx: ApiContext): Promise<void> {
|
|
464
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
465
|
-
const id = ctx.segments[2]
|
|
466
|
-
const document = await ctx.deps.readConfig()
|
|
467
|
-
|
|
468
|
-
if (id === undefined) throw new HttpError(404, 'unknown plugin')
|
|
469
|
-
|
|
470
|
-
if (!isRecord(document.config.plugins[id])) {
|
|
471
|
-
throw new HttpError(404, `unknown plugin: ${id}`)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const existing = asRecord(document.config.plugins[id]) as unknown as Record<
|
|
475
|
-
string,
|
|
476
|
-
unknown
|
|
477
|
-
>
|
|
478
|
-
const next = { ...existing }
|
|
479
|
-
|
|
480
|
-
if (typeof body['enabled'] === 'boolean') {
|
|
481
|
-
next['enabled'] = body['enabled']
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (typeof body['private'] === 'boolean') {
|
|
485
|
-
next['private'] = body['private']
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (body['groups'] !== undefined) next['groups'] = body['groups']
|
|
489
|
-
|
|
490
|
-
if (body['options'] !== undefined) {
|
|
491
|
-
if (!isRecord(body['options'])) {
|
|
492
|
-
throw new HttpError(400, 'options must be an object')
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
for (const key of Object.keys(next)) {
|
|
496
|
-
if (!ENVELOPE_KEYS.has(key)) delete next[key]
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
for (const [key, value] of Object.entries(body['options'])) {
|
|
500
|
-
if (!ENVELOPE_KEYS.has(key)) next[key] = value
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const config = {
|
|
505
|
-
...document.config,
|
|
506
|
-
plugins: {
|
|
507
|
-
...document.config.plugins,
|
|
508
|
-
[id]: next,
|
|
509
|
-
},
|
|
510
|
-
}
|
|
511
|
-
const baseHash = body['baseHash']
|
|
512
|
-
|
|
513
|
-
sendData(
|
|
514
|
-
ctx.res,
|
|
515
|
-
200,
|
|
516
|
-
await ctx.deps.applyConfig(
|
|
517
|
-
config,
|
|
518
|
-
typeof baseHash === 'string' ? baseHash : undefined
|
|
519
|
-
)
|
|
520
|
-
)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
async function handleInstallerAction(ctx: ApiContext): Promise<void> {
|
|
524
|
-
const runtime = requireRuntime(ctx.deps)
|
|
525
|
-
|
|
526
|
-
if (!runtime.installer.enabled) {
|
|
527
|
-
throw new HttpError(403, '插件安装器未启用')
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
531
|
-
const target = typeof body['target'] === 'string' ? body['target'] : ''
|
|
532
|
-
const spec = typeof body['spec'] === 'string' ? body['spec'] : ''
|
|
533
|
-
|
|
534
|
-
if (ctx.pathname.endsWith('/install')) {
|
|
535
|
-
if (!spec) throw new HttpError(400, 'spec is required')
|
|
536
|
-
|
|
537
|
-
sendData(
|
|
538
|
-
ctx.res,
|
|
539
|
-
200,
|
|
540
|
-
await ctx.deps.runExclusive(() => runtime.installer.install(spec))
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
return
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (ctx.pathname.endsWith('/update')) {
|
|
547
|
-
sendData(
|
|
548
|
-
ctx.res,
|
|
549
|
-
200,
|
|
550
|
-
await ctx.deps.runExclusive(() =>
|
|
551
|
-
runtime.installer.update(target || undefined)
|
|
552
|
-
)
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
return
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
sendData(
|
|
559
|
-
ctx.res,
|
|
560
|
-
200,
|
|
561
|
-
await ctx.deps.runExclusive(async () =>
|
|
562
|
-
runtime.installer.remove(target, undefined, {
|
|
563
|
-
force: body['force'] === true,
|
|
564
|
-
})
|
|
565
|
-
)
|
|
566
|
-
)
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function handleConfigGet(ctx: ApiContext): Promise<void> {
|
|
570
|
-
const document = await ctx.deps.readConfig()
|
|
571
|
-
const masked = maskConfig(document.config)
|
|
572
|
-
|
|
573
|
-
sendData(ctx.res, 200, {
|
|
574
|
-
config: masked.config,
|
|
575
|
-
meta: {
|
|
576
|
-
path: document.path,
|
|
577
|
-
hash: document.hash,
|
|
578
|
-
accessTokenSet: masked.accessTokenSet,
|
|
579
|
-
webuiPasswordSet: masked.webuiPasswordSet,
|
|
580
|
-
},
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
async function handleConfigValidate(ctx: ApiContext): Promise<void> {
|
|
585
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
586
|
-
const result = await ctx.deps.validateConfig(body['config'])
|
|
587
|
-
|
|
588
|
-
sendData(ctx.res, 200, {
|
|
589
|
-
ok: result.ok,
|
|
590
|
-
issues: result.issues,
|
|
591
|
-
})
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function handleConfigPut(ctx: ApiContext): Promise<void> {
|
|
595
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
596
|
-
const baseHash = body['baseHash']
|
|
597
|
-
|
|
598
|
-
sendData(
|
|
599
|
-
ctx.res,
|
|
600
|
-
200,
|
|
601
|
-
await ctx.deps.applyConfig(
|
|
602
|
-
body['config'],
|
|
603
|
-
typeof baseHash === 'string' ? baseHash : undefined
|
|
604
|
-
)
|
|
605
|
-
)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function assertPluginId(value: string): string {
|
|
609
|
-
if (!/^\w[\w.-]*$/.test(value)) {
|
|
610
|
-
throw new HttpError(400, `invalid plugin id: ${value}`)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return value
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function assertKey(value: string): string {
|
|
617
|
-
if (value.length === 0 || value.length > 512) {
|
|
618
|
-
throw new HttpError(400, 'invalid key length')
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
return value
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function parseKvValue(value: string): unknown {
|
|
625
|
-
try {
|
|
626
|
-
return JSON.parse(value) as unknown
|
|
627
|
-
} catch {
|
|
628
|
-
return value
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
interface KvRow {
|
|
633
|
-
key: string
|
|
634
|
-
value: string
|
|
635
|
-
updated_at: number
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function handleKvPlugins(ctx: ApiContext): Promise<void> {
|
|
639
|
-
const runtime = requireRuntime(ctx.deps)
|
|
640
|
-
const rows = runtime.database.connection
|
|
641
|
-
.prepare(
|
|
642
|
-
'SELECT plugin_id, COUNT(*) AS count FROM plugin_kv GROUP BY plugin_id ORDER BY plugin_id'
|
|
643
|
-
)
|
|
644
|
-
.all() as Array<{ plugin_id: string; count: number }>
|
|
645
|
-
|
|
646
|
-
sendData(
|
|
647
|
-
ctx.res,
|
|
648
|
-
200,
|
|
649
|
-
rows.map((row) => ({ pluginId: row.plugin_id, count: row.count }))
|
|
650
|
-
)
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async function handleKvList(ctx: ApiContext): Promise<void> {
|
|
654
|
-
const runtime = requireRuntime(ctx.deps)
|
|
655
|
-
const pluginId = assertPluginId(ctx.search.get('pluginId') ?? '')
|
|
656
|
-
const prefix = ctx.search.get('prefix') ?? ''
|
|
657
|
-
const limit = clampInt(ctx.search.get('limit'), 100, 1, 500)
|
|
658
|
-
const offset = clampInt(ctx.search.get('offset'), 0, 0, 10_000_000)
|
|
659
|
-
const pattern =
|
|
660
|
-
prefix === ''
|
|
661
|
-
? '%'
|
|
662
|
-
: `${prefix.replace(/[%_]/g, (match) => `\\${match}`)}%`
|
|
663
|
-
const rows = runtime.database.connection
|
|
664
|
-
.prepare(
|
|
665
|
-
`SELECT key, value, updated_at FROM plugin_kv
|
|
666
|
-
WHERE plugin_id = ? AND key LIKE ? ESCAPE '\\'
|
|
667
|
-
ORDER BY key
|
|
668
|
-
LIMIT ? OFFSET ?`
|
|
669
|
-
)
|
|
670
|
-
.all(pluginId, pattern, limit, offset) as unknown as KvRow[]
|
|
671
|
-
const totalRow = runtime.database.connection
|
|
672
|
-
.prepare(
|
|
673
|
-
`SELECT COUNT(*) AS count FROM plugin_kv
|
|
674
|
-
WHERE plugin_id = ? AND key LIKE ? ESCAPE '\\'`
|
|
675
|
-
)
|
|
676
|
-
.get(pluginId, pattern) as { count: number } | undefined
|
|
677
|
-
|
|
678
|
-
sendData(ctx.res, 200, {
|
|
679
|
-
items: rows.map((row) => ({
|
|
680
|
-
pluginId,
|
|
681
|
-
key: row.key,
|
|
682
|
-
value: parseKvValue(row.value),
|
|
683
|
-
updatedAt: row.updated_at,
|
|
684
|
-
})),
|
|
685
|
-
total: totalRow?.count ?? 0,
|
|
686
|
-
limit,
|
|
687
|
-
offset,
|
|
688
|
-
})
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async function handleKvPut(ctx: ApiContext): Promise<void> {
|
|
692
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
693
|
-
const runtime = requireRuntime(ctx.deps)
|
|
694
|
-
const pluginId = assertPluginId(
|
|
695
|
-
typeof body['pluginId'] === 'string' ? body['pluginId'] : ''
|
|
696
|
-
)
|
|
697
|
-
const key = assertKey(typeof body['key'] === 'string' ? body['key'] : '')
|
|
698
|
-
|
|
699
|
-
if (typeof body['value'] === 'function') {
|
|
700
|
-
throw new HttpError(400, 'invalid value')
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
await new SqlitePluginKV(runtime.database, pluginId).set(
|
|
704
|
-
key,
|
|
705
|
-
body['value'] as never
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
sendData(ctx.res, 200, { ok: true })
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
async function handleKvDelete(ctx: ApiContext): Promise<void> {
|
|
712
|
-
const runtime = requireRuntime(ctx.deps)
|
|
713
|
-
const pluginId = assertPluginId(ctx.search.get('pluginId') ?? '')
|
|
714
|
-
const key = assertKey(ctx.search.get('key') ?? '')
|
|
715
|
-
const deleted = await new SqlitePluginKV(runtime.database, pluginId).delete(
|
|
716
|
-
key
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
sendData(ctx.res, 200, { ok: deleted })
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async function handleActions(ctx: ApiContext): Promise<void> {
|
|
723
|
-
const body = asRecord(await readJsonBody(ctx.req))
|
|
724
|
-
const action = body['action']
|
|
725
|
-
|
|
726
|
-
if (
|
|
727
|
-
typeof action !== 'string' ||
|
|
728
|
-
action.length === 0 ||
|
|
729
|
-
action.length > 128
|
|
730
|
-
) {
|
|
731
|
-
throw new HttpError(400, 'action is required')
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const runtime = requireRuntime(ctx.deps)
|
|
735
|
-
const params = body['params'] ?? undefined
|
|
736
|
-
const result = await runtime.api.call(action as never, params as never)
|
|
737
|
-
|
|
738
|
-
sendData(ctx.res, 200, { result: result ?? null })
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
async function handleRuntimeCommand(ctx: ApiContext): Promise<void> {
|
|
742
|
-
if (ctx.pathname.endsWith('/restart')) {
|
|
743
|
-
sendData(ctx.res, 200, {
|
|
744
|
-
ok: await ctx.deps.restart(),
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
return
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (ctx.pathname.endsWith('/pause')) {
|
|
751
|
-
ctx.deps.pause()
|
|
752
|
-
sendData(ctx.res, 200, { ok: true })
|
|
753
|
-
|
|
754
|
-
return
|
|
755
|
-
}
|
|
9
|
+
handleSessionDelete,
|
|
10
|
+
handleSessionGet,
|
|
11
|
+
handleSessionList,
|
|
12
|
+
handleSessionMessages,
|
|
13
|
+
handleSessionPut,
|
|
14
|
+
handleSessionSend,
|
|
15
|
+
} from './routes/sessions.js'
|
|
16
|
+
import {
|
|
17
|
+
handleInstallerAction,
|
|
18
|
+
handlePluginConfigPut,
|
|
19
|
+
handlePluginsList,
|
|
20
|
+
} from './routes/plugins.js'
|
|
21
|
+
import {
|
|
22
|
+
handleConfigGet,
|
|
23
|
+
handleConfigPut,
|
|
24
|
+
handleConfigValidate,
|
|
25
|
+
} from './routes/config.js'
|
|
26
|
+
import {
|
|
27
|
+
handleKvDelete,
|
|
28
|
+
handleKvList,
|
|
29
|
+
handleKvPlugins,
|
|
30
|
+
handleKvPut,
|
|
31
|
+
} from './routes/kv.js'
|
|
32
|
+
import { handleActions, handleRuntimeCommand } from './routes/actions.js'
|
|
33
|
+
import type { ApiContext } from './routes/shared.js'
|
|
756
34
|
|
|
757
|
-
|
|
758
|
-
ctx.deps.resume()
|
|
759
|
-
sendData(ctx.res, 200, { ok: true })
|
|
760
|
-
}
|
|
761
|
-
}
|
|
35
|
+
export type { ApiContext } from './routes/shared.js'
|
|
762
36
|
|
|
763
37
|
export async function handleApiRoute(ctx: ApiContext): Promise<boolean> {
|
|
764
38
|
const { method, pathname } = ctx
|
|
@@ -770,7 +44,7 @@ export async function handleApiRoute(ctx: ApiContext): Promise<boolean> {
|
|
|
770
44
|
}
|
|
771
45
|
|
|
772
46
|
if (method === 'GET' && pathname === '/api/commands') {
|
|
773
|
-
|
|
47
|
+
await handleCommands(ctx)
|
|
774
48
|
|
|
775
49
|
return true
|
|
776
50
|
}
|