@zhin.js/command 1.1.17 → 1.2.1

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/lib/messages.js CHANGED
@@ -6,6 +6,7 @@ export function commandDescription(value, locale) {
6
6
  /** Static dictionaries; the caller supplies the operation's language. */
7
7
  export const commandMessages = createMessages({
8
8
  menuKeyword: { 'zh-CN': '菜单', en: 'menu' },
9
+ progress: { 'zh-CN': '正在处理中,请稍候。', en: 'Working on it, please wait.' },
9
10
  invalidPage: { 'zh-CN': '页码必须是正整数。用法:{command} [插件名] --page <页码>', en: 'The page must be a positive integer. Usage: {command} [plugin] --page <page>' },
10
11
  missingPlugin: { 'zh-CN': '未找到插件: {plugin}', en: 'Plugin not found: {plugin}' },
11
12
  pageOutOfRange: { 'zh-CN': '页码超出范围:共 {pages} 页。请使用「{command} --page <页码>」。', en: 'Page out of range: {pages} pages available. Use {command} --page <page>.' },
@@ -54,5 +55,5 @@ export const commandMessages = createMessages({
54
55
  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' },
55
56
  simulatedIdentity: { 'zh-CN': '身份与角色仅用于模拟,未向平台核实', en: 'Identity and roles are hypothetical and have not been verified with the platform' },
56
57
  unevaluatedCode: { 'zh-CN': '未执行命令、动态默认值、平台或自定义 checker', en: 'Commands, dynamic defaults, platform checks and custom checkers have not been executed' },
57
- 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' },
58
+ defaultedRole: { 'zh-CN': '输入未附聊天角色时按普通用户(无 owner / admin / trusted)解释;这不是已核验的角色', en: 'Input without a chat role is treated as an ordinary user (no owner / admin / trusted); this is not a verified role' },
58
59
  });
@@ -20,14 +20,13 @@ export function explainCommandPermissions(command, options) {
20
20
  const inputSender = options.source && typeof options.source === 'object'
21
21
  ? options.source.sender : undefined;
22
22
  const structuredRoles = inputSender && typeof inputSender === 'object'
23
- && Array.isArray(inputSender.role);
23
+ && 'chatRole' in inputSender;
24
24
  // A sender-only assumption has no synthetic conversation. Apply the real canonical
25
25
  // sender defaults without inventing an Endpoint or treating unknown roles as known.
26
26
  const senderOnly = options.source && typeof options.source === 'object' && !('conversation' in options.source) && !structuredRoles
27
27
  ? resolveCommandSender(options.source, undefined) : undefined;
28
28
  const subject = senderOnly ? { ...projected, sender: senderOnly } : projected;
29
- const defaultedRoles = options.rolesKnown && Array.isArray(inputSender?.roles) && inputSender.roles.length === 0
30
- && subject.sender?.role.length === 1 && subject.sender.role[0] === 'user';
29
+ const defaultedRoles = options.rolesKnown === true && !!subject.sender && subject.sender.chatRole === undefined;
31
30
  const actualAdapter = operationClientAdapter(options.source);
32
31
  const adapter = !command.adapter
33
32
  ? { outcome: 'not_evaluated', reason: commandMessages(locale, 'noAdapter') }
package/lib/permit.d.ts CHANGED
@@ -3,11 +3,14 @@
3
3
  * 保留 CommandSession 兼容签名供 command-index 过渡使用。
4
4
  */
5
5
  import { type PermissionSubject } from '@zhin.js/permission';
6
- /** 会话管理者:群主 / 群管理员 / 机器人主人(适配器投影的规范角色名)。 */
7
- export declare const CONVERSATION_MANAGER_PERMIT = "role(owner,admin,master)";
8
6
  /**
9
- * 「私聊,或 owner / admin / master」。`permit` 列表只支持 AND,
10
- * 订阅类命令需要这个 OR 规则时用它,并以命令会话的 `scene` / `sender.role` 为输入。
7
+ * 会话管理者:本会话层级链上的 zhin 聊天角色 `admin`(含 `owner`)。平台群主 / 管理员不自动具备
8
+ * (ADR 0056 §4,2026-11-12 修订):订阅等 zhin 功能需要被授予的角色。
9
+ */
10
+ export declare const CONVERSATION_MANAGER_PERMIT = "admin";
11
+ /**
12
+ * 「私聊,或本会话 admin」。`permit` 列表只支持 AND,
13
+ * 订阅类命令需要这个 OR 规则时用它,并以命令会话的 `scene` / `sender.chatRole` 为输入。
11
14
  */
12
15
  export declare function canManageConversation(subject: PermissionSubject): boolean;
13
- export { type PermitKind, type ParsedPermit, parsePermitName, isBuiltinPermit, isPlatformPermit, assertPermitSyntax as assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, } from '@zhin.js/permission';
16
+ export { type PermitKind, type ParsedPermit, parsePermitName, isBuiltinPermit, removedPermitReplacement, assertPermitSyntax as assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, } from '@zhin.js/permission';
package/lib/permit.js CHANGED
@@ -3,14 +3,17 @@
3
3
  * 保留 CommandSession 兼容签名供 command-index 过渡使用。
4
4
  */
5
5
  import { checkBuiltinPermit } from '@zhin.js/permission';
6
- /** 会话管理者:群主 / 群管理员 / 机器人主人(适配器投影的规范角色名)。 */
7
- export const CONVERSATION_MANAGER_PERMIT = 'role(owner,admin,master)';
8
6
  /**
9
- * 「私聊,或 owner / admin / master」。`permit` 列表只支持 AND,
10
- * 订阅类命令需要这个 OR 规则时用它,并以命令会话的 `scene` / `sender.role` 为输入。
7
+ * 会话管理者:本会话层级链上的 zhin 聊天角色 `admin`(含 `owner`)。平台群主 / 管理员不自动具备
8
+ * (ADR 0056 §4,2026-11-12 修订):订阅等 zhin 功能需要被授予的角色。
9
+ */
10
+ export const CONVERSATION_MANAGER_PERMIT = 'admin';
11
+ /**
12
+ * 「私聊,或本会话 admin」。`permit` 列表只支持 AND,
13
+ * 订阅类命令需要这个 OR 规则时用它,并以命令会话的 `scene` / `sender.chatRole` 为输入。
11
14
  */
12
15
  export function canManageConversation(subject) {
13
16
  return checkBuiltinPermit('private()', subject)
14
17
  || checkBuiltinPermit(CONVERSATION_MANAGER_PERMIT, subject);
15
18
  }
16
- export { parsePermitName, isBuiltinPermit, isPlatformPermit, assertPermitSyntax as assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, } from '@zhin.js/permission';
19
+ export { parsePermitName, isBuiltinPermit, removedPermitReplacement, assertPermitSyntax as assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, } from '@zhin.js/permission';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.1.17",
3
+ "version": "1.2.1",
4
4
  "description": "Convention-based Command Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -16,15 +16,14 @@
16
16
  }
17
17
  },
18
18
  "files": [
19
- "lib",
20
- "src"
19
+ "lib"
21
20
  ],
22
21
  "dependencies": {
23
22
  "segment-matcher": "^1.0.5",
24
- "@zhin.js/feature-kit": "1.1.15",
25
- "@zhin.js/im-contract": "1.1.13",
26
- "@zhin.js/permission": "1.1.15",
27
- "@zhin.js/plugin-runtime": "1.1.23"
23
+ "@zhin.js/feature-kit": "1.1.16",
24
+ "@zhin.js/im-contract": "1.1.14",
25
+ "@zhin.js/permission": "1.2.0",
26
+ "@zhin.js/plugin-runtime": "1.2.0"
28
27
  },
29
28
  "devDependencies": {
30
29
  "@types/node": "^26.1.2",
package/src/arguments.ts DELETED
@@ -1,308 +0,0 @@
1
- import type {
2
- CommandDefinition, CommandParams, CommandOptions, CommandParamSchema,
3
- CommandParameterValue, CommandSegment, CommandSession,
4
- } from './definition.js';
5
- import type { Locale } from '@zhin.js/plugin-runtime';
6
- import { commandDescription, commandMessages } from './messages.js';
7
-
8
- export type CommandValidationIssueCode = 'expectedType' | 'expectedArray' | 'minimum' | 'maximum'
9
- | 'choices' | 'invalidFormat' | 'invalidValue' | 'unknownOption' | 'duplicateOption'
10
- | 'defaultUndefined' | 'parameterUndefined' | 'missingOption' | 'missingRequiredParam' | 'unexpectedArguments'
11
- | 'invalidParameter' | 'custom';
12
-
13
- export interface CommandValidationIssue {
14
- readonly field: string;
15
- readonly message: string;
16
- readonly code?: CommandValidationIssueCode;
17
- readonly parameters?: Readonly<Record<string, string | number>>;
18
- }
19
-
20
- /** Stable validation reason and values; never reverse-parse an English message. */
21
- export function commandValidationIssue(
22
- code: Exclude<CommandValidationIssueCode, 'custom'>,
23
- field: string,
24
- locale: Locale,
25
- parameters: Readonly<Record<string, string | number>> = {},
26
- ): CommandValidationIssue {
27
- const values = Object.freeze({ field, ...parameters });
28
- return Object.freeze({ field, code, parameters: values, message: commandMessages(locale, code, values) });
29
- }
30
- export interface ParsedCommandArguments {
31
- readonly params: Readonly<Record<string, CommandParameterValue>>;
32
- readonly options: Readonly<Record<string, CommandParameterValue>>;
33
- readonly remaining: readonly Readonly<CommandSegment>[];
34
- readonly issues: readonly CommandValidationIssue[];
35
- }
36
- const scalarTypes = new Set(['string', 'word', 'text', 'number', 'integer', 'float', 'boolean']);
37
-
38
- /** Validate the single declaration used by parsing, help and execution. */
39
- export function validateArgumentSchemas(params?: CommandParams, options?: CommandOptions): void {
40
- let optional = false;
41
- let rest = false;
42
- for (const [name, schema] of Object.entries(params ?? {})) {
43
- validateSchema(name, schema);
44
- if (rest) throw new TypeError('Command rest parameter must be last');
45
- if (optional && !schema.optional && schema.default === undefined) {
46
- throw new TypeError(`Required Command parameter ${name} cannot follow an optional parameter`);
47
- }
48
- optional ||= schema.optional === true || schema.default !== undefined;
49
- rest = schema.rest === true;
50
- }
51
- if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
52
- throw new TypeError('Command options must be a record');
53
- }
54
- const aliases = new Set<string>();
55
- for (const [name, schema] of Object.entries(options ?? {})) {
56
- validateSchema(name, schema);
57
- if (!scalarTypes.has(schema.type) || schema.type === 'text') {
58
- throw new TypeError(`Command option ${name} requires a scalar type`);
59
- }
60
- if (name === 'help') throw new TypeError('Command option help is reserved');
61
- if (schema.alias !== undefined) {
62
- if (!/^[a-zA-Z]$/u.test(schema.alias) || schema.alias === 'h' || aliases.has(schema.alias)) {
63
- throw new TypeError(`Invalid or duplicate Command option alias ${schema.alias}`);
64
- }
65
- aliases.add(schema.alias);
66
- }
67
- }
68
- }
69
- function validateSchema(name: string, schema: CommandParamSchema): void {
70
- if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/u.test(name)) throw new TypeError(`Invalid Command argument name ${name}`);
71
- if (!schema || typeof schema !== 'object') throw new TypeError(`Invalid Command argument ${name}`);
72
- if (schema.min !== undefined && !Number.isFinite(schema.min)
73
- || schema.max !== undefined && !Number.isFinite(schema.max)
74
- || schema.min !== undefined && schema.max !== undefined && schema.min > schema.max) {
75
- throw new TypeError(`Invalid Command argument bounds for ${name}`);
76
- }
77
- if (schema.pattern !== undefined && !(schema.pattern instanceof RegExp)) {
78
- throw new TypeError(`Command argument ${name}.pattern must be a RegExp`);
79
- }
80
- if (schema.validate !== undefined && typeof schema.validate !== 'function') {
81
- throw new TypeError(`Command argument ${name}.validate must be a function`);
82
- }
83
- if (schema.choices !== undefined && !Array.isArray(schema.choices)) {
84
- throw new TypeError(`Command argument ${name}.choices must be an array`);
85
- }
86
- if (schema.descriptionI18n !== undefined && (!schema.descriptionI18n || typeof schema.descriptionI18n !== 'object'
87
- || Array.isArray(schema.descriptionI18n)
88
- || Object.entries(schema.descriptionI18n).some(([locale, text]) => !['zh-CN', 'en'].includes(locale) || typeof text !== 'string'))) {
89
- throw new TypeError(`Command argument ${name}.descriptionI18n must map zh-CN/en to strings`);
90
- }
91
- if (schema.default !== undefined && !scalarTypes.has(schema.type)) throw new TypeError(`Structured Command argument ${name} cannot declare a default`);
92
- if (schema.default !== undefined && typeof schema.default !== 'function') {
93
- if (Array.isArray(schema.default) && !schema.rest) throw new TypeError(`Command argument ${name} requires rest: true for an array default`);
94
- const values = schema.rest ? schema.default : [schema.default];
95
- if (!Array.isArray(values) || values.some((value) => !valueMatches(schema, value))) {
96
- throw new TypeError(`Invalid Command argument default for ${name}`);
97
- }
98
- const defaultSchema = { ...schema, validate: undefined };
99
- const issue = validateValue(name, defaultSchema, schema.default, {});
100
- if (issue) throw new TypeError(`Invalid Command argument default for ${name}: ${issue.message}`);
101
- }
102
- }
103
-
104
- interface TextToken {
105
- readonly value: string;
106
- readonly quoted: boolean;
107
- readonly raw?: string;
108
- readonly before?: string;
109
- readonly first?: boolean;
110
- readonly after?: string;
111
- /** Original message segment, retained while options are removed. */
112
- readonly segment?: number;
113
- }
114
- type Token = TextToken | Readonly<CommandSegment>;
115
- function isTextToken(value: Token | undefined): value is TextToken {
116
- return value !== undefined && 'value' in value;
117
- }
118
- function tokenize(segments: readonly Readonly<CommandSegment>[]): Token[] {
119
- return segments.flatMap<Token>((segment, segmentIndex) => {
120
- if (segment.type !== 'text' || typeof segment.data.text !== 'string') return [segment];
121
- const tokens: Token[] = [];
122
- const pattern = /(--?[a-zA-Z][a-zA-Z0-9_-]*=)(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')|"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|(\S+)/gu;
123
- let end = 0;
124
- for (const match of segment.data.text.matchAll(pattern)) {
125
- const value = match[1] !== undefined ? match[1] + (match[2] ?? match[3] ?? '') : match[4] ?? match[5] ?? match[6] ?? '';
126
- tokens.push({ value: value.replace(/\\(["'\\])/gu, '$1'), quoted: match[1] === undefined && match[6] === undefined, raw: match[0], before: segment.data.text.slice(end, match.index), first: tokens.length === 0, segment: segmentIndex });
127
- end = (match.index ?? 0) + match[0].length;
128
- }
129
- const last = tokens.at(-1);
130
- if (isTextToken(last)) tokens[tokens.length - 1] = { ...last, after: segment.data.text.slice(end) };
131
- return tokens;
132
- });
133
- }
134
- /** Reserved flags use the same quoted-token and option-terminator rules as arguments. */
135
- export function requestsCommandHelp(segments: readonly Readonly<CommandSegment>[]): boolean {
136
- for (const token of tokenize(segments)) {
137
- if (!isTextToken(token) || token.quoted) continue;
138
- if (token.value === '--') return false;
139
- if (token.value === '--help' || token.value === '-h') return true;
140
- }
141
- return false;
142
- }
143
- function segmentsFromTokens(tokens: readonly Token[]): readonly Readonly<CommandSegment>[] {
144
- const result: CommandSegment[] = [];
145
- let lastTextSegment: number | undefined;
146
- for (const token of tokens) {
147
- if (isTextToken(token)) {
148
- const last = result.at(-1);
149
- const text = (token.raw ?? token.value) + (token.after ?? '');
150
- if (last?.type === 'text' && lastTextSegment === token.segment) result[result.length - 1] = { type: 'text', data: { text: `${last.data.text}${token.before || ' '}${text}` } };
151
- else result.push({ type: 'text', data: { text: (token.first ? token.before ?? '' : '') + text } });
152
- lastTextSegment = token.segment;
153
- } else { result.push(token); lastTextSegment = undefined; }
154
- }
155
- return Object.freeze(result);
156
- }
157
- function convert(schema: CommandParamSchema, token: Token | undefined): CommandParameterValue | undefined {
158
- if (!token) return undefined;
159
- if (isTextToken(token)) {
160
- const text = token.value;
161
- if (schema.type === 'string' || schema.type === 'word' || schema.type === 'text') return text;
162
- if (schema.type === 'boolean') return text === 'true' ? true : text === 'false' ? false : undefined;
163
- if (schema.type === 'number' || schema.type === 'integer' || schema.type === 'float') {
164
- const value = Number(text);
165
- return text !== '' && Number.isFinite(value) && (schema.type !== 'integer' || Number.isInteger(value))
166
- && (schema.type !== 'float' || text.includes('.')) ? value : undefined;
167
- }
168
- return undefined;
169
- }
170
- const type = typeof token.type === 'string' ? token.type : token.type.name;
171
- if (type !== schema.type && !(schema.type === 'mention' && type === 'at')) return undefined;
172
- const data = token.data;
173
- const value = schema.type === 'image' ? data.media ?? data.file ?? data.url ?? data.src
174
- : schema.type === 'mention' ? data.target ?? data.user_id ?? data.qq
175
- : schema.type === 'reply' ? data.message_id ?? data.reply_id ?? data.id
176
- : schema.type === 'forward' ? data.forward_id ?? data.res_id ?? data.message_id ?? data.id
177
- : schema.type === 'dice' || schema.type === 'rps' ? data.result : data.id;
178
- return value as CommandParameterValue | undefined;
179
- }
180
- function valueMatches(schema: CommandParamSchema, value: unknown): boolean {
181
- if (schema.type === 'number' || schema.type === 'integer' || schema.type === 'float' || schema.type === 'dice' || schema.type === 'rps') {
182
- return typeof value === 'number' && Number.isFinite(value) && (schema.type !== 'integer' || Number.isInteger(value));
183
- }
184
- if (schema.type === 'mention' || schema.type === 'face' || schema.type === 'reply' || schema.type === 'forward') return typeof value === 'string' || typeof value === 'number' && Number.isFinite(value);
185
- if (schema.type === 'boolean') return typeof value === 'boolean';
186
- if (schema.type === 'image') return typeof value === 'string' || !!value && typeof value === 'object';
187
- return typeof value === 'string';
188
- }
189
- function validateValue(field: string, schema: CommandParamSchema, value: CommandParameterValue, session: CommandSession, locale: Locale = 'zh-CN'): CommandValidationIssue | undefined {
190
- if (schema.rest && !Array.isArray(value)) return commandValidationIssue('expectedArray', field, locale, { type: schema.type });
191
- const values = schema.rest ? (Array.isArray(value) ? value : [value]) : [value];
192
- for (const item of values) {
193
- if (!valueMatches(schema, item)) return commandValidationIssue('expectedType', field, locale, { type: schema.type });
194
- const size = typeof item === 'number' ? item : typeof item === 'string' ? item.length : undefined;
195
- if (schema.min !== undefined && (size === undefined || size < schema.min)) return commandValidationIssue('minimum', field, locale, { minimum: schema.min });
196
- if (schema.max !== undefined && (size === undefined || size > schema.max)) return commandValidationIssue('maximum', field, locale, { maximum: schema.max });
197
- if (schema.choices && !schema.choices.includes(item)) return commandValidationIssue('choices', field, locale, { choices: schema.choices.join(', ') });
198
- if (schema.pattern) {
199
- const pattern = new RegExp(schema.pattern.source, schema.pattern.flags.replace(/[gy]/gu, ''));
200
- if (typeof item !== 'string' || !pattern.test(item)) return commandValidationIssue('invalidFormat', field, locale);
201
- }
202
- }
203
- const result = schema.validate?.(value, session);
204
- return typeof result === 'string' ? Object.freeze({ field, code: 'custom', message: result })
205
- : result === false ? commandValidationIssue('invalidValue', field, locale) : undefined;
206
- }
207
-
208
- /** Parse arguments after a route match; the CommandIndex remains the only dispatcher. */
209
- export function parseCommandArguments(
210
- definition: CommandDefinition,
211
- remaining: readonly Readonly<CommandSegment>[],
212
- initial: Readonly<Record<string, CommandParameterValue>>,
213
- session: CommandSession,
214
- locale: Locale = 'zh-CN',
215
- ): ParsedCommandArguments {
216
- const params: Record<string, CommandParameterValue> = { __proto__: null, ...initial };
217
- const options: Record<string, CommandParameterValue> = { __proto__: null };
218
- const issues: CommandValidationIssue[] = [];
219
- const tokens = tokenize(remaining);
220
- const positional: Token[] = [];
221
- const schemas: CommandOptions | undefined = definition.options ?? (definition.params === undefined ? undefined : {});
222
- const optionNames = new Map(Object.entries(schemas ?? {}).flatMap<[string, string]>(([name, schema]) => [[`--${name}`, name], ...(schema.alias ? [[`-${schema.alias}`, name] as [string, string]] : [])]));
223
- let terminated = false;
224
- for (let index = 0; index < tokens.length; index++) {
225
- const token = tokens[index];
226
- if (!token) continue;
227
- if (!isTextToken(token) || token.quoted || terminated || schemas === undefined) { positional.push(token); continue; }
228
- if (token.value === '--') { terminated = true; continue; }
229
- if (!/^--?[a-zA-Z]/u.test(token.value)) { positional.push(token); continue; }
230
- const [rawFlag, inline] = token.value.split(/=(.*)/u);
231
- const flag = rawFlag ?? token.value;
232
- const name = optionNames.get(flag);
233
- if (!name) { issues.push(commandValidationIssue('unknownOption', flag, locale)); continue; }
234
- const schema = schemas[name];
235
- if (!schema) continue;
236
- if (Object.hasOwn(options, name)) { issues.push(commandValidationIssue('duplicateOption', name, locale)); continue; }
237
- let value: CommandParameterValue | undefined;
238
- if (inline !== undefined) value = convert(schema, { value: inline, quoted: false });
239
- else if (schema.type === 'boolean') value = true;
240
- else {
241
- const next = tokens[index + 1];
242
- if (isTextToken(next) && !(!next.quoted && /^--?[a-zA-Z]/u.test(next.value))) { index++; value = convert(schema, next); }
243
- }
244
- if (value === undefined) issues.push(commandValidationIssue('expectedType', name, locale, { field: `--${name}`, type: schema.type }));
245
- else options[name] = value;
246
- }
247
- for (const [name, schema] of Object.entries(schemas ?? {})) {
248
- if (!Object.hasOwn(options, name) && schema.default !== undefined) options[name] = typeof schema.default === 'function' ? schema.default(session) : schema.default;
249
- if (Object.hasOwn(options, name) && options[name] === undefined) issues.push(commandValidationIssue('defaultUndefined', name, locale, { field: `--${name}` }));
250
- else if (options[name] !== undefined) { const issue = validateValue(`--${name}`, schema, options[name], session, locale); if (issue) issues.push(issue); }
251
- else if (schema.optional === false) issues.push(commandValidationIssue('missingOption', name, locale));
252
- }
253
- const declared = Object.entries(definition.params ?? {});
254
- for (const [name, declaredSchema] of declared) {
255
- const schema = definition.$parameter?.name === name ? { ...declaredSchema, optional: definition.$parameter.optional ?? declaredSchema.optional, rest: definition.$parameter.rest ?? declaredSchema.rest } : declaredSchema;
256
- if (!Object.hasOwn(params, name)) {
257
- let value: CommandParameterValue | undefined;
258
- if (schema.rest) {
259
- const raw = positional.splice(0);
260
- const converted = schema.type === 'text'
261
- ? segmentsFromTokens(raw).map((segment) => segment.type === 'text' && typeof segment.data.text === 'string' ? segment.data.text : undefined)
262
- : raw.map((token) => convert(schema, token));
263
- if (converted.some((item) => item === undefined)) issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
264
- else if (converted.length > 0 || schema.optional) value = converted as readonly (string | number | boolean | Readonly<Record<string, unknown>>)[];
265
- } else if (schema.type === 'text') {
266
- const raw: string[] = [];
267
- while (isTextToken(positional[0])) {
268
- const token = positional.shift() as Extract<Token, { value: string }>;
269
- raw.push(`${raw.length > 0 ? token.before || ' ' : ''}${token.raw ?? token.value}${token.after ?? ''}`);
270
- }
271
- if (raw.length > 0) value = raw.join('');
272
- } else if (positional.length > 0) {
273
- value = convert(schema, positional.shift());
274
- // Defaults apply to omitted input, never to a supplied value of the wrong type.
275
- if (value === undefined) {
276
- issues.push(commandValidationIssue('expectedType', name, locale, { type: schema.type }));
277
- continue;
278
- }
279
- }
280
- if (value !== undefined) params[name] = value;
281
- else if (schema.default !== undefined) params[name] = typeof schema.default === 'function' ? schema.default(session) : schema.default;
282
- else if (!schema.optional && !(definition.$parameter?.name === name && definition.$parameter.optional)) {
283
- issues.push(commandValidationIssue('missingRequiredParam', name, locale, { type: schema.type }));
284
- }
285
- }
286
- if (Object.hasOwn(params, name) && params[name] === undefined) issues.push(commandValidationIssue('parameterUndefined', name, locale));
287
- else if (params[name] !== undefined) { const effective = definition.$parameter?.name === name && definition.$parameter.rest ? { ...schema, rest: true } : schema; const issue = validateValue(name, effective, params[name], session, locale); if (issue) issues.push(issue); }
288
- }
289
- if (declared.length > 0 && !definition.$parameter && positional.length > 0) issues.push(commandValidationIssue('unexpectedArguments', 'arguments', locale));
290
- 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) });
291
- }
292
-
293
- export function commandUsage(name: string, definition: CommandDefinition): string {
294
- const parameters = Object.entries(definition.params ?? {}).filter(([key]) => key !== definition.$parameter?.name)
295
- .map(([key, schema]) => `${schema.optional || schema.default !== undefined ? '[' : '<'}${schema.rest ? '...' : ''}${key}${schema.optional || schema.default !== undefined ? ']' : '>'}`);
296
- const options = Object.entries(definition.options ?? {}).map(([key, schema]) => `[--${key}${schema.type === 'boolean' ? '' : ` <${schema.type}>`}]`);
297
- return [name, ...parameters, ...options].join(' ');
298
- }
299
- export function commandHelp(name: string, definition: CommandDefinition, prefix = '', locale: Locale = 'zh-CN'): string {
300
- return [prefix + commandUsage(name, definition), commandDescription(definition, locale),
301
- ...Object.entries(definition.params ?? {}).map(([key, schema]) => argumentHelp(key, schema, locale)),
302
- ...Object.entries(definition.options ?? {}).map(([key, schema]) => argumentHelp(`--${key}${schema.alias ? ` (-${schema.alias})` : ''}`, schema, locale)),
303
- ].filter(Boolean).join('\n');
304
- }
305
- function argumentHelp(name: string, schema: CommandParamSchema, locale: Locale): string {
306
- const description = commandDescription(schema, locale);
307
- 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}` : ''}`;
308
- }