@zhin.js/command 1.0.9 → 1.0.10

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 { SegmentMatcher, TypeMatcherRegistry, } from 'segment-matcher';
2
- import { createCommandContext, resolveCommandSession, } from './definition.js';
2
+ import { createCommandContext, resolveCommandSession, resolveDynamicParams, } from './definition.js';
3
3
  import { permissionHostToken } from '@zhin.js/permission';
4
4
  import { toPermissionSubject } from '@zhin.js/permission';
5
5
  const segmentFields = {
@@ -114,8 +114,8 @@ export class CommandIndex {
114
114
  this.#diagnoseParameter(name);
115
115
  throw new Error(`Unknown Command: ${name}`);
116
116
  }
117
- // Host / 无 session:跳过 permit。
118
- return match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, match.params));
117
+ // Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session
118
+ return match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, undefined)));
119
119
  }
120
120
  async dispatch(input, source = undefined) {
121
121
  const shortcut = this.#matchShortcut(input);
@@ -123,7 +123,7 @@ export class CommandIndex {
123
123
  if (!(await this.#permitAllows(shortcut.record, source))) {
124
124
  return Object.freeze({ matched: false });
125
125
  }
126
- const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), shortcut.params, source, Object.freeze([])));
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([])));
127
127
  return Object.freeze({
128
128
  matched: true,
129
129
  command: shortcut.record.name,
@@ -138,7 +138,7 @@ export class CommandIndex {
138
138
  return Object.freeze({ matched: false });
139
139
  }
140
140
  const args = textArgs(match.remaining);
141
- const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, match.params, source, 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));
142
142
  return Object.freeze({
143
143
  matched: true,
144
144
  command: match.command.name,
@@ -370,7 +370,7 @@ function matcherPattern(segments, parameter) {
370
370
  return `<${parameter.name}:${type}>`;
371
371
  return parameter.defaultValue === undefined
372
372
  ? `[${parameter.name}:${type}]`
373
- : `[${parameter.name}:${type}=${String(parameter.defaultValue)}]`;
373
+ : `[${parameter.name}:${type}=${typeof parameter.defaultValue === 'function' ? '<dynamic>' : String(parameter.defaultValue)}]`;
374
374
  }).join(' ');
375
375
  }
376
376
  function matcherType(type) {
@@ -3,6 +3,26 @@ import { type CapabilityContext } from '@zhin.js/feature-kit';
3
3
  declare const commandBrand: "zhin.command/1";
4
4
  export type CommandParameterType = 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'word' | 'text' | 'mention' | 'image' | 'face' | 'reply' | 'forward' | 'dice' | 'rps';
5
5
  export type CommandParameterValue = string | number | boolean | ReadonlyArray<string | number | boolean> | Readonly<Record<string, unknown>> | null;
6
+ /**
7
+ * 可从运行时会话上下文动态解析的参数值。
8
+ *
9
+ * 静态值直接使用;函数值在命令派发时接收 {@link CommandSession},
10
+ * 返回最终的 {@link CommandParameterValue}。适用于 shortcut 预填
11
+ * 和 params.default。
12
+ *
13
+ * ```ts
14
+ * defineCommand({
15
+ * params: {
16
+ * user_id: { type: 'string', default: (s) => s.sender?.id ?? '' },
17
+ * },
18
+ * shortcut: {
19
+ * '查看我的信息': { user_id: (s) => s.sender?.id ?? '' },
20
+ * },
21
+ * execute: ({ params }) => `profile:${params.user_id}`,
22
+ * })
23
+ * ```
24
+ */
25
+ export type CommandDynamicValue = CommandParameterValue | ((session: CommandSession) => CommandParameterValue);
6
26
  export declare const commandParameterTypes: ReadonlySet<CommandParameterType>;
7
27
  /**
8
28
  * Next.js 风格参数声明(`defineCommand({ params: ... })`)。
@@ -11,7 +31,7 @@ export declare const commandParameterTypes: ReadonlySet<CommandParameterType>;
11
31
  */
12
32
  export interface CommandParamSchema {
13
33
  readonly type: CommandParameterType;
14
- readonly default?: CommandParameterValue;
34
+ readonly default?: CommandDynamicValue;
15
35
  readonly description?: string;
16
36
  }
17
37
  /** Minimal structural contract shared with canonical IM segments. */
@@ -24,7 +44,7 @@ export interface CommandSegment {
24
44
  export interface CommandParameterDefinition {
25
45
  readonly name: string;
26
46
  readonly type: CommandParameterType;
27
- readonly defaultValue?: CommandParameterValue;
47
+ readonly defaultValue?: CommandDynamicValue;
28
48
  /** `[[name]]` / `[[...name]]` 可选段;缺省按 `defaultValue === undefined` 推断。 */
29
49
  readonly optional?: boolean;
30
50
  /** `[...name]` / `[[...name]]` 捕获所有段,运行时值为 `string[]`。 */
@@ -157,7 +177,7 @@ export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput
157
177
  * 全局整句快捷方式:触发串(trim 后全文相等)→ 预填 params。
158
178
  * 可打破 owner 命名空间。
159
179
  */
160
- readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>>;
180
+ readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
161
181
  execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
162
182
  }
163
183
  declare module '@zhin.js/plugin-runtime' {
@@ -172,6 +192,11 @@ declare module '@zhin.js/plugin-runtime' {
172
192
  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>>;
173
193
  export declare function bindCommandParameter<TConfig, TResult, TInput extends CommandMessage>(definition: CommandDefinition<TConfig, TResult, TInput>, parameter: CommandParameterDefinition | undefined): Readonly<CommandDefinition<TConfig, TResult, TInput>>;
174
194
  export declare function parseCommandDefinition(value: unknown): CommandDefinition;
195
+ /**
196
+ * 将动态参数值(可能包含函数)批量解析为静态值。
197
+ * 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
198
+ */
199
+ export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
175
200
  export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[]): CommandContext;
176
201
  /**
177
202
  * 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
package/lib/definition.js CHANGED
@@ -100,6 +100,18 @@ export function parseCommandDefinition(value) {
100
100
  }
101
101
  return definition;
102
102
  }
103
+ /**
104
+ * 将动态参数值(可能包含函数)批量解析为静态值。
105
+ * 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
106
+ */
107
+ export function resolveDynamicParams(params, source) {
108
+ const session = resolveCommandSession(source);
109
+ const resolved = {};
110
+ for (const [key, value] of Object.entries(params)) {
111
+ resolved[key] = typeof value === 'function' ? value(session) : value;
112
+ }
113
+ return Object.freeze(resolved);
114
+ }
103
115
  export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([])) {
104
116
  const context = createCapabilityContext(snapshot, ownerId);
105
117
  const session = resolveCommandSession(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Convention-based Command Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,8 +19,8 @@
19
19
  "dependencies": {
20
20
  "segment-matcher": "^1.0.5",
21
21
  "@zhin.js/feature-kit": "1.0.8",
22
- "@zhin.js/plugin-runtime": "1.1.5",
23
- "@zhin.js/permission": "1.0.1"
22
+ "@zhin.js/permission": "1.0.1",
23
+ "@zhin.js/plugin-runtime": "1.1.5"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.1.2",
@@ -16,6 +16,8 @@ import {
16
16
  type CommandParameterType,
17
17
  type CommandParameterValue,
18
18
  type CommandSegment,
19
+ type CommandDynamicValue,
20
+ resolveDynamicParams,
19
21
  } from './definition.js';
20
22
  import { permissionHostToken, type PermissionHost } from '@zhin.js/permission';
21
23
  import { toPermissionSubject } from '@zhin.js/permission';
@@ -57,12 +59,12 @@ interface CommandRoute {
57
59
 
58
60
  interface ShortcutEntry {
59
61
  readonly record: CommandRecord;
60
- readonly params: Readonly<Record<string, CommandParameterValue>>;
62
+ readonly params: Readonly<Record<string, CommandDynamicValue>>;
61
63
  }
62
64
 
63
65
  interface CommandMatch {
64
66
  readonly command: CommandRecord;
65
- readonly params: Readonly<Record<string, CommandParameterValue>>;
67
+ readonly params: Readonly<Record<string, CommandDynamicValue>>;
66
68
  readonly remaining: readonly Readonly<CommandSegment>[];
67
69
  }
68
70
 
@@ -197,13 +199,13 @@ export class CommandIndex {
197
199
  this.#diagnoseParameter(name);
198
200
  throw new Error(`Unknown Command: ${name}`);
199
201
  }
200
- // Host / 无 session:跳过 permit。
202
+ // Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session
201
203
  return match.command.slot.definition.execute(
202
204
  createCommandContext(
203
205
  this.snapshot,
204
206
  match.command.slot.owner,
205
207
  args,
206
- match.params,
208
+ resolveDynamicParams(match.params, undefined),
207
209
  ),
208
210
  );
209
211
  }
@@ -222,7 +224,7 @@ export class CommandIndex {
222
224
  this.snapshot,
223
225
  shortcut.record.slot.owner,
224
226
  Object.freeze([]),
225
- shortcut.params,
227
+ resolveDynamicParams(shortcut.params, source),
226
228
  source,
227
229
  Object.freeze([]),
228
230
  ),
@@ -246,7 +248,7 @@ export class CommandIndex {
246
248
  this.snapshot,
247
249
  match.command.slot.owner,
248
250
  args,
249
- match.params,
251
+ resolveDynamicParams(match.params, source),
250
252
  source,
251
253
  match.remaining,
252
254
  ),
@@ -302,7 +304,7 @@ export class CommandIndex {
302
304
  const result = route.matcher.match(asMatcherSegments(segments));
303
305
  if (!result || !hasCommandBoundary(result.remaining)) continue;
304
306
  const parameter = route.record.parameter;
305
- const params: Record<string, CommandParameterValue> = { ...result.params };
307
+ const params: Record<string, CommandDynamicValue> = { ...result.params };
306
308
  if (parameter?.rest) {
307
309
  const raw = result.params[parameter.name];
308
310
  const coerced = coerceRestValues(parameter, Array.isArray(raw) ? raw : []);
@@ -390,9 +392,9 @@ function normalizeAliasList(
390
392
 
391
393
  function resolveShortcutParams(
392
394
  definition: CommandDefinition,
393
- prefill: Readonly<Record<string, CommandParameterValue>>,
395
+ prefill: Readonly<Record<string, CommandDynamicValue>>,
394
396
  source: string,
395
- ): Record<string, CommandParameterValue> {
397
+ ): Record<string, CommandDynamicValue> {
396
398
  const allowed = new Set<string>();
397
399
  const parameter = definition.$parameter;
398
400
  if (parameter) allowed.add(parameter.name);
@@ -408,7 +410,7 @@ function resolveShortcutParams(
408
410
  }
409
411
  }
410
412
 
411
- const result: Record<string, CommandParameterValue> = { ...prefill };
413
+ const result: Record<string, CommandDynamicValue> = { ...prefill };
412
414
 
413
415
  if (parameter) {
414
416
  if (result[parameter.name] === undefined) {
@@ -508,7 +510,7 @@ function matcherPattern(
508
510
  if (isRequiredParameter(parameter)) return `<${parameter.name}:${type}>`;
509
511
  return parameter.defaultValue === undefined
510
512
  ? `[${parameter.name}:${type}]`
511
- : `[${parameter.name}:${type}=${String(parameter.defaultValue)}]`;
513
+ : `[${parameter.name}:${type}=${typeof parameter.defaultValue === 'function' ? '<dynamic>' : String(parameter.defaultValue)}]`;
512
514
  }).join(' ');
513
515
  }
514
516
 
package/src/definition.ts CHANGED
@@ -30,6 +30,29 @@ export type CommandParameterValue =
30
30
  | Readonly<Record<string, unknown>>
31
31
  | null;
32
32
 
33
+ /**
34
+ * 可从运行时会话上下文动态解析的参数值。
35
+ *
36
+ * 静态值直接使用;函数值在命令派发时接收 {@link CommandSession},
37
+ * 返回最终的 {@link CommandParameterValue}。适用于 shortcut 预填
38
+ * 和 params.default。
39
+ *
40
+ * ```ts
41
+ * defineCommand({
42
+ * params: {
43
+ * user_id: { type: 'string', default: (s) => s.sender?.id ?? '' },
44
+ * },
45
+ * shortcut: {
46
+ * '查看我的信息': { user_id: (s) => s.sender?.id ?? '' },
47
+ * },
48
+ * execute: ({ params }) => `profile:${params.user_id}`,
49
+ * })
50
+ * ```
51
+ */
52
+ export type CommandDynamicValue =
53
+ | CommandParameterValue
54
+ | ((session: CommandSession) => CommandParameterValue);
55
+
33
56
  export const commandParameterTypes: ReadonlySet<CommandParameterType> = new Set([
34
57
  'string',
35
58
  'number',
@@ -54,7 +77,7 @@ export const commandParameterTypes: ReadonlySet<CommandParameterType> = new Set(
54
77
  */
55
78
  export interface CommandParamSchema {
56
79
  readonly type: CommandParameterType;
57
- readonly default?: CommandParameterValue;
80
+ readonly default?: CommandDynamicValue;
58
81
  readonly description?: string;
59
82
  }
60
83
 
@@ -67,7 +90,7 @@ export interface CommandSegment {
67
90
  export interface CommandParameterDefinition {
68
91
  readonly name: string;
69
92
  readonly type: CommandParameterType;
70
- readonly defaultValue?: CommandParameterValue;
93
+ readonly defaultValue?: CommandDynamicValue;
71
94
  /** `[[name]]` / `[[...name]]` 可选段;缺省按 `defaultValue === undefined` 推断。 */
72
95
  readonly optional?: boolean;
73
96
  /** `[...name]` / `[[...name]]` 捕获所有段,运行时值为 `string[]`。 */
@@ -212,7 +235,7 @@ export interface CommandDefinition<
212
235
  * 全局整句快捷方式:触发串(trim 后全文相等)→ 预填 params。
213
236
  * 可打破 owner 命名空间。
214
237
  */
215
- readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>>;
238
+ readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
216
239
  execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
217
240
  }
218
241
 
@@ -286,7 +309,7 @@ function validateCommandPermit(permit: readonly string[] | undefined): void {
286
309
  }
287
310
 
288
311
  function validateCommandShortcutShape(
289
- shortcut: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>> | undefined,
312
+ shortcut: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>> | undefined,
290
313
  ): void {
291
314
  if (shortcut === undefined) return;
292
315
  if (!shortcut || typeof shortcut !== 'object' || Array.isArray(shortcut)) {
@@ -325,6 +348,22 @@ export function parseCommandDefinition(value: unknown): CommandDefinition {
325
348
  return definition as CommandDefinition;
326
349
  }
327
350
 
351
+ /**
352
+ * 将动态参数值(可能包含函数)批量解析为静态值。
353
+ * 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
354
+ */
355
+ export function resolveDynamicParams(
356
+ params: Readonly<Record<string, CommandDynamicValue>>,
357
+ source: unknown,
358
+ ): Readonly<Record<string, CommandParameterValue>> {
359
+ const session = resolveCommandSession(source);
360
+ const resolved: Record<string, CommandParameterValue> = {};
361
+ for (const [key, value] of Object.entries(params)) {
362
+ resolved[key] = typeof value === 'function' ? value(session) : value;
363
+ }
364
+ return Object.freeze(resolved);
365
+ }
366
+
328
367
  export function createCommandContext(
329
368
  snapshot: RuntimeSnapshot,
330
369
  ownerId: PluginId,