@jsbot-tellus/core 2.5.0 → 2.6.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 (175) 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/plugin/command-params.d.ts +3 -37
  13. package/dist/core/plugin/command-params.d.ts.map +1 -1
  14. package/dist/core/plugin/command-params.js +2 -580
  15. package/dist/core/plugin/command-params.js.map +1 -1
  16. package/dist/core/plugin/install/paths.d.ts +7 -0
  17. package/dist/core/plugin/install/paths.d.ts.map +1 -0
  18. package/dist/core/plugin/install/paths.js +49 -0
  19. package/dist/core/plugin/install/paths.js.map +1 -0
  20. package/dist/core/plugin/install/state.d.ts +23 -0
  21. package/dist/core/plugin/install/state.d.ts.map +1 -0
  22. package/dist/core/plugin/install/state.js +129 -0
  23. package/dist/core/plugin/install/state.js.map +1 -0
  24. package/dist/core/plugin/install/validate-script.d.ts +3 -0
  25. package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
  26. package/dist/core/plugin/install/validate-script.js +154 -0
  27. package/dist/core/plugin/install/validate-script.js.map +1 -0
  28. package/dist/core/plugin/install-dir.d.ts +3 -28
  29. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  30. package/dist/core/plugin/install-dir.js +3 -326
  31. package/dist/core/plugin/install-dir.js.map +1 -1
  32. package/dist/core/plugin/installer/checks.d.ts +6 -0
  33. package/dist/core/plugin/installer/checks.d.ts.map +1 -0
  34. package/dist/core/plugin/installer/checks.js +99 -0
  35. package/dist/core/plugin/installer/checks.js.map +1 -0
  36. package/dist/core/plugin/installer/support.d.ts +37 -0
  37. package/dist/core/plugin/installer/support.d.ts.map +1 -0
  38. package/dist/core/plugin/installer/support.js +67 -0
  39. package/dist/core/plugin/installer/support.js.map +1 -0
  40. package/dist/core/plugin/installer/warnings.d.ts +5 -0
  41. package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
  42. package/dist/core/plugin/installer/warnings.js +28 -0
  43. package/dist/core/plugin/installer/warnings.js.map +1 -0
  44. package/dist/core/plugin/installer.d.ts +4 -30
  45. package/dist/core/plugin/installer.d.ts.map +1 -1
  46. package/dist/core/plugin/installer.js +21 -202
  47. package/dist/core/plugin/installer.js.map +1 -1
  48. package/dist/core/plugin/params/compile.d.ts +3 -0
  49. package/dist/core/plugin/params/compile.d.ts.map +1 -0
  50. package/dist/core/plugin/params/compile.js +23 -0
  51. package/dist/core/plugin/params/compile.js.map +1 -0
  52. package/dist/core/plugin/params/declaration.d.ts +9 -0
  53. package/dist/core/plugin/params/declaration.d.ts.map +1 -0
  54. package/dist/core/plugin/params/declaration.js +286 -0
  55. package/dist/core/plugin/params/declaration.js.map +1 -0
  56. package/dist/core/plugin/params/input.d.ts +5 -0
  57. package/dist/core/plugin/params/input.d.ts.map +1 -0
  58. package/dist/core/plugin/params/input.js +196 -0
  59. package/dist/core/plugin/params/input.js.map +1 -0
  60. package/dist/core/plugin/params/reply.d.ts +3 -0
  61. package/dist/core/plugin/params/reply.d.ts.map +1 -0
  62. package/dist/core/plugin/params/reply.js +49 -0
  63. package/dist/core/plugin/params/reply.js.map +1 -0
  64. package/dist/core/plugin/params/type-level.d.ts +24 -0
  65. package/dist/core/plugin/params/type-level.d.ts.map +1 -0
  66. package/dist/core/plugin/params/type-level.js +2 -0
  67. package/dist/core/plugin/params/type-level.js.map +1 -0
  68. package/dist/core/plugin/params/types.d.ts +33 -0
  69. package/dist/core/plugin/params/types.d.ts.map +1 -0
  70. package/dist/core/plugin/params/types.js +12 -0
  71. package/dist/core/plugin/params/types.js.map +1 -0
  72. package/dist/core/plugin/params/values.d.ts +7 -0
  73. package/dist/core/plugin/params/values.d.ts.map +1 -0
  74. package/dist/core/plugin/params/values.js +111 -0
  75. package/dist/core/plugin/params/values.js.map +1 -0
  76. package/dist/core/web/api.d.ts +2 -14
  77. package/dist/core/web/api.d.ts.map +1 -1
  78. package/dist/core/web/api.js +8 -500
  79. package/dist/core/web/api.js.map +1 -1
  80. package/dist/core/web/routes/actions.d.ts +4 -0
  81. package/dist/core/web/routes/actions.d.ts.map +1 -0
  82. package/dist/core/web/routes/actions.js +33 -0
  83. package/dist/core/web/routes/actions.js.map +1 -0
  84. package/dist/core/web/routes/config.d.ts +5 -0
  85. package/dist/core/web/routes/config.d.ts.map +1 -0
  86. package/dist/core/web/routes/config.js +30 -0
  87. package/dist/core/web/routes/config.js.map +1 -0
  88. package/dist/core/web/routes/kv.d.ts +6 -0
  89. package/dist/core/web/routes/kv.d.ts.map +1 -0
  90. package/dist/core/web/routes/kv.js +80 -0
  91. package/dist/core/web/routes/kv.js.map +1 -0
  92. package/dist/core/web/routes/plugins.d.ts +5 -0
  93. package/dist/core/web/routes/plugins.d.ts.map +1 -0
  94. package/dist/core/web/routes/plugins.js +98 -0
  95. package/dist/core/web/routes/plugins.js.map +1 -0
  96. package/dist/core/web/routes/sessions.d.ts +8 -0
  97. package/dist/core/web/routes/sessions.d.ts.map +1 -0
  98. package/dist/core/web/routes/sessions.js +130 -0
  99. package/dist/core/web/routes/sessions.js.map +1 -0
  100. package/dist/core/web/routes/shared.d.ts +18 -0
  101. package/dist/core/web/routes/shared.d.ts.map +1 -0
  102. package/dist/core/web/routes/shared.js +19 -0
  103. package/dist/core/web/routes/shared.js.map +1 -0
  104. package/dist/core/web/routes/status.d.ts +6 -0
  105. package/dist/core/web/routes/status.d.ts.map +1 -0
  106. package/dist/core/web/routes/status.js +132 -0
  107. package/dist/core/web/routes/status.js.map +1 -0
  108. package/dist/core/web/server/auth.d.ts +17 -0
  109. package/dist/core/web/server/auth.d.ts.map +1 -0
  110. package/dist/core/web/server/auth.js +68 -0
  111. package/dist/core/web/server/auth.js.map +1 -0
  112. package/dist/core/web/server/csp.d.ts +11 -0
  113. package/dist/core/web/server/csp.d.ts.map +1 -0
  114. package/dist/core/web/server/csp.js +31 -0
  115. package/dist/core/web/server/csp.js.map +1 -0
  116. package/dist/core/web/server/request.d.ts +6 -0
  117. package/dist/core/web/server/request.d.ts.map +1 -0
  118. package/dist/core/web/server/request.js +33 -0
  119. package/dist/core/web/server/request.js.map +1 -0
  120. package/dist/core/web/server/security.d.ts +4 -0
  121. package/dist/core/web/server/security.d.ts.map +1 -0
  122. package/dist/core/web/server/security.js +12 -0
  123. package/dist/core/web/server/security.js.map +1 -0
  124. package/dist/core/web/server/sse.d.ts +18 -0
  125. package/dist/core/web/server/sse.d.ts.map +1 -0
  126. package/dist/core/web/server/sse.js +49 -0
  127. package/dist/core/web/server/sse.js.map +1 -0
  128. package/dist/core/web/server/static.d.ts +16 -0
  129. package/dist/core/web/server/static.d.ts.map +1 -0
  130. package/dist/core/web/server/static.js +80 -0
  131. package/dist/core/web/server/static.js.map +1 -0
  132. package/dist/core/web/server.d.ts +0 -7
  133. package/dist/core/web/server.d.ts.map +1 -1
  134. package/dist/core/web/server.js +41 -263
  135. package/dist/core/web/server.js.map +1 -1
  136. package/docs/architecture.md +28 -11
  137. package/docs/configuration.md +1 -1
  138. package/docs/migration.md +2 -2
  139. package/docs/plugin-api.md +24 -11
  140. package/docs/plugin-development.md +1 -1
  141. package/package.json +19 -17
  142. package/src/app/cli.ts +47 -0
  143. package/src/app/runtime.ts +522 -0
  144. package/src/app.ts +87 -395
  145. package/src/core/plugin/command-params.ts +8 -871
  146. package/src/core/plugin/install/paths.ts +72 -0
  147. package/src/core/plugin/install/state.ts +196 -0
  148. package/src/core/plugin/install/validate-script.ts +154 -0
  149. package/src/core/plugin/install-dir.ts +19 -427
  150. package/src/core/plugin/installer/checks.ts +137 -0
  151. package/src/core/plugin/installer/support.ts +120 -0
  152. package/src/core/plugin/installer/warnings.ts +52 -0
  153. package/src/core/plugin/installer.ts +52 -301
  154. package/src/core/plugin/params/compile.ts +41 -0
  155. package/src/core/plugin/params/declaration.ts +394 -0
  156. package/src/core/plugin/params/input.ts +281 -0
  157. package/src/core/plugin/params/reply.ts +59 -0
  158. package/src/core/plugin/params/type-level.ts +75 -0
  159. package/src/core/plugin/params/types.ts +55 -0
  160. package/src/core/plugin/params/values.ts +150 -0
  161. package/src/core/web/api.ts +33 -759
  162. package/src/core/web/routes/actions.ts +43 -0
  163. package/src/core/web/routes/config.ts +42 -0
  164. package/src/core/web/routes/kv.ts +122 -0
  165. package/src/core/web/routes/plugins.ts +151 -0
  166. package/src/core/web/routes/sessions.ts +188 -0
  167. package/src/core/web/routes/shared.ts +42 -0
  168. package/src/core/web/routes/status.ts +191 -0
  169. package/src/core/web/server/auth.ts +126 -0
  170. package/src/core/web/server/csp.ts +33 -0
  171. package/src/core/web/server/request.ts +38 -0
  172. package/src/core/web/server/security.ts +14 -0
  173. package/src/core/web/server/sse.ts +74 -0
  174. package/src/core/web/server/static.ts +112 -0
  175. package/src/core/web/server.ts +41 -383
@@ -0,0 +1,394 @@
1
+ import {
2
+ declarationError,
3
+ type CommandParamType,
4
+ type ParamLength,
5
+ type ParamSpec,
6
+ } from './types.js'
7
+ import {
8
+ measureLength,
9
+ parseBoolean,
10
+ parseTimeValue,
11
+ unquote,
12
+ } from './values.js'
13
+
14
+ const NAME_PATTERN = /^[\p{L}_][\p{L}\p{N}_-]*$/u
15
+ const LENGTH_PATTERN = /^(\d*)\.\.(\d*)$/
16
+
17
+ export function parseType(value: string): CommandParamType {
18
+ switch (value) {
19
+ case 'string':
20
+ case 'text':
21
+ case 'number':
22
+ case 'integer':
23
+ case 'boolean':
24
+ case 'at':
25
+ case 'reply':
26
+ case 'time':
27
+ return value
28
+ default:
29
+ throw declarationError(`未知的参数类型 "${value}"`)
30
+ }
31
+ }
32
+
33
+ interface ParsedType {
34
+ type: CommandParamType
35
+ length?: ParamLength | undefined
36
+ }
37
+
38
+ function parseLengthNumber(raw: string, token: string): number {
39
+ const value = Number(raw)
40
+
41
+ if (!Number.isSafeInteger(value)) {
42
+ throw declarationError(`长度限制不是安全的整数:${token}`)
43
+ }
44
+
45
+ return value
46
+ }
47
+
48
+ function parseLength(raw: string, token: string): ParamLength {
49
+ const value = raw.trim()
50
+ const match = LENGTH_PATTERN.exec(value)
51
+
52
+ if (!match) {
53
+ throw declarationError(`非法的长度限制 "${raw}":${token}`)
54
+ }
55
+
56
+ const minRaw = match[1] ?? ''
57
+ const maxRaw = match[2] ?? ''
58
+
59
+ if (minRaw === '' && maxRaw === '') {
60
+ throw declarationError(`长度限制不能为空:${token}`)
61
+ }
62
+
63
+ const min = minRaw === '' ? undefined : parseLengthNumber(minRaw, token)
64
+ const max = maxRaw === '' ? undefined : parseLengthNumber(maxRaw, token)
65
+
66
+ if (min !== undefined && max !== undefined && min > max) {
67
+ throw declarationError(`长度下限不能大于上限:${token}`)
68
+ }
69
+
70
+ return { min, max }
71
+ }
72
+
73
+ function parseTypeDeclaration(raw: string, token: string): ParsedType {
74
+ const value = raw.trim()
75
+ const open = value.indexOf('(')
76
+
77
+ if (open < 0) {
78
+ return { type: parseType(value) }
79
+ }
80
+
81
+ if (!value.endsWith(')')) {
82
+ throw declarationError(`非法的参数声明 "${token}"`)
83
+ }
84
+
85
+ const type = parseType(value.slice(0, open).trim())
86
+ const length = parseLength(value.slice(open + 1, -1), token)
87
+
88
+ if (type !== 'string' && type !== 'text') {
89
+ throw declarationError(`只有 string/text 参数支持长度限制:${token}`)
90
+ }
91
+
92
+ return { type, length }
93
+ }
94
+
95
+ function validateDefaultLength(
96
+ name: string,
97
+ value: string,
98
+ length: ParamLength | undefined
99
+ ): void {
100
+ if (length === undefined) return
101
+
102
+ const measured = measureLength(value)
103
+
104
+ if (length.min !== undefined && measured < length.min) {
105
+ throw declarationError(
106
+ `参数 ${name} 的默认值至少需要 ${length.min} 个字符`
107
+ )
108
+ }
109
+
110
+ if (length.max !== undefined && measured > length.max) {
111
+ throw declarationError(
112
+ `参数 ${name} 的默认值不能超过 ${length.max} 个字符`
113
+ )
114
+ }
115
+ }
116
+
117
+ export function validateName(value: string): string {
118
+ const name = value.trim()
119
+
120
+ if (!name) {
121
+ throw declarationError('参数名不能为空')
122
+ }
123
+
124
+ if (!NAME_PATTERN.test(name)) {
125
+ throw declarationError(`非法的参数名 "${name}"`)
126
+ }
127
+
128
+ return name
129
+ }
130
+
131
+ export function splitDeclaration(input: string): string[] {
132
+ const parts: string[] = []
133
+ let index = 0
134
+
135
+ while (index < input.length) {
136
+ const char = input[index]
137
+
138
+ if (char === undefined) break
139
+
140
+ if (/\s/.test(char)) {
141
+ index += 1
142
+ continue
143
+ }
144
+
145
+ if (char !== '<' && char !== '[') {
146
+ throw declarationError(`无法识别的字符 "${char}"`)
147
+ }
148
+
149
+ const close = char === '<' ? '>' : ']'
150
+ let end = index + 1
151
+ let quote: string | null = null
152
+
153
+ for (; end < input.length; end += 1) {
154
+ const current = input[end]
155
+
156
+ if (current === undefined) break
157
+
158
+ if (quote !== null) {
159
+ if (current === '\\') {
160
+ end += 1
161
+ continue
162
+ }
163
+
164
+ if (current === quote) {
165
+ quote = null
166
+ }
167
+
168
+ continue
169
+ }
170
+
171
+ if (current === '"' || current === "'") {
172
+ quote = current
173
+ continue
174
+ }
175
+
176
+ if (current === close) break
177
+ }
178
+
179
+ if (end >= input.length || input[end] !== close) {
180
+ throw declarationError(`参数 "${input.slice(index)}" 缺少 ${close}`)
181
+ }
182
+
183
+ parts.push(input.slice(index, end + 1))
184
+ index = end + 1
185
+ }
186
+
187
+ return parts
188
+ }
189
+
190
+ export function parseToken(token: string): ParamSpec {
191
+ const open = token[0]
192
+ const close = token[token.length - 1]
193
+
194
+ if (
195
+ (open !== '<' && open !== '[') ||
196
+ (close !== '>' && close !== ']') ||
197
+ (open === '<') !== (close === '>')
198
+ ) {
199
+ throw declarationError(`非法的参数声明 "${token}"`)
200
+ }
201
+
202
+ const required = open === '<'
203
+ const inner = token.slice(1, -1).trim()
204
+
205
+ if (!inner) {
206
+ throw declarationError('参数名不能为空')
207
+ }
208
+
209
+ if (inner.startsWith('...')) {
210
+ if (required) {
211
+ throw declarationError('变长参数必须使用 [ ]')
212
+ }
213
+
214
+ const rest = inner.slice(3)
215
+
216
+ if (rest.includes('(')) {
217
+ throw declarationError(`变长参数不支持长度限制:${token}`)
218
+ }
219
+
220
+ const name = validateName(rest)
221
+
222
+ return {
223
+ name,
224
+ type: 'string',
225
+ required: false,
226
+ rest: true,
227
+ hasDefault: false,
228
+ }
229
+ }
230
+
231
+ if (inner.includes('...')) {
232
+ throw declarationError(`非法的参数声明 "${token}"`)
233
+ }
234
+
235
+ const equals = inner.indexOf('=')
236
+ const head = equals >= 0 ? inner.slice(0, equals).trim() : inner.trim()
237
+ const defaultRaw = equals >= 0 ? inner.slice(equals + 1).trim() : undefined
238
+ const colon = head.indexOf(':')
239
+ const name = validateName(colon >= 0 ? head.slice(0, colon) : head)
240
+ const { type, length } = parseTypeDeclaration(
241
+ colon >= 0 ? head.slice(colon + 1) : 'string',
242
+ token
243
+ )
244
+
245
+ if (required && defaultRaw !== undefined) {
246
+ throw declarationError(`必填参数不能有默认值:${token}`)
247
+ }
248
+
249
+ if (defaultRaw !== undefined) {
250
+ const defaultValue = parseDefaultValue(type, defaultRaw, name)
251
+
252
+ if (typeof defaultValue === 'string') {
253
+ validateDefaultLength(name, defaultValue, length)
254
+ }
255
+
256
+ return {
257
+ name,
258
+ type,
259
+ required,
260
+ rest: false,
261
+ hasDefault: true,
262
+ defaultValue,
263
+ length,
264
+ }
265
+ }
266
+
267
+ return {
268
+ name,
269
+ type,
270
+ required,
271
+ rest: false,
272
+ hasDefault: false,
273
+ length,
274
+ }
275
+ }
276
+
277
+ export function validateSpecs(specs: ParamSpec[]): void {
278
+ const names = new Set<string>()
279
+ let seenOptional = false
280
+ let seenRest = false
281
+ let seenText = false
282
+
283
+ for (let index = 0; index < specs.length; index += 1) {
284
+ const spec = specs[index]
285
+
286
+ if (!spec) continue
287
+
288
+ if (names.has(spec.name)) {
289
+ throw declarationError(`参数名重复 "${spec.name}"`)
290
+ }
291
+
292
+ names.add(spec.name)
293
+
294
+ if (seenRest) {
295
+ throw declarationError('变长参数后面不能再有参数')
296
+ }
297
+
298
+ if (seenText) {
299
+ throw declarationError('文本参数后面不能再有参数')
300
+ }
301
+
302
+ if (spec.rest) {
303
+ if (index !== specs.length - 1) {
304
+ throw declarationError('变长参数必须放在最后')
305
+ }
306
+
307
+ seenRest = true
308
+ continue
309
+ }
310
+
311
+ if (spec.type === 'text') {
312
+ if (index !== specs.length - 1) {
313
+ throw declarationError('文本参数必须放在最后')
314
+ }
315
+
316
+ seenText = true
317
+ }
318
+
319
+ if (spec.required) {
320
+ if (seenOptional) {
321
+ throw declarationError('必填参数不能出现在可选参数后面')
322
+ }
323
+ } else {
324
+ seenOptional = true
325
+ }
326
+ }
327
+ }
328
+
329
+ export function usageOf(specs: ParamSpec[]): string {
330
+ return specs
331
+ .map((spec) => {
332
+ if (spec.rest) {
333
+ return `[...${spec.name}]`
334
+ }
335
+
336
+ const name = spec.type === 'text' ? `${spec.name}...` : spec.name
337
+
338
+ return spec.required ? `<${name}>` : `[${name}]`
339
+ })
340
+ .join(' ')
341
+ }
342
+
343
+ export function parseDefaultValue(
344
+ type: CommandParamType,
345
+ raw: string,
346
+ name: string
347
+ ): unknown {
348
+ const value = unquote(raw)
349
+
350
+ switch (type) {
351
+ case 'string':
352
+ case 'text':
353
+ return value
354
+ case 'number': {
355
+ const number = value.trim() === '' ? Number.NaN : Number(value)
356
+
357
+ if (!Number.isFinite(number)) {
358
+ throw declarationError(`参数 ${name} 的默认值不是数字`)
359
+ }
360
+
361
+ return number
362
+ }
363
+ case 'integer': {
364
+ const number = value.trim() === '' ? Number.NaN : Number(value)
365
+
366
+ if (!Number.isInteger(number)) {
367
+ throw declarationError(`参数 ${name} 的默认值不是整数`)
368
+ }
369
+
370
+ return number
371
+ }
372
+ case 'boolean': {
373
+ const boolean = parseBoolean(value)
374
+
375
+ if (boolean === null) {
376
+ throw declarationError(`参数 ${name} 的默认值不是布尔值`)
377
+ }
378
+
379
+ return boolean
380
+ }
381
+ case 'at':
382
+ case 'reply':
383
+ throw declarationError(`参数 ${name} 不支持默认值`)
384
+ case 'time': {
385
+ const seconds = parseTimeValue(value)
386
+
387
+ if (seconds === null) {
388
+ throw declarationError(`参数 ${name} 的默认值不是时间`)
389
+ }
390
+
391
+ return seconds
392
+ }
393
+ }
394
+ }
@@ -0,0 +1,281 @@
1
+ import {
2
+ CommandParamsError,
3
+ type InputToken,
4
+ type ParamSpec,
5
+ type ParsedCommandParams,
6
+ } from './types.js'
7
+ import { measureLength, parseAt, parseBoolean, parseTime } from './values.js'
8
+
9
+ export function tokenizeInput(input: string, usage: string): InputToken[] {
10
+ const tokens: InputToken[] = []
11
+ let index = 0
12
+
13
+ while (index < input.length) {
14
+ const char = input[index]
15
+
16
+ if (char === undefined) break
17
+
18
+ if (/\s/.test(char)) {
19
+ index += 1
20
+ continue
21
+ }
22
+
23
+ const start = index
24
+ let value = ''
25
+ let quote: string | null = null
26
+
27
+ while (index < input.length) {
28
+ const current = input[index]
29
+
30
+ if (current === undefined) break
31
+
32
+ if (quote !== null) {
33
+ if (current === '\\') {
34
+ const next = input[index + 1]
35
+
36
+ if (next !== undefined) {
37
+ value += next
38
+ index += 2
39
+ continue
40
+ }
41
+
42
+ value += current
43
+ index += 1
44
+ continue
45
+ }
46
+
47
+ if (current === quote) {
48
+ quote = null
49
+ index += 1
50
+ continue
51
+ }
52
+
53
+ value += current
54
+ index += 1
55
+ continue
56
+ }
57
+
58
+ if (/\s/.test(current)) break
59
+
60
+ if (current === '[' && input.startsWith('[CQ:', index)) {
61
+ const close = input.indexOf(']', index + 4)
62
+
63
+ if (close >= 0) {
64
+ value += input.slice(index, close + 1)
65
+ index = close + 1
66
+ continue
67
+ }
68
+ }
69
+
70
+ if (current === '"' || current === "'") {
71
+ quote = current
72
+ index += 1
73
+ continue
74
+ }
75
+
76
+ if (current === '\\') {
77
+ const next = input[index + 1]
78
+
79
+ if (next !== undefined) {
80
+ value += next
81
+ index += 2
82
+ continue
83
+ }
84
+
85
+ value += current
86
+ index += 1
87
+ continue
88
+ }
89
+
90
+ value += current
91
+ index += 1
92
+ }
93
+
94
+ if (quote !== null) {
95
+ throw new CommandParamsError('参数引号未闭合', usage)
96
+ }
97
+
98
+ tokens.push({ value, start })
99
+ }
100
+
101
+ return tokens
102
+ }
103
+
104
+ function enforceLength(spec: ParamSpec, value: string, usage: string): string {
105
+ const length = spec.length
106
+
107
+ if (length === undefined) {
108
+ return value
109
+ }
110
+
111
+ const measured = measureLength(value)
112
+
113
+ if (length.min !== undefined && measured < length.min) {
114
+ throw new CommandParamsError(
115
+ `参数 ${spec.name} 至少需要 ${length.min} 个字符`,
116
+ usage
117
+ )
118
+ }
119
+
120
+ if (length.max !== undefined && measured > length.max) {
121
+ throw new CommandParamsError(
122
+ `参数 ${spec.name} 不能超过 ${length.max} 个字符`,
123
+ usage
124
+ )
125
+ }
126
+
127
+ return value
128
+ }
129
+
130
+ export function parseValue(
131
+ spec: ParamSpec,
132
+ raw: string,
133
+ usage: string
134
+ ): unknown {
135
+ switch (spec.type) {
136
+ case 'string':
137
+ return enforceLength(spec, raw, usage)
138
+ case 'text':
139
+ return raw
140
+ case 'number': {
141
+ const value = raw.trim() === '' ? Number.NaN : Number(raw)
142
+
143
+ if (!Number.isFinite(value)) {
144
+ throw new CommandParamsError(
145
+ `参数 ${spec.name} 需要是数字`,
146
+ usage
147
+ )
148
+ }
149
+
150
+ return value
151
+ }
152
+ case 'integer': {
153
+ const value = raw.trim() === '' ? Number.NaN : Number(raw)
154
+
155
+ if (!Number.isInteger(value)) {
156
+ throw new CommandParamsError(
157
+ `参数 ${spec.name} 需要是整数`,
158
+ usage
159
+ )
160
+ }
161
+
162
+ return value
163
+ }
164
+ case 'boolean': {
165
+ const value = parseBoolean(raw)
166
+
167
+ if (value === null) {
168
+ throw new CommandParamsError(
169
+ `参数 ${spec.name} 需要是布尔值`,
170
+ usage
171
+ )
172
+ }
173
+
174
+ return value
175
+ }
176
+ case 'at':
177
+ return parseAt(raw, spec.name, usage)
178
+ case 'time':
179
+ return parseTime(raw, spec.name, usage)
180
+ }
181
+ }
182
+
183
+ export function parseInput(
184
+ specs: ParamSpec[],
185
+ input: string,
186
+ usage: string,
187
+ quoteId: number | undefined
188
+ ): ParsedCommandParams {
189
+ const tokens = tokenizeInput(input, usage)
190
+ const args: unknown[] = []
191
+ let index = 0
192
+
193
+ for (const spec of specs) {
194
+ if (spec.type === 'reply') {
195
+ if (quoteId !== undefined) {
196
+ args.push(quoteId)
197
+ } else if (spec.required) {
198
+ throw new CommandParamsError(
199
+ `参数 ${spec.name} 需要引用一条消息`,
200
+ usage
201
+ )
202
+ } else {
203
+ args.push(undefined)
204
+ }
205
+
206
+ continue
207
+ }
208
+
209
+ if (spec.rest) {
210
+ for (
211
+ let tokenIndex = index;
212
+ tokenIndex < tokens.length;
213
+ tokenIndex += 1
214
+ ) {
215
+ const token = tokens[tokenIndex]
216
+
217
+ if (token) {
218
+ args.push(token.value)
219
+ }
220
+ }
221
+
222
+ index = tokens.length
223
+ continue
224
+ }
225
+
226
+ if (spec.type === 'text') {
227
+ const token = tokens[index]
228
+
229
+ if (!token) {
230
+ if (spec.hasDefault) {
231
+ args.push(spec.defaultValue)
232
+ } else if (spec.required) {
233
+ throw new CommandParamsError(
234
+ `缺少参数:${spec.name}`,
235
+ usage
236
+ )
237
+ } else {
238
+ args.push(undefined)
239
+ }
240
+
241
+ continue
242
+ }
243
+
244
+ const remaining = tokens.length - index
245
+ const value =
246
+ remaining === 1 ? token.value : input.slice(token.start).trim()
247
+
248
+ args.push(enforceLength(spec, value, usage))
249
+ index = tokens.length
250
+ continue
251
+ }
252
+
253
+ const token = tokens[index]
254
+
255
+ if (!token) {
256
+ if (spec.hasDefault) {
257
+ args.push(spec.defaultValue)
258
+ } else if (spec.required) {
259
+ throw new CommandParamsError(`缺少参数:${spec.name}`, usage)
260
+ } else {
261
+ args.push(undefined)
262
+ }
263
+
264
+ continue
265
+ }
266
+
267
+ index += 1
268
+ args.push(parseValue(spec, token.value, usage))
269
+ }
270
+
271
+ while (args.length > 0 && args[args.length - 1] === undefined) {
272
+ args.pop()
273
+ }
274
+
275
+ const ignored =
276
+ index < tokens.length
277
+ ? tokens.slice(index).map((token) => token.value)
278
+ : []
279
+
280
+ return { args, ignored }
281
+ }