@jsbot-tellus/core 2.0.3 → 2.2.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 (83) hide show
  1. package/README.md +23 -27
  2. package/dist/app.js +14 -40
  3. package/dist/app.js.map +1 -1
  4. package/dist/core/config/schema.d.ts +0 -2
  5. package/dist/core/config/schema.d.ts.map +1 -1
  6. package/dist/core/config/schema.js +0 -1
  7. package/dist/core/config/schema.js.map +1 -1
  8. package/dist/core/dispatch/command.d.ts +24 -2
  9. package/dist/core/dispatch/command.d.ts.map +1 -1
  10. package/dist/core/dispatch/command.js +297 -60
  11. package/dist/core/dispatch/command.js.map +1 -1
  12. package/dist/core/dispatch/router.d.ts +17 -3
  13. package/dist/core/dispatch/router.d.ts.map +1 -1
  14. package/dist/core/dispatch/router.js +38 -2
  15. package/dist/core/dispatch/router.js.map +1 -1
  16. package/dist/core/index.d.ts +2 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/core/kernel.d.ts.map +1 -1
  20. package/dist/core/kernel.js +5 -1
  21. package/dist/core/kernel.js.map +1 -1
  22. package/dist/core/plugin/command-params.d.ts +37 -0
  23. package/dist/core/plugin/command-params.d.ts.map +1 -0
  24. package/dist/core/plugin/command-params.js +427 -0
  25. package/dist/core/plugin/command-params.js.map +1 -0
  26. package/dist/core/plugin/context.d.ts.map +1 -1
  27. package/dist/core/plugin/context.js +27 -0
  28. package/dist/core/plugin/context.js.map +1 -1
  29. package/dist/core/plugin/install-dir.d.ts +6 -9
  30. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  31. package/dist/core/plugin/install-dir.js +99 -79
  32. package/dist/core/plugin/install-dir.js.map +1 -1
  33. package/dist/core/plugin/install-validate.d.ts +2 -1
  34. package/dist/core/plugin/install-validate.d.ts.map +1 -1
  35. package/dist/core/plugin/install-validate.js +25 -17
  36. package/dist/core/plugin/install-validate.js.map +1 -1
  37. package/dist/core/plugin/installer.d.ts +3 -3
  38. package/dist/core/plugin/installer.d.ts.map +1 -1
  39. package/dist/core/plugin/installer.js +143 -140
  40. package/dist/core/plugin/installer.js.map +1 -1
  41. package/dist/core/plugin/loader.d.ts +13 -1
  42. package/dist/core/plugin/loader.d.ts.map +1 -1
  43. package/dist/core/plugin/loader.js +106 -2
  44. package/dist/core/plugin/loader.js.map +1 -1
  45. package/dist/core/plugin/types.d.ts +14 -5
  46. package/dist/core/plugin/types.d.ts.map +1 -1
  47. package/dist/core/plugin/types.js.map +1 -1
  48. package/dist/plugins/admin/index.d.ts.map +1 -1
  49. package/dist/plugins/admin/index.js +119 -86
  50. package/dist/plugins/admin/index.js.map +1 -1
  51. package/dist/plugins/core/index.d.ts.map +1 -1
  52. package/dist/plugins/core/index.js +5 -2
  53. package/dist/plugins/core/index.js.map +1 -1
  54. package/docs/architecture.md +22 -24
  55. package/docs/configuration.md +19 -23
  56. package/docs/feature-parity.md +4 -4
  57. package/docs/migration.md +13 -13
  58. package/docs/plugin-api.md +164 -27
  59. package/docs/plugin-development.md +400 -0
  60. package/docs/release.md +2 -2
  61. package/examples/plugins/hello/README.md +10 -5
  62. package/examples/plugins/hello/index.js +3 -6
  63. package/package.json +4 -1
  64. package/src/app.ts +18 -55
  65. package/src/core/config/schema.ts +0 -1
  66. package/src/core/dispatch/command.ts +478 -70
  67. package/src/core/dispatch/router.ts +77 -3
  68. package/src/core/index.ts +3 -0
  69. package/src/core/kernel.ts +8 -1
  70. package/src/core/plugin/command-params.ts +667 -0
  71. package/src/core/plugin/context.ts +51 -2
  72. package/src/core/plugin/install-dir.ts +117 -111
  73. package/src/core/plugin/install-validate.ts +36 -34
  74. package/src/core/plugin/installer.ts +228 -184
  75. package/src/core/plugin/loader.ts +180 -3
  76. package/src/core/plugin/types.ts +31 -5
  77. package/src/plugins/admin/index.ts +302 -233
  78. package/src/plugins/core/index.ts +6 -2
  79. package/dist/core/plugin/installed.d.ts +0 -9
  80. package/dist/core/plugin/installed.d.ts.map +0 -1
  81. package/dist/core/plugin/installed.js +0 -113
  82. package/dist/core/plugin/installed.js.map +0 -1
  83. package/src/core/plugin/installed.ts +0 -165
@@ -1,10 +1,21 @@
1
+ import type { OneBotMessageEvent } from '@snowluma/sdk'
2
+ import {
3
+ compileCommandParams,
4
+ type CommandParamArgs,
5
+ type CompiledCommandParams,
6
+ } from '../plugin/command-params.js'
1
7
  import type {
8
+ CommandGroupOptions,
9
+ CommandGroupRegistrar,
2
10
  CommandHandler,
3
11
  CommandInfo,
4
12
  CommandMatch,
13
+ CommandOptions,
5
14
  } from '../plugin/types.js'
6
15
  import type { Permission } from '../security/permission.js'
7
16
 
17
+ type AnyCommandHandler = CommandHandler<OneBotMessageEvent, any[]>
18
+
8
19
  export interface CommandRegistration {
9
20
  pluginId: string
10
21
  name: string
@@ -12,7 +23,21 @@ export interface CommandRegistration {
12
23
  prefixes: string[]
13
24
  permission: Permission
14
25
  description?: string | undefined
15
- handler: CommandHandler
26
+ params?: string | undefined
27
+ paramsParser?: CompiledCommandParams | undefined
28
+ usage?: string | undefined
29
+ handler: AnyCommandHandler
30
+ }
31
+
32
+ export interface CommandGroupRegistration {
33
+ pluginId: string
34
+ name: string
35
+ aliases: string[]
36
+ prefixes: string[]
37
+ permission: Permission
38
+ description?: string | undefined
39
+ params?: string | undefined
40
+ handler?: AnyCommandHandler | undefined
16
41
  }
17
42
 
18
43
  export interface RegisteredCommand {
@@ -20,80 +45,167 @@ export interface RegisteredCommand {
20
45
  match: CommandMatch
21
46
  }
22
47
 
23
- export class CommandRegistry {
24
- private readonly index = new Map<string, Map<string, CommandRegistration>>()
25
- private readonly registrations = new Set<CommandRegistration>()
26
- private readonly byPlugin = new Map<string, Set<CommandRegistration>>()
48
+ interface GroupChild {
49
+ name: string
50
+ description?: string | undefined
51
+ usage?: string | undefined
52
+ }
27
53
 
28
- register(registration: CommandRegistration): () => void {
29
- const names = [
30
- ...new Set(
31
- [registration.name, ...registration.aliases].map((name) =>
32
- name.toLowerCase()
33
- )
34
- ),
35
- ]
36
- const prefixes = [...new Set(registration.prefixes)]
54
+ interface GroupScope {
55
+ registrar: CommandGroupRegistrar
56
+ disposeAll: () => void
57
+ track: (dispose: () => void, cleanup?: () => void) => () => void
58
+ }
37
59
 
38
- for (const prefix of prefixes) {
39
- for (const name of names) {
40
- const existing = this.index.get(prefix)?.get(name)
60
+ function normalizeSegment(label: string, value: string): string {
61
+ const trimmed = value.trim()
41
62
 
42
- if (existing) {
43
- throw new Error(`command conflict: ${prefix}${name}`)
44
- }
45
- }
46
- }
63
+ if (!trimmed) {
64
+ throw new Error(`${label} must not be empty`)
65
+ }
47
66
 
48
- for (const prefix of prefixes) {
49
- const bucket =
50
- this.index.get(prefix) ?? new Map<string, CommandRegistration>()
67
+ if (/\s/.test(trimmed)) {
68
+ throw new Error(
69
+ `${label} must be a single segment; use commands.group() for multi-level commands`
70
+ )
71
+ }
51
72
 
52
- for (const name of names) {
53
- bucket.set(name, registration)
54
- }
73
+ return trimmed
74
+ }
55
75
 
56
- this.index.set(prefix, bucket)
57
- }
76
+ function uniqueStrings(values: string[]): string[] {
77
+ const seen = new Set<string>()
78
+ const result: string[] = []
58
79
 
59
- this.registrations.add(registration)
80
+ for (const value of values) {
81
+ const key = value.toLowerCase()
60
82
 
61
- const pluginSet =
62
- this.byPlugin.get(registration.pluginId) ??
63
- new Set<CommandRegistration>()
64
- pluginSet.add(registration)
65
- this.byPlugin.set(registration.pluginId, pluginSet)
83
+ if (seen.has(key)) continue
66
84
 
67
- let disposed = false
85
+ seen.add(key)
86
+ result.push(value)
87
+ }
68
88
 
69
- return () => {
70
- if (disposed) return
89
+ return result
90
+ }
71
91
 
72
- disposed = true
92
+ function groupBases(name: string, aliases: string[]): string[] {
93
+ return uniqueStrings([
94
+ normalizeSegment('group name', name),
95
+ ...aliases.map((alias) => normalizeSegment('group alias', alias)),
96
+ ])
97
+ }
73
98
 
74
- for (const prefix of prefixes) {
75
- const bucket = this.index.get(prefix)
99
+ function composedBases(
100
+ parentBases: string[],
101
+ name: string,
102
+ aliases: string[],
103
+ kind: 'command' | 'group'
104
+ ): string[] {
105
+ const variants = uniqueStrings([
106
+ normalizeSegment(`${kind} name`, name),
107
+ ...aliases.map((alias) => normalizeSegment(`${kind} alias`, alias)),
108
+ ])
109
+ const bases: string[] = []
110
+ const seen = new Set<string>()
111
+
112
+ for (const parent of parentBases) {
113
+ for (const variant of variants) {
114
+ const path = `${parent} ${variant}`
115
+ const key = path.toLowerCase()
116
+
117
+ if (seen.has(key)) continue
118
+
119
+ seen.add(key)
120
+ bases.push(path)
121
+ }
122
+ }
76
123
 
77
- if (!bucket) continue
124
+ return bases
125
+ }
78
126
 
79
- for (const name of names) {
80
- if (bucket.get(name) === registration) {
81
- bucket.delete(name)
82
- }
83
- }
127
+ function createGroupUsageHandler(children: GroupChild[]): AnyCommandHandler {
128
+ return async (_event, context, match) => {
129
+ const root = `${match.prefix}${match.command}`
130
+ const names = children.map((child) => child.name)
131
+ const lines = [
132
+ names.length > 0
133
+ ? `用法:${root} <${names.join('|')}>`
134
+ : `用法:${root}`,
135
+ ]
84
136
 
85
- if (bucket.size === 0) {
86
- this.index.delete(prefix)
87
- }
88
- }
137
+ for (const child of children) {
138
+ const usage = child.usage !== undefined ? ` ${child.usage}` : ''
139
+ const command = `${root} ${child.name}${usage}`
89
140
 
90
- this.registrations.delete(registration)
91
- pluginSet.delete(registration)
141
+ lines.push(
142
+ child.description !== undefined
143
+ ? `${command} - ${child.description}`
144
+ : command
145
+ )
146
+ }
92
147
 
93
- if (pluginSet.size === 0) {
94
- this.byPlugin.delete(registration.pluginId)
95
- }
148
+ await context.send(lines.join('\n'))
149
+ }
150
+ }
151
+
152
+ export class CommandRegistry {
153
+ private readonly index = new Map<string, Map<string, CommandRegistration>>()
154
+ private readonly registrations = new Set<CommandRegistration>()
155
+ private readonly byPlugin = new Map<string, Set<CommandRegistration>>()
156
+
157
+ register(registration: CommandRegistration): () => void {
158
+ const name = normalizeSegment('command name', registration.name)
159
+ const aliases = uniqueStrings(
160
+ registration.aliases.map((alias) =>
161
+ normalizeSegment('command alias', alias)
162
+ )
163
+ ).filter((alias) => alias.toLowerCase() !== name.toLowerCase())
164
+ const normalized = this.withParams({
165
+ ...registration,
166
+ name,
167
+ aliases,
168
+ })
169
+
170
+ return this.insert(normalized, [name, ...aliases])
171
+ }
172
+
173
+ group(
174
+ registration: CommandGroupRegistration,
175
+ setup: (group: CommandGroupRegistrar) => void
176
+ ): () => void {
177
+ const prefixes = uniqueStrings(registration.prefixes)
178
+ const bases = groupBases(registration.name, registration.aliases)
179
+ const children: GroupChild[] = []
180
+ const scope = this.createGroupScope(
181
+ registration.pluginId,
182
+ bases,
183
+ prefixes,
184
+ registration.permission,
185
+ children
186
+ )
187
+
188
+ try {
189
+ setup(scope.registrar)
190
+
191
+ const dispose = this.registerGroupRoot(
192
+ registration.pluginId,
193
+ bases,
194
+ prefixes,
195
+ registration.permission,
196
+ registration.description,
197
+ registration.params,
198
+ registration.handler,
199
+ children
200
+ )
201
+
202
+ scope.track(dispose)
203
+ } catch (error) {
204
+ scope.disposeAll()
205
+ throw error
96
206
  }
207
+
208
+ return scope.disposeAll
97
209
  }
98
210
 
99
211
  removePlugin(pluginId: string): void {
@@ -102,12 +214,11 @@ export class CommandRegistry {
102
214
  if (!registrations) return
103
215
 
104
216
  for (const registration of [...registrations]) {
105
- const prefixes = registration.prefixes
106
217
  const names = [registration.name, ...registration.aliases].map(
107
218
  (name) => name.toLowerCase()
108
219
  )
109
220
 
110
- for (const prefix of prefixes) {
221
+ for (const prefix of registration.prefixes) {
111
222
  const bucket = this.index.get(prefix)
112
223
 
113
224
  if (!bucket) continue
@@ -139,6 +250,9 @@ export class CommandRegistry {
139
250
  ...(registration.description !== undefined
140
251
  ? { description: registration.description }
141
252
  : {}),
253
+ ...(registration.usage !== undefined
254
+ ? { usage: registration.usage }
255
+ : {}),
142
256
  pluginId: registration.pluginId,
143
257
  }))
144
258
  }
@@ -159,22 +273,316 @@ export class CommandRegistry {
159
273
 
160
274
  if (!text) continue
161
275
 
162
- const [head = '', ...args] = text.split(/\s+/)
163
- const registration = this.index.get(prefix)?.get(head.toLowerCase())
276
+ const tokens = [...text.matchAll(/\S+/g)]
277
+
278
+ if (tokens.length === 0) continue
279
+
280
+ for (let count = tokens.length; count >= 1; count -= 1) {
281
+ const matched = tokens.slice(0, count)
282
+ const first = matched[0]
283
+ const last = matched[matched.length - 1]
284
+
285
+ if (!first || !last) continue
286
+
287
+ const key = matched
288
+ .map((token) => token[0])
289
+ .join(' ')
290
+ .toLowerCase()
291
+ const registration = this.index.get(prefix)?.get(key)
164
292
 
165
- if (!registration) continue
293
+ if (!registration) continue
166
294
 
167
- return {
168
- registration,
169
- match: {
170
- command: head,
171
- prefix,
172
- args,
173
- rest: text.slice(head.length).trim(),
174
- },
295
+ const start = first.index ?? 0
296
+ const end = (last.index ?? 0) + last[0].length
297
+
298
+ return {
299
+ registration,
300
+ match: {
301
+ command: text
302
+ .slice(start, end)
303
+ .replace(/\s+/g, ' ')
304
+ .trim(),
305
+ prefix,
306
+ args: tokens.slice(count).map((token) => token[0]),
307
+ rest: text.slice(end).trim(),
308
+ },
309
+ }
175
310
  }
176
311
  }
177
312
 
178
313
  return null
179
314
  }
315
+
316
+ private withParams(registration: CommandRegistration): CommandRegistration {
317
+ if (registration.params === undefined) return registration
318
+
319
+ const compiled = compileCommandParams(registration.params)
320
+
321
+ return {
322
+ ...registration,
323
+ paramsParser: compiled,
324
+ usage: compiled.usage,
325
+ }
326
+ }
327
+
328
+ private insert(
329
+ registration: CommandRegistration,
330
+ paths: string[]
331
+ ): () => void {
332
+ const prefixes = uniqueStrings(registration.prefixes)
333
+ const names = uniqueStrings(paths)
334
+
335
+ for (const prefix of prefixes) {
336
+ for (const name of names) {
337
+ const existing = this.index.get(prefix)?.get(name.toLowerCase())
338
+
339
+ if (existing) {
340
+ throw new Error(`command conflict: ${prefix}${name}`)
341
+ }
342
+ }
343
+ }
344
+
345
+ for (const prefix of prefixes) {
346
+ const bucket =
347
+ this.index.get(prefix) ?? new Map<string, CommandRegistration>()
348
+
349
+ for (const name of names) {
350
+ bucket.set(name.toLowerCase(), registration)
351
+ }
352
+
353
+ this.index.set(prefix, bucket)
354
+ }
355
+
356
+ this.registrations.add(registration)
357
+
358
+ const pluginSet =
359
+ this.byPlugin.get(registration.pluginId) ??
360
+ new Set<CommandRegistration>()
361
+ pluginSet.add(registration)
362
+ this.byPlugin.set(registration.pluginId, pluginSet)
363
+
364
+ let disposed = false
365
+
366
+ return () => {
367
+ if (disposed) return
368
+
369
+ disposed = true
370
+
371
+ for (const prefix of prefixes) {
372
+ const bucket = this.index.get(prefix)
373
+
374
+ if (!bucket) continue
375
+
376
+ for (const name of names) {
377
+ if (bucket.get(name.toLowerCase()) === registration) {
378
+ bucket.delete(name.toLowerCase())
379
+ }
380
+ }
381
+
382
+ if (bucket.size === 0) {
383
+ this.index.delete(prefix)
384
+ }
385
+ }
386
+
387
+ this.registrations.delete(registration)
388
+ pluginSet.delete(registration)
389
+
390
+ if (pluginSet.size === 0) {
391
+ this.byPlugin.delete(registration.pluginId)
392
+ }
393
+ }
394
+ }
395
+
396
+ private createGroupScope(
397
+ pluginId: string,
398
+ bases: string[],
399
+ prefixes: string[],
400
+ permission: Permission,
401
+ children: GroupChild[]
402
+ ): GroupScope {
403
+ const disposers: Array<() => void> = []
404
+ let disposed = false
405
+
406
+ const track = (
407
+ dispose: () => void,
408
+ cleanup?: () => void
409
+ ): (() => void) => {
410
+ let done = false
411
+
412
+ const tracked = (): void => {
413
+ if (done) return
414
+
415
+ done = true
416
+ cleanup?.()
417
+ dispose()
418
+ }
419
+
420
+ disposers.push(tracked)
421
+
422
+ return tracked
423
+ }
424
+
425
+ const disposeAll = (): void => {
426
+ if (disposed) return
427
+
428
+ disposed = true
429
+
430
+ for (const dispose of disposers.splice(0).reverse()) {
431
+ dispose()
432
+ }
433
+ }
434
+
435
+ const registrar: CommandGroupRegistrar = {
436
+ register: <Declaration extends string = string>(
437
+ options: CommandOptions<Declaration>,
438
+ handler: CommandHandler<
439
+ OneBotMessageEvent,
440
+ CommandParamArgs<Declaration>
441
+ >
442
+ ) => {
443
+ const childBases = composedBases(
444
+ bases,
445
+ options.name,
446
+ options.aliases ?? [],
447
+ 'command'
448
+ )
449
+ const name = childBases[0]
450
+
451
+ if (!name) {
452
+ throw new Error('command name must not be empty')
453
+ }
454
+
455
+ const registration = this.withParams({
456
+ pluginId,
457
+ name,
458
+ aliases: childBases.slice(1),
459
+ prefixes: options.prefixes ?? prefixes,
460
+ permission: options.permission ?? permission,
461
+ ...(options.description !== undefined
462
+ ? { description: options.description }
463
+ : {}),
464
+ ...(options.params !== undefined
465
+ ? { params: options.params }
466
+ : {}),
467
+ handler,
468
+ })
469
+ const dispose = this.insert(registration, childBases)
470
+ const record: GroupChild = {
471
+ name: options.name.trim(),
472
+ ...(options.description !== undefined
473
+ ? { description: options.description }
474
+ : {}),
475
+ ...(registration.usage !== undefined
476
+ ? { usage: registration.usage }
477
+ : {}),
478
+ }
479
+
480
+ children.push(record)
481
+
482
+ return track(dispose, () => {
483
+ const index = children.indexOf(record)
484
+
485
+ if (index >= 0) {
486
+ children.splice(index, 1)
487
+ }
488
+ })
489
+ },
490
+
491
+ group: <Declaration extends string = string>(
492
+ options: CommandGroupOptions<Declaration>,
493
+ nestedSetup: (group: CommandGroupRegistrar) => void
494
+ ) => {
495
+ const nestedBases = composedBases(
496
+ bases,
497
+ options.name,
498
+ options.aliases ?? [],
499
+ 'group'
500
+ )
501
+ const nestedPrefixes = options.prefixes ?? prefixes
502
+ const nestedPermission = options.permission ?? permission
503
+ const nestedChildren: GroupChild[] = []
504
+ const nestedScope = this.createGroupScope(
505
+ pluginId,
506
+ nestedBases,
507
+ nestedPrefixes,
508
+ nestedPermission,
509
+ nestedChildren
510
+ )
511
+
512
+ try {
513
+ nestedSetup(nestedScope.registrar)
514
+
515
+ const dispose = this.registerGroupRoot(
516
+ pluginId,
517
+ nestedBases,
518
+ nestedPrefixes,
519
+ nestedPermission,
520
+ options.description,
521
+ options.params,
522
+ options.handler,
523
+ nestedChildren
524
+ )
525
+
526
+ nestedScope.track(dispose)
527
+ } catch (error) {
528
+ nestedScope.disposeAll()
529
+ throw error
530
+ }
531
+
532
+ const record: GroupChild = {
533
+ name: options.name.trim(),
534
+ ...(options.description !== undefined
535
+ ? { description: options.description }
536
+ : {}),
537
+ ...(options.params !== undefined
538
+ ? {
539
+ usage: compileCommandParams(options.params).usage,
540
+ }
541
+ : {}),
542
+ }
543
+
544
+ children.push(record)
545
+
546
+ return track(nestedScope.disposeAll, () => {
547
+ const index = children.indexOf(record)
548
+
549
+ if (index >= 0) {
550
+ children.splice(index, 1)
551
+ }
552
+ })
553
+ },
554
+ }
555
+
556
+ return { registrar, disposeAll, track }
557
+ }
558
+
559
+ private registerGroupRoot(
560
+ pluginId: string,
561
+ bases: string[],
562
+ prefixes: string[],
563
+ permission: Permission,
564
+ description: string | undefined,
565
+ params: string | undefined,
566
+ handler: AnyCommandHandler | undefined,
567
+ children: GroupChild[]
568
+ ): () => void {
569
+ const name = bases[0]
570
+
571
+ if (!name) {
572
+ throw new Error('group name must not be empty')
573
+ }
574
+
575
+ const registration = this.withParams({
576
+ pluginId,
577
+ name,
578
+ aliases: bases.slice(1),
579
+ prefixes,
580
+ permission,
581
+ ...(description !== undefined ? { description } : {}),
582
+ ...(params !== undefined ? { params } : {}),
583
+ handler: handler ?? createGroupUsageHandler(children),
584
+ })
585
+
586
+ return this.insert(registration, bases)
587
+ }
180
588
  }