@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
@@ -1,871 +1,8 @@
1
- import { parseSegments, type OneBotMessageEvent } from '@snowluma/sdk'
2
-
3
- export type CommandParamType =
4
- | 'string'
5
- | 'text'
6
- | 'number'
7
- | 'integer'
8
- | 'boolean'
9
- | 'at'
10
- | 'reply'
11
- | 'time'
12
-
13
- export interface ParsedCommandParams {
14
- args: unknown[]
15
- ignored: string[]
16
- }
17
-
18
- export interface CompiledCommandParams {
19
- usage: string
20
- parse(input: string, event?: OneBotMessageEvent): ParsedCommandParams
21
- }
22
-
23
- export class CommandParamsError extends Error {
24
- readonly usage: string
25
-
26
- constructor(message: string, usage: string) {
27
- super(message)
28
- this.name = 'CommandParamsError'
29
- this.usage = usage
30
- }
31
- }
32
-
33
- interface ParamSpec {
34
- name: string
35
- type: CommandParamType
36
- required: boolean
37
- rest: boolean
38
- hasDefault: boolean
39
- defaultValue?: unknown
40
- }
41
-
42
- interface InputToken {
43
- value: string
44
- start: number
45
- }
46
-
47
- const NAME_PATTERN = /^[\p{L}_][\p{L}\p{N}_-]*$/u
48
-
49
- function declarationError(message: string): Error {
50
- return new Error(`参数声明错误:${message}`)
51
- }
52
-
53
- function parseType(value: string): CommandParamType {
54
- switch (value) {
55
- case 'string':
56
- case 'text':
57
- case 'number':
58
- case 'integer':
59
- case 'boolean':
60
- case 'at':
61
- case 'reply':
62
- case 'time':
63
- return value
64
- default:
65
- throw declarationError(`未知的参数类型 "${value}"`)
66
- }
67
- }
68
-
69
- function validateName(value: string): string {
70
- const name = value.trim()
71
-
72
- if (!name) {
73
- throw declarationError('参数名不能为空')
74
- }
75
-
76
- if (!NAME_PATTERN.test(name)) {
77
- throw declarationError(`非法的参数名 "${name}"`)
78
- }
79
-
80
- return name
81
- }
82
-
83
- function splitDeclaration(input: string): string[] {
84
- const parts: string[] = []
85
- let index = 0
86
-
87
- while (index < input.length) {
88
- const char = input[index]
89
-
90
- if (char === undefined) break
91
-
92
- if (/\s/.test(char)) {
93
- index += 1
94
- continue
95
- }
96
-
97
- if (char !== '<' && char !== '[') {
98
- throw declarationError(`无法识别的字符 "${char}"`)
99
- }
100
-
101
- const close = char === '<' ? '>' : ']'
102
- let end = index + 1
103
- let quote: string | null = null
104
-
105
- for (; end < input.length; end += 1) {
106
- const current = input[end]
107
-
108
- if (current === undefined) break
109
-
110
- if (quote !== null) {
111
- if (current === '\\') {
112
- end += 1
113
- continue
114
- }
115
-
116
- if (current === quote) {
117
- quote = null
118
- }
119
-
120
- continue
121
- }
122
-
123
- if (current === '"' || current === "'") {
124
- quote = current
125
- continue
126
- }
127
-
128
- if (current === close) break
129
- }
130
-
131
- if (end >= input.length || input[end] !== close) {
132
- throw declarationError(`参数 "${input.slice(index)}" 缺少 ${close}`)
133
- }
134
-
135
- parts.push(input.slice(index, end + 1))
136
- index = end + 1
137
- }
138
-
139
- return parts
140
- }
141
-
142
- function unquote(value: string): string {
143
- const trimmed = value.trim()
144
- const quote = trimmed[0]
145
-
146
- if (quote !== '"' && quote !== "'") {
147
- return trimmed
148
- }
149
-
150
- if (trimmed.length < 2 || trimmed[trimmed.length - 1] !== quote) {
151
- throw declarationError('默认值引号未闭合')
152
- }
153
-
154
- let result = ''
155
-
156
- for (let index = 1; index < trimmed.length - 1; index += 1) {
157
- const char = trimmed[index]
158
-
159
- if (char === undefined) break
160
-
161
- if (char === '\\') {
162
- const next = trimmed[index + 1]
163
-
164
- if (next !== undefined) {
165
- result += next
166
- index += 1
167
- }
168
-
169
- continue
170
- }
171
-
172
- result += char
173
- }
174
-
175
- return result
176
- }
177
-
178
- function parseBoolean(value: string): boolean | null {
179
- const normalized = value.trim().toLowerCase()
180
-
181
- if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) {
182
- return true
183
- }
184
-
185
- if (['false', '0', 'no', 'n', 'off'].includes(normalized)) {
186
- return false
187
- }
188
-
189
- return null
190
- }
191
-
192
- function parseAt(value: string, name: string, usage: string): number {
193
- const raw = value.trim()
194
- const segments = parseSegments(raw)
195
- const segment = segments[0]
196
-
197
- if (segments.length === 1 && segment?.type === 'at') {
198
- const qq = segment.data.qq
199
-
200
- if (qq !== 'all') {
201
- const parsed = Number(qq)
202
-
203
- if (Number.isSafeInteger(parsed) && parsed > 0) {
204
- return parsed
205
- }
206
- }
207
- } else if (/^\d+$/.test(raw)) {
208
- const parsed = Number(raw)
209
-
210
- if (Number.isSafeInteger(parsed) && parsed > 0) {
211
- return parsed
212
- }
213
- }
214
-
215
- throw new CommandParamsError(
216
- `参数 ${name} 需要 @ 一个用户或填写 QQ 号`,
217
- usage
218
- )
219
- }
220
-
221
- const TIME_UNITS: Record<string, number> = {
222
- s: 1,
223
- 秒: 1,
224
- m: 60,
225
- 分: 60,
226
- 分钟: 60,
227
- h: 3600,
228
- 时: 3600,
229
- 小时: 3600,
230
- d: 86400,
231
- 天: 86400,
232
- w: 604800,
233
- 周: 604800,
234
- }
235
-
236
- const TIME_PART = /^(\d+(?:\.\d+)?)(分钟|小时|秒|分|时|天|周|[smhdw])/i
237
-
238
- function parseTimeValue(value: string): number | null {
239
- const raw = value.trim()
240
-
241
- if (/^\d+$/.test(raw)) {
242
- const seconds = Number(raw)
243
-
244
- return Number.isSafeInteger(seconds) ? seconds : null
245
- }
246
-
247
- let index = 0
248
- let total = 0
249
- let parsed = false
250
-
251
- while (index < raw.length) {
252
- const match = TIME_PART.exec(raw.slice(index))
253
-
254
- if (!match) return null
255
-
256
- const unit = TIME_UNITS[match[2]?.toLowerCase() ?? '']
257
- const amount = Number(match[1])
258
-
259
- if (unit === undefined) return null
260
-
261
- total += amount * unit
262
- index += match[0].length
263
- parsed = true
264
- }
265
-
266
- if (!parsed || !Number.isSafeInteger(total) || total < 0) {
267
- return null
268
- }
269
-
270
- return total
271
- }
272
-
273
- function parseTime(value: string, name: string, usage: string): number {
274
- const seconds = parseTimeValue(value)
275
-
276
- if (seconds === null) {
277
- throw new CommandParamsError(
278
- `参数 ${name} 需要是时间,例如 10s、5m、1h30m`,
279
- usage
280
- )
281
- }
282
-
283
- return seconds
284
- }
285
-
286
- function isRecord(value: unknown): value is Record<string, unknown> {
287
- return typeof value === 'object' && value !== null && !Array.isArray(value)
288
- }
289
-
290
- function toMessageId(value: unknown): number | undefined {
291
- const raw = typeof value === 'string' ? value.trim() : value
292
-
293
- if (typeof raw !== 'number' && typeof raw !== 'string') {
294
- return undefined
295
- }
296
-
297
- const parsed =
298
- typeof raw === 'string'
299
- ? /^-?\d+$/.test(raw)
300
- ? Number(raw)
301
- : Number.NaN
302
- : raw
303
-
304
- return Number.isSafeInteger(parsed) && parsed !== 0 ? parsed : undefined
305
- }
306
-
307
- function replyIdIn(segments: readonly unknown[]): number | undefined {
308
- for (const segment of segments) {
309
- if (!isRecord(segment) || segment.type !== 'reply') continue
310
-
311
- const data = isRecord(segment.data) ? segment.data : undefined
312
- const id = toMessageId(data?.id)
313
-
314
- if (id !== undefined) return id
315
- }
316
-
317
- return undefined
318
- }
319
-
320
- function quoteIdOf(event: OneBotMessageEvent | undefined): number | undefined {
321
- if (!event) return undefined
322
-
323
- const message = event.message
324
-
325
- if (typeof message === 'string') {
326
- const id = replyIdIn(parseSegments(message))
327
-
328
- if (id !== undefined) return id
329
- } else if (Array.isArray(message)) {
330
- const id = replyIdIn(message)
331
-
332
- if (id !== undefined) return id
333
- } else if (isRecord(message)) {
334
- const id = replyIdIn([message])
335
-
336
- if (id !== undefined) return id
337
- }
338
-
339
- return replyIdIn(parseSegments(event.raw_message))
340
- }
341
-
342
- function parseDefaultValue(
343
- type: CommandParamType,
344
- raw: string,
345
- name: string
346
- ): unknown {
347
- const value = unquote(raw)
348
-
349
- switch (type) {
350
- case 'string':
351
- case 'text':
352
- return value
353
- case 'number': {
354
- const number = value.trim() === '' ? Number.NaN : Number(value)
355
-
356
- if (!Number.isFinite(number)) {
357
- throw declarationError(`参数 ${name} 的默认值不是数字`)
358
- }
359
-
360
- return number
361
- }
362
- case 'integer': {
363
- const number = value.trim() === '' ? Number.NaN : Number(value)
364
-
365
- if (!Number.isInteger(number)) {
366
- throw declarationError(`参数 ${name} 的默认值不是整数`)
367
- }
368
-
369
- return number
370
- }
371
- case 'boolean': {
372
- const boolean = parseBoolean(value)
373
-
374
- if (boolean === null) {
375
- throw declarationError(`参数 ${name} 的默认值不是布尔值`)
376
- }
377
-
378
- return boolean
379
- }
380
- case 'at':
381
- case 'reply':
382
- throw declarationError(`参数 ${name} 不支持默认值`)
383
- case 'time': {
384
- const seconds = parseTimeValue(value)
385
-
386
- if (seconds === null) {
387
- throw declarationError(`参数 ${name} 的默认值不是时间`)
388
- }
389
-
390
- return seconds
391
- }
392
- }
393
- }
394
-
395
- function parseToken(token: string): ParamSpec {
396
- const open = token[0]
397
- const close = token[token.length - 1]
398
-
399
- if (
400
- (open !== '<' && open !== '[') ||
401
- (close !== '>' && close !== ']') ||
402
- (open === '<') !== (close === '>')
403
- ) {
404
- throw declarationError(`非法的参数声明 "${token}"`)
405
- }
406
-
407
- const required = open === '<'
408
- const inner = token.slice(1, -1).trim()
409
-
410
- if (!inner) {
411
- throw declarationError('参数名不能为空')
412
- }
413
-
414
- if (inner.startsWith('...')) {
415
- if (required) {
416
- throw declarationError('变长参数必须使用 [ ]')
417
- }
418
-
419
- const name = validateName(inner.slice(3))
420
-
421
- return {
422
- name,
423
- type: 'string',
424
- required: false,
425
- rest: true,
426
- hasDefault: false,
427
- }
428
- }
429
-
430
- if (inner.includes('...')) {
431
- throw declarationError(`非法的参数声明 "${token}"`)
432
- }
433
-
434
- const equals = inner.indexOf('=')
435
- const head = equals >= 0 ? inner.slice(0, equals).trim() : inner.trim()
436
- const defaultRaw = equals >= 0 ? inner.slice(equals + 1).trim() : undefined
437
- const colon = head.indexOf(':')
438
- const name = validateName(colon >= 0 ? head.slice(0, colon) : head)
439
- const type = parseType(colon >= 0 ? head.slice(colon + 1).trim() : 'string')
440
-
441
- if (required && defaultRaw !== undefined) {
442
- throw declarationError(`必填参数不能有默认值:${token}`)
443
- }
444
-
445
- if (defaultRaw !== undefined) {
446
- return {
447
- name,
448
- type,
449
- required,
450
- rest: false,
451
- hasDefault: true,
452
- defaultValue: parseDefaultValue(type, defaultRaw, name),
453
- }
454
- }
455
-
456
- return {
457
- name,
458
- type,
459
- required,
460
- rest: false,
461
- hasDefault: false,
462
- }
463
- }
464
-
465
- function validateSpecs(specs: ParamSpec[]): void {
466
- const names = new Set<string>()
467
- let seenOptional = false
468
- let seenRest = false
469
- let seenText = false
470
-
471
- for (let index = 0; index < specs.length; index += 1) {
472
- const spec = specs[index]
473
-
474
- if (!spec) continue
475
-
476
- if (names.has(spec.name)) {
477
- throw declarationError(`参数名重复 "${spec.name}"`)
478
- }
479
-
480
- names.add(spec.name)
481
-
482
- if (seenRest) {
483
- throw declarationError('变长参数后面不能再有参数')
484
- }
485
-
486
- if (seenText) {
487
- throw declarationError('文本参数后面不能再有参数')
488
- }
489
-
490
- if (spec.rest) {
491
- if (index !== specs.length - 1) {
492
- throw declarationError('变长参数必须放在最后')
493
- }
494
-
495
- seenRest = true
496
- continue
497
- }
498
-
499
- if (spec.type === 'text') {
500
- if (index !== specs.length - 1) {
501
- throw declarationError('文本参数必须放在最后')
502
- }
503
-
504
- seenText = true
505
- }
506
-
507
- if (spec.required) {
508
- if (seenOptional) {
509
- throw declarationError('必填参数不能出现在可选参数后面')
510
- }
511
- } else {
512
- seenOptional = true
513
- }
514
- }
515
- }
516
-
517
- function usageOf(specs: ParamSpec[]): string {
518
- return specs
519
- .map((spec) => {
520
- if (spec.rest) {
521
- return `[...${spec.name}]`
522
- }
523
-
524
- const name = spec.type === 'text' ? `${spec.name}...` : spec.name
525
-
526
- return spec.required ? `<${name}>` : `[${name}]`
527
- })
528
- .join(' ')
529
- }
530
-
531
- function tokenizeInput(input: string, usage: string): InputToken[] {
532
- const tokens: InputToken[] = []
533
- let index = 0
534
-
535
- while (index < input.length) {
536
- const char = input[index]
537
-
538
- if (char === undefined) break
539
-
540
- if (/\s/.test(char)) {
541
- index += 1
542
- continue
543
- }
544
-
545
- const start = index
546
- let value = ''
547
- let quote: string | null = null
548
-
549
- while (index < input.length) {
550
- const current = input[index]
551
-
552
- if (current === undefined) break
553
-
554
- if (quote !== null) {
555
- if (current === '\\') {
556
- const next = input[index + 1]
557
-
558
- if (next !== undefined) {
559
- value += next
560
- index += 2
561
- continue
562
- }
563
-
564
- value += current
565
- index += 1
566
- continue
567
- }
568
-
569
- if (current === quote) {
570
- quote = null
571
- index += 1
572
- continue
573
- }
574
-
575
- value += current
576
- index += 1
577
- continue
578
- }
579
-
580
- if (/\s/.test(current)) break
581
-
582
- if (current === '[' && input.startsWith('[CQ:', index)) {
583
- const close = input.indexOf(']', index + 4)
584
-
585
- if (close >= 0) {
586
- value += input.slice(index, close + 1)
587
- index = close + 1
588
- continue
589
- }
590
- }
591
-
592
- if (current === '"' || current === "'") {
593
- quote = current
594
- index += 1
595
- continue
596
- }
597
-
598
- if (current === '\\') {
599
- const next = input[index + 1]
600
-
601
- if (next !== undefined) {
602
- value += next
603
- index += 2
604
- continue
605
- }
606
-
607
- value += current
608
- index += 1
609
- continue
610
- }
611
-
612
- value += current
613
- index += 1
614
- }
615
-
616
- if (quote !== null) {
617
- throw new CommandParamsError('参数引号未闭合', usage)
618
- }
619
-
620
- tokens.push({ value, start })
621
- }
622
-
623
- return tokens
624
- }
625
-
626
- function parseValue(spec: ParamSpec, raw: string, usage: string): unknown {
627
- switch (spec.type) {
628
- case 'string':
629
- case 'text':
630
- return raw
631
- case 'number': {
632
- const value = raw.trim() === '' ? Number.NaN : Number(raw)
633
-
634
- if (!Number.isFinite(value)) {
635
- throw new CommandParamsError(
636
- `参数 ${spec.name} 需要是数字`,
637
- usage
638
- )
639
- }
640
-
641
- return value
642
- }
643
- case 'integer': {
644
- const value = raw.trim() === '' ? Number.NaN : Number(raw)
645
-
646
- if (!Number.isInteger(value)) {
647
- throw new CommandParamsError(
648
- `参数 ${spec.name} 需要是整数`,
649
- usage
650
- )
651
- }
652
-
653
- return value
654
- }
655
- case 'boolean': {
656
- const value = parseBoolean(raw)
657
-
658
- if (value === null) {
659
- throw new CommandParamsError(
660
- `参数 ${spec.name} 需要是布尔值`,
661
- usage
662
- )
663
- }
664
-
665
- return value
666
- }
667
- case 'at':
668
- return parseAt(raw, spec.name, usage)
669
- case 'time':
670
- return parseTime(raw, spec.name, usage)
671
- }
672
- }
673
-
674
- function parseInput(
675
- specs: ParamSpec[],
676
- input: string,
677
- usage: string,
678
- quoteId: number | undefined
679
- ): ParsedCommandParams {
680
- const tokens = tokenizeInput(input, usage)
681
- const args: unknown[] = []
682
- let index = 0
683
-
684
- for (const spec of specs) {
685
- if (spec.type === 'reply') {
686
- if (quoteId !== undefined) {
687
- args.push(quoteId)
688
- } else if (spec.required) {
689
- throw new CommandParamsError(
690
- `参数 ${spec.name} 需要引用一条消息`,
691
- usage
692
- )
693
- } else {
694
- args.push(undefined)
695
- }
696
-
697
- continue
698
- }
699
-
700
- if (spec.rest) {
701
- for (
702
- let tokenIndex = index;
703
- tokenIndex < tokens.length;
704
- tokenIndex += 1
705
- ) {
706
- const token = tokens[tokenIndex]
707
-
708
- if (token) {
709
- args.push(token.value)
710
- }
711
- }
712
-
713
- index = tokens.length
714
- continue
715
- }
716
-
717
- if (spec.type === 'text') {
718
- const token = tokens[index]
719
-
720
- if (!token) {
721
- if (spec.hasDefault) {
722
- args.push(spec.defaultValue)
723
- } else if (spec.required) {
724
- throw new CommandParamsError(
725
- `缺少参数:${spec.name}`,
726
- usage
727
- )
728
- } else {
729
- args.push(undefined)
730
- }
731
-
732
- continue
733
- }
734
-
735
- const remaining = tokens.length - index
736
- const value =
737
- remaining === 1 ? token.value : input.slice(token.start).trim()
738
-
739
- args.push(value)
740
- index = tokens.length
741
- continue
742
- }
743
-
744
- const token = tokens[index]
745
-
746
- if (!token) {
747
- if (spec.hasDefault) {
748
- args.push(spec.defaultValue)
749
- } else if (spec.required) {
750
- throw new CommandParamsError(`缺少参数:${spec.name}`, usage)
751
- } else {
752
- args.push(undefined)
753
- }
754
-
755
- continue
756
- }
757
-
758
- index += 1
759
- args.push(parseValue(spec, token.value, usage))
760
- }
761
-
762
- while (args.length > 0 && args[args.length - 1] === undefined) {
763
- args.pop()
764
- }
765
-
766
- const ignored =
767
- index < tokens.length
768
- ? tokens.slice(index).map((token) => token.value)
769
- : []
770
-
771
- return { args, ignored }
772
- }
773
-
774
- export function compileCommandParams(
775
- declaration: string
776
- ): CompiledCommandParams {
777
- const trimmed = declaration.trim()
778
-
779
- if (!trimmed) {
780
- throw declarationError('参数声明不能为空')
781
- }
782
-
783
- const specs = splitDeclaration(trimmed).map(parseToken)
784
-
785
- if (specs.length === 0) {
786
- throw declarationError('参数声明不能为空')
787
- }
788
-
789
- validateSpecs(specs)
790
-
791
- const usage = usageOf(specs)
792
-
793
- return {
794
- usage,
795
- parse(input: string, event?: OneBotMessageEvent): ParsedCommandParams {
796
- return parseInput(specs, input, usage, quoteIdOf(event))
797
- },
798
- }
799
- }
800
-
801
- type Whitespace = ' ' | '\n' | '\r' | '\t' | '\v' | '\f'
802
-
803
- type TrimLeft<Source extends string> =
804
- Source extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : Source
805
-
806
- type TrimRight<Source extends string> =
807
- Source extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : Source
808
-
809
- type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
810
-
811
- type ParamValue<Type extends string> =
812
- Trim<Type> extends 'string' | 'text'
813
- ? string
814
- : Trim<Type> extends 'number' | 'integer' | 'at' | 'reply' | 'time'
815
- ? number
816
- : Trim<Type> extends 'boolean'
817
- ? boolean
818
- : unknown
819
-
820
- interface VariadicParam {
821
- readonly variadic: true
822
- }
823
-
824
- type ParamType<Head extends string> = Head extends `${string}:${infer Type}`
825
- ? ParamValue<Type>
826
- : string
827
-
828
- type ParamElement<Body extends string, Required extends boolean> =
829
- Trim<Body> extends `...${string}`
830
- ? VariadicParam
831
- : Trim<Body> extends `${infer Head}=${string}`
832
- ? ParamType<Head>
833
- : Required extends true
834
- ? ParamType<Body>
835
- : ParamType<Body> | undefined
836
-
837
- type AppendParam<Acc extends unknown[], Element> = [Element] extends [
838
- VariadicParam,
839
- ]
840
- ? [...Acc, ...string[]]
841
- : [...Acc, Element]
842
-
843
- type ParseCommandParams<Source extends string, Acc extends unknown[] = []> =
844
- Trim<Source> extends ''
845
- ? Acc
846
- : Trim<Source> extends `<${infer Body}>${infer Rest}`
847
- ? ParamElement<Body, true> extends VariadicParam
848
- ? any[]
849
- : ParseCommandParams<
850
- Rest,
851
- AppendParam<Acc, ParamElement<Body, true>>
852
- >
853
- : Trim<Source> extends `[${infer Body}]${infer Rest}`
854
- ? ParamElement<Body, false> extends VariadicParam
855
- ? Trim<Rest> extends ''
856
- ? [...Acc, ...string[]]
857
- : any[]
858
- : ParseCommandParams<
859
- Rest,
860
- AppendParam<Acc, ParamElement<Body, false>>
861
- >
862
- : any[]
863
-
864
- /**
865
- * 把 `params` 声明字符串推导成 handler 的位置参数 tuple。
866
- *
867
- * 非字面量声明(例如运行时拼接的 `string`)或无法解析的声明会回退为
868
- * `any[]`,以保证现有插件仍然可以编译。
869
- */
870
- export type CommandParamArgs<Declaration extends string> =
871
- string extends Declaration ? any[] : ParseCommandParams<Declaration>
1
+ export {
2
+ CommandParamsError,
3
+ type CommandParamType,
4
+ type CompiledCommandParams,
5
+ type ParsedCommandParams,
6
+ } from './params/types.js'
7
+ export { compileCommandParams } from './params/compile.js'
8
+ export type { CommandParamArgs } from './params/type-level.js'