@zhin.js/command 1.1.11 → 1.1.13
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 +1 -1
- package/lib/command-index.d.ts +10 -1
- package/lib/command-index.js +18 -10
- package/lib/definition.d.ts +37 -8
- package/lib/definition.js +23 -3
- package/package.json +4 -4
- package/src/command-index.ts +38 -21
- package/src/definition.ts +66 -20
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ export default defineCommand({
|
|
|
117
117
|
后面的内容作为参数。未知/重复选项拒绝。`remind --help` 从同一声明生成帮助,包含
|
|
118
118
|
Endpoint 前缀及显式插件命名空间,不执行命令;同一权限声明仍约束帮助读取。
|
|
119
119
|
|
|
120
|
-
`dispatch()` 返回 `status`:`unmatched`(继续普通消息处理)、`invalid`(`issues` +
|
|
120
|
+
`dispatch({ input, source, snapshot })` 返回 `status`:`unmatched`(继续普通消息处理)、`invalid`(`issues` +
|
|
121
121
|
`help`)、`denied`(权限拒绝)、`limited`(执行预算拒绝)、`executed`。后四者 `matched: true`,只有 `executed`
|
|
122
122
|
包含业务结果。可靠静态路径匹配后才提示错误;纯动态路由的模糊聊天保持未匹配。
|
|
123
123
|
受限命令没有可鉴权主体时拒绝,Host 可通过 `dispatch` 提供真实 `PermissionSubject`;
|
package/lib/command-index.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface CommandDescriptor {
|
|
|
25
25
|
/** shortcut 触发键列表(不含预填 params)。 */
|
|
26
26
|
readonly shortcut?: readonly string[];
|
|
27
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
|
+
}
|
|
28
37
|
export interface CommandDispatchResult {
|
|
29
38
|
readonly matched: boolean;
|
|
30
39
|
readonly status: 'unmatched' | 'invalid' | 'denied' | 'limited' | 'executed';
|
|
@@ -72,7 +81,7 @@ export declare class CommandIndex {
|
|
|
72
81
|
formatMenu(prefix: string | undefined, locale: Locale): string | undefined;
|
|
73
82
|
has(name: string): boolean;
|
|
74
83
|
execute(name: string, args?: readonly string[], operationSnapshot?: RuntimeSnapshot): Promise<unknown>;
|
|
75
|
-
dispatch(input
|
|
84
|
+
dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot, locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult>;
|
|
76
85
|
}
|
|
77
86
|
export declare function isCommandIndex(value: unknown): value is CommandIndex;
|
|
78
87
|
/**
|
package/lib/command-index.js
CHANGED
|
@@ -186,18 +186,21 @@ export class CommandIndex {
|
|
|
186
186
|
if (admission.limit)
|
|
187
187
|
throw new CommandExecutionLimitedError(admission.limit);
|
|
188
188
|
try {
|
|
189
|
-
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
|
+
}));
|
|
190
193
|
}
|
|
191
194
|
finally {
|
|
192
195
|
admission.release();
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
|
-
async dispatch(input, source
|
|
198
|
+
async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot, locale: localeOverride }) {
|
|
196
199
|
const locale = localeOverride ?? commandLocale(source, operationSnapshot);
|
|
197
200
|
const prefix = commandPrefix ?? '';
|
|
198
201
|
if (this.#menu) {
|
|
199
202
|
const keyword = this.#menu.keyword ?? commandMessages(locale, 'menuKeyword');
|
|
200
|
-
const menuResult = this.#dispatchMenu(input, prefix, operationSnapshot, locale, keyword);
|
|
203
|
+
const menuResult = this.#dispatchMenu(input, { commandPrefix: prefix, snapshot: operationSnapshot, locale, keyword });
|
|
201
204
|
if (menuResult !== undefined) {
|
|
202
205
|
return Object.freeze({
|
|
203
206
|
matched: true,
|
|
@@ -244,7 +247,11 @@ export class CommandIndex {
|
|
|
244
247
|
const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
|
|
245
248
|
if (limit)
|
|
246
249
|
return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
|
|
247
|
-
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 }));
|
|
248
255
|
return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
|
|
249
256
|
}
|
|
250
257
|
#denied(record) {
|
|
@@ -362,12 +369,13 @@ export class CommandIndex {
|
|
|
362
369
|
// ==========================================================================
|
|
363
370
|
// 内置菜单命令
|
|
364
371
|
// ==========================================================================
|
|
365
|
-
#dispatchMenu(input,
|
|
372
|
+
#dispatchMenu(input, context) {
|
|
373
|
+
const { commandPrefix, keyword, locale } = context;
|
|
366
374
|
const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
|
|
367
375
|
if (text === undefined)
|
|
368
376
|
return undefined;
|
|
369
377
|
if (text === keyword)
|
|
370
|
-
return this.#buildMenu(
|
|
378
|
+
return this.#buildMenu({}, context);
|
|
371
379
|
if (text.startsWith(keyword + ' ')) {
|
|
372
380
|
const key = text.slice(keyword.length + 1).trim();
|
|
373
381
|
const args = key.split(/\s+/u);
|
|
@@ -378,15 +386,15 @@ export class CommandIndex {
|
|
|
378
386
|
|| !Number.isSafeInteger(Number(pageText))) {
|
|
379
387
|
return { value: commandMessages(locale, 'invalidPage', { command: `${commandPrefix}${keyword}` }) };
|
|
380
388
|
}
|
|
381
|
-
return this.#buildMenu(flag === 1 ? args[0] : undefined,
|
|
389
|
+
return this.#buildMenu({ pluginKey: flag === 1 ? args[0] : undefined, page: Number(pageText) }, context);
|
|
382
390
|
}
|
|
383
391
|
if (key)
|
|
384
|
-
return this.#buildMenu(
|
|
385
|
-
return this.#buildMenu(
|
|
392
|
+
return this.#buildMenu({ pluginKey: key }, context);
|
|
393
|
+
return this.#buildMenu({}, context);
|
|
386
394
|
}
|
|
387
395
|
return undefined;
|
|
388
396
|
}
|
|
389
|
-
#buildMenu(pluginKey, commandPrefix, keyword, snapshot, locale
|
|
397
|
+
#buildMenu({ pluginKey, page = 1 }, { commandPrefix, keyword, snapshot, locale }) {
|
|
390
398
|
const root = snapshot.root;
|
|
391
399
|
const targetId = pluginKey
|
|
392
400
|
? `${root}/${pluginKey.split('.').join('/')}`
|
package/lib/definition.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module zhin.js/command
|
|
4
4
|
*/
|
|
5
5
|
import { type Locale, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
6
|
-
import { type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
|
|
6
|
+
import { type OperationSession, type AdapterClient, type RegisteredAdapterName, type CapabilityContext } from '@zhin.js/feature-kit';
|
|
7
7
|
import type { UserInteraction } from '@zhin.js/interaction';
|
|
8
8
|
import { type CommandExecutionPolicy } from './execution-policy.js';
|
|
9
9
|
export type { CommandExecutionPolicy, CommandExecutionLimit } from './execution-policy.js';
|
|
@@ -233,6 +233,8 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
|
|
|
233
233
|
readonly input?: TInput;
|
|
234
234
|
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
235
235
|
readonly $client: AdapterClient<TAdapter>;
|
|
236
|
+
/** Bound by the actual IM operation; direct CommandIndex.execute calls may have no Session. */
|
|
237
|
+
readonly session: OperationSession<TAdapter> | undefined;
|
|
236
238
|
/**
|
|
237
239
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
238
240
|
*/
|
|
@@ -300,13 +302,24 @@ type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extend
|
|
|
300
302
|
readonly params: keyof P extends never ? Readonly<Record<string, CommandParameterValue>> : InferCommandParams<P>;
|
|
301
303
|
readonly options: InferCommandOptions<O>;
|
|
302
304
|
};
|
|
305
|
+
/** The returned declaration retains its platform, parameter schemas and callback result. */
|
|
306
|
+
type DeclaredCommandAdapter<D> = D extends {
|
|
307
|
+
readonly adapter: infer A extends string;
|
|
308
|
+
} ? A : D extends {
|
|
309
|
+
readonly adapter?: infer A extends string | undefined;
|
|
310
|
+
} ? A : undefined;
|
|
311
|
+
export type TypedCommandDefinition<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> = Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, 'params' | 'options' | 'execute'> & {
|
|
312
|
+
readonly params?: P;
|
|
313
|
+
readonly options?: O;
|
|
314
|
+
execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
|
|
315
|
+
};
|
|
303
316
|
/**
|
|
304
317
|
* `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
|
|
305
318
|
* 仍从定义推断。
|
|
306
319
|
* @public
|
|
307
320
|
*/
|
|
308
|
-
export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <TResult = unknown, const P extends CommandParams = {}, const O extends CommandOptions = {}>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>) => Readonly<
|
|
309
|
-
export declare function defineCommand<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage, const P extends CommandParams = {}, const O extends CommandOptions = {}>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>): Readonly<
|
|
321
|
+
export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <const TResult = unknown, const P extends CommandParams = {}, const O extends CommandOptions = {}, const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D) => Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
|
|
322
|
+
export declare function defineCommand<TConfig = unknown, const TResult = unknown, TInput extends CommandMessage = CommandMessage, const P extends CommandParams = {}, const O extends CommandOptions = {}, const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>>(definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D): Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
|
|
310
323
|
export declare namespace defineCommand {
|
|
311
324
|
var withConfig: <TConfig = unknown, TInput extends CommandMessage = CommandMessage>() => CommandDefiner<TConfig, TInput>;
|
|
312
325
|
}
|
|
@@ -322,11 +335,27 @@ export declare function parseCommandDefinition(value: unknown): CommandDefinitio
|
|
|
322
335
|
* @internal Command projection helper.
|
|
323
336
|
*/
|
|
324
337
|
export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
|
|
325
|
-
/**
|
|
326
|
-
export
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
338
|
+
/** Parsed invocation values, independent of the caller's transport and lifetime. */
|
|
339
|
+
export interface CommandInvocationArguments {
|
|
340
|
+
readonly args: readonly string[];
|
|
341
|
+
readonly params?: Readonly<Record<string, CommandParameterValue>>;
|
|
342
|
+
readonly options?: Readonly<Record<string, CommandParameterValue>>;
|
|
343
|
+
readonly segments?: readonly Readonly<CommandSegment>[];
|
|
344
|
+
}
|
|
345
|
+
/** Author and lifetime authority captured by the dispatcher for this invocation. */
|
|
346
|
+
export interface CommandInvocationOperation {
|
|
347
|
+
readonly snapshot: RuntimeSnapshot;
|
|
348
|
+
readonly ownerId: PluginId;
|
|
349
|
+
readonly input?: unknown;
|
|
350
|
+
readonly interaction?: UserInteraction;
|
|
351
|
+
readonly adapter?: string;
|
|
352
|
+
readonly presentation?: Readonly<{
|
|
353
|
+
locale: Locale;
|
|
354
|
+
formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
|
|
355
|
+
}>;
|
|
356
|
+
}
|
|
357
|
+
/** @internal Command dispatcher assembly helper; each value has one owner. */
|
|
358
|
+
export declare function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext;
|
|
330
359
|
/**
|
|
331
360
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
332
361
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
package/lib/definition.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module zhin.js/command
|
|
4
4
|
*/
|
|
5
5
|
import { resolveLocale } from '@zhin.js/plugin-runtime';
|
|
6
|
-
import { createCapabilityContext, readOperationClient, readOperationInputSignal, } from '@zhin.js/feature-kit';
|
|
6
|
+
import { createCapabilityContext, readOperationClient, readOperationSession, readOperationInputSignal, } from '@zhin.js/feature-kit';
|
|
7
7
|
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
8
8
|
import { validateArgumentSchemas } from './arguments.js';
|
|
9
9
|
import { normalizeExecutionPolicy } from './execution-policy.js';
|
|
@@ -173,8 +173,10 @@ export function resolveDynamicParams(params, source) {
|
|
|
173
173
|
}
|
|
174
174
|
return Object.freeze(resolved);
|
|
175
175
|
}
|
|
176
|
-
/** @internal Command dispatcher assembly helper. */
|
|
177
|
-
export function createCommandContext(
|
|
176
|
+
/** @internal Command dispatcher assembly helper; each value has one owner. */
|
|
177
|
+
export function createCommandContext(operation, invocation) {
|
|
178
|
+
const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
|
|
179
|
+
const { args, params = {}, options = {}, segments = [] } = invocation;
|
|
178
180
|
const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
|
|
179
181
|
const session = resolveCommandSession(input);
|
|
180
182
|
const parameterValues = { __proto__: null, ...params };
|
|
@@ -191,6 +193,10 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
|
|
|
191
193
|
...(input !== undefined ? { input: input } : {}),
|
|
192
194
|
...(interaction !== undefined ? { interaction } : {}),
|
|
193
195
|
};
|
|
196
|
+
Object.defineProperty(result, 'session', {
|
|
197
|
+
enumerable: true,
|
|
198
|
+
get: () => readOperationSession(input, adapter),
|
|
199
|
+
});
|
|
194
200
|
Object.defineProperty(result, '$client', {
|
|
195
201
|
enumerable: true,
|
|
196
202
|
get: () => readOperationClient(input, adapter),
|
|
@@ -203,6 +209,20 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
|
|
|
203
209
|
* @internal Command dispatcher projection helper.
|
|
204
210
|
*/
|
|
205
211
|
export function resolveCommandSession(input) {
|
|
212
|
+
const operation = readOperationSession(input);
|
|
213
|
+
if (operation) {
|
|
214
|
+
const endpoint = input && typeof input === 'object' ? input.endpointId : undefined;
|
|
215
|
+
const sender = operation.sender;
|
|
216
|
+
const roles = [...new Set([...sender?.authorizationRoles ?? [], ...sender?.platformRoles ?? []])];
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
adapter: operation.scene.endpoint.adapter,
|
|
219
|
+
...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
|
|
220
|
+
scene: Object.freeze({ id: operation.scene.id, type: operation.scene.kind,
|
|
221
|
+
...(operation.scene.name !== undefined ? { name: operation.scene.name } : {}) }),
|
|
222
|
+
...(sender ? { sender: Object.freeze({ id: sender.id, ...(sender.name !== undefined ? { name: sender.name } : {}),
|
|
223
|
+
role: Object.freeze(roles.length ? roles : ['user']) }) } : {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
206
226
|
if (!isCommandMessageLike(input))
|
|
207
227
|
return Object.freeze({});
|
|
208
228
|
const metadata = input.metadata && typeof input.metadata === 'object'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/command",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "Convention-based Command Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"segment-matcher": "^1.0.5",
|
|
24
|
-
"@zhin.js/feature-kit": "1.1.
|
|
24
|
+
"@zhin.js/feature-kit": "1.1.11",
|
|
25
25
|
"@zhin.js/interaction": "1.1.2",
|
|
26
|
-
"@zhin.js/permission": "1.1.
|
|
27
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
26
|
+
"@zhin.js/permission": "1.1.11",
|
|
27
|
+
"@zhin.js/plugin-runtime": "1.1.19"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^26.1.2",
|
package/src/command-index.ts
CHANGED
|
@@ -58,6 +58,24 @@ export interface CommandDescriptor {
|
|
|
58
58
|
readonly shortcut?: readonly string[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** One dispatch reads the caller's exact operation generation and presentation. */
|
|
62
|
+
export interface CommandDispatchRequest {
|
|
63
|
+
readonly input: CommandMatchInput;
|
|
64
|
+
readonly source?: unknown;
|
|
65
|
+
readonly interactionFactory?: UserInteractionFactory;
|
|
66
|
+
readonly commandPrefix?: string;
|
|
67
|
+
readonly snapshot?: RuntimeSnapshot;
|
|
68
|
+
readonly locale?: Locale;
|
|
69
|
+
}
|
|
70
|
+
/** Presentation and generation shared across one menu lookup/render operation. */
|
|
71
|
+
interface CommandMenuContext {
|
|
72
|
+
readonly commandPrefix: string;
|
|
73
|
+
readonly keyword: string;
|
|
74
|
+
readonly snapshot: RuntimeSnapshot;
|
|
75
|
+
readonly locale: Locale;
|
|
76
|
+
}
|
|
77
|
+
interface CommandMenuSelection { readonly pluginKey?: string; readonly page?: number }
|
|
78
|
+
|
|
61
79
|
export interface CommandDispatchResult {
|
|
62
80
|
readonly matched: boolean;
|
|
63
81
|
readonly status: 'unmatched' | 'invalid' | 'denied' | 'limited' | 'executed';
|
|
@@ -323,26 +341,22 @@ export class CommandIndex {
|
|
|
323
341
|
const admission = admitDirectCommandExecution(match.command.slot.id, match.command.execution, operationSnapshot);
|
|
324
342
|
if (admission.limit) throw new CommandExecutionLimitedError(admission.limit);
|
|
325
343
|
try {
|
|
326
|
-
return await definition.execute(createCommandContext(operationSnapshot, match.command.slot.owner,
|
|
327
|
-
|
|
344
|
+
return await definition.execute(createCommandContext({ snapshot: operationSnapshot, ownerId: match.command.slot.owner,
|
|
345
|
+
adapter: definition.adapter, presentation: { locale } }, {
|
|
346
|
+
args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options,
|
|
347
|
+
}));
|
|
328
348
|
} finally {
|
|
329
349
|
admission.release();
|
|
330
350
|
}
|
|
331
351
|
}
|
|
332
352
|
|
|
333
|
-
async dispatch(
|
|
334
|
-
|
|
335
|
-
source: unknown = undefined,
|
|
336
|
-
interactionFactory?: UserInteractionFactory,
|
|
337
|
-
commandPrefix?: string,
|
|
338
|
-
operationSnapshot: RuntimeSnapshot = this.snapshot,
|
|
339
|
-
localeOverride?: Locale,
|
|
340
|
-
): Promise<CommandDispatchResult> {
|
|
353
|
+
async dispatch({ input, source, interactionFactory, commandPrefix, snapshot: operationSnapshot = this.snapshot,
|
|
354
|
+
locale: localeOverride }: CommandDispatchRequest): Promise<CommandDispatchResult> {
|
|
341
355
|
const locale = localeOverride ?? commandLocale(source, operationSnapshot);
|
|
342
356
|
const prefix = commandPrefix ?? '';
|
|
343
357
|
if (this.#menu) {
|
|
344
358
|
const keyword = this.#menu.keyword ?? commandMessages(locale, 'menuKeyword');
|
|
345
|
-
const menuResult = this.#dispatchMenu(input, prefix, operationSnapshot, locale, keyword);
|
|
359
|
+
const menuResult = this.#dispatchMenu(input, { commandPrefix: prefix, snapshot: operationSnapshot, locale, keyword });
|
|
346
360
|
if (menuResult !== undefined) {
|
|
347
361
|
return Object.freeze({
|
|
348
362
|
matched: true,
|
|
@@ -381,10 +395,12 @@ export class CommandIndex {
|
|
|
381
395
|
owner: match.command.slot.owner, issues: parsed.issues, help });
|
|
382
396
|
const limit = admitCommandExecution(authorSource, match.command.slot.id, match.command.slot.owner, match.command.execution, operationSnapshot);
|
|
383
397
|
if (limit) return Object.freeze({ matched: true, status: 'limited', command: match.command.name, owner: match.command.slot.owner, limit });
|
|
384
|
-
const value = await definition.execute(createCommandContext(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
398
|
+
const value = await definition.execute(createCommandContext({
|
|
399
|
+
snapshot: operationSnapshot, ownerId: match.command.slot.owner, input: authorSource,
|
|
400
|
+
interaction: interactionFactory?.(authorSource), adapter: definition.adapter,
|
|
401
|
+
presentation: { locale, formatCommand: (localPath, owner = match.command.owner) => this.formatCommand(owner, localPath,
|
|
402
|
+
source && typeof source === 'object' && 'conversation' in source ? commandPrefix : undefined, locale) },
|
|
403
|
+
}, { args: textArgs(parsed.remaining), params: parsed.params, segments: parsed.remaining, options: parsed.options }));
|
|
388
404
|
return Object.freeze({ matched: true, status: 'executed', command: match.command.name, owner: match.command.slot.owner, value });
|
|
389
405
|
}
|
|
390
406
|
|
|
@@ -496,10 +512,11 @@ export class CommandIndex {
|
|
|
496
512
|
// 内置菜单命令
|
|
497
513
|
// ==========================================================================
|
|
498
514
|
|
|
499
|
-
#dispatchMenu(input: CommandMatchInput,
|
|
515
|
+
#dispatchMenu(input: CommandMatchInput, context: CommandMenuContext): CommandMenuResult | undefined {
|
|
516
|
+
const { commandPrefix, keyword, locale } = context;
|
|
500
517
|
const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
|
|
501
518
|
if (text === undefined) return undefined;
|
|
502
|
-
if (text === keyword) return this.#buildMenu(
|
|
519
|
+
if (text === keyword) return this.#buildMenu({}, context);
|
|
503
520
|
if (text.startsWith(keyword + ' ')) {
|
|
504
521
|
const key = text.slice(keyword.length + 1).trim();
|
|
505
522
|
const args = key.split(/\s+/u);
|
|
@@ -510,15 +527,15 @@ export class CommandIndex {
|
|
|
510
527
|
|| !Number.isSafeInteger(Number(pageText))) {
|
|
511
528
|
return { value: commandMessages(locale, 'invalidPage', { command: `${commandPrefix}${keyword}` }) };
|
|
512
529
|
}
|
|
513
|
-
return this.#buildMenu(flag === 1 ? args[0] : undefined,
|
|
530
|
+
return this.#buildMenu({ pluginKey: flag === 1 ? args[0] : undefined, page: Number(pageText) }, context);
|
|
514
531
|
}
|
|
515
|
-
if (key) return this.#buildMenu(
|
|
516
|
-
return this.#buildMenu(
|
|
532
|
+
if (key) return this.#buildMenu({ pluginKey: key }, context);
|
|
533
|
+
return this.#buildMenu({}, context);
|
|
517
534
|
}
|
|
518
535
|
return undefined;
|
|
519
536
|
}
|
|
520
537
|
|
|
521
|
-
#buildMenu(pluginKey
|
|
538
|
+
#buildMenu({ pluginKey, page = 1 }: CommandMenuSelection, { commandPrefix, keyword, snapshot, locale }: CommandMenuContext): CommandMenuResult {
|
|
522
539
|
const root = snapshot.root;
|
|
523
540
|
const targetId = pluginKey
|
|
524
541
|
? `${root}/${pluginKey.split('.').join('/')}` as PluginId
|
package/src/definition.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { resolveLocale, type Locale, type PluginId, type RuntimeSnapshot } from
|
|
|
6
6
|
import {
|
|
7
7
|
createCapabilityContext,
|
|
8
8
|
readOperationClient,
|
|
9
|
+
readOperationSession,
|
|
10
|
+
type OperationSession,
|
|
9
11
|
readOperationInputSignal,
|
|
10
12
|
type AdapterClient,
|
|
11
13
|
type RegisteredAdapterName,
|
|
@@ -291,6 +293,8 @@ export interface CommandContext<
|
|
|
291
293
|
readonly input?: TInput;
|
|
292
294
|
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
293
295
|
readonly $client: AdapterClient<TAdapter>;
|
|
296
|
+
/** Bound by the actual IM operation; direct CommandIndex.execute calls may have no Session. */
|
|
297
|
+
readonly session: OperationSession<TAdapter> | undefined;
|
|
294
298
|
/**
|
|
295
299
|
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
296
300
|
*/
|
|
@@ -378,28 +382,40 @@ type TypedCommandContext<TConfig, TInput extends CommandMessage, TAdapter extend
|
|
|
378
382
|
readonly options: InferCommandOptions<O>;
|
|
379
383
|
};
|
|
380
384
|
|
|
385
|
+
/** The returned declaration retains its platform, parameter schemas and callback result. */
|
|
386
|
+
type DeclaredCommandAdapter<D> = D extends { readonly adapter: infer A extends string } ? A
|
|
387
|
+
: D extends { readonly adapter?: infer A extends string | undefined } ? A : undefined;
|
|
388
|
+
export type TypedCommandDefinition<TConfig, TResult, TInput extends CommandMessage, TAdapter extends string | undefined, P extends CommandParams, O extends CommandOptions> =
|
|
389
|
+
Omit<CommandDefinition<TConfig, TResult, TInput, TAdapter>, 'params' | 'options' | 'execute'> & {
|
|
390
|
+
readonly params?: P;
|
|
391
|
+
readonly options?: O;
|
|
392
|
+
execute(context: TypedCommandContext<TConfig, TInput, TAdapter, P, O>): TResult | Promise<TResult>;
|
|
393
|
+
};
|
|
394
|
+
|
|
381
395
|
/**
|
|
382
396
|
* `defineCommand.withConfig<Config>()` 返回的定义函数:config 类型已固定,params / options / 返回值
|
|
383
397
|
* 仍从定义推断。
|
|
384
398
|
* @public
|
|
385
399
|
*/
|
|
386
400
|
export type CommandDefiner<TConfig, TInput extends CommandMessage = CommandMessage> = <
|
|
387
|
-
TResult = unknown,
|
|
401
|
+
const TResult = unknown,
|
|
388
402
|
const P extends CommandParams = {},
|
|
389
403
|
const O extends CommandOptions = {},
|
|
404
|
+
const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
|
|
390
405
|
>(
|
|
391
|
-
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O
|
|
392
|
-
) => Readonly<
|
|
406
|
+
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
|
|
407
|
+
) => Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>>;
|
|
393
408
|
|
|
394
409
|
export function defineCommand<
|
|
395
410
|
TConfig = unknown,
|
|
396
|
-
TResult = unknown,
|
|
411
|
+
const TResult = unknown,
|
|
397
412
|
TInput extends CommandMessage = CommandMessage,
|
|
398
413
|
const P extends CommandParams = {},
|
|
399
414
|
const O extends CommandOptions = {},
|
|
415
|
+
const D extends CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> = CommandAuthoringDefinition<TConfig, TResult, TInput, P, O>,
|
|
400
416
|
>(
|
|
401
|
-
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O
|
|
402
|
-
): Readonly<
|
|
417
|
+
definition: CommandAuthoringDefinition<TConfig, TResult, TInput, P, O> & D,
|
|
418
|
+
): Readonly<TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>> {
|
|
403
419
|
if (typeof definition.execute !== 'function') {
|
|
404
420
|
throw new TypeError('Command execute must be a function');
|
|
405
421
|
}
|
|
@@ -423,7 +439,7 @@ export function defineCommand<
|
|
|
423
439
|
validateCommandShortcutShape(definition.shortcut);
|
|
424
440
|
const execution = normalizeExecutionPolicy(definition.execution);
|
|
425
441
|
return Object.freeze({ $feature: commandBrand, ...definition, ...(execution ? { execution } : {}) }) as Readonly<
|
|
426
|
-
|
|
442
|
+
TypedCommandDefinition<TConfig, TResult, TInput, DeclaredCommandAdapter<D>, P, O> & Pick<D, keyof D & 'adapter'>
|
|
427
443
|
>;
|
|
428
444
|
}
|
|
429
445
|
|
|
@@ -570,19 +586,31 @@ export function resolveDynamicParams(
|
|
|
570
586
|
return Object.freeze(resolved);
|
|
571
587
|
}
|
|
572
588
|
|
|
573
|
-
/**
|
|
574
|
-
export
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
589
|
+
/** Parsed invocation values, independent of the caller's transport and lifetime. */
|
|
590
|
+
export interface CommandInvocationArguments {
|
|
591
|
+
readonly args: readonly string[];
|
|
592
|
+
readonly params?: Readonly<Record<string, CommandParameterValue>>;
|
|
593
|
+
readonly options?: Readonly<Record<string, CommandParameterValue>>;
|
|
594
|
+
readonly segments?: readonly Readonly<CommandSegment>[];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Author and lifetime authority captured by the dispatcher for this invocation. */
|
|
598
|
+
export interface CommandInvocationOperation {
|
|
599
|
+
readonly snapshot: RuntimeSnapshot;
|
|
600
|
+
readonly ownerId: PluginId;
|
|
601
|
+
readonly input?: unknown;
|
|
602
|
+
readonly interaction?: UserInteraction;
|
|
603
|
+
readonly adapter?: string;
|
|
604
|
+
readonly presentation?: Readonly<{
|
|
605
|
+
locale: Locale;
|
|
606
|
+
formatCommand?: (localPath: string, owner?: PluginId) => string | undefined;
|
|
607
|
+
}>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @internal Command dispatcher assembly helper; each value has one owner. */
|
|
611
|
+
export function createCommandContext(operation: CommandInvocationOperation, invocation: CommandInvocationArguments): CommandContext {
|
|
612
|
+
const { snapshot, ownerId, input, interaction, adapter, presentation } = operation;
|
|
613
|
+
const { args, params = {}, options = {}, segments = [] } = invocation;
|
|
586
614
|
const context = createCapabilityContext(snapshot, ownerId, undefined, readOperationInputSignal(input));
|
|
587
615
|
const session = resolveCommandSession(input);
|
|
588
616
|
const parameterValues: Record<string, CommandParameterValue> = { __proto__: null, ...params };
|
|
@@ -599,6 +627,10 @@ export function createCommandContext(
|
|
|
599
627
|
...(input !== undefined ? { input: input as CommandMessage } : {}),
|
|
600
628
|
...(interaction !== undefined ? { interaction } : {}),
|
|
601
629
|
} as CommandContext;
|
|
630
|
+
Object.defineProperty(result, 'session', {
|
|
631
|
+
enumerable: true,
|
|
632
|
+
get: () => readOperationSession(input, adapter),
|
|
633
|
+
});
|
|
602
634
|
Object.defineProperty(result, '$client', {
|
|
603
635
|
enumerable: true,
|
|
604
636
|
get: () => readOperationClient(input, adapter),
|
|
@@ -612,6 +644,20 @@ export function createCommandContext(
|
|
|
612
644
|
* @internal Command dispatcher projection helper.
|
|
613
645
|
*/
|
|
614
646
|
export function resolveCommandSession(input: unknown): CommandSession {
|
|
647
|
+
const operation = readOperationSession(input);
|
|
648
|
+
if (operation) {
|
|
649
|
+
const endpoint = input && typeof input === 'object' ? (input as { readonly endpointId?: unknown }).endpointId : undefined;
|
|
650
|
+
const sender = operation.sender;
|
|
651
|
+
const roles = [...new Set([...sender?.authorizationRoles ?? [], ...sender?.platformRoles ?? []])];
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
adapter: operation.scene.endpoint.adapter,
|
|
654
|
+
...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
|
|
655
|
+
scene: Object.freeze({ id: operation.scene.id, type: operation.scene.kind,
|
|
656
|
+
...(operation.scene.name !== undefined ? { name: operation.scene.name } : {}) }),
|
|
657
|
+
...(sender ? { sender: Object.freeze({ id: sender.id, ...(sender.name !== undefined ? { name: sender.name } : {}),
|
|
658
|
+
role: Object.freeze(roles.length ? roles : ['user']) }) } : {}),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
615
661
|
if (!isCommandMessageLike(input)) return Object.freeze({});
|
|
616
662
|
|
|
617
663
|
const metadata = input.metadata && typeof input.metadata === 'object'
|