@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 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
 
@@ -1,11 +1,13 @@
1
1
  import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
- import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment, type CommandPromptFactory } from './definition.js';
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, promptFactory?: CommandPromptFactory, commandPrefix?: string): Promise<CommandDispatchResult>;
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 {
@@ -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, promptFactory, commandPrefix = '') {
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 prompt = promptFactory?.(source);
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([]), prompt));
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, prompt));
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}`);
@@ -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` 来源时由 {@link resolveCommandSession} 填充;
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.prompt`);Host / CLI 无消息来源时为 `undefined`。
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.prompt!.text('请输入你的名字');
175
- * const age = await context.prompt!.number('请输入你的年龄');
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 CommandPrompt {
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
- readonly prompt?: CommandPrompt;
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
- export declare function defineCommand<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage>(definition: Omit<CommandDefinition<TConfig, TResult, TInput>, '$feature' | '$parameter'>): Readonly<CommandDefinition<TConfig, TResult, TInput>>;
247
- export declare function bindCommandParameter<TConfig, TResult, TInput extends CommandMessage>(definition: CommandDefinition<TConfig, TResult, TInput>, parameter: CommandParameterDefinition | undefined): Readonly<CommandDefinition<TConfig, TResult, TInput>>;
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
- export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[], prompt?: CommandPrompt): CommandContext;
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
- export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), prompt) {
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
- return Object.freeze({
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
- ...(prompt !== undefined ? { prompt } : {}),
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.14",
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.11",
22
- "@zhin.js/permission": "1.0.2",
23
- "@zhin.js/plugin-runtime": "1.1.6"
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",
@@ -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
- promptFactory?: CommandPromptFactory,
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 prompt = promptFactory?.(source);
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
- prompt,
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
- prompt,
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` 来源时由 {@link resolveCommandSession} 填充;
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.prompt`);Host / CLI 无消息来源时为 `undefined`。
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.prompt!.text('请输入你的名字');
225
- * const age = await context.prompt!.number('请输入你的年龄');
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
- readonly prompt?: CommandPrompt;
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<TResult = unknown, TInput extends CommandMessage = CommandMessage>(
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: Omit<CommandDefinition<TConfig, TResult, TInput>, '$feature' | '$parameter'>,
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
- prompt?: CommandPrompt,
445
+ interaction?: UserInteraction,
446
+ adapter?: string,
424
447
  ): CommandContext {
425
448
  const context = createCapabilityContext(snapshot, ownerId);
426
449
  const session = resolveCommandSession(input);
427
- return Object.freeze({
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
- ...(prompt !== undefined ? { prompt } : {}),
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,