@zhin.js/command 1.1.13 → 1.1.15

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.
@@ -1,6 +1,6 @@
1
1
  import type { CommandDefinition, CommandParams, CommandOptions, CommandParameterValue, CommandSegment, CommandSession } from './definition.js';
2
2
  import type { Locale } from '@zhin.js/plugin-runtime';
3
- export type CommandValidationIssueCode = 'expectedType' | 'expectedArray' | 'minimum' | 'maximum' | 'choices' | 'invalidFormat' | 'invalidValue' | 'unknownOption' | 'duplicateOption' | 'defaultUndefined' | 'parameterUndefined' | 'missingOption' | 'unexpectedArguments' | 'invalidParameter' | 'custom';
3
+ export type CommandValidationIssueCode = 'expectedType' | 'expectedArray' | 'minimum' | 'maximum' | 'choices' | 'invalidFormat' | 'invalidValue' | 'unknownOption' | 'duplicateOption' | 'defaultUndefined' | 'parameterUndefined' | 'missingOption' | 'missingRequiredParam' | 'unexpectedArguments' | 'invalidParameter' | 'custom';
4
4
  export interface CommandValidationIssue {
5
5
  readonly field: string;
6
6
  readonly message: string;
package/lib/arguments.js CHANGED
@@ -105,7 +105,7 @@ export function requestsCommandHelp(segments) {
105
105
  continue;
106
106
  if (token.value === '--')
107
107
  return false;
108
- if (token.value === '--help')
108
+ if (token.value === '--help' || token.value === '-h')
109
109
  return true;
110
110
  }
111
111
  return false;
@@ -300,7 +300,7 @@ export function parseCommandArguments(definition, remaining, initial, session, l
300
300
  else if (schema.default !== undefined)
301
301
  params[name] = typeof schema.default === 'function' ? schema.default(session) : schema.default;
302
302
  else if (!schema.optional && !(definition.$parameter?.name === name && definition.$parameter.optional)) {
303
- issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
303
+ issues.push(commandValidationIssue('missingRequiredParam', name, locale, { type: schema.type }));
304
304
  }
305
305
  }
306
306
  if (Object.hasOwn(params, name) && params[name] === undefined)
@@ -1,5 +1,6 @@
1
1
  import { type CapabilityId, type CapabilitySlot, type PluginId, type RuntimeSnapshot, type Locale } from '@zhin.js/plugin-runtime';
2
2
  import type { UserInteractionFactory } from '@zhin.js/interaction';
3
+ import { type CapabilityOperationFactory } from '@zhin.js/feature-kit';
3
4
  import { type CommandValidationIssue } from './arguments.js';
4
5
  import { type CommandMenuEntry, type CommandMenuAppearance } from './command-menu.js';
5
6
  export type { CommandMenuEntry, CommandMenuAppearance } from './command-menu.js';
@@ -32,6 +33,7 @@ export interface CommandDispatchRequest {
32
33
  readonly interactionFactory?: UserInteractionFactory;
33
34
  readonly commandPrefix?: string;
34
35
  readonly snapshot?: RuntimeSnapshot;
36
+ readonly operation?: CapabilityOperationFactory;
35
37
  readonly locale?: Locale;
36
38
  }
37
39
  export interface CommandDispatchResult {
@@ -81,7 +83,7 @@ export declare class CommandIndex {
81
83
  formatMenu(prefix: string | undefined, locale: Locale): string | undefined;
82
84
  has(name: string): boolean;
83
85
  execute(name: string, args?: readonly string[], operationSnapshot?: RuntimeSnapshot): Promise<unknown>;
84
- dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot, locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult>;
86
+ dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot, locale: localeOverride, operation: operationFactory }: CommandDispatchRequest): Promise<CommandDispatchResult>;
85
87
  }
86
88
  export declare function isCommandIndex(value: unknown): value is CommandIndex;
87
89
  /**
@@ -21,6 +21,7 @@ const segmentFields = {
21
21
  dice: 'result',
22
22
  rps: 'result',
23
23
  };
24
+ const DEFAULT_MENU_KEYWORDS = new Set(['菜单', '帮助', 'help', 'menu']);
24
25
  export class CommandIndex {
25
26
  snapshot;
26
27
  $projection = 'zhin.command-index/1';
@@ -195,7 +196,7 @@ export class CommandIndex {
195
196
  admission.release();
196
197
  }
197
198
  }
198
- async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot, locale: localeOverride }) {
199
+ async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot, locale: localeOverride, operation: operationFactory }) {
199
200
  const locale = localeOverride ?? commandLocale(source, operationSnapshot);
200
201
  const prefix = commandPrefix ?? '';
201
202
  if (this.#menu) {
@@ -247,12 +248,22 @@ export class CommandIndex {
247
248
  const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
248
249
  if (limit)
249
250
  return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
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 }));
255
- return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
251
+ const operation = operationFactory?.create({ owner: match.command.slot.owner, signal: operationFactory.signal });
252
+ try {
253
+ const value = await definition.execute(createCommandContext({
254
+ snapshot: operationSnapshot, ownerId: match.command.slot.owner, input: authorSource,
255
+ operation,
256
+ interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
257
+ presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath, source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
258
+ }, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options }));
259
+ return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
260
+ }
261
+ finally {
262
+ try {
263
+ await operation?.resources?.close();
264
+ }
265
+ catch { /* Cleanup never replaces the command's actual outcome. */ }
266
+ }
256
267
  }
257
268
  #denied(record) {
258
269
  return Object.freeze({ matched: true, status: 'denied', command: record.name, owner: record.slot.owner });
@@ -374,10 +385,19 @@ export class CommandIndex {
374
385
  const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
375
386
  if (text === undefined)
376
387
  return undefined;
377
- if (text === keyword)
388
+ const spaceIndex = text.search(/\s/u);
389
+ const firstToken = spaceIndex === -1 ? text : text.slice(0, spaceIndex);
390
+ const normalizedFirst = !commandPrefix && firstToken.startsWith('/')
391
+ ? firstToken.slice(1)
392
+ : firstToken;
393
+ const matchedDefaultAlias = this.#menu?.keyword === undefined
394
+ && DEFAULT_MENU_KEYWORDS.has(normalizedFirst.toLowerCase());
395
+ if (text === keyword || (matchedDefaultAlias && spaceIndex === -1))
378
396
  return this.#buildMenu({}, context);
379
- if (text.startsWith(keyword + ' ')) {
380
- const key = text.slice(keyword.length + 1).trim();
397
+ if (text.startsWith(keyword + ' ') || (matchedDefaultAlias && spaceIndex !== -1)) {
398
+ const key = (text.startsWith(keyword + ' ')
399
+ ? text.slice(keyword.length + 1)
400
+ : text.slice(spaceIndex + 1)).trim();
381
401
  const args = key.split(/\s+/u);
382
402
  const flag = args.indexOf('--page');
383
403
  if (flag !== -1) {
@@ -406,17 +426,23 @@ export class CommandIndex {
406
426
  const children = node.children
407
427
  .map((childId) => snapshot.tree.get(childId))
408
428
  .filter((child) => !!child);
409
- const pages = Math.max(1, Math.ceil((directCommands.length + children.length) / MENU_PAGE_SIZE));
429
+ const effectiveCommands = targetId === root
430
+ && directCommands.length === 0
431
+ && this.#commands.length > 0
432
+ && this.#commands.length <= MENU_PAGE_SIZE
433
+ ? this.#commands
434
+ : directCommands;
435
+ const pages = Math.max(1, Math.ceil((effectiveCommands.length + children.length) / MENU_PAGE_SIZE));
410
436
  const invocation = `${commandPrefix}${keyword}${pluginKey ? ` ${pluginKey}` : ''}`;
411
437
  if (page > pages)
412
438
  return { value: commandMessages(locale, 'pageOutOfRange', { pages, command: invocation }) };
413
439
  const offset = (page - 1) * MENU_PAGE_SIZE;
414
- const pageCommands = directCommands.slice(offset, offset + MENU_PAGE_SIZE);
415
- const pageChildren = children.slice(Math.max(0, offset - directCommands.length), Math.max(0, offset + MENU_PAGE_SIZE - directCommands.length));
440
+ const pageCommands = effectiveCommands.slice(offset, offset + MENU_PAGE_SIZE);
441
+ const pageChildren = children.slice(Math.max(0, offset - effectiveCommands.length), Math.max(0, offset + MENU_PAGE_SIZE - effectiveCommands.length));
416
442
  const displayKey = pluginKey ?? node.instanceKey;
417
443
  const lines = [commandMessages(locale, 'menuTitle', { plugin: displayKey })];
418
444
  if (pages > 1)
419
- lines.push(commandMessages(locale, 'menuSummary', { page, pages, commands: directCommands.length, plugins: children.length }));
445
+ lines.push(commandMessages(locale, 'menuSummary', { page, pages, commands: effectiveCommands.length, plugins: children.length }));
420
446
  if (pageCommands.length > 0) {
421
447
  lines.push('');
422
448
  for (const cmd of pageCommands) {
@@ -434,7 +460,7 @@ export class CommandIndex {
434
460
  lines.push(` ${childKey} (${label})`);
435
461
  }
436
462
  }
437
- if (directCommands.length === 0 && children.length === 0) {
463
+ if (effectiveCommands.length === 0 && children.length === 0) {
438
464
  lines.push('');
439
465
  lines.push(commandMessages(locale, 'emptyMenu'));
440
466
  }
@@ -461,7 +487,7 @@ export class CommandIndex {
461
487
  name: pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey,
462
488
  description: child.metadata?.displayName ?? child.instanceKey,
463
489
  }))),
464
- commandTotal: directCommands.length, pluginTotal: children.length, hint, text,
490
+ commandTotal: effectiveCommands.length, pluginTotal: children.length, hint, text,
465
491
  appearance: Object.freeze(readMenuAppearance(config?.menuStyle)),
466
492
  });
467
493
  if (!snapshot.resources.get(root)?.has(htmlRendererToken.id))
@@ -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 OperationSession, type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
6
+ import { type CapabilityOperation, 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';
@@ -346,6 +346,7 @@ export interface CommandInvocationArguments {
346
346
  export interface CommandInvocationOperation {
347
347
  readonly snapshot: RuntimeSnapshot;
348
348
  readonly ownerId: PluginId;
349
+ readonly operation?: CapabilityOperation;
349
350
  readonly input?: unknown;
350
351
  readonly interaction?: UserInteraction;
351
352
  readonly adapter?: string;
package/lib/definition.js CHANGED
@@ -177,7 +177,7 @@ export function resolveDynamicParams(params, source) {
177
177
  export function createCommandContext(operation, invocation) {
178
178
  const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
179
179
  const { args, params = {}, options = {}, segments = [] } = invocation;
180
- const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
180
+ const context = createCapabilityContext({ snapshot, ownerId, signal: readOperationInputSignal(input), operation: operation.operation });
181
181
  const session = resolveCommandSession(input);
182
182
  const parameterValues = { __proto__: null, ...params };
183
183
  const optionValues = { __proto__: null, ...options };
package/lib/messages.d.ts CHANGED
@@ -5,4 +5,4 @@ export declare function commandDescription(value: Readonly<{
5
5
  descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
6
6
  }>, locale: Locale): string | undefined;
7
7
  /** Static dictionaries; the caller supplies the operation's language. */
8
- export declare const commandMessages: (locale: Locale | undefined, key: "choices" | "defaultValue" | "menuKeyword" | "invalidPage" | "missingPlugin" | "pageOutOfRange" | "menuTitle" | "menuSummary" | "childPlugins" | "emptyMenu" | "pluginHint" | "previousPage" | "nextPage" | "guideTitle" | "guideSubtitle" | "commandCount" | "pluginCount" | "commandIndex" | "pluginIndex" | "guideEmpty" | "helpHint" | "genericHelpHint" | "expectedType" | "expectedArray" | "minimum" | "maximum" | "invalidFormat" | "invalidValue" | "unknownOption" | "duplicateOption" | "defaultUndefined" | "parameterUndefined" | "missingOption" | "unexpectedArguments" | "invalidParameter" | "dynamicValue" | "noAdapter" | "missingEndpoint" | "adapterComparison" | "conditionMismatch" | "missingPermissionHost" | "missingEvidence" | "noPermit" | "uncheckedConditions" | "conditionsMatch" | "unconstrained" | "simulatedIdentity" | "unevaluatedCode" | "defaultedRole", values?: import("@zhin.js/plugin-runtime").MessageValues) => string;
8
+ export declare const commandMessages: (locale: Locale | undefined, key: "choices" | "defaultValue" | "menuKeyword" | "invalidPage" | "missingPlugin" | "pageOutOfRange" | "menuTitle" | "menuSummary" | "childPlugins" | "emptyMenu" | "pluginHint" | "previousPage" | "nextPage" | "guideTitle" | "guideSubtitle" | "commandCount" | "pluginCount" | "commandIndex" | "pluginIndex" | "guideEmpty" | "helpHint" | "genericHelpHint" | "expectedType" | "expectedArray" | "minimum" | "maximum" | "invalidFormat" | "invalidValue" | "unknownOption" | "duplicateOption" | "defaultUndefined" | "parameterUndefined" | "missingOption" | "missingRequiredParam" | "unexpectedArguments" | "invalidParameter" | "dynamicValue" | "noAdapter" | "missingEndpoint" | "adapterComparison" | "conditionMismatch" | "missingPermissionHost" | "missingEvidence" | "noPermit" | "uncheckedConditions" | "conditionsMatch" | "unconstrained" | "simulatedIdentity" | "unevaluatedCode" | "defaultedRole", values?: import("@zhin.js/plugin-runtime").MessageValues) => string;
package/lib/messages.js CHANGED
@@ -37,6 +37,7 @@ export const commandMessages = createMessages({
37
37
  defaultUndefined: { 'zh-CN': '{field}:默认值返回了 undefined', en: '{field}: default returned undefined' },
38
38
  parameterUndefined: { 'zh-CN': '{field}:默认值或快捷指令返回了 undefined', en: '{field}: default or shortcut returned undefined' },
39
39
  missingOption: { 'zh-CN': '缺少选项 --{field}', en: 'Missing option --{field}' },
40
+ missingRequiredParam: { 'zh-CN': '缺少必填参数 {field}(类型:{type})', en: 'Missing required parameter {field} (expected {type})' },
40
41
  unexpectedArguments: { 'zh-CN': '存在多余参数', en: 'Unexpected arguments' },
41
42
  invalidParameter: { 'zh-CN': '缺少指令参数或参数不正确', en: 'Missing or invalid command parameter' },
42
43
  defaultValue: { 'zh-CN': '默认值:{value}', en: 'default: {value}' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
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.11",
25
24
  "@zhin.js/interaction": "1.1.2",
26
- "@zhin.js/permission": "1.1.11",
27
- "@zhin.js/plugin-runtime": "1.1.19"
25
+ "@zhin.js/permission": "1.1.13",
26
+ "@zhin.js/plugin-runtime": "1.1.21",
27
+ "@zhin.js/feature-kit": "1.1.13"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.1.2",
package/src/arguments.ts CHANGED
@@ -7,7 +7,7 @@ import { commandDescription, commandMessages } from './messages.js';
7
7
 
8
8
  export type CommandValidationIssueCode = 'expectedType' | 'expectedArray' | 'minimum' | 'maximum'
9
9
  | 'choices' | 'invalidFormat' | 'invalidValue' | 'unknownOption' | 'duplicateOption'
10
- | 'defaultUndefined' | 'parameterUndefined' | 'missingOption' | 'unexpectedArguments'
10
+ | 'defaultUndefined' | 'parameterUndefined' | 'missingOption' | 'missingRequiredParam' | 'unexpectedArguments'
11
11
  | 'invalidParameter' | 'custom';
12
12
 
13
13
  export interface CommandValidationIssue {
@@ -136,7 +136,7 @@ export function requestsCommandHelp(segments: readonly Readonly<CommandSegment>[
136
136
  for (const token of tokenize(segments)) {
137
137
  if (!isTextToken(token) || token.quoted) continue;
138
138
  if (token.value === '--') return false;
139
- if (token.value === '--help') return true;
139
+ if (token.value === '--help' || token.value === '-h') return true;
140
140
  }
141
141
  return false;
142
142
  }
@@ -280,7 +280,7 @@ export function parseCommandArguments(
280
280
  if (value !== undefined) params[name] = value;
281
281
  else if (schema.default !== undefined) params[name] = typeof schema.default === 'function' ? schema.default(session) : schema.default;
282
282
  else if (!schema.optional && !(definition.$parameter?.name === name && definition.$parameter.optional)) {
283
- issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
283
+ issues.push(commandValidationIssue('missingRequiredParam', name, locale, { type: schema.type }));
284
284
  }
285
285
  }
286
286
  if (Object.hasOwn(params, name) && params[name] === undefined) issues.push(commandValidationIssue('parameterUndefined', name, locale));
@@ -13,7 +13,7 @@ import {
13
13
  type Locale,
14
14
  } from '@zhin.js/plugin-runtime';
15
15
  import type { UserInteractionFactory } from '@zhin.js/interaction';
16
- import { bindOperationInput } from '@zhin.js/feature-kit';
16
+ import { bindOperationInput, type CapabilityOperationFactory } from '@zhin.js/feature-kit';
17
17
  import {
18
18
  permissionHostToken,
19
19
  type PermissionHost,
@@ -65,6 +65,7 @@ export interface CommandDispatchRequest {
65
65
  readonly interactionFactory?: UserInteractionFactory;
66
66
  readonly commandPrefix?: string;
67
67
  readonly snapshot?: RuntimeSnapshot;
68
+ readonly operation?: CapabilityOperationFactory;
68
69
  readonly locale?: Locale;
69
70
  }
70
71
  /** Presentation and generation shared across one menu lookup/render operation. */
@@ -161,6 +162,8 @@ export interface CommandMenuConfig {
161
162
  readonly keyword?: string;
162
163
  }
163
164
 
165
+ const DEFAULT_MENU_KEYWORDS = new Set(['菜单', '帮助', 'help', 'menu']);
166
+
164
167
  export class CommandIndex {
165
168
  readonly $projection = 'zhin.command-index/1' as const;
166
169
  readonly #commands: readonly CommandRecord[];
@@ -351,7 +354,7 @@ export class CommandIndex {
351
354
  }
352
355
 
353
356
  async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot,
354
- locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult> {
357
+ locale: localeOverride, operation: operationFactory }: CommandDispatchRequest): Promise<CommandDispatchResult> {
355
358
  const locale = localeOverride ?? commandLocale(source, operationSnapshot);
356
359
  const prefix = commandPrefix ?? '';
357
360
  if (this.#menu) {
@@ -395,13 +398,19 @@ export class CommandIndex {
395
398
  owner: match.command.slot.owner, issues: parsed.issues, help });
396
399
  const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
397
400
  if (limit) return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
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 }));
404
- return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
401
+ const operation = operationFactory?.create({ owner: match.command.slot.owner, signal: operationFactory.signal });
402
+ try {
403
+ const value = await definition.execute(createCommandContext({
404
+ snapshot: operationSnapshot, ownerId: match.command.slot.owner, input: authorSource,
405
+ operation,
406
+ interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
407
+ presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath,
408
+ source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
409
+ }, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options }));
410
+ return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
411
+ } finally {
412
+ try { await operation?.resources?.close(); } catch { /* Cleanup never replaces the command's actual outcome. */ }
413
+ }
405
414
  }
406
415
 
407
416
  #denied(record: CommandRecord): CommandDispatchResult {
@@ -516,9 +525,18 @@ export class CommandIndex {
516
525
  const { commandPrefix, keyword, locale } = context;
517
526
  const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
518
527
  if (text === undefined) return undefined;
519
- if (text === keyword) return this.#buildMenu({}, context);
520
- if (text.startsWith(keyword + ' ')) {
521
- const key = text.slice(keyword.length + 1).trim();
528
+ const spaceIndex = text.search(/\s/u);
529
+ const firstToken = spaceIndex === -1 ? text : text.slice(0, spaceIndex);
530
+ const normalizedFirst = !commandPrefix && firstToken.startsWith('/')
531
+ ? firstToken.slice(1)
532
+ : firstToken;
533
+ const matchedDefaultAlias = this.#menu?.keyword === undefined
534
+ && DEFAULT_MENU_KEYWORDS.has(normalizedFirst.toLowerCase());
535
+ if (text === keyword || (matchedDefaultAlias && spaceIndex === -1)) return this.#buildMenu({}, context);
536
+ if (text.startsWith(keyword + ' ') || (matchedDefaultAlias && spaceIndex !== -1)) {
537
+ const key = (text.startsWith(keyword + ' ')
538
+ ? text.slice(keyword.length + 1)
539
+ : text.slice(spaceIndex + 1)).trim();
522
540
  const args = key.split(/\s+/u);
523
541
  const flag = args.indexOf('--page');
524
542
  if (flag !== -1) {
@@ -548,17 +566,23 @@ export class CommandIndex {
548
566
  const children = node.children
549
567
  .map((childId) => snapshot.tree.get(childId))
550
568
  .filter((child): child is NonNullable<typeof child> => !!child);
551
-
552
- const pages = Math.max(1, Math.ceil((directCommands.length + children.length) / MENU_PAGE_SIZE));
569
+ const effectiveCommands = targetId === root
570
+ && directCommands.length === 0
571
+ && this.#commands.length > 0
572
+ && this.#commands.length <= MENU_PAGE_SIZE
573
+ ? this.#commands
574
+ : directCommands;
575
+
576
+ const pages = Math.max(1, Math.ceil((effectiveCommands.length + children.length) / MENU_PAGE_SIZE));
553
577
  const invocation = `${commandPrefix}${keyword}${pluginKey ? ` ${pluginKey}` : ''}`;
554
578
  if (page > pages) return { value: commandMessages(locale, 'pageOutOfRange', { pages, command: invocation }) };
555
579
  const offset = (page - 1) * MENU_PAGE_SIZE;
556
- const pageCommands = directCommands.slice(offset, offset + MENU_PAGE_SIZE);
557
- const pageChildren = children.slice(Math.max(0, offset - directCommands.length), Math.max(0, offset + MENU_PAGE_SIZE - directCommands.length));
580
+ const pageCommands = effectiveCommands.slice(offset, offset + MENU_PAGE_SIZE);
581
+ const pageChildren = children.slice(Math.max(0, offset - effectiveCommands.length), Math.max(0, offset + MENU_PAGE_SIZE - effectiveCommands.length));
558
582
 
559
583
  const displayKey = pluginKey ?? node.instanceKey;
560
584
  const lines: string[] = [commandMessages(locale, 'menuTitle', { plugin: displayKey })];
561
- if (pages > 1) lines.push(commandMessages(locale, 'menuSummary', { page, pages, commands: directCommands.length, plugins: children.length }));
585
+ if (pages > 1) lines.push(commandMessages(locale, 'menuSummary', { page, pages, commands: effectiveCommands.length, plugins: children.length }));
562
586
 
563
587
  if (pageCommands.length > 0) {
564
588
  lines.push('');
@@ -579,7 +603,7 @@ export class CommandIndex {
579
603
  }
580
604
  }
581
605
 
582
- if (directCommands.length === 0 && children.length === 0) {
606
+ if (effectiveCommands.length === 0 && children.length === 0) {
583
607
  lines.push('');
584
608
  lines.push(commandMessages(locale, 'emptyMenu'));
585
609
  }
@@ -605,7 +629,7 @@ export class CommandIndex {
605
629
  name: pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey,
606
630
  description: child.metadata?.displayName ?? child.instanceKey,
607
631
  }))),
608
- commandTotal: directCommands.length, pluginTotal: children.length, hint, text,
632
+ commandTotal: effectiveCommands.length, pluginTotal: children.length, hint, text,
609
633
  appearance: Object.freeze(readMenuAppearance(config?.menuStyle)),
610
634
  });
611
635
  if (!snapshot.resources.get(root)?.has(htmlRendererToken.id)) return { value: text, menu };
package/src/definition.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { resolveLocale, type Locale, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
6
6
  import {
7
7
  createCapabilityContext,
8
+ type CapabilityOperation,
8
9
  readOperationClient,
9
10
  readOperationSession,
10
11
  type OperationSession,
@@ -598,6 +599,7 @@ export interface CommandInvocationArguments {
598
599
  export interface CommandInvocationOperation {
599
600
  readonly snapshot: RuntimeSnapshot;
600
601
  readonly ownerId: PluginId;
602
+ readonly operation?: CapabilityOperation;
601
603
  readonly input?: unknown;
602
604
  readonly interaction?: UserInteraction;
603
605
  readonly adapter?: string;
@@ -611,7 +613,7 @@ export interface CommandInvocationOperation {
611
613
  export function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext {
612
614
  const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
613
615
  const { args, params = {}, options = {}, segments = [] } = invocation;
614
- const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
616
+ const context = createCapabilityContext({ snapshot, ownerId, signal: readOperationInputSignal(input), operation: operation.operation });
615
617
  const session = resolveCommandSession(input);
616
618
  const parameterValues: Record<string, CommandParameterValue> = { __proto__: null, ...params };
617
619
  const optionValues: Record<string, CommandParameterValue> = { __proto__: null, ...options };
package/src/messages.ts CHANGED
@@ -42,6 +42,7 @@ export const commandMessages = createMessages({
42
42
  defaultUndefined: { 'zh-CN': '{field}:默认值返回了 undefined', en: '{field}: default returned undefined' },
43
43
  parameterUndefined: { 'zh-CN': '{field}:默认值或快捷指令返回了 undefined', en: '{field}: default or shortcut returned undefined' },
44
44
  missingOption: { 'zh-CN': '缺少选项 --{field}', en: 'Missing option --{field}' },
45
+ missingRequiredParam: { 'zh-CN': '缺少必填参数 {field}(类型:{type})', en: 'Missing required parameter {field} (expected {type})' },
45
46
  unexpectedArguments: { 'zh-CN': '存在多余参数', en: 'Unexpected arguments' },
46
47
  invalidParameter: { 'zh-CN': '缺少指令参数或参数不正确', en: 'Missing or invalid command parameter' },
47
48
  defaultValue: { 'zh-CN': '默认值:{value}', en: 'default: {value}' },