@zhin.js/command 1.1.10 → 1.1.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,8 +1,9 @@
1
- interface MenuEntry {
1
+ import type { Locale } from '@zhin.js/plugin-runtime';
2
+ export interface CommandMenuEntry {
2
3
  readonly name: string;
3
4
  readonly description?: string;
4
5
  }
5
- interface MenuAppearance {
6
+ export interface CommandMenuAppearance {
6
7
  readonly title?: string;
7
8
  readonly subtitle?: string;
8
9
  readonly accentColor?: string;
@@ -19,13 +20,13 @@ export interface CommandMenuSegment {
19
20
  }
20
21
  export declare const MENU_PAGE_SIZE = 30;
21
22
  /** Configuration is text/data, never an interpolated CSS or HTML fragment. */
22
- export declare function readMenuAppearance(value: unknown): MenuAppearance;
23
+ export declare function readMenuAppearance(value: unknown): CommandMenuAppearance;
23
24
  /** Presentation only: the unified outbound pipeline owns image rendering and fallback. */
24
- export declare function renderCommandMenu(title: string, commands: readonly MenuEntry[], children: readonly MenuEntry[], hint: string | undefined, text: string, options?: {
25
+ export declare function renderCommandMenu(title: string, commands: readonly CommandMenuEntry[], children: readonly CommandMenuEntry[], hint: string | undefined, text: string, options?: {
25
26
  readonly page?: number;
26
27
  readonly pages?: number;
28
+ readonly locale?: Locale;
27
29
  readonly commandTotal?: number;
28
30
  readonly pluginTotal?: number;
29
- readonly appearance?: MenuAppearance;
31
+ readonly appearance?: CommandMenuAppearance;
30
32
  }): CommandMenuSegment;
31
- export {};
@@ -1,3 +1,4 @@
1
+ import { commandMessages } from './messages.js';
1
2
  export const MENU_PAGE_SIZE = 30;
2
3
  const escape = (value) => value.replace(/[&<>"']/gu, char => ({
3
4
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
@@ -46,6 +47,7 @@ function icon(name, plugin = false) {
46
47
  }
47
48
  /** Presentation only: the unified outbound pipeline owns image rendering and fallback. */
48
49
  export function renderCommandMenu(title, commands, children, hint, text, options = {}) {
50
+ const locale = options.locale ?? 'zh-CN';
49
51
  const appearance = options.appearance ?? {};
50
52
  const background = appearance.backgroundImage ?? `data:image/svg+xml,${encodeURIComponent(landscape)}`;
51
53
  // CSS string escaping additionally prevents an artwork URL from closing <style>.
@@ -60,7 +62,7 @@ export function renderCommandMenu(title, commands, children, hint, text, options
60
62
  type: 'html',
61
63
  data: {
62
64
  width: 1080, fileName: 'command-menu.png', text,
63
- html: `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><style>
65
+ html: `<!doctype html><html lang="${locale}"><head><meta charset="utf-8"><style>
64
66
  *{box-sizing:border-box}body{margin:0;background:#253b46;color:#f4f0e6;font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:18px;line-height:1.5;--accent:${appearance.accentColor ?? '#e7c993'}}
65
67
  main{padding:42px 28px 24px;background-image:linear-gradient(180deg,#0c202a0a,#0c202a55),url(${cssUrl});background-size:cover;background-position:center top}
66
68
  header{min-height:210px;padding:10px 22px 30px;position:relative}.eyebrow{font-size:12px;letter-spacing:5px;color:#f4e4c4;margin:0 0 17px}
@@ -71,11 +73,11 @@ h2{margin:0;font-size:20px;font-weight:700;letter-spacing:1px;color:var(--accent
71
73
  .grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));background:#20363fdb;border-radius:0 0 13px 13px;overflow:hidden}
72
74
  .entry{display:flex;align-items:center;gap:12px;padding:19px 17px;min-width:0;min-height:106px;border-top:1px solid #e7e6d614;border-right:1px solid #e7e6d614;background:linear-gradient(120deg,#ffffff08,#ffffff00)}.entry:nth-child(3n){border-right:0}
73
75
  .icon{width:35px;height:35px;flex:0 0 35px;color:var(--accent);filter:drop-shadow(0 1px 6px #d9ba7c22)}.icon svg{display:block;width:100%;height:100%}.copy{min-width:0}.name{font-size:18px;font-weight:700;color:var(--accent)}.description{margin:4px 0 0;color:#e1e8e5;font-size:15px;line-height:1.45}
74
- .name,.description,h1{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere;white-space:pre-wrap}
76
+ .name{overflow-wrap:anywhere;white-space:pre-wrap}.description,h1{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere;white-space:pre-wrap}
75
77
  footer{margin:6px 5px 0;padding:15px 18px;display:flex;align-items:center;justify-content:space-between;gap:22px;background:#142a34da;border:1px solid #f0e2c71c;border-radius:13px}.hint{font-size:14px;color:#cfdbd7;white-space:pre-wrap;overflow-wrap:anywhere}.page{font-size:20px;color:var(--accent);white-space:nowrap;font-family:Georgia,serif}.signature{margin:16px 0 0;text-align:center;font-size:10px;letter-spacing:4px;color:#c4d0c899}.empty{margin:0 0 20px;padding:28px;background:#142a34da;border-radius:16px;color:#dbe4de}
76
- </style></head><body><main><header><div class="eyebrow">ZHIN / COMMAND ATLAS</div><h1>${escape(appearance.title || `${title} · 指令指南`)}</h1><div class="subtitle">${escape(appearance.subtitle ?? '每一个灵感,都有回应。')}</div><div class="summary"><i></i>${options.commandTotal ?? commands.length} 条指令<span>·</span>${options.pluginTotal ?? children.length} 个子插件<span>·</span>${escape(title)}</div></header>
77
- ${section('指令索引', commands, options.commandTotal ?? commands.length)}${section('插件导航', children, options.pluginTotal ?? children.length, true)}${commands.length || children.length ? '' : '<p class="empty">暂无指令和子插件</p>'}
78
- <footer><div class="hint">${escape(hint ?? '查看参数与用法:指令名 --help')}</div><div class="page">${String(page).padStart(2, '0')} / ${String(pages).padStart(2, '0')}</div></footer><div class="signature">ZHIN · MAKE EVERY MESSAGE MATTER</div></main></body></html>`,
78
+ </style></head><body><main><header><div class="eyebrow">ZHIN / COMMAND ATLAS</div><h1>${escape(appearance.title || commandMessages(locale, 'guideTitle', { title }))}</h1><div class="subtitle">${escape(appearance.subtitle ?? commandMessages(locale, 'guideSubtitle'))}</div><div class="summary"><i></i>${escape(commandMessages(locale, 'commandCount', { count: options.commandTotal ?? commands.length }))}<span>·</span>${escape(commandMessages(locale, 'pluginCount', { count: options.pluginTotal ?? children.length }))}<span>·</span>${escape(title)}</div></header>
79
+ ${section(commandMessages(locale, 'commandIndex'), commands, options.commandTotal ?? commands.length)}${section(commandMessages(locale, 'pluginIndex'), children, options.pluginTotal ?? children.length, true)}${commands.length || children.length ? '' : `<p class="empty">${commandMessages(locale, 'guideEmpty')}</p>`}
80
+ <footer><div class="hint">${escape(hint ?? commandMessages(locale, 'genericHelpHint'))}</div><div class="page">${String(page).padStart(2, '0')} / ${String(pages).padStart(2, '0')}</div></footer><div class="signature">ZHIN · MAKE EVERY MESSAGE MATTER</div></main></body></html>`,
79
81
  },
80
82
  };
81
83
  }
@@ -2,8 +2,8 @@
2
2
  * Command authoring API consumed from `zhin.js/command`.
3
3
  * @module zhin.js/command
4
4
  */
5
- import type { PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
6
- import { type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
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';
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';
@@ -41,6 +41,8 @@ export interface CommandParamSchema {
41
41
  readonly type: CommandParameterType;
42
42
  readonly default?: CommandDynamicValue;
43
43
  readonly description?: string;
44
+ /** Optional translated display text; machine keys and values remain unchanged. */
45
+ readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
44
46
  readonly optional?: boolean;
45
47
  readonly rest?: boolean;
46
48
  readonly min?: number;
@@ -95,6 +97,7 @@ export interface CommandParameterDefinition {
95
97
  /** `[...name]` / `[[...name]]` 捕获所有段,运行时值为 `string[]`。 */
96
98
  readonly rest?: boolean;
97
99
  readonly description?: string;
100
+ readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
98
101
  }
99
102
  /** 场景:群 / 私聊 / 频道等。 */
100
103
  export interface CommandScene {
@@ -138,6 +141,8 @@ export interface CommandConversation {
138
141
  export interface CommandMessage {
139
142
  readonly conversation: CommandConversation;
140
143
  readonly content: string;
144
+ /** Display preference resolved by the IM runtime, when this is an IM operation. */
145
+ readonly locale?: Locale;
141
146
  /** 发送者(结构化视图见 CommandContext.sender)。 */
142
147
  readonly sender?: {
143
148
  readonly id: string;
@@ -206,6 +211,16 @@ export interface CommandSession {
206
211
  * ```
207
212
  */
208
213
  export interface CommandContext<TConfig = unknown, TInput extends CommandMessage = CommandMessage, TAdapter extends string | undefined = undefined> extends CapabilityContext<TConfig>, CommandSession {
214
+ /** Language resolved once for the current operation. */
215
+ readonly locale: Locale;
216
+ /**
217
+ * Resolve a static local command path to its current chat invocation. Defaults to this
218
+ * plugin; an explicit owner may reference another indexed plugin.
219
+ * Includes the actual Endpoint prefix and configured commandNamespace. Arguments
220
+ * are appended explicitly by the caller. Returns undefined for unknown/disabled
221
+ * paths or calls without a known chat command environment.
222
+ */
223
+ formatCommand(localPath: string, owner?: PluginId): string | undefined;
209
224
  readonly args: readonly string[];
210
225
  readonly params: Readonly<Record<string, CommandParameterValue>>;
211
226
  readonly options: Readonly<Record<string, CommandParameterValue>>;
@@ -218,6 +233,8 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
218
233
  readonly input?: TInput;
219
234
  /** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
220
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;
221
238
  /**
222
239
  * 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
223
240
  */
@@ -230,6 +247,7 @@ export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput
230
247
  /** @internal Convention-derived parameter metadata. */
231
248
  readonly $parameter?: CommandParameterDefinition;
232
249
  readonly description?: string;
250
+ readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
233
251
  /** Restrict this command to one adapter and infer `context.$client`. */
234
252
  readonly adapter?: TAdapter;
235
253
  /**
@@ -243,6 +261,8 @@ export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput
243
261
  * 用户路由;alias 只替换文件路径提供的静态段。
244
262
  */
245
263
  readonly alias?: readonly string[];
264
+ /** Display an explicitly declared alias for this locale; every value must also occur in alias. */
265
+ readonly preferredAliasI18n?: Readonly<Partial<Record<Locale, string>>>;
246
266
  /**
247
267
  * 内置 permit DSL(AND)。单项内逗号为 OR。
248
268
  * 例:`adapter(icqq)`、`role(master)`、`group(123,456)`。
@@ -282,16 +302,29 @@ type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extend
282
302
  readonly params: keyof P extends never ? Readonly<Record<string, CommandParameterValue>> : InferCommandParams<P>;
283
303
  readonly options: InferCommandOptions<O>;
284
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
+ };
285
316
  /**
286
317
  * `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
287
318
  * 仍从定义推断。
288
319
  * @public
289
320
  */
290
- 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>>;
291
- 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'>>;
292
323
  export declare namespace defineCommand {
293
324
  var withConfig: <TConfig = unknown, TInput extends CommandMessage = CommandMessage>() => CommandDefiner<TConfig, TInput>;
294
325
  }
326
+ /** @internal Validate display-only translations without changing their fallback text. */
327
+ export declare function validateDescriptionI18n(value: CommandDefinition['descriptionI18n']): void;
295
328
  /** @internal Convention-loader assembly helper. */
296
329
  export declare function bindCommandParameter<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined>(definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>, parameter: CommandParameterDefinition | undefined): Readonly<CommandDefinition<TConfig, TResult, TInput, TAdapter>>;
297
330
  /** @internal Runtime validation for convention-discovered modules. */
@@ -302,8 +335,27 @@ export declare function parseCommandDefinition(value: unknown): CommandDefinitio
302
335
  * @internal Command projection helper.
303
336
  */
304
337
  export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
305
- /** @internal Command dispatcher assembly helper. */
306
- 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>>): 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;
307
359
  /**
308
360
  * 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
309
361
  * 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
package/lib/definition.js CHANGED
@@ -1,4 +1,9 @@
1
- import { createCapabilityContext, readOperationClient, readOperationInputSignal, } from '@zhin.js/feature-kit';
1
+ /**
2
+ * Command authoring API consumed from `zhin.js/command`.
3
+ * @module zhin.js/command
4
+ */
5
+ import { resolveLocale } from '@zhin.js/plugin-runtime';
6
+ import { createCapabilityContext, readOperationClient, readOperationSession, readOperationInputSignal, } from '@zhin.js/feature-kit';
2
7
  import { assertPermitSyntax } from '@zhin.js/permission';
3
8
  import { validateArgumentSchemas } from './arguments.js';
4
9
  import { normalizeExecutionPolicy } from './execution-policy.js';
@@ -37,12 +42,23 @@ export function defineCommand(definition) {
37
42
  }
38
43
  }
39
44
  validateArgumentSchemas(definition.params, definition.options);
45
+ validateDescriptionI18n(definition.descriptionI18n);
40
46
  validateCommandAlias(definition.alias);
47
+ validatePreferredAliases(definition);
41
48
  validateCommandPermit(definition.permit);
42
49
  validateCommandShortcutShape(definition.shortcut);
43
50
  const execution = normalizeExecutionPolicy(definition.execution);
44
51
  return Object.freeze({ $feature: commandBrand, ...definition, ...(execution ? { execution } : {}) });
45
52
  }
53
+ /** @internal Validate display-only translations without changing their fallback text. */
54
+ export function validateDescriptionI18n(value) {
55
+ if (value === undefined)
56
+ return;
57
+ if (!value || typeof value !== 'object' || Array.isArray(value)
58
+ || Object.entries(value).some(([locale, text]) => !['zh-CN', 'en'].includes(locale) || typeof text !== 'string')) {
59
+ throw new TypeError('Command descriptionI18n must map zh-CN/en to strings');
60
+ }
61
+ }
46
62
  /**
47
63
  * 先固定插件 config 类型,再定义命令;params / options / 返回值仍从定义推断。
48
64
  *
@@ -66,6 +82,19 @@ function validateAdapterName(adapter) {
66
82
  throw new TypeError('Command adapter must be a non-empty string');
67
83
  }
68
84
  }
85
+ function validatePreferredAliases(definition) {
86
+ const preferred = definition.preferredAliasI18n;
87
+ if (preferred === undefined)
88
+ return;
89
+ validateDescriptionI18n(preferred);
90
+ const normalize = (value) => value.trim().split(/\s+/u).filter(Boolean).join(' ');
91
+ const aliases = new Set(definition.alias?.map(normalize));
92
+ for (const [locale, alias] of Object.entries(preferred)) {
93
+ if (!aliases.has(normalize(alias))) {
94
+ throw new TypeError(`Command preferredAliasI18n.${locale} must reference a declared alias`);
95
+ }
96
+ }
97
+ }
69
98
  function validateCommandAlias(alias) {
70
99
  if (alias === undefined)
71
100
  return;
@@ -127,6 +156,8 @@ export function parseCommandDefinition(value) {
127
156
  }
128
157
  validateAdapterName(definition.adapter);
129
158
  normalizeExecutionPolicy(definition.execution);
159
+ validateCommandAlias(definition.alias);
160
+ validatePreferredAliases(definition);
130
161
  return definition;
131
162
  }
132
163
  /**
@@ -142,8 +173,10 @@ export function resolveDynamicParams(params, source) {
142
173
  }
143
174
  return Object.freeze(resolved);
144
175
  }
145
- /** @internal Command dispatcher assembly helper. */
146
- export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), interaction, adapter, options = Object.freeze({})) {
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;
147
180
  const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
148
181
  const session = resolveCommandSession(input);
149
182
  const parameterValues = { __proto__: null, ...params };
@@ -151,6 +184,8 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
151
184
  const result = {
152
185
  ...context,
153
186
  ...session,
187
+ locale: presentation?.locale ?? resolveLocale(snapshot.config.get(snapshot.root)?.locale),
188
+ formatCommand: presentation?.formatCommand ?? (() => undefined),
154
189
  args: Object.freeze([...args]),
155
190
  params: Object.freeze(parameterValues),
156
191
  options: Object.freeze(optionValues),
@@ -158,6 +193,10 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
158
193
  ...(input !== undefined ? { input: input } : {}),
159
194
  ...(interaction !== undefined ? { interaction } : {}),
160
195
  };
196
+ Object.defineProperty(result, 'session', {
197
+ enumerable: true,
198
+ get: () => readOperationSession(input, adapter),
199
+ });
161
200
  Object.defineProperty(result, '$client', {
162
201
  enumerable: true,
163
202
  get: () => readOperationClient(input, adapter),
@@ -170,6 +209,20 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
170
209
  * @internal Command dispatcher projection helper.
171
210
  */
172
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
+ }
173
226
  if (!isCommandMessageLike(input))
174
227
  return Object.freeze({});
175
228
  const metadata = input.metadata && typeof input.metadata === 'object'
package/lib/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export * from './permission-explanation.js';
10
10
  export { CONVERSATION_MANAGER_PERMIT, canManageConversation, assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, isBuiltinPermit, isPlatformPermit, parsePermitName, type ParsedPermit, type PermitKind, } from './permit.js';
11
11
  export { bindCommandSource, CommandPathSyntaxError, commandFeatureId, type BoundCommandSource, default as commandFeature, } from './provider.js';
12
12
  export { default } from './provider.js';
13
- export type { CommandValidationIssue } from './arguments.js';
13
+ export type { CommandValidationIssue, CommandValidationIssueCode } from './arguments.js';
@@ -0,0 +1,8 @@
1
+ import { type Locale } from '@zhin.js/plugin-runtime';
2
+ /** Translates only display metadata explicitly supplied by its author. */
3
+ export declare function commandDescription(value: Readonly<{
4
+ description?: string;
5
+ descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
6
+ }>, locale: Locale): string | undefined;
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;
@@ -0,0 +1,57 @@
1
+ import { createMessages } from '@zhin.js/plugin-runtime';
2
+ /** Translates only display metadata explicitly supplied by its author. */
3
+ export function commandDescription(value, locale) {
4
+ return value.descriptionI18n?.[locale] ?? value.description;
5
+ }
6
+ /** Static dictionaries; the caller supplies the operation's language. */
7
+ export const commandMessages = createMessages({
8
+ menuKeyword: { 'zh-CN': '菜单', en: 'menu' },
9
+ invalidPage: { 'zh-CN': '页码必须是正整数。用法:{command} [插件名] --page <页码>', en: 'The page must be a positive integer. Usage: {command} [plugin] --page <page>' },
10
+ missingPlugin: { 'zh-CN': '未找到插件: {plugin}', en: 'Plugin not found: {plugin}' },
11
+ pageOutOfRange: { 'zh-CN': '页码超出范围:共 {pages} 页。请使用「{command} --page <页码>」。', en: 'Page out of range: {pages} pages available. Use {command} --page <page>.' },
12
+ menuTitle: { 'zh-CN': '=== {plugin} 指令菜单 ===', en: '=== {plugin} command menu ===' },
13
+ menuSummary: { 'zh-CN': '第 {page}/{pages} 页 · {commands} 条指令 · {plugins} 个子插件', en: 'Page {page}/{pages} · Commands: {commands} · Plugins: {plugins}' },
14
+ childPlugins: { 'zh-CN': '子插件:', en: 'Plugins:' },
15
+ emptyMenu: { 'zh-CN': '(暂无指令和子插件)', en: '(No commands or plugins)' },
16
+ pluginHint: { 'zh-CN': '提示:使用「{command} <插件名>」查看子插件的指令', en: 'Use {command} <plugin> to view a plugin’s commands.' },
17
+ previousPage: { 'zh-CN': '上一页:{command}', en: 'Previous page: {command}' },
18
+ nextPage: { 'zh-CN': '下一页:{command}', en: 'Next page: {command}' },
19
+ guideTitle: { 'zh-CN': '{title} · 指令指南', en: '{title} · Command guide' },
20
+ guideSubtitle: { 'zh-CN': '每一个灵感,都有回应。', en: 'An answer to every idea.' },
21
+ commandCount: { 'zh-CN': '{count} 条指令', en: 'Commands: {count}' },
22
+ pluginCount: { 'zh-CN': '{count} 个子插件', en: 'Plugins: {count}' },
23
+ commandIndex: { 'zh-CN': '指令索引', en: 'Commands' },
24
+ pluginIndex: { 'zh-CN': '插件导航', en: 'Plugins' },
25
+ guideEmpty: { 'zh-CN': '暂无指令和子插件', en: 'No commands or plugins' },
26
+ helpHint: { 'zh-CN': '查看参数与用法:{command} --help', en: 'View arguments and usage: {command} --help' },
27
+ genericHelpHint: { 'zh-CN': '在当前环境的指令后追加 --help 查看用法', en: 'Append --help to a command in this environment to view its usage' },
28
+ expectedType: { 'zh-CN': '{field}:应为 {type}', en: '{field}: expected {type}' },
29
+ expectedArray: { 'zh-CN': '{field}:应为 {type} 数组', en: '{field}: expected an array of {type}' },
30
+ minimum: { 'zh-CN': '{field}:最小值为 {minimum}', en: '{field}: minimum {minimum}' },
31
+ maximum: { 'zh-CN': '{field}:最大值为 {maximum}', en: '{field}: maximum {maximum}' },
32
+ choices: { 'zh-CN': '{field}:可选值为 {choices}', en: '{field}: expected one of {choices}' },
33
+ invalidFormat: { 'zh-CN': '{field}:格式不正确', en: '{field}: invalid format' },
34
+ invalidValue: { 'zh-CN': '{field}:值不正确', en: '{field}: invalid value' },
35
+ unknownOption: { 'zh-CN': '未知选项 {field}', en: 'Unknown option {field}' },
36
+ duplicateOption: { 'zh-CN': '重复选项 --{field}', en: 'Duplicate option --{field}' },
37
+ defaultUndefined: { 'zh-CN': '{field}:默认值返回了 undefined', en: '{field}: default returned undefined' },
38
+ parameterUndefined: { 'zh-CN': '{field}:默认值或快捷指令返回了 undefined', en: '{field}: default or shortcut returned undefined' },
39
+ missingOption: { 'zh-CN': '缺少选项 --{field}', en: 'Missing option --{field}' },
40
+ unexpectedArguments: { 'zh-CN': '存在多余参数', en: 'Unexpected arguments' },
41
+ invalidParameter: { 'zh-CN': '缺少指令参数或参数不正确', en: 'Missing or invalid command parameter' },
42
+ defaultValue: { 'zh-CN': '默认值:{value}', en: 'default: {value}' },
43
+ dynamicValue: { 'zh-CN': '动态计算', en: 'dynamic' },
44
+ noAdapter: { 'zh-CN': '命令未声明 adapter 约束', en: 'The command does not declare an adapter restriction' },
45
+ missingEndpoint: { 'zh-CN': '未选择当前 Endpoint,无法判断命令的精确 adapter 约束', en: 'Select an Endpoint to evaluate the command’s exact adapter restriction' },
46
+ adapterComparison: { 'zh-CN': '命令 adapter 与当前 Endpoint 声明的原生适配器精确比较;未探测在线状态', en: 'Exact comparison with the Endpoint’s declared native adapter; online status was not checked' },
47
+ conditionMismatch: { 'zh-CN': '至少一个声明条件与假设输入不符;这不是实际准入检查', en: 'At least one declared condition does not match the hypothetical input; this is not a live admission check' },
48
+ missingPermissionHost: { 'zh-CN': 'PermissionHost 缺失或无法确认;不能推断命令可执行', en: 'PermissionHost is missing or unverified; command execution cannot be inferred' },
49
+ missingEvidence: { 'zh-CN': '存在缺少依据的条件,不能推断命令可执行', en: 'Some conditions lack evidence; command execution cannot be inferred' },
50
+ noPermit: { 'zh-CN': '命令未声明 permit 限制;这不表示已获得执行权限', en: 'The command does not declare permit restrictions; this does not establish authorization' },
51
+ uncheckedConditions: { 'zh-CN': '存在未执行的平台或自定义条件,不能推断命令可执行', en: 'Platform or custom conditions have not been evaluated; command execution cannot be inferred' },
52
+ conditionsMatch: { 'zh-CN': '已解释的 builtin 条件与假设输入匹配;不代表真实授权或命令可执行', en: 'The explained builtin conditions match the hypothetical input; this does not establish authorization or executability' },
53
+ unconstrained: { 'zh-CN': '已解释的 builtin 规则不检查具体 Endpoint、parent 或 thread;保留完整目标不等于这些维度受规则限制', en: 'The explained builtin rules do not constrain a specific Endpoint, parent or thread; retaining the full target does not add such restrictions' },
54
+ simulatedIdentity: { 'zh-CN': '身份与角色仅用于模拟,未向平台核实', en: 'Identity and roles are hypothetical and have not been verified with the platform' },
55
+ unevaluatedCode: { 'zh-CN': '未执行命令、动态默认值、平台或自定义 checker', en: 'Commands, dynamic defaults, platform checks and custom checkers have not been executed' },
56
+ defaultedRole: { 'zh-CN': 'canonical 输入未附角色时按命令运行时默认 user 解释;这不是已核验的平台角色', en: 'Canonical input without roles uses the command runtime’s default user role; this is not a verified platform role' },
57
+ });
@@ -1,5 +1,6 @@
1
1
  import { type PermissionSubject, type PermissionConditionExplanation } from '@zhin.js/permission';
2
2
  import type { CommandDescriptor } from './command-index.js';
3
+ import { type Locale } from '@zhin.js/plugin-runtime';
3
4
  /** The exact subject projection used by the real command permit gate. No factories or checkers run. */
4
5
  export declare function toCommandPermissionSubject(source: unknown): PermissionSubject;
5
6
  /** Exact authoring adapter restriction, distinct from builtin adapter(...) matching. */
@@ -12,6 +13,7 @@ export declare function explainCommandPermissions(command: Pick<CommandDescripto
12
13
  source?: unknown;
13
14
  rolesKnown?: boolean;
14
15
  permissionHost: CommandPermissionHostStatus;
16
+ locale?: Locale;
15
17
  }>): Readonly<{
16
18
  permissionHost: CommandPermissionHostStatus;
17
19
  adapter: Readonly<PermissionConditionExplanation>;
@@ -1,6 +1,8 @@
1
1
  import { operationClientAdapter } from '@zhin.js/feature-kit';
2
2
  import { explainPermitRules, toPermissionSubject } from '@zhin.js/permission';
3
3
  import { resolveCommandSender, resolveCommandSession } from './definition.js';
4
+ import { resolveLocale } from '@zhin.js/plugin-runtime';
5
+ import { commandMessages } from './messages.js';
4
6
  /** The exact subject projection used by the real command permit gate. No factories or checkers run. */
5
7
  export function toCommandPermissionSubject(source) {
6
8
  const projected = toPermissionSubject(source);
@@ -13,6 +15,7 @@ export function commandAdapterMatches(command, source) {
13
15
  }
14
16
  /** Read-only hypothetical conditions. A match is not permission to execute the command. */
15
17
  export function explainCommandPermissions(command, options) {
18
+ const locale = resolveLocale(options.locale);
16
19
  const projected = toCommandPermissionSubject(options.source);
17
20
  const inputSender = options.source && typeof options.source === 'object'
18
21
  ? options.source.sender : undefined;
@@ -27,28 +30,28 @@ export function explainCommandPermissions(command, options) {
27
30
  && subject.sender?.role.length === 1 && subject.sender.role[0] === 'user';
28
31
  const actualAdapter = operationClientAdapter(options.source);
29
32
  const adapter = !command.adapter
30
- ? { outcome: 'not_evaluated', reason: '命令未声明 adapter 约束' }
31
- : !actualAdapter ? { outcome: 'unknown', reason: '未选择当前 Endpoint,无法判断命令的精确 adapter 约束' }
32
- : { outcome: commandAdapterMatches(command, options.source) ? 'match' : 'mismatch', reason: '命令 adapter 与当前 Endpoint 声明的原生适配器精确比较;未探测在线状态' };
33
- const rules = explainPermitRules(command.permit ?? [], subject, { rolesKnown: options.rolesKnown });
33
+ ? { outcome: 'not_evaluated', reason: commandMessages(locale, 'noAdapter') }
34
+ : !actualAdapter ? { outcome: 'unknown', reason: commandMessages(locale, 'missingEndpoint') }
35
+ : { outcome: commandAdapterMatches(command, options.source) ? 'match' : 'mismatch', reason: commandMessages(locale, 'adapterComparison') };
36
+ const rules = explainPermitRules(command.permit ?? [], subject, { rolesKnown: options.rolesKnown, locale });
34
37
  const conditions = [...(command.adapter ? [adapter] : []), ...rules];
35
38
  let summary;
36
39
  if (conditions.some(item => item.outcome === 'mismatch'))
37
- summary = { outcome: 'mismatch', reason: '至少一个声明条件与假设输入不符;这不是实际准入检查' };
40
+ summary = { outcome: 'mismatch', reason: commandMessages(locale, 'conditionMismatch') };
38
41
  else if ((rules.length > 0 && options.permissionHost !== 'present') || conditions.some(item => item.outcome === 'unknown')) {
39
42
  summary = { outcome: 'unknown', reason: rules.length > 0 && options.permissionHost !== 'present'
40
- ? 'PermissionHost 缺失或无法确认;不能推断命令可执行' : '存在缺少依据的条件,不能推断命令可执行' };
43
+ ? commandMessages(locale, 'missingPermissionHost') : commandMessages(locale, 'missingEvidence') };
41
44
  }
42
45
  else if (rules.length === 0)
43
- summary = { outcome: 'not_evaluated', reason: '命令未声明 permit 限制;这不表示已获得执行权限' };
46
+ summary = { outcome: 'not_evaluated', reason: commandMessages(locale, 'noPermit') };
44
47
  else if (conditions.some(item => item.outcome === 'not_evaluated'))
45
- summary = { outcome: 'not_evaluated', reason: '存在未执行的平台或自定义条件,不能推断命令可执行' };
48
+ summary = { outcome: 'not_evaluated', reason: commandMessages(locale, 'uncheckedConditions') };
46
49
  else
47
- summary = { outcome: 'match', reason: '已解释的 builtin 条件与假设输入匹配;不代表真实授权或命令可执行' };
50
+ summary = { outcome: 'match', reason: commandMessages(locale, 'conditionsMatch') };
48
51
  return Object.freeze({ permissionHost: options.permissionHost, adapter: Object.freeze(adapter), rules, summary: Object.freeze(summary),
49
- unconstrained: Object.freeze(['已解释的 builtin 规则不检查具体 Endpoint、parent 或 thread;保留完整目标不等于这些维度受规则限制']),
50
- assumptions: Object.freeze(['身份与角色仅用于模拟,未向平台核实', '未执行命令、动态默认值、平台或自定义 checker',
52
+ unconstrained: Object.freeze([commandMessages(locale, 'unconstrained')]),
53
+ assumptions: Object.freeze([commandMessages(locale, 'simulatedIdentity'), commandMessages(locale, 'unevaluatedCode'),
51
54
  ...(defaultedRoles
52
- ? ['canonical 输入未附角色时按命令运行时默认 user 解释;这不是已核验的平台角色'] : [])]),
55
+ ? [commandMessages(locale, 'defaultedRole')] : [])]),
53
56
  });
54
57
  }
package/lib/provider.js CHANGED
@@ -223,7 +223,7 @@ const commandFeature = defineFeatureProvider({
223
223
  const rawKeyword = rootConfig?.menuKeyword;
224
224
  const menu = rawKeyword === false || rawKeyword === ''
225
225
  ? undefined
226
- : { keyword: typeof rawKeyword === 'string' ? rawKeyword : '菜单' };
226
+ : typeof rawKeyword === 'string' ? { keyword: rawKeyword } : {};
227
227
  return { value: new CommandIndex(slots, context.snapshot, menu) };
228
228
  },
229
229
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.1.10",
3
+ "version": "1.1.12",
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.8",
24
+ "@zhin.js/feature-kit": "1.1.10",
25
25
  "@zhin.js/interaction": "1.1.2",
26
- "@zhin.js/permission": "1.1.8",
27
- "@zhin.js/plugin-runtime": "1.1.16"
26
+ "@zhin.js/permission": "1.1.10",
27
+ "@zhin.js/plugin-runtime": "1.1.18"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.1.2",