@zhin.js/command 1.0.7 → 1.0.9
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 +13 -0
- package/lib/command-index.d.ts +4 -0
- package/lib/command-index.js +204 -51
- package/lib/definition.d.ts +43 -5
- package/lib/definition.js +59 -18
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/permit.d.ts +5 -0
- package/lib/permit.js +5 -0
- package/lib/provider.js +9 -7
- package/package.json +4 -3
- package/src/command-index.ts +260 -49
- package/src/definition.ts +102 -22
- package/src/index.ts +10 -0
- package/src/permit.ts +14 -0
- package/src/provider.ts +10 -8
package/src/definition.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createCapabilityContext,
|
|
4
4
|
type CapabilityContext,
|
|
5
5
|
} from '@zhin.js/feature-kit';
|
|
6
|
+
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
6
7
|
|
|
7
8
|
const commandBrand = 'zhin.command/1' as const;
|
|
8
9
|
|
|
@@ -104,7 +105,7 @@ export interface CommandConversation {
|
|
|
104
105
|
readonly kind: 'private' | 'group' | 'channel';
|
|
105
106
|
readonly id: string;
|
|
106
107
|
readonly parent?: Readonly<{
|
|
107
|
-
readonly kind: 'group' | 'channel';
|
|
108
|
+
readonly kind: 'private' | 'group' | 'channel';
|
|
108
109
|
readonly id: string;
|
|
109
110
|
}>;
|
|
110
111
|
readonly threadId?: string;
|
|
@@ -119,14 +120,35 @@ export interface CommandConversation {
|
|
|
119
120
|
export interface CommandMessage {
|
|
120
121
|
readonly conversation: CommandConversation;
|
|
121
122
|
readonly content: string;
|
|
122
|
-
/**
|
|
123
|
-
readonly sender?: string;
|
|
123
|
+
/** 发送者(结构化视图见 CommandContext.sender)。 */
|
|
124
|
+
readonly sender?: { readonly id: string; readonly name?: string; readonly roles?: readonly string[] };
|
|
124
125
|
readonly id?: string;
|
|
125
126
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
126
127
|
/** 若上游已结构化,优先采用。 */
|
|
127
128
|
readonly scene?: CommandScene;
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
// 方法式声明(而非属性式函数类型):方法参数双变,runtime `Message` 的
|
|
130
|
+
// `$reply(content: SendContent)` 等才能鸭式兼容本契约(属性式是抗变,会报错)。
|
|
131
|
+
$reply?(content: unknown): Promise<unknown>;
|
|
132
|
+
$replyFrom?(requester: string, content: unknown): Promise<unknown>;
|
|
133
|
+
/** 向同 Endpoint 的另一个通道发送消息(结构兼容 `Message.$sendTo`)。 */
|
|
134
|
+
$sendTo?(
|
|
135
|
+
conversation: {
|
|
136
|
+
readonly kind: 'private' | 'group' | 'channel';
|
|
137
|
+
readonly id: string;
|
|
138
|
+
readonly parent?: Readonly<{ readonly kind: 'private' | 'group' | 'channel'; readonly id: string }>;
|
|
139
|
+
readonly threadId?: string;
|
|
140
|
+
},
|
|
141
|
+
content: unknown,
|
|
142
|
+
): Promise<unknown>;
|
|
143
|
+
/** 私信当前消息的发送者(结构兼容 `Message.$replyToPrivate`)。 */
|
|
144
|
+
$replyToPrivate?(
|
|
145
|
+
content: unknown,
|
|
146
|
+
from?: boolean | { readonly kind: 'group' | 'channel'; readonly id: string },
|
|
147
|
+
): Promise<unknown>;
|
|
148
|
+
/** 向指定群发送消息(结构兼容 `Message.$replyToGroup`)。 */
|
|
149
|
+
$replyToGroup?(groupId: string, content: unknown): Promise<unknown>;
|
|
150
|
+
/** 向指定频道发送消息(结构兼容 `Message.$replyToChannel`)。 */
|
|
151
|
+
$replyToChannel?(channelId: string, guildId: string, content: unknown, threadId?: string): Promise<unknown>;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
/**
|
|
@@ -176,6 +198,21 @@ export interface CommandDefinition<
|
|
|
176
198
|
* 类型 / 默认值 / 描述使用。静态命令可忽略本字段。
|
|
177
199
|
*/
|
|
178
200
|
readonly params?: Readonly<Record<string, CommandParamSchema>>;
|
|
201
|
+
/**
|
|
202
|
+
* 本地静态段别名(可多词,如 `'gh issue'`)。替换全部本地静态段后仍挂
|
|
203
|
+
* owner 前缀;不打破子插件命名空间。
|
|
204
|
+
*/
|
|
205
|
+
readonly alias?: readonly string[];
|
|
206
|
+
/**
|
|
207
|
+
* 内置 permit DSL(AND)。单项内逗号为 OR。
|
|
208
|
+
* 例:`adapter(icqq)`、`role(master)`、`group(123,456)`。
|
|
209
|
+
*/
|
|
210
|
+
readonly permit?: readonly string[];
|
|
211
|
+
/**
|
|
212
|
+
* 全局整句快捷方式:触发串(trim 后全文相等)→ 预填 params。
|
|
213
|
+
* 可打破 owner 命名空间。
|
|
214
|
+
*/
|
|
215
|
+
readonly shortcut?: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>>;
|
|
179
216
|
execute(context: CommandContext<TConfig, TInput>): TResult | Promise<TResult>;
|
|
180
217
|
}
|
|
181
218
|
|
|
@@ -213,9 +250,58 @@ export function defineCommand<
|
|
|
213
250
|
}
|
|
214
251
|
}
|
|
215
252
|
}
|
|
253
|
+
validateCommandAlias(definition.alias);
|
|
254
|
+
validateCommandPermit(definition.permit);
|
|
255
|
+
validateCommandShortcutShape(definition.shortcut);
|
|
216
256
|
return Object.freeze({ $feature: commandBrand, ...definition });
|
|
217
257
|
}
|
|
218
258
|
|
|
259
|
+
function validateCommandAlias(alias: readonly string[] | undefined): void {
|
|
260
|
+
if (alias === undefined) return;
|
|
261
|
+
if (!Array.isArray(alias)) {
|
|
262
|
+
throw new TypeError('Command alias must be a readonly string[]');
|
|
263
|
+
}
|
|
264
|
+
for (const [index, entry] of alias.entries()) {
|
|
265
|
+
if (typeof entry !== 'string') {
|
|
266
|
+
throw new TypeError(`Command alias[${index}] must be a string`);
|
|
267
|
+
}
|
|
268
|
+
const tokens = entry.trim().split(/\s+/u).filter(Boolean);
|
|
269
|
+
if (tokens.length === 0) {
|
|
270
|
+
throw new TypeError(`Command alias[${index}] must contain at least one token`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateCommandPermit(permit: readonly string[] | undefined): void {
|
|
276
|
+
if (permit === undefined) return;
|
|
277
|
+
if (!Array.isArray(permit)) {
|
|
278
|
+
throw new TypeError('Command permit must be a readonly string[]');
|
|
279
|
+
}
|
|
280
|
+
for (const [index, entry] of permit.entries()) {
|
|
281
|
+
if (typeof entry !== 'string') {
|
|
282
|
+
throw new TypeError(`Command permit[${index}] must be a string`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
assertPermitSyntax(permit);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateCommandShortcutShape(
|
|
289
|
+
shortcut: Readonly<Record<string, Readonly<Record<string, CommandParameterValue>>>> | undefined,
|
|
290
|
+
): void {
|
|
291
|
+
if (shortcut === undefined) return;
|
|
292
|
+
if (!shortcut || typeof shortcut !== 'object' || Array.isArray(shortcut)) {
|
|
293
|
+
throw new TypeError('Command shortcut must be a Record<string, Record<string, value>>');
|
|
294
|
+
}
|
|
295
|
+
for (const [trigger, params] of Object.entries(shortcut)) {
|
|
296
|
+
if (!trigger.trim()) {
|
|
297
|
+
throw new TypeError('Command shortcut keys must be non-empty after trim');
|
|
298
|
+
}
|
|
299
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
300
|
+
throw new TypeError(`Command shortcut[${JSON.stringify(trigger)}] must be a params Record`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
219
305
|
export function bindCommandParameter<
|
|
220
306
|
TConfig,
|
|
221
307
|
TResult,
|
|
@@ -271,9 +357,8 @@ export function resolveCommandSession(input: unknown): CommandSession {
|
|
|
271
357
|
: undefined;
|
|
272
358
|
|
|
273
359
|
const adapter = input.conversation.endpoint.adapter || undefined;
|
|
274
|
-
const endpoint =
|
|
275
|
-
? metadata.endpoint
|
|
276
|
-
: undefined;
|
|
360
|
+
const endpoint = (input as { endpointId?: string }).endpointId
|
|
361
|
+
|| (typeof metadata?.endpoint === 'string' && metadata.endpoint ? metadata.endpoint : undefined);
|
|
277
362
|
|
|
278
363
|
const scene = resolveScene(input, metadata);
|
|
279
364
|
const sender = resolveSender(input, metadata);
|
|
@@ -307,11 +392,8 @@ function resolveScene(
|
|
|
307
392
|
}
|
|
308
393
|
|
|
309
394
|
const conversation = input.conversation;
|
|
310
|
-
const type =
|
|
311
|
-
|
|
312
|
-
|| conversation.kind;
|
|
313
|
-
const id = (typeof metadata?.channelId === 'string' && metadata.channelId)
|
|
314
|
-
|| conversation.id;
|
|
395
|
+
const type = conversation.kind;
|
|
396
|
+
const id = conversation.id;
|
|
315
397
|
if (!type || !id) return undefined;
|
|
316
398
|
|
|
317
399
|
const name = firstString(
|
|
@@ -336,18 +418,12 @@ function resolveSender(
|
|
|
336
418
|
if (isCommandSender(structured)) {
|
|
337
419
|
return freezeSender(structured);
|
|
338
420
|
}
|
|
339
|
-
// 允许上游把 sender 直接做成对象(未来 Runtime Message 演进)
|
|
340
|
-
if (isCommandSender(input.sender)) {
|
|
341
|
-
return freezeSender(input.sender);
|
|
342
|
-
}
|
|
343
421
|
|
|
344
|
-
const id =
|
|
345
|
-
? input.sender
|
|
346
|
-
: firstString(metadata?.user_id, metadata?.userId);
|
|
422
|
+
const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
|
|
347
423
|
if (!id) return undefined;
|
|
348
424
|
|
|
349
|
-
const name = firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
350
|
-
const role = resolveRoles(metadata);
|
|
425
|
+
const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
426
|
+
const role = resolveRoles(input, metadata);
|
|
351
427
|
|
|
352
428
|
return Object.freeze({
|
|
353
429
|
id,
|
|
@@ -357,8 +433,12 @@ function resolveSender(
|
|
|
357
433
|
}
|
|
358
434
|
|
|
359
435
|
function resolveRoles(
|
|
436
|
+
input: CommandMessage,
|
|
360
437
|
metadata: Readonly<Record<string, unknown>> | undefined,
|
|
361
438
|
): readonly string[] {
|
|
439
|
+
if (input.sender?.roles?.length) {
|
|
440
|
+
return Object.freeze([...input.sender.roles]);
|
|
441
|
+
}
|
|
362
442
|
const roles: string[] = [];
|
|
363
443
|
const push = (value: unknown) => {
|
|
364
444
|
if (typeof value !== 'string') return;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export * from './command-index.js';
|
|
2
2
|
export * from './definition.js';
|
|
3
|
+
export {
|
|
4
|
+
assertBuiltinPermits,
|
|
5
|
+
checkBuiltinPermit,
|
|
6
|
+
checkBuiltinPermitList,
|
|
7
|
+
isBuiltinPermit,
|
|
8
|
+
isPlatformPermit,
|
|
9
|
+
parsePermitName,
|
|
10
|
+
type ParsedPermit,
|
|
11
|
+
type PermitKind,
|
|
12
|
+
} from './permit.js';
|
|
3
13
|
export {
|
|
4
14
|
CommandPathSyntaxError,
|
|
5
15
|
commandFeatureId,
|
package/src/permit.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export permit 解析/校验 from @zhin.js/permission (SSOT)。
|
|
3
|
+
* 保留 CommandSession 兼容签名供 command-index 过渡使用。
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
type PermitKind,
|
|
7
|
+
type ParsedPermit,
|
|
8
|
+
parsePermitName,
|
|
9
|
+
isBuiltinPermit,
|
|
10
|
+
isPlatformPermit,
|
|
11
|
+
assertPermitSyntax as assertBuiltinPermits,
|
|
12
|
+
checkBuiltinPermit,
|
|
13
|
+
checkBuiltinPermitList,
|
|
14
|
+
} from '@zhin.js/permission';
|
package/src/provider.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename, join, parse, sep } from 'node:path';
|
|
2
|
-
import { featureId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { featureId, isCapabilityLocalSegment } from '@zhin.js/plugin-runtime';
|
|
3
3
|
import {
|
|
4
4
|
defineFeatureProvider,
|
|
5
5
|
type DiscoveryContext,
|
|
@@ -54,7 +54,7 @@ async function* discoverCommandDirectory(
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
for (const entry of entries) {
|
|
57
|
-
if (entry.kind === 'directory' &&
|
|
57
|
+
if (entry.kind === 'directory' && isCapabilityLocalSegment(entry.name)) {
|
|
58
58
|
yield* discoverCommandDirectory(
|
|
59
59
|
context,
|
|
60
60
|
join(directory, entry.name),
|
|
@@ -74,10 +74,6 @@ async function* discoverCommandDirectory(
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function isCommandSegment(value: string): boolean {
|
|
78
|
-
return /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
77
|
interface ParsedCommandFile {
|
|
82
78
|
readonly localSegment: string;
|
|
83
79
|
readonly parameter?: CommandParameterHint;
|
|
@@ -101,9 +97,15 @@ const dynamicCommandFilePatterns: ReadonlyArray<{
|
|
|
101
97
|
{ pattern: /^\[([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: false },
|
|
102
98
|
];
|
|
103
99
|
|
|
100
|
+
const commandModuleExtension = /\.(?:tsx?|[cm]?js)$/u;
|
|
101
|
+
|
|
104
102
|
function parseCommandFile(value: string): ParsedCommandFile | undefined {
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
// 静态段:ASCII kebab(hello.ts)或 Unicode 名(赞我.ts);与 isCapabilityLocalSegment 对齐。
|
|
104
|
+
if (commandModuleExtension.test(value)) {
|
|
105
|
+
const localSegment = parse(value).name;
|
|
106
|
+
if (isCapabilityLocalSegment(localSegment)) {
|
|
107
|
+
return { localSegment };
|
|
108
|
+
}
|
|
107
109
|
}
|
|
108
110
|
for (const { pattern, optional, rest } of dynamicCommandFilePatterns) {
|
|
109
111
|
const match = pattern.exec(value);
|