@zhin.js/command 1.0.7 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/lib/command-index.d.ts +4 -0
- package/lib/command-index.js +207 -54
- package/lib/definition.d.ts +70 -7
- package/lib/definition.js +71 -18
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/permit.d.ts +5 -0
- package/lib/permit.js +5 -0
- package/lib/provider.js +9 -7
- package/package.json +4 -3
- package/src/command-index.ts +267 -54
- package/src/definition.ts +143 -24
- package/src/index.ts +10 -0
- package/src/permit.ts +14 -0
- package/src/provider.ts +10 -8
package/src/command-index.ts
CHANGED
|
@@ -10,12 +10,17 @@ import type {
|
|
|
10
10
|
} from '@zhin.js/plugin-runtime';
|
|
11
11
|
import {
|
|
12
12
|
createCommandContext,
|
|
13
|
+
resolveCommandSession,
|
|
13
14
|
type CommandDefinition,
|
|
14
15
|
type CommandParameterDefinition,
|
|
15
16
|
type CommandParameterType,
|
|
16
17
|
type CommandParameterValue,
|
|
17
18
|
type CommandSegment,
|
|
19
|
+
type CommandDynamicValue,
|
|
20
|
+
resolveDynamicParams,
|
|
18
21
|
} from './definition.js';
|
|
22
|
+
import { permissionHostToken, type PermissionHost } from '@zhin.js/permission';
|
|
23
|
+
import { toPermissionSubject } from '@zhin.js/permission';
|
|
19
24
|
|
|
20
25
|
export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
21
26
|
readonly required: boolean;
|
|
@@ -26,6 +31,10 @@ export interface CommandDescriptor {
|
|
|
26
31
|
readonly description?: string;
|
|
27
32
|
readonly source: string;
|
|
28
33
|
readonly parameters: readonly CommandParameterDescriptor[];
|
|
34
|
+
readonly alias?: readonly string[];
|
|
35
|
+
readonly permit?: readonly string[];
|
|
36
|
+
/** shortcut 触发键列表(不含预填 params)。 */
|
|
37
|
+
readonly shortcut?: readonly string[];
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export interface CommandDispatchResult {
|
|
@@ -39,12 +48,23 @@ interface CommandRecord extends CommandDescriptor {
|
|
|
39
48
|
readonly slot: Readonly<CapabilitySlot<CommandDefinition>>;
|
|
40
49
|
readonly segments: readonly string[];
|
|
41
50
|
readonly parameter?: CommandParameterDefinition;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface CommandRoute {
|
|
54
|
+
readonly record: CommandRecord;
|
|
55
|
+
readonly segments: readonly string[];
|
|
42
56
|
readonly matcher: SegmentMatcher;
|
|
57
|
+
readonly kind: 'primary' | 'alias';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ShortcutEntry {
|
|
61
|
+
readonly record: CommandRecord;
|
|
62
|
+
readonly params: Readonly<Record<string, CommandDynamicValue>>;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
interface CommandMatch {
|
|
46
66
|
readonly command: CommandRecord;
|
|
47
|
-
readonly params: Readonly<Record<string,
|
|
67
|
+
readonly params: Readonly<Record<string, CommandDynamicValue>>;
|
|
48
68
|
readonly remaining: readonly Readonly<CommandSegment>[];
|
|
49
69
|
}
|
|
50
70
|
|
|
@@ -66,19 +86,39 @@ const segmentFields = {
|
|
|
66
86
|
export class CommandIndex {
|
|
67
87
|
readonly $projection = 'zhin.command-index/1' as const;
|
|
68
88
|
readonly #commands: readonly CommandRecord[];
|
|
89
|
+
readonly #routes: readonly CommandRoute[];
|
|
90
|
+
readonly #shortcuts: ReadonlyMap<string, ShortcutEntry>;
|
|
69
91
|
|
|
70
92
|
constructor(
|
|
71
93
|
slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[],
|
|
72
94
|
private readonly snapshot: RuntimeSnapshot,
|
|
73
95
|
) {
|
|
74
96
|
const commands: CommandRecord[] = [];
|
|
75
|
-
const
|
|
76
|
-
const
|
|
97
|
+
const routes: CommandRoute[] = [];
|
|
98
|
+
const occupancy = new Map<string, string>();
|
|
99
|
+
const shortcuts = new Map<string, ShortcutEntry>();
|
|
100
|
+
|
|
101
|
+
const claim = (key: string, source: string): void => {
|
|
102
|
+
const existing = occupancy.get(key);
|
|
103
|
+
if (existing !== undefined) {
|
|
104
|
+
throw new Error(`Duplicate Command route "${key}" (${source} vs ${existing})`);
|
|
105
|
+
}
|
|
106
|
+
occupancy.set(key, source);
|
|
107
|
+
};
|
|
108
|
+
|
|
77
109
|
for (const slot of slots) {
|
|
78
|
-
const
|
|
110
|
+
const primarySegments = runtimeSegments(slot.owner, slot.localName);
|
|
79
111
|
const parameter = slot.definition.$parameter;
|
|
80
|
-
assertParameterSegment(
|
|
81
|
-
const name = displayName(
|
|
112
|
+
assertParameterSegment(primarySegments, parameter, slot.source);
|
|
113
|
+
const name = displayName(primarySegments, parameter);
|
|
114
|
+
const alias = normalizeAliasList(slot.definition.alias);
|
|
115
|
+
const permit = slot.definition.permit
|
|
116
|
+
? Object.freeze([...slot.definition.permit])
|
|
117
|
+
: undefined;
|
|
118
|
+
const shortcutKeys = slot.definition.shortcut
|
|
119
|
+
? Object.freeze(Object.keys(slot.definition.shortcut).map((key) => key.trim()))
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
82
122
|
const record: CommandRecord = Object.freeze({
|
|
83
123
|
name,
|
|
84
124
|
description: slot.definition.description,
|
|
@@ -87,23 +127,57 @@ export class CommandIndex {
|
|
|
87
127
|
...parameter,
|
|
88
128
|
required: isRequiredParameter(parameter),
|
|
89
129
|
}] : []),
|
|
130
|
+
...(alias ? { alias } : {}),
|
|
131
|
+
...(permit ? { permit } : {}),
|
|
132
|
+
...(shortcutKeys && shortcutKeys.length > 0 ? { shortcut: shortcutKeys } : {}),
|
|
90
133
|
slot,
|
|
91
|
-
segments: Object.freeze(
|
|
134
|
+
segments: Object.freeze(primarySegments),
|
|
92
135
|
parameter,
|
|
93
|
-
matcher: new SegmentMatcher(matcherPattern(segments, parameter), segmentFields),
|
|
94
136
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
|
|
138
|
+
claim(occupancyKey(primarySegments, parameter), slot.source);
|
|
139
|
+
routes.push({
|
|
140
|
+
record,
|
|
141
|
+
segments: primarySegments,
|
|
142
|
+
matcher: new SegmentMatcher(matcherPattern(primarySegments, parameter), segmentFields),
|
|
143
|
+
kind: 'primary',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (alias) {
|
|
147
|
+
for (const entry of alias) {
|
|
148
|
+
const aliasSegments = aliasRuntimeSegments(slot.owner, entry, primarySegments);
|
|
149
|
+
assertParameterSegment(aliasSegments, parameter, `${slot.source} alias ${JSON.stringify(entry)}`);
|
|
150
|
+
claim(occupancyKey(aliasSegments, parameter), `${slot.source} alias ${JSON.stringify(entry)}`);
|
|
151
|
+
routes.push({
|
|
152
|
+
record,
|
|
153
|
+
segments: Object.freeze(aliasSegments),
|
|
154
|
+
matcher: new SegmentMatcher(matcherPattern(aliasSegments, parameter), segmentFields),
|
|
155
|
+
kind: 'alias',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
103
158
|
}
|
|
159
|
+
|
|
160
|
+
if (slot.definition.shortcut) {
|
|
161
|
+
for (const [rawTrigger, prefill] of Object.entries(slot.definition.shortcut)) {
|
|
162
|
+
const trigger = rawTrigger.trim();
|
|
163
|
+
claim(trigger, `${slot.source} shortcut ${JSON.stringify(trigger)}`);
|
|
164
|
+
shortcuts.set(trigger, {
|
|
165
|
+
record,
|
|
166
|
+
params: Object.freeze(resolveShortcutParams(
|
|
167
|
+
slot.definition,
|
|
168
|
+
prefill,
|
|
169
|
+
`${slot.source} shortcut ${JSON.stringify(trigger)}`,
|
|
170
|
+
)),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
104
175
|
commands.push(record);
|
|
105
176
|
}
|
|
106
|
-
|
|
177
|
+
|
|
178
|
+
this.#commands = Object.freeze(commands.sort(compareRecords));
|
|
179
|
+
this.#routes = Object.freeze(routes.sort(compareRoutes));
|
|
180
|
+
this.#shortcuts = shortcuts;
|
|
107
181
|
}
|
|
108
182
|
|
|
109
183
|
list(): readonly CommandDescriptor[] {
|
|
@@ -125,12 +199,13 @@ export class CommandIndex {
|
|
|
125
199
|
this.#diagnoseParameter(name);
|
|
126
200
|
throw new Error(`Unknown Command: ${name}`);
|
|
127
201
|
}
|
|
202
|
+
// Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session。
|
|
128
203
|
return match.command.slot.definition.execute(
|
|
129
204
|
createCommandContext(
|
|
130
205
|
this.snapshot,
|
|
131
206
|
match.command.slot.owner,
|
|
132
207
|
args,
|
|
133
|
-
match.params,
|
|
208
|
+
resolveDynamicParams(match.params, undefined),
|
|
134
209
|
),
|
|
135
210
|
);
|
|
136
211
|
}
|
|
@@ -139,15 +214,41 @@ export class CommandIndex {
|
|
|
139
214
|
input: CommandMatchInput,
|
|
140
215
|
source: unknown = undefined,
|
|
141
216
|
): Promise<CommandDispatchResult> {
|
|
217
|
+
const shortcut = this.#matchShortcut(input);
|
|
218
|
+
if (shortcut) {
|
|
219
|
+
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
220
|
+
return Object.freeze({ matched: false });
|
|
221
|
+
}
|
|
222
|
+
const value = await shortcut.record.slot.definition.execute(
|
|
223
|
+
createCommandContext(
|
|
224
|
+
this.snapshot,
|
|
225
|
+
shortcut.record.slot.owner,
|
|
226
|
+
Object.freeze([]),
|
|
227
|
+
resolveDynamicParams(shortcut.params, source),
|
|
228
|
+
source,
|
|
229
|
+
Object.freeze([]),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
matched: true,
|
|
234
|
+
command: shortcut.record.name,
|
|
235
|
+
owner: shortcut.record.slot.owner,
|
|
236
|
+
value,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
142
240
|
const match = this.#match(input, false);
|
|
143
241
|
if (!match) return Object.freeze({ matched: false });
|
|
242
|
+
if (!(await this.#permitAllows(match.command, source))) {
|
|
243
|
+
return Object.freeze({ matched: false });
|
|
244
|
+
}
|
|
144
245
|
const args = textArgs(match.remaining);
|
|
145
246
|
const value = await match.command.slot.definition.execute(
|
|
146
247
|
createCommandContext(
|
|
147
248
|
this.snapshot,
|
|
148
249
|
match.command.slot.owner,
|
|
149
250
|
args,
|
|
150
|
-
match.params,
|
|
251
|
+
resolveDynamicParams(match.params, source),
|
|
151
252
|
source,
|
|
152
253
|
match.remaining,
|
|
153
254
|
),
|
|
@@ -160,6 +261,35 @@ export class CommandIndex {
|
|
|
160
261
|
});
|
|
161
262
|
}
|
|
162
263
|
|
|
264
|
+
async #permitAllows(record: CommandRecord, source: unknown): Promise<boolean> {
|
|
265
|
+
const permits = record.permit;
|
|
266
|
+
if (!permits || permits.length === 0) return true;
|
|
267
|
+
if (!hasImSession(source)) return true;
|
|
268
|
+
const host = this.#resolveHost();
|
|
269
|
+
if (!host) return false;
|
|
270
|
+
const subject = toPermissionSubject(resolveCommandSession(source));
|
|
271
|
+
return host.checkAll(permits, subject);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#resolveHost(): PermissionHost | undefined {
|
|
275
|
+
try {
|
|
276
|
+
const resources = this.snapshot.resources.get(this.snapshot.root);
|
|
277
|
+
if (!resources) return undefined;
|
|
278
|
+
const host = resources.get(permissionHostToken.id);
|
|
279
|
+
return host && typeof (host as PermissionHost).check === 'function'
|
|
280
|
+
? host as PermissionHost
|
|
281
|
+
: undefined;
|
|
282
|
+
} catch {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#matchShortcut(input: CommandMatchInput): ShortcutEntry | undefined {
|
|
288
|
+
const text = exactMessageText(input);
|
|
289
|
+
if (text === undefined) return undefined;
|
|
290
|
+
return this.#shortcuts.get(text);
|
|
291
|
+
}
|
|
292
|
+
|
|
163
293
|
#match(input: CommandMatchInput, exact: boolean): CommandMatch | undefined {
|
|
164
294
|
const segments = normalizeSegments(
|
|
165
295
|
typeof input === 'string'
|
|
@@ -170,29 +300,26 @@ export class CommandIndex {
|
|
|
170
300
|
);
|
|
171
301
|
if (segments.length === 0) return undefined;
|
|
172
302
|
|
|
173
|
-
for (const
|
|
174
|
-
const result =
|
|
303
|
+
for (const route of this.#routes) {
|
|
304
|
+
const result = route.matcher.match(asMatcherSegments(segments));
|
|
175
305
|
if (!result || !hasCommandBoundary(result.remaining)) continue;
|
|
176
|
-
const parameter =
|
|
177
|
-
const params: Record<string,
|
|
306
|
+
const parameter = route.record.parameter;
|
|
307
|
+
const params: Record<string, CommandDynamicValue> = { ...result.params };
|
|
178
308
|
if (parameter?.rest) {
|
|
179
309
|
const raw = result.params[parameter.name];
|
|
180
310
|
const coerced = coerceRestValues(parameter, Array.isArray(raw) ? raw : []);
|
|
181
|
-
// 必需 `[...name]` 捕获所有:零元素视为不匹配;标量逐词转换失败同样不匹配。
|
|
182
311
|
if (!coerced || (isRequiredParameter(parameter) && coerced.length === 0)) continue;
|
|
183
312
|
params[parameter.name] = coerced;
|
|
184
313
|
}
|
|
185
314
|
const remaining = normalizeSegments(result.remaining);
|
|
186
315
|
if (exact && remaining.length > 0) continue;
|
|
187
|
-
// `[[name]]` 无 default 且未命中时,matcher 对 text 回退 ''、其他类型回退 null;
|
|
188
|
-
// 按契约(省略 default 时未匹配为 undefined)删除该键。
|
|
189
316
|
if (parameter && !parameter.rest && parameter.optional === true
|
|
190
317
|
&& parameter.defaultValue === undefined
|
|
191
318
|
&& (params[parameter.name] === '' || params[parameter.name] === null)) {
|
|
192
319
|
delete params[parameter.name];
|
|
193
320
|
}
|
|
194
321
|
return {
|
|
195
|
-
command,
|
|
322
|
+
command: route.record,
|
|
196
323
|
params: Object.freeze(params),
|
|
197
324
|
remaining,
|
|
198
325
|
};
|
|
@@ -202,12 +329,12 @@ export class CommandIndex {
|
|
|
202
329
|
|
|
203
330
|
#diagnoseParameter(name: string): void {
|
|
204
331
|
const words = splitCommand(name);
|
|
205
|
-
for (const
|
|
206
|
-
const parameter =
|
|
332
|
+
for (const route of this.#routes) {
|
|
333
|
+
const parameter = route.record.parameter;
|
|
207
334
|
if (!parameter || parameter.rest) continue;
|
|
208
|
-
const parameterIndex =
|
|
209
|
-
if (words.length !==
|
|
210
|
-
if (!
|
|
335
|
+
const parameterIndex = route.segments.findIndex((segment) => segment.startsWith('$'));
|
|
336
|
+
if (words.length !== route.segments.length) continue;
|
|
337
|
+
if (!route.segments.every((segment, index) =>
|
|
211
338
|
index === parameterIndex || segment === words[index])) continue;
|
|
212
339
|
const value = words[parameterIndex];
|
|
213
340
|
if (value === undefined || matchesParameter(parameter.type, value)) continue;
|
|
@@ -234,6 +361,103 @@ function runtimeSegments(owner: string, localName: string): string[] {
|
|
|
234
361
|
return [`${prefix}.${localSegments[0]}`, ...localSegments.slice(1)];
|
|
235
362
|
}
|
|
236
363
|
|
|
364
|
+
/**
|
|
365
|
+
* 用 alias 词序列替换全部本地静态段,再按 owner 规则重挂前缀;动态段保留。
|
|
366
|
+
*/
|
|
367
|
+
function aliasRuntimeSegments(
|
|
368
|
+
owner: string,
|
|
369
|
+
alias: string,
|
|
370
|
+
primarySegments: readonly string[],
|
|
371
|
+
): string[] {
|
|
372
|
+
const aliasTokens = alias.trim().split(/\s+/u).filter(Boolean);
|
|
373
|
+
const dynamicTail = primarySegments.filter((segment) => segment.startsWith('$'));
|
|
374
|
+
if (owner === 'root') return [...aliasTokens, ...dynamicTail];
|
|
375
|
+
const prefix = owner.slice('root/'.length).split('/').join('.');
|
|
376
|
+
return [`${prefix}.${aliasTokens[0]}`, ...aliasTokens.slice(1), ...dynamicTail];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function occupancyKey(
|
|
380
|
+
segments: readonly string[],
|
|
381
|
+
parameter: CommandParameterDefinition | undefined,
|
|
382
|
+
): string {
|
|
383
|
+
return parameter ? routeShape(segments) : segments.join(' ');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeAliasList(
|
|
387
|
+
alias: readonly string[] | undefined,
|
|
388
|
+
): readonly string[] | undefined {
|
|
389
|
+
if (!alias || alias.length === 0) return undefined;
|
|
390
|
+
return Object.freeze(alias.map((entry) => entry.trim().split(/\s+/u).filter(Boolean).join(' ')));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function resolveShortcutParams(
|
|
394
|
+
definition: CommandDefinition,
|
|
395
|
+
prefill: Readonly<Record<string, CommandDynamicValue>>,
|
|
396
|
+
source: string,
|
|
397
|
+
): Record<string, CommandDynamicValue> {
|
|
398
|
+
const allowed = new Set<string>();
|
|
399
|
+
const parameter = definition.$parameter;
|
|
400
|
+
if (parameter) allowed.add(parameter.name);
|
|
401
|
+
if (definition.params) {
|
|
402
|
+
for (const key of Object.keys(definition.params)) allowed.add(key);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const key of Object.keys(prefill)) {
|
|
406
|
+
if (!allowed.has(key)) {
|
|
407
|
+
throw new TypeError(
|
|
408
|
+
`Invalid shortcut params for ${source}: unknown key ${JSON.stringify(key)}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const result: Record<string, CommandDynamicValue> = { ...prefill };
|
|
414
|
+
|
|
415
|
+
if (parameter) {
|
|
416
|
+
if (result[parameter.name] === undefined) {
|
|
417
|
+
if (parameter.defaultValue !== undefined) {
|
|
418
|
+
result[parameter.name] = parameter.defaultValue;
|
|
419
|
+
} else if (isRequiredParameter(parameter)) {
|
|
420
|
+
throw new TypeError(
|
|
421
|
+
`Invalid shortcut params for ${source}: missing required ${parameter.name}`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else if (allowed.size === 0 && Object.keys(prefill).length > 0) {
|
|
426
|
+
throw new TypeError(
|
|
427
|
+
`Invalid shortcut params for ${source}: command has no params declaration`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (definition.params) {
|
|
432
|
+
for (const [name, schema] of Object.entries(definition.params)) {
|
|
433
|
+
if (result[name] === undefined && schema.default !== undefined) {
|
|
434
|
+
result[name] = schema.default;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function hasImSession(source: unknown): boolean {
|
|
443
|
+
if (!source || typeof source !== 'object') return false;
|
|
444
|
+
const conversation = (source as { conversation?: unknown }).conversation;
|
|
445
|
+
return !!conversation && typeof conversation === 'object';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function exactMessageText(input: CommandMatchInput): string | undefined {
|
|
449
|
+
if (typeof input === 'string') {
|
|
450
|
+
const trimmed = input.trim();
|
|
451
|
+
return trimmed || undefined;
|
|
452
|
+
}
|
|
453
|
+
// 仅纯单 text 段可作整句 shortcut;含 mention/image 等则不走 shortcut。
|
|
454
|
+
if (input.length !== 1) return undefined;
|
|
455
|
+
const only = input[0];
|
|
456
|
+
if (!only || only.type !== 'text' || typeof only.data.text !== 'string') return undefined;
|
|
457
|
+
const trimmed = only.data.text.trim();
|
|
458
|
+
return trimmed || undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
237
461
|
function assertParameterSegment(
|
|
238
462
|
segments: readonly string[],
|
|
239
463
|
parameter: CommandParameterDefinition | undefined,
|
|
@@ -280,14 +504,13 @@ function matcherPattern(
|
|
|
280
504
|
if (!segment.startsWith('$')) return segment;
|
|
281
505
|
if (!parameter) throw new Error(`Missing Command parameter metadata: ${segment}`);
|
|
282
506
|
const type = matcherType(parameter.type);
|
|
283
|
-
// rest:结构化类型按消息段收集;标量类型先按 text 段收集,再在 #match 里逐词切分转换。
|
|
284
507
|
if (parameter.rest) {
|
|
285
508
|
return `[...${parameter.name}:${isStructuredRestType(parameter.type) ? type : 'text'}]`;
|
|
286
509
|
}
|
|
287
510
|
if (isRequiredParameter(parameter)) return `<${parameter.name}:${type}>`;
|
|
288
511
|
return parameter.defaultValue === undefined
|
|
289
512
|
? `[${parameter.name}:${type}]`
|
|
290
|
-
: `[${parameter.name}:${type}=${String(parameter.defaultValue)}]`;
|
|
513
|
+
: `[${parameter.name}:${type}=${typeof parameter.defaultValue === 'function' ? '<dynamic>' : String(parameter.defaultValue)}]`;
|
|
291
514
|
}).join(' ');
|
|
292
515
|
}
|
|
293
516
|
|
|
@@ -295,7 +518,6 @@ function matcherType(type: CommandParameterType): string {
|
|
|
295
518
|
return type === 'string' ? 'word' : type;
|
|
296
519
|
}
|
|
297
520
|
|
|
298
|
-
/** rest 参数中按消息段收集(matcher 原生行为)的结构化类型。 */
|
|
299
521
|
function isStructuredRestType(type: CommandParameterType): boolean {
|
|
300
522
|
return type === 'mention'
|
|
301
523
|
|| type === 'image'
|
|
@@ -306,19 +528,12 @@ function isStructuredRestType(type: CommandParameterType): boolean {
|
|
|
306
528
|
|| type === 'rps';
|
|
307
529
|
}
|
|
308
530
|
|
|
309
|
-
/**
|
|
310
|
-
* 捕获所有参数的取值粒度由类型决定:
|
|
311
|
-
* - `text` / 结构化类型:逐消息段(matcher 原生结果);
|
|
312
|
-
* - `word` / `string`:逐词(空白切分);
|
|
313
|
-
* - `number` / `integer` / `float` / `boolean`:逐词切分后逐个转换,任一失败返回 undefined(不匹配)。
|
|
314
|
-
*/
|
|
315
531
|
function coerceRestValues(
|
|
316
532
|
parameter: CommandParameterDefinition,
|
|
317
533
|
values: readonly unknown[],
|
|
318
534
|
): readonly (string | number | boolean)[] | undefined {
|
|
319
535
|
const type = parameter.type;
|
|
320
536
|
if (type === 'text' || isStructuredRestType(type)) {
|
|
321
|
-
// 逐消息段,保持 matcher 原生提取值(text 为 string,结构化类型可能是 number 等)。
|
|
322
537
|
return values as readonly (string | number | boolean)[];
|
|
323
538
|
}
|
|
324
539
|
const words = values.flatMap((value) =>
|
|
@@ -346,18 +561,21 @@ function routeShape(segments: readonly string[]): string {
|
|
|
346
561
|
return segments.map((segment) => segment.startsWith('$') ? '$' : segment).join(' ');
|
|
347
562
|
}
|
|
348
563
|
|
|
349
|
-
function
|
|
564
|
+
function compareRecords(left: CommandRecord, right: CommandRecord): number {
|
|
565
|
+
return left.name.localeCompare(right.name);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function compareRoutes(left: CommandRoute, right: CommandRoute): number {
|
|
350
569
|
return dynamicWeight(left) - dynamicWeight(right)
|
|
351
570
|
|| staticSegmentCount(right.segments) - staticSegmentCount(left.segments)
|
|
352
571
|
|| right.segments.length - left.segments.length
|
|
353
|
-
|| right.name.length - left.name.length
|
|
354
|
-
|| left.name.localeCompare(right.name);
|
|
572
|
+
|| right.record.name.length - left.record.name.length
|
|
573
|
+
|| left.record.name.localeCompare(right.record.name);
|
|
355
574
|
}
|
|
356
575
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return command.parameter.rest ? 2 : 1;
|
|
576
|
+
function dynamicWeight(route: CommandRoute): number {
|
|
577
|
+
if (!route.record.parameter) return 0;
|
|
578
|
+
return route.record.parameter.rest ? 2 : 1;
|
|
361
579
|
}
|
|
362
580
|
|
|
363
581
|
function staticSegmentCount(segments: readonly string[]): number {
|
|
@@ -435,16 +653,11 @@ function toDescriptor({
|
|
|
435
653
|
slot: _slot,
|
|
436
654
|
segments: _segments,
|
|
437
655
|
parameter: _parameter,
|
|
438
|
-
matcher: _matcher,
|
|
439
656
|
...descriptor
|
|
440
657
|
}: CommandRecord): CommandDescriptor {
|
|
441
658
|
return descriptor;
|
|
442
659
|
}
|
|
443
660
|
|
|
444
|
-
function duplicateCommand(name: string): Error {
|
|
445
|
-
return new Error(`Duplicate runtime Command: ${name}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
661
|
export class CommandParameterValueError extends TypeError {
|
|
449
662
|
constructor(name: string, type: CommandParameterType, value: string) {
|
|
450
663
|
super(`Invalid value for Command parameter ${name}:${type}: ${value}`);
|