@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.
- package/README.md +13 -17
- package/dist/app.js +12 -40
- package/dist/app.js.map +1 -1
- package/dist/core/config/schema.d.ts +0 -2
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +0 -1
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/dispatch/command.d.ts +24 -2
- package/dist/core/dispatch/command.d.ts.map +1 -1
- package/dist/core/dispatch/command.js +297 -60
- package/dist/core/dispatch/command.js.map +1 -1
- package/dist/core/dispatch/router.d.ts +17 -3
- package/dist/core/dispatch/router.d.ts.map +1 -1
- package/dist/core/dispatch/router.js +38 -2
- package/dist/core/dispatch/router.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/plugin/command-params.d.ts +15 -0
- package/dist/core/plugin/command-params.d.ts.map +1 -0
- package/dist/core/plugin/command-params.js +427 -0
- package/dist/core/plugin/command-params.js.map +1 -0
- package/dist/core/plugin/context.d.ts.map +1 -1
- package/dist/core/plugin/context.js +27 -0
- package/dist/core/plugin/context.js.map +1 -1
- package/dist/core/plugin/install-dir.d.ts +6 -9
- package/dist/core/plugin/install-dir.d.ts.map +1 -1
- package/dist/core/plugin/install-dir.js +99 -79
- package/dist/core/plugin/install-dir.js.map +1 -1
- package/dist/core/plugin/install-validate.d.ts +2 -1
- package/dist/core/plugin/install-validate.d.ts.map +1 -1
- package/dist/core/plugin/install-validate.js +25 -17
- package/dist/core/plugin/install-validate.js.map +1 -1
- package/dist/core/plugin/installer.d.ts +3 -3
- package/dist/core/plugin/installer.d.ts.map +1 -1
- package/dist/core/plugin/installer.js +143 -140
- package/dist/core/plugin/installer.js.map +1 -1
- package/dist/core/plugin/loader.d.ts +1 -0
- package/dist/core/plugin/loader.d.ts.map +1 -1
- package/dist/core/plugin/loader.js +24 -2
- package/dist/core/plugin/loader.js.map +1 -1
- package/dist/core/plugin/types.d.ts +11 -3
- package/dist/core/plugin/types.d.ts.map +1 -1
- package/dist/core/plugin/types.js.map +1 -1
- package/dist/plugins/admin/index.d.ts.map +1 -1
- package/dist/plugins/admin/index.js +119 -86
- package/dist/plugins/admin/index.js.map +1 -1
- package/dist/plugins/core/index.d.ts.map +1 -1
- package/dist/plugins/core/index.js +4 -1
- package/dist/plugins/core/index.js.map +1 -1
- package/docs/architecture.md +22 -24
- package/docs/configuration.md +19 -23
- package/docs/feature-parity.md +4 -4
- package/docs/migration.md +13 -13
- package/docs/plugin-api.md +156 -26
- package/docs/plugin-development.md +390 -0
- package/docs/release.md +2 -2
- package/examples/plugins/hello/README.md +10 -5
- package/examples/plugins/hello/index.js +3 -6
- package/package.json +2 -1
- package/src/app.ts +16 -55
- package/src/core/config/schema.ts +0 -1
- package/src/core/dispatch/command.ts +474 -70
- package/src/core/dispatch/router.ts +77 -3
- package/src/core/index.ts +2 -0
- package/src/core/plugin/command-params.ts +595 -0
- package/src/core/plugin/context.ts +47 -2
- package/src/core/plugin/install-dir.ts +117 -111
- package/src/core/plugin/install-validate.ts +36 -34
- package/src/core/plugin/installer.ts +228 -184
- package/src/core/plugin/loader.ts +36 -2
- package/src/core/plugin/types.ts +23 -3
- package/src/plugins/admin/index.ts +302 -233
- package/src/plugins/core/index.ts +5 -1
- package/dist/core/plugin/installed.d.ts +0 -9
- package/dist/core/plugin/installed.d.ts.map +0 -1
- package/dist/core/plugin/installed.js +0 -113
- package/dist/core/plugin/installed.js.map +0 -1
- 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 {
|
|
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
|
|
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
|
},
|