@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
@@ -0,0 +1,667 @@
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
+ }
596
+
597
+ type Whitespace = ' ' | '\n' | '\r' | '\t' | '\v' | '\f'
598
+
599
+ type TrimLeft<Source extends string> =
600
+ Source extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : Source
601
+
602
+ type TrimRight<Source extends string> =
603
+ Source extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : Source
604
+
605
+ type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
606
+
607
+ type ParamValue<Type extends string> =
608
+ Trim<Type> extends 'string' | 'text'
609
+ ? string
610
+ : Trim<Type> extends 'number' | 'integer'
611
+ ? number
612
+ : Trim<Type> extends 'boolean'
613
+ ? boolean
614
+ : unknown
615
+
616
+ interface VariadicParam {
617
+ readonly variadic: true
618
+ }
619
+
620
+ type ParamType<Head extends string> = Head extends `${string}:${infer Type}`
621
+ ? ParamValue<Type>
622
+ : string
623
+
624
+ type ParamElement<Body extends string, Required extends boolean> =
625
+ Trim<Body> extends `...${string}`
626
+ ? VariadicParam
627
+ : Trim<Body> extends `${infer Head}=${string}`
628
+ ? ParamType<Head>
629
+ : Required extends true
630
+ ? ParamType<Body>
631
+ : ParamType<Body> | undefined
632
+
633
+ type AppendParam<Acc extends unknown[], Element> = [Element] extends [
634
+ VariadicParam,
635
+ ]
636
+ ? [...Acc, ...string[]]
637
+ : [...Acc, Element]
638
+
639
+ type ParseCommandParams<Source extends string, Acc extends unknown[] = []> =
640
+ Trim<Source> extends ''
641
+ ? Acc
642
+ : Trim<Source> extends `<${infer Body}>${infer Rest}`
643
+ ? ParamElement<Body, true> extends VariadicParam
644
+ ? any[]
645
+ : ParseCommandParams<
646
+ Rest,
647
+ AppendParam<Acc, ParamElement<Body, true>>
648
+ >
649
+ : Trim<Source> extends `[${infer Body}]${infer Rest}`
650
+ ? ParamElement<Body, false> extends VariadicParam
651
+ ? Trim<Rest> extends ''
652
+ ? [...Acc, ...string[]]
653
+ : any[]
654
+ : ParseCommandParams<
655
+ Rest,
656
+ AppendParam<Acc, ParamElement<Body, false>>
657
+ >
658
+ : any[]
659
+
660
+ /**
661
+ * 把 `params` 声明字符串推导成 handler 的位置参数 tuple。
662
+ *
663
+ * 非字面量声明(例如运行时拼接的 `string`)或无法解析的声明会回退为
664
+ * `any[]`,以保证现有插件仍然可以编译。
665
+ */
666
+ export type CommandParamArgs<Declaration extends string> =
667
+ string extends Declaration ? any[] : ParseCommandParams<Declaration>