@zhin.js/command 1.0.15 → 1.1.0

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.
@@ -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[];
@@ -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}`);
@@ -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` 来源时由 {@link resolveCommandSession} 填充;
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
- 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>>;
214
- 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. */
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
- export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[], interaction?: UserInteraction): 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;
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
- export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), interaction) {
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]),
@@ -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.15",
3
+ "version": "1.1.0",
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/interaction": "1.0.1",
22
- "@zhin.js/permission": "1.0.3",
23
- "@zhin.js/feature-kit": "1.0.12",
24
- "@zhin.js/plugin-runtime": "1.1.7"
21
+ "@zhin.js/feature-kit": "1.1.0",
22
+ "@zhin.js/interaction": "1.1.0",
23
+ "@zhin.js/plugin-runtime": "1.1.9",
24
+ "@zhin.js/permission": "1.1.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",
@@ -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` 来源时由 {@link resolveCommandSession} 填充;
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<TResult = unknown, TInput extends CommandMessage = CommandMessage>(
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: Omit<CommandDefinition<TConfig, TResult, TInput>, '$feature' | '$parameter'>,
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
- return Object.freeze({
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,