@zhin.js/command 1.0.7 → 1.0.10
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 +207 -54
- package/lib/definition.d.ts +70 -7
- package/lib/definition.js +71 -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 +267 -54
- package/src/definition.ts +143 -24
- package/src/index.ts +10 -0
- package/src/permit.ts +14 -0
- package/src/provider.ts +10 -8
package/lib/definition.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCapabilityContext, } from '@zhin.js/feature-kit';
|
|
2
|
+
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
2
3
|
const commandBrand = 'zhin.command/1';
|
|
3
4
|
export const commandParameterTypes = new Set([
|
|
4
5
|
'string',
|
|
@@ -35,8 +36,55 @@ export function defineCommand(definition) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
}
|
|
39
|
+
validateCommandAlias(definition.alias);
|
|
40
|
+
validateCommandPermit(definition.permit);
|
|
41
|
+
validateCommandShortcutShape(definition.shortcut);
|
|
38
42
|
return Object.freeze({ $feature: commandBrand, ...definition });
|
|
39
43
|
}
|
|
44
|
+
function validateCommandAlias(alias) {
|
|
45
|
+
if (alias === undefined)
|
|
46
|
+
return;
|
|
47
|
+
if (!Array.isArray(alias)) {
|
|
48
|
+
throw new TypeError('Command alias must be a readonly string[]');
|
|
49
|
+
}
|
|
50
|
+
for (const [index, entry] of alias.entries()) {
|
|
51
|
+
if (typeof entry !== 'string') {
|
|
52
|
+
throw new TypeError(`Command alias[${index}] must be a string`);
|
|
53
|
+
}
|
|
54
|
+
const tokens = entry.trim().split(/\s+/u).filter(Boolean);
|
|
55
|
+
if (tokens.length === 0) {
|
|
56
|
+
throw new TypeError(`Command alias[${index}] must contain at least one token`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function validateCommandPermit(permit) {
|
|
61
|
+
if (permit === undefined)
|
|
62
|
+
return;
|
|
63
|
+
if (!Array.isArray(permit)) {
|
|
64
|
+
throw new TypeError('Command permit must be a readonly string[]');
|
|
65
|
+
}
|
|
66
|
+
for (const [index, entry] of permit.entries()) {
|
|
67
|
+
if (typeof entry !== 'string') {
|
|
68
|
+
throw new TypeError(`Command permit[${index}] must be a string`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
assertPermitSyntax(permit);
|
|
72
|
+
}
|
|
73
|
+
function validateCommandShortcutShape(shortcut) {
|
|
74
|
+
if (shortcut === undefined)
|
|
75
|
+
return;
|
|
76
|
+
if (!shortcut || typeof shortcut !== 'object' || Array.isArray(shortcut)) {
|
|
77
|
+
throw new TypeError('Command shortcut must be a Record<string, Record<string, value>>');
|
|
78
|
+
}
|
|
79
|
+
for (const [trigger, params] of Object.entries(shortcut)) {
|
|
80
|
+
if (!trigger.trim()) {
|
|
81
|
+
throw new TypeError('Command shortcut keys must be non-empty after trim');
|
|
82
|
+
}
|
|
83
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
84
|
+
throw new TypeError(`Command shortcut[${JSON.stringify(trigger)}] must be a params Record`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
40
88
|
export function bindCommandParameter(definition, parameter) {
|
|
41
89
|
if (!parameter)
|
|
42
90
|
return definition;
|
|
@@ -52,6 +100,18 @@ export function parseCommandDefinition(value) {
|
|
|
52
100
|
}
|
|
53
101
|
return definition;
|
|
54
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* 将动态参数值(可能包含函数)批量解析为静态值。
|
|
105
|
+
* 函数值接收从 `source`(通常是 IM Runtime `Message`)解析出的 {@link CommandSession}。
|
|
106
|
+
*/
|
|
107
|
+
export function resolveDynamicParams(params, source) {
|
|
108
|
+
const session = resolveCommandSession(source);
|
|
109
|
+
const resolved = {};
|
|
110
|
+
for (const [key, value] of Object.entries(params)) {
|
|
111
|
+
resolved[key] = typeof value === 'function' ? value(session) : value;
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze(resolved);
|
|
114
|
+
}
|
|
55
115
|
export function createCommandContext(snapshot, ownerId, args, params = Object.freeze({}), input = undefined, segments = Object.freeze([])) {
|
|
56
116
|
const context = createCapabilityContext(snapshot, ownerId);
|
|
57
117
|
const session = resolveCommandSession(input);
|
|
@@ -75,9 +135,8 @@ export function resolveCommandSession(input) {
|
|
|
75
135
|
? input.metadata
|
|
76
136
|
: undefined;
|
|
77
137
|
const adapter = input.conversation.endpoint.adapter || undefined;
|
|
78
|
-
const endpoint =
|
|
79
|
-
? metadata.endpoint
|
|
80
|
-
: undefined;
|
|
138
|
+
const endpoint = input.endpointId
|
|
139
|
+
|| (typeof metadata?.endpoint === 'string' && metadata.endpoint ? metadata.endpoint : undefined);
|
|
81
140
|
const scene = resolveScene(input, metadata);
|
|
82
141
|
const sender = resolveSender(input, metadata);
|
|
83
142
|
return Object.freeze({
|
|
@@ -104,11 +163,8 @@ function resolveScene(input, metadata) {
|
|
|
104
163
|
});
|
|
105
164
|
}
|
|
106
165
|
const conversation = input.conversation;
|
|
107
|
-
const type =
|
|
108
|
-
|
|
109
|
-
|| conversation.kind;
|
|
110
|
-
const id = (typeof metadata?.channelId === 'string' && metadata.channelId)
|
|
111
|
-
|| conversation.id;
|
|
166
|
+
const type = conversation.kind;
|
|
167
|
+
const id = conversation.id;
|
|
112
168
|
if (!type || !id)
|
|
113
169
|
return undefined;
|
|
114
170
|
const name = firstString(metadata?.channelName, metadata?.group_name, metadata?.groupName, metadata?.sceneName);
|
|
@@ -123,24 +179,21 @@ function resolveSender(input, metadata) {
|
|
|
123
179
|
if (isCommandSender(structured)) {
|
|
124
180
|
return freezeSender(structured);
|
|
125
181
|
}
|
|
126
|
-
|
|
127
|
-
if (isCommandSender(input.sender)) {
|
|
128
|
-
return freezeSender(input.sender);
|
|
129
|
-
}
|
|
130
|
-
const id = typeof input.sender === 'string' && input.sender
|
|
131
|
-
? input.sender
|
|
132
|
-
: firstString(metadata?.user_id, metadata?.userId);
|
|
182
|
+
const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
|
|
133
183
|
if (!id)
|
|
134
184
|
return undefined;
|
|
135
|
-
const name = firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
136
|
-
const role = resolveRoles(metadata);
|
|
185
|
+
const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
|
|
186
|
+
const role = resolveRoles(input, metadata);
|
|
137
187
|
return Object.freeze({
|
|
138
188
|
id,
|
|
139
189
|
...(name !== undefined ? { name } : {}),
|
|
140
190
|
role,
|
|
141
191
|
});
|
|
142
192
|
}
|
|
143
|
-
function resolveRoles(metadata) {
|
|
193
|
+
function resolveRoles(input, metadata) {
|
|
194
|
+
if (input.sender?.roles?.length) {
|
|
195
|
+
return Object.freeze([...input.sender.roles]);
|
|
196
|
+
}
|
|
144
197
|
const roles = [];
|
|
145
198
|
const push = (value) => {
|
|
146
199
|
if (typeof value !== 'string')
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './command-index.js';
|
|
2
2
|
export * from './definition.js';
|
|
3
|
+
export { assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, isBuiltinPermit, isPlatformPermit, parsePermitName, type ParsedPermit, type PermitKind, } from './permit.js';
|
|
3
4
|
export { CommandPathSyntaxError, commandFeatureId, default as commandFeature, } from './provider.js';
|
|
4
5
|
export { default } from './provider.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './command-index.js';
|
|
2
2
|
export * from './definition.js';
|
|
3
|
+
export { assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, isBuiltinPermit, isPlatformPermit, parsePermitName, } from './permit.js';
|
|
3
4
|
export { CommandPathSyntaxError, commandFeatureId, default as commandFeature, } from './provider.js';
|
|
4
5
|
export { default } from './provider.js';
|
package/lib/permit.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export permit 解析/校验 from @zhin.js/permission (SSOT)。
|
|
3
|
+
* 保留 CommandSession 兼容签名供 command-index 过渡使用。
|
|
4
|
+
*/
|
|
5
|
+
export { type PermitKind, type ParsedPermit, parsePermitName, isBuiltinPermit, isPlatformPermit, assertPermitSyntax as assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, } from '@zhin.js/permission';
|
package/lib/permit.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export permit 解析/校验 from @zhin.js/permission (SSOT)。
|
|
3
|
+
* 保留 CommandSession 兼容签名供 command-index 过渡使用。
|
|
4
|
+
*/
|
|
5
|
+
export { parsePermitName, isBuiltinPermit, isPlatformPermit, assertPermitSyntax as assertBuiltinPermits, checkBuiltinPermit, checkBuiltinPermitList, } from '@zhin.js/permission';
|
package/lib/provider.js
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 { defineFeatureProvider, } from '@zhin.js/feature-kit';
|
|
4
4
|
import { CommandIndex } from './command-index.js';
|
|
5
5
|
import { bindCommandParameter, parseCommandDefinition, } from './definition.js';
|
|
@@ -38,7 +38,7 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
for (const entry of entries) {
|
|
41
|
-
if (entry.kind === 'directory' &&
|
|
41
|
+
if (entry.kind === 'directory' && isCapabilityLocalSegment(entry.name)) {
|
|
42
42
|
yield* discoverCommandDirectory(context, join(directory, entry.name), [...ancestors, entry.name]);
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
@@ -56,18 +56,20 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
function isCommandSegment(value) {
|
|
60
|
-
return /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
61
|
-
}
|
|
62
59
|
const dynamicCommandFilePatterns = [
|
|
63
60
|
{ pattern: /^\[\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: true },
|
|
64
61
|
{ pattern: /^\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: true },
|
|
65
62
|
{ pattern: /^\[\[([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: false },
|
|
66
63
|
{ pattern: /^\[([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: false },
|
|
67
64
|
];
|
|
65
|
+
const commandModuleExtension = /\.(?:tsx?|[cm]?js)$/u;
|
|
68
66
|
function parseCommandFile(value) {
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
// 静态段:ASCII kebab(hello.ts)或 Unicode 名(赞我.ts);与 isCapabilityLocalSegment 对齐。
|
|
68
|
+
if (commandModuleExtension.test(value)) {
|
|
69
|
+
const localSegment = parse(value).name;
|
|
70
|
+
if (isCapabilityLocalSegment(localSegment)) {
|
|
71
|
+
return { localSegment };
|
|
72
|
+
}
|
|
71
73
|
}
|
|
72
74
|
for (const { pattern, optional, rest } of dynamicCommandFilePatterns) {
|
|
73
75
|
const match = pattern.exec(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/command",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Convention-based Command Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"segment-matcher": "^1.0.5",
|
|
21
|
-
"@zhin.js/feature-kit": "1.0.
|
|
22
|
-
"@zhin.js/
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.8",
|
|
22
|
+
"@zhin.js/permission": "1.0.1",
|
|
23
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^26.1.2",
|