@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.
- package/README.md +8 -5
- package/dist/app/cli.d.ts +10 -0
- package/dist/app/cli.d.ts.map +1 -0
- package/dist/app/cli.js +37 -0
- package/dist/app/cli.js.map +1 -0
- package/dist/app/runtime.d.ts +121 -0
- package/dist/app/runtime.d.ts.map +1 -0
- package/dist/app/runtime.js +296 -0
- package/dist/app/runtime.js.map +1 -0
- package/dist/app.js +63 -273
- package/dist/app.js.map +1 -1
- package/dist/core/plugin/command-params.d.ts +3 -37
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/command-params.js +2 -580
- package/dist/core/plugin/command-params.js.map +1 -1
- package/dist/core/plugin/install/paths.d.ts +7 -0
- package/dist/core/plugin/install/paths.d.ts.map +1 -0
- package/dist/core/plugin/install/paths.js +49 -0
- package/dist/core/plugin/install/paths.js.map +1 -0
- package/dist/core/plugin/install/state.d.ts +23 -0
- package/dist/core/plugin/install/state.d.ts.map +1 -0
- package/dist/core/plugin/install/state.js +129 -0
- package/dist/core/plugin/install/state.js.map +1 -0
- package/dist/core/plugin/install/validate-script.d.ts +3 -0
- package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
- package/dist/core/plugin/install/validate-script.js +154 -0
- package/dist/core/plugin/install/validate-script.js.map +1 -0
- package/dist/core/plugin/install-dir.d.ts +3 -28
- package/dist/core/plugin/install-dir.d.ts.map +1 -1
- package/dist/core/plugin/install-dir.js +3 -326
- package/dist/core/plugin/install-dir.js.map +1 -1
- package/dist/core/plugin/installer/checks.d.ts +6 -0
- package/dist/core/plugin/installer/checks.d.ts.map +1 -0
- package/dist/core/plugin/installer/checks.js +99 -0
- package/dist/core/plugin/installer/checks.js.map +1 -0
- package/dist/core/plugin/installer/support.d.ts +37 -0
- package/dist/core/plugin/installer/support.d.ts.map +1 -0
- package/dist/core/plugin/installer/support.js +67 -0
- package/dist/core/plugin/installer/support.js.map +1 -0
- package/dist/core/plugin/installer/warnings.d.ts +5 -0
- package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
- package/dist/core/plugin/installer/warnings.js +28 -0
- package/dist/core/plugin/installer/warnings.js.map +1 -0
- package/dist/core/plugin/installer.d.ts +4 -30
- package/dist/core/plugin/installer.d.ts.map +1 -1
- package/dist/core/plugin/installer.js +21 -202
- package/dist/core/plugin/installer.js.map +1 -1
- package/dist/core/plugin/params/compile.d.ts +3 -0
- package/dist/core/plugin/params/compile.d.ts.map +1 -0
- package/dist/core/plugin/params/compile.js +23 -0
- package/dist/core/plugin/params/compile.js.map +1 -0
- package/dist/core/plugin/params/declaration.d.ts +9 -0
- package/dist/core/plugin/params/declaration.d.ts.map +1 -0
- package/dist/core/plugin/params/declaration.js +286 -0
- package/dist/core/plugin/params/declaration.js.map +1 -0
- package/dist/core/plugin/params/input.d.ts +5 -0
- package/dist/core/plugin/params/input.d.ts.map +1 -0
- package/dist/core/plugin/params/input.js +196 -0
- package/dist/core/plugin/params/input.js.map +1 -0
- package/dist/core/plugin/params/reply.d.ts +3 -0
- package/dist/core/plugin/params/reply.d.ts.map +1 -0
- package/dist/core/plugin/params/reply.js +49 -0
- package/dist/core/plugin/params/reply.js.map +1 -0
- package/dist/core/plugin/params/type-level.d.ts +24 -0
- package/dist/core/plugin/params/type-level.d.ts.map +1 -0
- package/dist/core/plugin/params/type-level.js +2 -0
- package/dist/core/plugin/params/type-level.js.map +1 -0
- package/dist/core/plugin/params/types.d.ts +33 -0
- package/dist/core/plugin/params/types.d.ts.map +1 -0
- package/dist/core/plugin/params/types.js +12 -0
- package/dist/core/plugin/params/types.js.map +1 -0
- package/dist/core/plugin/params/values.d.ts +7 -0
- package/dist/core/plugin/params/values.d.ts.map +1 -0
- package/dist/core/plugin/params/values.js +111 -0
- package/dist/core/plugin/params/values.js.map +1 -0
- package/dist/core/web/api.d.ts +2 -14
- package/dist/core/web/api.d.ts.map +1 -1
- package/dist/core/web/api.js +8 -500
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/routes/actions.d.ts +4 -0
- package/dist/core/web/routes/actions.d.ts.map +1 -0
- package/dist/core/web/routes/actions.js +33 -0
- package/dist/core/web/routes/actions.js.map +1 -0
- package/dist/core/web/routes/config.d.ts +5 -0
- package/dist/core/web/routes/config.d.ts.map +1 -0
- package/dist/core/web/routes/config.js +30 -0
- package/dist/core/web/routes/config.js.map +1 -0
- package/dist/core/web/routes/kv.d.ts +6 -0
- package/dist/core/web/routes/kv.d.ts.map +1 -0
- package/dist/core/web/routes/kv.js +80 -0
- package/dist/core/web/routes/kv.js.map +1 -0
- package/dist/core/web/routes/plugins.d.ts +5 -0
- package/dist/core/web/routes/plugins.d.ts.map +1 -0
- package/dist/core/web/routes/plugins.js +98 -0
- package/dist/core/web/routes/plugins.js.map +1 -0
- package/dist/core/web/routes/sessions.d.ts +8 -0
- package/dist/core/web/routes/sessions.d.ts.map +1 -0
- package/dist/core/web/routes/sessions.js +130 -0
- package/dist/core/web/routes/sessions.js.map +1 -0
- package/dist/core/web/routes/shared.d.ts +18 -0
- package/dist/core/web/routes/shared.d.ts.map +1 -0
- package/dist/core/web/routes/shared.js +19 -0
- package/dist/core/web/routes/shared.js.map +1 -0
- package/dist/core/web/routes/status.d.ts +6 -0
- package/dist/core/web/routes/status.d.ts.map +1 -0
- package/dist/core/web/routes/status.js +132 -0
- package/dist/core/web/routes/status.js.map +1 -0
- package/dist/core/web/server/auth.d.ts +17 -0
- package/dist/core/web/server/auth.d.ts.map +1 -0
- package/dist/core/web/server/auth.js +68 -0
- package/dist/core/web/server/auth.js.map +1 -0
- package/dist/core/web/server/csp.d.ts +11 -0
- package/dist/core/web/server/csp.d.ts.map +1 -0
- package/dist/core/web/server/csp.js +31 -0
- package/dist/core/web/server/csp.js.map +1 -0
- package/dist/core/web/server/request.d.ts +6 -0
- package/dist/core/web/server/request.d.ts.map +1 -0
- package/dist/core/web/server/request.js +33 -0
- package/dist/core/web/server/request.js.map +1 -0
- package/dist/core/web/server/security.d.ts +4 -0
- package/dist/core/web/server/security.d.ts.map +1 -0
- package/dist/core/web/server/security.js +12 -0
- package/dist/core/web/server/security.js.map +1 -0
- package/dist/core/web/server/sse.d.ts +18 -0
- package/dist/core/web/server/sse.d.ts.map +1 -0
- package/dist/core/web/server/sse.js +49 -0
- package/dist/core/web/server/sse.js.map +1 -0
- package/dist/core/web/server/static.d.ts +16 -0
- package/dist/core/web/server/static.d.ts.map +1 -0
- package/dist/core/web/server/static.js +80 -0
- package/dist/core/web/server/static.js.map +1 -0
- package/dist/core/web/server.d.ts +0 -7
- package/dist/core/web/server.d.ts.map +1 -1
- package/dist/core/web/server.js +41 -263
- package/dist/core/web/server.js.map +1 -1
- package/docs/architecture.md +28 -11
- package/docs/configuration.md +1 -1
- package/docs/migration.md +2 -2
- package/docs/plugin-api.md +24 -11
- package/docs/plugin-development.md +1 -1
- package/package.json +19 -17
- package/src/app/cli.ts +47 -0
- package/src/app/runtime.ts +522 -0
- package/src/app.ts +87 -395
- package/src/core/plugin/command-params.ts +8 -871
- package/src/core/plugin/install/paths.ts +72 -0
- package/src/core/plugin/install/state.ts +196 -0
- package/src/core/plugin/install/validate-script.ts +154 -0
- package/src/core/plugin/install-dir.ts +19 -427
- package/src/core/plugin/installer/checks.ts +137 -0
- package/src/core/plugin/installer/support.ts +120 -0
- package/src/core/plugin/installer/warnings.ts +52 -0
- package/src/core/plugin/installer.ts +52 -301
- package/src/core/plugin/params/compile.ts +41 -0
- package/src/core/plugin/params/declaration.ts +394 -0
- package/src/core/plugin/params/input.ts +281 -0
- package/src/core/plugin/params/reply.ts +59 -0
- package/src/core/plugin/params/type-level.ts +75 -0
- package/src/core/plugin/params/types.ts +55 -0
- package/src/core/plugin/params/values.ts +150 -0
- package/src/core/web/api.ts +33 -759
- package/src/core/web/routes/actions.ts +43 -0
- package/src/core/web/routes/config.ts +42 -0
- package/src/core/web/routes/kv.ts +122 -0
- package/src/core/web/routes/plugins.ts +151 -0
- package/src/core/web/routes/sessions.ts +188 -0
- package/src/core/web/routes/shared.ts +42 -0
- package/src/core/web/routes/status.ts +191 -0
- package/src/core/web/server/auth.ts +126 -0
- package/src/core/web/server/csp.ts +33 -0
- package/src/core/web/server/request.ts +38 -0
- package/src/core/web/server/security.ts +14 -0
- package/src/core/web/server/sse.ts +74 -0
- package/src/core/web/server/static.ts +112 -0
- 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
|
+
}
|