@zhin.js/command 1.0.6 → 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/lib/definition.js CHANGED
@@ -1,5 +1,22 @@
1
1
  import { createCapabilityContext, } from '@zhin.js/feature-kit';
2
+ import { assertPermitSyntax } from '@zhin.js/permission';
2
3
  const commandBrand = 'zhin.command/1';
4
+ export const commandParameterTypes = new Set([
5
+ 'string',
6
+ 'number',
7
+ 'integer',
8
+ 'float',
9
+ 'boolean',
10
+ 'word',
11
+ 'text',
12
+ 'mention',
13
+ 'image',
14
+ 'face',
15
+ 'reply',
16
+ 'forward',
17
+ 'dice',
18
+ 'rps',
19
+ ]);
3
20
  /**
4
21
  * 定义一个命令模块(`commands/` 约定目录下默认导出)。
5
22
  * @public 用户侧创作面,承诺 semver(见 docs/contributing/public-api-surface.md)。
@@ -8,8 +25,66 @@ export function defineCommand(definition) {
8
25
  if (typeof definition.execute !== 'function') {
9
26
  throw new TypeError('Command execute must be a function');
10
27
  }
28
+ if (definition.params !== undefined) {
29
+ if (!definition.params || typeof definition.params !== 'object') {
30
+ throw new TypeError('Command params must be a Record<string, CommandParamSchema>');
31
+ }
32
+ for (const [name, schema] of Object.entries(definition.params)) {
33
+ if (!schema || typeof schema !== 'object'
34
+ || !commandParameterTypes.has(schema.type)) {
35
+ throw new TypeError(`Command params.${name} requires a valid type`);
36
+ }
37
+ }
38
+ }
39
+ validateCommandAlias(definition.alias);
40
+ validateCommandPermit(definition.permit);
41
+ validateCommandShortcutShape(definition.shortcut);
11
42
  return Object.freeze({ $feature: commandBrand, ...definition });
12
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
+ }
13
88
  export function bindCommandParameter(definition, parameter) {
14
89
  if (!parameter)
15
90
  return definition;
@@ -47,10 +122,9 @@ export function resolveCommandSession(input) {
47
122
  const metadata = input.metadata && typeof input.metadata === 'object'
48
123
  ? input.metadata
49
124
  : undefined;
50
- const adapter = input.adapter.split('\0')[0] || undefined;
51
- const endpoint = typeof metadata?.endpoint === 'string' && metadata.endpoint
52
- ? metadata.endpoint
53
- : undefined;
125
+ const adapter = input.conversation.endpoint.adapter || undefined;
126
+ const endpoint = input.endpointId
127
+ || (typeof metadata?.endpoint === 'string' && metadata.endpoint ? metadata.endpoint : undefined);
54
128
  const scene = resolveScene(input, metadata);
55
129
  const sender = resolveSender(input, metadata);
56
130
  return Object.freeze({
@@ -64,8 +138,8 @@ function isCommandMessageLike(input) {
64
138
  if (!input || typeof input !== 'object')
65
139
  return false;
66
140
  const value = input;
67
- return typeof value.adapter === 'string'
68
- && typeof value.target === 'string'
141
+ return !!value.conversation
142
+ && typeof value.conversation === 'object'
69
143
  && typeof value.content === 'string';
70
144
  }
71
145
  function resolveScene(input, metadata) {
@@ -76,12 +150,9 @@ function resolveScene(input, metadata) {
76
150
  ...(input.scene.name !== undefined ? { name: input.scene.name } : {}),
77
151
  });
78
152
  }
79
- const parsed = parseTarget(input.target);
80
- const type = (typeof metadata?.channelType === 'string' && metadata.channelType)
81
- || (typeof metadata?.type === 'string' && metadata.type)
82
- || parsed?.type;
83
- const id = (typeof metadata?.channelId === 'string' && metadata.channelId)
84
- || parsed?.id;
153
+ const conversation = input.conversation;
154
+ const type = conversation.kind;
155
+ const id = conversation.id;
85
156
  if (!type || !id)
86
157
  return undefined;
87
158
  const name = firstString(metadata?.channelName, metadata?.group_name, metadata?.groupName, metadata?.sceneName);
@@ -96,24 +167,21 @@ function resolveSender(input, metadata) {
96
167
  if (isCommandSender(structured)) {
97
168
  return freezeSender(structured);
98
169
  }
99
- // 允许上游把 sender 直接做成对象(未来 Runtime Message 演进)
100
- if (isCommandSender(input.sender)) {
101
- return freezeSender(input.sender);
102
- }
103
- const id = typeof input.sender === 'string' && input.sender
104
- ? input.sender
105
- : firstString(metadata?.user_id, metadata?.userId);
170
+ const id = input.sender?.id || firstString(metadata?.user_id, metadata?.userId);
106
171
  if (!id)
107
172
  return undefined;
108
- const name = firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
109
- const role = resolveRoles(metadata);
173
+ const name = input.sender?.name || firstString(metadata?.nickname, metadata?.senderName, metadata?.name);
174
+ const role = resolveRoles(input, metadata);
110
175
  return Object.freeze({
111
176
  id,
112
177
  ...(name !== undefined ? { name } : {}),
113
178
  role,
114
179
  });
115
180
  }
116
- function resolveRoles(metadata) {
181
+ function resolveRoles(input, metadata) {
182
+ if (input.sender?.roles?.length) {
183
+ return Object.freeze([...input.sender.roles]);
184
+ }
117
185
  const roles = [];
118
186
  const push = (value) => {
119
187
  if (typeof value !== 'string')
@@ -136,24 +204,6 @@ function resolveRoles(metadata) {
136
204
  roles.push('user');
137
205
  return Object.freeze(roles);
138
206
  }
139
- function parseTarget(target) {
140
- const parts = target.split(':').filter(Boolean);
141
- if (parts.length < 2)
142
- return undefined;
143
- const [kind, ...rest] = parts;
144
- if (!kind)
145
- return undefined;
146
- const lastPart = parts.at(-1);
147
- if (!lastPart)
148
- return undefined;
149
- if (kind === 'channel' && parts.length >= 3) {
150
- return { type: 'channel', id: lastPart };
151
- }
152
- if (kind === 'temp' && parts.length >= 3) {
153
- return { type: 'private', id: lastPart };
154
- }
155
- return { type: kind, id: rest.join(':') };
156
- }
157
207
  function isCommandScene(value) {
158
208
  if (!value || typeof value !== 'object')
159
209
  return false;
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';
@@ -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.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { CommandIndex } from './command-index.js';
2
+ import { type CommandDefinition } from './definition.js';
2
3
  export declare const commandFeatureId: import("@zhin.js/plugin-runtime").FeatureId;
3
4
  export declare class CommandPathSyntaxError extends TypeError {
4
5
  constructor(file: string, detail?: string);
5
6
  }
6
- declare const commandFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").CommandDefinition<unknown, unknown, import("./definition.js").CommandMessage>, CommandIndex>>;
7
+ declare const commandFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<CommandDefinition<unknown, unknown, import("./definition.js").CommandMessage>, CommandIndex>>;
7
8
  export default commandFeature;
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';
@@ -14,7 +14,7 @@ const commandFiles = {
14
14
  const module = await context.host.loadModule(source.source);
15
15
  const definition = parseCommandDefinition(module.default);
16
16
  const file = parseCommandFile(basename(source.source));
17
- return bindCommandParameter(definition, file?.parameter);
17
+ return bindCommandParameter(definition, resolveParameter(definition, file, source.source));
18
18
  },
19
19
  };
20
20
  async function* discoverCommandDirectory(context, directory, ancestors) {
@@ -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' && isCommandSegment(entry.name)) {
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,48 +56,58 @@ 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
- const dynamicCommandFilePattern = /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.(?:tsx?|[cm]?js)$/;
63
- const commandParameterTypes = new Set([
64
- 'string',
65
- 'number',
66
- 'integer',
67
- 'float',
68
- 'boolean',
69
- 'word',
70
- 'text',
71
- 'mention',
72
- 'image',
73
- 'face',
74
- 'reply',
75
- 'forward',
76
- 'dice',
77
- 'rps',
78
- ]);
59
+ const dynamicCommandFilePatterns = [
60
+ { pattern: /^\[\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: true },
61
+ { pattern: /^\[\.\.\.([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: true },
62
+ { pattern: /^\[\[([a-zA-Z][a-zA-Z0-9]*)\]\]\.(?:tsx?|[cm]?js)$/, optional: true, rest: false },
63
+ { pattern: /^\[([a-zA-Z][a-zA-Z0-9]*)\]\.(?:tsx?|[cm]?js)$/, optional: false, rest: false },
64
+ ];
65
+ const commandModuleExtension = /\.(?:tsx?|[cm]?js)$/u;
79
66
  function parseCommandFile(value) {
80
- if (/^[a-z0-9][a-z0-9-]*\.(?:tsx?|[cm]?js)$/.test(value)) {
81
- return { localSegment: parse(value).name };
82
- }
83
- const match = dynamicCommandFilePattern.exec(value);
84
- if (match) {
85
- const [, name, rawType, rawDefault] = match;
86
- if (!name || !rawType || !commandParameterTypes.has(rawType)) {
87
- throw new CommandPathSyntaxError(value, `unsupported parameter type: ${rawType ?? ''}`);
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 };
88
72
  }
89
- const type = rawType;
73
+ }
74
+ for (const { pattern, optional, rest } of dynamicCommandFilePatterns) {
75
+ const match = pattern.exec(value);
76
+ if (!match || !match[1])
77
+ continue;
78
+ const name = match[1];
90
79
  // Metadata can change during HMR while $name keeps the Capability identity stable.
91
- const parameter = rawDefault === undefined
92
- ? { name, type }
93
- : { name, type, defaultValue: parseParameterValue(name, type, rawDefault, value) };
94
- return { localSegment: `$${name}`, parameter };
80
+ return {
81
+ localSegment: `$${name}`,
82
+ parameter: { name, optional, rest },
83
+ };
95
84
  }
96
85
  if (value.startsWith('[') || value.includes(']')) {
97
86
  throw new CommandPathSyntaxError(value);
98
87
  }
99
88
  return undefined;
100
89
  }
90
+ /** 把文件名形态与 `definition.params` 合并成完整参数定义。 */
91
+ function resolveParameter(definition, file, source) {
92
+ const hint = file?.parameter;
93
+ if (!hint)
94
+ return undefined;
95
+ const schema = definition.params?.[hint.name];
96
+ if (!schema) {
97
+ throw new CommandPathSyntaxError(source, `missing params.${hint.name} declaration in defineCommand({ params })`);
98
+ }
99
+ if (!hint.optional && schema.default !== undefined) {
100
+ throw new CommandPathSyntaxError(source, `params.${hint.name} has a default but the file is required: rename to [[${hint.name}]]`);
101
+ }
102
+ return {
103
+ name: hint.name,
104
+ type: schema.type,
105
+ ...(schema.default !== undefined ? { defaultValue: schema.default } : {}),
106
+ optional: hint.optional,
107
+ rest: hint.rest,
108
+ ...(schema.description !== undefined ? { description: schema.description } : {}),
109
+ };
110
+ }
101
111
  function commandFilePriority(value, preferJavaScript) {
102
112
  const extension = value.slice(value.lastIndexOf('.') + 1);
103
113
  const order = preferJavaScript
@@ -106,39 +116,8 @@ function commandFilePriority(value, preferJavaScript) {
106
116
  const priority = order.indexOf(extension);
107
117
  return priority < 0 ? Number.MAX_SAFE_INTEGER : priority;
108
118
  }
109
- function parseParameterValue(name, type, value, source) {
110
- if (type === 'string' || type === 'word' || type === 'text')
111
- return value;
112
- if (type === 'number' || type === 'integer' || type === 'float') {
113
- const number = Number(value);
114
- if (value.trim().length > 0
115
- && Number.isFinite(number)
116
- && (type !== 'integer' || Number.isInteger(number))
117
- && (type !== 'float' || value.includes('.')))
118
- return number;
119
- throw new CommandPathSyntaxError(source, `default for ${name}:${type} is invalid`);
120
- }
121
- if (type === 'boolean') {
122
- if (value === 'true' || value === 'false')
123
- return value === 'true';
124
- throw new CommandPathSyntaxError(source, `default for ${name}:${type} is invalid`);
125
- }
126
- if (isStructuredParameter(type)) {
127
- throw new CommandPathSyntaxError(source, `default for structured parameter ${name}:${type} is not supported`);
128
- }
129
- throw new CommandPathSyntaxError(source, `default for ${name}:${type} is invalid`);
130
- }
131
- function isStructuredParameter(type) {
132
- return type === 'mention'
133
- || type === 'image'
134
- || type === 'face'
135
- || type === 'reply'
136
- || type === 'forward'
137
- || type === 'dice'
138
- || type === 'rps';
139
- }
140
119
  export class CommandPathSyntaxError extends TypeError {
141
- constructor(file, detail = 'expected [name:type=default].ts(x)') {
120
+ constructor(file, detail = 'expected [name].ts(x), [[name]].ts(x), [...name].ts(x) or [[...name]].ts(x)') {
142
121
  super(`Invalid Command path ${file}: ${detail}`);
143
122
  this.name = 'CommandPathSyntaxError';
144
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/command",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
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.5",
22
- "@zhin.js/plugin-runtime": "1.1.2"
21
+ "@zhin.js/feature-kit": "1.0.8",
22
+ "@zhin.js/plugin-runtime": "1.1.5",
23
+ "@zhin.js/permission": "1.0.1"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/node": "^26.1.2",