@zhin.js/command 1.0.11 → 1.0.12

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,5 +1,5 @@
1
1
  import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
- import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment } from './definition.js';
2
+ import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment, type CommandPromptFactory } from './definition.js';
3
3
  export interface CommandParameterDescriptor extends CommandParameterDefinition {
4
4
  readonly required: boolean;
5
5
  }
@@ -20,15 +20,18 @@ export interface CommandDispatchResult {
20
20
  readonly value?: unknown;
21
21
  }
22
22
  export type CommandMatchInput = string | readonly Readonly<CommandSegment>[];
23
+ export interface CommandMenuConfig {
24
+ readonly keyword: string;
25
+ }
23
26
  export declare class CommandIndex {
24
27
  #private;
25
28
  private readonly snapshot;
26
29
  readonly $projection: "zhin.command-index/1";
27
- constructor(slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[], snapshot: RuntimeSnapshot);
30
+ constructor(slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[], snapshot: RuntimeSnapshot, menu?: CommandMenuConfig);
28
31
  list(): readonly CommandDescriptor[];
29
32
  has(name: string): boolean;
30
33
  execute(name: string, args?: readonly string[]): Promise<unknown>;
31
- dispatch(input: CommandMatchInput, source?: unknown): Promise<CommandDispatchResult>;
34
+ dispatch(input: CommandMatchInput, source?: unknown, promptFactory?: CommandPromptFactory): Promise<CommandDispatchResult>;
32
35
  }
33
36
  export declare function isCommandIndex(value: unknown): value is CommandIndex;
34
37
  export declare class CommandParameterValueError extends TypeError {
@@ -19,8 +19,10 @@ export class CommandIndex {
19
19
  #commands;
20
20
  #routes;
21
21
  #shortcuts;
22
- constructor(slots, snapshot) {
22
+ #menu;
23
+ constructor(slots, snapshot, menu) {
23
24
  this.snapshot = snapshot;
25
+ this.#menu = menu;
24
26
  const commands = [];
25
27
  const routes = [];
26
28
  const occupancy = new Map();
@@ -117,13 +119,25 @@ export class CommandIndex {
117
119
  // Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session。
118
120
  return match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, undefined)));
119
121
  }
120
- async dispatch(input, source = undefined) {
122
+ async dispatch(input, source = undefined, promptFactory) {
123
+ if (this.#menu) {
124
+ const menuValue = this.#dispatchMenu(input);
125
+ if (menuValue !== undefined) {
126
+ return Object.freeze({
127
+ matched: true,
128
+ command: this.#menu.keyword,
129
+ owner: this.snapshot.root,
130
+ value: menuValue,
131
+ });
132
+ }
133
+ }
134
+ const prompt = promptFactory?.(source);
121
135
  const shortcut = this.#matchShortcut(input);
122
136
  if (shortcut) {
123
137
  if (!(await this.#permitAllows(shortcut.record, source))) {
124
138
  return Object.freeze({ matched: false });
125
139
  }
126
- const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), resolveDynamicParams(shortcut.params, source), source, Object.freeze([])));
140
+ const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), resolveDynamicParams(shortcut.params, source), source, Object.freeze([]), prompt));
127
141
  return Object.freeze({
128
142
  matched: true,
129
143
  command: shortcut.record.name,
@@ -138,7 +152,7 @@ export class CommandIndex {
138
152
  return Object.freeze({ matched: false });
139
153
  }
140
154
  const args = textArgs(match.remaining);
141
- const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining));
155
+ const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining, prompt));
142
156
  return Object.freeze({
143
157
  matched: true,
144
158
  command: match.command.name,
@@ -232,6 +246,65 @@ export class CommandIndex {
232
246
  throw new CommandParameterValueError(parameter.name, parameter.type, value);
233
247
  }
234
248
  }
249
+ // ==========================================================================
250
+ // 内置菜单命令
251
+ // ==========================================================================
252
+ #dispatchMenu(input) {
253
+ const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
254
+ if (text === undefined)
255
+ return undefined;
256
+ const keyword = this.#menu.keyword;
257
+ if (text === keyword)
258
+ return this.#buildMenu();
259
+ if (text.startsWith(keyword + ' ')) {
260
+ const key = text.slice(keyword.length + 1).trim();
261
+ if (key)
262
+ return this.#buildMenu(key);
263
+ return this.#buildMenu();
264
+ }
265
+ return undefined;
266
+ }
267
+ #buildMenu(pluginKey) {
268
+ const root = this.snapshot.root;
269
+ const targetId = pluginKey
270
+ ? `${root}/${pluginKey.split('.').join('/')}`
271
+ : root;
272
+ const node = this.snapshot.tree.get(targetId);
273
+ if (!node)
274
+ return `未找到插件: ${pluginKey}`;
275
+ const directCommands = this.#commands.filter((cmd) => cmd.slot.owner === targetId);
276
+ const children = node.children
277
+ .map((childId) => this.snapshot.tree.get(childId))
278
+ .filter((child) => !!child);
279
+ const keyword = this.#menu.keyword;
280
+ const displayKey = pluginKey ?? node.instanceKey;
281
+ const lines = [`=== ${displayKey} 指令菜单 ===`];
282
+ if (directCommands.length > 0) {
283
+ lines.push('');
284
+ for (const cmd of directCommands) {
285
+ const desc = cmd.description ? ` - ${cmd.description}` : '';
286
+ lines.push(` ${cmd.name}${desc}`);
287
+ }
288
+ }
289
+ if (children.length > 0) {
290
+ lines.push('');
291
+ lines.push('子插件:');
292
+ for (const child of children) {
293
+ const childKey = pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey;
294
+ const label = child.metadata?.displayName ?? child.instanceKey;
295
+ lines.push(` ${childKey} (${label})`);
296
+ }
297
+ }
298
+ if (directCommands.length === 0 && children.length === 0) {
299
+ lines.push('');
300
+ lines.push('(暂无指令和子插件)');
301
+ }
302
+ if (children.length > 0) {
303
+ lines.push('');
304
+ lines.push(`提示:使用「${keyword} <插件名>」查看子插件的指令`);
305
+ }
306
+ return lines.join('\n');
307
+ }
235
308
  }
236
309
  export function isCommandIndex(value) {
237
310
  return !!value && typeof value === 'object'
@@ -143,6 +143,54 @@ export interface CommandSession {
143
143
  /** 发送者对象(id / name / role[])。 */
144
144
  readonly sender?: CommandSender;
145
145
  }
146
+ export interface CommandPromptOptions {
147
+ readonly timeout?: number;
148
+ readonly timeoutText?: string;
149
+ }
150
+ export interface CommandPromptListOptions extends CommandPromptOptions {
151
+ readonly type?: 'text' | 'number' | 'boolean';
152
+ readonly separator?: string;
153
+ readonly default?: readonly (string | number | boolean)[];
154
+ }
155
+ export interface CommandPromptPickOptions<V = unknown> extends CommandPromptOptions {
156
+ readonly options: readonly {
157
+ readonly label: string;
158
+ readonly value: V;
159
+ }[];
160
+ readonly multiple?: boolean;
161
+ readonly separator?: string;
162
+ readonly default?: V | readonly V[];
163
+ }
164
+ /**
165
+ * 命令内对话式交互输入。
166
+ *
167
+ * IM 派发时自动注入(`context.prompt`);Host / CLI 无消息来源时为 `undefined`。
168
+ *
169
+ * ```ts
170
+ * defineCommand({
171
+ * execute: async (context) => {
172
+ * const name = await context.prompt!.text('请输入你的名字');
173
+ * const age = await context.prompt!.number('请输入你的年龄');
174
+ * return `你好 ${name},你 ${age} 岁了`;
175
+ * },
176
+ * });
177
+ * ```
178
+ */
179
+ export interface CommandPrompt {
180
+ text(tips: string, options?: CommandPromptOptions & {
181
+ readonly default?: string;
182
+ }): Promise<string>;
183
+ number(tips: string, options?: CommandPromptOptions & {
184
+ readonly default?: number;
185
+ }): Promise<number>;
186
+ confirm(tips: string, options?: CommandPromptOptions & {
187
+ readonly condition?: string;
188
+ readonly default?: boolean;
189
+ }): Promise<boolean>;
190
+ list(tips: string, options?: CommandPromptListOptions): Promise<readonly (string | number | boolean)[]>;
191
+ pick<V = unknown>(tips: string, options: CommandPromptPickOptions<V>): Promise<V | readonly V[]>;
192
+ }
193
+ export type CommandPromptFactory = (source: unknown) => CommandPrompt | undefined;
146
194
  export interface CommandContext<TConfig = unknown, TInput extends CommandMessage = CommandMessage> extends CapabilityContext<TConfig>, CommandSession {
147
195
  readonly args: readonly string[];
148
196
  readonly params: Readonly<Record<string, CommandParameterValue>>;
@@ -153,6 +201,10 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
153
201
  * Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
154
202
  */
155
203
  readonly input?: TInput;
204
+ /**
205
+ * 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
206
+ */
207
+ readonly prompt?: CommandPrompt;
156
208
  }
157
209
  export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage> {
158
210
  readonly $feature: typeof commandBrand;
@@ -197,7 +249,7 @@ export declare function parseCommandDefinition(value: unknown): CommandDefinitio
197
249
  * 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
198
250
  */
199
251
  export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
200
- export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[]): CommandContext;
252
+ export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[], prompt?: CommandPrompt): CommandContext;
201
253
  /**
202
254
  * 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
203
255
  * 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
package/lib/definition.js CHANGED
@@ -112,7 +112,7 @@ export function resolveDynamicParams(params, source) {
112
112
  }
113
113
  return Object.freeze(resolved);
114
114
  }
115
- export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([])) {
115
+ export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), prompt) {
116
116
  const context = createCapabilityContext(snapshot, ownerId);
117
117
  const session = resolveCommandSession(input);
118
118
  return Object.freeze({
@@ -122,6 +122,7 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
122
122
  params: Object.freeze({ ...params }),
123
123
  segments: freezeSegments(segments),
124
124
  ...(input !== undefined ? { input: input } : {}),
125
+ ...(prompt !== undefined ? { prompt } : {}),
125
126
  });
126
127
  }
127
128
  /**
@@ -179,15 +180,10 @@ function resolveSender(input, metadata) {
179
180
  if (isCommandSender(structured)) {
180
181
  return freezeSender(structured);
181
182
  }
182
- const $sender = input.$sender;
183
- const id = input.sender?.id
184
- || (typeof $sender?.id === 'string' ? $sender.id : undefined)
185
- || firstString(metadata?.user_id, metadata?.userId);
183
+ const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
186
184
  if (!id)
187
185
  return undefined;
188
- const name = input.sender?.name
189
- || (typeof $sender?.name === 'string' ? $sender.name : undefined)
190
- || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
186
+ const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
191
187
  const role = resolveRoles(input, metadata);
192
188
  return Object.freeze({
193
189
  id,
@@ -207,19 +203,16 @@ function resolveRoles(input, metadata) {
207
203
  if (trimmed && !roles.includes(trimmed))
208
204
  roles.push(trimmed);
209
205
  };
210
- // IM Message 的 $sender 携带 enrich 快照(isMaster/isTrusted)和平台群角色(role)
211
- const $sender = input.$sender;
212
- if ($sender?.isMaster === true || metadata?.isMaster === true)
213
- push('master');
214
- else if ($sender?.isTrusted === true || metadata?.isTrusted === true)
215
- push('trusted');
216
- push($sender?.role);
217
206
  if (Array.isArray(metadata?.roles)) {
218
207
  for (const item of metadata.roles)
219
208
  push(item);
220
209
  }
221
210
  push(metadata?.senderRole);
222
211
  push(metadata?.role);
212
+ if (metadata?.isMaster === true)
213
+ push('master');
214
+ if (metadata?.isTrusted === true)
215
+ push('trusted');
223
216
  if (roles.length === 0)
224
217
  roles.push('user');
225
218
  return Object.freeze(roles);
package/lib/provider.js CHANGED
@@ -132,7 +132,12 @@ const commandFeature = defineFeatureProvider({
132
132
  },
133
133
  runtime: {
134
134
  project(slots, context) {
135
- return { value: new CommandIndex(slots, context.snapshot) };
135
+ const rootConfig = context.snapshot.config.get(context.snapshot.root);
136
+ const rawKeyword = rootConfig?.menuKeyword;
137
+ const menu = rawKeyword === false || rawKeyword === ''
138
+ ? undefined
139
+ : { keyword: typeof rawKeyword === 'string' ? rawKeyword : '菜单' };
140
+ return { value: new CommandIndex(slots, context.snapshot, menu) };
136
141
  },
137
142
  },
138
143
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Convention-based Command Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,9 +18,9 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "segment-matcher": "^1.0.5",
21
- "@zhin.js/feature-kit": "1.0.8",
22
- "@zhin.js/permission": "1.0.1",
23
- "@zhin.js/plugin-runtime": "1.1.5"
21
+ "@zhin.js/permission": "1.0.2",
22
+ "@zhin.js/plugin-runtime": "1.1.6",
23
+ "@zhin.js/feature-kit": "1.0.9"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.1.2",
@@ -17,6 +17,7 @@ import {
17
17
  type CommandParameterValue,
18
18
  type CommandSegment,
19
19
  type CommandDynamicValue,
20
+ type CommandPromptFactory,
20
21
  resolveDynamicParams,
21
22
  } from './definition.js';
22
23
  import { permissionHostToken, type PermissionHost } from '@zhin.js/permission';
@@ -83,16 +84,23 @@ const segmentFields = {
83
84
  rps: 'result',
84
85
  };
85
86
 
87
+ export interface CommandMenuConfig {
88
+ readonly keyword: string;
89
+ }
90
+
86
91
  export class CommandIndex {
87
92
  readonly $projection = 'zhin.command-index/1' as const;
88
93
  readonly #commands: readonly CommandRecord[];
89
94
  readonly #routes: readonly CommandRoute[];
90
95
  readonly #shortcuts: ReadonlyMap<string, ShortcutEntry>;
96
+ readonly #menu?: CommandMenuConfig;
91
97
 
92
98
  constructor(
93
99
  slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[],
94
100
  private readonly snapshot: RuntimeSnapshot,
101
+ menu?: CommandMenuConfig,
95
102
  ) {
103
+ this.#menu = menu;
96
104
  const commands: CommandRecord[] = [];
97
105
  const routes: CommandRoute[] = [];
98
106
  const occupancy = new Map<string, string>();
@@ -213,7 +221,20 @@ export class CommandIndex {
213
221
  async dispatch(
214
222
  input: CommandMatchInput,
215
223
  source: unknown = undefined,
224
+ promptFactory?: CommandPromptFactory,
216
225
  ): Promise<CommandDispatchResult> {
226
+ if (this.#menu) {
227
+ const menuValue = this.#dispatchMenu(input);
228
+ if (menuValue !== undefined) {
229
+ return Object.freeze({
230
+ matched: true,
231
+ command: this.#menu.keyword,
232
+ owner: this.snapshot.root,
233
+ value: menuValue,
234
+ });
235
+ }
236
+ }
237
+ const prompt = promptFactory?.(source);
217
238
  const shortcut = this.#matchShortcut(input);
218
239
  if (shortcut) {
219
240
  if (!(await this.#permitAllows(shortcut.record, source))) {
@@ -227,6 +248,7 @@ export class CommandIndex {
227
248
  resolveDynamicParams(shortcut.params, source),
228
249
  source,
229
250
  Object.freeze([]),
251
+ prompt,
230
252
  ),
231
253
  );
232
254
  return Object.freeze({
@@ -251,6 +273,7 @@ export class CommandIndex {
251
273
  resolveDynamicParams(match.params, source),
252
274
  source,
253
275
  match.remaining,
276
+ prompt,
254
277
  ),
255
278
  );
256
279
  return Object.freeze({
@@ -341,6 +364,72 @@ export class CommandIndex {
341
364
  throw new CommandParameterValueError(parameter.name, parameter.type, value);
342
365
  }
343
366
  }
367
+
368
+ // ==========================================================================
369
+ // 内置菜单命令
370
+ // ==========================================================================
371
+
372
+ #dispatchMenu(input: CommandMatchInput): string | undefined {
373
+ const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
374
+ if (text === undefined) return undefined;
375
+ const keyword = this.#menu!.keyword;
376
+ if (text === keyword) return this.#buildMenu();
377
+ if (text.startsWith(keyword + ' ')) {
378
+ const key = text.slice(keyword.length + 1).trim();
379
+ if (key) return this.#buildMenu(key);
380
+ return this.#buildMenu();
381
+ }
382
+ return undefined;
383
+ }
384
+
385
+ #buildMenu(pluginKey?: string): string {
386
+ const root = this.snapshot.root;
387
+ const targetId = pluginKey
388
+ ? `${root}/${pluginKey.split('.').join('/')}` as PluginId
389
+ : root;
390
+
391
+ const node = this.snapshot.tree.get(targetId);
392
+ if (!node) return `未找到插件: ${pluginKey}`;
393
+
394
+ const directCommands = this.#commands.filter((cmd) => cmd.slot.owner === targetId);
395
+ const children = node.children
396
+ .map((childId) => this.snapshot.tree.get(childId))
397
+ .filter((child): child is NonNullable<typeof child> => !!child);
398
+
399
+ const keyword = this.#menu!.keyword;
400
+ const displayKey = pluginKey ?? node.instanceKey;
401
+ const lines: string[] = [`=== ${displayKey} 指令菜单 ===`];
402
+
403
+ if (directCommands.length > 0) {
404
+ lines.push('');
405
+ for (const cmd of directCommands) {
406
+ const desc = cmd.description ? ` - ${cmd.description}` : '';
407
+ lines.push(` ${cmd.name}${desc}`);
408
+ }
409
+ }
410
+
411
+ if (children.length > 0) {
412
+ lines.push('');
413
+ lines.push('子插件:');
414
+ for (const child of children) {
415
+ const childKey = pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey;
416
+ const label = child.metadata?.displayName ?? child.instanceKey;
417
+ lines.push(` ${childKey} (${label})`);
418
+ }
419
+ }
420
+
421
+ if (directCommands.length === 0 && children.length === 0) {
422
+ lines.push('');
423
+ lines.push('(暂无指令和子插件)');
424
+ }
425
+
426
+ if (children.length > 0) {
427
+ lines.push('');
428
+ lines.push(`提示:使用「${keyword} <插件名>」查看子插件的指令`);
429
+ }
430
+
431
+ return lines.join('\n');
432
+ }
344
433
  }
345
434
 
346
435
  export function isCommandIndex(value: unknown): value is CommandIndex {
package/src/definition.ts CHANGED
@@ -193,6 +193,49 @@ export interface CommandSession {
193
193
  readonly sender?: CommandSender;
194
194
  }
195
195
 
196
+ export interface CommandPromptOptions {
197
+ readonly timeout?: number;
198
+ readonly timeoutText?: string;
199
+ }
200
+
201
+ export interface CommandPromptListOptions extends CommandPromptOptions {
202
+ readonly type?: 'text' | 'number' | 'boolean';
203
+ readonly separator?: string;
204
+ readonly default?: readonly (string | number | boolean)[];
205
+ }
206
+
207
+ export interface CommandPromptPickOptions<V = unknown> extends CommandPromptOptions {
208
+ readonly options: readonly { readonly label: string; readonly value: V }[];
209
+ readonly multiple?: boolean;
210
+ readonly separator?: string;
211
+ readonly default?: V | readonly V[];
212
+ }
213
+
214
+ /**
215
+ * 命令内对话式交互输入。
216
+ *
217
+ * IM 派发时自动注入(`context.prompt`);Host / CLI 无消息来源时为 `undefined`。
218
+ *
219
+ * ```ts
220
+ * defineCommand({
221
+ * execute: async (context) => {
222
+ * const name = await context.prompt!.text('请输入你的名字');
223
+ * const age = await context.prompt!.number('请输入你的年龄');
224
+ * return `你好 ${name},你 ${age} 岁了`;
225
+ * },
226
+ * });
227
+ * ```
228
+ */
229
+ export interface CommandPrompt {
230
+ text(tips: string, options?: CommandPromptOptions & { readonly default?: string }): Promise<string>;
231
+ number(tips: string, options?: CommandPromptOptions & { readonly default?: number }): Promise<number>;
232
+ confirm(tips: string, options?: CommandPromptOptions & { readonly condition?: string; readonly default?: boolean }): Promise<boolean>;
233
+ list(tips: string, options?: CommandPromptListOptions): Promise<readonly (string | number | boolean)[]>;
234
+ pick<V = unknown>(tips: string, options: CommandPromptPickOptions<V>): Promise<V | readonly V[]>;
235
+ }
236
+
237
+ export type CommandPromptFactory = (source: unknown) => CommandPrompt | undefined;
238
+
196
239
  export interface CommandContext<
197
240
  TConfig = unknown,
198
241
  TInput extends CommandMessage = CommandMessage,
@@ -206,6 +249,10 @@ export interface CommandContext<
206
249
  * Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
207
250
  */
208
251
  readonly input?: TInput;
252
+ /**
253
+ * 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
254
+ */
255
+ readonly prompt?: CommandPrompt;
209
256
  }
210
257
 
211
258
  export interface CommandDefinition<
@@ -371,6 +418,7 @@ export function createCommandContext(
371
418
  params: Readonly<Record<string, CommandParameterValue>> = Object.freeze({}),
372
419
  input: unknown = undefined,
373
420
  segments: readonly Readonly<CommandSegment>[] = Object.freeze([]),
421
+ prompt?: CommandPrompt,
374
422
  ): CommandContext {
375
423
  const context = createCapabilityContext(snapshot, ownerId);
376
424
  const session = resolveCommandSession(input);
@@ -381,6 +429,7 @@ export function createCommandContext(
381
429
  params: Object.freeze({ ...params }),
382
430
  segments: freezeSegments(segments),
383
431
  ...(input !== undefined ? { input: input as CommandMessage } : {}),
432
+ ...(prompt !== undefined ? { prompt } : {}),
384
433
  });
385
434
  }
386
435
 
@@ -458,15 +507,10 @@ function resolveSender(
458
507
  return freezeSender(structured);
459
508
  }
460
509
 
461
- const $sender = (input as { readonly $sender?: Readonly<Record<string, unknown>> }).$sender;
462
- const id = input.sender?.id
463
- || (typeof $sender?.id === 'string' ? $sender.id : undefined)
464
- || firstString(metadata?.user_id, metadata?.userId);
510
+ const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
465
511
  if (!id) return undefined;
466
512
 
467
- const name = input.sender?.name
468
- || (typeof $sender?.name === 'string' ? $sender.name : undefined)
469
- || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
513
+ const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
470
514
  const role = resolveRoles(input, metadata);
471
515
 
472
516
  return Object.freeze({
@@ -490,18 +534,13 @@ function resolveRoles(
490
534
  if (trimmed && !roles.includes(trimmed)) roles.push(trimmed);
491
535
  };
492
536
 
493
- // IM Message 的 $sender 携带 enrich 快照(isMaster/isTrusted)和平台群角色(role)
494
- const $sender = (input as { readonly $sender?: Readonly<Record<string, unknown>> }).$sender;
495
-
496
- if ($sender?.isMaster === true || metadata?.isMaster === true) push('master');
497
- else if ($sender?.isTrusted === true || metadata?.isTrusted === true) push('trusted');
498
-
499
- push($sender?.role);
500
537
  if (Array.isArray(metadata?.roles)) {
501
538
  for (const item of metadata.roles) push(item);
502
539
  }
503
540
  push(metadata?.senderRole);
504
541
  push(metadata?.role);
542
+ if (metadata?.isMaster === true) push('master');
543
+ if (metadata?.isTrusted === true) push('trusted');
505
544
  if (roles.length === 0) roles.push('user');
506
545
  return Object.freeze(roles);
507
546
  }
package/src/provider.ts CHANGED
@@ -183,7 +183,13 @@ const commandFeature = defineFeatureProvider({
183
183
  },
184
184
  runtime: {
185
185
  project(slots, context) {
186
- return { value: new CommandIndex(slots, context.snapshot) };
186
+ const rootConfig = context.snapshot.config.get(context.snapshot.root) as
187
+ | Record<string, unknown> | undefined;
188
+ const rawKeyword = rootConfig?.menuKeyword;
189
+ const menu = rawKeyword === false || rawKeyword === ''
190
+ ? undefined
191
+ : { keyword: typeof rawKeyword === 'string' ? rawKeyword : '菜单' };
192
+ return { value: new CommandIndex(slots, context.snapshot, menu) };
187
193
  },
188
194
  },
189
195
  });