@zhin.js/command 1.1.17 → 1.2.1
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 +3 -3
- package/lib/command-index.d.ts +10 -0
- package/lib/command-index.js +80 -26
- package/lib/definition.d.ts +47 -9
- package/lib/definition.js +54 -43
- package/lib/execution-runtime.d.ts +3 -1
- package/lib/execution-runtime.js +3 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/messages.d.ts +1 -1
- package/lib/messages.js +2 -1
- package/lib/permission-explanation.js +2 -3
- package/lib/permit.d.ts +8 -5
- package/lib/permit.js +8 -5
- package/package.json +6 -7
- package/src/arguments.ts +0 -308
- package/src/command-index.ts +0 -1063
- package/src/command-menu.ts +0 -116
- package/src/definition.ts +0 -816
- package/src/execution-policy.ts +0 -39
- package/src/execution-runtime.ts +0 -202
- package/src/index.ts +0 -31
- package/src/messages.ts +0 -63
- package/src/permission-explanation.ts +0 -61
- package/src/permit.ts +0 -28
- package/src/provider.ts +0 -320
package/src/execution-policy.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/** Optional per-capability budget, isolated by sender and the complete conversation identity. */
|
|
2
|
-
export interface CommandExecutionPolicy {
|
|
3
|
-
readonly cooldownMs?: number;
|
|
4
|
-
readonly maxConcurrent?: number;
|
|
5
|
-
/** One best-effort progress message while business logic is waiting; disabled by default. */
|
|
6
|
-
readonly progressAfterMs?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface CommandExecutionLimit {
|
|
10
|
-
readonly reason: 'cooldown' | 'busy' | 'capacity' | 'operation_unavailable' | 'identity_unavailable' | 'service_unavailable';
|
|
11
|
-
/** Exact remaining cooldown; a busy operation has no predictable completion time. */
|
|
12
|
-
readonly retryAfterMs?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class CommandExecutionLimitedError extends Error {
|
|
16
|
-
readonly limit: CommandExecutionLimit;
|
|
17
|
-
constructor(limit: CommandExecutionLimit) {
|
|
18
|
-
super(`Command execution limited: ${limit.reason}`);
|
|
19
|
-
this.name = 'CommandExecutionLimitedError';
|
|
20
|
-
this.limit = limit;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** @internal Validate at declaration and projection, without coercion or silent clamping. */
|
|
25
|
-
export function normalizeExecutionPolicy(value: unknown): Readonly<CommandExecutionPolicy> | undefined {
|
|
26
|
-
if (value === undefined) return undefined;
|
|
27
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Command execution must be an object');
|
|
28
|
-
const limits = { cooldownMs: 86_400_000, maxConcurrent: 1000, progressAfterMs: 3_600_000 };
|
|
29
|
-
const result: Record<string, number> = {};
|
|
30
|
-
for (const [key, item] of Object.entries(value)) {
|
|
31
|
-
if (!Object.hasOwn(limits, key) || !Number.isSafeInteger(item) || (item as number) <= 0
|
|
32
|
-
|| (item as number) > limits[key as keyof typeof limits]) {
|
|
33
|
-
throw new TypeError(`Command execution.${key} must be a positive bounded integer`);
|
|
34
|
-
}
|
|
35
|
-
result[key] = item as number;
|
|
36
|
-
}
|
|
37
|
-
if (Object.keys(result).length === 0) throw new TypeError('Command execution requires at least one policy');
|
|
38
|
-
return Object.freeze(result);
|
|
39
|
-
}
|
package/src/execution-runtime.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/** Internal composition contract. Not part of the command authoring facade. */
|
|
2
|
-
import { createToken, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
3
|
-
import type { CommandExecutionLimit, CommandExecutionPolicy } from './execution-policy.js';
|
|
4
|
-
|
|
5
|
-
interface ConversationIdentity {
|
|
6
|
-
readonly endpoint: { readonly adapter: string; readonly id: string };
|
|
7
|
-
readonly kind: string;
|
|
8
|
-
readonly id: string;
|
|
9
|
-
readonly parent?: { readonly kind: string; readonly id: string };
|
|
10
|
-
readonly threadId?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface AdmissionReader {
|
|
14
|
-
admit(source: unknown, capability: string, owner: PluginId, policy: CommandExecutionPolicy, snapshot: RuntimeSnapshot): CommandExecutionLimit | undefined;
|
|
15
|
-
/**
|
|
16
|
-
* Programmatic call with no inbound source (e.g. `CommandIndex.execute`). The policy is keyed by command only:
|
|
17
|
-
* there is no sender or conversation to budget, and no reply channel for progress feedback.
|
|
18
|
-
* On success the caller must invoke `release` once the execution settles.
|
|
19
|
-
*/
|
|
20
|
-
admitDirect(capability: string, policy: CommandExecutionPolicy): DirectAdmission;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type DirectAdmission = { readonly limit: CommandExecutionLimit; readonly release?: undefined } | { readonly limit?: undefined; readonly release: () => void };
|
|
24
|
-
|
|
25
|
-
/** @internal Admission-only Root resource; no bind or finish authority is published. */
|
|
26
|
-
export const commandExecutionAdmissionToken = createToken<AdmissionReader>('zhin.command.execution-admission');
|
|
27
|
-
|
|
28
|
-
/** @internal Held only by Core's actual leased operation. */
|
|
29
|
-
export interface CommandOperationController {
|
|
30
|
-
bind(view: object, owner: PluginId): void;
|
|
31
|
-
/** Core registers actual started delivery promises, never a caller-supplied completion callback. */
|
|
32
|
-
trackSend(send: Promise<unknown>): void;
|
|
33
|
-
/** Stop pending feedback and drain already-started feedback before the final reply. */
|
|
34
|
-
stopProgress(): Promise<void>;
|
|
35
|
-
/** Called only by the real outer operation's finally, after all final sends settle. */
|
|
36
|
-
finish(): Promise<void>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface Entry { active: number; cooldownUntil: number }
|
|
40
|
-
interface Operation {
|
|
41
|
-
snapshot?: RuntimeSnapshot;
|
|
42
|
-
key?: readonly string[];
|
|
43
|
-
active: boolean;
|
|
44
|
-
progressStopped: boolean;
|
|
45
|
-
admitted: boolean;
|
|
46
|
-
signal: AbortSignal;
|
|
47
|
-
reply?: (owner: PluginId, text: string) => Promise<unknown>;
|
|
48
|
-
readonly releases: (() => void)[];
|
|
49
|
-
readonly timers: Set<ReturnType<typeof setTimeout>>;
|
|
50
|
-
readonly feedback: Promise<void>[];
|
|
51
|
-
readonly accepted: Set<string>;
|
|
52
|
-
readonly sends: Set<Promise<void>>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** @internal Root-instance-owned state shared by projections and draining retired generations. */
|
|
56
|
-
export class CommandExecutionBudget {
|
|
57
|
-
readonly #entries = new Map<string, Entry>();
|
|
58
|
-
readonly #views = new WeakMap<object, { operation: Operation; owner: PluginId }>();
|
|
59
|
-
readonly #clock: () => number;
|
|
60
|
-
readonly #capacity: number;
|
|
61
|
-
readonly admission: AdmissionReader;
|
|
62
|
-
|
|
63
|
-
constructor(options: { readonly clock?: () => number; readonly capacity?: number } = {}) {
|
|
64
|
-
this.#clock = options.clock ?? (() => performance.now());
|
|
65
|
-
this.#capacity = options.capacity ?? 10_000;
|
|
66
|
-
if (!Number.isSafeInteger(this.#capacity) || this.#capacity < 1) throw new TypeError('Invalid command budget capacity');
|
|
67
|
-
this.admission = Object.freeze<AdmissionReader>({ admit: (source, capability, owner, policy, snapshot) => this.#admit(source, capability, owner, policy, snapshot),
|
|
68
|
-
admitDirect: (capability, policy) => this.#admitDirect(capability, policy) });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
openOperation(input: {
|
|
72
|
-
readonly snapshot: RuntimeSnapshot;
|
|
73
|
-
readonly conversation: ConversationIdentity;
|
|
74
|
-
readonly senderId?: string;
|
|
75
|
-
readonly retirementSignal: AbortSignal;
|
|
76
|
-
readonly reply: (owner: PluginId, text: string) => Promise<unknown>;
|
|
77
|
-
}): CommandOperationController {
|
|
78
|
-
const c = input.conversation;
|
|
79
|
-
const values = [c.endpoint.adapter, c.endpoint.id, c.kind, c.id, c.parent?.kind ?? '', c.parent?.id ?? '', c.threadId ?? '', input.senderId ?? ''];
|
|
80
|
-
const valid = values.slice(0, 4).every(nonempty) && ['private', 'group', 'channel'].includes(c.kind) && nonempty(input.senderId)
|
|
81
|
-
&& (c.parent === undefined || (nonempty(c.parent.kind) && nonempty(c.parent.id)))
|
|
82
|
-
&& (c.threadId === undefined || nonempty(c.threadId)) && values.every(value => value.length <= 1024);
|
|
83
|
-
const operation: Operation = {
|
|
84
|
-
snapshot: input.snapshot, key: valid ? Object.freeze(values) : undefined,
|
|
85
|
-
active: true, progressStopped: false, admitted: false, signal: input.retirementSignal,
|
|
86
|
-
reply: input.reply, releases: [], timers: new Set(), feedback: [], accepted: new Set(), sends: new Set(),
|
|
87
|
-
};
|
|
88
|
-
const stop = () => {
|
|
89
|
-
operation.progressStopped = true;
|
|
90
|
-
for (const timer of operation.timers) clearTimeout(timer);
|
|
91
|
-
operation.timers.clear();
|
|
92
|
-
};
|
|
93
|
-
input.retirementSignal.addEventListener('abort', stop, { once: true });
|
|
94
|
-
return Object.freeze({
|
|
95
|
-
bind: (view: object, owner: PluginId) => { if (operation.active) this.#views.set(view, { operation, owner }); },
|
|
96
|
-
trackSend: (send: Promise<unknown>) => {
|
|
97
|
-
if (!operation.active) return;
|
|
98
|
-
const settled = send.then(() => { operation.sends.delete(settled); }, () => { operation.sends.delete(settled); });
|
|
99
|
-
operation.sends.add(settled);
|
|
100
|
-
},
|
|
101
|
-
stopProgress: async () => { stop(); await Promise.all(operation.feedback); },
|
|
102
|
-
finish: async () => {
|
|
103
|
-
stop();
|
|
104
|
-
await Promise.all(operation.feedback);
|
|
105
|
-
// New sends may start while an earlier send is draining. Do not release from an initial snapshot alone.
|
|
106
|
-
if (operation.admitted) while (operation.sends.size > 0) await Promise.all(operation.sends);
|
|
107
|
-
if (!operation.active) return;
|
|
108
|
-
operation.active = false;
|
|
109
|
-
operation.signal.removeEventListener('abort', stop);
|
|
110
|
-
for (const release of operation.releases) release();
|
|
111
|
-
operation.releases.length = 0;
|
|
112
|
-
operation.feedback.length = 0;
|
|
113
|
-
operation.accepted.clear();
|
|
114
|
-
operation.sends.clear();
|
|
115
|
-
operation.reply = undefined;
|
|
116
|
-
operation.snapshot = undefined;
|
|
117
|
-
operation.key = undefined;
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#admitDirect(capability: string, policy: CommandExecutionPolicy): DirectAdmission {
|
|
123
|
-
return this.#acquire(JSON.stringify([capability, 'direct']), policy);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Shared entry accounting: concurrency, cooldown and capacity. Returns a one-shot release on success. */
|
|
127
|
-
#acquire(key: string, policy: CommandExecutionPolicy): DirectAdmission {
|
|
128
|
-
const now = this.#clock();
|
|
129
|
-
let entry = this.#entries.get(key);
|
|
130
|
-
if (entry && policy.maxConcurrent !== undefined && entry.active >= policy.maxConcurrent) return { limit: limited('busy') };
|
|
131
|
-
if (entry && entry.cooldownUntil > now) return { limit: Object.freeze({ reason: 'cooldown', retryAfterMs: Math.ceil(entry.cooldownUntil - now) }) };
|
|
132
|
-
if (!entry) {
|
|
133
|
-
if (this.#entries.size >= this.#capacity) {
|
|
134
|
-
for (const [oldKey, old] of this.#entries) if (old.active === 0 && old.cooldownUntil <= now) this.#entries.delete(oldKey);
|
|
135
|
-
if (this.#entries.size >= this.#capacity) {
|
|
136
|
-
// Still full of cooling identities: drop the oldest idle one. Forgetting a cooldown is acceptable;
|
|
137
|
-
// refusing every new identity is not. Only entries with running operations are never evicted.
|
|
138
|
-
for (const [oldKey, old] of this.#entries) if (old.active === 0) { this.#entries.delete(oldKey); break; }
|
|
139
|
-
}
|
|
140
|
-
if (this.#entries.size >= this.#capacity) return { limit: limited('capacity') };
|
|
141
|
-
}
|
|
142
|
-
entry = { active: 0, cooldownUntil: 0 };
|
|
143
|
-
this.#entries.set(key, entry);
|
|
144
|
-
}
|
|
145
|
-
entry.active++;
|
|
146
|
-
entry.cooldownUntil = now + (policy.cooldownMs ?? 0);
|
|
147
|
-
const acceptedEntry = entry;
|
|
148
|
-
let released = false;
|
|
149
|
-
return {
|
|
150
|
-
release: () => {
|
|
151
|
-
if (released) return;
|
|
152
|
-
released = true;
|
|
153
|
-
acceptedEntry.active--;
|
|
154
|
-
if (acceptedEntry.active === 0 && acceptedEntry.cooldownUntil <= this.#clock()) this.#entries.delete(key);
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
#admit(source: unknown, capability: string, owner: PluginId, policy: CommandExecutionPolicy, snapshot: RuntimeSnapshot): CommandExecutionLimit | undefined {
|
|
160
|
-
const binding = source !== null && typeof source === 'object' ? this.#views.get(source) : undefined;
|
|
161
|
-
const operation = binding?.operation;
|
|
162
|
-
if (!operation?.active || operation.snapshot !== snapshot || binding?.owner !== owner || operation.signal.aborted) return limited('operation_unavailable');
|
|
163
|
-
if (!operation.key) return limited('identity_unavailable');
|
|
164
|
-
const key = JSON.stringify([capability, ...operation.key]);
|
|
165
|
-
// Re-entering the same capability is a second attempt, rather than a second free execution.
|
|
166
|
-
const acquired = this.#acquire(key, policy);
|
|
167
|
-
if (acquired.limit) return acquired.limit;
|
|
168
|
-
operation.admitted = true;
|
|
169
|
-
operation.releases.push(acquired.release);
|
|
170
|
-
if (policy.progressAfterMs !== undefined && !operation.progressStopped && !operation.accepted.has(capability)) {
|
|
171
|
-
const timer = setTimeout(() => {
|
|
172
|
-
operation.timers.delete(timer);
|
|
173
|
-
if (!operation.active || operation.progressStopped || operation.signal.aborted || !operation.reply) return;
|
|
174
|
-
// Observe failure once. Never retry uncertain delivery and never expose the exception.
|
|
175
|
-
let send: Promise<void>;
|
|
176
|
-
try { send = Promise.resolve(operation.reply(owner, '正在处理中,请稍候。')).then(() => {}, () => {}); }
|
|
177
|
-
catch { send = Promise.resolve(); }
|
|
178
|
-
operation.feedback.push(send);
|
|
179
|
-
}, policy.progressAfterMs);
|
|
180
|
-
operation.timers.add(timer);
|
|
181
|
-
}
|
|
182
|
-
operation.accepted.add(capability);
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function nonempty(value: unknown): value is string { return typeof value === 'string' && value.length > 0; }
|
|
188
|
-
function limited(reason: CommandExecutionLimit['reason']): CommandExecutionLimit { return Object.freeze({ reason }); }
|
|
189
|
-
|
|
190
|
-
/** @internal Projection reads the Root resource; ordinary authoring values cannot bind an operation. */
|
|
191
|
-
export function admitCommandExecution(source: unknown, capability: string, owner: PluginId, policy: CommandExecutionPolicy | undefined, snapshot: RuntimeSnapshot): CommandExecutionLimit | undefined {
|
|
192
|
-
if (!policy) return undefined;
|
|
193
|
-
const reader = snapshot.resources.get(snapshot.root)?.get(commandExecutionAdmissionToken.id) as AdmissionReader | undefined;
|
|
194
|
-
return reader ? reader.admit(source, capability, owner, policy, snapshot) : limited('service_unavailable');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** @internal Programmatic (source-less) admission: keyed by command only, never refused for lacking a source. */
|
|
198
|
-
export function admitDirectCommandExecution(capability: string, policy: CommandExecutionPolicy | undefined, snapshot: RuntimeSnapshot): DirectAdmission {
|
|
199
|
-
if (!policy) return { release: () => {} };
|
|
200
|
-
const reader = snapshot.resources.get(snapshot.root)?.get(commandExecutionAdmissionToken.id) as AdmissionReader | undefined;
|
|
201
|
-
return reader ? reader.admitDirect(capability, policy) : { limit: limited('service_unavailable') };
|
|
202
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command authoring contracts, parameter schemas, and permit helpers.
|
|
3
|
-
* @module @zhin.js/command
|
|
4
|
-
*/
|
|
5
|
-
/** @internal Runtime command projection. */
|
|
6
|
-
export * from './command-index.js';
|
|
7
|
-
/** @public Command authoring contract and `defineCommand`. */
|
|
8
|
-
export * from './definition.js';
|
|
9
|
-
export * from './permission-explanation.js';
|
|
10
|
-
export {
|
|
11
|
-
CONVERSATION_MANAGER_PERMIT,
|
|
12
|
-
canManageConversation,
|
|
13
|
-
assertBuiltinPermits,
|
|
14
|
-
checkBuiltinPermit,
|
|
15
|
-
checkBuiltinPermitList,
|
|
16
|
-
isBuiltinPermit,
|
|
17
|
-
isPlatformPermit,
|
|
18
|
-
parsePermitName,
|
|
19
|
-
type ParsedPermit,
|
|
20
|
-
type PermitKind,
|
|
21
|
-
} from './permit.js';
|
|
22
|
-
export {
|
|
23
|
-
bindCommandSource,
|
|
24
|
-
CommandPathSyntaxError,
|
|
25
|
-
commandFeatureId,
|
|
26
|
-
type BoundCommandSource,
|
|
27
|
-
default as commandFeature,
|
|
28
|
-
} from './provider.js';
|
|
29
|
-
export { default } from './provider.js';
|
|
30
|
-
|
|
31
|
-
export type { CommandValidationIssue, CommandValidationIssueCode } from './arguments.js';
|
package/src/messages.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { createMessages, type Locale } from '@zhin.js/plugin-runtime';
|
|
2
|
-
|
|
3
|
-
/** Translates only display metadata explicitly supplied by its author. */
|
|
4
|
-
export function commandDescription(
|
|
5
|
-
value: Readonly<{ description?: string; descriptionI18n?: Readonly<Partial<Record<Locale, string>>> }>,
|
|
6
|
-
locale: Locale,
|
|
7
|
-
): string | undefined {
|
|
8
|
-
return value.descriptionI18n?.[locale] ?? value.description;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Static dictionaries; the caller supplies the operation's language. */
|
|
12
|
-
export const commandMessages = createMessages({
|
|
13
|
-
menuKeyword: { 'zh-CN': '菜单', en: 'menu' },
|
|
14
|
-
invalidPage: { 'zh-CN': '页码必须是正整数。用法:{command} [插件名] --page <页码>', en: 'The page must be a positive integer. Usage: {command} [plugin] --page <page>' },
|
|
15
|
-
missingPlugin: { 'zh-CN': '未找到插件: {plugin}', en: 'Plugin not found: {plugin}' },
|
|
16
|
-
pageOutOfRange: { 'zh-CN': '页码超出范围:共 {pages} 页。请使用「{command} --page <页码>」。', en: 'Page out of range: {pages} pages available. Use {command} --page <page>.' },
|
|
17
|
-
menuTitle: { 'zh-CN': '=== {plugin} 指令菜单 ===', en: '=== {plugin} command menu ===' },
|
|
18
|
-
menuSummary: { 'zh-CN': '第 {page}/{pages} 页 · {commands} 条指令 · {plugins} 个子插件', en: 'Page {page}/{pages} · Commands: {commands} · Plugins: {plugins}' },
|
|
19
|
-
childPlugins: { 'zh-CN': '子插件:', en: 'Plugins:' },
|
|
20
|
-
emptyMenu: { 'zh-CN': '(暂无指令和子插件)', en: '(No commands or plugins)' },
|
|
21
|
-
pluginHint: { 'zh-CN': '提示:使用「{command} <插件名>」查看子插件的指令', en: 'Use {command} <plugin> to view a plugin’s commands.' },
|
|
22
|
-
previousPage: { 'zh-CN': '上一页:{command}', en: 'Previous page: {command}' },
|
|
23
|
-
nextPage: { 'zh-CN': '下一页:{command}', en: 'Next page: {command}' },
|
|
24
|
-
guideTitle: { 'zh-CN': '{title} · 指令指南', en: '{title} · Command guide' },
|
|
25
|
-
guideSubtitle: { 'zh-CN': '每一个灵感,都有回应。', en: 'An answer to every idea.' },
|
|
26
|
-
commandCount: { 'zh-CN': '{count} 条指令', en: 'Commands: {count}' },
|
|
27
|
-
pluginCount: { 'zh-CN': '{count} 个子插件', en: 'Plugins: {count}' },
|
|
28
|
-
commandIndex: { 'zh-CN': '指令索引', en: 'Commands' },
|
|
29
|
-
pluginIndex: { 'zh-CN': '插件导航', en: 'Plugins' },
|
|
30
|
-
guideEmpty: { 'zh-CN': '暂无指令和子插件', en: 'No commands or plugins' },
|
|
31
|
-
helpHint: { 'zh-CN': '查看参数与用法:{command} --help', en: 'View arguments and usage: {command} --help' },
|
|
32
|
-
genericHelpHint: { 'zh-CN': '在当前环境的指令后追加 --help 查看用法', en: 'Append --help to a command in this environment to view its usage' },
|
|
33
|
-
expectedType: { 'zh-CN': '{field}:应为 {type}', en: '{field}: expected {type}' },
|
|
34
|
-
expectedArray: { 'zh-CN': '{field}:应为 {type} 数组', en: '{field}: expected an array of {type}' },
|
|
35
|
-
minimum: { 'zh-CN': '{field}:最小值为 {minimum}', en: '{field}: minimum {minimum}' },
|
|
36
|
-
maximum: { 'zh-CN': '{field}:最大值为 {maximum}', en: '{field}: maximum {maximum}' },
|
|
37
|
-
choices: { 'zh-CN': '{field}:可选值为 {choices}', en: '{field}: expected one of {choices}' },
|
|
38
|
-
invalidFormat: { 'zh-CN': '{field}:格式不正确', en: '{field}: invalid format' },
|
|
39
|
-
invalidValue: { 'zh-CN': '{field}:值不正确', en: '{field}: invalid value' },
|
|
40
|
-
unknownOption: { 'zh-CN': '未知选项 {field}', en: 'Unknown option {field}' },
|
|
41
|
-
duplicateOption: { 'zh-CN': '重复选项 --{field}', en: 'Duplicate option --{field}' },
|
|
42
|
-
defaultUndefined: { 'zh-CN': '{field}:默认值返回了 undefined', en: '{field}: default returned undefined' },
|
|
43
|
-
parameterUndefined: { 'zh-CN': '{field}:默认值或快捷指令返回了 undefined', en: '{field}: default or shortcut returned undefined' },
|
|
44
|
-
missingOption: { 'zh-CN': '缺少选项 --{field}', en: 'Missing option --{field}' },
|
|
45
|
-
missingRequiredParam: { 'zh-CN': '缺少必填参数 {field}(类型:{type})', en: 'Missing required parameter {field} (expected {type})' },
|
|
46
|
-
unexpectedArguments: { 'zh-CN': '存在多余参数', en: 'Unexpected arguments' },
|
|
47
|
-
invalidParameter: { 'zh-CN': '缺少指令参数或参数不正确', en: 'Missing or invalid command parameter' },
|
|
48
|
-
defaultValue: { 'zh-CN': '默认值:{value}', en: 'default: {value}' },
|
|
49
|
-
dynamicValue: { 'zh-CN': '动态计算', en: 'dynamic' },
|
|
50
|
-
noAdapter: { 'zh-CN': '命令未声明 adapter 约束', en: 'The command does not declare an adapter restriction' },
|
|
51
|
-
missingEndpoint: { 'zh-CN': '未选择当前 Endpoint,无法判断命令的精确 adapter 约束', en: 'Select an Endpoint to evaluate the command’s exact adapter restriction' },
|
|
52
|
-
adapterComparison: { 'zh-CN': '命令 adapter 与当前 Endpoint 声明的原生适配器精确比较;未探测在线状态', en: 'Exact comparison with the Endpoint’s declared native adapter; online status was not checked' },
|
|
53
|
-
conditionMismatch: { 'zh-CN': '至少一个声明条件与假设输入不符;这不是实际准入检查', en: 'At least one declared condition does not match the hypothetical input; this is not a live admission check' },
|
|
54
|
-
missingPermissionHost: { 'zh-CN': 'PermissionHost 缺失或无法确认;不能推断命令可执行', en: 'PermissionHost is missing or unverified; command execution cannot be inferred' },
|
|
55
|
-
missingEvidence: { 'zh-CN': '存在缺少依据的条件,不能推断命令可执行', en: 'Some conditions lack evidence; command execution cannot be inferred' },
|
|
56
|
-
noPermit: { 'zh-CN': '命令未声明 permit 限制;这不表示已获得执行权限', en: 'The command does not declare permit restrictions; this does not establish authorization' },
|
|
57
|
-
uncheckedConditions: { 'zh-CN': '存在未执行的平台或自定义条件,不能推断命令可执行', en: 'Platform or custom conditions have not been evaluated; command execution cannot be inferred' },
|
|
58
|
-
conditionsMatch: { 'zh-CN': '已解释的 builtin 条件与假设输入匹配;不代表真实授权或命令可执行', en: 'The explained builtin conditions match the hypothetical input; this does not establish authorization or executability' },
|
|
59
|
-
unconstrained: { 'zh-CN': '已解释的 builtin 规则不检查具体 Endpoint、parent 或 thread;保留完整目标不等于这些维度受规则限制', en: 'The explained builtin rules do not constrain a specific Endpoint, parent or thread; retaining the full target does not add such restrictions' },
|
|
60
|
-
simulatedIdentity: { 'zh-CN': '身份与角色仅用于模拟,未向平台核实', en: 'Identity and roles are hypothetical and have not been verified with the platform' },
|
|
61
|
-
unevaluatedCode: { 'zh-CN': '未执行命令、动态默认值、平台或自定义 checker', en: 'Commands, dynamic defaults, platform checks and custom checkers have not been executed' },
|
|
62
|
-
defaultedRole: { 'zh-CN': 'canonical 输入未附角色时按命令运行时默认 user 解释;这不是已核验的平台角色', en: 'Canonical input without roles uses the command runtime’s default user role; this is not a verified platform role' },
|
|
63
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { operationClientAdapter } from '@zhin.js/feature-kit';
|
|
2
|
-
import { explainPermitRules, toPermissionSubject, type PermissionSubject, type PermissionConditionExplanation } from '@zhin.js/permission';
|
|
3
|
-
import { resolveCommandSender, resolveCommandSession, type CommandMessage } from './definition.js';
|
|
4
|
-
import type { CommandDescriptor } from './command-index.js';
|
|
5
|
-
import { resolveLocale, type Locale } from '@zhin.js/plugin-runtime';
|
|
6
|
-
import { commandMessages } from './messages.js';
|
|
7
|
-
|
|
8
|
-
/** The exact subject projection used by the real command permit gate. No factories or checkers run. */
|
|
9
|
-
export function toCommandPermissionSubject(source: unknown): PermissionSubject {
|
|
10
|
-
const projected = toPermissionSubject(source);
|
|
11
|
-
const session = resolveCommandSession(source);
|
|
12
|
-
return { ...projected, ...session, ...(session.sender ? { sender: { ...projected.sender, ...session.sender } } : {}) };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Exact authoring adapter restriction, distinct from builtin adapter(...) matching. */
|
|
16
|
-
export function commandAdapterMatches(command: Readonly<{ adapter?: string }>, source: unknown): boolean {
|
|
17
|
-
return !command.adapter || operationClientAdapter(source) === command.adapter;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type CommandPermissionHostStatus = 'present' | 'missing' | 'unverified';
|
|
21
|
-
|
|
22
|
-
/** Read-only hypothetical conditions. A match is not permission to execute the command. */
|
|
23
|
-
export function explainCommandPermissions(
|
|
24
|
-
command: Pick<CommandDescriptor, 'adapter' | 'permit'>,
|
|
25
|
-
options: Readonly<{ source?: unknown; rolesKnown?: boolean; permissionHost: CommandPermissionHostStatus; locale?: Locale }>,
|
|
26
|
-
) {
|
|
27
|
-
const locale = resolveLocale(options.locale);
|
|
28
|
-
const projected = toCommandPermissionSubject(options.source);
|
|
29
|
-
const inputSender = options.source && typeof options.source === 'object'
|
|
30
|
-
? (options.source as Pick<CommandMessage, 'sender'>).sender : undefined;
|
|
31
|
-
const structuredRoles = inputSender && typeof inputSender === 'object'
|
|
32
|
-
&& Array.isArray((inputSender as { readonly role?: unknown }).role);
|
|
33
|
-
// A sender-only assumption has no synthetic conversation. Apply the real canonical
|
|
34
|
-
// sender defaults without inventing an Endpoint or treating unknown roles as known.
|
|
35
|
-
const senderOnly = options.source && typeof options.source === 'object' && !('conversation' in options.source) && !structuredRoles
|
|
36
|
-
? resolveCommandSender(options.source as Pick<CommandMessage, 'sender'>, undefined) : undefined;
|
|
37
|
-
const subject = senderOnly ? { ...projected, sender: senderOnly } : projected;
|
|
38
|
-
const defaultedRoles = options.rolesKnown && Array.isArray(inputSender?.roles) && inputSender.roles.length === 0
|
|
39
|
-
&& subject.sender?.role.length === 1 && subject.sender.role[0] === 'user';
|
|
40
|
-
const actualAdapter = operationClientAdapter(options.source);
|
|
41
|
-
const adapter: PermissionConditionExplanation = !command.adapter
|
|
42
|
-
? { outcome: 'not_evaluated', reason: commandMessages(locale, 'noAdapter') }
|
|
43
|
-
: !actualAdapter ? { outcome: 'unknown', reason: commandMessages(locale, 'missingEndpoint') }
|
|
44
|
-
: { outcome: commandAdapterMatches(command, options.source) ? 'match' : 'mismatch', reason: commandMessages(locale, 'adapterComparison') };
|
|
45
|
-
const rules = explainPermitRules(command.permit ?? [], subject, { rolesKnown: options.rolesKnown, locale });
|
|
46
|
-
const conditions = [...(command.adapter ? [adapter] : []), ...rules];
|
|
47
|
-
let summary: PermissionConditionExplanation;
|
|
48
|
-
if (conditions.some(item => item.outcome === 'mismatch')) summary = { outcome: 'mismatch', reason: commandMessages(locale, 'conditionMismatch') };
|
|
49
|
-
else if ((rules.length > 0 && options.permissionHost !== 'present') || conditions.some(item => item.outcome === 'unknown')) {
|
|
50
|
-
summary = { outcome: 'unknown', reason: rules.length > 0 && options.permissionHost !== 'present'
|
|
51
|
-
? commandMessages(locale, 'missingPermissionHost') : commandMessages(locale, 'missingEvidence') };
|
|
52
|
-
} else if (rules.length === 0) summary = { outcome: 'not_evaluated', reason: commandMessages(locale, 'noPermit') };
|
|
53
|
-
else if (conditions.some(item => item.outcome === 'not_evaluated')) summary = { outcome: 'not_evaluated', reason: commandMessages(locale, 'uncheckedConditions') };
|
|
54
|
-
else summary = { outcome: 'match', reason: commandMessages(locale, 'conditionsMatch') };
|
|
55
|
-
return Object.freeze({ permissionHost: options.permissionHost, adapter: Object.freeze(adapter), rules, summary: Object.freeze(summary),
|
|
56
|
-
unconstrained: Object.freeze([commandMessages(locale, 'unconstrained')]),
|
|
57
|
-
assumptions: Object.freeze([commandMessages(locale, 'simulatedIdentity'), commandMessages(locale, 'unevaluatedCode'),
|
|
58
|
-
...(defaultedRoles
|
|
59
|
-
? [commandMessages(locale, 'defaultedRole')] : [])]),
|
|
60
|
-
});
|
|
61
|
-
}
|
package/src/permit.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Re-export permit 解析/校验 from @zhin.js/permission (SSOT)。
|
|
3
|
-
* 保留 CommandSession 兼容签名供 command-index 过渡使用。
|
|
4
|
-
*/
|
|
5
|
-
import { checkBuiltinPermit, type PermissionSubject } from '@zhin.js/permission';
|
|
6
|
-
|
|
7
|
-
/** 会话管理者:群主 / 群管理员 / 机器人主人(适配器投影的规范角色名)。 */
|
|
8
|
-
export const CONVERSATION_MANAGER_PERMIT = 'role(owner,admin,master)';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 「私聊,或 owner / admin / master」。`permit` 列表只支持 AND,
|
|
12
|
-
* 订阅类命令需要这个 OR 规则时用它,并以命令会话的 `scene` / `sender.role` 为输入。
|
|
13
|
-
*/
|
|
14
|
-
export function canManageConversation(subject: PermissionSubject): boolean {
|
|
15
|
-
return checkBuiltinPermit('private()', subject)
|
|
16
|
-
|| checkBuiltinPermit(CONVERSATION_MANAGER_PERMIT, subject);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
type PermitKind,
|
|
21
|
-
type ParsedPermit,
|
|
22
|
-
parsePermitName,
|
|
23
|
-
isBuiltinPermit,
|
|
24
|
-
isPlatformPermit,
|
|
25
|
-
assertPermitSyntax as assertBuiltinPermits,
|
|
26
|
-
checkBuiltinPermit,
|
|
27
|
-
checkBuiltinPermitList,
|
|
28
|
-
} from '@zhin.js/permission';
|