@zhin.js/command 1.0.15 → 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/lib/command-index.d.ts +1 -0
- package/lib/command-index.js +14 -3
- package/lib/definition.d.ts +32 -9
- package/lib/definition.js +21 -7
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/provider.d.ts +1 -1
- package/package.json +4 -4
- package/src/command-index.ts +19 -0
- package/src/definition.ts +67 -10
- package/src/index.ts +6 -0
package/lib/command-index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
|
7
7
|
export interface CommandDescriptor {
|
|
8
8
|
readonly name: string;
|
|
9
9
|
readonly description?: string;
|
|
10
|
+
readonly adapter?: string;
|
|
10
11
|
readonly source: string;
|
|
11
12
|
readonly parameters: readonly CommandParameterDescriptor[];
|
|
12
13
|
readonly alias?: readonly string[];
|
package/lib/command-index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SegmentMatcher, TypeMatcherRegistry, } from 'segment-matcher';
|
|
2
|
+
import { operationClientAdapter } from '@zhin.js/feature-kit';
|
|
2
3
|
import { permissionHostToken, toPermissionSubject, } from '@zhin.js/permission';
|
|
3
4
|
import { createCommandContext, resolveCommandSession, resolveDynamicParams, } from './definition.js';
|
|
4
5
|
const segmentFields = {
|
|
@@ -48,6 +49,7 @@ export class CommandIndex {
|
|
|
48
49
|
const record = Object.freeze({
|
|
49
50
|
name,
|
|
50
51
|
description: slot.definition.description,
|
|
52
|
+
...(slot.definition.adapter ? { adapter: slot.definition.adapter } : {}),
|
|
51
53
|
source: slot.source,
|
|
52
54
|
parameters: Object.freeze(parameter ? [{
|
|
53
55
|
...parameter,
|
|
@@ -116,7 +118,7 @@ export class CommandIndex {
|
|
|
116
118
|
throw new Error(`Unknown Command: ${name}`);
|
|
117
119
|
}
|
|
118
120
|
// Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session。
|
|
119
|
-
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));
|
|
120
122
|
}
|
|
121
123
|
async dispatch(input, source = undefined, interactionFactory, commandPrefix = '') {
|
|
122
124
|
if (this.#menu) {
|
|
@@ -133,10 +135,13 @@ export class CommandIndex {
|
|
|
133
135
|
const interaction = interactionFactory?.(source);
|
|
134
136
|
const shortcut = this.#matchShortcut(input);
|
|
135
137
|
if (shortcut) {
|
|
138
|
+
if (!commandAdapterMatches(shortcut.record, source)) {
|
|
139
|
+
return Object.freeze({ matched: false });
|
|
140
|
+
}
|
|
136
141
|
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
137
142
|
return Object.freeze({ matched: false });
|
|
138
143
|
}
|
|
139
|
-
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));
|
|
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));
|
|
140
145
|
return Object.freeze({
|
|
141
146
|
matched: true,
|
|
142
147
|
command: shortcut.record.name,
|
|
@@ -147,11 +152,14 @@ export class CommandIndex {
|
|
|
147
152
|
const match = this.#match(input, false);
|
|
148
153
|
if (!match)
|
|
149
154
|
return Object.freeze({ matched: false });
|
|
155
|
+
if (!commandAdapterMatches(match.command, source)) {
|
|
156
|
+
return Object.freeze({ matched: false });
|
|
157
|
+
}
|
|
150
158
|
if (!(await this.#permitAllows(match.command, source))) {
|
|
151
159
|
return Object.freeze({ matched: false });
|
|
152
160
|
}
|
|
153
161
|
const args = textArgs(match.remaining);
|
|
154
|
-
const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining, interaction));
|
|
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));
|
|
155
163
|
return Object.freeze({
|
|
156
164
|
matched: true,
|
|
157
165
|
command: match.command.name,
|
|
@@ -564,6 +572,9 @@ function textArgs(segments) {
|
|
|
564
572
|
function toDescriptor({ slot: _slot, segments: _segments, parameter: _parameter, ...descriptor }) {
|
|
565
573
|
return descriptor;
|
|
566
574
|
}
|
|
575
|
+
function commandAdapterMatches(record, source) {
|
|
576
|
+
return !record.adapter || operationClientAdapter(source) === record.adapter;
|
|
577
|
+
}
|
|
567
578
|
export class CommandParameterValueError extends TypeError {
|
|
568
579
|
constructor(name, type, value) {
|
|
569
580
|
super(`Invalid value for Command parameter ${name}:${type}: ${value}`);
|
package/lib/definition.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
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';
|
|
3
7
|
import type { UserInteraction } from '@zhin.js/interaction';
|
|
4
8
|
declare const commandBrand: "zhin.command/1";
|
|
5
9
|
export type CommandParameterType = 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'word' | 'text' | 'mention' | 'image' | 'face' | 'reply' | 'forward' | 'dice' | 'rps';
|
|
@@ -24,6 +28,7 @@ export type CommandParameterValue = string | number | boolean | ReadonlyArray<st
|
|
|
24
28
|
* ```
|
|
25
29
|
*/
|
|
26
30
|
export type CommandDynamicValue = CommandParameterValue | ((session: CommandSession) => CommandParameterValue);
|
|
31
|
+
/** @internal Runtime validator lookup. */
|
|
27
32
|
export declare const commandParameterTypes: ReadonlySet<CommandParameterType>;
|
|
28
33
|
/**
|
|
29
34
|
* Next.js 风格参数声明(`defineCommand({ params: ... })`)。
|
|
@@ -102,6 +107,8 @@ export interface CommandMessage {
|
|
|
102
107
|
};
|
|
103
108
|
readonly id?: string;
|
|
104
109
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
110
|
+
readonly clientAdapter?: string;
|
|
111
|
+
readonly $client?: unknown;
|
|
105
112
|
/** 若上游已结构化,优先采用。 */
|
|
106
113
|
readonly scene?: CommandScene;
|
|
107
114
|
$reply?(content: unknown): Promise<unknown>;
|
|
@@ -128,7 +135,7 @@ export interface CommandMessage {
|
|
|
128
135
|
}
|
|
129
136
|
/**
|
|
130
137
|
* IM 入站快捷字段。
|
|
131
|
-
* 有 `CommandMessage` 来源时由
|
|
138
|
+
* 有 `CommandMessage` 来源时由 Runtime 从消息结构填充;
|
|
132
139
|
* `CommandIndex.execute(name)` 等无消息路径下为 `undefined`。
|
|
133
140
|
*/
|
|
134
141
|
export interface CommandSession {
|
|
@@ -159,7 +166,7 @@ export interface CommandSession {
|
|
|
159
166
|
* });
|
|
160
167
|
* ```
|
|
161
168
|
*/
|
|
162
|
-
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 {
|
|
163
170
|
readonly args: readonly string[];
|
|
164
171
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
165
172
|
/** Structured arguments left after the command pattern was consumed. */
|
|
@@ -169,16 +176,22 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
|
|
|
169
176
|
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
170
177
|
*/
|
|
171
178
|
readonly input?: TInput;
|
|
179
|
+
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
180
|
+
readonly $client: AdapterClient<TAdapter>;
|
|
172
181
|
/**
|
|
173
182
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
174
183
|
*/
|
|
175
184
|
/** Canonical user-facing input/confirmation/selection module. */
|
|
176
185
|
readonly interaction?: UserInteraction;
|
|
177
186
|
}
|
|
178
|
-
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. */
|
|
179
189
|
readonly $feature: typeof commandBrand;
|
|
190
|
+
/** @internal Convention-derived parameter metadata. */
|
|
180
191
|
readonly $parameter?: CommandParameterDefinition;
|
|
181
192
|
readonly description?: string;
|
|
193
|
+
/** Restrict this command to one adapter and infer `context.$client`. */
|
|
194
|
+
readonly adapter?: TAdapter;
|
|
182
195
|
/**
|
|
183
196
|
* Next.js 风格参数声明:动态段文件名(`[name]` 等)的形态配合这里的
|
|
184
197
|
* 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
|
|
@@ -199,29 +212,39 @@ export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput
|
|
|
199
212
|
* 可打破 owner 命名空间。
|
|
200
213
|
*/
|
|
201
214
|
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
|
|
202
|
-
execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
|
|
215
|
+
execute(context: CommandContext<TConfig, TInput, TAdapter>): TResult | Promise<TResult>;
|
|
203
216
|
}
|
|
204
217
|
declare module '@zhin.js/plugin-runtime' {
|
|
205
218
|
interface PluginSetupContext<TConfig = unknown> {
|
|
206
|
-
addCommand<TResult = unknown, TInput extends CommandMessage = CommandMessage>(localName: string, definition: CommandDefinition<TConfig, TResult, TInput>): void;
|
|
219
|
+
addCommand<TResult = unknown, TInput extends CommandMessage = CommandMessage, TAdapter extends string | undefined = undefined>(localName: string, definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>): void;
|
|
207
220
|
}
|
|
208
221
|
}
|
|
209
222
|
/**
|
|
210
223
|
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
211
224
|
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
212
225
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
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. */
|
|
215
235
|
export declare function parseCommandDefinition(value: unknown): CommandDefinition;
|
|
216
236
|
/**
|
|
217
237
|
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
218
238
|
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
239
|
+
* @internal Command projection helper.
|
|
219
240
|
*/
|
|
220
241
|
export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
|
|
221
|
-
|
|
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;
|
|
222
244
|
/**
|
|
223
245
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
224
246
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
|
247
|
+
* @internal Command dispatcher projection helper.
|
|
225
248
|
*/
|
|
226
249
|
export declare function resolveCommandSession(input: unknown): CommandSession;
|
|
227
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,10 +119,11 @@ 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]),
|
|
@@ -123,11 +131,17 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
|
|
|
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,10 +18,10 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"segment-matcher": "^1.0.5",
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.13",
|
|
21
22
|
"@zhin.js/interaction": "1.0.1",
|
|
22
|
-
"@zhin.js/permission": "1.0.
|
|
23
|
-
"@zhin.js/
|
|
24
|
-
"@zhin.js/plugin-runtime": "1.1.7"
|
|
23
|
+
"@zhin.js/permission": "1.0.4",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.8"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|
package/src/command-index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
RuntimeSnapshot,
|
|
10
10
|
} from '@zhin.js/plugin-runtime';
|
|
11
11
|
import type { UserInteractionFactory } from '@zhin.js/interaction';
|
|
12
|
+
import { operationClientAdapter } from '@zhin.js/feature-kit';
|
|
12
13
|
import {
|
|
13
14
|
permissionHostToken,
|
|
14
15
|
toPermissionSubject,
|
|
@@ -32,6 +33,7 @@ export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
|
32
33
|
export interface CommandDescriptor {
|
|
33
34
|
readonly name: string;
|
|
34
35
|
readonly description?: string;
|
|
36
|
+
readonly adapter?: string;
|
|
35
37
|
readonly source: string;
|
|
36
38
|
readonly parameters: readonly CommandParameterDescriptor[];
|
|
37
39
|
readonly alias?: readonly string[];
|
|
@@ -132,6 +134,7 @@ export class CommandIndex {
|
|
|
132
134
|
const record: CommandRecord = Object.freeze({
|
|
133
135
|
name,
|
|
134
136
|
description: slot.definition.description,
|
|
137
|
+
...(slot.definition.adapter ? { adapter: slot.definition.adapter } : {}),
|
|
135
138
|
source: slot.source,
|
|
136
139
|
parameters: Object.freeze(parameter ? [{
|
|
137
140
|
...parameter,
|
|
@@ -216,6 +219,10 @@ export class CommandIndex {
|
|
|
216
219
|
match.command.slot.owner,
|
|
217
220
|
args,
|
|
218
221
|
resolveDynamicParams(match.params, undefined),
|
|
222
|
+
undefined,
|
|
223
|
+
Object.freeze([]),
|
|
224
|
+
undefined,
|
|
225
|
+
match.command.slot.definition.adapter,
|
|
219
226
|
),
|
|
220
227
|
);
|
|
221
228
|
}
|
|
@@ -240,6 +247,9 @@ export class CommandIndex {
|
|
|
240
247
|
const interaction = interactionFactory?.(source);
|
|
241
248
|
const shortcut = this.#matchShortcut(input);
|
|
242
249
|
if (shortcut) {
|
|
250
|
+
if (!commandAdapterMatches(shortcut.record, source)) {
|
|
251
|
+
return Object.freeze({ matched: false });
|
|
252
|
+
}
|
|
243
253
|
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
244
254
|
return Object.freeze({ matched: false });
|
|
245
255
|
}
|
|
@@ -252,6 +262,7 @@ export class CommandIndex {
|
|
|
252
262
|
source,
|
|
253
263
|
Object.freeze([]),
|
|
254
264
|
interaction,
|
|
265
|
+
shortcut.record.slot.definition.adapter,
|
|
255
266
|
),
|
|
256
267
|
);
|
|
257
268
|
return Object.freeze({
|
|
@@ -264,6 +275,9 @@ export class CommandIndex {
|
|
|
264
275
|
|
|
265
276
|
const match = this.#match(input, false);
|
|
266
277
|
if (!match) return Object.freeze({ matched: false });
|
|
278
|
+
if (!commandAdapterMatches(match.command, source)) {
|
|
279
|
+
return Object.freeze({ matched: false });
|
|
280
|
+
}
|
|
267
281
|
if (!(await this.#permitAllows(match.command, source))) {
|
|
268
282
|
return Object.freeze({ matched: false });
|
|
269
283
|
}
|
|
@@ -277,6 +291,7 @@ export class CommandIndex {
|
|
|
277
291
|
source,
|
|
278
292
|
match.remaining,
|
|
279
293
|
interaction,
|
|
294
|
+
match.command.slot.definition.adapter,
|
|
280
295
|
),
|
|
281
296
|
);
|
|
282
297
|
return Object.freeze({
|
|
@@ -750,6 +765,10 @@ function toDescriptor({
|
|
|
750
765
|
return descriptor;
|
|
751
766
|
}
|
|
752
767
|
|
|
768
|
+
function commandAdapterMatches(record: CommandRecord, source: unknown): boolean {
|
|
769
|
+
return !record.adapter || operationClientAdapter(source) === record.adapter;
|
|
770
|
+
}
|
|
771
|
+
|
|
753
772
|
export class CommandParameterValueError extends TypeError {
|
|
754
773
|
constructor(name: string, type: CommandParameterType, value: string) {
|
|
755
774
|
super(`Invalid value for Command parameter ${name}:${type}: ${value}`);
|
package/src/definition.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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';
|
|
@@ -54,6 +61,7 @@ export type CommandDynamicValue =
|
|
|
54
61
|
| CommandParameterValue
|
|
55
62
|
| ((session: CommandSession) => CommandParameterValue);
|
|
56
63
|
|
|
64
|
+
/** @internal Runtime validator lookup. */
|
|
57
65
|
export const commandParameterTypes: ReadonlySet<CommandParameterType> = new Set([
|
|
58
66
|
'string',
|
|
59
67
|
'number',
|
|
@@ -148,6 +156,8 @@ export interface CommandMessage {
|
|
|
148
156
|
readonly sender?: { readonly id: string; readonly name?: string; readonly roles?: readonly string[] };
|
|
149
157
|
readonly id?: string;
|
|
150
158
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
159
|
+
readonly clientAdapter?: string;
|
|
160
|
+
readonly $client?: unknown;
|
|
151
161
|
/** 若上游已结构化,优先采用。 */
|
|
152
162
|
readonly scene?: CommandScene;
|
|
153
163
|
// 方法式声明(而非属性式函数类型):方法参数双变,runtime `Message` 的
|
|
@@ -177,7 +187,7 @@ export interface CommandMessage {
|
|
|
177
187
|
|
|
178
188
|
/**
|
|
179
189
|
* IM 入站快捷字段。
|
|
180
|
-
* 有 `CommandMessage` 来源时由
|
|
190
|
+
* 有 `CommandMessage` 来源时由 Runtime 从消息结构填充;
|
|
181
191
|
* `CommandIndex.execute(name)` 等无消息路径下为 `undefined`。
|
|
182
192
|
*/
|
|
183
193
|
export interface CommandSession {
|
|
@@ -212,6 +222,7 @@ export interface CommandSession {
|
|
|
212
222
|
export interface CommandContext<
|
|
213
223
|
TConfig = unknown,
|
|
214
224
|
TInput extends CommandMessage = CommandMessage,
|
|
225
|
+
TAdapter extends string | undefined = undefined,
|
|
215
226
|
> extends CapabilityContext<TConfig>, CommandSession {
|
|
216
227
|
readonly args: readonly string[];
|
|
217
228
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
@@ -222,6 +233,8 @@ export interface CommandContext<
|
|
|
222
233
|
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
223
234
|
*/
|
|
224
235
|
readonly input?: TInput;
|
|
236
|
+
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
237
|
+
readonly $client: AdapterClient<TAdapter>;
|
|
225
238
|
/**
|
|
226
239
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
227
240
|
*/
|
|
@@ -233,10 +246,15 @@ export interface CommandDefinition<
|
|
|
233
246
|
TConfig = unknown,
|
|
234
247
|
TResult = unknown,
|
|
235
248
|
TInput extends CommandMessage = CommandMessage,
|
|
249
|
+
TAdapter extends string | undefined = string | undefined,
|
|
236
250
|
> {
|
|
251
|
+
/** @internal Runtime feature brand. */
|
|
237
252
|
readonly $feature: typeof commandBrand;
|
|
253
|
+
/** @internal Convention-derived parameter metadata. */
|
|
238
254
|
readonly $parameter?: CommandParameterDefinition;
|
|
239
255
|
readonly description?: string;
|
|
256
|
+
/** Restrict this command to one adapter and infer `context.$client`. */
|
|
257
|
+
readonly adapter?: TAdapter;
|
|
240
258
|
/**
|
|
241
259
|
* Next.js 风格参数声明:动态段文件名(`[name]` 等)的形态配合这里的
|
|
242
260
|
* 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
|
|
@@ -257,14 +275,18 @@ export interface CommandDefinition<
|
|
|
257
275
|
* 可打破 owner 命名空间。
|
|
258
276
|
*/
|
|
259
277
|
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
|
|
260
|
-
execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
|
|
278
|
+
execute(context: CommandContext<TConfig, TInput, TAdapter>): TResult | Promise<TResult>;
|
|
261
279
|
}
|
|
262
280
|
|
|
263
281
|
declare module '@zhin.js/plugin-runtime' {
|
|
264
282
|
interface PluginSetupContext<TConfig = unknown> {
|
|
265
|
-
addCommand<
|
|
283
|
+
addCommand<
|
|
284
|
+
TResult = unknown,
|
|
285
|
+
TInput extends CommandMessage = CommandMessage,
|
|
286
|
+
TAdapter extends string | undefined = undefined,
|
|
287
|
+
>(
|
|
266
288
|
localName: string,
|
|
267
|
-
definition: CommandDefinition<TConfig, TResult, TInput>,
|
|
289
|
+
definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
268
290
|
): void;
|
|
269
291
|
}
|
|
270
292
|
}
|
|
@@ -273,16 +295,30 @@ declare module '@zhin.js/plugin-runtime' {
|
|
|
273
295
|
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
274
296
|
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
275
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
|
+
|
|
276
311
|
export function defineCommand<
|
|
277
312
|
TConfig = unknown,
|
|
278
313
|
TResult = unknown,
|
|
279
314
|
TInput extends CommandMessage = CommandMessage,
|
|
280
315
|
>(
|
|
281
|
-
definition:
|
|
282
|
-
): Readonly<CommandDefinition<TConfig, TResult, TInput>> {
|
|
316
|
+
definition: CommandAuthoringDefinition<TConfig, TResult, TInput>,
|
|
317
|
+
): Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>> {
|
|
283
318
|
if (typeof definition.execute !== 'function') {
|
|
284
319
|
throw new TypeError('Command execute must be a function');
|
|
285
320
|
}
|
|
321
|
+
validateAdapterName(definition.adapter);
|
|
286
322
|
if (definition.params !== undefined) {
|
|
287
323
|
if (!definition.params || typeof definition.params !== 'object') {
|
|
288
324
|
throw new TypeError('Command params must be a Record<string, CommandParamSchema>');
|
|
@@ -297,7 +333,15 @@ export function defineCommand<
|
|
|
297
333
|
validateCommandAlias(definition.alias);
|
|
298
334
|
validateCommandPermit(definition.permit);
|
|
299
335
|
validateCommandShortcutShape(definition.shortcut);
|
|
300
|
-
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
|
+
}
|
|
301
345
|
}
|
|
302
346
|
|
|
303
347
|
function validateCommandAlias(alias: readonly string[] | undefined): void {
|
|
@@ -346,18 +390,21 @@ function validateCommandShortcutShape(
|
|
|
346
390
|
}
|
|
347
391
|
}
|
|
348
392
|
|
|
393
|
+
/** @internal Convention-loader assembly helper. */
|
|
349
394
|
export function bindCommandParameter<
|
|
350
395
|
TConfig,
|
|
351
396
|
TResult,
|
|
352
397
|
TInput extends CommandMessage,
|
|
398
|
+
TAdapter extends string | undefined,
|
|
353
399
|
>(
|
|
354
|
-
definition: CommandDefinition<TConfig, TResult, TInput>,
|
|
400
|
+
definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
355
401
|
parameter: CommandParameterDefinition | undefined,
|
|
356
|
-
): Readonly<CommandDefinition<TConfig, TResult, TInput>> {
|
|
402
|
+
): Readonly<CommandDefinition<TConfig, TResult, TInput, TAdapter>> {
|
|
357
403
|
if (!parameter) return definition;
|
|
358
404
|
return Object.freeze({ ...definition, $parameter: Object.freeze({ ...parameter }) });
|
|
359
405
|
}
|
|
360
406
|
|
|
407
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
361
408
|
export function parseCommandDefinition(value: unknown): CommandDefinition {
|
|
362
409
|
if (!value || typeof value !== 'object') {
|
|
363
410
|
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
@@ -366,12 +413,14 @@ export function parseCommandDefinition(value: unknown): CommandDefinition {
|
|
|
366
413
|
if (definition.$feature !== commandBrand || typeof definition.execute !== 'function') {
|
|
367
414
|
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
368
415
|
}
|
|
416
|
+
validateAdapterName(definition.adapter);
|
|
369
417
|
return definition as CommandDefinition;
|
|
370
418
|
}
|
|
371
419
|
|
|
372
420
|
/**
|
|
373
421
|
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
374
422
|
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
423
|
+
* @internal Command projection helper.
|
|
375
424
|
*/
|
|
376
425
|
export function resolveDynamicParams(
|
|
377
426
|
params: Readonly<Record<string, CommandDynamicValue>>,
|
|
@@ -385,6 +434,7 @@ export function resolveDynamicParams(
|
|
|
385
434
|
return Object.freeze(resolved);
|
|
386
435
|
}
|
|
387
436
|
|
|
437
|
+
/** @internal Command dispatcher assembly helper. */
|
|
388
438
|
export function createCommandContext(
|
|
389
439
|
snapshot: RuntimeSnapshot,
|
|
390
440
|
ownerId: PluginId,
|
|
@@ -393,10 +443,11 @@ export function createCommandContext(
|
|
|
393
443
|
input: unknown = undefined,
|
|
394
444
|
segments: readonly Readonly<CommandSegment>[] = Object.freeze([]),
|
|
395
445
|
interaction?: UserInteraction,
|
|
446
|
+
adapter?: string,
|
|
396
447
|
): CommandContext {
|
|
397
448
|
const context = createCapabilityContext(snapshot, ownerId);
|
|
398
449
|
const session = resolveCommandSession(input);
|
|
399
|
-
|
|
450
|
+
const result = {
|
|
400
451
|
...context,
|
|
401
452
|
...session,
|
|
402
453
|
args: Object.freeze([...args]),
|
|
@@ -404,12 +455,18 @@ export function createCommandContext(
|
|
|
404
455
|
segments: freezeSegments(segments),
|
|
405
456
|
...(input !== undefined ? { input: input as CommandMessage } : {}),
|
|
406
457
|
...(interaction !== undefined ? { interaction } : {}),
|
|
458
|
+
} as CommandContext;
|
|
459
|
+
Object.defineProperty(result, '$client', {
|
|
460
|
+
enumerable: true,
|
|
461
|
+
get: () => readOperationClient(input, adapter),
|
|
407
462
|
});
|
|
463
|
+
return Object.freeze(result);
|
|
408
464
|
}
|
|
409
465
|
|
|
410
466
|
/**
|
|
411
467
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
412
468
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
|
469
|
+
* @internal Command dispatcher projection helper.
|
|
413
470
|
*/
|
|
414
471
|
export function resolveCommandSession(input: unknown): CommandSession {
|
|
415
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,
|