@zhin.js/command 1.2.0 → 1.2.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.
- package/lib/command-index.d.ts +2 -0
- package/lib/command-index.js +25 -3
- package/lib/definition.d.ts +30 -6
- package/lib/definition.js +31 -5
- package/package.json +3 -3
package/lib/command-index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface CommandDescriptor {
|
|
|
25
25
|
readonly execution?: CommandDefinition['execution'];
|
|
26
26
|
/** shortcut 触发键列表(不含预填 params)。 */
|
|
27
27
|
readonly shortcut?: readonly string[];
|
|
28
|
+
/** `match` 正则的字面形式(如 `/^demo$/i`)。 */
|
|
29
|
+
readonly match?: string;
|
|
28
30
|
}
|
|
29
31
|
/** One dispatch reads the caller's exact operation generation and presentation. */
|
|
30
32
|
export interface CommandDispatchRequest {
|
package/lib/command-index.js
CHANGED
|
@@ -90,6 +90,7 @@ export class CommandIndex {
|
|
|
90
90
|
...(alias ? { alias } : {}),
|
|
91
91
|
...(permit ? { permit } : {}),
|
|
92
92
|
...(shortcutKeys && shortcutKeys.length > 0 ? { shortcut: shortcutKeys } : {}),
|
|
93
|
+
...(slot.definition.match ? { match: String(slot.definition.match), pattern: slot.definition.match } : {}),
|
|
93
94
|
slot,
|
|
94
95
|
segments: Object.freeze(primarySegments),
|
|
95
96
|
preferredSegments: Object.freeze(Object.fromEntries(Object.entries(slot.definition.preferredAliasI18n ?? {}).map(([locale, entry]) => [locale, Object.freeze([...namespace, ...aliasRuntimeSegments(entry, primarySegments.slice(namespace.length))])]))),
|
|
@@ -213,9 +214,10 @@ export class CommandIndex {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
const shortcut = this.#matchShortcut(input);
|
|
216
|
-
const
|
|
217
|
+
const routed = shortcut
|
|
217
218
|
? { command: shortcut.record, params: shortcut.params, remaining: Object.freeze([]), helpRequested: false }
|
|
218
219
|
: this.#match(input, false);
|
|
220
|
+
const match = routed ? withPattern(routed, input) : this.#matchPattern(input);
|
|
219
221
|
if (!match) {
|
|
220
222
|
const candidate = this.#invalidRoute(input);
|
|
221
223
|
if (!candidate)
|
|
@@ -257,7 +259,8 @@ export class CommandIndex {
|
|
|
257
259
|
operation,
|
|
258
260
|
interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
|
|
259
261
|
presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath, source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
|
|
260
|
-
}, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options
|
|
262
|
+
}, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options,
|
|
263
|
+
...(match.regex ? { match: match.regex } : {}) }));
|
|
261
264
|
return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
|
|
262
265
|
}
|
|
263
266
|
finally {
|
|
@@ -336,6 +339,18 @@ export class CommandIndex {
|
|
|
336
339
|
return undefined;
|
|
337
340
|
}
|
|
338
341
|
}
|
|
342
|
+
/** `match` 正则:路由不中时,按指令列表顺序取第一条正文符合的指令(只看纯文本消息)。 */
|
|
343
|
+
#matchPattern(input) {
|
|
344
|
+
const text = exactMessageText(input);
|
|
345
|
+
if (text === undefined)
|
|
346
|
+
return undefined;
|
|
347
|
+
for (const command of this.#commands) {
|
|
348
|
+
const regex = command.pattern?.exec(text);
|
|
349
|
+
if (regex)
|
|
350
|
+
return { command, params: Object.freeze({}), remaining: Object.freeze([]), helpRequested: false, regex };
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
339
354
|
#matchShortcut(input) {
|
|
340
355
|
const text = exactMessageText(input);
|
|
341
356
|
if (text === undefined)
|
|
@@ -881,7 +896,14 @@ function textArgs(segments) {
|
|
|
881
896
|
return splitCommand(segment.data.text);
|
|
882
897
|
}));
|
|
883
898
|
}
|
|
884
|
-
|
|
899
|
+
/** A routed command whose `match` also fits the text exposes the match result too. */
|
|
900
|
+
function withPattern(match, input) {
|
|
901
|
+
const pattern = match.command.pattern;
|
|
902
|
+
const text = pattern ? exactMessageText(input) : undefined;
|
|
903
|
+
const regex = pattern && text !== undefined ? pattern.exec(text) : null;
|
|
904
|
+
return regex ? { ...match, regex } : match;
|
|
905
|
+
}
|
|
906
|
+
function toDescriptor({ slot: _slot, segments: _segments, parameter: _parameter, pattern: _pattern, ...descriptor }) {
|
|
885
907
|
return descriptor;
|
|
886
908
|
}
|
|
887
909
|
export class CommandParameterValueError extends TypeError {
|
package/lib/definition.d.ts
CHANGED
|
@@ -254,6 +254,11 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
|
|
|
254
254
|
*/
|
|
255
255
|
/** Canonical user-facing input/confirmation/selection module. */
|
|
256
256
|
readonly interaction?: UserInteraction;
|
|
257
|
+
/**
|
|
258
|
+
* 由 `match` 正则触发时的匹配结果(捕获组、具名组 `match.groups`)。
|
|
259
|
+
* 走指令名路由且正文同样符合 `match` 时也会填入;其余情况为 `undefined`。
|
|
260
|
+
*/
|
|
261
|
+
readonly match?: Readonly<RegExpExecArray>;
|
|
257
262
|
}
|
|
258
263
|
export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage, TAdapter extends string | undefined = string | undefined> {
|
|
259
264
|
/** @internal Runtime feature brand. */
|
|
@@ -289,6 +294,17 @@ export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput
|
|
|
289
294
|
* 与普通路由一样是全局用户输入,冲突会在 generation 构建时拒绝。
|
|
290
295
|
*/
|
|
291
296
|
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
|
|
297
|
+
/**
|
|
298
|
+
* 正则触发:消息正文(去掉指令前缀并 trim 后、只含文本时)符合此正则即执行本指令,
|
|
299
|
+
* 匹配结果见 `context.match`。指令名路由照常可用,先按路由匹配,路由不中再试正则;
|
|
300
|
+
* 多条指令的正则都符合时取指令列表顺序中的第一条。不允许 `g` / `y` 标志(有状态)。
|
|
301
|
+
*/
|
|
302
|
+
readonly match?: RegExp;
|
|
303
|
+
/**
|
|
304
|
+
* 固定回复:与 `execute` 二选一,等价于 `execute: () => reply`。
|
|
305
|
+
* 可以是文本、消息段或 JSX 等任何 `execute` 可返回的内容。
|
|
306
|
+
*/
|
|
307
|
+
readonly reply?: TResult;
|
|
292
308
|
execute(context: CommandContext<TConfig, TInput, TAdapter>): TResult | Promise<TResult>;
|
|
293
309
|
}
|
|
294
310
|
declare module '@zhin.js/plugin-runtime' {
|
|
@@ -300,18 +316,23 @@ declare module '@zhin.js/plugin-runtime' {
|
|
|
300
316
|
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
301
317
|
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
302
318
|
*/
|
|
303
|
-
type CommandAuthoringDefinition<TConfig, TResult, TInput extends CommandMessage, P extends CommandParams, O extends CommandOptions> =
|
|
319
|
+
type CommandAuthoringDefinition<TConfig, TResult, TInput extends CommandMessage, P extends CommandParams, O extends CommandOptions> = Omit<CommandDefinition<TConfig, TResult, TInput, undefined>, '$feature' | '$parameter' | 'params' | 'options' | 'execute' | 'reply'> & {
|
|
304
320
|
readonly params?: P;
|
|
305
321
|
readonly options?: O;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
[TAdapter in RegisteredAdapterName]: Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, '$feature' | '$parameter' | 'params' | 'options' | 'execute'> & {
|
|
322
|
+
} & CommandBody<TConfig, TResult, TInput, undefined, P, O> | {
|
|
323
|
+
[TAdapter in RegisteredAdapterName]: Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, '$feature' | '$parameter' | 'params' | 'options' | 'execute' | 'reply'> & {
|
|
309
324
|
readonly adapter: TAdapter;
|
|
310
325
|
readonly params?: P;
|
|
311
326
|
readonly options?: O;
|
|
312
|
-
|
|
313
|
-
};
|
|
327
|
+
} & CommandBody<TConfig, TResult, TInput, TAdapter, P, O>;
|
|
314
328
|
}[RegisteredAdapterName];
|
|
329
|
+
/** `execute` 与固定回复 `reply` 二选一。 */
|
|
330
|
+
type CommandBody<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> = {
|
|
331
|
+
execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
|
|
332
|
+
} | {
|
|
333
|
+
readonly reply: TResult;
|
|
334
|
+
readonly execute?: undefined;
|
|
335
|
+
};
|
|
315
336
|
type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> = Omit<CommandContext<TConfig, TInput, TAdapter>, 'params' | 'options'> & {
|
|
316
337
|
readonly params: keyof P extends never ? Readonly<Record<string, CommandParameterValue>> : InferCommandParams<P>;
|
|
317
338
|
readonly options: InferCommandOptions<O>;
|
|
@@ -339,6 +360,8 @@ export declare namespace defineCommand {
|
|
|
339
360
|
}
|
|
340
361
|
/** @internal Validate display-only translations without changing their fallback text. */
|
|
341
362
|
export declare function validateDescriptionI18n(value: CommandDefinition['descriptionI18n']): void;
|
|
363
|
+
/** @internal `match` is tested many times per generation, so stateful flags would skip matches. */
|
|
364
|
+
export declare function validateCommandMatch(match: unknown): void;
|
|
342
365
|
/** @internal Convention-loader assembly helper. */
|
|
343
366
|
export declare function bindCommandParameter<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined>(definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>, parameter: CommandParameterDefinition | undefined): Readonly<CommandDefinition<TConfig, TResult, TInput, TAdapter>>;
|
|
344
367
|
/** @internal Runtime validation for convention-discovered modules. */
|
|
@@ -355,6 +378,7 @@ export interface CommandInvocationArguments {
|
|
|
355
378
|
readonly params?: Readonly<Record<string, CommandParameterValue>>;
|
|
356
379
|
readonly options?: Readonly<Record<string, CommandParameterValue>>;
|
|
357
380
|
readonly segments?: readonly Readonly<CommandSegment>[];
|
|
381
|
+
readonly match?: RegExpExecArray;
|
|
358
382
|
}
|
|
359
383
|
/** Author and lifetime authority captured by the dispatcher for this invocation. */
|
|
360
384
|
export interface CommandInvocationOperation {
|
package/lib/definition.js
CHANGED
|
@@ -26,9 +26,7 @@ export const commandParameterTypes = new Set([
|
|
|
26
26
|
'rps',
|
|
27
27
|
]);
|
|
28
28
|
export function defineCommand(definition) {
|
|
29
|
-
|
|
30
|
-
throw new TypeError('Command execute must be a function');
|
|
31
|
-
}
|
|
29
|
+
const body = normalizeCommandBody(definition);
|
|
32
30
|
validateAdapterName(definition.adapter);
|
|
33
31
|
if (definition.params !== undefined) {
|
|
34
32
|
if (!definition.params || typeof definition.params !== 'object') {
|
|
@@ -47,8 +45,9 @@ export function defineCommand(definition) {
|
|
|
47
45
|
validatePreferredAliases(definition);
|
|
48
46
|
validateCommandPermit(definition.permit);
|
|
49
47
|
validateCommandShortcutShape(definition.shortcut);
|
|
48
|
+
validateCommandMatch(definition.match);
|
|
50
49
|
const execution = normalizeExecutionPolicy(definition.execution);
|
|
51
|
-
return Object.freeze({ $feature: commandBrand, ...definition, ...(execution ? { execution } : {}) });
|
|
50
|
+
return Object.freeze({ $feature: commandBrand, ...definition, ...body, ...(execution ? { execution } : {}) });
|
|
52
51
|
}
|
|
53
52
|
/** @internal Validate display-only translations without changing their fallback text. */
|
|
54
53
|
export function validateDescriptionI18n(value) {
|
|
@@ -77,6 +76,32 @@ export function validateDescriptionI18n(value) {
|
|
|
77
76
|
defineCommand.withConfig = function withConfig() {
|
|
78
77
|
return (definition) => defineCommand(definition);
|
|
79
78
|
};
|
|
79
|
+
function normalizeCommandBody(definition) {
|
|
80
|
+
const hasReply = Object.hasOwn(definition, 'reply') && definition.reply !== undefined;
|
|
81
|
+
if (typeof definition.execute === 'function') {
|
|
82
|
+
if (hasReply)
|
|
83
|
+
throw new TypeError('Command declares both execute and reply; use one of them');
|
|
84
|
+
return { execute: definition.execute };
|
|
85
|
+
}
|
|
86
|
+
if (definition.execute !== undefined)
|
|
87
|
+
throw new TypeError('Command execute must be a function');
|
|
88
|
+
if (!hasReply)
|
|
89
|
+
throw new TypeError('Command needs execute or reply');
|
|
90
|
+
if (typeof definition.reply === 'function') {
|
|
91
|
+
throw new TypeError('Command reply is a fixed value; pass a function as execute instead');
|
|
92
|
+
}
|
|
93
|
+
const reply = definition.reply;
|
|
94
|
+
return { execute: () => reply };
|
|
95
|
+
}
|
|
96
|
+
/** @internal `match` is tested many times per generation, so stateful flags would skip matches. */
|
|
97
|
+
export function validateCommandMatch(match) {
|
|
98
|
+
if (match === undefined)
|
|
99
|
+
return;
|
|
100
|
+
if (!(match instanceof RegExp))
|
|
101
|
+
throw new TypeError('Command match must be a RegExp');
|
|
102
|
+
if (match.global || match.sticky)
|
|
103
|
+
throw new TypeError('Command match must not use the g or y flag');
|
|
104
|
+
}
|
|
80
105
|
function validateAdapterName(adapter) {
|
|
81
106
|
if (adapter !== undefined && (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
82
107
|
throw new TypeError('Command adapter must be a non-empty string');
|
|
@@ -176,7 +201,7 @@ export function resolveDynamicParams(params, source) {
|
|
|
176
201
|
/** @internal Command dispatcher assembly helper; each value has one owner. */
|
|
177
202
|
export function createCommandContext(operation, invocation) {
|
|
178
203
|
const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
|
|
179
|
-
const { args, params = {}, options = {}, segments = [] } = invocation;
|
|
204
|
+
const { args, params = {}, options = {}, segments = [], match } = invocation;
|
|
180
205
|
const context = createCapabilityContext({ snapshot, ownerId, signal: readOperationInputSignal(input), operation: operation.operation });
|
|
181
206
|
const session = resolveCommandSession(input);
|
|
182
207
|
const parameterValues = { __proto__: null, ...params };
|
|
@@ -192,6 +217,7 @@ export function createCommandContext(operation, invocation) {
|
|
|
192
217
|
segments: freezeSegments(segments),
|
|
193
218
|
...(input !== undefined ? { input: input } : {}),
|
|
194
219
|
...(interaction !== undefined ? { interaction } : {}),
|
|
220
|
+
...(match !== undefined ? { match: Object.freeze(match) } : {}),
|
|
195
221
|
};
|
|
196
222
|
Object.defineProperty(result, 'session', {
|
|
197
223
|
enumerable: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/command",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Convention-based Command Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"segment-matcher": "^1.0.5",
|
|
23
23
|
"@zhin.js/feature-kit": "1.1.16",
|
|
24
|
-
"@zhin.js/
|
|
24
|
+
"@zhin.js/im-contract": "1.1.14",
|
|
25
25
|
"@zhin.js/permission": "1.2.0",
|
|
26
|
-
"@zhin.js/
|
|
26
|
+
"@zhin.js/plugin-runtime": "1.2.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^26.1.2",
|