@jsbot-tellus/core 2.4.0 → 2.5.1

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 (183) 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/dispatch/command.d.ts.map +1 -1
  13. package/dist/core/dispatch/command.js +5 -1
  14. package/dist/core/dispatch/command.js.map +1 -1
  15. package/dist/core/dispatch/router.d.ts.map +1 -1
  16. package/dist/core/dispatch/router.js +1 -1
  17. package/dist/core/dispatch/router.js.map +1 -1
  18. package/dist/core/plugin/command-params.d.ts +3 -36
  19. package/dist/core/plugin/command-params.d.ts.map +1 -1
  20. package/dist/core/plugin/command-params.js +2 -461
  21. package/dist/core/plugin/command-params.js.map +1 -1
  22. package/dist/core/plugin/install/paths.d.ts +7 -0
  23. package/dist/core/plugin/install/paths.d.ts.map +1 -0
  24. package/dist/core/plugin/install/paths.js +49 -0
  25. package/dist/core/plugin/install/paths.js.map +1 -0
  26. package/dist/core/plugin/install/state.d.ts +23 -0
  27. package/dist/core/plugin/install/state.d.ts.map +1 -0
  28. package/dist/core/plugin/install/state.js +129 -0
  29. package/dist/core/plugin/install/state.js.map +1 -0
  30. package/dist/core/plugin/install/validate-script.d.ts +3 -0
  31. package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
  32. package/dist/core/plugin/install/validate-script.js +154 -0
  33. package/dist/core/plugin/install/validate-script.js.map +1 -0
  34. package/dist/core/plugin/install-dir.d.ts +3 -28
  35. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  36. package/dist/core/plugin/install-dir.js +3 -326
  37. package/dist/core/plugin/install-dir.js.map +1 -1
  38. package/dist/core/plugin/installer/checks.d.ts +6 -0
  39. package/dist/core/plugin/installer/checks.d.ts.map +1 -0
  40. package/dist/core/plugin/installer/checks.js +99 -0
  41. package/dist/core/plugin/installer/checks.js.map +1 -0
  42. package/dist/core/plugin/installer/support.d.ts +37 -0
  43. package/dist/core/plugin/installer/support.d.ts.map +1 -0
  44. package/dist/core/plugin/installer/support.js +67 -0
  45. package/dist/core/plugin/installer/support.js.map +1 -0
  46. package/dist/core/plugin/installer/warnings.d.ts +5 -0
  47. package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
  48. package/dist/core/plugin/installer/warnings.js +28 -0
  49. package/dist/core/plugin/installer/warnings.js.map +1 -0
  50. package/dist/core/plugin/installer.d.ts +4 -30
  51. package/dist/core/plugin/installer.d.ts.map +1 -1
  52. package/dist/core/plugin/installer.js +21 -202
  53. package/dist/core/plugin/installer.js.map +1 -1
  54. package/dist/core/plugin/params/compile.d.ts +3 -0
  55. package/dist/core/plugin/params/compile.d.ts.map +1 -0
  56. package/dist/core/plugin/params/compile.js +23 -0
  57. package/dist/core/plugin/params/compile.js.map +1 -0
  58. package/dist/core/plugin/params/declaration.d.ts +9 -0
  59. package/dist/core/plugin/params/declaration.d.ts.map +1 -0
  60. package/dist/core/plugin/params/declaration.js +223 -0
  61. package/dist/core/plugin/params/declaration.js.map +1 -0
  62. package/dist/core/plugin/params/input.d.ts +5 -0
  63. package/dist/core/plugin/params/input.d.ts.map +1 -0
  64. package/dist/core/plugin/params/input.js +181 -0
  65. package/dist/core/plugin/params/input.js.map +1 -0
  66. package/dist/core/plugin/params/reply.d.ts +3 -0
  67. package/dist/core/plugin/params/reply.d.ts.map +1 -0
  68. package/dist/core/plugin/params/reply.js +49 -0
  69. package/dist/core/plugin/params/reply.js.map +1 -0
  70. package/dist/core/plugin/params/type-level.d.ts +23 -0
  71. package/dist/core/plugin/params/type-level.d.ts.map +1 -0
  72. package/dist/core/plugin/params/type-level.js +2 -0
  73. package/dist/core/plugin/params/type-level.js.map +1 -0
  74. package/dist/core/plugin/params/types.d.ts +28 -0
  75. package/dist/core/plugin/params/types.d.ts.map +1 -0
  76. package/dist/core/plugin/params/types.js +12 -0
  77. package/dist/core/plugin/params/types.js.map +1 -0
  78. package/dist/core/plugin/params/values.d.ts +6 -0
  79. package/dist/core/plugin/params/values.d.ts.map +1 -0
  80. package/dist/core/plugin/params/values.js +108 -0
  81. package/dist/core/plugin/params/values.js.map +1 -0
  82. package/dist/core/web/api.d.ts +2 -14
  83. package/dist/core/web/api.d.ts.map +1 -1
  84. package/dist/core/web/api.js +8 -500
  85. package/dist/core/web/api.js.map +1 -1
  86. package/dist/core/web/routes/actions.d.ts +4 -0
  87. package/dist/core/web/routes/actions.d.ts.map +1 -0
  88. package/dist/core/web/routes/actions.js +33 -0
  89. package/dist/core/web/routes/actions.js.map +1 -0
  90. package/dist/core/web/routes/config.d.ts +5 -0
  91. package/dist/core/web/routes/config.d.ts.map +1 -0
  92. package/dist/core/web/routes/config.js +30 -0
  93. package/dist/core/web/routes/config.js.map +1 -0
  94. package/dist/core/web/routes/kv.d.ts +6 -0
  95. package/dist/core/web/routes/kv.d.ts.map +1 -0
  96. package/dist/core/web/routes/kv.js +80 -0
  97. package/dist/core/web/routes/kv.js.map +1 -0
  98. package/dist/core/web/routes/plugins.d.ts +5 -0
  99. package/dist/core/web/routes/plugins.d.ts.map +1 -0
  100. package/dist/core/web/routes/plugins.js +98 -0
  101. package/dist/core/web/routes/plugins.js.map +1 -0
  102. package/dist/core/web/routes/sessions.d.ts +8 -0
  103. package/dist/core/web/routes/sessions.d.ts.map +1 -0
  104. package/dist/core/web/routes/sessions.js +130 -0
  105. package/dist/core/web/routes/sessions.js.map +1 -0
  106. package/dist/core/web/routes/shared.d.ts +18 -0
  107. package/dist/core/web/routes/shared.d.ts.map +1 -0
  108. package/dist/core/web/routes/shared.js +19 -0
  109. package/dist/core/web/routes/shared.js.map +1 -0
  110. package/dist/core/web/routes/status.d.ts +6 -0
  111. package/dist/core/web/routes/status.d.ts.map +1 -0
  112. package/dist/core/web/routes/status.js +132 -0
  113. package/dist/core/web/routes/status.js.map +1 -0
  114. package/dist/core/web/server/auth.d.ts +17 -0
  115. package/dist/core/web/server/auth.d.ts.map +1 -0
  116. package/dist/core/web/server/auth.js +68 -0
  117. package/dist/core/web/server/auth.js.map +1 -0
  118. package/dist/core/web/server/csp.d.ts +11 -0
  119. package/dist/core/web/server/csp.d.ts.map +1 -0
  120. package/dist/core/web/server/csp.js +31 -0
  121. package/dist/core/web/server/csp.js.map +1 -0
  122. package/dist/core/web/server/request.d.ts +6 -0
  123. package/dist/core/web/server/request.d.ts.map +1 -0
  124. package/dist/core/web/server/request.js +33 -0
  125. package/dist/core/web/server/request.js.map +1 -0
  126. package/dist/core/web/server/security.d.ts +4 -0
  127. package/dist/core/web/server/security.d.ts.map +1 -0
  128. package/dist/core/web/server/security.js +12 -0
  129. package/dist/core/web/server/security.js.map +1 -0
  130. package/dist/core/web/server/sse.d.ts +18 -0
  131. package/dist/core/web/server/sse.d.ts.map +1 -0
  132. package/dist/core/web/server/sse.js +49 -0
  133. package/dist/core/web/server/sse.js.map +1 -0
  134. package/dist/core/web/server/static.d.ts +16 -0
  135. package/dist/core/web/server/static.d.ts.map +1 -0
  136. package/dist/core/web/server/static.js +80 -0
  137. package/dist/core/web/server/static.js.map +1 -0
  138. package/dist/core/web/server.d.ts +0 -7
  139. package/dist/core/web/server.d.ts.map +1 -1
  140. package/dist/core/web/server.js +41 -263
  141. package/dist/core/web/server.js.map +1 -1
  142. package/docs/architecture.md +28 -11
  143. package/docs/configuration.md +1 -1
  144. package/docs/migration.md +2 -2
  145. package/docs/plugin-api.md +3 -1
  146. package/docs/plugin-development.md +1 -1
  147. package/package.json +19 -17
  148. package/src/app/cli.ts +47 -0
  149. package/src/app/runtime.ts +522 -0
  150. package/src/app.ts +87 -395
  151. package/src/core/dispatch/command.ts +7 -1
  152. package/src/core/dispatch/router.ts +2 -1
  153. package/src/core/plugin/command-params.ts +8 -713
  154. package/src/core/plugin/install/paths.ts +72 -0
  155. package/src/core/plugin/install/state.ts +196 -0
  156. package/src/core/plugin/install/validate-script.ts +154 -0
  157. package/src/core/plugin/install-dir.ts +19 -427
  158. package/src/core/plugin/installer/checks.ts +137 -0
  159. package/src/core/plugin/installer/support.ts +120 -0
  160. package/src/core/plugin/installer/warnings.ts +52 -0
  161. package/src/core/plugin/installer.ts +52 -301
  162. package/src/core/plugin/params/compile.ts +41 -0
  163. package/src/core/plugin/params/declaration.ts +286 -0
  164. package/src/core/plugin/params/input.ts +254 -0
  165. package/src/core/plugin/params/reply.ts +59 -0
  166. package/src/core/plugin/params/type-level.ts +71 -0
  167. package/src/core/plugin/params/types.ts +49 -0
  168. package/src/core/plugin/params/values.ts +146 -0
  169. package/src/core/web/api.ts +33 -759
  170. package/src/core/web/routes/actions.ts +43 -0
  171. package/src/core/web/routes/config.ts +42 -0
  172. package/src/core/web/routes/kv.ts +122 -0
  173. package/src/core/web/routes/plugins.ts +151 -0
  174. package/src/core/web/routes/sessions.ts +188 -0
  175. package/src/core/web/routes/shared.ts +42 -0
  176. package/src/core/web/routes/status.ts +191 -0
  177. package/src/core/web/server/auth.ts +126 -0
  178. package/src/core/web/server/csp.ts +33 -0
  179. package/src/core/web/server/request.ts +38 -0
  180. package/src/core/web/server/security.ts +14 -0
  181. package/src/core/web/server/sse.ts +74 -0
  182. package/src/core/web/server/static.ts +112 -0
  183. package/src/core/web/server.ts +41 -383
@@ -0,0 +1,286 @@
1
+ import {
2
+ declarationError,
3
+ type CommandParamType,
4
+ type ParamSpec,
5
+ } from './types.js'
6
+ import { parseBoolean, parseTimeValue, unquote } from './values.js'
7
+
8
+ const NAME_PATTERN = /^[\p{L}_][\p{L}\p{N}_-]*$/u
9
+
10
+ export function parseType(value: string): CommandParamType {
11
+ switch (value) {
12
+ case 'string':
13
+ case 'text':
14
+ case 'number':
15
+ case 'integer':
16
+ case 'boolean':
17
+ case 'at':
18
+ case 'reply':
19
+ case 'time':
20
+ return value
21
+ default:
22
+ throw declarationError(`未知的参数类型 "${value}"`)
23
+ }
24
+ }
25
+
26
+ export function validateName(value: string): string {
27
+ const name = value.trim()
28
+
29
+ if (!name) {
30
+ throw declarationError('参数名不能为空')
31
+ }
32
+
33
+ if (!NAME_PATTERN.test(name)) {
34
+ throw declarationError(`非法的参数名 "${name}"`)
35
+ }
36
+
37
+ return name
38
+ }
39
+
40
+ export function splitDeclaration(input: string): string[] {
41
+ const parts: string[] = []
42
+ let index = 0
43
+
44
+ while (index < input.length) {
45
+ const char = input[index]
46
+
47
+ if (char === undefined) break
48
+
49
+ if (/\s/.test(char)) {
50
+ index += 1
51
+ continue
52
+ }
53
+
54
+ if (char !== '<' && char !== '[') {
55
+ throw declarationError(`无法识别的字符 "${char}"`)
56
+ }
57
+
58
+ const close = char === '<' ? '>' : ']'
59
+ let end = index + 1
60
+ let quote: string | null = null
61
+
62
+ for (; end < input.length; end += 1) {
63
+ const current = input[end]
64
+
65
+ if (current === undefined) break
66
+
67
+ if (quote !== null) {
68
+ if (current === '\\') {
69
+ end += 1
70
+ continue
71
+ }
72
+
73
+ if (current === quote) {
74
+ quote = null
75
+ }
76
+
77
+ continue
78
+ }
79
+
80
+ if (current === '"' || current === "'") {
81
+ quote = current
82
+ continue
83
+ }
84
+
85
+ if (current === close) break
86
+ }
87
+
88
+ if (end >= input.length || input[end] !== close) {
89
+ throw declarationError(`参数 "${input.slice(index)}" 缺少 ${close}`)
90
+ }
91
+
92
+ parts.push(input.slice(index, end + 1))
93
+ index = end + 1
94
+ }
95
+
96
+ return parts
97
+ }
98
+
99
+ export function parseToken(token: string): ParamSpec {
100
+ const open = token[0]
101
+ const close = token[token.length - 1]
102
+
103
+ if (
104
+ (open !== '<' && open !== '[') ||
105
+ (close !== '>' && close !== ']') ||
106
+ (open === '<') !== (close === '>')
107
+ ) {
108
+ throw declarationError(`非法的参数声明 "${token}"`)
109
+ }
110
+
111
+ const required = open === '<'
112
+ const inner = token.slice(1, -1).trim()
113
+
114
+ if (!inner) {
115
+ throw declarationError('参数名不能为空')
116
+ }
117
+
118
+ if (inner.startsWith('...')) {
119
+ if (required) {
120
+ throw declarationError('变长参数必须使用 [ ]')
121
+ }
122
+
123
+ const name = validateName(inner.slice(3))
124
+
125
+ return {
126
+ name,
127
+ type: 'string',
128
+ required: false,
129
+ rest: true,
130
+ hasDefault: false,
131
+ }
132
+ }
133
+
134
+ if (inner.includes('...')) {
135
+ throw declarationError(`非法的参数声明 "${token}"`)
136
+ }
137
+
138
+ const equals = inner.indexOf('=')
139
+ const head = equals >= 0 ? inner.slice(0, equals).trim() : inner.trim()
140
+ const defaultRaw = equals >= 0 ? inner.slice(equals + 1).trim() : undefined
141
+ const colon = head.indexOf(':')
142
+ const name = validateName(colon >= 0 ? head.slice(0, colon) : head)
143
+ const type = parseType(colon >= 0 ? head.slice(colon + 1).trim() : 'string')
144
+
145
+ if (required && defaultRaw !== undefined) {
146
+ throw declarationError(`必填参数不能有默认值:${token}`)
147
+ }
148
+
149
+ if (defaultRaw !== undefined) {
150
+ return {
151
+ name,
152
+ type,
153
+ required,
154
+ rest: false,
155
+ hasDefault: true,
156
+ defaultValue: parseDefaultValue(type, defaultRaw, name),
157
+ }
158
+ }
159
+
160
+ return {
161
+ name,
162
+ type,
163
+ required,
164
+ rest: false,
165
+ hasDefault: false,
166
+ }
167
+ }
168
+
169
+ export function validateSpecs(specs: ParamSpec[]): void {
170
+ const names = new Set<string>()
171
+ let seenOptional = false
172
+ let seenRest = false
173
+ let seenText = false
174
+
175
+ for (let index = 0; index < specs.length; index += 1) {
176
+ const spec = specs[index]
177
+
178
+ if (!spec) continue
179
+
180
+ if (names.has(spec.name)) {
181
+ throw declarationError(`参数名重复 "${spec.name}"`)
182
+ }
183
+
184
+ names.add(spec.name)
185
+
186
+ if (seenRest) {
187
+ throw declarationError('变长参数后面不能再有参数')
188
+ }
189
+
190
+ if (seenText) {
191
+ throw declarationError('文本参数后面不能再有参数')
192
+ }
193
+
194
+ if (spec.rest) {
195
+ if (index !== specs.length - 1) {
196
+ throw declarationError('变长参数必须放在最后')
197
+ }
198
+
199
+ seenRest = true
200
+ continue
201
+ }
202
+
203
+ if (spec.type === 'text') {
204
+ if (index !== specs.length - 1) {
205
+ throw declarationError('文本参数必须放在最后')
206
+ }
207
+
208
+ seenText = true
209
+ }
210
+
211
+ if (spec.required) {
212
+ if (seenOptional) {
213
+ throw declarationError('必填参数不能出现在可选参数后面')
214
+ }
215
+ } else {
216
+ seenOptional = true
217
+ }
218
+ }
219
+ }
220
+
221
+ export function usageOf(specs: ParamSpec[]): string {
222
+ return specs
223
+ .map((spec) => {
224
+ if (spec.rest) {
225
+ return `[...${spec.name}]`
226
+ }
227
+
228
+ const name = spec.type === 'text' ? `${spec.name}...` : spec.name
229
+
230
+ return spec.required ? `<${name}>` : `[${name}]`
231
+ })
232
+ .join(' ')
233
+ }
234
+
235
+ export function parseDefaultValue(
236
+ type: CommandParamType,
237
+ raw: string,
238
+ name: string
239
+ ): unknown {
240
+ const value = unquote(raw)
241
+
242
+ switch (type) {
243
+ case 'string':
244
+ case 'text':
245
+ return value
246
+ case 'number': {
247
+ const number = value.trim() === '' ? Number.NaN : Number(value)
248
+
249
+ if (!Number.isFinite(number)) {
250
+ throw declarationError(`参数 ${name} 的默认值不是数字`)
251
+ }
252
+
253
+ return number
254
+ }
255
+ case 'integer': {
256
+ const number = value.trim() === '' ? Number.NaN : Number(value)
257
+
258
+ if (!Number.isInteger(number)) {
259
+ throw declarationError(`参数 ${name} 的默认值不是整数`)
260
+ }
261
+
262
+ return number
263
+ }
264
+ case 'boolean': {
265
+ const boolean = parseBoolean(value)
266
+
267
+ if (boolean === null) {
268
+ throw declarationError(`参数 ${name} 的默认值不是布尔值`)
269
+ }
270
+
271
+ return boolean
272
+ }
273
+ case 'at':
274
+ case 'reply':
275
+ throw declarationError(`参数 ${name} 不支持默认值`)
276
+ case 'time': {
277
+ const seconds = parseTimeValue(value)
278
+
279
+ if (seconds === null) {
280
+ throw declarationError(`参数 ${name} 的默认值不是时间`)
281
+ }
282
+
283
+ return seconds
284
+ }
285
+ }
286
+ }
@@ -0,0 +1,254 @@
1
+ import {
2
+ CommandParamsError,
3
+ type InputToken,
4
+ type ParamSpec,
5
+ type ParsedCommandParams,
6
+ } from './types.js'
7
+ import { 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
+ export function parseValue(
105
+ spec: ParamSpec,
106
+ raw: string,
107
+ usage: string
108
+ ): unknown {
109
+ switch (spec.type) {
110
+ case 'string':
111
+ case 'text':
112
+ return raw
113
+ case 'number': {
114
+ const value = raw.trim() === '' ? Number.NaN : Number(raw)
115
+
116
+ if (!Number.isFinite(value)) {
117
+ throw new CommandParamsError(
118
+ `参数 ${spec.name} 需要是数字`,
119
+ usage
120
+ )
121
+ }
122
+
123
+ return value
124
+ }
125
+ case 'integer': {
126
+ const value = raw.trim() === '' ? Number.NaN : Number(raw)
127
+
128
+ if (!Number.isInteger(value)) {
129
+ throw new CommandParamsError(
130
+ `参数 ${spec.name} 需要是整数`,
131
+ usage
132
+ )
133
+ }
134
+
135
+ return value
136
+ }
137
+ case 'boolean': {
138
+ const value = parseBoolean(raw)
139
+
140
+ if (value === null) {
141
+ throw new CommandParamsError(
142
+ `参数 ${spec.name} 需要是布尔值`,
143
+ usage
144
+ )
145
+ }
146
+
147
+ return value
148
+ }
149
+ case 'at':
150
+ return parseAt(raw, spec.name, usage)
151
+ case 'time':
152
+ return parseTime(raw, spec.name, usage)
153
+ }
154
+ }
155
+
156
+ export function parseInput(
157
+ specs: ParamSpec[],
158
+ input: string,
159
+ usage: string,
160
+ quoteId: number | undefined
161
+ ): ParsedCommandParams {
162
+ const tokens = tokenizeInput(input, usage)
163
+ const args: unknown[] = []
164
+ let index = 0
165
+
166
+ for (const spec of specs) {
167
+ if (spec.type === 'reply') {
168
+ if (quoteId !== undefined) {
169
+ args.push(quoteId)
170
+ } else if (spec.required) {
171
+ throw new CommandParamsError(
172
+ `参数 ${spec.name} 需要引用一条消息`,
173
+ usage
174
+ )
175
+ } else {
176
+ args.push(undefined)
177
+ }
178
+
179
+ continue
180
+ }
181
+
182
+ if (spec.rest) {
183
+ for (
184
+ let tokenIndex = index;
185
+ tokenIndex < tokens.length;
186
+ tokenIndex += 1
187
+ ) {
188
+ const token = tokens[tokenIndex]
189
+
190
+ if (token) {
191
+ args.push(token.value)
192
+ }
193
+ }
194
+
195
+ index = tokens.length
196
+ continue
197
+ }
198
+
199
+ if (spec.type === 'text') {
200
+ const token = tokens[index]
201
+
202
+ if (!token) {
203
+ if (spec.hasDefault) {
204
+ args.push(spec.defaultValue)
205
+ } else if (spec.required) {
206
+ throw new CommandParamsError(
207
+ `缺少参数:${spec.name}`,
208
+ usage
209
+ )
210
+ } else {
211
+ args.push(undefined)
212
+ }
213
+
214
+ continue
215
+ }
216
+
217
+ const remaining = tokens.length - index
218
+ const value =
219
+ remaining === 1 ? token.value : input.slice(token.start).trim()
220
+
221
+ args.push(value)
222
+ index = tokens.length
223
+ continue
224
+ }
225
+
226
+ const token = tokens[index]
227
+
228
+ if (!token) {
229
+ if (spec.hasDefault) {
230
+ args.push(spec.defaultValue)
231
+ } else if (spec.required) {
232
+ throw new CommandParamsError(`缺少参数:${spec.name}`, usage)
233
+ } else {
234
+ args.push(undefined)
235
+ }
236
+
237
+ continue
238
+ }
239
+
240
+ index += 1
241
+ args.push(parseValue(spec, token.value, usage))
242
+ }
243
+
244
+ while (args.length > 0 && args[args.length - 1] === undefined) {
245
+ args.pop()
246
+ }
247
+
248
+ const ignored =
249
+ index < tokens.length
250
+ ? tokens.slice(index).map((token) => token.value)
251
+ : []
252
+
253
+ return { args, ignored }
254
+ }
@@ -0,0 +1,59 @@
1
+ import { parseSegments, type OneBotMessageEvent } from '@snowluma/sdk'
2
+
3
+ function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
5
+ }
6
+
7
+ function toMessageId(value: unknown): number | undefined {
8
+ const raw = typeof value === 'string' ? value.trim() : value
9
+
10
+ if (typeof raw !== 'number' && typeof raw !== 'string') {
11
+ return undefined
12
+ }
13
+
14
+ const parsed =
15
+ typeof raw === 'string'
16
+ ? /^-?\d+$/.test(raw)
17
+ ? Number(raw)
18
+ : Number.NaN
19
+ : raw
20
+
21
+ return Number.isSafeInteger(parsed) && parsed !== 0 ? parsed : undefined
22
+ }
23
+
24
+ function replyIdIn(segments: readonly unknown[]): number | undefined {
25
+ for (const segment of segments) {
26
+ if (!isRecord(segment) || segment.type !== 'reply') continue
27
+
28
+ const data = isRecord(segment.data) ? segment.data : undefined
29
+ const id = toMessageId(data?.id)
30
+
31
+ if (id !== undefined) return id
32
+ }
33
+
34
+ return undefined
35
+ }
36
+
37
+ export function quoteIdOf(
38
+ event: OneBotMessageEvent | undefined
39
+ ): number | undefined {
40
+ if (!event) return undefined
41
+
42
+ const message = event.message
43
+
44
+ if (typeof message === 'string') {
45
+ const id = replyIdIn(parseSegments(message))
46
+
47
+ if (id !== undefined) return id
48
+ } else if (Array.isArray(message)) {
49
+ const id = replyIdIn(message)
50
+
51
+ if (id !== undefined) return id
52
+ } else if (isRecord(message)) {
53
+ const id = replyIdIn([message])
54
+
55
+ if (id !== undefined) return id
56
+ }
57
+
58
+ return replyIdIn(parseSegments(event.raw_message))
59
+ }
@@ -0,0 +1,71 @@
1
+ type Whitespace = ' ' | '\n' | '\r' | '\t' | '\v' | '\f'
2
+
3
+ type TrimLeft<Source extends string> =
4
+ Source extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : Source
5
+
6
+ type TrimRight<Source extends string> =
7
+ Source extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : Source
8
+
9
+ type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
10
+
11
+ type ParamValue<Type extends string> =
12
+ Trim<Type> extends 'string' | 'text'
13
+ ? string
14
+ : Trim<Type> extends 'number' | 'integer' | 'at' | 'reply' | 'time'
15
+ ? number
16
+ : Trim<Type> extends 'boolean'
17
+ ? boolean
18
+ : unknown
19
+
20
+ interface VariadicParam {
21
+ readonly variadic: true
22
+ }
23
+
24
+ type ParamType<Head extends string> = Head extends `${string}:${infer Type}`
25
+ ? ParamValue<Type>
26
+ : string
27
+
28
+ type ParamElement<Body extends string, Required extends boolean> =
29
+ Trim<Body> extends `...${string}`
30
+ ? VariadicParam
31
+ : Trim<Body> extends `${infer Head}=${string}`
32
+ ? ParamType<Head>
33
+ : Required extends true
34
+ ? ParamType<Body>
35
+ : ParamType<Body> | undefined
36
+
37
+ type AppendParam<Acc extends unknown[], Element> = [Element] extends [
38
+ VariadicParam,
39
+ ]
40
+ ? [...Acc, ...string[]]
41
+ : [...Acc, Element]
42
+
43
+ type ParseCommandParams<Source extends string, Acc extends unknown[] = []> =
44
+ Trim<Source> extends ''
45
+ ? Acc
46
+ : Trim<Source> extends `<${infer Body}>${infer Rest}`
47
+ ? ParamElement<Body, true> extends VariadicParam
48
+ ? any[]
49
+ : ParseCommandParams<
50
+ Rest,
51
+ AppendParam<Acc, ParamElement<Body, true>>
52
+ >
53
+ : Trim<Source> extends `[${infer Body}]${infer Rest}`
54
+ ? ParamElement<Body, false> extends VariadicParam
55
+ ? Trim<Rest> extends ''
56
+ ? [...Acc, ...string[]]
57
+ : any[]
58
+ : ParseCommandParams<
59
+ Rest,
60
+ AppendParam<Acc, ParamElement<Body, false>>
61
+ >
62
+ : any[]
63
+
64
+ /**
65
+ * 把 `params` 声明字符串推导成 handler 的位置参数 tuple。
66
+ *
67
+ * 非字面量声明(例如运行时拼接的 `string`)或无法解析的声明会回退为
68
+ * `any[]`,以保证现有插件仍然可以编译。
69
+ */
70
+ export type CommandParamArgs<Declaration extends string> =
71
+ string extends Declaration ? any[] : ParseCommandParams<Declaration>
@@ -0,0 +1,49 @@
1
+ import 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
+ export interface ParamSpec {
34
+ name: string
35
+ type: CommandParamType
36
+ required: boolean
37
+ rest: boolean
38
+ hasDefault: boolean
39
+ defaultValue?: unknown
40
+ }
41
+
42
+ export interface InputToken {
43
+ value: string
44
+ start: number
45
+ }
46
+
47
+ export function declarationError(message: string): Error {
48
+ return new Error(`参数声明错误:${message}`)
49
+ }