@jsbot-tellus/core 2.4.0 → 2.5.1

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 (183) hide show
  1. package/README.md +8 -5
  2. package/dist/app/cli.d.ts +10 -0
  3. package/dist/app/cli.d.ts.map +1 -0
  4. package/dist/app/cli.js +37 -0
  5. package/dist/app/cli.js.map +1 -0
  6. package/dist/app/runtime.d.ts +121 -0
  7. package/dist/app/runtime.d.ts.map +1 -0
  8. package/dist/app/runtime.js +296 -0
  9. package/dist/app/runtime.js.map +1 -0
  10. package/dist/app.js +63 -273
  11. package/dist/app.js.map +1 -1
  12. package/dist/core/dispatch/command.d.ts.map +1 -1
  13. package/dist/core/dispatch/command.js +5 -1
  14. package/dist/core/dispatch/command.js.map +1 -1
  15. package/dist/core/dispatch/router.d.ts.map +1 -1
  16. package/dist/core/dispatch/router.js +1 -1
  17. package/dist/core/dispatch/router.js.map +1 -1
  18. package/dist/core/plugin/command-params.d.ts +3 -36
  19. package/dist/core/plugin/command-params.d.ts.map +1 -1
  20. package/dist/core/plugin/command-params.js +2 -461
  21. package/dist/core/plugin/command-params.js.map +1 -1
  22. package/dist/core/plugin/install/paths.d.ts +7 -0
  23. package/dist/core/plugin/install/paths.d.ts.map +1 -0
  24. package/dist/core/plugin/install/paths.js +49 -0
  25. package/dist/core/plugin/install/paths.js.map +1 -0
  26. package/dist/core/plugin/install/state.d.ts +23 -0
  27. package/dist/core/plugin/install/state.d.ts.map +1 -0
  28. package/dist/core/plugin/install/state.js +129 -0
  29. package/dist/core/plugin/install/state.js.map +1 -0
  30. package/dist/core/plugin/install/validate-script.d.ts +3 -0
  31. package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
  32. package/dist/core/plugin/install/validate-script.js +154 -0
  33. package/dist/core/plugin/install/validate-script.js.map +1 -0
  34. package/dist/core/plugin/install-dir.d.ts +3 -28
  35. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  36. package/dist/core/plugin/install-dir.js +3 -326
  37. package/dist/core/plugin/install-dir.js.map +1 -1
  38. package/dist/core/plugin/installer/checks.d.ts +6 -0
  39. package/dist/core/plugin/installer/checks.d.ts.map +1 -0
  40. package/dist/core/plugin/installer/checks.js +99 -0
  41. package/dist/core/plugin/installer/checks.js.map +1 -0
  42. package/dist/core/plugin/installer/support.d.ts +37 -0
  43. package/dist/core/plugin/installer/support.d.ts.map +1 -0
  44. package/dist/core/plugin/installer/support.js +67 -0
  45. package/dist/core/plugin/installer/support.js.map +1 -0
  46. package/dist/core/plugin/installer/warnings.d.ts +5 -0
  47. package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
  48. package/dist/core/plugin/installer/warnings.js +28 -0
  49. package/dist/core/plugin/installer/warnings.js.map +1 -0
  50. package/dist/core/plugin/installer.d.ts +4 -30
  51. package/dist/core/plugin/installer.d.ts.map +1 -1
  52. package/dist/core/plugin/installer.js +21 -202
  53. package/dist/core/plugin/installer.js.map +1 -1
  54. package/dist/core/plugin/params/compile.d.ts +3 -0
  55. package/dist/core/plugin/params/compile.d.ts.map +1 -0
  56. package/dist/core/plugin/params/compile.js +23 -0
  57. package/dist/core/plugin/params/compile.js.map +1 -0
  58. package/dist/core/plugin/params/declaration.d.ts +9 -0
  59. package/dist/core/plugin/params/declaration.d.ts.map +1 -0
  60. package/dist/core/plugin/params/declaration.js +223 -0
  61. package/dist/core/plugin/params/declaration.js.map +1 -0
  62. package/dist/core/plugin/params/input.d.ts +5 -0
  63. package/dist/core/plugin/params/input.d.ts.map +1 -0
  64. package/dist/core/plugin/params/input.js +181 -0
  65. package/dist/core/plugin/params/input.js.map +1 -0
  66. package/dist/core/plugin/params/reply.d.ts +3 -0
  67. package/dist/core/plugin/params/reply.d.ts.map +1 -0
  68. package/dist/core/plugin/params/reply.js +49 -0
  69. package/dist/core/plugin/params/reply.js.map +1 -0
  70. package/dist/core/plugin/params/type-level.d.ts +23 -0
  71. package/dist/core/plugin/params/type-level.d.ts.map +1 -0
  72. package/dist/core/plugin/params/type-level.js +2 -0
  73. package/dist/core/plugin/params/type-level.js.map +1 -0
  74. package/dist/core/plugin/params/types.d.ts +28 -0
  75. package/dist/core/plugin/params/types.d.ts.map +1 -0
  76. package/dist/core/plugin/params/types.js +12 -0
  77. package/dist/core/plugin/params/types.js.map +1 -0
  78. package/dist/core/plugin/params/values.d.ts +6 -0
  79. package/dist/core/plugin/params/values.d.ts.map +1 -0
  80. package/dist/core/plugin/params/values.js +108 -0
  81. package/dist/core/plugin/params/values.js.map +1 -0
  82. package/dist/core/web/api.d.ts +2 -14
  83. package/dist/core/web/api.d.ts.map +1 -1
  84. package/dist/core/web/api.js +8 -500
  85. package/dist/core/web/api.js.map +1 -1
  86. package/dist/core/web/routes/actions.d.ts +4 -0
  87. package/dist/core/web/routes/actions.d.ts.map +1 -0
  88. package/dist/core/web/routes/actions.js +33 -0
  89. package/dist/core/web/routes/actions.js.map +1 -0
  90. package/dist/core/web/routes/config.d.ts +5 -0
  91. package/dist/core/web/routes/config.d.ts.map +1 -0
  92. package/dist/core/web/routes/config.js +30 -0
  93. package/dist/core/web/routes/config.js.map +1 -0
  94. package/dist/core/web/routes/kv.d.ts +6 -0
  95. package/dist/core/web/routes/kv.d.ts.map +1 -0
  96. package/dist/core/web/routes/kv.js +80 -0
  97. package/dist/core/web/routes/kv.js.map +1 -0
  98. package/dist/core/web/routes/plugins.d.ts +5 -0
  99. package/dist/core/web/routes/plugins.d.ts.map +1 -0
  100. package/dist/core/web/routes/plugins.js +98 -0
  101. package/dist/core/web/routes/plugins.js.map +1 -0
  102. package/dist/core/web/routes/sessions.d.ts +8 -0
  103. package/dist/core/web/routes/sessions.d.ts.map +1 -0
  104. package/dist/core/web/routes/sessions.js +130 -0
  105. package/dist/core/web/routes/sessions.js.map +1 -0
  106. package/dist/core/web/routes/shared.d.ts +18 -0
  107. package/dist/core/web/routes/shared.d.ts.map +1 -0
  108. package/dist/core/web/routes/shared.js +19 -0
  109. package/dist/core/web/routes/shared.js.map +1 -0
  110. package/dist/core/web/routes/status.d.ts +6 -0
  111. package/dist/core/web/routes/status.d.ts.map +1 -0
  112. package/dist/core/web/routes/status.js +132 -0
  113. package/dist/core/web/routes/status.js.map +1 -0
  114. package/dist/core/web/server/auth.d.ts +17 -0
  115. package/dist/core/web/server/auth.d.ts.map +1 -0
  116. package/dist/core/web/server/auth.js +68 -0
  117. package/dist/core/web/server/auth.js.map +1 -0
  118. package/dist/core/web/server/csp.d.ts +11 -0
  119. package/dist/core/web/server/csp.d.ts.map +1 -0
  120. package/dist/core/web/server/csp.js +31 -0
  121. package/dist/core/web/server/csp.js.map +1 -0
  122. package/dist/core/web/server/request.d.ts +6 -0
  123. package/dist/core/web/server/request.d.ts.map +1 -0
  124. package/dist/core/web/server/request.js +33 -0
  125. package/dist/core/web/server/request.js.map +1 -0
  126. package/dist/core/web/server/security.d.ts +4 -0
  127. package/dist/core/web/server/security.d.ts.map +1 -0
  128. package/dist/core/web/server/security.js +12 -0
  129. package/dist/core/web/server/security.js.map +1 -0
  130. package/dist/core/web/server/sse.d.ts +18 -0
  131. package/dist/core/web/server/sse.d.ts.map +1 -0
  132. package/dist/core/web/server/sse.js +49 -0
  133. package/dist/core/web/server/sse.js.map +1 -0
  134. package/dist/core/web/server/static.d.ts +16 -0
  135. package/dist/core/web/server/static.d.ts.map +1 -0
  136. package/dist/core/web/server/static.js +80 -0
  137. package/dist/core/web/server/static.js.map +1 -0
  138. package/dist/core/web/server.d.ts +0 -7
  139. package/dist/core/web/server.d.ts.map +1 -1
  140. package/dist/core/web/server.js +41 -263
  141. package/dist/core/web/server.js.map +1 -1
  142. package/docs/architecture.md +28 -11
  143. package/docs/configuration.md +1 -1
  144. package/docs/migration.md +2 -2
  145. package/docs/plugin-api.md +3 -1
  146. package/docs/plugin-development.md +1 -1
  147. package/package.json +19 -17
  148. package/src/app/cli.ts +47 -0
  149. package/src/app/runtime.ts +522 -0
  150. package/src/app.ts +87 -395
  151. package/src/core/dispatch/command.ts +7 -1
  152. package/src/core/dispatch/router.ts +2 -1
  153. package/src/core/plugin/command-params.ts +8 -713
  154. package/src/core/plugin/install/paths.ts +72 -0
  155. package/src/core/plugin/install/state.ts +196 -0
  156. package/src/core/plugin/install/validate-script.ts +154 -0
  157. package/src/core/plugin/install-dir.ts +19 -427
  158. package/src/core/plugin/installer/checks.ts +137 -0
  159. package/src/core/plugin/installer/support.ts +120 -0
  160. package/src/core/plugin/installer/warnings.ts +52 -0
  161. package/src/core/plugin/installer.ts +52 -301
  162. package/src/core/plugin/params/compile.ts +41 -0
  163. package/src/core/plugin/params/declaration.ts +286 -0
  164. package/src/core/plugin/params/input.ts +254 -0
  165. package/src/core/plugin/params/reply.ts +59 -0
  166. package/src/core/plugin/params/type-level.ts +71 -0
  167. package/src/core/plugin/params/types.ts +49 -0
  168. package/src/core/plugin/params/values.ts +146 -0
  169. package/src/core/web/api.ts +33 -759
  170. package/src/core/web/routes/actions.ts +43 -0
  171. package/src/core/web/routes/config.ts +42 -0
  172. package/src/core/web/routes/kv.ts +122 -0
  173. package/src/core/web/routes/plugins.ts +151 -0
  174. package/src/core/web/routes/sessions.ts +188 -0
  175. package/src/core/web/routes/shared.ts +42 -0
  176. package/src/core/web/routes/status.ts +191 -0
  177. package/src/core/web/server/auth.ts +126 -0
  178. package/src/core/web/server/csp.ts +33 -0
  179. package/src/core/web/server/request.ts +38 -0
  180. package/src/core/web/server/security.ts +14 -0
  181. package/src/core/web/server/sse.ts +74 -0
  182. package/src/core/web/server/static.ts +112 -0
  183. package/src/core/web/server.ts +41 -383
@@ -0,0 +1,52 @@
1
+ import type { InstalledPackageRecord, InstallState } from '../install-dir.js'
2
+
3
+ export function configuredWarnings(
4
+ pluginIds: readonly string[],
5
+ configured: ReadonlySet<string>
6
+ ): string[] {
7
+ return pluginIds
8
+ .filter((id) => configured.has(id))
9
+ .map(
10
+ (id) =>
11
+ `配置里的 plugins.${id} 仍然存在,重启前请删除,否则会因未知插件 ID 启动失败`
12
+ )
13
+ }
14
+
15
+ export function removalWarnings(
16
+ packageName: string,
17
+ record: InstalledPackageRecord | undefined,
18
+ state: InstallState,
19
+ configured: ReadonlySet<string>
20
+ ): string[] {
21
+ return [
22
+ ...configuredWarnings(record?.pluginIds ?? [], configured),
23
+ ...dependentWarnings(packageName, record, state),
24
+ ]
25
+ }
26
+
27
+ export function dependentWarnings(
28
+ packageName: string,
29
+ record: InstalledPackageRecord | undefined,
30
+ state: InstallState
31
+ ): string[] {
32
+ const removedIds = new Set(record?.pluginIds ?? [])
33
+ const warnings: string[] = []
34
+
35
+ for (const [name, other] of Object.entries(state.packages)) {
36
+ if (name === packageName) continue
37
+
38
+ for (const [pluginId, dependencies] of Object.entries(
39
+ other.dependencies
40
+ )) {
41
+ for (const dependency of dependencies) {
42
+ if (!removedIds.has(dependency)) continue
43
+
44
+ warnings.push(
45
+ `插件包 ${name} 的插件 ${pluginId} 依赖已卸载的插件 ${dependency}`
46
+ )
47
+ }
48
+ }
49
+ }
50
+
51
+ return warnings
52
+ }
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto'
2
1
  import { cp, mkdir, rename, rm, writeFile } from 'node:fs/promises'
3
2
  import { join } from 'node:path'
4
3
  import type { InstallerConfig } from '../config/schema.js'
@@ -6,7 +5,6 @@ import type { Logger } from '../logging/logger.js'
6
5
  import {
7
6
  ensureCoreLink,
8
7
  ensureInstallRoot,
9
- packageDirectoryName,
10
8
  pathExists,
11
9
  readInstallState,
12
10
  readJsonFile,
@@ -19,15 +17,32 @@ import {
19
17
  import {
20
18
  validateInstalledPackage,
21
19
  type PackageValidator,
22
- type ValidatedPlugin,
23
20
  } from './install-validate.js'
24
- import { NpmCliRunner, type NpmRunner } from './npm-runner.js'
25
21
  import {
26
- isPackageAllowed,
27
- parsePackageSpec,
28
- PackageSpecError,
29
- type PackageSpec,
30
- } from './spec.js'
22
+ assertTargetAvailable,
23
+ checkPlugins,
24
+ resolveTarget,
25
+ } from './installer/checks.js'
26
+ import {
27
+ asInstallerError,
28
+ assertInstallerEnabled,
29
+ createTaskSerializer,
30
+ installDirectoryName,
31
+ InstallerError,
32
+ messageOf,
33
+ NPM_FLAGS,
34
+ parseInstallerSpec,
35
+ STAGING_MANIFEST,
36
+ temporaryPath,
37
+ type InstallerOptions,
38
+ } from './installer/support.js'
39
+ import {
40
+ configuredWarnings,
41
+ dependentWarnings,
42
+ removalWarnings,
43
+ } from './installer/warnings.js'
44
+ import { NpmCliRunner, type NpmRunner } from './npm-runner.js'
45
+ import type { PackageSpec } from './spec.js'
31
46
  import type {
32
47
  InstalledPackageInfo,
33
48
  PluginInstallResult,
@@ -36,56 +51,8 @@ import type {
36
51
  PluginUpdateResult,
37
52
  } from './types.js'
38
53
 
39
- export class InstallerError extends Error {
40
- constructor(message: string, options?: ErrorOptions) {
41
- super(message, options)
42
- this.name = 'InstallerError'
43
- }
44
- }
45
-
46
- export interface InstallerOptions {
47
- /** Plugins root. Installed packages are placed in subdirectories here. */
48
- dir: string
49
- config: InstallerConfig
50
- logger: Logger
51
- runner?: NpmRunner | undefined
52
- validate?: PackageValidator | undefined
53
- reservedIds?: (() => ReadonlySet<string>) | undefined
54
- configuredPluginIds?: (() => ReadonlySet<string>) | undefined
55
- now?: (() => Date) | undefined
56
- }
57
-
58
- const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i
59
- const NPM_FLAGS = ['--no-audit', '--no-fund', '--loglevel=error']
60
- const STAGING_MANIFEST = {
61
- name: '@jsbot-tellus/plugin-staging',
62
- private: true,
63
- version: '0.0.0',
64
- type: 'module',
65
- }
66
-
67
- function messageOf(error: unknown): string {
68
- return error instanceof Error ? error.message : String(error)
69
- }
70
-
71
- function asInstallerError(error: unknown): InstallerError {
72
- if (error instanceof InstallerError) return error
73
-
74
- if (error instanceof PackageSpecError) {
75
- return new InstallerError(error.message, { cause: error })
76
- }
77
-
78
- return new InstallerError(messageOf(error), { cause: error })
79
- }
80
-
81
- function installDirectoryName(
82
- packageName: string,
83
- pluginIds: readonly string[]
84
- ): string {
85
- const only = pluginIds.length === 1 ? pluginIds[0] : undefined
86
-
87
- return only ?? packageDirectoryName(packageName)
88
- }
54
+ export { createDisabledInstaller, InstallerError } from './installer/support.js'
55
+ export type { InstallerOptions } from './installer/support.js'
89
56
 
90
57
  export class NpmPluginInstaller implements PluginInstaller {
91
58
  readonly enabled: boolean
@@ -98,7 +65,7 @@ export class NpmPluginInstaller implements PluginInstaller {
98
65
  private readonly reservedIds: () => ReadonlySet<string>
99
66
  private readonly configuredPluginIds: () => ReadonlySet<string>
100
67
  private readonly now: () => Date
101
- private tail: Promise<unknown> = Promise.resolve()
68
+ private readonly serialize = createTaskSerializer()
102
69
 
103
70
  constructor(options: InstallerOptions) {
104
71
  this.dir = options.dir
@@ -147,8 +114,8 @@ export class NpmPluginInstaller implements PluginInstaller {
147
114
  signal?: AbortSignal | undefined
148
115
  ): Promise<PluginInstallResult> {
149
116
  try {
150
- this.assertEnabled()
151
- const parsed = this.parse(spec)
117
+ assertInstallerEnabled(this.enabled)
118
+ const parsed = parseInstallerSpec(spec, this.config.allowedPackages)
152
119
 
153
120
  return this.serialize(() => this.installSpec(parsed, signal))
154
121
  } catch (error) {
@@ -162,22 +129,27 @@ export class NpmPluginInstaller implements PluginInstaller {
162
129
  options?: { force?: boolean | undefined } | undefined
163
130
  ): Promise<PluginRemoveResult> {
164
131
  try {
165
- this.assertEnabled()
132
+ assertInstallerEnabled(this.enabled)
166
133
  } catch (error) {
167
134
  return Promise.reject(asInstallerError(error))
168
135
  }
169
136
 
170
137
  return this.serialize(async () => {
171
138
  const state = await readInstallState(this.dir)
172
- const name = this.resolveTarget(target, state)
139
+ const name = resolveTarget(target, state)
173
140
  const record = state.packages[name]
174
141
 
175
142
  if (!record) {
176
143
  throw new InstallerError(`未找到已安装的插件包: ${name}`)
177
144
  }
178
145
 
179
- const warnings = this.removalWarnings(name, record, state)
180
- const dependents = this.dependentWarnings(name, record, state)
146
+ const warnings = removalWarnings(
147
+ name,
148
+ record,
149
+ state,
150
+ this.configuredPluginIds()
151
+ )
152
+ const dependents = dependentWarnings(name, record, state)
181
153
 
182
154
  if (dependents.length > 0 && options?.force !== true) {
183
155
  throw new InstallerError(
@@ -190,7 +162,7 @@ export class NpmPluginInstaller implements PluginInstaller {
190
162
  }
191
163
 
192
164
  const destination = join(this.dir, record.dir)
193
- const backup = this.temporaryPath('backup')
165
+ const backup = temporaryPath(this.dir, 'backup')
194
166
  const existed = await pathExists(destination)
195
167
 
196
168
  try {
@@ -243,7 +215,7 @@ export class NpmPluginInstaller implements PluginInstaller {
243
215
  signal?: AbortSignal | undefined
244
216
  ): Promise<PluginUpdateResult> {
245
217
  try {
246
- this.assertEnabled()
218
+ assertInstallerEnabled(this.enabled)
247
219
  } catch (error) {
248
220
  return Promise.reject(asInstallerError(error))
249
221
  }
@@ -253,7 +225,7 @@ export class NpmPluginInstaller implements PluginInstaller {
253
225
  const names =
254
226
  target === undefined || target.trim().length === 0
255
227
  ? Object.keys(state.packages).sort()
256
- : [this.resolveTarget(target, state)]
228
+ : [resolveTarget(target, state)]
257
229
 
258
230
  const updated: PluginInstallResult[] = []
259
231
  const failed: Array<{ packageName: string; message: string }> = []
@@ -265,7 +237,10 @@ export class NpmPluginInstaller implements PluginInstaller {
265
237
  }
266
238
 
267
239
  try {
268
- const parsed = this.parse(name)
240
+ const parsed = parseInstallerSpec(
241
+ name,
242
+ this.config.allowedPackages
243
+ )
269
244
 
270
245
  updated.push(
271
246
  await this.installSpec(
@@ -297,8 +272,8 @@ export class NpmPluginInstaller implements PluginInstaller {
297
272
 
298
273
  const state = await readInstallState(this.dir)
299
274
  const previous = state.packages[spec.name]
300
- const staging = this.temporaryPath('staging')
301
- const backup = this.temporaryPath('backup')
275
+ const staging = temporaryPath(this.dir, 'staging')
276
+ const backup = temporaryPath(this.dir, 'backup')
302
277
  let target: string | null = null
303
278
  let movedTarget = false
304
279
  let movedBackup = false
@@ -360,7 +335,7 @@ export class NpmPluginInstaller implements PluginInstaller {
360
335
  script: join(this.dir, '.jsbot', VALIDATE_SCRIPT_FILE),
361
336
  })
362
337
 
363
- this.checkPlugins(spec.name, validated, state)
338
+ checkPlugins(spec.name, validated, state, () => this.reservedIds())
364
339
 
365
340
  const version = await readPackageVersion(flat)
366
341
 
@@ -374,7 +349,7 @@ export class NpmPluginInstaller implements PluginInstaller {
374
349
 
375
350
  target = join(this.dir, directory)
376
351
 
377
- await this.assertTargetAvailable(
352
+ await assertTargetAvailable(
378
353
  state,
379
354
  spec.name,
380
355
  directory,
@@ -382,10 +357,11 @@ export class NpmPluginInstaller implements PluginInstaller {
382
357
  previous
383
358
  )
384
359
 
385
- const warnings = this.configuredWarnings(
360
+ const warnings = configuredWarnings(
386
361
  (previous?.pluginIds ?? []).filter(
387
362
  (id) => !pluginIds.includes(id)
388
- )
363
+ ),
364
+ this.configuredPluginIds()
389
365
  )
390
366
 
391
367
  if (await pathExists(target)) {
@@ -467,229 +443,4 @@ export class NpmPluginInstaller implements PluginInstaller {
467
443
  )
468
444
  }
469
445
  }
470
-
471
- private async assertTargetAvailable(
472
- state: InstallState,
473
- packageName: string,
474
- directory: string,
475
- target: string,
476
- previous: InstalledPackageRecord | undefined
477
- ): Promise<void> {
478
- for (const [name, record] of Object.entries(state.packages)) {
479
- if (name === packageName) continue
480
- if (record.dir !== directory) continue
481
-
482
- throw new InstallerError(
483
- `插件目录已被其它已安装包使用: ${record.dir}(${name})`
484
- )
485
- }
486
-
487
- if (previous !== undefined) return
488
-
489
- if (await pathExists(target)) {
490
- throw new InstallerError(
491
- `安装目录已存在且不是由安装器管理: ${target}。请先移除该本地插件或为其改名。`
492
- )
493
- }
494
- }
495
-
496
- private checkPlugins(
497
- packageName: string,
498
- plugins: ValidatedPlugin[],
499
- state: InstallState
500
- ): void {
501
- const ownIds = new Set(state.packages[packageName]?.pluginIds ?? [])
502
- const ids = new Set<string>()
503
- const taken = new Set<string>()
504
-
505
- for (const id of this.reservedIds()) {
506
- if (!ownIds.has(id)) taken.add(id)
507
- }
508
-
509
- for (const [name, record] of Object.entries(state.packages)) {
510
- if (name === packageName) continue
511
-
512
- for (const id of record.pluginIds) taken.add(id)
513
- }
514
-
515
- for (const plugin of plugins) {
516
- if (!ID_PATTERN.test(plugin.id)) {
517
- throw new InstallerError(`插件 id 无效: ${plugin.id}`)
518
- }
519
-
520
- if (ids.has(plugin.id)) {
521
- throw new InstallerError(`插件 id 重复: ${plugin.id}`)
522
- }
523
-
524
- if (taken.has(plugin.id)) {
525
- throw new InstallerError(
526
- `插件 id 已被占用: ${plugin.id}(内置插件或其他已安装包)`
527
- )
528
- }
529
-
530
- ids.add(plugin.id)
531
- }
532
-
533
- const graph = new Map<string, string[]>()
534
-
535
- for (const [name, record] of Object.entries(state.packages)) {
536
- if (name === packageName) continue
537
-
538
- for (const id of record.pluginIds) {
539
- graph.set(id, record.dependencies[id] ?? [])
540
- }
541
- }
542
-
543
- for (const plugin of plugins) {
544
- graph.set(plugin.id, plugin.dependencies)
545
- }
546
-
547
- const reserved = this.reservedIds()
548
- const available = new Set(graph.keys())
549
-
550
- for (const id of reserved) {
551
- if (!ownIds.has(id)) available.add(id)
552
- }
553
-
554
- for (const [id, dependencies] of graph) {
555
- for (const dependency of dependencies) {
556
- if (!available.has(dependency)) {
557
- throw new InstallerError(
558
- `插件 ${id} 依赖未加载的插件: ${dependency}`
559
- )
560
- }
561
- }
562
- }
563
-
564
- const visiting = new Set<string>()
565
- const visited = new Set<string>()
566
-
567
- const visit = (id: string): void => {
568
- if (visited.has(id)) return
569
-
570
- if (visiting.has(id)) {
571
- throw new InstallerError(`插件依赖存在循环: ${id}`)
572
- }
573
-
574
- visiting.add(id)
575
-
576
- for (const dependency of graph.get(id) ?? []) {
577
- if (graph.has(dependency)) visit(dependency)
578
- }
579
-
580
- visiting.delete(id)
581
- visited.add(id)
582
- }
583
-
584
- for (const id of graph.keys()) visit(id)
585
- }
586
-
587
- private resolveTarget(target: string, state: InstallState): string {
588
- const value = target.trim()
589
-
590
- if (value.length === 0) {
591
- throw new InstallerError('请提供包名或插件 id')
592
- }
593
-
594
- if (state.packages[value] !== undefined) return value
595
-
596
- for (const [name, record] of Object.entries(state.packages)) {
597
- if (record.pluginIds.includes(value)) return name
598
- }
599
-
600
- throw new InstallerError(`未找到已安装的插件包: ${value}`)
601
- }
602
-
603
- private removalWarnings(
604
- packageName: string,
605
- record: InstalledPackageRecord | undefined,
606
- state: InstallState
607
- ): string[] {
608
- return [
609
- ...this.configuredWarnings(record?.pluginIds ?? []),
610
- ...this.dependentWarnings(packageName, record, state),
611
- ]
612
- }
613
-
614
- private configuredWarnings(pluginIds: readonly string[]): string[] {
615
- const configured = this.configuredPluginIds()
616
-
617
- return pluginIds
618
- .filter((id) => configured.has(id))
619
- .map(
620
- (id) =>
621
- `配置里的 plugins.${id} 仍然存在,重启前请删除,否则会因未知插件 ID 启动失败`
622
- )
623
- }
624
-
625
- private dependentWarnings(
626
- packageName: string,
627
- record: InstalledPackageRecord | undefined,
628
- state: InstallState
629
- ): string[] {
630
- const removedIds = new Set(record?.pluginIds ?? [])
631
- const warnings: string[] = []
632
-
633
- for (const [name, other] of Object.entries(state.packages)) {
634
- if (name === packageName) continue
635
-
636
- for (const [pluginId, dependencies] of Object.entries(
637
- other.dependencies
638
- )) {
639
- for (const dependency of dependencies) {
640
- if (!removedIds.has(dependency)) continue
641
-
642
- warnings.push(
643
- `插件包 ${name} 的插件 ${pluginId} 依赖已卸载的插件 ${dependency}`
644
- )
645
- }
646
- }
647
- }
648
-
649
- return warnings
650
- }
651
-
652
- private parse(spec: string): PackageSpec {
653
- const parsed = parsePackageSpec(spec)
654
-
655
- if (!isPackageAllowed(parsed.name, this.config.allowedPackages)) {
656
- throw new InstallerError(`包不在允许列表: ${parsed.name}`)
657
- }
658
-
659
- return parsed
660
- }
661
-
662
- private temporaryPath(kind: 'staging' | 'backup'): string {
663
- return join(this.dir, `.${kind}-${randomUUID()}`)
664
- }
665
-
666
- private assertEnabled(): void {
667
- if (!this.enabled) {
668
- throw new InstallerError(
669
- '插件安装功能未启用(installer.enabled=false)'
670
- )
671
- }
672
- }
673
-
674
- private serialize<T>(task: () => Promise<T>): Promise<T> {
675
- const run = this.tail.then(task, task)
676
- this.tail = run.then(
677
- () => undefined,
678
- () => undefined
679
- )
680
- return run
681
- }
682
- }
683
-
684
- export function createDisabledInstaller(): PluginInstaller {
685
- const fail = (): Promise<never> =>
686
- Promise.reject(new InstallerError('插件安装功能未启用'))
687
-
688
- return {
689
- enabled: false,
690
- list: () => Promise.resolve([]),
691
- install: fail,
692
- remove: fail,
693
- update: fail,
694
- }
695
446
  }
@@ -0,0 +1,41 @@
1
+ import type { OneBotMessageEvent } from '@snowluma/sdk'
2
+ import {
3
+ declarationError,
4
+ type CompiledCommandParams,
5
+ type ParsedCommandParams,
6
+ } from './types.js'
7
+ import {
8
+ splitDeclaration,
9
+ parseToken,
10
+ usageOf,
11
+ validateSpecs,
12
+ } from './declaration.js'
13
+ import { parseInput } from './input.js'
14
+ import { quoteIdOf } from './reply.js'
15
+
16
+ export function compileCommandParams(
17
+ declaration: string
18
+ ): CompiledCommandParams {
19
+ const trimmed = declaration.trim()
20
+
21
+ if (!trimmed) {
22
+ throw declarationError('参数声明不能为空')
23
+ }
24
+
25
+ const specs = splitDeclaration(trimmed).map(parseToken)
26
+
27
+ if (specs.length === 0) {
28
+ throw declarationError('参数声明不能为空')
29
+ }
30
+
31
+ validateSpecs(specs)
32
+
33
+ const usage = usageOf(specs)
34
+
35
+ return {
36
+ usage,
37
+ parse(input: string, event?: OneBotMessageEvent): ParsedCommandParams {
38
+ return parseInput(specs, input, usage, quoteIdOf(event))
39
+ },
40
+ }
41
+ }