@zhin.js/command 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/command-index.d.ts +6 -3
- package/lib/command-index.js +77 -4
- package/lib/definition.d.ts +53 -1
- package/lib/definition.js +8 -15
- package/lib/provider.js +6 -1
- package/package.json +4 -4
- package/src/command-index.ts +89 -0
- package/src/definition.ts +53 -14
- package/src/provider.ts +7 -1
package/lib/command-index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment } from './definition.js';
|
|
2
|
+
import { type CommandDefinition, type CommandParameterDefinition, type CommandParameterType, type CommandSegment, type CommandPromptFactory } from './definition.js';
|
|
3
3
|
export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
4
4
|
readonly required: boolean;
|
|
5
5
|
}
|
|
@@ -20,15 +20,18 @@ export interface CommandDispatchResult {
|
|
|
20
20
|
readonly value?: unknown;
|
|
21
21
|
}
|
|
22
22
|
export type CommandMatchInput = string | readonly Readonly<CommandSegment>[];
|
|
23
|
+
export interface CommandMenuConfig {
|
|
24
|
+
readonly keyword: string;
|
|
25
|
+
}
|
|
23
26
|
export declare class CommandIndex {
|
|
24
27
|
#private;
|
|
25
28
|
private readonly snapshot;
|
|
26
29
|
readonly $projection: "zhin.command-index/1";
|
|
27
|
-
constructor(slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[], snapshot: RuntimeSnapshot);
|
|
30
|
+
constructor(slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[], snapshot: RuntimeSnapshot, menu?: CommandMenuConfig);
|
|
28
31
|
list(): readonly CommandDescriptor[];
|
|
29
32
|
has(name: string): boolean;
|
|
30
33
|
execute(name: string, args?: readonly string[]): Promise<unknown>;
|
|
31
|
-
dispatch(input: CommandMatchInput, source?: unknown): Promise<CommandDispatchResult>;
|
|
34
|
+
dispatch(input: CommandMatchInput, source?: unknown, promptFactory?: CommandPromptFactory): Promise<CommandDispatchResult>;
|
|
32
35
|
}
|
|
33
36
|
export declare function isCommandIndex(value: unknown): value is CommandIndex;
|
|
34
37
|
export declare class CommandParameterValueError extends TypeError {
|
package/lib/command-index.js
CHANGED
|
@@ -19,8 +19,10 @@ export class CommandIndex {
|
|
|
19
19
|
#commands;
|
|
20
20
|
#routes;
|
|
21
21
|
#shortcuts;
|
|
22
|
-
|
|
22
|
+
#menu;
|
|
23
|
+
constructor(slots, snapshot, menu) {
|
|
23
24
|
this.snapshot = snapshot;
|
|
25
|
+
this.#menu = menu;
|
|
24
26
|
const commands = [];
|
|
25
27
|
const routes = [];
|
|
26
28
|
const occupancy = new Map();
|
|
@@ -117,13 +119,25 @@ export class CommandIndex {
|
|
|
117
119
|
// Host / 无 session:跳过 permit;无 source 时函数默认值得到空 session。
|
|
118
120
|
return match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, undefined)));
|
|
119
121
|
}
|
|
120
|
-
async dispatch(input, source = undefined) {
|
|
122
|
+
async dispatch(input, source = undefined, promptFactory) {
|
|
123
|
+
if (this.#menu) {
|
|
124
|
+
const menuValue = this.#dispatchMenu(input);
|
|
125
|
+
if (menuValue !== undefined) {
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
matched: true,
|
|
128
|
+
command: this.#menu.keyword,
|
|
129
|
+
owner: this.snapshot.root,
|
|
130
|
+
value: menuValue,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const prompt = promptFactory?.(source);
|
|
121
135
|
const shortcut = this.#matchShortcut(input);
|
|
122
136
|
if (shortcut) {
|
|
123
137
|
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
124
138
|
return Object.freeze({ matched: false });
|
|
125
139
|
}
|
|
126
|
-
const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), resolveDynamicParams(shortcut.params, source), source, Object.freeze([])));
|
|
140
|
+
const value = await shortcut.record.slot.definition.execute(createCommandContext(this.snapshot, shortcut.record.slot.owner, Object.freeze([]), resolveDynamicParams(shortcut.params, source), source, Object.freeze([]), prompt));
|
|
127
141
|
return Object.freeze({
|
|
128
142
|
matched: true,
|
|
129
143
|
command: shortcut.record.name,
|
|
@@ -138,7 +152,7 @@ export class CommandIndex {
|
|
|
138
152
|
return Object.freeze({ matched: false });
|
|
139
153
|
}
|
|
140
154
|
const args = textArgs(match.remaining);
|
|
141
|
-
const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining));
|
|
155
|
+
const value = await match.command.slot.definition.execute(createCommandContext(this.snapshot, match.command.slot.owner, args, resolveDynamicParams(match.params, source), source, match.remaining, prompt));
|
|
142
156
|
return Object.freeze({
|
|
143
157
|
matched: true,
|
|
144
158
|
command: match.command.name,
|
|
@@ -232,6 +246,65 @@ export class CommandIndex {
|
|
|
232
246
|
throw new CommandParameterValueError(parameter.name, parameter.type, value);
|
|
233
247
|
}
|
|
234
248
|
}
|
|
249
|
+
// ==========================================================================
|
|
250
|
+
// 内置菜单命令
|
|
251
|
+
// ==========================================================================
|
|
252
|
+
#dispatchMenu(input) {
|
|
253
|
+
const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
|
|
254
|
+
if (text === undefined)
|
|
255
|
+
return undefined;
|
|
256
|
+
const keyword = this.#menu.keyword;
|
|
257
|
+
if (text === keyword)
|
|
258
|
+
return this.#buildMenu();
|
|
259
|
+
if (text.startsWith(keyword + ' ')) {
|
|
260
|
+
const key = text.slice(keyword.length + 1).trim();
|
|
261
|
+
if (key)
|
|
262
|
+
return this.#buildMenu(key);
|
|
263
|
+
return this.#buildMenu();
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
#buildMenu(pluginKey) {
|
|
268
|
+
const root = this.snapshot.root;
|
|
269
|
+
const targetId = pluginKey
|
|
270
|
+
? `${root}/${pluginKey.split('.').join('/')}`
|
|
271
|
+
: root;
|
|
272
|
+
const node = this.snapshot.tree.get(targetId);
|
|
273
|
+
if (!node)
|
|
274
|
+
return `未找到插件: ${pluginKey}`;
|
|
275
|
+
const directCommands = this.#commands.filter((cmd) => cmd.slot.owner === targetId);
|
|
276
|
+
const children = node.children
|
|
277
|
+
.map((childId) => this.snapshot.tree.get(childId))
|
|
278
|
+
.filter((child) => !!child);
|
|
279
|
+
const keyword = this.#menu.keyword;
|
|
280
|
+
const displayKey = pluginKey ?? node.instanceKey;
|
|
281
|
+
const lines = [`=== ${displayKey} 指令菜单 ===`];
|
|
282
|
+
if (directCommands.length > 0) {
|
|
283
|
+
lines.push('');
|
|
284
|
+
for (const cmd of directCommands) {
|
|
285
|
+
const desc = cmd.description ? ` - ${cmd.description}` : '';
|
|
286
|
+
lines.push(` ${cmd.name}${desc}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (children.length > 0) {
|
|
290
|
+
lines.push('');
|
|
291
|
+
lines.push('子插件:');
|
|
292
|
+
for (const child of children) {
|
|
293
|
+
const childKey = pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey;
|
|
294
|
+
const label = child.metadata?.displayName ?? child.instanceKey;
|
|
295
|
+
lines.push(` ${childKey} (${label})`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (directCommands.length === 0 && children.length === 0) {
|
|
299
|
+
lines.push('');
|
|
300
|
+
lines.push('(暂无指令和子插件)');
|
|
301
|
+
}
|
|
302
|
+
if (children.length > 0) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push(`提示:使用「${keyword} <插件名>」查看子插件的指令`);
|
|
305
|
+
}
|
|
306
|
+
return lines.join('\n');
|
|
307
|
+
}
|
|
235
308
|
}
|
|
236
309
|
export function isCommandIndex(value) {
|
|
237
310
|
return !!value && typeof value === 'object'
|
package/lib/definition.d.ts
CHANGED
|
@@ -143,6 +143,54 @@ export interface CommandSession {
|
|
|
143
143
|
/** 发送者对象(id / name / role[])。 */
|
|
144
144
|
readonly sender?: CommandSender;
|
|
145
145
|
}
|
|
146
|
+
export interface CommandPromptOptions {
|
|
147
|
+
readonly timeout?: number;
|
|
148
|
+
readonly timeoutText?: string;
|
|
149
|
+
}
|
|
150
|
+
export interface CommandPromptListOptions extends CommandPromptOptions {
|
|
151
|
+
readonly type?: 'text' | 'number' | 'boolean';
|
|
152
|
+
readonly separator?: string;
|
|
153
|
+
readonly default?: readonly (string | number | boolean)[];
|
|
154
|
+
}
|
|
155
|
+
export interface CommandPromptPickOptions<V = unknown> extends CommandPromptOptions {
|
|
156
|
+
readonly options: readonly {
|
|
157
|
+
readonly label: string;
|
|
158
|
+
readonly value: V;
|
|
159
|
+
}[];
|
|
160
|
+
readonly multiple?: boolean;
|
|
161
|
+
readonly separator?: string;
|
|
162
|
+
readonly default?: V | readonly V[];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 命令内对话式交互输入。
|
|
166
|
+
*
|
|
167
|
+
* IM 派发时自动注入(`context.prompt`);Host / CLI 无消息来源时为 `undefined`。
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* defineCommand({
|
|
171
|
+
* execute: async (context) => {
|
|
172
|
+
* const name = await context.prompt!.text('请输入你的名字');
|
|
173
|
+
* const age = await context.prompt!.number('请输入你的年龄');
|
|
174
|
+
* return `你好 ${name},你 ${age} 岁了`;
|
|
175
|
+
* },
|
|
176
|
+
* });
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export interface CommandPrompt {
|
|
180
|
+
text(tips: string, options?: CommandPromptOptions & {
|
|
181
|
+
readonly default?: string;
|
|
182
|
+
}): Promise<string>;
|
|
183
|
+
number(tips: string, options?: CommandPromptOptions & {
|
|
184
|
+
readonly default?: number;
|
|
185
|
+
}): Promise<number>;
|
|
186
|
+
confirm(tips: string, options?: CommandPromptOptions & {
|
|
187
|
+
readonly condition?: string;
|
|
188
|
+
readonly default?: boolean;
|
|
189
|
+
}): Promise<boolean>;
|
|
190
|
+
list(tips: string, options?: CommandPromptListOptions): Promise<readonly (string | number | boolean)[]>;
|
|
191
|
+
pick<V = unknown>(tips: string, options: CommandPromptPickOptions<V>): Promise<V | readonly V[]>;
|
|
192
|
+
}
|
|
193
|
+
export type CommandPromptFactory = (source: unknown) => CommandPrompt | undefined;
|
|
146
194
|
export interface CommandContext<TConfig = unknown, TInput extends CommandMessage = CommandMessage> extends CapabilityContext<TConfig>, CommandSession {
|
|
147
195
|
readonly args: readonly string[];
|
|
148
196
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
@@ -153,6 +201,10 @@ export interface CommandContext<TConfig = unknown, TInput extends CommandMessage
|
|
|
153
201
|
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
154
202
|
*/
|
|
155
203
|
readonly input?: TInput;
|
|
204
|
+
/**
|
|
205
|
+
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
206
|
+
*/
|
|
207
|
+
readonly prompt?: CommandPrompt;
|
|
156
208
|
}
|
|
157
209
|
export interface CommandDefinition<TConfig = unknown, TResult = unknown, TInput extends CommandMessage = CommandMessage> {
|
|
158
210
|
readonly $feature: typeof commandBrand;
|
|
@@ -197,7 +249,7 @@ export declare function parseCommandDefinition(value: unknown): CommandDefinitio
|
|
|
197
249
|
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
198
250
|
*/
|
|
199
251
|
export declare function resolveDynamicParams(params: Readonly<Record<string, CommandDynamicValue>>, source: unknown): Readonly<Record<string, CommandParameterValue>>;
|
|
200
|
-
export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[]): CommandContext;
|
|
252
|
+
export declare function createCommandContext(snapshot: RuntimeSnapshot, ownerId: PluginId, args: readonly string[], params?: Readonly<Record<string, CommandParameterValue>>, input?: unknown, segments?: readonly Readonly<CommandSegment>[], prompt?: CommandPrompt): CommandContext;
|
|
201
253
|
/**
|
|
202
254
|
* 从派发来源(通常是 Runtime `Message`)解析入站快捷字段。
|
|
203
255
|
* 不依赖 `@zhin.js/core`,按 {@link CommandMessage} 结构鸭式识别。
|
package/lib/definition.js
CHANGED
|
@@ -112,7 +112,7 @@ export function resolveDynamicParams(params, source) {
|
|
|
112
112
|
}
|
|
113
113
|
return Object.freeze(resolved);
|
|
114
114
|
}
|
|
115
|
-
export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([])) {
|
|
115
|
+
export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([]), prompt) {
|
|
116
116
|
const context = createCapabilityContext(snapshot, ownerId);
|
|
117
117
|
const session = resolveCommandSession(input);
|
|
118
118
|
return Object.freeze({
|
|
@@ -122,6 +122,7 @@ export function createCommandContext(snapshot, ownerId, args, params = Object.fr
|
|
|
122
122
|
params: Object.freeze({ ...params }),
|
|
123
123
|
segments: freezeSegments(segments),
|
|
124
124
|
...(input !== undefined ? { input: input } : {}),
|
|
125
|
+
...(prompt !== undefined ? { prompt } : {}),
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
/**
|
|
@@ -179,15 +180,10 @@ function resolveSender(input, metadata) {
|
|
|
179
180
|
if (isCommandSender(structured)) {
|
|
180
181
|
return freezeSender(structured);
|
|
181
182
|
}
|
|
182
|
-
const
|
|
183
|
-
const id = input.sender?.id
|
|
184
|
-
|| (typeof $sender?.id === 'string' ? $sender.id : undefined)
|
|
185
|
-
|| firstString(metadata?.user_id, metadata?.userId);
|
|
183
|
+
const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
|
|
186
184
|
if (!id)
|
|
187
185
|
return undefined;
|
|
188
|
-
const name = input.sender?.name
|
|
189
|
-
|| (typeof $sender?.name === 'string' ? $sender.name : undefined)
|
|
190
|
-
|| firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
186
|
+
const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
191
187
|
const role = resolveRoles(input, metadata);
|
|
192
188
|
return Object.freeze({
|
|
193
189
|
id,
|
|
@@ -207,19 +203,16 @@ function resolveRoles(input, metadata) {
|
|
|
207
203
|
if (trimmed && !roles.includes(trimmed))
|
|
208
204
|
roles.push(trimmed);
|
|
209
205
|
};
|
|
210
|
-
// IM Message 的 $sender 携带 enrich 快照(isMaster/isTrusted)和平台群角色(role)
|
|
211
|
-
const $sender = input.$sender;
|
|
212
|
-
if ($sender?.isMaster === true || metadata?.isMaster === true)
|
|
213
|
-
push('master');
|
|
214
|
-
else if ($sender?.isTrusted === true || metadata?.isTrusted === true)
|
|
215
|
-
push('trusted');
|
|
216
|
-
push($sender?.role);
|
|
217
206
|
if (Array.isArray(metadata?.roles)) {
|
|
218
207
|
for (const item of metadata.roles)
|
|
219
208
|
push(item);
|
|
220
209
|
}
|
|
221
210
|
push(metadata?.senderRole);
|
|
222
211
|
push(metadata?.role);
|
|
212
|
+
if (metadata?.isMaster === true)
|
|
213
|
+
push('master');
|
|
214
|
+
if (metadata?.isTrusted === true)
|
|
215
|
+
push('trusted');
|
|
223
216
|
if (roles.length === 0)
|
|
224
217
|
roles.push('user');
|
|
225
218
|
return Object.freeze(roles);
|
package/lib/provider.js
CHANGED
|
@@ -132,7 +132,12 @@ const commandFeature = defineFeatureProvider({
|
|
|
132
132
|
},
|
|
133
133
|
runtime: {
|
|
134
134
|
project(slots, context) {
|
|
135
|
-
|
|
135
|
+
const rootConfig = context.snapshot.config.get(context.snapshot.root);
|
|
136
|
+
const rawKeyword = rootConfig?.menuKeyword;
|
|
137
|
+
const menu = rawKeyword === false || rawKeyword === ''
|
|
138
|
+
? undefined
|
|
139
|
+
: { keyword: typeof rawKeyword === 'string' ? rawKeyword : '菜单' };
|
|
140
|
+
return { value: new CommandIndex(slots, context.snapshot, menu) };
|
|
136
141
|
},
|
|
137
142
|
},
|
|
138
143
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/command",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Convention-based Command Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"segment-matcher": "^1.0.5",
|
|
21
|
-
"@zhin.js/
|
|
22
|
-
"@zhin.js/
|
|
23
|
-
"@zhin.js/
|
|
21
|
+
"@zhin.js/permission": "1.0.2",
|
|
22
|
+
"@zhin.js/plugin-runtime": "1.1.6",
|
|
23
|
+
"@zhin.js/feature-kit": "1.0.9"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^26.1.2",
|
package/src/command-index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type CommandParameterValue,
|
|
18
18
|
type CommandSegment,
|
|
19
19
|
type CommandDynamicValue,
|
|
20
|
+
type CommandPromptFactory,
|
|
20
21
|
resolveDynamicParams,
|
|
21
22
|
} from './definition.js';
|
|
22
23
|
import { permissionHostToken, type PermissionHost } from '@zhin.js/permission';
|
|
@@ -83,16 +84,23 @@ const segmentFields = {
|
|
|
83
84
|
rps: 'result',
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
export interface CommandMenuConfig {
|
|
88
|
+
readonly keyword: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
86
91
|
export class CommandIndex {
|
|
87
92
|
readonly $projection = 'zhin.command-index/1' as const;
|
|
88
93
|
readonly #commands: readonly CommandRecord[];
|
|
89
94
|
readonly #routes: readonly CommandRoute[];
|
|
90
95
|
readonly #shortcuts: ReadonlyMap<string, ShortcutEntry>;
|
|
96
|
+
readonly #menu?: CommandMenuConfig;
|
|
91
97
|
|
|
92
98
|
constructor(
|
|
93
99
|
slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[],
|
|
94
100
|
private readonly snapshot: RuntimeSnapshot,
|
|
101
|
+
menu?: CommandMenuConfig,
|
|
95
102
|
) {
|
|
103
|
+
this.#menu = menu;
|
|
96
104
|
const commands: CommandRecord[] = [];
|
|
97
105
|
const routes: CommandRoute[] = [];
|
|
98
106
|
const occupancy = new Map<string, string>();
|
|
@@ -213,7 +221,20 @@ export class CommandIndex {
|
|
|
213
221
|
async dispatch(
|
|
214
222
|
input: CommandMatchInput,
|
|
215
223
|
source: unknown = undefined,
|
|
224
|
+
promptFactory?: CommandPromptFactory,
|
|
216
225
|
): Promise<CommandDispatchResult> {
|
|
226
|
+
if (this.#menu) {
|
|
227
|
+
const menuValue = this.#dispatchMenu(input);
|
|
228
|
+
if (menuValue !== undefined) {
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
matched: true,
|
|
231
|
+
command: this.#menu.keyword,
|
|
232
|
+
owner: this.snapshot.root,
|
|
233
|
+
value: menuValue,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const prompt = promptFactory?.(source);
|
|
217
238
|
const shortcut = this.#matchShortcut(input);
|
|
218
239
|
if (shortcut) {
|
|
219
240
|
if (!(await this.#permitAllows(shortcut.record, source))) {
|
|
@@ -227,6 +248,7 @@ export class CommandIndex {
|
|
|
227
248
|
resolveDynamicParams(shortcut.params, source),
|
|
228
249
|
source,
|
|
229
250
|
Object.freeze([]),
|
|
251
|
+
prompt,
|
|
230
252
|
),
|
|
231
253
|
);
|
|
232
254
|
return Object.freeze({
|
|
@@ -251,6 +273,7 @@ export class CommandIndex {
|
|
|
251
273
|
resolveDynamicParams(match.params, source),
|
|
252
274
|
source,
|
|
253
275
|
match.remaining,
|
|
276
|
+
prompt,
|
|
254
277
|
),
|
|
255
278
|
);
|
|
256
279
|
return Object.freeze({
|
|
@@ -341,6 +364,72 @@ export class CommandIndex {
|
|
|
341
364
|
throw new CommandParameterValueError(parameter.name, parameter.type, value);
|
|
342
365
|
}
|
|
343
366
|
}
|
|
367
|
+
|
|
368
|
+
// ==========================================================================
|
|
369
|
+
// 内置菜单命令
|
|
370
|
+
// ==========================================================================
|
|
371
|
+
|
|
372
|
+
#dispatchMenu(input: CommandMatchInput): string | undefined {
|
|
373
|
+
const text = typeof input === 'string' ? input.trim() : exactMessageText(input);
|
|
374
|
+
if (text === undefined) return undefined;
|
|
375
|
+
const keyword = this.#menu!.keyword;
|
|
376
|
+
if (text === keyword) return this.#buildMenu();
|
|
377
|
+
if (text.startsWith(keyword + ' ')) {
|
|
378
|
+
const key = text.slice(keyword.length + 1).trim();
|
|
379
|
+
if (key) return this.#buildMenu(key);
|
|
380
|
+
return this.#buildMenu();
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#buildMenu(pluginKey?: string): string {
|
|
386
|
+
const root = this.snapshot.root;
|
|
387
|
+
const targetId = pluginKey
|
|
388
|
+
? `${root}/${pluginKey.split('.').join('/')}` as PluginId
|
|
389
|
+
: root;
|
|
390
|
+
|
|
391
|
+
const node = this.snapshot.tree.get(targetId);
|
|
392
|
+
if (!node) return `未找到插件: ${pluginKey}`;
|
|
393
|
+
|
|
394
|
+
const directCommands = this.#commands.filter((cmd) => cmd.slot.owner === targetId);
|
|
395
|
+
const children = node.children
|
|
396
|
+
.map((childId) => this.snapshot.tree.get(childId))
|
|
397
|
+
.filter((child): child is NonNullable<typeof child> => !!child);
|
|
398
|
+
|
|
399
|
+
const keyword = this.#menu!.keyword;
|
|
400
|
+
const displayKey = pluginKey ?? node.instanceKey;
|
|
401
|
+
const lines: string[] = [`=== ${displayKey} 指令菜单 ===`];
|
|
402
|
+
|
|
403
|
+
if (directCommands.length > 0) {
|
|
404
|
+
lines.push('');
|
|
405
|
+
for (const cmd of directCommands) {
|
|
406
|
+
const desc = cmd.description ? ` - ${cmd.description}` : '';
|
|
407
|
+
lines.push(` ${cmd.name}${desc}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (children.length > 0) {
|
|
412
|
+
lines.push('');
|
|
413
|
+
lines.push('子插件:');
|
|
414
|
+
for (const child of children) {
|
|
415
|
+
const childKey = pluginKey ? `${pluginKey}.${child.instanceKey}` : child.instanceKey;
|
|
416
|
+
const label = child.metadata?.displayName ?? child.instanceKey;
|
|
417
|
+
lines.push(` ${childKey} (${label})`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (directCommands.length === 0 && children.length === 0) {
|
|
422
|
+
lines.push('');
|
|
423
|
+
lines.push('(暂无指令和子插件)');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (children.length > 0) {
|
|
427
|
+
lines.push('');
|
|
428
|
+
lines.push(`提示:使用「${keyword} <插件名>」查看子插件的指令`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return lines.join('\n');
|
|
432
|
+
}
|
|
344
433
|
}
|
|
345
434
|
|
|
346
435
|
export function isCommandIndex(value: unknown): value is CommandIndex {
|
package/src/definition.ts
CHANGED
|
@@ -193,6 +193,49 @@ export interface CommandSession {
|
|
|
193
193
|
readonly sender?: CommandSender;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
export interface CommandPromptOptions {
|
|
197
|
+
readonly timeout?: number;
|
|
198
|
+
readonly timeoutText?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface CommandPromptListOptions extends CommandPromptOptions {
|
|
202
|
+
readonly type?: 'text' | 'number' | 'boolean';
|
|
203
|
+
readonly separator?: string;
|
|
204
|
+
readonly default?: readonly (string | number | boolean)[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface CommandPromptPickOptions<V = unknown> extends CommandPromptOptions {
|
|
208
|
+
readonly options: readonly { readonly label: string; readonly value: V }[];
|
|
209
|
+
readonly multiple?: boolean;
|
|
210
|
+
readonly separator?: string;
|
|
211
|
+
readonly default?: V | readonly V[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 命令内对话式交互输入。
|
|
216
|
+
*
|
|
217
|
+
* IM 派发时自动注入(`context.prompt`);Host / CLI 无消息来源时为 `undefined`。
|
|
218
|
+
*
|
|
219
|
+
* ```ts
|
|
220
|
+
* defineCommand({
|
|
221
|
+
* execute: async (context) => {
|
|
222
|
+
* const name = await context.prompt!.text('请输入你的名字');
|
|
223
|
+
* const age = await context.prompt!.number('请输入你的年龄');
|
|
224
|
+
* return `你好 ${name},你 ${age} 岁了`;
|
|
225
|
+
* },
|
|
226
|
+
* });
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export interface CommandPrompt {
|
|
230
|
+
text(tips: string, options?: CommandPromptOptions & { readonly default?: string }): Promise<string>;
|
|
231
|
+
number(tips: string, options?: CommandPromptOptions & { readonly default?: number }): Promise<number>;
|
|
232
|
+
confirm(tips: string, options?: CommandPromptOptions & { readonly condition?: string; readonly default?: boolean }): Promise<boolean>;
|
|
233
|
+
list(tips: string, options?: CommandPromptListOptions): Promise<readonly (string | number | boolean)[]>;
|
|
234
|
+
pick<V = unknown>(tips: string, options: CommandPromptPickOptions<V>): Promise<V | readonly V[]>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export type CommandPromptFactory = (source: unknown) => CommandPrompt | undefined;
|
|
238
|
+
|
|
196
239
|
export interface CommandContext<
|
|
197
240
|
TConfig = unknown,
|
|
198
241
|
TInput extends CommandMessage = CommandMessage,
|
|
@@ -206,6 +249,10 @@ export interface CommandContext<
|
|
|
206
249
|
* Host / `CommandIndex.execute` 等无消息路径可能为 `undefined`。
|
|
207
250
|
*/
|
|
208
251
|
readonly input?: TInput;
|
|
252
|
+
/**
|
|
253
|
+
* 对话式交互输入。IM 派发时自动注入;无消息来源时为 `undefined`。
|
|
254
|
+
*/
|
|
255
|
+
readonly prompt?: CommandPrompt;
|
|
209
256
|
}
|
|
210
257
|
|
|
211
258
|
export interface CommandDefinition<
|
|
@@ -371,6 +418,7 @@ export function createCommandContext(
|
|
|
371
418
|
params: Readonly<Record<string, CommandParameterValue>> = Object.freeze({}),
|
|
372
419
|
input: unknown = undefined,
|
|
373
420
|
segments: readonly Readonly<CommandSegment>[] = Object.freeze([]),
|
|
421
|
+
prompt?: CommandPrompt,
|
|
374
422
|
): CommandContext {
|
|
375
423
|
const context = createCapabilityContext(snapshot, ownerId);
|
|
376
424
|
const session = resolveCommandSession(input);
|
|
@@ -381,6 +429,7 @@ export function createCommandContext(
|
|
|
381
429
|
params: Object.freeze({ ...params }),
|
|
382
430
|
segments: freezeSegments(segments),
|
|
383
431
|
...(input !== undefined ? { input: input as CommandMessage } : {}),
|
|
432
|
+
...(prompt !== undefined ? { prompt } : {}),
|
|
384
433
|
});
|
|
385
434
|
}
|
|
386
435
|
|
|
@@ -458,15 +507,10 @@ function resolveSender(
|
|
|
458
507
|
return freezeSender(structured);
|
|
459
508
|
}
|
|
460
509
|
|
|
461
|
-
const
|
|
462
|
-
const id = input.sender?.id
|
|
463
|
-
|| (typeof $sender?.id === 'string' ? $sender.id : undefined)
|
|
464
|
-
|| firstString(metadata?.user_id, metadata?.userId);
|
|
510
|
+
const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
|
|
465
511
|
if (!id) return undefined;
|
|
466
512
|
|
|
467
|
-
const name = input.sender?.name
|
|
468
|
-
|| (typeof $sender?.name === 'string' ? $sender.name : undefined)
|
|
469
|
-
|| firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
513
|
+
const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
470
514
|
const role = resolveRoles(input, metadata);
|
|
471
515
|
|
|
472
516
|
return Object.freeze({
|
|
@@ -490,18 +534,13 @@ function resolveRoles(
|
|
|
490
534
|
if (trimmed && !roles.includes(trimmed)) roles.push(trimmed);
|
|
491
535
|
};
|
|
492
536
|
|
|
493
|
-
// IM Message 的 $sender 携带 enrich 快照(isMaster/isTrusted)和平台群角色(role)
|
|
494
|
-
const $sender = (input as { readonly $sender?: Readonly<Record<string, unknown>> }).$sender;
|
|
495
|
-
|
|
496
|
-
if ($sender?.isMaster === true || metadata?.isMaster === true) push('master');
|
|
497
|
-
else if ($sender?.isTrusted === true || metadata?.isTrusted === true) push('trusted');
|
|
498
|
-
|
|
499
|
-
push($sender?.role);
|
|
500
537
|
if (Array.isArray(metadata?.roles)) {
|
|
501
538
|
for (const item of metadata.roles) push(item);
|
|
502
539
|
}
|
|
503
540
|
push(metadata?.senderRole);
|
|
504
541
|
push(metadata?.role);
|
|
542
|
+
if (metadata?.isMaster === true) push('master');
|
|
543
|
+
if (metadata?.isTrusted === true) push('trusted');
|
|
505
544
|
if (roles.length === 0) roles.push('user');
|
|
506
545
|
return Object.freeze(roles);
|
|
507
546
|
}
|
package/src/provider.ts
CHANGED
|
@@ -183,7 +183,13 @@ const commandFeature = defineFeatureProvider({
|
|
|
183
183
|
},
|
|
184
184
|
runtime: {
|
|
185
185
|
project(slots, context) {
|
|
186
|
-
|
|
186
|
+
const rootConfig = context.snapshot.config.get(context.snapshot.root) as
|
|
187
|
+
| Record<string, unknown> | undefined;
|
|
188
|
+
const rawKeyword = rootConfig?.menuKeyword;
|
|
189
|
+
const menu = rawKeyword === false || rawKeyword === ''
|
|
190
|
+
? undefined
|
|
191
|
+
: { keyword: typeof rawKeyword === 'string' ? rawKeyword : '菜单' };
|
|
192
|
+
return { value: new CommandIndex(slots, context.snapshot, menu) };
|
|
187
193
|
},
|
|
188
194
|
},
|
|
189
195
|
});
|