@zhin.js/command 1.1.17 → 1.2.0
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 +3 -3
- package/lib/command-index.d.ts +8 -0
- package/lib/command-index.js +55 -23
- package/lib/definition.d.ts +17 -3
- package/lib/definition.js +23 -38
- package/lib/execution-runtime.d.ts +3 -1
- package/lib/execution-runtime.js +3 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/messages.d.ts +1 -1
- package/lib/messages.js +2 -1
- package/lib/permission-explanation.js +2 -3
- package/lib/permit.d.ts +8 -5
- package/lib/permit.js +8 -5
- package/package.json +6 -7
- package/src/arguments.ts +0 -308
- package/src/command-index.ts +0 -1063
- package/src/command-menu.ts +0 -116
- package/src/definition.ts +0 -816
- package/src/execution-policy.ts +0 -39
- package/src/execution-runtime.ts +0 -202
- package/src/index.ts +0 -31
- package/src/messages.ts +0 -63
- package/src/permission-explanation.ts +0 -61
- package/src/permit.ts +0 -28
- package/src/provider.ts +0 -320
package/src/definition.ts
DELETED
|
@@ -1,816 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command authoring API consumed from `zhin.js/command`.
|
|
3
|
-
* @module zhin.js/command
|
|
4
|
-
*/
|
|
5
|
-
import { resolveLocale, type Locale, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
6
|
-
import {
|
|
7
|
-
createCapabilityContext,
|
|
8
|
-
type CapabilityOperation,
|
|
9
|
-
readOperationClient,
|
|
10
|
-
readOperationSession,
|
|
11
|
-
type OperationSession,
|
|
12
|
-
readOperationInputSignal,
|
|
13
|
-
type AdapterClient,
|
|
14
|
-
type RegisteredAdapterName,
|
|
15
|
-
type CapabilityContext,
|
|
16
|
-
} from '@zhin.js/feature-kit';
|
|
17
|
-
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
18
|
-
import { validateArgumentSchemas } from './arguments.js';
|
|
19
|
-
import type { UserInteraction } from '@zhin.js/im-contract';
|
|
20
|
-
import { normalizeExecutionPolicy, type CommandExecutionPolicy } from './execution-policy.js';
|
|
21
|
-
export type { CommandExecutionPolicy, CommandExecutionLimit } from './execution-policy.js';
|
|
22
|
-
|
|
23
|
-
const commandBrand = 'zhin.command/1' as const;
|
|
24
|
-
|
|
25
|
-
export type CommandParameterType =
|
|
26
|
-
| 'string'
|
|
27
|
-
| 'number'
|
|
28
|
-
| 'integer'
|
|
29
|
-
| 'float'
|
|
30
|
-
| 'boolean'
|
|
31
|
-
| 'word'
|
|
32
|
-
| 'text'
|
|
33
|
-
| 'mention'
|
|
34
|
-
| 'image'
|
|
35
|
-
| 'face'
|
|
36
|
-
| 'reply'
|
|
37
|
-
| 'forward'
|
|
38
|
-
| 'dice'
|
|
39
|
-
| 'rps';
|
|
40
|
-
export type CommandParameterValue =
|
|
41
|
-
| string
|
|
42
|
-
| number
|
|
43
|
-
| boolean
|
|
44
|
-
| ReadonlyArray<string | number | boolean | Readonly<Record<string, unknown>>>
|
|
45
|
-
| Readonly<Record<string, unknown>>
|
|
46
|
-
| null;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 可从运行时会话上下文动态解析的参数值。
|
|
50
|
-
*
|
|
51
|
-
* 静态值直接使用;函数值在命令派发时接收 {@link CommandSession},
|
|
52
|
-
* 返回最终的 {@link CommandParameterValue}。适用于 shortcut 预填
|
|
53
|
-
* 和 params.default。
|
|
54
|
-
*
|
|
55
|
-
* ```ts
|
|
56
|
-
* defineCommand({
|
|
57
|
-
* params: {
|
|
58
|
-
* user_id: { type: 'string', default: (s) => s.sender?.id ?? '' },
|
|
59
|
-
* },
|
|
60
|
-
* shortcut: {
|
|
61
|
-
* '查看我的信息': { user_id: (s) => s.sender?.id ?? '' },
|
|
62
|
-
* },
|
|
63
|
-
* execute: ({ params }) => `profile:${params.user_id}`,
|
|
64
|
-
* })
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
export type CommandDynamicValue =
|
|
68
|
-
| CommandParameterValue
|
|
69
|
-
| ((session: CommandSession) => CommandParameterValue);
|
|
70
|
-
|
|
71
|
-
/** @internal Runtime validator lookup. */
|
|
72
|
-
export const commandParameterTypes: ReadonlySet<CommandParameterType> = new Set([
|
|
73
|
-
'string',
|
|
74
|
-
'number',
|
|
75
|
-
'integer',
|
|
76
|
-
'float',
|
|
77
|
-
'boolean',
|
|
78
|
-
'word',
|
|
79
|
-
'text',
|
|
80
|
-
'mention',
|
|
81
|
-
'image',
|
|
82
|
-
'face',
|
|
83
|
-
'reply',
|
|
84
|
-
'forward',
|
|
85
|
-
'dice',
|
|
86
|
-
'rps',
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Next.js 风格参数声明(`defineCommand({ params: ... })`)。
|
|
91
|
-
* 文件名只声明参数形态(`[name]` / `[[name]]` / `[...name]` / `[[...name]]`),
|
|
92
|
-
* 类型与默认值统一在这里声明。
|
|
93
|
-
*/
|
|
94
|
-
export interface CommandParamSchema {
|
|
95
|
-
readonly type: CommandParameterType;
|
|
96
|
-
readonly default?: CommandDynamicValue;
|
|
97
|
-
readonly description?: string;
|
|
98
|
-
/** Optional translated display text; machine keys and values remain unchanged. */
|
|
99
|
-
readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
100
|
-
readonly optional?: boolean;
|
|
101
|
-
readonly rest?: boolean;
|
|
102
|
-
readonly min?: number;
|
|
103
|
-
readonly max?: number;
|
|
104
|
-
readonly choices?: readonly CommandParameterValue[];
|
|
105
|
-
readonly pattern?: RegExp;
|
|
106
|
-
readonly validate?: (value: CommandParameterValue, session: CommandSession) => boolean | string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface CommandOptionSchema extends Omit<CommandParamSchema, 'rest'> {
|
|
110
|
-
readonly alias?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export type CommandParams = Readonly<Record<string, CommandParamSchema>>;
|
|
114
|
-
export type CommandOptions = Readonly<Record<string, CommandOptionSchema>>;
|
|
115
|
-
type ParameterScalar<S extends CommandParamSchema> = S['type'] extends 'number' | 'integer' | 'float' | 'dice' | 'rps'
|
|
116
|
-
? number : S['type'] extends 'boolean' ? boolean
|
|
117
|
-
: S['type'] extends 'image' ? Readonly<Record<string, unknown>> | string
|
|
118
|
-
: S['type'] extends 'mention' | 'face' | 'reply' | 'forward' ? string | number : string;
|
|
119
|
-
type ParameterResult<S extends CommandParamSchema> = S extends { readonly rest: true }
|
|
120
|
-
? readonly ParameterScalar<S>[] : ParameterScalar<S>;
|
|
121
|
-
type HasDefault<S extends CommandParamSchema> = S extends { readonly default: infer D }
|
|
122
|
-
? undefined extends D ? false : true : false;
|
|
123
|
-
type OptionalParam<S extends CommandParamSchema> = S extends { readonly rest: true } ? false
|
|
124
|
-
: HasDefault<S> extends true ? false : S extends { readonly optional: true } ? true : false;
|
|
125
|
-
type OptionalOption<S extends CommandOptionSchema> = HasDefault<S> extends true ? false
|
|
126
|
-
: S extends { readonly optional: false } ? false : true;
|
|
127
|
-
export type InferCommandParams<P extends CommandParams> = {
|
|
128
|
-
readonly [K in keyof P as OptionalParam<P[K]> extends true ? never : K]: ParameterResult<P[K]>;
|
|
129
|
-
} & {
|
|
130
|
-
readonly [K in keyof P as OptionalParam<P[K]> extends true ? K : never]?: ParameterResult<P[K]>;
|
|
131
|
-
};
|
|
132
|
-
export type InferCommandOptions<O extends CommandOptions> = {
|
|
133
|
-
readonly [K in keyof O as OptionalOption<O[K]> extends true ? never : K]: ParameterResult<O[K]>;
|
|
134
|
-
} & {
|
|
135
|
-
readonly [K in keyof O as OptionalOption<O[K]> extends true ? K : never]?: ParameterResult<O[K]>;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
/** Minimal structural contract shared with canonical IM segments. */
|
|
139
|
-
export interface CommandSegment {
|
|
140
|
-
readonly type: string | { readonly name: string };
|
|
141
|
-
readonly data: Readonly<Record<string, unknown>>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export interface CommandParameterDefinition {
|
|
145
|
-
readonly name: string;
|
|
146
|
-
readonly type: CommandParameterType;
|
|
147
|
-
readonly defaultValue?: CommandDynamicValue;
|
|
148
|
-
/** `[[name]]` / `[[...name]]` 可选段;缺省按 `defaultValue === undefined` 推断。 */
|
|
149
|
-
readonly optional?: boolean;
|
|
150
|
-
/** `[...name]` / `[[...name]]` 捕获所有段,运行时值为 `string[]`。 */
|
|
151
|
-
readonly rest?: boolean;
|
|
152
|
-
readonly description?: string;
|
|
153
|
-
readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 场景:群 / 私聊 / 频道等。 */
|
|
157
|
-
export interface CommandScene {
|
|
158
|
-
readonly id: string;
|
|
159
|
-
readonly type: string;
|
|
160
|
-
readonly name?: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 发送者。
|
|
165
|
-
* `role` 为角色列表(如 `user` / `trusted` / `master`,以及平台侧 `owner` / `admin` 等)。
|
|
166
|
-
*/
|
|
167
|
-
export interface CommandSender {
|
|
168
|
-
readonly id: string;
|
|
169
|
-
readonly name?: string;
|
|
170
|
-
readonly role: readonly string[];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* 命令侧入站会话契约(结构对齐 `@zhin.js/im-contract` 的 ConversationRef;
|
|
175
|
-
* command 为 Feature 层,不能 import core / IM 契约包,故独立声明)。
|
|
176
|
-
*/
|
|
177
|
-
export interface CommandConversation {
|
|
178
|
-
readonly endpoint: Readonly<{
|
|
179
|
-
readonly id: string;
|
|
180
|
-
/** 适配器插件 owner(PluginId),与 `snapshot.config.get(adapter)` 对齐。 */
|
|
181
|
-
readonly adapter: string;
|
|
182
|
-
}>;
|
|
183
|
-
readonly kind: 'private' | 'group' | 'channel';
|
|
184
|
-
readonly id: string;
|
|
185
|
-
readonly parent?: Readonly<{
|
|
186
|
-
readonly kind: 'private' | 'group' | 'channel';
|
|
187
|
-
readonly id: string;
|
|
188
|
-
}>;
|
|
189
|
-
readonly threadId?: string;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* 命令侧入站消息契约。
|
|
194
|
-
*
|
|
195
|
-
* `@zhin.js/core/runtime` 的 `Message` 结构兼容本接口(duck typing)。
|
|
196
|
-
* 因架构分层(command 为 Feature 层,不能 import core),此处独立声明。
|
|
197
|
-
*/
|
|
198
|
-
export interface CommandMessage {
|
|
199
|
-
readonly conversation: CommandConversation;
|
|
200
|
-
readonly content: string;
|
|
201
|
-
/** Display preference resolved by the IM runtime, when this is an IM operation. */
|
|
202
|
-
readonly locale?: Locale;
|
|
203
|
-
/** 发送者(结构化视图见 CommandContext.sender)。 */
|
|
204
|
-
readonly sender?: { readonly id: string; readonly name?: string; readonly roles?: readonly string[] };
|
|
205
|
-
readonly id?: string;
|
|
206
|
-
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
207
|
-
readonly clientAdapter?: string;
|
|
208
|
-
readonly $client?: unknown;
|
|
209
|
-
/** 若上游已结构化,优先采用。 */
|
|
210
|
-
readonly scene?: CommandScene;
|
|
211
|
-
// 方法式声明(而非属性式函数类型):方法参数双变,runtime `Message` 的
|
|
212
|
-
// `$reply(content: SendContent)` 等才能鸭式兼容本契约(属性式是抗变,会报错)。
|
|
213
|
-
$reply?(content: unknown): Promise<unknown>;
|
|
214
|
-
$replyFrom?(requester: string, content: unknown): Promise<unknown>;
|
|
215
|
-
/** 向同 Endpoint 的另一个通道发送消息(结构兼容 `Message.$sendTo`)。 */
|
|
216
|
-
$sendTo?(
|
|
217
|
-
conversation: {
|
|
218
|
-
readonly kind: 'private' | 'group' | 'channel';
|
|
219
|
-
readonly id: string;
|
|
220
|
-
readonly parent?: Readonly<{ readonly kind: 'private' | 'group' | 'channel'; readonly id: string }>;
|
|
221
|
-
readonly threadId?: string;
|
|
222
|
-
},
|
|
223
|
-
content: unknown,
|
|
224
|
-
): Promise<unknown>;
|
|
225
|
-
/** 私信当前消息的发送者(结构兼容 `Message.$replyToPrivate`)。 */
|
|
226
|
-
$replyToPrivate?(
|
|
227
|
-
content: unknown,
|
|
228
|
-
from?: boolean | { readonly kind: 'group' | 'channel'; readonly id: string },
|
|
229
|
-
): Promise<unknown>;
|
|
230
|
-
/** 向指定群发送消息(结构兼容 `Message.$replyToGroup`)。 */
|
|
231
|
-
$replyToGroup?(groupId: string, content: unknown): Promise<unknown>;
|
|
232
|
-
/** 向指定频道发送消息(结构兼容 `Message.$replyToChannel`)。 */
|
|
233
|
-
$replyToChannel?(channelId: string, guildId: string, content: unknown, threadId?: string): Promise<unknown>;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* IM 入站快捷字段。
|
|
238
|
-
* 有 `CommandMessage` 来源时由 Runtime 从消息结构填充;
|
|
239
|
-
* `CommandIndex.execute(name)` 等无消息路径下为 `undefined`。
|
|
240
|
-
*/
|
|
241
|
-
export interface CommandSession {
|
|
242
|
-
/**
|
|
243
|
-
* 适配器插件实例 id(CapabilityId 的 owner 段,如 `root/icqq`)。
|
|
244
|
-
* 与 `snapshot.config.get(adapter)` 对齐。
|
|
245
|
-
*/
|
|
246
|
-
readonly adapter?: string;
|
|
247
|
-
/** Endpoint 名(`metadata.endpoint`)。 */
|
|
248
|
-
readonly endpoint?: string;
|
|
249
|
-
/** 场景对象(id / type / name)。 */
|
|
250
|
-
readonly scene?: CommandScene;
|
|
251
|
-
/** 发送者对象(id / name / role[])。 */
|
|
252
|
-
readonly sender?: CommandSender;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* 命令内对话式交互输入。
|
|
257
|
-
*
|
|
258
|
-
* IM 派发时自动注入(`context.interaction`);Host / CLI 无消息来源时为 `undefined`。
|
|
259
|
-
*
|
|
260
|
-
* ```ts
|
|
261
|
-
* defineCommand({
|
|
262
|
-
* execute: async (context) => {
|
|
263
|
-
* const name = await context.interaction!.ask({ type: 'text', title: '请输入你的名字' });
|
|
264
|
-
* const age = await context.interaction!.ask({ type: 'number', title: '请输入你的年龄' });
|
|
265
|
-
* return `你好 ${name},你 ${age} 岁了`;
|
|
266
|
-
* },
|
|
267
|
-
* });
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
export interface CommandContext<
|
|
271
|
-
TConfig = unknown,
|
|
272
|
-
TInput extends CommandMessage = CommandMessage,
|
|
273
|
-
TAdapter extends string | undefined = undefined,
|
|
274
|
-
> extends CapabilityContext<TConfig>, CommandSession {
|
|
275
|
-
/** Language resolved once for the current operation. */
|
|
276
|
-
readonly locale: Locale;
|
|
277
|
-
/**
|
|
278
|
-
* Resolve a static local command path to its current chat invocation. Defaults to this
|
|
279
|
-
* plugin; an explicit owner may reference another indexed plugin.
|
|
280
|
-
* Includes the actual Endpoint prefix and configured commandNamespace. Arguments
|
|
281
|
-
* are appended explicitly by the caller. Returns undefined for unknown/disabled
|
|
282
|
-
* paths or calls without a known chat command environment.
|
|
283
|
-
*/
|
|
284
|
-
formatCommand(localPath: string, owner?: PluginId): string | undefined;
|
|
285
|
-
readonly args: readonly string[];
|
|
286
|
-
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
287
|
-
readonly options: Readonly<Record<string, CommandParameterValue>>;
|
|
288
|
-
/** Structured arguments left after the command pattern was consumed. */
|
|
289
|
-
readonly segments: readonly Readonly<CommandSegment>[];
|
|
290
|
-
/**
|
|
291
|
-
* 派发来源。IM 命中时为 Runtime `Message`(满足 {@link CommandMessage});
|
|
292
|
-
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
293
|
-
*/
|
|
294
|
-
readonly input?: TInput;
|
|
295
|
-
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
296
|
-
readonly $client: AdapterClient<TAdapter>;
|
|
297
|
-
/** Bound by the actual IM operation; direct CommandIndex.execute calls may have no Session. */
|
|
298
|
-
readonly session: OperationSession<TAdapter> | undefined;
|
|
299
|
-
/**
|
|
300
|
-
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
301
|
-
*/
|
|
302
|
-
/** Canonical user-facing input/confirmation/selection module. */
|
|
303
|
-
readonly interaction?: UserInteraction;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export interface CommandDefinition<
|
|
307
|
-
TConfig = unknown,
|
|
308
|
-
TResult = unknown,
|
|
309
|
-
TInput extends CommandMessage = CommandMessage,
|
|
310
|
-
TAdapter extends string | undefined = string | undefined,
|
|
311
|
-
> {
|
|
312
|
-
/** @internal Runtime feature brand. */
|
|
313
|
-
readonly $feature: typeof commandBrand;
|
|
314
|
-
/** @internal Convention-derived parameter metadata. */
|
|
315
|
-
readonly $parameter?: CommandParameterDefinition;
|
|
316
|
-
readonly description?: string;
|
|
317
|
-
readonly descriptionI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
318
|
-
/** Restrict this command to one adapter and infer `context.$client`. */
|
|
319
|
-
readonly adapter?: TAdapter;
|
|
320
|
-
/**
|
|
321
|
-
* Next.js 风格参数声明:动态段文件名(`[name]` 等)的形态配合这里的
|
|
322
|
-
* 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
|
|
323
|
-
*/
|
|
324
|
-
readonly params?: CommandParams;
|
|
325
|
-
readonly options?: CommandOptions;
|
|
326
|
-
/**
|
|
327
|
-
* 本地静态段别名(可多词,如 `'gh issue'`)。Capability owner 不参与
|
|
328
|
-
* 用户路由;alias 只替换文件路径提供的静态段。
|
|
329
|
-
*/
|
|
330
|
-
readonly alias?: readonly string[];
|
|
331
|
-
/** Display an explicitly declared alias for this locale; every value must also occur in alias. */
|
|
332
|
-
readonly preferredAliasI18n?: Readonly<Partial<Record<Locale, string>>>;
|
|
333
|
-
/**
|
|
334
|
-
* 内置 permit DSL(AND)。单项内逗号为 OR。
|
|
335
|
-
* 例:`adapter(icqq)`、`role(master)`、`group(123,456)`。
|
|
336
|
-
*/
|
|
337
|
-
readonly permit?: readonly string[];
|
|
338
|
-
/** Optional full-operation cooldown, concurrency and one-shot progress feedback. */
|
|
339
|
-
readonly execution?: Readonly<CommandExecutionPolicy>;
|
|
340
|
-
/**
|
|
341
|
-
* 全局整句快捷方式:触发串(trim 后全文相等)→ 预填 params。
|
|
342
|
-
* 与普通路由一样是全局用户输入,冲突会在 generation 构建时拒绝。
|
|
343
|
-
*/
|
|
344
|
-
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>>;
|
|
345
|
-
execute(context: CommandContext<TConfig, TInput, TAdapter>): TResult | Promise<TResult>;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
declare module '@zhin.js/plugin-runtime' {
|
|
349
|
-
interface PluginSetupContext<TConfig = unknown> {
|
|
350
|
-
addCommand<
|
|
351
|
-
TResult = unknown,
|
|
352
|
-
TInput extends CommandMessage = CommandMessage,
|
|
353
|
-
TAdapter extends string | undefined = undefined,
|
|
354
|
-
>(
|
|
355
|
-
localName: string,
|
|
356
|
-
definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
357
|
-
): void;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* 定义一个命令模块(`commands/` 约定目录下默认导出)。
|
|
363
|
-
* @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
|
|
364
|
-
*/
|
|
365
|
-
type CommandAuthoringDefinition<
|
|
366
|
-
TConfig,
|
|
367
|
-
TResult,
|
|
368
|
-
TInput extends CommandMessage,
|
|
369
|
-
P extends CommandParams,
|
|
370
|
-
O extends CommandOptions,
|
|
371
|
-
> =
|
|
372
|
-
| Omit<CommandDefinition<TConfig, TResult, TInput, undefined>, '$feature' | '$parameter' | 'params' | 'options' | 'execute'> & { readonly params?: P; readonly options?: O; execute(context: TypedCommandContext<TConfig, TInput, undefined, P, O>): TResult | Promise<TResult> }
|
|
373
|
-
| {
|
|
374
|
-
[TAdapter in RegisteredAdapterName]: Omit<
|
|
375
|
-
CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
376
|
-
'$feature' | '$parameter' | 'params' | 'options' | 'execute'
|
|
377
|
-
> & { readonly adapter: TAdapter; readonly params?: P; readonly options?: O; execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult> }
|
|
378
|
-
}[RegisteredAdapterName];
|
|
379
|
-
|
|
380
|
-
type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> =
|
|
381
|
-
Omit<CommandContext<TConfig, TInput, TAdapter>, 'params' | 'options'> & {
|
|
382
|
-
readonly params: keyof P extends never ? Readonly<Record<string, CommandParameterValue>> : InferCommandParams<P>;
|
|
383
|
-
readonly options: InferCommandOptions<O>;
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
/** The returned declaration retains its platform, parameter schemas and callback result. */
|
|
387
|
-
type DeclaredCommandAdapter<D> = D extends { readonly adapter: infer A extends string } ? A
|
|
388
|
-
: D extends { readonly adapter?: infer A extends string | undefined } ? A : undefined;
|
|
389
|
-
export type TypedCommandDefinition<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> =
|
|
390
|
-
Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, 'params' | 'options' | 'execute'> & {
|
|
391
|
-
readonly params?: P;
|
|
392
|
-
readonly options?: O;
|
|
393
|
-
execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
|
|
398
|
-
* 仍从定义推断。
|
|
399
|
-
* @public
|
|
400
|
-
*/
|
|
401
|
-
export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <
|
|
402
|
-
const TResult = unknown,
|
|
403
|
-
const P extends CommandParams = {},
|
|
404
|
-
const O extends CommandOptions = {},
|
|
405
|
-
const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
|
|
406
|
-
>(
|
|
407
|
-
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
|
|
408
|
-
) => Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
|
|
409
|
-
|
|
410
|
-
export function defineCommand<
|
|
411
|
-
TConfig = unknown,
|
|
412
|
-
const TResult = unknown,
|
|
413
|
-
TInput extends CommandMessage = CommandMessage,
|
|
414
|
-
const P extends CommandParams = {},
|
|
415
|
-
const O extends CommandOptions = {},
|
|
416
|
-
const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
|
|
417
|
-
>(
|
|
418
|
-
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
|
|
419
|
-
): Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>> {
|
|
420
|
-
if (typeof definition.execute !== 'function') {
|
|
421
|
-
throw new TypeError('Command execute must be a function');
|
|
422
|
-
}
|
|
423
|
-
validateAdapterName(definition.adapter);
|
|
424
|
-
if (definition.params !== undefined) {
|
|
425
|
-
if (!definition.params || typeof definition.params !== 'object') {
|
|
426
|
-
throw new TypeError('Command params must be a Record<string, CommandParamSchema>');
|
|
427
|
-
}
|
|
428
|
-
for (const [name, schema] of Object.entries(definition.params)) {
|
|
429
|
-
if (!schema || typeof schema !== 'object'
|
|
430
|
-
|| !commandParameterTypes.has((schema as CommandParamSchema).type)) {
|
|
431
|
-
throw new TypeError(`Command params.${name} requires a valid type`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
validateArgumentSchemas(definition.params, definition.options);
|
|
436
|
-
validateDescriptionI18n(definition.descriptionI18n);
|
|
437
|
-
validateCommandAlias(definition.alias);
|
|
438
|
-
validatePreferredAliases(definition);
|
|
439
|
-
validateCommandPermit(definition.permit);
|
|
440
|
-
validateCommandShortcutShape(definition.shortcut);
|
|
441
|
-
const execution = normalizeExecutionPolicy(definition.execution);
|
|
442
|
-
return Object.freeze({ $feature: commandBrand, ...definition, ...(execution ? { execution } : {}) }) as Readonly<
|
|
443
|
-
TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>
|
|
444
|
-
>;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/** @internal Validate display-only translations without changing their fallback text. */
|
|
448
|
-
export function validateDescriptionI18n(value: CommandDefinition['descriptionI18n']): void {
|
|
449
|
-
if (value === undefined) return;
|
|
450
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
451
|
-
|| Object.entries(value).some(([locale, text]) => !['zh-CN', 'en'].includes(locale) || typeof text !== 'string')) {
|
|
452
|
-
throw new TypeError('Command descriptionI18n must map zh-CN/en to strings');
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* 先固定插件 config 类型,再定义命令;params / options / 返回值仍从定义推断。
|
|
458
|
-
*
|
|
459
|
-
* ```ts
|
|
460
|
-
* export default defineCommand.withConfig<Config>()({
|
|
461
|
-
* params: { count: { type: 'integer' } },
|
|
462
|
-
* execute({ params, config }) { params.count; config.prefix },
|
|
463
|
-
* })
|
|
464
|
-
* ```
|
|
465
|
-
*
|
|
466
|
-
* `defineCommand<Config>({...})` 仍然可用,但 TypeScript 不支持部分推断,显式写了
|
|
467
|
-
* 类型参数后 params 会退化成 `Record<string, CommandParameterValue>`。
|
|
468
|
-
* 这里是独立的属性而不是 `defineCommand()` 重载:`defineCommand` 常被当作值传给
|
|
469
|
-
* `createEndpointCommands(spec, defineCommand)` 这类工厂,重载会让它推断错签名。
|
|
470
|
-
*/
|
|
471
|
-
defineCommand.withConfig = function withConfig<
|
|
472
|
-
TConfig = unknown,
|
|
473
|
-
TInput extends CommandMessage = CommandMessage,
|
|
474
|
-
>(): CommandDefiner<TConfig, TInput> {
|
|
475
|
-
return (definition) => defineCommand(definition);
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
function validateAdapterName(adapter: string | undefined): void {
|
|
479
|
-
if (adapter !== undefined && (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
480
|
-
throw new TypeError('Command adapter must be a non-empty string');
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function validatePreferredAliases(definition: Pick<CommandDefinition, 'alias' | 'preferredAliasI18n'>): void {
|
|
485
|
-
const preferred = definition.preferredAliasI18n;
|
|
486
|
-
if (preferred === undefined) return;
|
|
487
|
-
validateDescriptionI18n(preferred);
|
|
488
|
-
const normalize = (value: string) => value.trim().split(/\s+/u).filter(Boolean).join(' ');
|
|
489
|
-
const aliases = new Set(definition.alias?.map(normalize));
|
|
490
|
-
for (const [locale, alias] of Object.entries(preferred)) {
|
|
491
|
-
if (!aliases.has(normalize(alias))) {
|
|
492
|
-
throw new TypeError(`Command preferredAliasI18n.${locale} must reference a declared alias`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function validateCommandAlias(alias: readonly string[] | undefined): void {
|
|
498
|
-
if (alias === undefined) return;
|
|
499
|
-
if (!Array.isArray(alias)) {
|
|
500
|
-
throw new TypeError('Command alias must be a readonly string[]');
|
|
501
|
-
}
|
|
502
|
-
for (const [index, entry] of alias.entries()) {
|
|
503
|
-
if (typeof entry !== 'string') {
|
|
504
|
-
throw new TypeError(`Command alias[${index}] must be a string`);
|
|
505
|
-
}
|
|
506
|
-
const tokens = entry.trim().split(/\s+/u).filter(Boolean);
|
|
507
|
-
if (tokens.length === 0) {
|
|
508
|
-
throw new TypeError(`Command alias[${index}] must contain at least one token`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function validateCommandPermit(permit: readonly string[] | undefined): void {
|
|
514
|
-
if (permit === undefined) return;
|
|
515
|
-
if (!Array.isArray(permit)) {
|
|
516
|
-
throw new TypeError('Command permit must be a readonly string[]');
|
|
517
|
-
}
|
|
518
|
-
for (const [index, entry] of permit.entries()) {
|
|
519
|
-
if (typeof entry !== 'string') {
|
|
520
|
-
throw new TypeError(`Command permit[${index}] must be a string`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
assertPermitSyntax(permit);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function validateCommandShortcutShape(
|
|
527
|
-
shortcut: Readonly<Record<string, Readonly<Record<string, CommandDynamicValue>>>> | undefined,
|
|
528
|
-
): void {
|
|
529
|
-
if (shortcut === undefined) return;
|
|
530
|
-
if (!shortcut || typeof shortcut !== 'object' || Array.isArray(shortcut)) {
|
|
531
|
-
throw new TypeError('Command shortcut must be a Record<string, Record<string, value>>');
|
|
532
|
-
}
|
|
533
|
-
for (const [trigger, params] of Object.entries(shortcut)) {
|
|
534
|
-
if (!trigger.trim()) {
|
|
535
|
-
throw new TypeError('Command shortcut keys must be non-empty after trim');
|
|
536
|
-
}
|
|
537
|
-
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
538
|
-
throw new TypeError(`Command shortcut[${JSON.stringify(trigger)}] must be a params Record`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/** @internal Convention-loader assembly helper. */
|
|
544
|
-
export function bindCommandParameter<
|
|
545
|
-
TConfig,
|
|
546
|
-
TResult,
|
|
547
|
-
TInput extends CommandMessage,
|
|
548
|
-
TAdapter extends string | undefined,
|
|
549
|
-
>(
|
|
550
|
-
definition: CommandDefinition<TConfig, TResult, TInput, TAdapter>,
|
|
551
|
-
parameter: CommandParameterDefinition | undefined,
|
|
552
|
-
): Readonly<CommandDefinition<TConfig, TResult, TInput, TAdapter>> {
|
|
553
|
-
if (!parameter) return definition;
|
|
554
|
-
return Object.freeze({ ...definition, $parameter: Object.freeze({ ...parameter }) });
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** @internal Runtime validation for convention-discovered modules. */
|
|
558
|
-
export function parseCommandDefinition(value: unknown): CommandDefinition {
|
|
559
|
-
if (!value || typeof value !== 'object') {
|
|
560
|
-
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
561
|
-
}
|
|
562
|
-
const definition = value as Partial<CommandDefinition>;
|
|
563
|
-
if (definition.$feature !== commandBrand || typeof definition.execute !== 'function') {
|
|
564
|
-
throw new TypeError('Command module must default-export defineCommand(...)');
|
|
565
|
-
}
|
|
566
|
-
validateAdapterName(definition.adapter);
|
|
567
|
-
normalizeExecutionPolicy(definition.execution);
|
|
568
|
-
validateCommandAlias(definition.alias);
|
|
569
|
-
validatePreferredAliases(definition);
|
|
570
|
-
return definition as CommandDefinition;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
575
|
-
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
576
|
-
* @internal Command projection helper.
|
|
577
|
-
*/
|
|
578
|
-
export function resolveDynamicParams(
|
|
579
|
-
params: Readonly<Record<string, CommandDynamicValue>>,
|
|
580
|
-
source: unknown,
|
|
581
|
-
): Readonly<Record<string, CommandParameterValue>> {
|
|
582
|
-
const session = resolveCommandSession(source);
|
|
583
|
-
const resolved: Record<string, CommandParameterValue> = {};
|
|
584
|
-
for (const [key, value] of Object.entries(params)) {
|
|
585
|
-
resolved[key] = typeof value === 'function' ? value(session) : value;
|
|
586
|
-
}
|
|
587
|
-
return Object.freeze(resolved);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Parsed invocation values, independent of the caller's transport and lifetime. */
|
|
591
|
-
export interface CommandInvocationArguments {
|
|
592
|
-
readonly args: readonly string[];
|
|
593
|
-
readonly params?: Readonly<Record<string, CommandParameterValue>>;
|
|
594
|
-
readonly options?: Readonly<Record<string, CommandParameterValue>>;
|
|
595
|
-
readonly segments?: readonly Readonly<CommandSegment>[];
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/** Author and lifetime authority captured by the dispatcher for this invocation. */
|
|
599
|
-
export interface CommandInvocationOperation {
|
|
600
|
-
readonly snapshot: RuntimeSnapshot;
|
|
601
|
-
readonly ownerId: PluginId;
|
|
602
|
-
readonly operation?: CapabilityOperation;
|
|
603
|
-
readonly input?: unknown;
|
|
604
|
-
readonly interaction?: UserInteraction;
|
|
605
|
-
readonly adapter?: string;
|
|
606
|
-
readonly presentation?: Readonly<{
|
|
607
|
-
locale: Locale;
|
|
608
|
-
formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
|
|
609
|
-
}>;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/** @internal Command dispatcher assembly helper; each value has one owner. */
|
|
613
|
-
export function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext {
|
|
614
|
-
const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
|
|
615
|
-
const { args, params = {}, options = {}, segments = [] } = invocation;
|
|
616
|
-
const context = createCapabilityContext({ snapshot, ownerId, signal: readOperationInputSignal(input), operation: operation.operation });
|
|
617
|
-
const session = resolveCommandSession(input);
|
|
618
|
-
const parameterValues: Record<string, CommandParameterValue> = { __proto__: null, ...params };
|
|
619
|
-
const optionValues: Record<string, CommandParameterValue> = { __proto__: null, ...options };
|
|
620
|
-
const result = {
|
|
621
|
-
...context,
|
|
622
|
-
...session,
|
|
623
|
-
locale: presentation?.locale ?? resolveLocale((snapshot.config.get(snapshot.root) as { locale?: unknown } | undefined)?.locale),
|
|
624
|
-
formatCommand: presentation?.formatCommand ?? (() => undefined),
|
|
625
|
-
args: Object.freeze([...args]),
|
|
626
|
-
params: Object.freeze(parameterValues),
|
|
627
|
-
options: Object.freeze(optionValues),
|
|
628
|
-
segments: freezeSegments(segments),
|
|
629
|
-
...(input !== undefined ? { input: input as CommandMessage } : {}),
|
|
630
|
-
...(interaction !== undefined ? { interaction } : {}),
|
|
631
|
-
} as CommandContext;
|
|
632
|
-
Object.defineProperty(result, 'session', {
|
|
633
|
-
enumerable: true,
|
|
634
|
-
get: () => readOperationSession(input, adapter),
|
|
635
|
-
});
|
|
636
|
-
Object.defineProperty(result, '$client', {
|
|
637
|
-
enumerable: true,
|
|
638
|
-
get: () => readOperationClient(input, adapter),
|
|
639
|
-
});
|
|
640
|
-
return Object.freeze(result);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
645
|
-
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
|
646
|
-
* @internal Command dispatcher projection helper.
|
|
647
|
-
*/
|
|
648
|
-
export function resolveCommandSession(input: unknown): CommandSession {
|
|
649
|
-
const operation = readOperationSession(input);
|
|
650
|
-
if (operation) {
|
|
651
|
-
const endpoint = input && typeof input === 'object' ? (input as { readonly endpointId?: unknown }).endpointId : undefined;
|
|
652
|
-
const sender = operation.sender;
|
|
653
|
-
const roles = [...new Set([...sender?.authorizationRoles ?? [], ...sender?.platformRoles ?? []])];
|
|
654
|
-
return Object.freeze({
|
|
655
|
-
adapter: operation.scene.endpoint.adapter,
|
|
656
|
-
...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
|
|
657
|
-
scene: Object.freeze({ id: operation.scene.id, type: operation.scene.kind,
|
|
658
|
-
...(operation.scene.name !== undefined ? { name: operation.scene.name } : {}) }),
|
|
659
|
-
...(sender ? { sender: Object.freeze({ id: sender.id, ...(sender.name !== undefined ? { name: sender.name } : {}),
|
|
660
|
-
role: Object.freeze(roles.length ? roles : ['user']) }) } : {}),
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
if (!isCommandMessageLike(input)) return Object.freeze({});
|
|
664
|
-
|
|
665
|
-
const metadata = input.metadata && typeof input.metadata === 'object'
|
|
666
|
-
? input.metadata as Readonly<Record<string, unknown>>
|
|
667
|
-
: undefined;
|
|
668
|
-
|
|
669
|
-
const adapter = input.conversation.endpoint.adapter || undefined;
|
|
670
|
-
const endpoint = (input as { endpointId?: string }).endpointId
|
|
671
|
-
|| (typeof metadata?.endpoint === 'string' && metadata.endpoint ? metadata.endpoint : undefined);
|
|
672
|
-
|
|
673
|
-
const scene = resolveScene(input, metadata);
|
|
674
|
-
const sender = resolveCommandSender(input, metadata);
|
|
675
|
-
|
|
676
|
-
return Object.freeze({
|
|
677
|
-
...(adapter ? { adapter } : {}),
|
|
678
|
-
...(endpoint !== undefined ? { endpoint } : {}),
|
|
679
|
-
...(scene !== undefined ? { scene } : {}),
|
|
680
|
-
...(sender !== undefined ? { sender } : {}),
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function isCommandMessageLike(input: unknown): input is CommandMessage {
|
|
685
|
-
if (!input || typeof input !== 'object') return false;
|
|
686
|
-
const value = input as Partial<CommandMessage>;
|
|
687
|
-
return !!value.conversation
|
|
688
|
-
&& typeof value.conversation === 'object'
|
|
689
|
-
&& typeof value.content === 'string';
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function resolveScene(
|
|
693
|
-
input: CommandMessage,
|
|
694
|
-
metadata: Readonly<Record<string, unknown>> | undefined,
|
|
695
|
-
): CommandScene | undefined {
|
|
696
|
-
if (isCommandScene(input.scene)) {
|
|
697
|
-
return Object.freeze({
|
|
698
|
-
id: input.scene.id,
|
|
699
|
-
type: input.scene.type,
|
|
700
|
-
...(input.scene.name !== undefined ? { name: input.scene.name } : {}),
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const conversation = input.conversation;
|
|
705
|
-
const type = conversation.kind;
|
|
706
|
-
const id = conversation.id;
|
|
707
|
-
if (!type || !id) return undefined;
|
|
708
|
-
|
|
709
|
-
const name = firstString(
|
|
710
|
-
metadata?.channelName,
|
|
711
|
-
metadata?.group_name,
|
|
712
|
-
metadata?.groupName,
|
|
713
|
-
metadata?.sceneName,
|
|
714
|
-
);
|
|
715
|
-
|
|
716
|
-
return Object.freeze({
|
|
717
|
-
id,
|
|
718
|
-
type,
|
|
719
|
-
...(name !== undefined ? { name } : {}),
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Pure canonical sender projection shared with read-only command condition simulation.
|
|
725
|
-
* @internal Command dispatcher projection helper.
|
|
726
|
-
*/
|
|
727
|
-
export function resolveCommandSender(
|
|
728
|
-
input: Pick<CommandMessage, 'sender'>,
|
|
729
|
-
metadata: Readonly<Record<string, unknown>> | undefined,
|
|
730
|
-
): CommandSender | undefined {
|
|
731
|
-
const structured = (input as { readonly from?: unknown }).from;
|
|
732
|
-
if (isCommandSender(structured)) {
|
|
733
|
-
return freezeSender(structured);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
|
|
737
|
-
if (!id) return undefined;
|
|
738
|
-
|
|
739
|
-
const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
740
|
-
const role = resolveRoles(input, metadata);
|
|
741
|
-
|
|
742
|
-
return Object.freeze({
|
|
743
|
-
id,
|
|
744
|
-
...(name !== undefined ? { name } : {}),
|
|
745
|
-
role,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function resolveRoles(
|
|
750
|
-
input: Pick<CommandMessage, 'sender'>,
|
|
751
|
-
metadata: Readonly<Record<string, unknown>> | undefined,
|
|
752
|
-
): readonly string[] {
|
|
753
|
-
if (input.sender?.roles?.length) {
|
|
754
|
-
return Object.freeze([...input.sender.roles]);
|
|
755
|
-
}
|
|
756
|
-
const roles: string[] = [];
|
|
757
|
-
const push = (value: unknown) => {
|
|
758
|
-
if (typeof value !== 'string') return;
|
|
759
|
-
const trimmed = value.trim();
|
|
760
|
-
if (trimmed && !roles.includes(trimmed)) roles.push(trimmed);
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
if (Array.isArray(metadata?.roles)) {
|
|
764
|
-
for (const item of metadata.roles) push(item);
|
|
765
|
-
}
|
|
766
|
-
push(metadata?.senderRole);
|
|
767
|
-
push(metadata?.role);
|
|
768
|
-
if (metadata?.isMaster === true) push('master');
|
|
769
|
-
if (metadata?.isTrusted === true) push('trusted');
|
|
770
|
-
if (roles.length === 0) roles.push('user');
|
|
771
|
-
return Object.freeze(roles);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function isCommandScene(value: unknown): value is CommandScene {
|
|
775
|
-
if (!value || typeof value !== 'object') return false;
|
|
776
|
-
const scene = value as Partial<CommandScene>;
|
|
777
|
-
return typeof scene.id === 'string'
|
|
778
|
-
&& scene.id.length > 0
|
|
779
|
-
&& typeof scene.type === 'string'
|
|
780
|
-
&& scene.type.length > 0;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function isCommandSender(value: unknown): value is CommandSender {
|
|
784
|
-
if (!value || typeof value !== 'object') return false;
|
|
785
|
-
const sender = value as Partial<CommandSender>;
|
|
786
|
-
return typeof sender.id === 'string'
|
|
787
|
-
&& sender.id.length > 0
|
|
788
|
-
&& Array.isArray(sender.role)
|
|
789
|
-
&& sender.role.every((item) => typeof item === 'string');
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function freezeSender(sender: CommandSender): CommandSender {
|
|
793
|
-
return Object.freeze({
|
|
794
|
-
id: sender.id,
|
|
795
|
-
...(sender.name !== undefined ? { name: sender.name } : {}),
|
|
796
|
-
role: Object.freeze([...sender.role]),
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function firstString(...values: unknown[]): string | undefined {
|
|
801
|
-
for (const value of values) {
|
|
802
|
-
if (typeof value === 'string' && value.trim()) return value;
|
|
803
|
-
}
|
|
804
|
-
return undefined;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function freezeSegments(
|
|
808
|
-
segments: readonly Readonly<CommandSegment>[],
|
|
809
|
-
): readonly Readonly<CommandSegment>[] {
|
|
810
|
-
return Object.freeze(segments.map((segment) => Object.freeze({
|
|
811
|
-
type: typeof segment.type === 'string'
|
|
812
|
-
? segment.type
|
|
813
|
-
: Object.freeze({ name: segment.type.name }),
|
|
814
|
-
data: Object.freeze({ ...segment.data }),
|
|
815
|
-
})));
|
|
816
|
-
}
|