@zhin.js/command 1.1.11 → 1.1.13

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
@@ -117,7 +117,7 @@ export default defineCommand({
117
117
  后面的内容作为参数。未知/重复选项拒绝。`remind --help` 从同一声明生成帮助,包含
118
118
  Endpoint 前缀及显式插件命名空间,不执行命令;同一权限声明仍约束帮助读取。
119
119
 
120
- `dispatch()` 返回 `status`:`unmatched`(继续普通消息处理)、`invalid`(`issues` +
120
+ `dispatch({ input, source, snapshot })` 返回 `status`:`unmatched`(继续普通消息处理)、`invalid`(`issues` +
121
121
  `help`)、`denied`(权限拒绝)、`limited`(执行预算拒绝)、`executed`。后四者 `matched: true`,只有 `executed`
122
122
  包含业务结果。可靠静态路径匹配后才提示错误;纯动态路由的模糊聊天保持未匹配。
123
123
  受限命令没有可鉴权主体时拒绝,Host 可通过 `dispatch` 提供真实 `PermissionSubject`;
@@ -25,6 +25,15 @@ export interface CommandDescriptor {
25
25
  /** shortcut 触发键列表(不含预填 params)。 */
26
26
  readonly shortcut?: readonly string[];
27
27
  }
28
+ /** One dispatch reads the caller's exact operation generation and presentation. */
29
+ export interface CommandDispatchRequest {
30
+ readonly input: CommandMatchInput;
31
+ readonly source?: unknown;
32
+ readonly interactionFactory?: UserInteractionFactory;
33
+ readonly commandPrefix?: string;
34
+ readonly snapshot?: RuntimeSnapshot;
35
+ readonly locale?: Locale;
36
+ }
28
37
  export interface CommandDispatchResult {
29
38
  readonly matched: boolean;
30
39
  readonly status: 'unmatched' | 'invalid' | 'denied' | 'limited' | 'executed';
@@ -72,7 +81,7 @@ export declare class CommandIndex {
72
81
  formatMenu(prefix: string | undefined, locale: Locale): string | undefined;
73
82
  has(name: string): boolean;
74
83
  execute(name: string, args?: readonly string[], operationSnapshot?: RuntimeSnapshot): Promise<unknown>;
75
- dispatch(input: CommandMatchInput, source?: unknown, interactionFactory?: UserInteractionFactory, commandPrefix?: string, operationSnapshot?: RuntimeSnapshot, localeOverride?: Locale): Promise<CommandDispatchResult>;
84
+ dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot, locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult>;
76
85
  }
77
86
  export declare function isCommandIndex(value: unknown): value is CommandIndex;
78
87
  /**
@@ -186,18 +186,21 @@ export class CommandIndex {
186
186
  if (admission.limit)
187
187
  throw new CommandExecutionLimitedError(admission.limit);
188
188
  try {
189
- return await definition.execute(createCommandContext(operationSnapshot, match.command.slot.owner, textArgs(parsed.remaining), parsed.params, undefined, parsed.remaining, undefined, definition.adapter, parsed.options, { locale }));
189
+ return await definition.execute(createCommandContext({ snapshot: operationSnapshot, ownerId: match.command.slot.owner,
190
+ adapter: definition.adapter, presentation: { locale } }, {
191
+ args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options,
192
+ }));
190
193
  }
191
194
  finally {
192
195
  admission.release();
193
196
  }
194
197
  }
195
- async dispatch(input, source = undefined, interactionFactory, commandPrefix, operationSnapshot = this.snapshot, localeOverride) {
198
+ async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot, locale: localeOverride }) {
196
199
  const locale = localeOverride ?? commandLocale(source, operationSnapshot);
197
200
  const prefix = commandPrefix ?? '';
198
201
  if (this.#menu) {
199
202
  const keyword = this.#menu.keyword ?? commandMessages(locale, 'menuKeyword');
200
- const menuResult = this.#dispatchMenu(input, prefix, operationSnapshot, locale, keyword);
203
+ const menuResult = this.#dispatchMenu(input, { commandPrefix: prefix, snapshot: operationSnapshot, locale, keyword });
201
204
  if (menuResult !== undefined) {
202
205
  return Object.freeze({
203
206
  matched: true,
@@ -244,7 +247,11 @@ export class CommandIndex {
244
247
  const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
245
248
  if (limit)
246
249
  return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
247
- const value = await definition.execute(createCommandContext(operationSnapshot, match.command.slot.owner, textArgs(parsed.remaining), parsed.params, authorSource, parsed.remaining, interactionFactory?.(authorSource), definition.adapter, parsed.options, { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath, source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) }));
250
+ const value = await definition.execute(createCommandContext({
251
+ snapshot: operationSnapshot, ownerId: match.command.slot.owner, input: authorSource,
252
+ interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
253
+ presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath, source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
254
+ }, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options }));
248
255
  return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
249
256
  }
250
257
  #denied(record) {
@@ -362,12 +369,13 @@ export class CommandIndex {
362
369
  // ==========================================================================
363
370
  // 内置菜单命令
364
371
  // ==========================================================================
365
- #dispatchMenu(input, commandPrefix, snapshot, locale, keyword) {
372
+ #dispatchMenu(input, context) {
373
+ const { commandPrefix, keyword, locale } = context;
366
374
  const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
367
375
  if (text === undefined)
368
376
  return undefined;
369
377
  if (text === keyword)
370
- return this.#buildMenu(undefined, commandPrefix, keyword, snapshot, locale);
378
+ return this.#buildMenu({}, context);
371
379
  if (text.startsWith(keyword + ' ')) {
372
380
  const key = text.slice(keyword.length + 1).trim();
373
381
  const args = key.split(/\s+/u);
@@ -378,15 +386,15 @@ export class CommandIndex {
378
386
  || !Number.isSafeInteger(Number(pageText))) {
379
387
  return { value: commandMessages(locale, 'invalidPage', { command: `${commandPrefix}${keyword}` }) };
380
388
  }
381
- return this.#buildMenu(flag === 1 ? args[0] : undefined, commandPrefix, keyword, snapshot, locale, Number(pageText));
389
+ return this.#buildMenu({ pluginKey: flag === 1 ? args[0] : undefined, page: Number(pageText) }, context);
382
390
  }
383
391
  if (key)
384
- return this.#buildMenu(key, commandPrefix, keyword, snapshot, locale);
385
- return this.#buildMenu(undefined, commandPrefix, keyword, snapshot, locale);
392
+ return this.#buildMenu({ pluginKey: key }, context);
393
+ return this.#buildMenu({}, context);
386
394
  }
387
395
  return undefined;
388
396
  }
389
- #buildMenu(pluginKey, commandPrefix, keyword, snapshot, locale, page = 1) {
397
+ #buildMenu({ pluginKey, page = 1 }, { commandPrefix, keyword, snapshot, locale }) {
390
398
  const root = snapshot.root;
391
399
  const targetId = pluginKey
392
400
  ? `${root}/${pluginKey.split('.').join('/')}`
@@ -3,7 +3,7 @@
3
3
  * @module zhin.js/command
4
4
  */
5
5
  import { type Locale, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
6
- import { type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
6
+ import { type OperationSession, type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
7
7
  import type { UserInteraction } from '@zhin.js/interaction';
8
8
  import { type CommandExecutionPolicy } from './execution-policy.js';
9
9
  export type { CommandExecutionPolicy, CommandExecutionLimit } from './execution-policy.js';
@@ -233,6 +233,8 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
233
233
  readonly input?: TInput;
234
234
  /** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
235
235
  readonly $client: AdapterClient<TAdapter>;
236
+ /** Bound by the actual IM operation; direct CommandIndex.execute calls may have no Session. */
237
+ readonly session: OperationSession<TAdapter> | undefined;
236
238
  /**
237
239
  * 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
238
240
  */
@@ -300,13 +302,24 @@ type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extend
300
302
  readonly params: keyof P extends never ? Readonly<Record<string, CommandParameterValue>> : InferCommandParams<P>;
301
303
  readonly options: InferCommandOptions<O>;
302
304
  };
305
+ /** The returned declaration retains its platform, parameter schemas and callback result. */
306
+ type DeclaredCommandAdapter<D> = D extends {
307
+ readonly adapter: infer A extends string;
308
+ } ? A : D extends {
309
+ readonly adapter?: infer A extends string | undefined;
310
+ } ? A : undefined;
311
+ export type TypedCommandDefinition<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> = Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, 'params' | 'options' | 'execute'> & {
312
+ readonly params?: P;
313
+ readonly options?: O;
314
+ execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
315
+ };
303
316
  /**
304
317
  * `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
305
318
  * 仍从定义推断。
306
319
  * @public
307
320
  */
308
- export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <TResult = unknown, const P extends CommandParams = {}, const O extends CommandOptions = {}>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>) => Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>>;
309
- export declare function defineCommand<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage, const P extends CommandParams = {}, const O extends CommandOptions = {}>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>): Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>>;
321
+ export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <const TResult = unknown, const P extends CommandParams = {}, const O extends CommandOptions = {}, const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D) => Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
322
+ export declare function defineCommand<TConfig = unknown, const TResult = unknown, TInput extends CommandMessage = CommandMessage, const P extends CommandParams = {}, const O extends CommandOptions = {}, const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D): Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
310
323
  export declare namespace defineCommand {
311
324
  var withConfig: <TConfig = unknown, TInput extends CommandMessage = CommandMessage>() => CommandDefiner<TConfig, TInput>;
312
325
  }
@@ -322,11 +335,27 @@ export declare function parseCommandDefinition(value: unknown): CommandDefinitio
322
335
  * @internal Command projection helper.
323
336
  */
324
337
  export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
325
- /** @internal Command dispatcher assembly helper. */
326
- 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, options?: Readonly<Record<string, CommandParameterValue>>, presentation?: Readonly<{
327
- locale: Locale;
328
- formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
329
- }>): CommandContext;
338
+ /** Parsed invocation values, independent of the caller's transport and lifetime. */
339
+ export interface CommandInvocationArguments {
340
+ readonly args: readonly string[];
341
+ readonly params?: Readonly<Record<string, CommandParameterValue>>;
342
+ readonly options?: Readonly<Record<string, CommandParameterValue>>;
343
+ readonly segments?: readonly Readonly<CommandSegment>[];
344
+ }
345
+ /** Author and lifetime authority captured by the dispatcher for this invocation. */
346
+ export interface CommandInvocationOperation {
347
+ readonly snapshot: RuntimeSnapshot;
348
+ readonly ownerId: PluginId;
349
+ readonly input?: unknown;
350
+ readonly interaction?: UserInteraction;
351
+ readonly adapter?: string;
352
+ readonly presentation?: Readonly<{
353
+ locale: Locale;
354
+ formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
355
+ }>;
356
+ }
357
+ /** @internal Command dispatcher assembly helper; each value has one owner. */
358
+ export declare function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext;
330
359
  /**
331
360
  * 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
332
361
  * 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
package/lib/definition.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module zhin.js/command
4
4
  */
5
5
  import { resolveLocale } from '@zhin.js/plugin-runtime';
6
- import { createCapabilityContext, readOperationClient, readOperationInputSignal, } from '@zhin.js/feature-kit';
6
+ import { createCapabilityContext, readOperationClient, readOperationSession, readOperationInputSignal, } from '@zhin.js/feature-kit';
7
7
  import { assertPermitSyntax } from '@zhin.js/permission';
8
8
  import { validateArgumentSchemas } from './arguments.js';
9
9
  import { normalizeExecutionPolicy } from './execution-policy.js';
@@ -173,8 +173,10 @@ export function resolveDynamicParams(params, source) {
173
173
  }
174
174
  return Object.freeze(resolved);
175
175
  }
176
- /** @internal Command dispatcher assembly helper. */
177
- export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), interaction, adapter, options = Object.freeze({}), presentation) {
176
+ /** @internal Command dispatcher assembly helper; each value has one owner. */
177
+ export function createCommandContext(operation, invocation) {
178
+ const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
179
+ const { args, params = {}, options = {}, segments = [] } = invocation;
178
180
  const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
179
181
  const session = resolveCommandSession(input);
180
182
  const parameterValues = { __proto__: null, ...params };
@@ -191,6 +193,10 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
191
193
  ...(input !== undefined ? { input: input } : {}),
192
194
  ...(interaction !== undefined ? { interaction } : {}),
193
195
  };
196
+ Object.defineProperty(result, 'session', {
197
+ enumerable: true,
198
+ get: () => readOperationSession(input, adapter),
199
+ });
194
200
  Object.defineProperty(result, '$client', {
195
201
  enumerable: true,
196
202
  get: () => readOperationClient(input, adapter),
@@ -203,6 +209,20 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
203
209
  * @internal Command dispatcher projection helper.
204
210
  */
205
211
  export function resolveCommandSession(input) {
212
+ const operation = readOperationSession(input);
213
+ if (operation) {
214
+ const endpoint = input && typeof input === 'object' ? input.endpointId : undefined;
215
+ const sender = operation.sender;
216
+ const roles = [...new Set([...sender?.authorizationRoles ?? [], ...sender?.platformRoles ?? []])];
217
+ return Object.freeze({
218
+ adapter: operation.scene.endpoint.adapter,
219
+ ...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
220
+ scene: Object.freeze({ id: operation.scene.id, type: operation.scene.kind,
221
+ ...(operation.scene.name !== undefined ? { name: operation.scene.name } : {}) }),
222
+ ...(sender ? { sender: Object.freeze({ id: sender.id, ...(sender.name !== undefined ? { name: sender.name } : {}),
223
+ role: Object.freeze(roles.length ? roles : ['user']) }) } : {}),
224
+ });
225
+ }
206
226
  if (!isCommandMessageLike(input))
207
227
  return Object.freeze({});
208
228
  const metadata = input.metadata && typeof input.metadata === 'object'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Convention-based Command Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -21,10 +21,10 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "segment-matcher": "^1.0.5",
24
- "@zhin.js/feature-kit": "1.1.9",
24
+ "@zhin.js/feature-kit": "1.1.11",
25
25
  "@zhin.js/interaction": "1.1.2",
26
- "@zhin.js/permission": "1.1.9",
27
- "@zhin.js/plugin-runtime": "1.1.17"
26
+ "@zhin.js/permission": "1.1.11",
27
+ "@zhin.js/plugin-runtime": "1.1.19"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.1.2",
@@ -58,6 +58,24 @@ export interface CommandDescriptor {
58
58
  readonly shortcut?: readonly string[];
59
59
  }
60
60
 
61
+ /** One dispatch reads the caller's exact operation generation and presentation. */
62
+ export interface CommandDispatchRequest {
63
+ readonly input: CommandMatchInput;
64
+ readonly source?: unknown;
65
+ readonly interactionFactory?: UserInteractionFactory;
66
+ readonly commandPrefix?: string;
67
+ readonly snapshot?: RuntimeSnapshot;
68
+ readonly locale?: Locale;
69
+ }
70
+ /** Presentation and generation shared across one menu lookup/render operation. */
71
+ interface CommandMenuContext {
72
+ readonly commandPrefix: string;
73
+ readonly keyword: string;
74
+ readonly snapshot: RuntimeSnapshot;
75
+ readonly locale: Locale;
76
+ }
77
+ interface CommandMenuSelection { readonly pluginKey?: string; readonly page?: number }
78
+
61
79
  export interface CommandDispatchResult {
62
80
  readonly matched: boolean;
63
81
  readonly status: 'unmatched' | 'invalid' | 'denied' | 'limited' | 'executed';
@@ -323,26 +341,22 @@ export class CommandIndex {
323
341
  const admission = admitDirectCommandExecution(match.command.slot.id, match.command.execution, operationSnapshot);
324
342
  if (admission.limit) throw new CommandExecutionLimitedError(admission.limit);
325
343
  try {
326
- return await definition.execute(createCommandContext(operationSnapshot, match.command.slot.owner,
327
- textArgs(parsed.remaining), parsed.params, undefined, parsed.remaining, undefined, definition.adapter, parsed.options, { locale }));
344
+ return await definition.execute(createCommandContext({ snapshot: operationSnapshot, ownerId: match.command.slot.owner,
345
+ adapter: definition.adapter, presentation: { locale } }, {
346
+ args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options,
347
+ }));
328
348
  } finally {
329
349
  admission.release();
330
350
  }
331
351
  }
332
352
 
333
- async dispatch(
334
- input: CommandMatchInput,
335
- source: unknown = undefined,
336
- interactionFactory?: UserInteractionFactory,
337
- commandPrefix?: string,
338
- operationSnapshot: RuntimeSnapshot = this.snapshot,
339
- localeOverride?: Locale,
340
- ): Promise<CommandDispatchResult> {
353
+ async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot,
354
+ locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult> {
341
355
  const locale = localeOverride ?? commandLocale(source, operationSnapshot);
342
356
  const prefix = commandPrefix ?? '';
343
357
  if (this.#menu) {
344
358
  const keyword = this.#menu.keyword ?? commandMessages(locale, 'menuKeyword');
345
- const menuResult = this.#dispatchMenu(input, prefix, operationSnapshot, locale, keyword);
359
+ const menuResult = this.#dispatchMenu(input, { commandPrefix: prefix, snapshot: operationSnapshot, locale, keyword });
346
360
  if (menuResult !== undefined) {
347
361
  return Object.freeze({
348
362
  matched: true,
@@ -381,10 +395,12 @@ export class CommandIndex {
381
395
  owner: match.command.slot.owner, issues: parsed.issues, help });
382
396
  const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
383
397
  if (limit) return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
384
- const value = await definition.execute(createCommandContext(operationSnapshot, match.command.slot.owner,
385
- textArgs(parsed.remaining), parsed.params, authorSource, parsed.remaining, interactionFactory?.(authorSource), definition.adapter, parsed.options,
386
- { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath,
387
- source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) }));
398
+ const value = await definition.execute(createCommandContext({
399
+ snapshot: operationSnapshot, ownerId: match.command.slot.owner, input: authorSource,
400
+ interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
401
+ presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath,
402
+ source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
403
+ }, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options }));
388
404
  return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
389
405
  }
390
406
 
@@ -496,10 +512,11 @@ export class CommandIndex {
496
512
  // 内置菜单命令
497
513
  // ==========================================================================
498
514
 
499
- #dispatchMenu(input: CommandMatchInput, commandPrefix: string, snapshot: RuntimeSnapshot, locale: Locale, keyword: string): CommandMenuResult | undefined {
515
+ #dispatchMenu(input: CommandMatchInput, context: CommandMenuContext): CommandMenuResult | undefined {
516
+ const { commandPrefix, keyword, locale } = context;
500
517
  const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
501
518
  if (text === undefined) return undefined;
502
- if (text === keyword) return this.#buildMenu(undefined, commandPrefix, keyword, snapshot, locale);
519
+ if (text === keyword) return this.#buildMenu({}, context);
503
520
  if (text.startsWith(keyword + ' ')) {
504
521
  const key = text.slice(keyword.length + 1).trim();
505
522
  const args = key.split(/\s+/u);
@@ -510,15 +527,15 @@ export class CommandIndex {
510
527
  || !Number.isSafeInteger(Number(pageText))) {
511
528
  return { value: commandMessages(locale, 'invalidPage', { command: `${commandPrefix}${keyword}` }) };
512
529
  }
513
- return this.#buildMenu(flag === 1 ? args[0] : undefined, commandPrefix, keyword, snapshot, locale, Number(pageText));
530
+ return this.#buildMenu({ pluginKey: flag === 1 ? args[0] : undefined, page: Number(pageText) }, context);
514
531
  }
515
- if (key) return this.#buildMenu(key, commandPrefix, keyword, snapshot, locale);
516
- return this.#buildMenu(undefined, commandPrefix, keyword, snapshot, locale);
532
+ if (key) return this.#buildMenu({ pluginKey: key }, context);
533
+ return this.#buildMenu({}, context);
517
534
  }
518
535
  return undefined;
519
536
  }
520
537
 
521
- #buildMenu(pluginKey: string | undefined, commandPrefix: string, keyword: string, snapshot: RuntimeSnapshot, locale: Locale, page = 1): CommandMenuResult {
538
+ #buildMenu({ pluginKey, page = 1 }: CommandMenuSelection, { commandPrefix, keyword, snapshot, locale }: CommandMenuContext): CommandMenuResult {
522
539
  const root = snapshot.root;
523
540
  const targetId = pluginKey
524
541
  ? `${root}/${pluginKey.split('.').join('/')}` as PluginId
package/src/definition.ts CHANGED
@@ -6,6 +6,8 @@ import { resolveLocale, type Locale, type PluginId, type RuntimeSnapshot } from
6
6
  import {
7
7
  createCapabilityContext,
8
8
  readOperationClient,
9
+ readOperationSession,
10
+ type OperationSession,
9
11
  readOperationInputSignal,
10
12
  type AdapterClient,
11
13
  type RegisteredAdapterName,
@@ -291,6 +293,8 @@ export interface CommandContext<
291
293
  readonly input?: TInput;
292
294
  /** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
293
295
  readonly $client: AdapterClient<TAdapter>;
296
+ /** Bound by the actual IM operation; direct CommandIndex.execute calls may have no Session. */
297
+ readonly session: OperationSession<TAdapter> | undefined;
294
298
  /**
295
299
  * 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
296
300
  */
@@ -378,28 +382,40 @@ type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extend
378
382
  readonly options: InferCommandOptions<O>;
379
383
  };
380
384
 
385
+ /** The returned declaration retains its platform, parameter schemas and callback result. */
386
+ type DeclaredCommandAdapter<D> = D extends { readonly adapter: infer A extends string } ? A
387
+ : D extends { readonly adapter?: infer A extends string | undefined } ? A : undefined;
388
+ export type TypedCommandDefinition<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> =
389
+ Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, 'params' | 'options' | 'execute'> & {
390
+ readonly params?: P;
391
+ readonly options?: O;
392
+ execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
393
+ };
394
+
381
395
  /**
382
396
  * `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
383
397
  * 仍从定义推断。
384
398
  * @public
385
399
  */
386
400
  export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <
387
- TResult = unknown,
401
+ const TResult = unknown,
388
402
  const P extends CommandParams = {},
389
403
  const O extends CommandOptions = {},
404
+ const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
390
405
  >(
391
- definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
392
- ) => Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>>;
406
+ definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
407
+ ) => Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
393
408
 
394
409
  export function defineCommand<
395
410
  TConfig = unknown,
396
- TResult = unknown,
411
+ const TResult = unknown,
397
412
  TInput extends CommandMessage = CommandMessage,
398
413
  const P extends CommandParams = {},
399
414
  const O extends CommandOptions = {},
415
+ const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
400
416
  >(
401
- definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
402
- ): Readonly<CommandDefinition<TConfig, TResult, TInput, string | undefined>> {
417
+ definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
418
+ ): Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>> {
403
419
  if (typeof definition.execute !== 'function') {
404
420
  throw new TypeError('Command execute must be a function');
405
421
  }
@@ -423,7 +439,7 @@ export function defineCommand<
423
439
  validateCommandShortcutShape(definition.shortcut);
424
440
  const execution = normalizeExecutionPolicy(definition.execution);
425
441
  return Object.freeze({ $feature: commandBrand, ...definition, ...(execution ? { execution } : {}) }) as Readonly<
426
- CommandDefinition<TConfig, TResult, TInput, string | undefined>
442
+ TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>
427
443
  >;
428
444
  }
429
445
 
@@ -570,19 +586,31 @@ export function resolveDynamicParams(
570
586
  return Object.freeze(resolved);
571
587
  }
572
588
 
573
- /** @internal Command dispatcher assembly helper. */
574
- export function createCommandContext(
575
- snapshot: RuntimeSnapshot,
576
- ownerId: PluginId,
577
- args: readonly string[],
578
- params: Readonly<Record<string, CommandParameterValue>> = Object.freeze({}),
579
- input: unknown = undefined,
580
- segments: readonly Readonly<CommandSegment>[] = Object.freeze([]),
581
- interaction?: UserInteraction,
582
- adapter?: string,
583
- options: Readonly<Record<string, CommandParameterValue>> = Object.freeze({}),
584
- presentation?: Readonly<{ locale: Locale; formatCommand?: (localPath: string, owner?: PluginId) => string | undefined }>,
585
- ): CommandContext {
589
+ /** Parsed invocation values, independent of the caller's transport and lifetime. */
590
+ export interface CommandInvocationArguments {
591
+ readonly args: readonly string[];
592
+ readonly params?: Readonly<Record<string, CommandParameterValue>>;
593
+ readonly options?: Readonly<Record<string, CommandParameterValue>>;
594
+ readonly segments?: readonly Readonly<CommandSegment>[];
595
+ }
596
+
597
+ /** Author and lifetime authority captured by the dispatcher for this invocation. */
598
+ export interface CommandInvocationOperation {
599
+ readonly snapshot: RuntimeSnapshot;
600
+ readonly ownerId: PluginId;
601
+ readonly input?: unknown;
602
+ readonly interaction?: UserInteraction;
603
+ readonly adapter?: string;
604
+ readonly presentation?: Readonly<{
605
+ locale: Locale;
606
+ formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
607
+ }>;
608
+ }
609
+
610
+ /** @internal Command dispatcher assembly helper; each value has one owner. */
611
+ export function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext {
612
+ const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
613
+ const { args, params = {}, options = {}, segments = [] } = invocation;
586
614
  const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
587
615
  const session = resolveCommandSession(input);
588
616
  const parameterValues: Record<string, CommandParameterValue> = { __proto__: null, ...params };
@@ -599,6 +627,10 @@ export function createCommandContext(
599
627
  ...(input !== undefined ? { input: input as CommandMessage } : {}),
600
628
  ...(interaction !== undefined ? { interaction } : {}),
601
629
  } as CommandContext;
630
+ Object.defineProperty(result, 'session', {
631
+ enumerable: true,
632
+ get: () => readOperationSession(input, adapter),
633
+ });
602
634
  Object.defineProperty(result, '$client', {
603
635
  enumerable: true,
604
636
  get: () => readOperationClient(input, adapter),
@@ -612,6 +644,20 @@ export function createCommandContext(
612
644
  * @internal Command dispatcher projection helper.
613
645
  */
614
646
  export function resolveCommandSession(input: unknown): CommandSession {
647
+ const operation = readOperationSession(input);
648
+ if (operation) {
649
+ const endpoint = input && typeof input === 'object' ? (input as { readonly endpointId?: unknown }).endpointId : undefined;
650
+ const sender = operation.sender;
651
+ const roles = [...new Set([...sender?.authorizationRoles ?? [], ...sender?.platformRoles ?? []])];
652
+ return Object.freeze({
653
+ adapter: operation.scene.endpoint.adapter,
654
+ ...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
655
+ scene: Object.freeze({ id: operation.scene.id, type: operation.scene.kind,
656
+ ...(operation.scene.name !== undefined ? { name: operation.scene.name } : {}) }),
657
+ ...(sender ? { sender: Object.freeze({ id: sender.id, ...(sender.name !== undefined ? { name: sender.name } : {}),
658
+ role: Object.freeze(roles.length ? roles : ['user']) }) } : {}),
659
+ });
660
+ }
615
661
  if (!isCommandMessageLike(input)) return Object.freeze({});
616
662
 
617
663
  const metadata = input.metadata && typeof input.metadata === 'object'