@zhin.js/command 1.0.14 → 1.0.16
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 +2 -0
- package/lib/command-index.d.ts +4 -2
- package/lib/command-index.js +17 -7
- package/lib/definition.d.ts +39 -49
- package/lib/definition.js +22 -8
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/provider.d.ts +1 -1
- package/package.json +5 -4
- package/src/command-index.ts +29 -8
- package/src/definition.ts +76 -47
- package/src/index.ts +6 -0
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ commands/search/[...kw].ts -> search <...kw> (params: { kw: { type: 'text
|
|
|
44
44
|
- `adapter` / `endpoint`:适配器实例 id 与 endpoint 名。
|
|
45
45
|
- `scene`:`{ id, type, name? }` 场景对象。
|
|
46
46
|
- `sender`:`{ id, name?, role: string[] }` 发送者对象。
|
|
47
|
+
- `interaction`:可选的 `UserInteraction`。用 `ask()` 获取单个类型化结论,或用
|
|
48
|
+
`sequence()` 连续收集多个结论;请求可声明 `title`、`description`、`tip` 与校验规则。
|
|
47
49
|
|
|
48
50
|
可选声明字段:
|
|
49
51
|
|
package/lib/command-index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import
|
|
2
|
+
import type { UserInteractionFactory } from '@zhin.js/interaction';
|
|
3
|
+
import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment } from './definition.js';
|
|
3
4
|
export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
4
5
|
readonly required: boolean;
|
|
5
6
|
}
|
|
6
7
|
export interface CommandDescriptor {
|
|
7
8
|
readonly name: string;
|
|
8
9
|
readonly description?: string;
|
|
10
|
+
readonly adapter?: string;
|
|
9
11
|
readonly source: string;
|
|
10
12
|
readonly parameters: readonly CommandParameterDescriptor[];
|
|
11
13
|
readonly alias?: readonly string[];
|
|
@@ -31,7 +33,7 @@ export declare class CommandIndex {
|
|
|
31
33
|
list(): readonly CommandDescriptor[];
|
|
32
34
|
has(name: string): boolean;
|
|
33
35
|
execute(name: string, args?: readonly string[]): Promise<unknown>;
|
|
34
|
-
dispatch(input: CommandMatchInput, source?: unknown,
|
|
36
|
+
dispatch(input: CommandMatchInput, source?: unknown, interactionFactory?: UserInteractionFactory, commandPrefix?: string): Promise<CommandDispatchResult>;
|
|
35
37
|
}
|
|
36
38
|
export declare function isCommandIndex(value: unknown): value is CommandIndex;
|
|
37
39
|
export declare class CommandParameterValueError extends TypeError {
|
package/lib/command-index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SegmentMatcher, TypeMatcherRegistry, } from 'segment-matcher';
|
|
2
|
+
import { operationClientAdapter } from '@zhin.js/feature-kit';
|
|
3
|
+
import { permissionHostToken, toPermissionSubject, } from '@zhin.js/permission';
|
|
2
4
|
import { createCommandContext, resolveCommandSession, resolveDynamicParams, } from './definition.js';
|
|
3
|
-
import { permissionHostToken } from '@zhin.js/permission';
|
|
4
|
-
import { toPermissionSubject } from '@zhin.js/permission';
|
|
5
5
|
const segmentFields = {
|
|
6
6
|
text: 'text',
|
|
7
7
|
mention: 'target',
|
|
@@ -49,6 +49,7 @@ export class CommandIndex {
|
|
|
49
49
|
const record = Object.freeze({
|
|
50
50
|
name,
|
|
51
51
|
description: slot.definition.description,
|
|
52
|
+
...(slot.definition.adapter ? { adapter: slot.definition.adapter } : {}),
|
|
52
53
|
source: slot.source,
|
|
53
54
|
parameters: Object.freeze(parameter ? [{
|
|
54
55
|
...parameter,
|
|
@@ -117,9 +118,9 @@ export class CommandIndex {
|
|
|
117
118
|
throw new Error(`Unknown Command: ${name}`);
|
|
118
119
|
}
|
|
119
120
|
// Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session。
|
|
120
|
-
return match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, undefined)));
|
|
121
|
+
return match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, undefined), undefined, Object.freeze([]), undefined, match.command.slot.definition.adapter));
|
|
121
122
|
}
|
|
122
|
-
async dispatch(input, source = undefined,
|
|
123
|
+
async dispatch(input, source = undefined, interactionFactory, commandPrefix = '') {
|
|
123
124
|
if (this.#menu) {
|
|
124
125
|
const menuValue = this.#dispatchMenu(input, commandPrefix);
|
|
125
126
|
if (menuValue !== undefined) {
|
|
@@ -131,13 +132,16 @@ export class CommandIndex {
|
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
|
-
const
|
|
135
|
+
const interaction = interactionFactory?.(source);
|
|
135
136
|
const shortcut = this.#matchShortcut(input);
|
|
136
137
|
if (shortcut) {
|
|
138
|
+
if (!commandAdapterMatches(shortcut.record, source)) {
|
|
139
|
+
return Object.freeze({ matched: false });
|
|
140
|
+
}
|
|
137
141
|
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
138
142
|
return Object.freeze({ matched: false });
|
|
139
143
|
}
|
|
140
|
-
const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), resolveDynamicParams(shortcut.params, source), source, Object.freeze([]),
|
|
144
|
+
const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), resolveDynamicParams(shortcut.params, source), source, Object.freeze([]), interaction, shortcut.record.slot.definition.adapter));
|
|
141
145
|
return Object.freeze({
|
|
142
146
|
matched: true,
|
|
143
147
|
command: shortcut.record.name,
|
|
@@ -148,11 +152,14 @@ export class CommandIndex {
|
|
|
148
152
|
const match = this.#match(input, false);
|
|
149
153
|
if (!match)
|
|
150
154
|
return Object.freeze({ matched: false });
|
|
155
|
+
if (!commandAdapterMatches(match.command, source)) {
|
|
156
|
+
return Object.freeze({ matched: false });
|
|
157
|
+
}
|
|
151
158
|
if (!(await this.#permitAllows(match.command, source))) {
|
|
152
159
|
return Object.freeze({ matched: false });
|
|
153
160
|
}
|
|
154
161
|
const args = textArgs(match.remaining);
|
|
155
|
-
const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining,
|
|
162
|
+
const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining, interaction, match.command.slot.definition.adapter));
|
|
156
163
|
return Object.freeze({
|
|
157
164
|
matched: true,
|
|
158
165
|
command: match.command.name,
|
|
@@ -565,6 +572,9 @@ function textArgs(segments) {
|
|
|
565
572
|
function toDescriptor({ slot: _slot, segments: _segments, parameter: _parameter, ...descriptor }) {
|
|
566
573
|
return descriptor;
|
|
567
574
|
}
|
|
575
|
+
function commandAdapterMatches(record, source) {
|
|
576
|
+
return !record.adapter || operationClientAdapter(source) === record.adapter;
|
|
577
|
+
}
|
|
568
578
|
export class CommandParameterValueError extends TypeError {
|
|
569
579
|
constructor(name, type, value) {
|
|
570
580
|
super(`Invalid value for Command parameter ${name}:${type}: ${value}`);
|
package/lib/definition.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command authoring API consumed from `zhin.js/command`.
|
|
3
|
+
* @module zhin.js/command
|
|
4
|
+
*/
|
|
1
5
|
import type { PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { type CapabilityContext } from '@zhin.js/feature-kit';
|
|
6
|
+
import { type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
|
|
7
|
+
import type { UserInteraction } from '@zhin.js/interaction';
|
|
3
8
|
declare const commandBrand: "zhin.command/1";
|
|
4
9
|
export type CommandParameterType = 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'word' | 'text' | 'mention' | 'image' | 'face' | 'reply' | 'forward' | 'dice' | 'rps';
|
|
5
10
|
export type CommandParameterValue = string | number | boolean | ReadonlyArray<string | number | boolean> | Readonly<Record<string, unknown>> | null;
|
|
@@ -23,6 +28,7 @@ export type CommandParameterValue = string | number | boolean | ReadonlyArray<st
|
|
|
23
28
|
* ```
|
|
24
29
|
*/
|
|
25
30
|
export type CommandDynamicValue = CommandParameterValue | ((session: CommandSession) => CommandParameterValue);
|
|
31
|
+
/** @internal Runtime validator lookup. */
|
|
26
32
|
export declare const commandParameterTypes: ReadonlySet<CommandParameterType>;
|
|
27
33
|
/**
|
|
28
34
|
* Next.js 风格参数声明(`defineCommand({ params: ... })`)。
|
|
@@ -101,6 +107,8 @@ export interface CommandMessage {
|
|
|
101
107
|
};
|
|
102
108
|
readonly id?: string;
|
|
103
109
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
110
|
+
readonly clientAdapter?: string;
|
|
111
|
+
readonly $client?: unknown;
|
|
104
112
|
/** 若上游已结构化,优先采用。 */
|
|
105
113
|
readonly scene?: CommandScene;
|
|
106
114
|
$reply?(content: unknown): Promise<unknown>;
|
|
@@ -127,7 +135,7 @@ export interface CommandMessage {
|
|
|
127
135
|
}
|
|
128
136
|
/**
|
|
129
137
|
* IM 入站快捷字段。
|
|
130
|
-
* 有 `CommandMessage` 来源时由
|
|
138
|
+
* 有 `CommandMessage` 来源时由 Runtime 从消息结构填充;
|
|
131
139
|
* `CommandIndex.execute(name)` 等无消息路径下为 `undefined`。
|
|
132
140
|
*/
|
|
133
141
|
export interface CommandSession {
|
|
@@ -143,57 +151,22 @@ export interface CommandSession {
|
|
|
143
151
|
/** 发送者对象(id / name / role[])。 */
|
|
144
152
|
readonly sender?: CommandSender;
|
|
145
153
|
}
|
|
146
|
-
export interface CommandPromptOptions {
|
|
147
|
-
readonly timeout?: number;
|
|
148
|
-
readonly timeoutText?: string;
|
|
149
|
-
/** Cancel the pending claim when the owning turn aborts. */
|
|
150
|
-
readonly signal?: AbortSignal;
|
|
151
|
-
}
|
|
152
|
-
export interface CommandPromptListOptions extends CommandPromptOptions {
|
|
153
|
-
readonly type?: 'text' | 'number' | 'boolean';
|
|
154
|
-
readonly separator?: string;
|
|
155
|
-
readonly default?: readonly (string | number | boolean)[];
|
|
156
|
-
}
|
|
157
|
-
export interface CommandPromptPickOptions<V = unknown> extends CommandPromptOptions {
|
|
158
|
-
readonly options: readonly {
|
|
159
|
-
readonly label: string;
|
|
160
|
-
readonly value: V;
|
|
161
|
-
}[];
|
|
162
|
-
readonly multiple?: boolean;
|
|
163
|
-
readonly separator?: string;
|
|
164
|
-
readonly default?: V | readonly V[];
|
|
165
|
-
}
|
|
166
154
|
/**
|
|
167
155
|
* 命令内对话式交互输入。
|
|
168
156
|
*
|
|
169
|
-
* IM 派发时自动注入(`context.
|
|
157
|
+
* IM 派发时自动注入(`context.interaction`);Host / CLI 无消息来源时为 `undefined`。
|
|
170
158
|
*
|
|
171
159
|
* ```ts
|
|
172
160
|
* defineCommand({
|
|
173
161
|
* execute: async (context) => {
|
|
174
|
-
* const name = await context.
|
|
175
|
-
* const age = await context.
|
|
162
|
+
* const name = await context.interaction!.ask({ type: 'text', title: '请输入你的名字' });
|
|
163
|
+
* const age = await context.interaction!.ask({ type: 'number', title: '请输入你的年龄' });
|
|
176
164
|
* return `你好 ${name},你 ${age} 岁了`;
|
|
177
165
|
* },
|
|
178
166
|
* });
|
|
179
167
|
* ```
|
|
180
168
|
*/
|
|
181
|
-
export interface
|
|
182
|
-
text(tips: string, options?: CommandPromptOptions & {
|
|
183
|
-
readonly default?: string;
|
|
184
|
-
}): Promise<string>;
|
|
185
|
-
number(tips: string, options?: CommandPromptOptions & {
|
|
186
|
-
readonly default?: number;
|
|
187
|
-
}): Promise<number>;
|
|
188
|
-
confirm(tips: string, options?: CommandPromptOptions & {
|
|
189
|
-
readonly condition?: string;
|
|
190
|
-
readonly default?: boolean;
|
|
191
|
-
}): Promise<boolean>;
|
|
192
|
-
list(tips: string, options?: CommandPromptListOptions): Promise<readonly (string | number | boolean)[]>;
|
|
193
|
-
pick<V = unknown>(tips: string, options: CommandPromptPickOptions<V>): Promise<V | readonly V[]>;
|
|
194
|
-
}
|
|
195
|
-
export type CommandPromptFactory = (source: unknown) => CommandPrompt | undefined;
|
|
196
|
-
export interface CommandContext<TConfig = unknown, TInput extends CommandMessage = CommandMessage> extends CapabilityContext<TConfig>, CommandSession {
|
|
169
|
+
export interface CommandContext<TConfig = unknown, TInput extends CommandMessage = CommandMessage, TAdapter extends string | undefined = undefined> extends CapabilityContext<TConfig>, CommandSession {
|
|
197
170
|
readonly args: readonly string[];
|
|
198
171
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
199
172
|
/** Structured arguments left after the command pattern was consumed. */
|
|
@@ -203,15 +176,22 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
|
|
|
203
176
|
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
204
177
|
*/
|
|
205
178
|
readonly input?: TInput;
|
|
179
|
+
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
180
|
+
readonly $client: AdapterClient<TAdapter>;
|
|
206
181
|
/**
|
|
207
182
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
208
183
|
*/
|
|
209
|
-
|
|
184
|
+
/** Canonical user-facing input/confirmation/selection module. */
|
|
185
|
+
readonly interaction?: UserInteraction;
|
|
210
186
|
}
|
|
211
|
-
export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage> {
|
|
187
|
+
export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage, TAdapter extends string | undefined = string | undefined> {
|
|
188
|
+
/** @internal Runtime feature brand. */
|
|
212
189
|
readonly $feature: typeof commandBrand;
|
|
190
|
+
/** @internal Convention-derived parameter metadata. */
|
|
213
191
|
readonly $parameter?: CommandParameterDefinition;
|
|
214
192
|
readonly description?: string;
|
|
193
|
+
/** Restrict this command to one adapter and infer `context.$client`. */
|
|
194
|
+
readonly adapter?: TAdapter;
|
|
215
195
|
/**
|
|
216
196
|
* Next.js 风格参数声明:动态段文件名(`[name]` 等)的形态配合这里的
|
|
217
197
|
* 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
|
|
@@ -232,29 +212,39 @@ export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput
|
|
|
232
212
|
* 可打破 owner 命名空间。
|
|
233
213
|
*/
|
|
234
214
|
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
|
|
235
|
-
execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
|
|
215
|
+
execute(context: CommandContext<TConfig, TInput, TAdapter>): TResult | Promise<TResult>;
|
|
236
216
|
}
|
|
237
217
|
declare module '@zhin.js/plugin-runtime' {
|
|
238
|
-
interface PluginSetupContext<TConfig> {
|
|
239
|
-
addCommand<TResult = unknown, TInput extends CommandMessage = CommandMessage>(localName: string, definition: CommandDefinition<TConfig, TResult, TInput>): void;
|
|
218
|
+
interface PluginSetupContext<TConfig = unknown> {
|
|
219
|
+
addCommand<TResult = unknown, TInput extends CommandMessage = CommandMessage, TAdapter extends string | undefined = undefined>(localName: string, definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>): void;
|
|
240
220
|
}
|
|
241
221
|
}
|
|
242
222
|
/**
|
|
243
223
|
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
244
224
|
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
245
225
|
*/
|
|
246
|
-
|
|
247
|
-
|
|
226
|
+
type CommandAuthoringDefinition<TConfig, TResult, TInput extends CommandMessage> = Omit<CommandDefinition<TConfig, TResult, TInput, undefined>, '$feature' | '$parameter'> | {
|
|
227
|
+
[TAdapter in RegisteredAdapterName]: Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, '$feature' | '$parameter'> & {
|
|
228
|
+
readonly adapter: TAdapter;
|
|
229
|
+
};
|
|
230
|
+
}[RegisteredAdapterName];
|
|
231
|
+
export declare function defineCommand<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput>): Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>>;
|
|
232
|
+
/** @internal Convention-loader assembly helper. */
|
|
233
|
+
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>>;
|
|
234
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
248
235
|
export declare function parseCommandDefinition(value: unknown): CommandDefinition;
|
|
249
236
|
/**
|
|
250
237
|
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
251
238
|
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
239
|
+
* @internal Command projection helper.
|
|
252
240
|
*/
|
|
253
241
|
export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
|
|
254
|
-
|
|
242
|
+
/** @internal Command dispatcher assembly helper. */
|
|
243
|
+
export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[], interaction?: UserInteraction, adapter?: string): CommandContext;
|
|
255
244
|
/**
|
|
256
245
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
257
246
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
|
247
|
+
* @internal Command dispatcher projection helper.
|
|
258
248
|
*/
|
|
259
249
|
export declare function resolveCommandSession(input: unknown): CommandSession;
|
|
260
250
|
export {};
|
package/lib/definition.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createCapabilityContext, } from '@zhin.js/feature-kit';
|
|
1
|
+
import { createCapabilityContext, readOperationClient, } from '@zhin.js/feature-kit';
|
|
2
2
|
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
3
3
|
const commandBrand = 'zhin.command/1';
|
|
4
|
+
/** @internal Runtime validator lookup. */
|
|
4
5
|
export const commandParameterTypes = new Set([
|
|
5
6
|
'string',
|
|
6
7
|
'number',
|
|
@@ -17,14 +18,11 @@ export const commandParameterTypes = new Set([
|
|
|
17
18
|
'dice',
|
|
18
19
|
'rps',
|
|
19
20
|
]);
|
|
20
|
-
/**
|
|
21
|
-
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
22
|
-
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
23
|
-
*/
|
|
24
21
|
export function defineCommand(definition) {
|
|
25
22
|
if (typeof definition.execute !== 'function') {
|
|
26
23
|
throw new TypeError('Command execute must be a function');
|
|
27
24
|
}
|
|
25
|
+
validateAdapterName(definition.adapter);
|
|
28
26
|
if (definition.params !== undefined) {
|
|
29
27
|
if (!definition.params || typeof definition.params !== 'object') {
|
|
30
28
|
throw new TypeError('Command params must be a Record<string, CommandParamSchema>');
|
|
@@ -41,6 +39,11 @@ export function defineCommand(definition) {
|
|
|
41
39
|
validateCommandShortcutShape(definition.shortcut);
|
|
42
40
|
return Object.freeze({ $feature: commandBrand, ...definition });
|
|
43
41
|
}
|
|
42
|
+
function validateAdapterName(adapter) {
|
|
43
|
+
if (adapter !== undefined && (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
44
|
+
throw new TypeError('Command adapter must be a non-empty string');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
44
47
|
function validateCommandAlias(alias) {
|
|
45
48
|
if (alias === undefined)
|
|
46
49
|
return;
|
|
@@ -85,11 +88,13 @@ function validateCommandShortcutShape(shortcut) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
}
|
|
91
|
+
/** @internal Convention-loader assembly helper. */
|
|
88
92
|
export function bindCommandParameter(definition, parameter) {
|
|
89
93
|
if (!parameter)
|
|
90
94
|
return definition;
|
|
91
95
|
return Object.freeze({ ...definition, $parameter: Object.freeze({ ...parameter }) });
|
|
92
96
|
}
|
|
97
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
93
98
|
export function parseCommandDefinition(value) {
|
|
94
99
|
if (!value || typeof value !== 'object') {
|
|
95
100
|
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
@@ -98,11 +103,13 @@ export function parseCommandDefinition(value) {
|
|
|
98
103
|
if (definition.$feature !== commandBrand || typeof definition.execute !== 'function') {
|
|
99
104
|
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
100
105
|
}
|
|
106
|
+
validateAdapterName(definition.adapter);
|
|
101
107
|
return definition;
|
|
102
108
|
}
|
|
103
109
|
/**
|
|
104
110
|
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
105
111
|
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
112
|
+
* @internal Command projection helper.
|
|
106
113
|
*/
|
|
107
114
|
export function resolveDynamicParams(params, source) {
|
|
108
115
|
const session = resolveCommandSession(source);
|
|
@@ -112,22 +119,29 @@ export function resolveDynamicParams(params, source) {
|
|
|
112
119
|
}
|
|
113
120
|
return Object.freeze(resolved);
|
|
114
121
|
}
|
|
115
|
-
|
|
122
|
+
/** @internal Command dispatcher assembly helper. */
|
|
123
|
+
export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), interaction, adapter) {
|
|
116
124
|
const context = createCapabilityContext(snapshot, ownerId);
|
|
117
125
|
const session = resolveCommandSession(input);
|
|
118
|
-
|
|
126
|
+
const result = {
|
|
119
127
|
...context,
|
|
120
128
|
...session,
|
|
121
129
|
args: Object.freeze([...args]),
|
|
122
130
|
params: Object.freeze({ ...params }),
|
|
123
131
|
segments: freezeSegments(segments),
|
|
124
132
|
...(input !== undefined ? { input: input } : {}),
|
|
125
|
-
...(
|
|
133
|
+
...(interaction !== undefined ? { interaction } : {}),
|
|
134
|
+
};
|
|
135
|
+
Object.defineProperty(result, '$client', {
|
|
136
|
+
enumerable: true,
|
|
137
|
+
get: () => readOperationClient(input, adapter),
|
|
126
138
|
});
|
|
139
|
+
return Object.freeze(result);
|
|
127
140
|
}
|
|
128
141
|
/**
|
|
129
142
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
130
143
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
|
144
|
+
* @internal Command dispatcher projection helper.
|
|
131
145
|
*/
|
|
132
146
|
export function resolveCommandSession(input) {
|
|
133
147
|
if (!isCommandMessageLike(input))
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command authoring contracts, parameter schemas, and permit helpers.
|
|
3
|
+
* @module @zhin.js/command
|
|
4
|
+
*/
|
|
5
|
+
/** @internal Runtime command projection. */
|
|
1
6
|
export * from './command-index.js';
|
|
7
|
+
/** @public Command authoring contract and `defineCommand`. */
|
|
2
8
|
export * from './definition.js';
|
|
3
9
|
export { assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, isBuiltinPermit, isPlatformPermit, parsePermitName, type ParsedPermit, type PermitKind, } from './permit.js';
|
|
4
10
|
export { CommandPathSyntaxError, commandFeatureId, default as commandFeature, } from './provider.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command authoring contracts, parameter schemas, and permit helpers.
|
|
3
|
+
* @module @zhin.js/command
|
|
4
|
+
*/
|
|
5
|
+
/** @internal Runtime command projection. */
|
|
1
6
|
export * from './command-index.js';
|
|
7
|
+
/** @public Command authoring contract and `defineCommand`. */
|
|
2
8
|
export * from './definition.js';
|
|
3
9
|
export { assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, isBuiltinPermit, isPlatformPermit, parsePermitName, } from './permit.js';
|
|
4
10
|
export { CommandPathSyntaxError, commandFeatureId, default as commandFeature, } from './provider.js';
|
package/lib/provider.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export declare const commandFeatureId: import("@zhin.js/plugin-runtime").Feature
|
|
|
4
4
|
export declare class CommandPathSyntaxError extends TypeError {
|
|
5
5
|
constructor(file: string, detail?: string);
|
|
6
6
|
}
|
|
7
|
-
declare const commandFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<CommandDefinition<unknown, unknown, import("./definition.js").CommandMessage>, CommandIndex>>;
|
|
7
|
+
declare const commandFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<CommandDefinition<unknown, unknown, import("./definition.js").CommandMessage, string | undefined>, CommandIndex>>;
|
|
8
8
|
export default commandFeature;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/command",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Convention-based Command Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"segment-matcher": "^1.0.5",
|
|
21
|
-
"@zhin.js/feature-kit": "1.0.
|
|
22
|
-
"@zhin.js/
|
|
23
|
-
"@zhin.js/
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.13",
|
|
22
|
+
"@zhin.js/interaction": "1.0.1",
|
|
23
|
+
"@zhin.js/permission": "1.0.4",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.8"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/node": "^26.1.2",
|
package/src/command-index.ts
CHANGED
|
@@ -8,20 +8,23 @@ import type {
|
|
|
8
8
|
PluginId,
|
|
9
9
|
RuntimeSnapshot,
|
|
10
10
|
} from '@zhin.js/plugin-runtime';
|
|
11
|
+
import type { UserInteractionFactory } from '@zhin.js/interaction';
|
|
12
|
+
import { operationClientAdapter } from '@zhin.js/feature-kit';
|
|
13
|
+
import {
|
|
14
|
+
permissionHostToken,
|
|
15
|
+
toPermissionSubject,
|
|
16
|
+
type PermissionHost,
|
|
17
|
+
} from '@zhin.js/permission';
|
|
11
18
|
import {
|
|
12
19
|
createCommandContext,
|
|
13
20
|
resolveCommandSession,
|
|
14
21
|
type CommandDefinition,
|
|
15
22
|
type CommandParameterDefinition,
|
|
16
23
|
type CommandParameterType,
|
|
17
|
-
type CommandParameterValue,
|
|
18
24
|
type CommandSegment,
|
|
19
25
|
type CommandDynamicValue,
|
|
20
|
-
type CommandPromptFactory,
|
|
21
26
|
resolveDynamicParams,
|
|
22
27
|
} from './definition.js';
|
|
23
|
-
import { permissionHostToken, type PermissionHost } from '@zhin.js/permission';
|
|
24
|
-
import { toPermissionSubject } from '@zhin.js/permission';
|
|
25
28
|
|
|
26
29
|
export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
27
30
|
readonly required: boolean;
|
|
@@ -30,6 +33,7 @@ export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
|
30
33
|
export interface CommandDescriptor {
|
|
31
34
|
readonly name: string;
|
|
32
35
|
readonly description?: string;
|
|
36
|
+
readonly adapter?: string;
|
|
33
37
|
readonly source: string;
|
|
34
38
|
readonly parameters: readonly CommandParameterDescriptor[];
|
|
35
39
|
readonly alias?: readonly string[];
|
|
@@ -130,6 +134,7 @@ export class CommandIndex {
|
|
|
130
134
|
const record: CommandRecord = Object.freeze({
|
|
131
135
|
name,
|
|
132
136
|
description: slot.definition.description,
|
|
137
|
+
...(slot.definition.adapter ? { adapter: slot.definition.adapter } : {}),
|
|
133
138
|
source: slot.source,
|
|
134
139
|
parameters: Object.freeze(parameter ? [{
|
|
135
140
|
...parameter,
|
|
@@ -214,6 +219,10 @@ export class CommandIndex {
|
|
|
214
219
|
match.command.slot.owner,
|
|
215
220
|
args,
|
|
216
221
|
resolveDynamicParams(match.params, undefined),
|
|
222
|
+
undefined,
|
|
223
|
+
Object.freeze([]),
|
|
224
|
+
undefined,
|
|
225
|
+
match.command.slot.definition.adapter,
|
|
217
226
|
),
|
|
218
227
|
);
|
|
219
228
|
}
|
|
@@ -221,7 +230,7 @@ export class CommandIndex {
|
|
|
221
230
|
async dispatch(
|
|
222
231
|
input: CommandMatchInput,
|
|
223
232
|
source: unknown = undefined,
|
|
224
|
-
|
|
233
|
+
interactionFactory?: UserInteractionFactory,
|
|
225
234
|
commandPrefix = '',
|
|
226
235
|
): Promise<CommandDispatchResult> {
|
|
227
236
|
if (this.#menu) {
|
|
@@ -235,9 +244,12 @@ export class CommandIndex {
|
|
|
235
244
|
});
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
|
-
const
|
|
247
|
+
const interaction = interactionFactory?.(source);
|
|
239
248
|
const shortcut = this.#matchShortcut(input);
|
|
240
249
|
if (shortcut) {
|
|
250
|
+
if (!commandAdapterMatches(shortcut.record, source)) {
|
|
251
|
+
return Object.freeze({ matched: false });
|
|
252
|
+
}
|
|
241
253
|
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
242
254
|
return Object.freeze({ matched: false });
|
|
243
255
|
}
|
|
@@ -249,7 +261,8 @@ export class CommandIndex {
|
|
|
249
261
|
resolveDynamicParams(shortcut.params, source),
|
|
250
262
|
source,
|
|
251
263
|
Object.freeze([]),
|
|
252
|
-
|
|
264
|
+
interaction,
|
|
265
|
+
shortcut.record.slot.definition.adapter,
|
|
253
266
|
),
|
|
254
267
|
);
|
|
255
268
|
return Object.freeze({
|
|
@@ -262,6 +275,9 @@ export class CommandIndex {
|
|
|
262
275
|
|
|
263
276
|
const match = this.#match(input, false);
|
|
264
277
|
if (!match) return Object.freeze({ matched: false });
|
|
278
|
+
if (!commandAdapterMatches(match.command, source)) {
|
|
279
|
+
return Object.freeze({ matched: false });
|
|
280
|
+
}
|
|
265
281
|
if (!(await this.#permitAllows(match.command, source))) {
|
|
266
282
|
return Object.freeze({ matched: false });
|
|
267
283
|
}
|
|
@@ -274,7 +290,8 @@ export class CommandIndex {
|
|
|
274
290
|
resolveDynamicParams(match.params, source),
|
|
275
291
|
source,
|
|
276
292
|
match.remaining,
|
|
277
|
-
|
|
293
|
+
interaction,
|
|
294
|
+
match.command.slot.definition.adapter,
|
|
278
295
|
),
|
|
279
296
|
);
|
|
280
297
|
return Object.freeze({
|
|
@@ -748,6 +765,10 @@ function toDescriptor({
|
|
|
748
765
|
return descriptor;
|
|
749
766
|
}
|
|
750
767
|
|
|
768
|
+
function commandAdapterMatches(record: CommandRecord, source: unknown): boolean {
|
|
769
|
+
return !record.adapter || operationClientAdapter(source) === record.adapter;
|
|
770
|
+
}
|
|
771
|
+
|
|
751
772
|
export class CommandParameterValueError extends TypeError {
|
|
752
773
|
constructor(name: string, type: CommandParameterType, value: string) {
|
|
753
774
|
super(`Invalid value for Command parameter ${name}:${type}: ${value}`);
|
package/src/definition.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command authoring API consumed from `zhin.js/command`.
|
|
3
|
+
* @module zhin.js/command
|
|
4
|
+
*/
|
|
1
5
|
import type { PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
6
|
import {
|
|
3
7
|
createCapabilityContext,
|
|
8
|
+
readOperationClient,
|
|
9
|
+
type AdapterClient,
|
|
10
|
+
type RegisteredAdapterName,
|
|
4
11
|
type CapabilityContext,
|
|
5
12
|
} from '@zhin.js/feature-kit';
|
|
6
13
|
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
14
|
+
import type { UserInteraction } from '@zhin.js/interaction';
|
|
7
15
|
|
|
8
16
|
const commandBrand = 'zhin.command/1' as const;
|
|
9
17
|
|
|
@@ -53,6 +61,7 @@ export type CommandDynamicValue =
|
|
|
53
61
|
| CommandParameterValue
|
|
54
62
|
| ((session: CommandSession) => CommandParameterValue);
|
|
55
63
|
|
|
64
|
+
/** @internal Runtime validator lookup. */
|
|
56
65
|
export const commandParameterTypes: ReadonlySet<CommandParameterType> = new Set([
|
|
57
66
|
'string',
|
|
58
67
|
'number',
|
|
@@ -147,6 +156,8 @@ export interface CommandMessage {
|
|
|
147
156
|
readonly sender?: { readonly id: string; readonly name?: string; readonly roles?: readonly string[] };
|
|
148
157
|
readonly id?: string;
|
|
149
158
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
159
|
+
readonly clientAdapter?: string;
|
|
160
|
+
readonly $client?: unknown;
|
|
150
161
|
/** 若上游已结构化,优先采用。 */
|
|
151
162
|
readonly scene?: CommandScene;
|
|
152
163
|
// 方法式声明(而非属性式函数类型):方法参数双变,runtime `Message` 的
|
|
@@ -176,7 +187,7 @@ export interface CommandMessage {
|
|
|
176
187
|
|
|
177
188
|
/**
|
|
178
189
|
* IM 入站快捷字段。
|
|
179
|
-
* 有 `CommandMessage` 来源时由
|
|
190
|
+
* 有 `CommandMessage` 来源时由 Runtime 从消息结构填充;
|
|
180
191
|
* `CommandIndex.execute(name)` 等无消息路径下为 `undefined`。
|
|
181
192
|
*/
|
|
182
193
|
export interface CommandSession {
|
|
@@ -193,54 +204,25 @@ export interface CommandSession {
|
|
|
193
204
|
readonly sender?: CommandSender;
|
|
194
205
|
}
|
|
195
206
|
|
|
196
|
-
export interface CommandPromptOptions {
|
|
197
|
-
readonly timeout?: number;
|
|
198
|
-
readonly timeoutText?: string;
|
|
199
|
-
/** Cancel the pending claim when the owning turn aborts. */
|
|
200
|
-
readonly signal?: AbortSignal;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export interface CommandPromptListOptions extends CommandPromptOptions {
|
|
204
|
-
readonly type?: 'text' | 'number' | 'boolean';
|
|
205
|
-
readonly separator?: string;
|
|
206
|
-
readonly default?: readonly (string | number | boolean)[];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export interface CommandPromptPickOptions<V = unknown> extends CommandPromptOptions {
|
|
210
|
-
readonly options: readonly { readonly label: string; readonly value: V }[];
|
|
211
|
-
readonly multiple?: boolean;
|
|
212
|
-
readonly separator?: string;
|
|
213
|
-
readonly default?: V | readonly V[];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
207
|
/**
|
|
217
208
|
* 命令内对话式交互输入。
|
|
218
209
|
*
|
|
219
|
-
* IM 派发时自动注入(`context.
|
|
210
|
+
* IM 派发时自动注入(`context.interaction`);Host / CLI 无消息来源时为 `undefined`。
|
|
220
211
|
*
|
|
221
212
|
* ```ts
|
|
222
213
|
* defineCommand({
|
|
223
214
|
* execute: async (context) => {
|
|
224
|
-
* const name = await context.
|
|
225
|
-
* const age = await context.
|
|
215
|
+
* const name = await context.interaction!.ask({ type: 'text', title: '请输入你的名字' });
|
|
216
|
+
* const age = await context.interaction!.ask({ type: 'number', title: '请输入你的年龄' });
|
|
226
217
|
* return `你好 ${name},你 ${age} 岁了`;
|
|
227
218
|
* },
|
|
228
219
|
* });
|
|
229
220
|
* ```
|
|
230
221
|
*/
|
|
231
|
-
export interface CommandPrompt {
|
|
232
|
-
text(tips: string, options?: CommandPromptOptions & { readonly default?: string }): Promise<string>;
|
|
233
|
-
number(tips: string, options?: CommandPromptOptions & { readonly default?: number }): Promise<number>;
|
|
234
|
-
confirm(tips: string, options?: CommandPromptOptions & { readonly condition?: string; readonly default?: boolean }): Promise<boolean>;
|
|
235
|
-
list(tips: string, options?: CommandPromptListOptions): Promise<readonly (string | number | boolean)[]>;
|
|
236
|
-
pick<V = unknown>(tips: string, options: CommandPromptPickOptions<V>): Promise<V | readonly V[]>;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export type CommandPromptFactory = (source: unknown) => CommandPrompt | undefined;
|
|
240
|
-
|
|
241
222
|
export interface CommandContext<
|
|
242
223
|
TConfig = unknown,
|
|
243
224
|
TInput extends CommandMessage = CommandMessage,
|
|
225
|
+
TAdapter extends string | undefined = undefined,
|
|
244
226
|
> extends CapabilityContext<TConfig>, CommandSession {
|
|
245
227
|
readonly args: readonly string[];
|
|
246
228
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
@@ -251,20 +233,28 @@ export interface CommandContext<
|
|
|
251
233
|
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
252
234
|
*/
|
|
253
235
|
readonly input?: TInput;
|
|
236
|
+
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
237
|
+
readonly $client: AdapterClient<TAdapter>;
|
|
254
238
|
/**
|
|
255
239
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
256
240
|
*/
|
|
257
|
-
|
|
241
|
+
/** Canonical user-facing input/confirmation/selection module. */
|
|
242
|
+
readonly interaction?: UserInteraction;
|
|
258
243
|
}
|
|
259
244
|
|
|
260
245
|
export interface CommandDefinition<
|
|
261
246
|
TConfig = unknown,
|
|
262
247
|
TResult = unknown,
|
|
263
248
|
TInput extends CommandMessage = CommandMessage,
|
|
249
|
+
TAdapter extends string | undefined = string | undefined,
|
|
264
250
|
> {
|
|
251
|
+
/** @internal Runtime feature brand. */
|
|
265
252
|
readonly $feature: typeof commandBrand;
|
|
253
|
+
/** @internal Convention-derived parameter metadata. */
|
|
266
254
|
readonly $parameter?: CommandParameterDefinition;
|
|
267
255
|
readonly description?: string;
|
|
256
|
+
/** Restrict this command to one adapter and infer `context.$client`. */
|
|
257
|
+
readonly adapter?: TAdapter;
|
|
268
258
|
/**
|
|
269
259
|
* Next.js 风格参数声明:动态段文件名(`[name]` 等)的形态配合这里的
|
|
270
260
|
* 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
|
|
@@ -285,14 +275,18 @@ export interface CommandDefinition<
|
|
|
285
275
|
* 可打破 owner 命名空间。
|
|
286
276
|
*/
|
|
287
277
|
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
|
|
288
|
-
execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
|
|
278
|
+
execute(context: CommandContext<TConfig, TInput, TAdapter>): TResult | Promise<TResult>;
|
|
289
279
|
}
|
|
290
280
|
|
|
291
281
|
declare module '@zhin.js/plugin-runtime' {
|
|
292
|
-
interface PluginSetupContext<TConfig> {
|
|
293
|
-
addCommand<
|
|
282
|
+
interface PluginSetupContext<TConfig = unknown> {
|
|
283
|
+
addCommand<
|
|
284
|
+
TResult = unknown,
|
|
285
|
+
TInput extends CommandMessage = CommandMessage,
|
|
286
|
+
TAdapter extends string | undefined = undefined,
|
|
287
|
+
>(
|
|
294
288
|
localName: string,
|
|
295
|
-
definition: CommandDefinition<TConfig, TResult, TInput>,
|
|
289
|
+
definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
296
290
|
): void;
|
|
297
291
|
}
|
|
298
292
|
}
|
|
@@ -301,16 +295,30 @@ declare module '@zhin.js/plugin-runtime' {
|
|
|
301
295
|
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
302
296
|
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
303
297
|
*/
|
|
298
|
+
type CommandAuthoringDefinition<
|
|
299
|
+
TConfig,
|
|
300
|
+
TResult,
|
|
301
|
+
TInput extends CommandMessage,
|
|
302
|
+
> =
|
|
303
|
+
| Omit<CommandDefinition<TConfig, TResult, TInput, undefined>, '$feature' | '$parameter'>
|
|
304
|
+
| {
|
|
305
|
+
[TAdapter in RegisteredAdapterName]: Omit<
|
|
306
|
+
CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
307
|
+
'$feature' | '$parameter'
|
|
308
|
+
> & { readonly adapter: TAdapter }
|
|
309
|
+
}[RegisteredAdapterName];
|
|
310
|
+
|
|
304
311
|
export function defineCommand<
|
|
305
312
|
TConfig = unknown,
|
|
306
313
|
TResult = unknown,
|
|
307
314
|
TInput extends CommandMessage = CommandMessage,
|
|
308
315
|
>(
|
|
309
|
-
definition:
|
|
310
|
-
): Readonly<CommandDefinition<TConfig, TResult, TInput>> {
|
|
316
|
+
definition: CommandAuthoringDefinition<TConfig, TResult, TInput>,
|
|
317
|
+
): Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>> {
|
|
311
318
|
if (typeof definition.execute !== 'function') {
|
|
312
319
|
throw new TypeError('Command execute must be a function');
|
|
313
320
|
}
|
|
321
|
+
validateAdapterName(definition.adapter);
|
|
314
322
|
if (definition.params !== undefined) {
|
|
315
323
|
if (!definition.params || typeof definition.params !== 'object') {
|
|
316
324
|
throw new TypeError('Command params must be a Record<string, CommandParamSchema>');
|
|
@@ -325,7 +333,15 @@ export function defineCommand<
|
|
|
325
333
|
validateCommandAlias(definition.alias);
|
|
326
334
|
validateCommandPermit(definition.permit);
|
|
327
335
|
validateCommandShortcutShape(definition.shortcut);
|
|
328
|
-
return Object.freeze({ $feature: commandBrand, ...definition })
|
|
336
|
+
return Object.freeze({ $feature: commandBrand, ...definition }) as Readonly<
|
|
337
|
+
CommandDefinition<TConfig, TResult, TInput, string | undefined>
|
|
338
|
+
>;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function validateAdapterName(adapter: string | undefined): void {
|
|
342
|
+
if (adapter !== undefined && (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
343
|
+
throw new TypeError('Command adapter must be a non-empty string');
|
|
344
|
+
}
|
|
329
345
|
}
|
|
330
346
|
|
|
331
347
|
function validateCommandAlias(alias: readonly string[] | undefined): void {
|
|
@@ -374,18 +390,21 @@ function validateCommandShortcutShape(
|
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
|
|
393
|
+
/** @internal Convention-loader assembly helper. */
|
|
377
394
|
export function bindCommandParameter<
|
|
378
395
|
TConfig,
|
|
379
396
|
TResult,
|
|
380
397
|
TInput extends CommandMessage,
|
|
398
|
+
TAdapter extends string | undefined,
|
|
381
399
|
>(
|
|
382
|
-
definition: CommandDefinition<TConfig, TResult, TInput>,
|
|
400
|
+
definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
383
401
|
parameter: CommandParameterDefinition | undefined,
|
|
384
|
-
): Readonly<CommandDefinition<TConfig, TResult, TInput>> {
|
|
402
|
+
): Readonly<CommandDefinition<TConfig, TResult, TInput, TAdapter>> {
|
|
385
403
|
if (!parameter) return definition;
|
|
386
404
|
return Object.freeze({ ...definition, $parameter: Object.freeze({ ...parameter }) });
|
|
387
405
|
}
|
|
388
406
|
|
|
407
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
389
408
|
export function parseCommandDefinition(value: unknown): CommandDefinition {
|
|
390
409
|
if (!value || typeof value !== 'object') {
|
|
391
410
|
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
@@ -394,12 +413,14 @@ export function parseCommandDefinition(value: unknown): CommandDefinition {
|
|
|
394
413
|
if (definition.$feature !== commandBrand || typeof definition.execute !== 'function') {
|
|
395
414
|
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
396
415
|
}
|
|
416
|
+
validateAdapterName(definition.adapter);
|
|
397
417
|
return definition as CommandDefinition;
|
|
398
418
|
}
|
|
399
419
|
|
|
400
420
|
/**
|
|
401
421
|
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
402
422
|
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
423
|
+
* @internal Command projection helper.
|
|
403
424
|
*/
|
|
404
425
|
export function resolveDynamicParams(
|
|
405
426
|
params: Readonly<Record<string, CommandDynamicValue>>,
|
|
@@ -413,6 +434,7 @@ export function resolveDynamicParams(
|
|
|
413
434
|
return Object.freeze(resolved);
|
|
414
435
|
}
|
|
415
436
|
|
|
437
|
+
/** @internal Command dispatcher assembly helper. */
|
|
416
438
|
export function createCommandContext(
|
|
417
439
|
snapshot: RuntimeSnapshot,
|
|
418
440
|
ownerId: PluginId,
|
|
@@ -420,24 +442,31 @@ export function createCommandContext(
|
|
|
420
442
|
params: Readonly<Record<string, CommandParameterValue>> = Object.freeze({}),
|
|
421
443
|
input: unknown = undefined,
|
|
422
444
|
segments: readonly Readonly<CommandSegment>[] = Object.freeze([]),
|
|
423
|
-
|
|
445
|
+
interaction?: UserInteraction,
|
|
446
|
+
adapter?: string,
|
|
424
447
|
): CommandContext {
|
|
425
448
|
const context = createCapabilityContext(snapshot, ownerId);
|
|
426
449
|
const session = resolveCommandSession(input);
|
|
427
|
-
|
|
450
|
+
const result = {
|
|
428
451
|
...context,
|
|
429
452
|
...session,
|
|
430
453
|
args: Object.freeze([...args]),
|
|
431
454
|
params: Object.freeze({ ...params }),
|
|
432
455
|
segments: freezeSegments(segments),
|
|
433
456
|
...(input !== undefined ? { input: input as CommandMessage } : {}),
|
|
434
|
-
...(
|
|
457
|
+
...(interaction !== undefined ? { interaction } : {}),
|
|
458
|
+
} as CommandContext;
|
|
459
|
+
Object.defineProperty(result, '$client', {
|
|
460
|
+
enumerable: true,
|
|
461
|
+
get: () => readOperationClient(input, adapter),
|
|
435
462
|
});
|
|
463
|
+
return Object.freeze(result);
|
|
436
464
|
}
|
|
437
465
|
|
|
438
466
|
/**
|
|
439
467
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
440
468
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
|
469
|
+
* @internal Command dispatcher projection helper.
|
|
441
470
|
*/
|
|
442
471
|
export function resolveCommandSession(input: unknown): CommandSession {
|
|
443
472
|
if (!isCommandMessageLike(input)) return Object.freeze({});
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command authoring contracts, parameter schemas, and permit helpers.
|
|
3
|
+
* @module @zhin.js/command
|
|
4
|
+
*/
|
|
5
|
+
/** @internal Runtime command projection. */
|
|
1
6
|
export * from './command-index.js';
|
|
7
|
+
/** @public Command authoring contract and `defineCommand`. */
|
|
2
8
|
export * from './definition.js';
|
|
3
9
|
export {
|
|
4
10
|
assertBuiltinPermits,
|