@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.
Files changed (39) hide show
  1. package/dist/app/runtime.d.ts +1 -0
  2. package/dist/app/runtime.d.ts.map +1 -1
  3. package/dist/app/runtime.js +1 -0
  4. package/dist/app/runtime.js.map +1 -1
  5. package/dist/app.js +6 -1
  6. package/dist/app.js.map +1 -1
  7. package/dist/core/plugin/params/declaration.d.ts +2 -2
  8. package/dist/core/plugin/params/declaration.d.ts.map +1 -1
  9. package/dist/core/plugin/params/declaration.js +189 -24
  10. package/dist/core/plugin/params/declaration.js.map +1 -1
  11. package/dist/core/plugin/params/input.d.ts.map +1 -1
  12. package/dist/core/plugin/params/input.js +35 -12
  13. package/dist/core/plugin/params/input.js.map +1 -1
  14. package/dist/core/plugin/params/type-level.d.ts +13 -3
  15. package/dist/core/plugin/params/type-level.d.ts.map +1 -1
  16. package/dist/core/plugin/params/types.d.ts +13 -2
  17. package/dist/core/plugin/params/types.d.ts.map +1 -1
  18. package/dist/core/plugin/params/types.js +22 -0
  19. package/dist/core/plugin/params/types.js.map +1 -1
  20. package/dist/core/web/config-store.d.ts.map +1 -1
  21. package/dist/core/web/config-store.js +14 -10
  22. package/dist/core/web/config-store.js.map +1 -1
  23. package/dist/core/web/routes/plugins.d.ts.map +1 -1
  24. package/dist/core/web/routes/plugins.js +24 -13
  25. package/dist/core/web/routes/plugins.js.map +1 -1
  26. package/dist/core/web/types.d.ts +6 -0
  27. package/dist/core/web/types.d.ts.map +1 -1
  28. package/docs/plugin-api.md +27 -3
  29. package/docs/webui.md +1 -1
  30. package/package.json +1 -1
  31. package/src/app/runtime.ts +2 -0
  32. package/src/app.ts +6 -1
  33. package/src/core/plugin/params/declaration.ts +269 -31
  34. package/src/core/plugin/params/input.ts +63 -12
  35. package/src/core/plugin/params/type-level.ts +114 -6
  36. package/src/core/plugin/params/types.ts +46 -2
  37. package/src/core/web/config-store.ts +18 -12
  38. package/src/core/web/routes/plugins.ts +30 -19
  39. 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
- type: CommandParamType
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 [id, envelope] of Object.entries(config.plugins)) {
132
- const definition = byId.get(id)
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
- (envelope as PluginEnvelope).enabled ??
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: true,
260
- issues: validatePlugins(parsed.data, this.options.getPlugins()),
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 = requireRuntime(ctx.deps)
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.kernel.listPlugins().map((entry) => [entry.id, entry])
50
+ (runtime?.kernel.listPlugins() ?? []).map((entry) => [entry.id, entry])
47
51
  )
48
- const plugins = runtime.plugins.map((definition) => {
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(runtime.config.plugins[id]) as Record<
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: status?.enabled ?? false,
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 installed = runtime.installer.enabled
75
- ? await runtime.installer.list()
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: runtime.installer.enabled,
82
- configHash: (await ctx.deps.readConfig()).hash,
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
- if (!isRecord(document.config.plugins[id])) {
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(document.config.plugins[id]) as unknown as Record<
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'])) {
@@ -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(