@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/README.md
CHANGED
|
@@ -42,6 +42,8 @@ commands/search/[...kw]/index.ts -> search <...kw> (params: { kw: { type:
|
|
|
42
42
|
- `config` / `use()` / `owner` / `generation`:owner-scoped Runtime 上下文。
|
|
43
43
|
- `signal`:真实 IM 入站操作的运行代退休时同步取消。补问、确认及其他需要随代结束的等待应观察此信号;它不会强制取消已经开始的发送,也不禁止持有旧 lease 的普通命令继续排空。没有携带运行代生命周期的输入(如普通 Host 调用)不会凭空获得 IM 生命周期。
|
|
44
44
|
- `input`:派发来源;IM 中为满足 `CommandMessage` 的 Runtime `Message`。
|
|
45
|
+
- `locale`:本次操作已经解析的 `zh-CN` 或 `en`,群聊也按发送者偏好;无个人偏好时沿用项目默认语言。
|
|
46
|
+
- `formatCommand(localPath, owner?)`:从当前索引取得静态命令调用头,默认本插件,也可显式传入已知的插件 owner,自动拼上实际 Endpoint 前缀和显式 `commandNamespace`。例如 `formatCommand('remind list')`;参数在返回的调用头后显式附加。命令不存在、被禁用或当前没有已知聊天命令环境时返回 `undefined`,不要以硬编码 `/` 或命令名掩盖该结果。
|
|
45
47
|
- `adapter` / `endpoint`:适配器实例 id 与 endpoint 名。
|
|
46
48
|
- `scene`:`{ id, type, name? }` 场景对象。
|
|
47
49
|
- `sender`:`{ id, name?, role: string[] }` 发送者对象。
|
|
@@ -78,7 +80,9 @@ PermissionHost 或缺少身份依据均不能被展示为“已授权”或“
|
|
|
78
80
|
|
|
79
81
|
## 集中声明参数、选项与帮助
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
**列出指令**:中文操作使用 `菜单`,英文操作使用 `menu`,每次只接受该操作语言的默认关键词。列出根插件的指令和直接子插件;`菜单 <插件实例键>` 查看子插件,嵌套实例用 `菜单 foo.bar`(英文对应 `menu foo.bar`)。菜单逐层浏览,不一次展开全树。账号设有命令前缀时同样要加前缀,如 `/菜单`。项目根配置的顶层 `menuKeyword` 可显式修改关键词,不随语言翻译;`false` 或空字符串关闭。详见[指令菜单](../../../docs/authoring/commands.md#查看已加载的指令菜单)。
|
|
84
|
+
|
|
85
|
+
命令、参数和选项保留 `description` 原文,可额外声明 `descriptionI18n: { en: 'English description' }`。菜单、`--help` 与 `CommandIndex.list(locale)` 使用对应说明,缺失时保留作者原文,不自动翻译机器名称、用户内容或参数值。参数错误带稳定的 `code` 与 `parameters`,`message` 是当前语言的展示;自定义 `validate` 返回的文本仍由作者负责。
|
|
82
86
|
|
|
83
87
|
安装 `@zhin.js/html-renderer` 后,菜单发出带原始文本备用内容的 HTML 段,复用统一发送链的图片渲染和平台策略。渲染失败或平台无法消费图片时保留原文本;无渲染器时直接返回文本。Command Feature 不调用渲染器,也不新增 Core 或样式组件依赖。
|
|
84
88
|
|
|
@@ -113,7 +117,7 @@ export default defineCommand({
|
|
|
113
117
|
后面的内容作为参数。未知/重复选项拒绝。`remind --help` 从同一声明生成帮助,包含
|
|
114
118
|
Endpoint 前缀及显式插件命名空间,不执行命令;同一权限声明仍约束帮助读取。
|
|
115
119
|
|
|
116
|
-
`dispatch()` 返回 `status`:`unmatched`(继续普通消息处理)、`invalid`(`issues` +
|
|
120
|
+
`dispatch({ input, source, snapshot })` 返回 `status`:`unmatched`(继续普通消息处理)、`invalid`(`issues` +
|
|
117
121
|
`help`)、`denied`(权限拒绝)、`limited`(执行预算拒绝)、`executed`。后四者 `matched: true`,只有 `executed`
|
|
118
122
|
包含业务结果。可靠静态路径匹配后才提示错误;纯动态路由的模糊聊天保持未匹配。
|
|
119
123
|
受限命令没有可鉴权主体时拒绝,Host 可通过 `dispatch` 提供真实 `PermissionSubject`;
|
|
@@ -170,3 +174,19 @@ pnpm --filter @zhin.js/command test
|
|
|
170
174
|
|
|
171
175
|
完整用户契约见[命令创作指南](../../../docs/authoring/commands.md),架构迁移背景见
|
|
172
176
|
[Plugin Runtime 原位迁移](../../../docs/architecture/target-implementation/in-place-migration.md)。
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
命令可声明 `descriptionI18n: { en: 'Create a reminder' }` 翻译描述。若静态目录名是中文,可沿用 `alias` 提供英文入口,并用 `preferredAliasI18n` 显式选择英文展示:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
export default defineCommand({
|
|
183
|
+
alias: ['reminder subscribe'],
|
|
184
|
+
preferredAliasI18n: { en: 'reminder subscribe' },
|
|
185
|
+
execute({ locale, formatCommand }) {
|
|
186
|
+
// 参数仍由调用方明确附加;不存在或没有真实聊天环境时返回 undefined。
|
|
187
|
+
return formatCommand('提醒 订阅');
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`preferredAliasI18n` 必须引用该命令已声明的别名;它只选择菜单、帮助与指令引用的展示入口,不创建新路由或停用既有别名。实际插件挂载命名空间与当前消息前缀由同一个 `CommandIndex` 拼接。
|
package/lib/arguments.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { CommandDefinition, CommandParams, CommandOptions, CommandParameterValue, CommandSegment, CommandSession } from './definition.js';
|
|
2
|
+
import type { Locale } from '@zhin.js/plugin-runtime';
|
|
3
|
+
export type CommandValidationIssueCode = 'expectedType' | 'expectedArray' | 'minimum' | 'maximum' | 'choices' | 'invalidFormat' | 'invalidValue' | 'unknownOption' | 'duplicateOption' | 'defaultUndefined' | 'parameterUndefined' | 'missingOption' | 'unexpectedArguments' | 'invalidParameter' | 'custom';
|
|
2
4
|
export interface CommandValidationIssue {
|
|
3
5
|
readonly field: string;
|
|
4
6
|
readonly message: string;
|
|
7
|
+
readonly code?: CommandValidationIssueCode;
|
|
8
|
+
readonly parameters?: Readonly<Record<string, string | number>>;
|
|
5
9
|
}
|
|
10
|
+
/** Stable validation reason and values; never reverse-parse an English message. */
|
|
11
|
+
export declare function commandValidationIssue(code: Exclude<CommandValidationIssueCode, 'custom'>, field: string, locale: Locale, parameters?: Readonly<Record<string, string | number>>): CommandValidationIssue;
|
|
6
12
|
export interface ParsedCommandArguments {
|
|
7
13
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
8
14
|
readonly options: Readonly<Record<string, CommandParameterValue>>;
|
|
@@ -14,6 +20,6 @@ export declare function validateArgumentSchemas(params?: CommandParams, options?
|
|
|
14
20
|
/** Reserved flags use the same quoted-token and option-terminator rules as arguments. */
|
|
15
21
|
export declare function requestsCommandHelp(segments: readonly Readonly<CommandSegment>[]): boolean;
|
|
16
22
|
/** Parse arguments after a route match; the CommandIndex remains the only dispatcher. */
|
|
17
|
-
export declare function parseCommandArguments(definition: CommandDefinition, remaining: readonly Readonly<CommandSegment>[], initial: Readonly<Record<string, CommandParameterValue>>, session: CommandSession): ParsedCommandArguments;
|
|
23
|
+
export declare function parseCommandArguments(definition: CommandDefinition, remaining: readonly Readonly<CommandSegment>[], initial: Readonly<Record<string, CommandParameterValue>>, session: CommandSession, locale?: Locale): ParsedCommandArguments;
|
|
18
24
|
export declare function commandUsage(name: string, definition: CommandDefinition): string;
|
|
19
|
-
export declare function commandHelp(name: string, definition: CommandDefinition, prefix?: string): string;
|
|
25
|
+
export declare function commandHelp(name: string, definition: CommandDefinition, prefix?: string, locale?: Locale): string;
|
package/lib/arguments.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { commandDescription, commandMessages } from './messages.js';
|
|
2
|
+
/** Stable validation reason and values; never reverse-parse an English message. */
|
|
3
|
+
export function commandValidationIssue(code, field, locale, parameters = {}) {
|
|
4
|
+
const values = Object.freeze({ field, ...parameters });
|
|
5
|
+
return Object.freeze({ field, code, parameters: values, message: commandMessages(locale, code, values) });
|
|
6
|
+
}
|
|
1
7
|
const scalarTypes = new Set(['string', 'word', 'text', 'number', 'integer', 'float', 'boolean']);
|
|
2
8
|
/** Validate the single declaration used by parsing, help and execution. */
|
|
3
9
|
export function validateArgumentSchemas(params, options) {
|
|
@@ -51,6 +57,11 @@ function validateSchema(name, schema) {
|
|
|
51
57
|
if (schema.choices !== undefined && !Array.isArray(schema.choices)) {
|
|
52
58
|
throw new TypeError(`Command argument ${name}.choices must be an array`);
|
|
53
59
|
}
|
|
60
|
+
if (schema.descriptionI18n !== undefined && (!schema.descriptionI18n || typeof schema.descriptionI18n !== 'object'
|
|
61
|
+
|| Array.isArray(schema.descriptionI18n)
|
|
62
|
+
|| Object.entries(schema.descriptionI18n).some(([locale, text]) => !['zh-CN', 'en'].includes(locale) || typeof text !== 'string'))) {
|
|
63
|
+
throw new TypeError(`Command argument ${name}.descriptionI18n must map zh-CN/en to strings`);
|
|
64
|
+
}
|
|
54
65
|
if (schema.default !== undefined && !scalarTypes.has(schema.type))
|
|
55
66
|
throw new TypeError(`Structured Command argument ${name} cannot declare a default`);
|
|
56
67
|
if (schema.default !== undefined && typeof schema.default !== 'function') {
|
|
@@ -158,31 +169,32 @@ function valueMatches(schema, value) {
|
|
|
158
169
|
return typeof value === 'string' || !!value && typeof value === 'object';
|
|
159
170
|
return typeof value === 'string';
|
|
160
171
|
}
|
|
161
|
-
function validateValue(field, schema, value, session) {
|
|
172
|
+
function validateValue(field, schema, value, session, locale = 'zh-CN') {
|
|
162
173
|
if (schema.rest && !Array.isArray(value))
|
|
163
|
-
return
|
|
174
|
+
return commandValidationIssue('expectedArray', field, locale, { type: schema.type });
|
|
164
175
|
const values = schema.rest ? (Array.isArray(value) ? value : [value]) : [value];
|
|
165
176
|
for (const item of values) {
|
|
166
177
|
if (!valueMatches(schema, item))
|
|
167
|
-
return
|
|
178
|
+
return commandValidationIssue('expectedType', field, locale, { type: schema.type });
|
|
168
179
|
const size = typeof item === 'number' ? item : typeof item === 'string' ? item.length : undefined;
|
|
169
180
|
if (schema.min !== undefined && (size === undefined || size < schema.min))
|
|
170
|
-
return
|
|
181
|
+
return commandValidationIssue('minimum', field, locale, { minimum: schema.min });
|
|
171
182
|
if (schema.max !== undefined && (size === undefined || size > schema.max))
|
|
172
|
-
return
|
|
183
|
+
return commandValidationIssue('maximum', field, locale, { maximum: schema.max });
|
|
173
184
|
if (schema.choices && !schema.choices.includes(item))
|
|
174
|
-
return
|
|
185
|
+
return commandValidationIssue('choices', field, locale, { choices: schema.choices.join(', ') });
|
|
175
186
|
if (schema.pattern) {
|
|
176
187
|
const pattern = new RegExp(schema.pattern.source, schema.pattern.flags.replace(/[gy]/gu, ''));
|
|
177
188
|
if (typeof item !== 'string' || !pattern.test(item))
|
|
178
|
-
return
|
|
189
|
+
return commandValidationIssue('invalidFormat', field, locale);
|
|
179
190
|
}
|
|
180
191
|
}
|
|
181
192
|
const result = schema.validate?.(value, session);
|
|
182
|
-
return
|
|
193
|
+
return typeof result === 'string' ? Object.freeze({ field, code: 'custom', message: result })
|
|
194
|
+
: result === false ? commandValidationIssue('invalidValue', field, locale) : undefined;
|
|
183
195
|
}
|
|
184
196
|
/** Parse arguments after a route match; the CommandIndex remains the only dispatcher. */
|
|
185
|
-
export function parseCommandArguments(definition, remaining, initial, session) {
|
|
197
|
+
export function parseCommandArguments(definition, remaining, initial, session, locale = 'zh-CN') {
|
|
186
198
|
const params = { __proto__: null, ...initial };
|
|
187
199
|
const options = { __proto__: null };
|
|
188
200
|
const issues = [];
|
|
@@ -211,14 +223,14 @@ export function parseCommandArguments(definition, remaining, initial, session) {
|
|
|
211
223
|
const flag = rawFlag ?? token.value;
|
|
212
224
|
const name = optionNames.get(flag);
|
|
213
225
|
if (!name) {
|
|
214
|
-
issues.push(
|
|
226
|
+
issues.push(commandValidationIssue('unknownOption', flag, locale));
|
|
215
227
|
continue;
|
|
216
228
|
}
|
|
217
229
|
const schema = schemas[name];
|
|
218
230
|
if (!schema)
|
|
219
231
|
continue;
|
|
220
232
|
if (Object.hasOwn(options, name)) {
|
|
221
|
-
issues.push(
|
|
233
|
+
issues.push(commandValidationIssue('duplicateOption', name, locale));
|
|
222
234
|
continue;
|
|
223
235
|
}
|
|
224
236
|
let value;
|
|
@@ -234,7 +246,7 @@ export function parseCommandArguments(definition, remaining, initial, session) {
|
|
|
234
246
|
}
|
|
235
247
|
}
|
|
236
248
|
if (value === undefined)
|
|
237
|
-
issues.push(
|
|
249
|
+
issues.push(commandValidationIssue('expectedType', name, locale, { field: `--${name}`, type: schema.type }));
|
|
238
250
|
else
|
|
239
251
|
options[name] = value;
|
|
240
252
|
}
|
|
@@ -242,14 +254,14 @@ export function parseCommandArguments(definition, remaining, initial, session) {
|
|
|
242
254
|
if (!Object.hasOwn(options, name) && schema.default !== undefined)
|
|
243
255
|
options[name] = typeof schema.default === 'function' ? schema.default(session) : schema.default;
|
|
244
256
|
if (Object.hasOwn(options, name) && options[name] === undefined)
|
|
245
|
-
issues.push(
|
|
257
|
+
issues.push(commandValidationIssue('defaultUndefined', name, locale, { field: `--${name}` }));
|
|
246
258
|
else if (options[name] !== undefined) {
|
|
247
|
-
const issue = validateValue(`--${name}`, schema, options[name], session);
|
|
259
|
+
const issue = validateValue(`--${name}`, schema, options[name], session, locale);
|
|
248
260
|
if (issue)
|
|
249
261
|
issues.push(issue);
|
|
250
262
|
}
|
|
251
263
|
else if (schema.optional === false)
|
|
252
|
-
issues.push(
|
|
264
|
+
issues.push(commandValidationIssue('missingOption', name, locale));
|
|
253
265
|
}
|
|
254
266
|
const declared = Object.entries(definition.params ?? {});
|
|
255
267
|
for (const [name, declaredSchema] of declared) {
|
|
@@ -262,7 +274,7 @@ export function parseCommandArguments(definition, remaining, initial, session) {
|
|
|
262
274
|
? segmentsFromTokens(raw).map((segment) => segment.type === 'text' && typeof segment.data.text === 'string' ? segment.data.text : undefined)
|
|
263
275
|
: raw.map((token) => convert(schema, token));
|
|
264
276
|
if (converted.some((item) => item === undefined))
|
|
265
|
-
issues.push(
|
|
277
|
+
issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
|
|
266
278
|
else if (converted.length > 0 || schema.optional)
|
|
267
279
|
value = converted;
|
|
268
280
|
}
|
|
@@ -279,7 +291,7 @@ export function parseCommandArguments(definition, remaining, initial, session) {
|
|
|
279
291
|
value = convert(schema, positional.shift());
|
|
280
292
|
// Defaults apply to omitted input, never to a supplied value of the wrong type.
|
|
281
293
|
if (value === undefined) {
|
|
282
|
-
issues.push(
|
|
294
|
+
issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
|
|
283
295
|
continue;
|
|
284
296
|
}
|
|
285
297
|
}
|
|
@@ -288,20 +300,20 @@ export function parseCommandArguments(definition, remaining, initial, session) {
|
|
|
288
300
|
else if (schema.default !== undefined)
|
|
289
301
|
params[name] = typeof schema.default === 'function' ? schema.default(session) : schema.default;
|
|
290
302
|
else if (!schema.optional && !(definition.$parameter?.name === name && definition.$parameter.optional)) {
|
|
291
|
-
issues.push(
|
|
303
|
+
issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
|
|
292
304
|
}
|
|
293
305
|
}
|
|
294
306
|
if (Object.hasOwn(params, name) && params[name] === undefined)
|
|
295
|
-
issues.push(
|
|
307
|
+
issues.push(commandValidationIssue('parameterUndefined', name, locale));
|
|
296
308
|
else if (params[name] !== undefined) {
|
|
297
309
|
const effective = definition.$parameter?.name === name && definition.$parameter.rest ? { ...schema, rest: true } : schema;
|
|
298
|
-
const issue = validateValue(name, effective, params[name], session);
|
|
310
|
+
const issue = validateValue(name, effective, params[name], session, locale);
|
|
299
311
|
if (issue)
|
|
300
312
|
issues.push(issue);
|
|
301
313
|
}
|
|
302
314
|
}
|
|
303
315
|
if (declared.length > 0 && !definition.$parameter && positional.length > 0)
|
|
304
|
-
issues.push(
|
|
316
|
+
issues.push(commandValidationIssue('unexpectedArguments', 'arguments', locale));
|
|
305
317
|
return Object.freeze({ params: Object.freeze(params), options: Object.freeze(options), remaining: schemas === undefined && declared.every(([name]) => Object.hasOwn(initial, name)) ? remaining : segmentsFromTokens(positional), issues: Object.freeze(issues) });
|
|
306
318
|
}
|
|
307
319
|
export function commandUsage(name, definition) {
|
|
@@ -310,12 +322,13 @@ export function commandUsage(name, definition) {
|
|
|
310
322
|
const options = Object.entries(definition.options ?? {}).map(([key, schema]) => `[--${key}${schema.type === 'boolean' ? '' : ` <${schema.type}>`}]`);
|
|
311
323
|
return [name, ...parameters, ...options].join(' ');
|
|
312
324
|
}
|
|
313
|
-
export function commandHelp(name, definition, prefix = '') {
|
|
314
|
-
return [prefix + commandUsage(name, definition), definition
|
|
315
|
-
...Object.entries(definition.params ?? {}).map(([key, schema]) => argumentHelp(key, schema)),
|
|
316
|
-
...Object.entries(definition.options ?? {}).map(([key, schema]) => argumentHelp(`--${key}${schema.alias ? ` (-${schema.alias})` : ''}`, schema)),
|
|
325
|
+
export function commandHelp(name, definition, prefix = '', locale = 'zh-CN') {
|
|
326
|
+
return [prefix + commandUsage(name, definition), commandDescription(definition, locale),
|
|
327
|
+
...Object.entries(definition.params ?? {}).map(([key, schema]) => argumentHelp(key, schema, locale)),
|
|
328
|
+
...Object.entries(definition.options ?? {}).map(([key, schema]) => argumentHelp(`--${key}${schema.alias ? ` (-${schema.alias})` : ''}`, schema, locale)),
|
|
317
329
|
].filter(Boolean).join('\n');
|
|
318
330
|
}
|
|
319
|
-
function argumentHelp(name, schema) {
|
|
320
|
-
|
|
331
|
+
function argumentHelp(name, schema, locale) {
|
|
332
|
+
const description = commandDescription(schema, locale);
|
|
333
|
+
return ` ${name}: ${schema.type}${description ? ` — ${description}` : ''}${schema.default !== undefined ? ` (${commandMessages(locale, 'defaultValue', { value: typeof schema.default === 'function' ? commandMessages(locale, 'dynamicValue') : JSON.stringify(schema.default) })})` : ''}${schema.choices ? ` (${schema.choices.join(', ')})` : ''}${schema.min !== undefined ? ` min=${schema.min}` : ''}${schema.max !== undefined ? ` max=${schema.max}` : ''}`;
|
|
321
334
|
}
|
package/lib/command-index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { type CapabilityId, type CapabilitySlot, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { type CapabilityId, type CapabilitySlot, type PluginId, type RuntimeSnapshot, type Locale } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { UserInteractionFactory } from '@zhin.js/interaction';
|
|
3
3
|
import { type CommandValidationIssue } from './arguments.js';
|
|
4
|
+
import { type CommandMenuEntry, type CommandMenuAppearance } from './command-menu.js';
|
|
5
|
+
export type { CommandMenuEntry, CommandMenuAppearance } from './command-menu.js';
|
|
4
6
|
import { type CommandExecutionLimit } from './execution-policy.js';
|
|
5
7
|
export { CommandExecutionLimitedError } from './execution-policy.js';
|
|
6
8
|
import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment } from './definition.js';
|
|
@@ -23,6 +25,15 @@ export interface CommandDescriptor {
|
|
|
23
25
|
/** shortcut 触发键列表(不含预填 params)。 */
|
|
24
26
|
readonly shortcut?: readonly string[];
|
|
25
27
|
}
|
|
28
|
+
/** One dispatch reads the caller's exact operation generation and presentation. */
|
|
29
|
+
export interface CommandDispatchRequest {
|
|
30
|
+
readonly input: CommandMatchInput;
|
|
31
|
+
readonly source?: unknown;
|
|
32
|
+
readonly interactionFactory?: UserInteractionFactory;
|
|
33
|
+
readonly commandPrefix?: string;
|
|
34
|
+
readonly snapshot?: RuntimeSnapshot;
|
|
35
|
+
readonly locale?: Locale;
|
|
36
|
+
}
|
|
26
37
|
export interface CommandDispatchResult {
|
|
27
38
|
readonly matched: boolean;
|
|
28
39
|
readonly status: 'unmatched' | 'invalid' | 'denied' | 'limited' | 'executed';
|
|
@@ -32,7 +43,7 @@ export interface CommandDispatchResult {
|
|
|
32
43
|
readonly command?: string;
|
|
33
44
|
readonly owner?: PluginId;
|
|
34
45
|
readonly value?: unknown;
|
|
35
|
-
readonly menu?:
|
|
46
|
+
readonly menu?: CommandMenuPage;
|
|
36
47
|
}
|
|
37
48
|
/** Semantic menu navigation; the IM composition layer supplies platform controls. */
|
|
38
49
|
export interface CommandMenuNavigation {
|
|
@@ -41,19 +52,36 @@ export interface CommandMenuNavigation {
|
|
|
41
52
|
readonly page: number;
|
|
42
53
|
readonly pages: number;
|
|
43
54
|
}
|
|
55
|
+
/** Immutable page data; presentation cannot change the catalog or execute commands. */
|
|
56
|
+
export interface CommandMenuPage extends CommandMenuNavigation {
|
|
57
|
+
readonly locale: Locale;
|
|
58
|
+
readonly title: string;
|
|
59
|
+
readonly commands: readonly CommandMenuEntry[];
|
|
60
|
+
readonly plugins: readonly CommandMenuEntry[];
|
|
61
|
+
readonly commandTotal: number;
|
|
62
|
+
readonly pluginTotal: number;
|
|
63
|
+
readonly hint?: string;
|
|
64
|
+
readonly text: string;
|
|
65
|
+
readonly appearance: CommandMenuAppearance;
|
|
66
|
+
}
|
|
44
67
|
export type CommandMatchInput = string | readonly Readonly<CommandSegment>[];
|
|
45
68
|
export interface CommandMenuConfig {
|
|
46
|
-
|
|
69
|
+
/** Omission selects the built-in keyword for each operation's language. */
|
|
70
|
+
readonly keyword?: string;
|
|
47
71
|
}
|
|
48
72
|
export declare class CommandIndex {
|
|
49
73
|
#private;
|
|
50
74
|
private readonly snapshot;
|
|
51
75
|
readonly $projection: "zhin.command-index/1";
|
|
52
76
|
constructor(slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[], snapshot: RuntimeSnapshot, menu?: CommandMenuConfig);
|
|
53
|
-
list(): readonly CommandDescriptor[];
|
|
77
|
+
list(locale?: Locale): readonly CommandDescriptor[];
|
|
78
|
+
/** Resolve a local static route without executing parameters, permission checks or commands. */
|
|
79
|
+
formatCommand(owner: PluginId, localPath: string, prefix: string | undefined, locale?: Locale): string | undefined;
|
|
80
|
+
/** Return the configured menu entry only when the calling environment is known. */
|
|
81
|
+
formatMenu(prefix: string | undefined, locale: Locale): string | undefined;
|
|
54
82
|
has(name: string): boolean;
|
|
55
83
|
execute(name: string, args?: readonly string[], operationSnapshot?: RuntimeSnapshot): Promise<unknown>;
|
|
56
|
-
dispatch(input
|
|
84
|
+
dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot, locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult>;
|
|
57
85
|
}
|
|
58
86
|
export declare function isCommandIndex(value: unknown): value is CommandIndex;
|
|
59
87
|
/**
|
package/lib/command-index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { SegmentMatcher, TypeMatcherRegistry, } from 'segment-matcher';
|
|
2
|
-
import { htmlRendererToken, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { htmlRendererToken, resolveLocale, } from '@zhin.js/plugin-runtime';
|
|
3
3
|
import { bindOperationInput } from '@zhin.js/feature-kit';
|
|
4
4
|
import { permissionHostToken, } from '@zhin.js/permission';
|
|
5
5
|
import { commandAdapterMatches, toCommandPermissionSubject } from './permission-explanation.js';
|
|
6
|
-
import { commandUsage, commandHelp, parseCommandArguments, requestsCommandHelp } from './arguments.js';
|
|
6
|
+
import { commandUsage, commandHelp, parseCommandArguments, requestsCommandHelp, commandValidationIssue } from './arguments.js';
|
|
7
|
+
import { commandDescription, commandMessages } from './messages.js';
|
|
7
8
|
import { MENU_PAGE_SIZE, readMenuAppearance, renderCommandMenu } from './command-menu.js';
|
|
8
9
|
import { admitCommandExecution, admitDirectCommandExecution } from './execution-runtime.js';
|
|
9
10
|
import { CommandExecutionLimitedError, normalizeExecutionPolicy } from './execution-policy.js';
|
|
@@ -90,6 +91,7 @@ export class CommandIndex {
|
|
|
90
91
|
...(shortcutKeys && shortcutKeys.length > 0 ? { shortcut: shortcutKeys } : {}),
|
|
91
92
|
slot,
|
|
92
93
|
segments: Object.freeze(primarySegments),
|
|
94
|
+
preferredSegments: Object.freeze(Object.fromEntries(Object.entries(slot.definition.preferredAliasI18n ?? {}).map(([locale, entry]) => [locale, Object.freeze([...namespace, ...aliasRuntimeSegments(entry, primarySegments.slice(namespace.length))])]))),
|
|
93
95
|
parameter,
|
|
94
96
|
});
|
|
95
97
|
claim([...new Set([occupancyKey(primarySegments, parameter), ...declaredRouteShapes(primarySegments, slot.definition)])], slot.source);
|
|
@@ -130,8 +132,30 @@ export class CommandIndex {
|
|
|
130
132
|
this.#routes = Object.freeze(routes.sort(compareRoutes));
|
|
131
133
|
this.#shortcuts = shortcuts;
|
|
132
134
|
}
|
|
133
|
-
list() {
|
|
134
|
-
return this.#commands.map(toDescriptor)
|
|
135
|
+
list(locale = 'zh-CN') {
|
|
136
|
+
return this.#commands.map(record => ({ ...toDescriptor(record),
|
|
137
|
+
name: commandDisplayName(record, locale), usage: commandUsage(commandDisplayName(record, locale), record.slot.definition),
|
|
138
|
+
description: commandDescription(record.slot.definition, locale),
|
|
139
|
+
parameters: record.parameters.map(parameter => ({ ...parameter, description: commandDescription(parameter, locale) })),
|
|
140
|
+
options: Object.freeze(Object.fromEntries(Object.entries(record.options).map(([key, schema]) => [key, { ...schema, description: commandDescription(schema, locale) }]))),
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
/** Resolve a local static route without executing parameters, permission checks or commands. */
|
|
144
|
+
formatCommand(owner, localPath, prefix, locale = 'zh-CN') {
|
|
145
|
+
if (prefix === undefined || typeof localPath !== 'string')
|
|
146
|
+
return undefined;
|
|
147
|
+
const path = localPath.trim().split(/\s+/u).filter(Boolean).join(' ');
|
|
148
|
+
if (!path)
|
|
149
|
+
return undefined;
|
|
150
|
+
const command = this.#commands.find(record => record.owner === owner
|
|
151
|
+
&& record.slot.localName.split('/').filter(segment => !segment.startsWith('$')).join(' ') === path);
|
|
152
|
+
return command ? prefix + commandDisplaySegments(command, locale).filter(segment => !segment.startsWith('$')).join(' ') : undefined;
|
|
153
|
+
}
|
|
154
|
+
/** Return the configured menu entry only when the calling environment is known. */
|
|
155
|
+
formatMenu(prefix, locale) {
|
|
156
|
+
if (prefix === undefined || !this.#menu)
|
|
157
|
+
return undefined;
|
|
158
|
+
return prefix + (this.#menu.keyword ?? commandMessages(locale, 'menuKeyword'));
|
|
135
159
|
}
|
|
136
160
|
has(name) {
|
|
137
161
|
try {
|
|
@@ -144,6 +168,7 @@ export class CommandIndex {
|
|
|
144
168
|
}
|
|
145
169
|
}
|
|
146
170
|
async execute(name, args = [], operationSnapshot = this.snapshot) {
|
|
171
|
+
const locale = commandLocale(undefined, operationSnapshot);
|
|
147
172
|
const match = this.#match(name, false);
|
|
148
173
|
if (!match) {
|
|
149
174
|
this.#diagnoseParameter(name);
|
|
@@ -153,28 +178,34 @@ export class CommandIndex {
|
|
|
153
178
|
throw new Error(`Permission denied for Command: ${match.command.name}`);
|
|
154
179
|
const definition = match.command.slot.definition;
|
|
155
180
|
const supplied = [...match.remaining, ...(args.length > 0 ? [{ type: 'text', data: { text: args.join(' ') } }] : [])];
|
|
156
|
-
const parsed = parseCommandArguments(definition, supplied, resolveDynamicParams(match.params, undefined), {});
|
|
181
|
+
const parsed = parseCommandArguments(definition, supplied, resolveDynamicParams(match.params, undefined), {}, locale);
|
|
157
182
|
if (parsed.issues.length > 0)
|
|
158
|
-
throw new CommandArgumentValidationError(parsed.issues, commandHelp(match.command
|
|
183
|
+
throw new CommandArgumentValidationError(parsed.issues, commandHelp(commandDisplayName(match.command, locale), definition, '', locale));
|
|
159
184
|
// No inbound source: execution policy applies per command, never as a blanket refusal.
|
|
160
185
|
const admission = admitDirectCommandExecution(match.command.slot.id, match.command.execution, operationSnapshot);
|
|
161
186
|
if (admission.limit)
|
|
162
187
|
throw new CommandExecutionLimitedError(admission.limit);
|
|
163
188
|
try {
|
|
164
|
-
return await definition.execute(createCommandContext(operationSnapshot, match.command.slot.owner,
|
|
189
|
+
return await definition.execute(createCommandContext({ snapshot: operationSnapshot, ownerId: match.command.slot.owner,
|
|
190
|
+
adapter: definition.adapter, presentation: { locale } }, {
|
|
191
|
+
args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options,
|
|
192
|
+
}));
|
|
165
193
|
}
|
|
166
194
|
finally {
|
|
167
195
|
admission.release();
|
|
168
196
|
}
|
|
169
197
|
}
|
|
170
|
-
async dispatch(input, source
|
|
198
|
+
async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot, locale: localeOverride }) {
|
|
199
|
+
const locale = localeOverride ?? commandLocale(source, operationSnapshot);
|
|
200
|
+
const prefix = commandPrefix ?? '';
|
|
171
201
|
if (this.#menu) {
|
|
172
|
-
const
|
|
202
|
+
const keyword = this.#menu.keyword ?? commandMessages(locale, 'menuKeyword');
|
|
203
|
+
const menuResult = this.#dispatchMenu(input, { commandPrefix: prefix, snapshot: operationSnapshot, locale, keyword });
|
|
173
204
|
if (menuResult !== undefined) {
|
|
174
205
|
return Object.freeze({
|
|
175
206
|
matched: true,
|
|
176
207
|
status: 'executed',
|
|
177
|
-
command:
|
|
208
|
+
command: keyword,
|
|
178
209
|
owner: operationSnapshot.root,
|
|
179
210
|
...menuResult,
|
|
180
211
|
});
|
|
@@ -192,12 +223,12 @@ export class CommandIndex {
|
|
|
192
223
|
return Object.freeze({ matched: false, status: 'unmatched' });
|
|
193
224
|
if (!(await this.#permitAllows(candidate, source, operationSnapshot)))
|
|
194
225
|
return this.#denied(candidate);
|
|
195
|
-
const help = commandHelp(candidate
|
|
226
|
+
const help = commandHelp(commandDisplayName(candidate, locale), candidate.slot.definition, prefix, locale);
|
|
196
227
|
const segments = normalizeSegments(typeof input === 'string' ? [{ type: 'text', data: { text: input } }] : input);
|
|
197
228
|
if (requestsCommandHelp(segments))
|
|
198
229
|
return Object.freeze({ matched: true, status: 'executed', command: candidate.name, owner: candidate.slot.owner, value: help });
|
|
199
230
|
return Object.freeze({ matched: true, status: 'invalid', command: candidate.name, owner: candidate.slot.owner,
|
|
200
|
-
issues: Object.freeze([
|
|
231
|
+
issues: Object.freeze([commandValidationIssue('invalidParameter', candidate.parameter?.name ?? 'arguments', locale)]), help });
|
|
201
232
|
}
|
|
202
233
|
if (!commandAdapterMatches(match.command, source))
|
|
203
234
|
return Object.freeze({ matched: false, status: 'unmatched' });
|
|
@@ -205,18 +236,22 @@ export class CommandIndex {
|
|
|
205
236
|
return this.#denied(match.command);
|
|
206
237
|
const definition = match.command.slot.definition;
|
|
207
238
|
const authorSource = bindOperationInput(source, match.command.slot.owner, operationSnapshot);
|
|
208
|
-
const help = commandHelp(match.command
|
|
239
|
+
const help = commandHelp(commandDisplayName(match.command, locale), definition, prefix, locale);
|
|
209
240
|
if (match.helpRequested) {
|
|
210
241
|
return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value: help });
|
|
211
242
|
}
|
|
212
|
-
const parsed = parseCommandArguments(definition, match.remaining, resolveDynamicParams(match.params, authorSource), resolveCommandSession(authorSource));
|
|
243
|
+
const parsed = parseCommandArguments(definition, match.remaining, resolveDynamicParams(match.params, authorSource), resolveCommandSession(authorSource), locale);
|
|
213
244
|
if (parsed.issues.length > 0)
|
|
214
245
|
return Object.freeze({ matched: true, status: 'invalid', command: match.command.name,
|
|
215
246
|
owner: match.command.slot.owner, issues: parsed.issues, help });
|
|
216
247
|
const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
|
|
217
248
|
if (limit)
|
|
218
249
|
return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
|
|
219
|
-
const value = await definition.execute(createCommandContext(
|
|
250
|
+
const value = await definition.execute(createCommandContext({
|
|
251
|
+
snapshot: operationSnapshot, ownerId: match.command.slot.owner, input: authorSource,
|
|
252
|
+
interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
|
|
253
|
+
presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath, source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
|
|
254
|
+
}, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options }));
|
|
220
255
|
return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
|
|
221
256
|
}
|
|
222
257
|
#denied(record) {
|
|
@@ -334,16 +369,13 @@ export class CommandIndex {
|
|
|
334
369
|
// ==========================================================================
|
|
335
370
|
// 内置菜单命令
|
|
336
371
|
// ==========================================================================
|
|
337
|
-
#dispatchMenu(input,
|
|
372
|
+
#dispatchMenu(input, context) {
|
|
373
|
+
const { commandPrefix, keyword, locale } = context;
|
|
338
374
|
const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
|
|
339
375
|
if (text === undefined)
|
|
340
376
|
return undefined;
|
|
341
|
-
const menu = this.#menu;
|
|
342
|
-
if (!menu)
|
|
343
|
-
return undefined;
|
|
344
|
-
const keyword = menu.keyword;
|
|
345
377
|
if (text === keyword)
|
|
346
|
-
return this.#buildMenu(
|
|
378
|
+
return this.#buildMenu({}, context);
|
|
347
379
|
if (text.startsWith(keyword + ' ')) {
|
|
348
380
|
const key = text.slice(keyword.length + 1).trim();
|
|
349
381
|
const args = key.split(/\s+/u);
|
|
@@ -352,24 +384,24 @@ export class CommandIndex {
|
|
|
352
384
|
const pageText = args[flag + 1] ?? '';
|
|
353
385
|
if (flag > 1 || args.length !== flag + 2 || !/^[1-9]\d*$/u.test(pageText)
|
|
354
386
|
|| !Number.isSafeInteger(Number(pageText))) {
|
|
355
|
-
return { value:
|
|
387
|
+
return { value: commandMessages(locale, 'invalidPage', { command: `${commandPrefix}${keyword}` }) };
|
|
356
388
|
}
|
|
357
|
-
return this.#buildMenu(flag === 1 ? args[0] : undefined,
|
|
389
|
+
return this.#buildMenu({ pluginKey: flag === 1 ? args[0] : undefined, page: Number(pageText) }, context);
|
|
358
390
|
}
|
|
359
391
|
if (key)
|
|
360
|
-
return this.#buildMenu(key
|
|
361
|
-
return this.#buildMenu(
|
|
392
|
+
return this.#buildMenu({ pluginKey: key }, context);
|
|
393
|
+
return this.#buildMenu({}, context);
|
|
362
394
|
}
|
|
363
395
|
return undefined;
|
|
364
396
|
}
|
|
365
|
-
#buildMenu(pluginKey, commandPrefix, keyword, snapshot,
|
|
397
|
+
#buildMenu({ pluginKey, page = 1 }, { commandPrefix, keyword, snapshot, locale }) {
|
|
366
398
|
const root = snapshot.root;
|
|
367
399
|
const targetId = pluginKey
|
|
368
400
|
? `${root}/${pluginKey.split('.').join('/')}`
|
|
369
401
|
: root;
|
|
370
402
|
const node = snapshot.tree.get(targetId);
|
|
371
403
|
if (!node)
|
|
372
|
-
return { value:
|
|
404
|
+
return { value: commandMessages(locale, 'missingPlugin', { plugin: pluginKey ?? '' }) };
|
|
373
405
|
const directCommands = this.#commands.filter((cmd) => cmd.slot.owner === targetId);
|
|
374
406
|
const children = node.children
|
|
375
407
|
.map((childId) => snapshot.tree.get(childId))
|
|
@@ -377,24 +409,25 @@ export class CommandIndex {
|
|
|
377
409
|
const pages = Math.max(1, Math.ceil((directCommands.length + children.length) / MENU_PAGE_SIZE));
|
|
378
410
|
const invocation = `${commandPrefix}${keyword}${pluginKey ? ` ${pluginKey}` : ''}`;
|
|
379
411
|
if (page > pages)
|
|
380
|
-
return { value:
|
|
412
|
+
return { value: commandMessages(locale, 'pageOutOfRange', { pages, command: invocation }) };
|
|
381
413
|
const offset = (page - 1) * MENU_PAGE_SIZE;
|
|
382
414
|
const pageCommands = directCommands.slice(offset, offset + MENU_PAGE_SIZE);
|
|
383
415
|
const pageChildren = children.slice(Math.max(0, offset - directCommands.length), Math.max(0, offset + MENU_PAGE_SIZE - directCommands.length));
|
|
384
416
|
const displayKey = pluginKey ?? node.instanceKey;
|
|
385
|
-
const lines = [
|
|
417
|
+
const lines = [commandMessages(locale, 'menuTitle', { plugin: displayKey })];
|
|
386
418
|
if (pages > 1)
|
|
387
|
-
lines.push(
|
|
419
|
+
lines.push(commandMessages(locale, 'menuSummary', { page, pages, commands: directCommands.length, plugins: children.length }));
|
|
388
420
|
if (pageCommands.length > 0) {
|
|
389
421
|
lines.push('');
|
|
390
422
|
for (const cmd of pageCommands) {
|
|
391
|
-
const
|
|
392
|
-
|
|
423
|
+
const description = commandDescription(cmd.slot.definition, locale);
|
|
424
|
+
const desc = description ? ` - ${description}` : '';
|
|
425
|
+
lines.push(` ${commandPrefix}${commandDisplayName(cmd, locale)}${desc}`);
|
|
393
426
|
}
|
|
394
427
|
}
|
|
395
428
|
if (pageChildren.length > 0) {
|
|
396
429
|
lines.push('');
|
|
397
|
-
lines.push('
|
|
430
|
+
lines.push(commandMessages(locale, 'childPlugins'));
|
|
398
431
|
for (const child of pageChildren) {
|
|
399
432
|
const childKey = pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey;
|
|
400
433
|
const label = child.metadata?.displayName ?? child.instanceKey;
|
|
@@ -403,32 +436,51 @@ export class CommandIndex {
|
|
|
403
436
|
}
|
|
404
437
|
if (directCommands.length === 0 && children.length === 0) {
|
|
405
438
|
lines.push('');
|
|
406
|
-
lines.push('
|
|
439
|
+
lines.push(commandMessages(locale, 'emptyMenu'));
|
|
407
440
|
}
|
|
408
441
|
const hints = [];
|
|
409
442
|
if (children.length > 0)
|
|
410
|
-
hints.push(
|
|
443
|
+
hints.push(commandMessages(locale, 'pluginHint', { command: `${commandPrefix}${keyword}` }));
|
|
411
444
|
if (page > 1)
|
|
412
|
-
hints.push(
|
|
445
|
+
hints.push(commandMessages(locale, 'previousPage', { command: `${invocation} --page ${page - 1}` }));
|
|
413
446
|
if (page < pages)
|
|
414
|
-
hints.push(
|
|
447
|
+
hints.push(commandMessages(locale, 'nextPage', { command: `${invocation} --page ${page + 1}` }));
|
|
415
448
|
const hint = hints.length ? hints.join('\n') : undefined;
|
|
416
449
|
if (hint) {
|
|
417
450
|
lines.push('');
|
|
418
451
|
lines.push(hint);
|
|
419
452
|
}
|
|
420
453
|
const text = lines.join('\n');
|
|
421
|
-
const menu = Object.freeze({ keyword, ...(pluginKey ? { pluginKey } : {}), page, pages });
|
|
422
|
-
if (!snapshot.resources.get(root)?.has(htmlRendererToken.id))
|
|
423
|
-
return { value: text, menu };
|
|
424
454
|
const config = snapshot.config.get(root);
|
|
425
|
-
|
|
455
|
+
const menu = Object.freeze({
|
|
456
|
+
keyword, ...(pluginKey ? { pluginKey } : {}), page, pages, locale, title: displayKey,
|
|
457
|
+
commands: Object.freeze(pageCommands.map(cmd => Object.freeze({
|
|
458
|
+
name: `${commandPrefix}${commandDisplayName(cmd, locale)}`, description: commandDescription(cmd.slot.definition, locale),
|
|
459
|
+
}))),
|
|
460
|
+
plugins: Object.freeze(pageChildren.map(child => Object.freeze({
|
|
426
461
|
name: pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey,
|
|
427
462
|
description: child.metadata?.displayName ?? child.instanceKey,
|
|
428
|
-
})),
|
|
429
|
-
|
|
463
|
+
}))),
|
|
464
|
+
commandTotal: directCommands.length, pluginTotal: children.length, hint, text,
|
|
465
|
+
appearance: Object.freeze(readMenuAppearance(config?.menuStyle)),
|
|
466
|
+
});
|
|
467
|
+
if (!snapshot.resources.get(root)?.has(htmlRendererToken.id))
|
|
468
|
+
return { value: text, menu };
|
|
469
|
+
return { menu, value: renderCommandMenu(displayKey, menu.commands, menu.plugins, hint, text, {
|
|
470
|
+
page, pages, locale, commandTotal: menu.commandTotal, pluginTotal: menu.pluginTotal, appearance: menu.appearance,
|
|
471
|
+
}) };
|
|
430
472
|
}
|
|
431
473
|
}
|
|
474
|
+
function commandDisplaySegments(record, locale) {
|
|
475
|
+
return record.preferredSegments[locale] ?? record.segments;
|
|
476
|
+
}
|
|
477
|
+
function commandDisplayName(record, locale) {
|
|
478
|
+
return displayName(commandDisplaySegments(record, locale), record.parameter);
|
|
479
|
+
}
|
|
480
|
+
function commandLocale(source, snapshot) {
|
|
481
|
+
const locale = source && typeof source === 'object' && 'locale' in source ? source.locale : undefined;
|
|
482
|
+
return resolveLocale(locale ?? snapshot.config.get(snapshot.root)?.locale);
|
|
483
|
+
}
|
|
432
484
|
export function isCommandIndex(value) {
|
|
433
485
|
return !!value && typeof value === 'object'
|
|
434
486
|
&& value.$projection === 'zhin.command-index/1';
|