@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.
- package/README.md +22 -2
- package/lib/arguments.d.ts +8 -2
- package/lib/arguments.js +40 -27
- package/lib/command-index.d.ts +33 -5
- package/lib/command-index.js +95 -43
- package/lib/command-menu.d.ts +7 -6
- package/lib/command-menu.js +7 -5
- package/lib/definition.d.ts +58 -6
- package/lib/definition.js +56 -3
- package/lib/index.d.ts +1 -1
- package/lib/messages.d.ts +8 -0
- package/lib/messages.js +57 -0
- package/lib/permission-explanation.d.ts +2 -0
- package/lib/permission-explanation.js +15 -12
- package/lib/provider.js +1 -1
- package/package.json +4 -4
- package/src/arguments.ts +54 -26
- package/src/command-index.ts +139 -54
- package/src/command-menu.ts +17 -13
- package/src/definition.ts +113 -20
- package/src/index.ts +1 -1
- package/src/messages.ts +62 -0
- package/src/permission-explanation.ts +16 -13
- package/src/provider.ts +1 -1
package/src/command-menu.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Locale } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { commandMessages } from './messages.js';
|
|
3
|
+
|
|
4
|
+
export interface CommandMenuEntry {
|
|
2
5
|
readonly name: string;
|
|
3
6
|
readonly description?: string;
|
|
4
7
|
}
|
|
5
8
|
|
|
6
|
-
interface
|
|
9
|
+
export interface CommandMenuAppearance {
|
|
7
10
|
readonly title?: string;
|
|
8
11
|
readonly subtitle?: string;
|
|
9
12
|
readonly accentColor?: string;
|
|
@@ -27,7 +30,7 @@ const escape = (value: string) => value.replace(/[&<>"']/gu, char => ({
|
|
|
27
30
|
})[char] ?? char);
|
|
28
31
|
|
|
29
32
|
/** Configuration is text/data, never an interpolated CSS or HTML fragment. */
|
|
30
|
-
export function readMenuAppearance(value: unknown):
|
|
33
|
+
export function readMenuAppearance(value: unknown): CommandMenuAppearance {
|
|
31
34
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
32
35
|
const input = value as Record<string, unknown>;
|
|
33
36
|
const result: { title?: string; subtitle?: string; accentColor?: string; backgroundImage?: string } = {};
|
|
@@ -69,20 +72,21 @@ function icon(name: string, plugin = false): string {
|
|
|
69
72
|
/** Presentation only: the unified outbound pipeline owns image rendering and fallback. */
|
|
70
73
|
export function renderCommandMenu(
|
|
71
74
|
title: string,
|
|
72
|
-
commands: readonly
|
|
73
|
-
children: readonly
|
|
75
|
+
commands: readonly CommandMenuEntry[],
|
|
76
|
+
children: readonly CommandMenuEntry[],
|
|
74
77
|
hint: string | undefined,
|
|
75
78
|
text: string,
|
|
76
|
-
options: { readonly page?: number; readonly pages?: number; readonly commandTotal?: number; readonly pluginTotal?: number; readonly appearance?:
|
|
79
|
+
options: { readonly page?: number; readonly pages?: number; readonly locale?: Locale; readonly commandTotal?: number; readonly pluginTotal?: number; readonly appearance?: CommandMenuAppearance } = {},
|
|
77
80
|
): CommandMenuSegment {
|
|
81
|
+
const locale = options.locale ?? 'zh-CN';
|
|
78
82
|
const appearance = options.appearance ?? {};
|
|
79
83
|
const background = appearance.backgroundImage ?? `data:image/svg+xml,${encodeURIComponent(landscape)}`;
|
|
80
84
|
// CSS string escaping additionally prevents an artwork URL from closing <style>.
|
|
81
85
|
const cssUrl = JSON.stringify(background).replace(/</gu, '\\3c ').replace(/>/gu, '\\3e ');
|
|
82
|
-
const entries = (items: readonly
|
|
86
|
+
const entries = (items: readonly CommandMenuEntry[], plugin: boolean) => items.map(item =>
|
|
83
87
|
`<div class="entry"><div class="icon">${icon(item.name, plugin)}</div><div class="copy"><div class="name">${escape(item.name)}</div>${item.description
|
|
84
88
|
? `<div class="description">${escape(item.description)}</div>` : ''}</div></div>`).join('');
|
|
85
|
-
const section = (label: string, items: readonly
|
|
89
|
+
const section = (label: string, items: readonly CommandMenuEntry[], total: number, plugin = false) => items.length === 0 ? ''
|
|
86
90
|
: `<section><div class="section-head"><h2>${label}</h2><span>${items.length} / ${total}</span></div><div class="grid">${entries(items, plugin)}</div></section>`;
|
|
87
91
|
const page = options.page ?? 1;
|
|
88
92
|
const pages = options.pages ?? 1;
|
|
@@ -91,7 +95,7 @@ export function renderCommandMenu(
|
|
|
91
95
|
type: 'html',
|
|
92
96
|
data: {
|
|
93
97
|
width: 1080, fileName: 'command-menu.png', text,
|
|
94
|
-
html: `<!doctype html><html lang="
|
|
98
|
+
html: `<!doctype html><html lang="${locale}"><head><meta charset="utf-8"><style>
|
|
95
99
|
*{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'}}
|
|
96
100
|
main{padding:42px 28px 24px;background-image:linear-gradient(180deg,#0c202a0a,#0c202a55),url(${cssUrl});background-size:cover;background-position:center top}
|
|
97
101
|
header{min-height:210px;padding:10px 22px 30px;position:relative}.eyebrow{font-size:12px;letter-spacing:5px;color:#f4e4c4;margin:0 0 17px}
|
|
@@ -102,11 +106,11 @@ h2{margin:0;font-size:20px;font-weight:700;letter-spacing:1px;color:var(--accent
|
|
|
102
106
|
.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));background:#20363fdb;border-radius:0 0 13px 13px;overflow:hidden}
|
|
103
107
|
.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}
|
|
104
108
|
.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}
|
|
105
|
-
.name
|
|
109
|
+
.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}
|
|
106
110
|
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}
|
|
107
|
-
</style></head><body><main><header><div class="eyebrow">ZHIN / COMMAND ATLAS</div><h1>${escape(appearance.title ||
|
|
108
|
-
${section('
|
|
109
|
-
<footer><div class="hint">${escape(hint ??
|
|
111
|
+
</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>
|
|
112
|
+
${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>`}
|
|
113
|
+
<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>`,
|
|
110
114
|
},
|
|
111
115
|
};
|
|
112
116
|
}
|
package/src/definition.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Command authoring API consumed from `zhin.js/command`.
|
|
3
3
|
* @module zhin.js/command
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
5
|
+
import { resolveLocale, type Locale, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
6
6
|
import {
|
|
7
7
|
createCapabilityContext,
|
|
8
8
|
readOperationClient,
|
|
9
|
+
readOperationSession,
|
|
10
|
+
type OperationSession,
|
|
9
11
|
readOperationInputSignal,
|
|
10
12
|
type AdapterClient,
|
|
11
13
|
type RegisteredAdapterName,
|
|
@@ -92,6 +94,8 @@ export interface CommandParamSchema {
|
|
|
92
94
|
readonly type: CommandParameterType;
|
|
93
95
|
readonly default?: CommandDynamicValue;
|
|
94
96
|
readonly description?: string;
|
|
97
|
+
/** Optional translated display text; machine keys and values remain unchanged. */
|
|
98
|
+
readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
95
99
|
readonly optional?: boolean;
|
|
96
100
|
readonly rest?: boolean;
|
|
97
101
|
readonly min?: number;
|
|
@@ -145,6 +149,7 @@ export interface CommandParameterDefinition {
|
|
|
145
149
|
/** `[...name]` / `[[...name]]` 捕获所有段,运行时值为 `string[]`。 */
|
|
146
150
|
readonly rest?: boolean;
|
|
147
151
|
readonly description?: string;
|
|
152
|
+
readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/** 场景:群 / 私聊 / 频道等。 */
|
|
@@ -192,6 +197,8 @@ export interface CommandConversation {
|
|
|
192
197
|
export interface CommandMessage {
|
|
193
198
|
readonly conversation: CommandConversation;
|
|
194
199
|
readonly content: string;
|
|
200
|
+
/** Display preference resolved by the IM runtime, when this is an IM operation. */
|
|
201
|
+
readonly locale?: Locale;
|
|
195
202
|
/** 发送者(结构化视图见 CommandContext.sender)。 */
|
|
196
203
|
readonly sender?: { readonly id: string; readonly name?: string; readonly roles?: readonly string[] };
|
|
197
204
|
readonly id?: string;
|
|
@@ -264,6 +271,16 @@ export interface CommandContext<
|
|
|
264
271
|
TInput extends CommandMessage = CommandMessage,
|
|
265
272
|
TAdapter extends string | undefined = undefined,
|
|
266
273
|
> extends CapabilityContext<TConfig>, CommandSession {
|
|
274
|
+
/** Language resolved once for the current operation. */
|
|
275
|
+
readonly locale: Locale;
|
|
276
|
+
/**
|
|
277
|
+
* Resolve a static local command path to its current chat invocation. Defaults to this
|
|
278
|
+
* plugin; an explicit owner may reference another indexed plugin.
|
|
279
|
+
* Includes the actual Endpoint prefix and configured commandNamespace. Arguments
|
|
280
|
+
* are appended explicitly by the caller. Returns undefined for unknown/disabled
|
|
281
|
+
* paths or calls without a known chat command environment.
|
|
282
|
+
*/
|
|
283
|
+
formatCommand(localPath: string, owner?: PluginId): string | undefined;
|
|
267
284
|
readonly args: readonly string[];
|
|
268
285
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
269
286
|
readonly options: Readonly<Record<string, CommandParameterValue>>;
|
|
@@ -276,6 +293,8 @@ export interface CommandContext<
|
|
|
276
293
|
readonly input?: TInput;
|
|
277
294
|
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
278
295
|
readonly $client: AdapterClient<TAdapter>;
|
|
296
|
+
/** Bound by the actual IM operation; direct CommandIndex.execute calls may have no Session. */
|
|
297
|
+
readonly session: OperationSession<TAdapter> | undefined;
|
|
279
298
|
/**
|
|
280
299
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
281
300
|
*/
|
|
@@ -294,6 +313,7 @@ export interface CommandDefinition<
|
|
|
294
313
|
/** @internal Convention-derived parameter metadata. */
|
|
295
314
|
readonly $parameter?: CommandParameterDefinition;
|
|
296
315
|
readonly description?: string;
|
|
316
|
+
readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
297
317
|
/** Restrict this command to one adapter and infer `context.$client`. */
|
|
298
318
|
readonly adapter?: TAdapter;
|
|
299
319
|
/**
|
|
@@ -307,6 +327,8 @@ export interface CommandDefinition<
|
|
|
307
327
|
* 用户路由;alias 只替换文件路径提供的静态段。
|
|
308
328
|
*/
|
|
309
329
|
readonly alias?: readonly string[];
|
|
330
|
+
/** Display an explicitly declared alias for this locale; every value must also occur in alias. */
|
|
331
|
+
readonly preferredAliasI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
310
332
|
/**
|
|
311
333
|
* 内置 permit DSL(AND)。单项内逗号为 OR。
|
|
312
334
|
* 例:`adapter(icqq)`、`role(master)`、`group(123,456)`。
|
|
@@ -360,28 +382,40 @@ type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extend
|
|
|
360
382
|
readonly options: InferCommandOptions<O>;
|
|
361
383
|
};
|
|
362
384
|
|
|
385
|
+
/** The returned declaration retains its platform, parameter schemas and callback result. */
|
|
386
|
+
type DeclaredCommandAdapter<D> = D extends { readonly adapter: infer A extends string } ? A
|
|
387
|
+
: D extends { readonly adapter?: infer A extends string | undefined } ? A : undefined;
|
|
388
|
+
export type TypedCommandDefinition<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> =
|
|
389
|
+
Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, 'params' | 'options' | 'execute'> & {
|
|
390
|
+
readonly params?: P;
|
|
391
|
+
readonly options?: O;
|
|
392
|
+
execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
|
|
393
|
+
};
|
|
394
|
+
|
|
363
395
|
/**
|
|
364
396
|
* `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
|
|
365
397
|
* 仍从定义推断。
|
|
366
398
|
* @public
|
|
367
399
|
*/
|
|
368
400
|
export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <
|
|
369
|
-
TResult = unknown,
|
|
401
|
+
const TResult = unknown,
|
|
370
402
|
const P extends CommandParams = {},
|
|
371
403
|
const O extends CommandOptions = {},
|
|
404
|
+
const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
|
|
372
405
|
>(
|
|
373
|
-
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O
|
|
374
|
-
) => Readonly<
|
|
406
|
+
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
|
|
407
|
+
) => Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
|
|
375
408
|
|
|
376
409
|
export function defineCommand<
|
|
377
410
|
TConfig = unknown,
|
|
378
|
-
TResult = unknown,
|
|
411
|
+
const TResult = unknown,
|
|
379
412
|
TInput extends CommandMessage = CommandMessage,
|
|
380
413
|
const P extends CommandParams = {},
|
|
381
414
|
const O extends CommandOptions = {},
|
|
415
|
+
const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
|
|
382
416
|
>(
|
|
383
|
-
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O
|
|
384
|
-
): Readonly<
|
|
417
|
+
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
|
|
418
|
+
): Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>> {
|
|
385
419
|
if (typeof definition.execute !== 'function') {
|
|
386
420
|
throw new TypeError('Command execute must be a function');
|
|
387
421
|
}
|
|
@@ -398,15 +432,26 @@ export function defineCommand<
|
|
|
398
432
|
}
|
|
399
433
|
}
|
|
400
434
|
validateArgumentSchemas(definition.params, definition.options);
|
|
435
|
+
validateDescriptionI18n(definition.descriptionI18n);
|
|
401
436
|
validateCommandAlias(definition.alias);
|
|
437
|
+
validatePreferredAliases(definition);
|
|
402
438
|
validateCommandPermit(definition.permit);
|
|
403
439
|
validateCommandShortcutShape(definition.shortcut);
|
|
404
440
|
const execution = normalizeExecutionPolicy(definition.execution);
|
|
405
441
|
return Object.freeze({ $feature: commandBrand, ...definition, ...(execution ? { execution } : {}) }) as Readonly<
|
|
406
|
-
|
|
442
|
+
TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>
|
|
407
443
|
>;
|
|
408
444
|
}
|
|
409
445
|
|
|
446
|
+
/** @internal Validate display-only translations without changing their fallback text. */
|
|
447
|
+
export function validateDescriptionI18n(value: CommandDefinition['descriptionI18n']): void {
|
|
448
|
+
if (value === undefined) return;
|
|
449
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
450
|
+
|| Object.entries(value).some(([locale, text]) => !['zh-CN', 'en'].includes(locale) || typeof text !== 'string')) {
|
|
451
|
+
throw new TypeError('Command descriptionI18n must map zh-CN/en to strings');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
410
455
|
/**
|
|
411
456
|
* 先固定插件 config 类型,再定义命令;params / options / 返回值仍从定义推断。
|
|
412
457
|
*
|
|
@@ -435,6 +480,19 @@ function validateAdapterName(adapter: string | undefined): void {
|
|
|
435
480
|
}
|
|
436
481
|
}
|
|
437
482
|
|
|
483
|
+
function validatePreferredAliases(definition: Pick<CommandDefinition, 'alias' | 'preferredAliasI18n'>): void {
|
|
484
|
+
const preferred = definition.preferredAliasI18n;
|
|
485
|
+
if (preferred === undefined) return;
|
|
486
|
+
validateDescriptionI18n(preferred);
|
|
487
|
+
const normalize = (value: string) => value.trim().split(/\s+/u).filter(Boolean).join(' ');
|
|
488
|
+
const aliases = new Set(definition.alias?.map(normalize));
|
|
489
|
+
for (const [locale, alias] of Object.entries(preferred)) {
|
|
490
|
+
if (!aliases.has(normalize(alias))) {
|
|
491
|
+
throw new TypeError(`Command preferredAliasI18n.${locale} must reference a declared alias`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
438
496
|
function validateCommandAlias(alias: readonly string[] | undefined): void {
|
|
439
497
|
if (alias === undefined) return;
|
|
440
498
|
if (!Array.isArray(alias)) {
|
|
@@ -506,6 +564,8 @@ export function parseCommandDefinition(value: unknown): CommandDefinition {
|
|
|
506
564
|
}
|
|
507
565
|
validateAdapterName(definition.adapter);
|
|
508
566
|
normalizeExecutionPolicy(definition.execution);
|
|
567
|
+
validateCommandAlias(definition.alias);
|
|
568
|
+
validatePreferredAliases(definition);
|
|
509
569
|
return definition as CommandDefinition;
|
|
510
570
|
}
|
|
511
571
|
|
|
@@ -526,18 +586,31 @@ export function resolveDynamicParams(
|
|
|
526
586
|
return Object.freeze(resolved);
|
|
527
587
|
}
|
|
528
588
|
|
|
529
|
-
/**
|
|
530
|
-
export
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
589
|
+
/** Parsed invocation values, independent of the caller's transport and lifetime. */
|
|
590
|
+
export interface CommandInvocationArguments {
|
|
591
|
+
readonly args: readonly string[];
|
|
592
|
+
readonly params?: Readonly<Record<string, CommandParameterValue>>;
|
|
593
|
+
readonly options?: Readonly<Record<string, CommandParameterValue>>;
|
|
594
|
+
readonly segments?: readonly Readonly<CommandSegment>[];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Author and lifetime authority captured by the dispatcher for this invocation. */
|
|
598
|
+
export interface CommandInvocationOperation {
|
|
599
|
+
readonly snapshot: RuntimeSnapshot;
|
|
600
|
+
readonly ownerId: PluginId;
|
|
601
|
+
readonly input?: unknown;
|
|
602
|
+
readonly interaction?: UserInteraction;
|
|
603
|
+
readonly adapter?: string;
|
|
604
|
+
readonly presentation?: Readonly<{
|
|
605
|
+
locale: Locale;
|
|
606
|
+
formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
|
|
607
|
+
}>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @internal Command dispatcher assembly helper; each value has one owner. */
|
|
611
|
+
export function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext {
|
|
612
|
+
const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
|
|
613
|
+
const { args, params = {}, options = {}, segments = [] } = invocation;
|
|
541
614
|
const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
|
|
542
615
|
const session = resolveCommandSession(input);
|
|
543
616
|
const parameterValues: Record<string, CommandParameterValue> = { __proto__: null, ...params };
|
|
@@ -545,6 +618,8 @@ export function createCommandContext(
|
|
|
545
618
|
const result = {
|
|
546
619
|
...context,
|
|
547
620
|
...session,
|
|
621
|
+
locale: presentation?.locale ?? resolveLocale((snapshot.config.get(snapshot.root) as { locale?: unknown } | undefined)?.locale),
|
|
622
|
+
formatCommand: presentation?.formatCommand ?? (() => undefined),
|
|
548
623
|
args: Object.freeze([...args]),
|
|
549
624
|
params: Object.freeze(parameterValues),
|
|
550
625
|
options: Object.freeze(optionValues),
|
|
@@ -552,6 +627,10 @@ export function createCommandContext(
|
|
|
552
627
|
...(input !== undefined ? { input: input as CommandMessage } : {}),
|
|
553
628
|
...(interaction !== undefined ? { interaction } : {}),
|
|
554
629
|
} as CommandContext;
|
|
630
|
+
Object.defineProperty(result, 'session', {
|
|
631
|
+
enumerable: true,
|
|
632
|
+
get: () => readOperationSession(input, adapter),
|
|
633
|
+
});
|
|
555
634
|
Object.defineProperty(result, '$client', {
|
|
556
635
|
enumerable: true,
|
|
557
636
|
get: () => readOperationClient(input, adapter),
|
|
@@ -565,6 +644,20 @@ export function createCommandContext(
|
|
|
565
644
|
* @internal Command dispatcher projection helper.
|
|
566
645
|
*/
|
|
567
646
|
export function resolveCommandSession(input: unknown): CommandSession {
|
|
647
|
+
const operation = readOperationSession(input);
|
|
648
|
+
if (operation) {
|
|
649
|
+
const endpoint = input && typeof input === 'object' ? (input as { readonly endpointId?: unknown }).endpointId : undefined;
|
|
650
|
+
const sender = operation.sender;
|
|
651
|
+
const roles = [...new Set([...sender?.authorizationRoles ?? [], ...sender?.platformRoles ?? []])];
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
adapter: operation.scene.endpoint.adapter,
|
|
654
|
+
...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
|
|
655
|
+
scene: Object.freeze({ id: operation.scene.id, type: operation.scene.kind,
|
|
656
|
+
...(operation.scene.name !== undefined ? { name: operation.scene.name } : {}) }),
|
|
657
|
+
...(sender ? { sender: Object.freeze({ id: sender.id, ...(sender.name !== undefined ? { name: sender.name } : {}),
|
|
658
|
+
role: Object.freeze(roles.length ? roles : ['user']) }) } : {}),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
568
661
|
if (!isCommandMessageLike(input)) return Object.freeze({});
|
|
569
662
|
|
|
570
663
|
const metadata = input.metadata && typeof input.metadata === 'object'
|
package/src/index.ts
CHANGED
package/src/messages.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createMessages, type Locale } from '@zhin.js/plugin-runtime';
|
|
2
|
+
|
|
3
|
+
/** Translates only display metadata explicitly supplied by its author. */
|
|
4
|
+
export function commandDescription(
|
|
5
|
+
value: Readonly<{ description?: string; descriptionI18n?: Readonly<Partial<Record<Locale, string>>> }>,
|
|
6
|
+
locale: Locale,
|
|
7
|
+
): string | undefined {
|
|
8
|
+
return value.descriptionI18n?.[locale] ?? value.description;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Static dictionaries; the caller supplies the operation's language. */
|
|
12
|
+
export const commandMessages = createMessages({
|
|
13
|
+
menuKeyword: { 'zh-CN': '菜单', en: 'menu' },
|
|
14
|
+
invalidPage: { 'zh-CN': '页码必须是正整数。用法:{command} [插件名] --page <页码>', en: 'The page must be a positive integer. Usage: {command} [plugin] --page <page>' },
|
|
15
|
+
missingPlugin: { 'zh-CN': '未找到插件: {plugin}', en: 'Plugin not found: {plugin}' },
|
|
16
|
+
pageOutOfRange: { 'zh-CN': '页码超出范围:共 {pages} 页。请使用「{command} --page <页码>」。', en: 'Page out of range: {pages} pages available. Use {command} --page <page>.' },
|
|
17
|
+
menuTitle: { 'zh-CN': '=== {plugin} 指令菜单 ===', en: '=== {plugin} command menu ===' },
|
|
18
|
+
menuSummary: { 'zh-CN': '第 {page}/{pages} 页 · {commands} 条指令 · {plugins} 个子插件', en: 'Page {page}/{pages} · Commands: {commands} · Plugins: {plugins}' },
|
|
19
|
+
childPlugins: { 'zh-CN': '子插件:', en: 'Plugins:' },
|
|
20
|
+
emptyMenu: { 'zh-CN': '(暂无指令和子插件)', en: '(No commands or plugins)' },
|
|
21
|
+
pluginHint: { 'zh-CN': '提示:使用「{command} <插件名>」查看子插件的指令', en: 'Use {command} <plugin> to view a plugin’s commands.' },
|
|
22
|
+
previousPage: { 'zh-CN': '上一页:{command}', en: 'Previous page: {command}' },
|
|
23
|
+
nextPage: { 'zh-CN': '下一页:{command}', en: 'Next page: {command}' },
|
|
24
|
+
guideTitle: { 'zh-CN': '{title} · 指令指南', en: '{title} · Command guide' },
|
|
25
|
+
guideSubtitle: { 'zh-CN': '每一个灵感,都有回应。', en: 'An answer to every idea.' },
|
|
26
|
+
commandCount: { 'zh-CN': '{count} 条指令', en: 'Commands: {count}' },
|
|
27
|
+
pluginCount: { 'zh-CN': '{count} 个子插件', en: 'Plugins: {count}' },
|
|
28
|
+
commandIndex: { 'zh-CN': '指令索引', en: 'Commands' },
|
|
29
|
+
pluginIndex: { 'zh-CN': '插件导航', en: 'Plugins' },
|
|
30
|
+
guideEmpty: { 'zh-CN': '暂无指令和子插件', en: 'No commands or plugins' },
|
|
31
|
+
helpHint: { 'zh-CN': '查看参数与用法:{command} --help', en: 'View arguments and usage: {command} --help' },
|
|
32
|
+
genericHelpHint: { 'zh-CN': '在当前环境的指令后追加 --help 查看用法', en: 'Append --help to a command in this environment to view its usage' },
|
|
33
|
+
expectedType: { 'zh-CN': '{field}:应为 {type}', en: '{field}: expected {type}' },
|
|
34
|
+
expectedArray: { 'zh-CN': '{field}:应为 {type} 数组', en: '{field}: expected an array of {type}' },
|
|
35
|
+
minimum: { 'zh-CN': '{field}:最小值为 {minimum}', en: '{field}: minimum {minimum}' },
|
|
36
|
+
maximum: { 'zh-CN': '{field}:最大值为 {maximum}', en: '{field}: maximum {maximum}' },
|
|
37
|
+
choices: { 'zh-CN': '{field}:可选值为 {choices}', en: '{field}: expected one of {choices}' },
|
|
38
|
+
invalidFormat: { 'zh-CN': '{field}:格式不正确', en: '{field}: invalid format' },
|
|
39
|
+
invalidValue: { 'zh-CN': '{field}:值不正确', en: '{field}: invalid value' },
|
|
40
|
+
unknownOption: { 'zh-CN': '未知选项 {field}', en: 'Unknown option {field}' },
|
|
41
|
+
duplicateOption: { 'zh-CN': '重复选项 --{field}', en: 'Duplicate option --{field}' },
|
|
42
|
+
defaultUndefined: { 'zh-CN': '{field}:默认值返回了 undefined', en: '{field}: default returned undefined' },
|
|
43
|
+
parameterUndefined: { 'zh-CN': '{field}:默认值或快捷指令返回了 undefined', en: '{field}: default or shortcut returned undefined' },
|
|
44
|
+
missingOption: { 'zh-CN': '缺少选项 --{field}', en: 'Missing option --{field}' },
|
|
45
|
+
unexpectedArguments: { 'zh-CN': '存在多余参数', en: 'Unexpected arguments' },
|
|
46
|
+
invalidParameter: { 'zh-CN': '缺少指令参数或参数不正确', en: 'Missing or invalid command parameter' },
|
|
47
|
+
defaultValue: { 'zh-CN': '默认值:{value}', en: 'default: {value}' },
|
|
48
|
+
dynamicValue: { 'zh-CN': '动态计算', en: 'dynamic' },
|
|
49
|
+
noAdapter: { 'zh-CN': '命令未声明 adapter 约束', en: 'The command does not declare an adapter restriction' },
|
|
50
|
+
missingEndpoint: { 'zh-CN': '未选择当前 Endpoint,无法判断命令的精确 adapter 约束', en: 'Select an Endpoint to evaluate the command’s exact adapter restriction' },
|
|
51
|
+
adapterComparison: { 'zh-CN': '命令 adapter 与当前 Endpoint 声明的原生适配器精确比较;未探测在线状态', en: 'Exact comparison with the Endpoint’s declared native adapter; online status was not checked' },
|
|
52
|
+
conditionMismatch: { 'zh-CN': '至少一个声明条件与假设输入不符;这不是实际准入检查', en: 'At least one declared condition does not match the hypothetical input; this is not a live admission check' },
|
|
53
|
+
missingPermissionHost: { 'zh-CN': 'PermissionHost 缺失或无法确认;不能推断命令可执行', en: 'PermissionHost is missing or unverified; command execution cannot be inferred' },
|
|
54
|
+
missingEvidence: { 'zh-CN': '存在缺少依据的条件,不能推断命令可执行', en: 'Some conditions lack evidence; command execution cannot be inferred' },
|
|
55
|
+
noPermit: { 'zh-CN': '命令未声明 permit 限制;这不表示已获得执行权限', en: 'The command does not declare permit restrictions; this does not establish authorization' },
|
|
56
|
+
uncheckedConditions: { 'zh-CN': '存在未执行的平台或自定义条件,不能推断命令可执行', en: 'Platform or custom conditions have not been evaluated; command execution cannot be inferred' },
|
|
57
|
+
conditionsMatch: { 'zh-CN': '已解释的 builtin 条件与假设输入匹配;不代表真实授权或命令可执行', en: 'The explained builtin conditions match the hypothetical input; this does not establish authorization or executability' },
|
|
58
|
+
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' },
|
|
59
|
+
simulatedIdentity: { 'zh-CN': '身份与角色仅用于模拟,未向平台核实', en: 'Identity and roles are hypothetical and have not been verified with the platform' },
|
|
60
|
+
unevaluatedCode: { 'zh-CN': '未执行命令、动态默认值、平台或自定义 checker', en: 'Commands, dynamic defaults, platform checks and custom checkers have not been executed' },
|
|
61
|
+
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' },
|
|
62
|
+
});
|
|
@@ -2,6 +2,8 @@ import { operationClientAdapter } from '@zhin.js/feature-kit';
|
|
|
2
2
|
import { explainPermitRules, toPermissionSubject, type PermissionSubject, type PermissionConditionExplanation } from '@zhin.js/permission';
|
|
3
3
|
import { resolveCommandSender, resolveCommandSession, type CommandMessage } from './definition.js';
|
|
4
4
|
import type { CommandDescriptor } from './command-index.js';
|
|
5
|
+
import { resolveLocale, type Locale } from '@zhin.js/plugin-runtime';
|
|
6
|
+
import { commandMessages } from './messages.js';
|
|
5
7
|
|
|
6
8
|
/** The exact subject projection used by the real command permit gate. No factories or checkers run. */
|
|
7
9
|
export function toCommandPermissionSubject(source: unknown): PermissionSubject {
|
|
@@ -20,8 +22,9 @@ export type CommandPermissionHostStatus = 'present' | 'missing' | 'unverified';
|
|
|
20
22
|
/** Read-only hypothetical conditions. A match is not permission to execute the command. */
|
|
21
23
|
export function explainCommandPermissions(
|
|
22
24
|
command: Pick<CommandDescriptor, 'adapter' | 'permit'>,
|
|
23
|
-
options: Readonly<{ source?: unknown; rolesKnown?: boolean; permissionHost: CommandPermissionHostStatus }>,
|
|
25
|
+
options: Readonly<{ source?: unknown; rolesKnown?: boolean; permissionHost: CommandPermissionHostStatus; locale?: Locale }>,
|
|
24
26
|
) {
|
|
27
|
+
const locale = resolveLocale(options.locale);
|
|
25
28
|
const projected = toCommandPermissionSubject(options.source);
|
|
26
29
|
const inputSender = options.source && typeof options.source === 'object'
|
|
27
30
|
? (options.source as Pick<CommandMessage, 'sender'>).sender : undefined;
|
|
@@ -36,23 +39,23 @@ export function explainCommandPermissions(
|
|
|
36
39
|
&& subject.sender?.role.length === 1 && subject.sender.role[0] === 'user';
|
|
37
40
|
const actualAdapter = operationClientAdapter(options.source);
|
|
38
41
|
const adapter: PermissionConditionExplanation = !command.adapter
|
|
39
|
-
? { outcome: 'not_evaluated', reason:
|
|
40
|
-
: !actualAdapter ? { outcome: 'unknown', reason:
|
|
41
|
-
: { outcome: commandAdapterMatches(command, options.source) ? 'match' : 'mismatch', reason:
|
|
42
|
-
const rules = explainPermitRules(command.permit ?? [], subject, { rolesKnown: options.rolesKnown });
|
|
42
|
+
? { outcome: 'not_evaluated', reason: commandMessages(locale, 'noAdapter') }
|
|
43
|
+
: !actualAdapter ? { outcome: 'unknown', reason: commandMessages(locale, 'missingEndpoint') }
|
|
44
|
+
: { outcome: commandAdapterMatches(command, options.source) ? 'match' : 'mismatch', reason: commandMessages(locale, 'adapterComparison') };
|
|
45
|
+
const rules = explainPermitRules(command.permit ?? [], subject, { rolesKnown: options.rolesKnown, locale });
|
|
43
46
|
const conditions = [...(command.adapter ? [adapter] : []), ...rules];
|
|
44
47
|
let summary: PermissionConditionExplanation;
|
|
45
|
-
if (conditions.some(item => item.outcome === 'mismatch')) summary = { outcome: 'mismatch', reason: '
|
|
48
|
+
if (conditions.some(item => item.outcome === 'mismatch')) summary = { outcome: 'mismatch', reason: commandMessages(locale, 'conditionMismatch') };
|
|
46
49
|
else if ((rules.length > 0 && options.permissionHost !== 'present') || conditions.some(item => item.outcome === 'unknown')) {
|
|
47
50
|
summary = { outcome: 'unknown', reason: rules.length > 0 && options.permissionHost !== 'present'
|
|
48
|
-
?
|
|
49
|
-
} else if (rules.length === 0) summary = { outcome: 'not_evaluated', reason:
|
|
50
|
-
else if (conditions.some(item => item.outcome === 'not_evaluated')) summary = { outcome: 'not_evaluated', reason: '
|
|
51
|
-
else summary = { outcome: 'match', reason:
|
|
51
|
+
? commandMessages(locale, 'missingPermissionHost') : commandMessages(locale, 'missingEvidence') };
|
|
52
|
+
} else if (rules.length === 0) summary = { outcome: 'not_evaluated', reason: commandMessages(locale, 'noPermit') };
|
|
53
|
+
else if (conditions.some(item => item.outcome === 'not_evaluated')) summary = { outcome: 'not_evaluated', reason: commandMessages(locale, 'uncheckedConditions') };
|
|
54
|
+
else summary = { outcome: 'match', reason: commandMessages(locale, 'conditionsMatch') };
|
|
52
55
|
return Object.freeze({ permissionHost: options.permissionHost, adapter: Object.freeze(adapter), rules, summary: Object.freeze(summary),
|
|
53
|
-
unconstrained: Object.freeze([
|
|
54
|
-
assumptions: Object.freeze(['
|
|
56
|
+
unconstrained: Object.freeze([commandMessages(locale, 'unconstrained')]),
|
|
57
|
+
assumptions: Object.freeze([commandMessages(locale, 'simulatedIdentity'), commandMessages(locale, 'unevaluatedCode'),
|
|
55
58
|
...(defaultedRoles
|
|
56
|
-
? [
|
|
59
|
+
? [commandMessages(locale, 'defaultedRole')] : [])]),
|
|
57
60
|
});
|
|
58
61
|
}
|
package/src/provider.ts
CHANGED
|
@@ -311,7 +311,7 @@ const commandFeature = defineFeatureProvider({
|
|
|
311
311
|
const rawKeyword = rootConfig?.menuKeyword;
|
|
312
312
|
const menu = rawKeyword === false || rawKeyword === ''
|
|
313
313
|
? undefined
|
|
314
|
-
:
|
|
314
|
+
: typeof rawKeyword === 'string' ? { keyword: rawKeyword } : {};
|
|
315
315
|
return { value: new CommandIndex(slots, context.snapshot, menu) };
|
|
316
316
|
},
|
|
317
317
|
},
|