@jsbot-tellus/core 2.0.3 → 2.1.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 (78) hide show
  1. package/README.md +13 -17
  2. package/dist/app.js +12 -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 +1 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/plugin/command-params.d.ts +15 -0
  19. package/dist/core/plugin/command-params.d.ts.map +1 -0
  20. package/dist/core/plugin/command-params.js +427 -0
  21. package/dist/core/plugin/command-params.js.map +1 -0
  22. package/dist/core/plugin/context.d.ts.map +1 -1
  23. package/dist/core/plugin/context.js +27 -0
  24. package/dist/core/plugin/context.js.map +1 -1
  25. package/dist/core/plugin/install-dir.d.ts +6 -9
  26. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  27. package/dist/core/plugin/install-dir.js +99 -79
  28. package/dist/core/plugin/install-dir.js.map +1 -1
  29. package/dist/core/plugin/install-validate.d.ts +2 -1
  30. package/dist/core/plugin/install-validate.d.ts.map +1 -1
  31. package/dist/core/plugin/install-validate.js +25 -17
  32. package/dist/core/plugin/install-validate.js.map +1 -1
  33. package/dist/core/plugin/installer.d.ts +3 -3
  34. package/dist/core/plugin/installer.d.ts.map +1 -1
  35. package/dist/core/plugin/installer.js +143 -140
  36. package/dist/core/plugin/installer.js.map +1 -1
  37. package/dist/core/plugin/loader.d.ts +1 -0
  38. package/dist/core/plugin/loader.d.ts.map +1 -1
  39. package/dist/core/plugin/loader.js +24 -2
  40. package/dist/core/plugin/loader.js.map +1 -1
  41. package/dist/core/plugin/types.d.ts +11 -3
  42. package/dist/core/plugin/types.d.ts.map +1 -1
  43. package/dist/core/plugin/types.js.map +1 -1
  44. package/dist/plugins/admin/index.d.ts.map +1 -1
  45. package/dist/plugins/admin/index.js +119 -86
  46. package/dist/plugins/admin/index.js.map +1 -1
  47. package/dist/plugins/core/index.d.ts.map +1 -1
  48. package/dist/plugins/core/index.js +4 -1
  49. package/dist/plugins/core/index.js.map +1 -1
  50. package/docs/architecture.md +22 -24
  51. package/docs/configuration.md +19 -23
  52. package/docs/feature-parity.md +4 -4
  53. package/docs/migration.md +13 -13
  54. package/docs/plugin-api.md +156 -26
  55. package/docs/plugin-development.md +390 -0
  56. package/docs/release.md +2 -2
  57. package/examples/plugins/hello/README.md +10 -5
  58. package/examples/plugins/hello/index.js +3 -6
  59. package/package.json +2 -1
  60. package/src/app.ts +16 -55
  61. package/src/core/config/schema.ts +0 -1
  62. package/src/core/dispatch/command.ts +474 -70
  63. package/src/core/dispatch/router.ts +77 -3
  64. package/src/core/index.ts +2 -0
  65. package/src/core/plugin/command-params.ts +595 -0
  66. package/src/core/plugin/context.ts +47 -2
  67. package/src/core/plugin/install-dir.ts +117 -111
  68. package/src/core/plugin/install-validate.ts +36 -34
  69. package/src/core/plugin/installer.ts +228 -184
  70. package/src/core/plugin/loader.ts +36 -2
  71. package/src/core/plugin/types.ts +23 -3
  72. package/src/plugins/admin/index.ts +302 -233
  73. package/src/plugins/core/index.ts +5 -1
  74. package/dist/core/plugin/installed.d.ts +0 -9
  75. package/dist/core/plugin/installed.d.ts.map +0 -1
  76. package/dist/core/plugin/installed.js +0 -113
  77. package/dist/core/plugin/installed.js.map +0 -1
  78. package/src/core/plugin/installed.ts +0 -165
@@ -0,0 +1,595 @@
1
+ export type CommandParamType =
2
+ 'string' | 'text' | 'number' | 'integer' | 'boolean'
3
+
4
+ export interface ParsedCommandParams {
5
+ args: unknown[]
6
+ ignored: string[]
7
+ }
8
+
9
+ export interface CompiledCommandParams {
10
+ usage: string
11
+ parse(input: string): ParsedCommandParams
12
+ }
13
+
14
+ export class CommandParamsError extends Error {
15
+ readonly usage: string
16
+
17
+ constructor(message: string, usage: string) {
18
+ super(message)
19
+ this.name = 'CommandParamsError'
20
+ this.usage = usage
21
+ }
22
+ }
23
+
24
+ interface ParamSpec {
25
+ name: string
26
+ type: CommandParamType
27
+ required: boolean
28
+ rest: boolean
29
+ hasDefault: boolean
30
+ defaultValue?: unknown
31
+ }
32
+
33
+ interface InputToken {
34
+ value: string
35
+ start: number
36
+ }
37
+
38
+ const NAME_PATTERN = /^[\p{L}_][\p{L}\p{N}_-]*$/u
39
+
40
+ function declarationError(message: string): Error {
41
+ return new Error(`参数声明错误:${message}`)
42
+ }
43
+
44
+ function parseType(value: string): CommandParamType {
45
+ switch (value) {
46
+ case 'string':
47
+ case 'text':
48
+ case 'number':
49
+ case 'integer':
50
+ case 'boolean':
51
+ return value
52
+ default:
53
+ throw declarationError(`未知的参数类型 "${value}"`)
54
+ }
55
+ }
56
+
57
+ function validateName(value: string): string {
58
+ const name = value.trim()
59
+
60
+ if (!name) {
61
+ throw declarationError('参数名不能为空')
62
+ }
63
+
64
+ if (!NAME_PATTERN.test(name)) {
65
+ throw declarationError(`非法的参数名 "${name}"`)
66
+ }
67
+
68
+ return name
69
+ }
70
+
71
+ function splitDeclaration(input: string): string[] {
72
+ const parts: string[] = []
73
+ let index = 0
74
+
75
+ while (index < input.length) {
76
+ const char = input[index]
77
+
78
+ if (char === undefined) break
79
+
80
+ if (/\s/.test(char)) {
81
+ index += 1
82
+ continue
83
+ }
84
+
85
+ if (char !== '<' && char !== '[') {
86
+ throw declarationError(`无法识别的字符 "${char}"`)
87
+ }
88
+
89
+ const close = char === '<' ? '>' : ']'
90
+ let end = index + 1
91
+ let quote: string | null = null
92
+
93
+ for (; end < input.length; end += 1) {
94
+ const current = input[end]
95
+
96
+ if (current === undefined) break
97
+
98
+ if (quote !== null) {
99
+ if (current === '\\') {
100
+ end += 1
101
+ continue
102
+ }
103
+
104
+ if (current === quote) {
105
+ quote = null
106
+ }
107
+
108
+ continue
109
+ }
110
+
111
+ if (current === '"' || current === "'") {
112
+ quote = current
113
+ continue
114
+ }
115
+
116
+ if (current === close) break
117
+ }
118
+
119
+ if (end >= input.length || input[end] !== close) {
120
+ throw declarationError(`参数 "${input.slice(index)}" 缺少 ${close}`)
121
+ }
122
+
123
+ parts.push(input.slice(index, end + 1))
124
+ index = end + 1
125
+ }
126
+
127
+ return parts
128
+ }
129
+
130
+ function unquote(value: string): string {
131
+ const trimmed = value.trim()
132
+ const quote = trimmed[0]
133
+
134
+ if (quote !== '"' && quote !== "'") {
135
+ return trimmed
136
+ }
137
+
138
+ if (trimmed.length < 2 || trimmed[trimmed.length - 1] !== quote) {
139
+ throw declarationError('默认值引号未闭合')
140
+ }
141
+
142
+ let result = ''
143
+
144
+ for (let index = 1; index < trimmed.length - 1; index += 1) {
145
+ const char = trimmed[index]
146
+
147
+ if (char === undefined) break
148
+
149
+ if (char === '\\') {
150
+ const next = trimmed[index + 1]
151
+
152
+ if (next !== undefined) {
153
+ result += next
154
+ index += 1
155
+ }
156
+
157
+ continue
158
+ }
159
+
160
+ result += char
161
+ }
162
+
163
+ return result
164
+ }
165
+
166
+ function parseBoolean(value: string): boolean | null {
167
+ const normalized = value.trim().toLowerCase()
168
+
169
+ if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) {
170
+ return true
171
+ }
172
+
173
+ if (['false', '0', 'no', 'n', 'off'].includes(normalized)) {
174
+ return false
175
+ }
176
+
177
+ return null
178
+ }
179
+
180
+ function parseDefaultValue(
181
+ type: CommandParamType,
182
+ raw: string,
183
+ name: string
184
+ ): unknown {
185
+ const value = unquote(raw)
186
+
187
+ switch (type) {
188
+ case 'string':
189
+ case 'text':
190
+ return value
191
+ case 'number': {
192
+ const number = value.trim() === '' ? Number.NaN : Number(value)
193
+
194
+ if (!Number.isFinite(number)) {
195
+ throw declarationError(`参数 ${name} 的默认值不是数字`)
196
+ }
197
+
198
+ return number
199
+ }
200
+ case 'integer': {
201
+ const number = value.trim() === '' ? Number.NaN : Number(value)
202
+
203
+ if (!Number.isInteger(number)) {
204
+ throw declarationError(`参数 ${name} 的默认值不是整数`)
205
+ }
206
+
207
+ return number
208
+ }
209
+ case 'boolean': {
210
+ const boolean = parseBoolean(value)
211
+
212
+ if (boolean === null) {
213
+ throw declarationError(`参数 ${name} 的默认值不是布尔值`)
214
+ }
215
+
216
+ return boolean
217
+ }
218
+ }
219
+ }
220
+
221
+ function parseToken(token: string): ParamSpec {
222
+ const open = token[0]
223
+ const close = token[token.length - 1]
224
+
225
+ if (
226
+ (open !== '<' && open !== '[') ||
227
+ (close !== '>' && close !== ']') ||
228
+ (open === '<') !== (close === '>')
229
+ ) {
230
+ throw declarationError(`非法的参数声明 "${token}"`)
231
+ }
232
+
233
+ const required = open === '<'
234
+ const inner = token.slice(1, -1).trim()
235
+
236
+ if (!inner) {
237
+ throw declarationError('参数名不能为空')
238
+ }
239
+
240
+ if (inner.startsWith('...')) {
241
+ if (required) {
242
+ throw declarationError('变长参数必须使用 [ ]')
243
+ }
244
+
245
+ const name = validateName(inner.slice(3))
246
+
247
+ return {
248
+ name,
249
+ type: 'string',
250
+ required: false,
251
+ rest: true,
252
+ hasDefault: false,
253
+ }
254
+ }
255
+
256
+ if (inner.includes('...')) {
257
+ throw declarationError(`非法的参数声明 "${token}"`)
258
+ }
259
+
260
+ const equals = inner.indexOf('=')
261
+ const head = equals >= 0 ? inner.slice(0, equals).trim() : inner.trim()
262
+ const defaultRaw = equals >= 0 ? inner.slice(equals + 1).trim() : undefined
263
+ const colon = head.indexOf(':')
264
+ const name = validateName(colon >= 0 ? head.slice(0, colon) : head)
265
+ const type = parseType(colon >= 0 ? head.slice(colon + 1).trim() : 'string')
266
+
267
+ if (required && defaultRaw !== undefined) {
268
+ throw declarationError(`必填参数不能有默认值:${token}`)
269
+ }
270
+
271
+ if (defaultRaw !== undefined) {
272
+ return {
273
+ name,
274
+ type,
275
+ required,
276
+ rest: false,
277
+ hasDefault: true,
278
+ defaultValue: parseDefaultValue(type, defaultRaw, name),
279
+ }
280
+ }
281
+
282
+ return {
283
+ name,
284
+ type,
285
+ required,
286
+ rest: false,
287
+ hasDefault: false,
288
+ }
289
+ }
290
+
291
+ function validateSpecs(specs: ParamSpec[]): void {
292
+ const names = new Set<string>()
293
+ let seenOptional = false
294
+ let seenRest = false
295
+ let seenText = false
296
+
297
+ for (let index = 0; index < specs.length; index += 1) {
298
+ const spec = specs[index]
299
+
300
+ if (!spec) continue
301
+
302
+ if (names.has(spec.name)) {
303
+ throw declarationError(`参数名重复 "${spec.name}"`)
304
+ }
305
+
306
+ names.add(spec.name)
307
+
308
+ if (seenRest) {
309
+ throw declarationError('变长参数后面不能再有参数')
310
+ }
311
+
312
+ if (seenText) {
313
+ throw declarationError('文本参数后面不能再有参数')
314
+ }
315
+
316
+ if (spec.rest) {
317
+ if (index !== specs.length - 1) {
318
+ throw declarationError('变长参数必须放在最后')
319
+ }
320
+
321
+ seenRest = true
322
+ continue
323
+ }
324
+
325
+ if (spec.type === 'text') {
326
+ if (index !== specs.length - 1) {
327
+ throw declarationError('文本参数必须放在最后')
328
+ }
329
+
330
+ seenText = true
331
+ }
332
+
333
+ if (spec.required) {
334
+ if (seenOptional) {
335
+ throw declarationError('必填参数不能出现在可选参数后面')
336
+ }
337
+ } else {
338
+ seenOptional = true
339
+ }
340
+ }
341
+ }
342
+
343
+ function usageOf(specs: ParamSpec[]): string {
344
+ return specs
345
+ .map((spec) => {
346
+ if (spec.rest) {
347
+ return `[...${spec.name}]`
348
+ }
349
+
350
+ const name = spec.type === 'text' ? `${spec.name}...` : spec.name
351
+
352
+ return spec.required ? `<${name}>` : `[${name}]`
353
+ })
354
+ .join(' ')
355
+ }
356
+
357
+ function tokenizeInput(input: string, usage: string): InputToken[] {
358
+ const tokens: InputToken[] = []
359
+ let index = 0
360
+
361
+ while (index < input.length) {
362
+ const char = input[index]
363
+
364
+ if (char === undefined) break
365
+
366
+ if (/\s/.test(char)) {
367
+ index += 1
368
+ continue
369
+ }
370
+
371
+ const start = index
372
+ let value = ''
373
+ let quote: string | null = null
374
+
375
+ while (index < input.length) {
376
+ const current = input[index]
377
+
378
+ if (current === undefined) break
379
+
380
+ if (quote !== null) {
381
+ if (current === '\\') {
382
+ const next = input[index + 1]
383
+
384
+ if (next !== undefined) {
385
+ value += next
386
+ index += 2
387
+ continue
388
+ }
389
+
390
+ value += current
391
+ index += 1
392
+ continue
393
+ }
394
+
395
+ if (current === quote) {
396
+ quote = null
397
+ index += 1
398
+ continue
399
+ }
400
+
401
+ value += current
402
+ index += 1
403
+ continue
404
+ }
405
+
406
+ if (/\s/.test(current)) break
407
+
408
+ if (current === '"' || current === "'") {
409
+ quote = current
410
+ index += 1
411
+ continue
412
+ }
413
+
414
+ if (current === '\\') {
415
+ const next = input[index + 1]
416
+
417
+ if (next !== undefined) {
418
+ value += next
419
+ index += 2
420
+ continue
421
+ }
422
+
423
+ value += current
424
+ index += 1
425
+ continue
426
+ }
427
+
428
+ value += current
429
+ index += 1
430
+ }
431
+
432
+ if (quote !== null) {
433
+ throw new CommandParamsError('参数引号未闭合', usage)
434
+ }
435
+
436
+ tokens.push({ value, start })
437
+ }
438
+
439
+ return tokens
440
+ }
441
+
442
+ function parseValue(spec: ParamSpec, raw: string, usage: string): unknown {
443
+ switch (spec.type) {
444
+ case 'string':
445
+ case 'text':
446
+ return raw
447
+ case 'number': {
448
+ const value = raw.trim() === '' ? Number.NaN : Number(raw)
449
+
450
+ if (!Number.isFinite(value)) {
451
+ throw new CommandParamsError(
452
+ `参数 ${spec.name} 需要是数字`,
453
+ usage
454
+ )
455
+ }
456
+
457
+ return value
458
+ }
459
+ case 'integer': {
460
+ const value = raw.trim() === '' ? Number.NaN : Number(raw)
461
+
462
+ if (!Number.isInteger(value)) {
463
+ throw new CommandParamsError(
464
+ `参数 ${spec.name} 需要是整数`,
465
+ usage
466
+ )
467
+ }
468
+
469
+ return value
470
+ }
471
+ case 'boolean': {
472
+ const value = parseBoolean(raw)
473
+
474
+ if (value === null) {
475
+ throw new CommandParamsError(
476
+ `参数 ${spec.name} 需要是布尔值`,
477
+ usage
478
+ )
479
+ }
480
+
481
+ return value
482
+ }
483
+ }
484
+ }
485
+
486
+ function parseInput(
487
+ specs: ParamSpec[],
488
+ input: string,
489
+ usage: string
490
+ ): ParsedCommandParams {
491
+ const tokens = tokenizeInput(input, usage)
492
+ const args: unknown[] = []
493
+ let index = 0
494
+
495
+ for (const spec of specs) {
496
+ if (spec.rest) {
497
+ for (
498
+ let tokenIndex = index;
499
+ tokenIndex < tokens.length;
500
+ tokenIndex += 1
501
+ ) {
502
+ const token = tokens[tokenIndex]
503
+
504
+ if (token) {
505
+ args.push(token.value)
506
+ }
507
+ }
508
+
509
+ index = tokens.length
510
+ continue
511
+ }
512
+
513
+ if (spec.type === 'text') {
514
+ const token = tokens[index]
515
+
516
+ if (!token) {
517
+ if (spec.hasDefault) {
518
+ args.push(spec.defaultValue)
519
+ } else if (spec.required) {
520
+ throw new CommandParamsError(
521
+ `缺少参数:${spec.name}`,
522
+ usage
523
+ )
524
+ } else {
525
+ args.push(undefined)
526
+ }
527
+
528
+ continue
529
+ }
530
+
531
+ const remaining = tokens.length - index
532
+ const value =
533
+ remaining === 1 ? token.value : input.slice(token.start).trim()
534
+
535
+ args.push(value)
536
+ index = tokens.length
537
+ continue
538
+ }
539
+
540
+ const token = tokens[index]
541
+
542
+ if (!token) {
543
+ if (spec.hasDefault) {
544
+ args.push(spec.defaultValue)
545
+ } else if (spec.required) {
546
+ throw new CommandParamsError(`缺少参数:${spec.name}`, usage)
547
+ } else {
548
+ args.push(undefined)
549
+ }
550
+
551
+ continue
552
+ }
553
+
554
+ index += 1
555
+ args.push(parseValue(spec, token.value, usage))
556
+ }
557
+
558
+ while (args.length > 0 && args[args.length - 1] === undefined) {
559
+ args.pop()
560
+ }
561
+
562
+ const ignored =
563
+ index < tokens.length
564
+ ? tokens.slice(index).map((token) => token.value)
565
+ : []
566
+
567
+ return { args, ignored }
568
+ }
569
+
570
+ export function compileCommandParams(
571
+ declaration: string
572
+ ): CompiledCommandParams {
573
+ const trimmed = declaration.trim()
574
+
575
+ if (!trimmed) {
576
+ throw declarationError('参数声明不能为空')
577
+ }
578
+
579
+ const specs = splitDeclaration(trimmed).map(parseToken)
580
+
581
+ if (specs.length === 0) {
582
+ throw declarationError('参数声明不能为空')
583
+ }
584
+
585
+ validateSpecs(specs)
586
+
587
+ const usage = usageOf(specs)
588
+
589
+ return {
590
+ usage,
591
+ parse(input: string): ParsedCommandParams {
592
+ return parseInput(specs, input, usage)
593
+ },
594
+ }
595
+ }
@@ -1,10 +1,19 @@
1
+ import type { OneBotMessageEvent } from '@snowluma/sdk'
1
2
  import type { BotApi } from '../api.js'
2
3
  import type { Router } from '../dispatch/router.js'
3
4
  import type { TaskQueue } from '../dispatch/task-queue.js'
4
5
  import type { Logger } from '../logging/logger.js'
5
6
  import type { SessionStore } from '../storage/session.js'
6
7
  import type { PluginKV } from './kv.js'
7
- import type { CoreServices, PluginContext, PluginDefinition } from './types.js'
8
+ import type {
9
+ CommandGroupOptions,
10
+ CommandGroupRegistrar,
11
+ CommandHandler,
12
+ CommandOptions,
13
+ CoreServices,
14
+ PluginContext,
15
+ PluginDefinition,
16
+ } from './types.js'
8
17
 
9
18
  export interface PluginContextDeps {
10
19
  config: unknown
@@ -34,7 +43,10 @@ export function createPluginContext<TConfig>(
34
43
  core: deps.core,
35
44
 
36
45
  commands: {
37
- register(options, handler) {
46
+ register<TArgs extends unknown[] = []>(
47
+ options: CommandOptions,
48
+ handler: CommandHandler<OneBotMessageEvent, TArgs>
49
+ ) {
38
50
  return deps.router.registerCommand({
39
51
  pluginId: id,
40
52
  name: options.name,
@@ -50,9 +62,42 @@ export function createPluginContext<TConfig>(
50
62
  ...(options.description !== undefined
51
63
  ? { description: options.description }
52
64
  : {}),
65
+ ...(options.params !== undefined
66
+ ? { params: options.params }
67
+ : {}),
53
68
  handler,
54
69
  })
55
70
  },
71
+ group<TArgs extends unknown[] = []>(
72
+ options: CommandGroupOptions<TArgs>,
73
+ setup: (group: CommandGroupRegistrar) => void
74
+ ) {
75
+ return deps.router.registerCommandGroup(
76
+ {
77
+ pluginId: id,
78
+ name: options.name,
79
+ ...(options.aliases !== undefined
80
+ ? { aliases: options.aliases }
81
+ : {}),
82
+ ...(options.prefixes !== undefined
83
+ ? { prefixes: options.prefixes }
84
+ : {}),
85
+ ...(options.permission !== undefined
86
+ ? { permission: options.permission }
87
+ : {}),
88
+ ...(options.description !== undefined
89
+ ? { description: options.description }
90
+ : {}),
91
+ ...(options.params !== undefined
92
+ ? { params: options.params }
93
+ : {}),
94
+ ...(options.handler !== undefined
95
+ ? { handler: options.handler }
96
+ : {}),
97
+ },
98
+ setup
99
+ )
100
+ },
56
101
  list() {
57
102
  return deps.router.listCommands()
58
103
  },