@jsbot-tellus/core 2.9.0 → 2.10.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/dist/app/runtime.d.ts +1 -0
- package/dist/app/runtime.d.ts.map +1 -1
- package/dist/app/runtime.js +1 -0
- package/dist/app/runtime.js.map +1 -1
- package/dist/app.js +6 -1
- package/dist/app.js.map +1 -1
- package/dist/core/plugin/params/declaration.d.ts +2 -2
- package/dist/core/plugin/params/declaration.d.ts.map +1 -1
- package/dist/core/plugin/params/declaration.js +189 -24
- package/dist/core/plugin/params/declaration.js.map +1 -1
- package/dist/core/plugin/params/input.d.ts.map +1 -1
- package/dist/core/plugin/params/input.js +35 -12
- package/dist/core/plugin/params/input.js.map +1 -1
- package/dist/core/plugin/params/type-level.d.ts +13 -3
- package/dist/core/plugin/params/type-level.d.ts.map +1 -1
- package/dist/core/plugin/params/types.d.ts +13 -2
- package/dist/core/plugin/params/types.d.ts.map +1 -1
- package/dist/core/plugin/params/types.js +22 -0
- package/dist/core/plugin/params/types.js.map +1 -1
- package/dist/core/web/config-store.d.ts.map +1 -1
- package/dist/core/web/config-store.js +14 -10
- package/dist/core/web/config-store.js.map +1 -1
- package/dist/core/web/routes/plugins.d.ts.map +1 -1
- package/dist/core/web/routes/plugins.js +24 -13
- package/dist/core/web/routes/plugins.js.map +1 -1
- package/dist/core/web/types.d.ts +6 -0
- package/dist/core/web/types.d.ts.map +1 -1
- package/docs/plugin-api.md +27 -3
- package/docs/webui.md +1 -1
- package/package.json +1 -1
- package/src/app/runtime.ts +2 -0
- package/src/app.ts +6 -1
- package/src/core/plugin/params/declaration.ts +269 -31
- package/src/core/plugin/params/input.ts +63 -12
- package/src/core/plugin/params/type-level.ts +114 -6
- package/src/core/plugin/params/types.ts +46 -2
- package/src/core/web/config-store.ts +18 -12
- package/src/core/web/routes/plugins.ts +30 -19
- package/src/core/web/types.ts +7 -0
|
@@ -35,14 +35,26 @@ export interface ParamLength {
|
|
|
35
35
|
max?: number | undefined
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface ParamTypeCandidate {
|
|
39
|
+
kind: 'type'
|
|
40
|
+
type: CommandParamType
|
|
41
|
+
length?: ParamLength | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ParamLiteralCandidate {
|
|
45
|
+
kind: 'literal'
|
|
46
|
+
value: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ParamCandidate = ParamTypeCandidate | ParamLiteralCandidate
|
|
50
|
+
|
|
38
51
|
export interface ParamSpec {
|
|
39
52
|
name: string
|
|
40
|
-
|
|
53
|
+
candidates: ParamCandidate[]
|
|
41
54
|
required: boolean
|
|
42
55
|
rest: boolean
|
|
43
56
|
hasDefault: boolean
|
|
44
57
|
defaultValue?: unknown
|
|
45
|
-
length?: ParamLength | undefined
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
export interface InputToken {
|
|
@@ -53,3 +65,35 @@ export interface InputToken {
|
|
|
53
65
|
export function declarationError(message: string): Error {
|
|
54
66
|
return new Error(`参数声明错误:${message}`)
|
|
55
67
|
}
|
|
68
|
+
|
|
69
|
+
export function candidateLabel(candidate: ParamCandidate): string {
|
|
70
|
+
if (candidate.kind === 'literal') {
|
|
71
|
+
const escaped = candidate.value
|
|
72
|
+
.replace(/\\/g, '\\\\')
|
|
73
|
+
.replace(/'/g, "\\'")
|
|
74
|
+
|
|
75
|
+
return `'${escaped}'`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const length = candidate.length
|
|
79
|
+
|
|
80
|
+
if (length === undefined) {
|
|
81
|
+
return candidate.type
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return `${candidate.type}(${length.min ?? ''}..${length.max ?? ''})`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function joinCandidateLabels(
|
|
88
|
+
candidates: readonly ParamCandidate[]
|
|
89
|
+
): string {
|
|
90
|
+
const labels = candidates.map(candidateLabel)
|
|
91
|
+
|
|
92
|
+
if (labels.length === 0) return ''
|
|
93
|
+
|
|
94
|
+
if (labels.length === 1) return labels[0] ?? ''
|
|
95
|
+
|
|
96
|
+
const last = labels[labels.length - 1] ?? ''
|
|
97
|
+
|
|
98
|
+
return `${labels.slice(0, -1).join('、')} 或 ${last}`
|
|
99
|
+
}
|
|
@@ -128,27 +128,28 @@ function validatePlugins(
|
|
|
128
128
|
definitions.map((definition) => [definition.manifest.id, definition])
|
|
129
129
|
)
|
|
130
130
|
|
|
131
|
-
for (const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!definition) {
|
|
131
|
+
for (const id of Object.keys(config.plugins)) {
|
|
132
|
+
if (!byId.has(id)) {
|
|
135
133
|
issues.push({
|
|
136
134
|
path: `plugins.${id}`,
|
|
137
135
|
message: `unknown plugin id: ${id}`,
|
|
138
136
|
})
|
|
139
|
-
|
|
140
|
-
continue
|
|
141
137
|
}
|
|
138
|
+
}
|
|
142
139
|
|
|
140
|
+
// Validate every enabled plugin, not only the ones already present in the
|
|
141
|
+
// config file. A newly installed plugin has no envelope yet; validating it
|
|
142
|
+
// here surfaces missing required options before the runtime is restarted.
|
|
143
|
+
for (const definition of definitions) {
|
|
144
|
+
const id = definition.manifest.id
|
|
145
|
+
const envelope = config.plugins[id]
|
|
143
146
|
const enabled =
|
|
144
|
-
|
|
145
|
-
definition.manifest.defaultEnabled ??
|
|
146
|
-
false
|
|
147
|
+
envelope?.enabled ?? definition.manifest.defaultEnabled ?? false
|
|
147
148
|
|
|
148
149
|
if (!enabled) continue
|
|
149
150
|
|
|
150
151
|
const result = definition.manifest.configSchema.safeParse(
|
|
151
|
-
stripPluginEnvelope(envelope as PluginEnvelope)
|
|
152
|
+
stripPluginEnvelope(envelope ?? ({} as PluginEnvelope))
|
|
152
153
|
)
|
|
153
154
|
|
|
154
155
|
if (!result.success) {
|
|
@@ -255,9 +256,14 @@ export class ConfigStore {
|
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
const pluginIssues = validatePlugins(
|
|
260
|
+
parsed.data,
|
|
261
|
+
this.options.getPlugins()
|
|
262
|
+
)
|
|
263
|
+
|
|
258
264
|
return {
|
|
259
|
-
ok:
|
|
260
|
-
issues:
|
|
265
|
+
ok: pluginIssues.length === 0,
|
|
266
|
+
issues: pluginIssues,
|
|
261
267
|
config: parsed.data,
|
|
262
268
|
}
|
|
263
269
|
}
|
|
@@ -41,14 +41,19 @@ function pluginJsonSchema(
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export async function handlePluginsList(ctx: ApiContext): Promise<void> {
|
|
44
|
-
const runtime =
|
|
44
|
+
const runtime = ctx.deps.getRuntime()
|
|
45
|
+
// The kernel can fail to start (for example when a plugin has no config
|
|
46
|
+
// yet). Keep serving the last loaded definitions so the WebUI can still
|
|
47
|
+
// edit plugin configs and recover without touching the file by hand.
|
|
48
|
+
const definitions = runtime?.plugins ?? ctx.deps.getPlugins()
|
|
45
49
|
const statuses = new Map(
|
|
46
|
-
runtime
|
|
50
|
+
(runtime?.kernel.listPlugins() ?? []).map((entry) => [entry.id, entry])
|
|
47
51
|
)
|
|
48
|
-
const
|
|
52
|
+
const document = await ctx.deps.readConfig()
|
|
53
|
+
const plugins = definitions.map((definition) => {
|
|
49
54
|
const id = definition.manifest.id
|
|
50
55
|
const status = statuses.get(id)
|
|
51
|
-
const envelope = asRecord(
|
|
56
|
+
const envelope = asRecord(document.config.plugins[id]) as Record<
|
|
52
57
|
string,
|
|
53
58
|
unknown
|
|
54
59
|
>
|
|
@@ -57,6 +62,11 @@ export async function handlePluginsList(ctx: ApiContext): Promise<void> {
|
|
|
57
62
|
const options = structuredClone(
|
|
58
63
|
stripPluginEnvelope(envelope as never)
|
|
59
64
|
) as Record<string, unknown>
|
|
65
|
+
const enabled =
|
|
66
|
+
status?.enabled ??
|
|
67
|
+
(typeof envelope['enabled'] === 'boolean'
|
|
68
|
+
? envelope['enabled']
|
|
69
|
+
: (definition.manifest.defaultEnabled ?? false))
|
|
60
70
|
|
|
61
71
|
return {
|
|
62
72
|
id,
|
|
@@ -64,22 +74,24 @@ export async function handlePluginsList(ctx: ApiContext): Promise<void> {
|
|
|
64
74
|
description: definition.manifest.description,
|
|
65
75
|
dependencies: definition.manifest.dependencies ?? [],
|
|
66
76
|
defaultEnabled: definition.manifest.defaultEnabled ?? false,
|
|
67
|
-
enabled
|
|
77
|
+
enabled,
|
|
68
78
|
loaded: status?.loaded ?? false,
|
|
69
79
|
config: envelope,
|
|
70
80
|
options,
|
|
71
81
|
configSchema: pluginJsonSchema(definition),
|
|
72
82
|
}
|
|
73
83
|
})
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
const installer = runtime?.installer
|
|
85
|
+
const installed =
|
|
86
|
+
installer !== undefined && installer.enabled
|
|
87
|
+
? await installer.list()
|
|
88
|
+
: []
|
|
77
89
|
|
|
78
90
|
sendData(ctx.res, 200, {
|
|
79
91
|
plugins,
|
|
80
92
|
installed,
|
|
81
|
-
installerEnabled:
|
|
82
|
-
configHash:
|
|
93
|
+
installerEnabled: installer?.enabled ?? false,
|
|
94
|
+
configHash: document.hash,
|
|
83
95
|
})
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -90,14 +102,17 @@ export async function handlePluginConfigPut(ctx: ApiContext): Promise<void> {
|
|
|
90
102
|
|
|
91
103
|
if (id === undefined) throw new HttpError(404, 'unknown plugin')
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
const runtime = ctx.deps.getRuntime()
|
|
106
|
+
const definition = (runtime?.plugins ?? ctx.deps.getPlugins()).find(
|
|
107
|
+
(plugin) => plugin.manifest.id === id
|
|
108
|
+
)
|
|
109
|
+
const entry = document.config.plugins[id]
|
|
110
|
+
|
|
111
|
+
if (definition === undefined && entry === undefined) {
|
|
94
112
|
throw new HttpError(404, `unknown plugin: ${id}`)
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
const existing = asRecord(
|
|
98
|
-
string,
|
|
99
|
-
unknown
|
|
100
|
-
>
|
|
115
|
+
const existing = asRecord(entry) as unknown as Record<string, unknown>
|
|
101
116
|
const next = { ...existing }
|
|
102
117
|
|
|
103
118
|
if (typeof body['enabled'] === 'boolean') {
|
|
@@ -115,10 +130,6 @@ export async function handlePluginConfigPut(ctx: ApiContext): Promise<void> {
|
|
|
115
130
|
throw new HttpError(400, 'options must be an object')
|
|
116
131
|
}
|
|
117
132
|
|
|
118
|
-
const runtime = ctx.deps.getRuntime()
|
|
119
|
-
const definition = runtime?.plugins.find(
|
|
120
|
-
(plugin) => plugin.manifest.id === id
|
|
121
|
-
)
|
|
122
133
|
const options: Record<string, unknown> = {}
|
|
123
134
|
|
|
124
135
|
for (const [key, value] of Object.entries(body['options'])) {
|
package/src/core/web/types.ts
CHANGED
|
@@ -64,6 +64,13 @@ export interface WebUiDeps {
|
|
|
64
64
|
/** Current webui settings; picked up after runtime restarts. */
|
|
65
65
|
getWebUiConfig(): WebUiConfig
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Plugin definitions from the last successfully loaded runtime spec.
|
|
69
|
+
* Unlike `getRuntime()`, this stays available when the kernel fails to
|
|
70
|
+
* start, so plugin configs can still be edited from the WebUI.
|
|
71
|
+
*/
|
|
72
|
+
getPlugins(): AnyPluginDefinition[]
|
|
73
|
+
|
|
67
74
|
readConfig(): Promise<ConfigDocument>
|
|
68
75
|
validateConfig(value: unknown): Promise<ConfigValidationResult>
|
|
69
76
|
applyConfig(
|