@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/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 = typeof metadata?.endpoint === 'string' && metadata.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 = (typeof metadata?.channelType === 'string' && metadata.channelType)
108
- || (typeof metadata?.type === 'string' && metadata.type)
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
- // 允许上游把 sender 直接做成对象(未来 Runtime Message 演进)
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';
@@ -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' && 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,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
- if (/^[a-z0-9][a-z0-9-]*\.(?:tsx?|[cm]?js)$/.test(value)) {
70
- return { localSegment: parse(value).name };
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.7",
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.6",
22
- "@zhin.js/plugin-runtime": "1.1.3"
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",