@aalis/plugin-authority 0.1.2 → 0.4.0

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/dist/index.js CHANGED
@@ -1,527 +1,9 @@
1
- import { Buffer } from 'node:buffer';
2
1
  import { useCommandService } from '@aalis/plugin-commands-api';
3
2
  import { getPlatformNames } from '@aalis/plugin-platform-api';
4
3
  import { createStorageGateway } from '@aalis/plugin-storage-api';
5
4
  import { useWebuiService } from '@aalis/plugin-webui-api';
6
- const BIND_CODE_TTL_MS = 5 * 60 * 1000;
7
- // 8 位、去易混淆字符(0O1IL)的码空间 31^8 ≈ 8.5e11,无需额外限流
8
- const BIND_CODE_ALPHABET = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
9
- function generateBindCode() {
10
- const bytes = crypto.getRandomValues(new Uint8Array(8));
11
- let code = '';
12
- for (const b of bytes)
13
- code += BIND_CODE_ALPHABET[b % BIND_CODE_ALPHABET.length];
14
- return code;
15
- }
16
- // 密码哈希:Web Crypto PBKDF2-SHA256(迭代数随凭据存储,便于将来上调不破坏旧凭据)
17
- const PBKDF2_ITERATIONS = 310_000;
18
- async function deriveHash(password, saltHex, iterations) {
19
- const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits']);
20
- const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: Buffer.from(saltHex, 'hex'), iterations }, key, 256);
21
- return Buffer.from(bits).toString('hex');
22
- }
23
- /** 恒定时间字符串比较(长度不同直接 false;不早退) */
24
- function timingSafeEqualStr(a, b) {
25
- if (a.length !== b.length)
26
- return false;
27
- let diff = 0;
28
- for (let i = 0; i < a.length; i++)
29
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
30
- return diff === 0;
31
- }
32
- export class AuthorityManager {
33
- users = new Map();
34
- /** 反向绑定索引:被绑平台身份键 → 主账户键(从 users[].links 重建) */
35
- linkIndex = new Map();
36
- /** 待消费绑定码(内存态) */
37
- bindCodes = new Map();
38
- config;
39
- logger;
40
- storage;
41
- fileUri;
42
- dirty = false;
43
- saveChain = Promise.resolve();
44
- confirmHandlers = new Map();
45
- dangerousGrants = new Map();
46
- grantSeq = 0;
47
- /**
48
- * dangerous 策略的开启时间(运行时状态,不序列化到 config)。
49
- * 进程重启后重置为 null,对限时策略而言等同于「重启即失效」。
50
- */
51
- dangerousEnabledAt = null;
52
- constructor(config, logger, storage) {
53
- this.config = config;
54
- this.logger = logger.child('authority');
55
- this.storage = storage;
56
- this.fileUri = 'data:/users.json';
57
- }
58
- getAuthority(platform, userId) {
59
- if (!userId)
60
- return this.config.get('defaultAuthority') ?? 1;
61
- if ((platform === 'webui' || platform === 'cli') && userId === 'console') {
62
- return this.config.get('ownerAuthority') ?? 5;
63
- }
64
- const owners = this.config.get('owners') ?? [];
65
- if (owners.some((o) => o.platform === platform && o.userId === userId)) {
66
- return this.config.get('ownerAuthority') ?? 5;
67
- }
68
- // 被绑身份解析到主账户(零合并单一真源)。递归至多一层:linkIndex 的
69
- // 键只会是外部平台身份(consumeBindCode 拒绝 webui/cli),值是 webui 账户。
70
- const linked = this.linkIndex.get(`${platform}:${userId}`);
71
- if (linked) {
72
- const idx = linked.indexOf(':');
73
- return this.getAuthority(linked.slice(0, idx), linked.slice(idx + 1));
74
- }
75
- return this.users.get(`${platform}:${userId}`)?.level ?? this.config.get('defaultAuthority') ?? 1;
76
- }
77
- setAuthority(platform, userId, level) {
78
- const key = `${platform}:${userId}`;
79
- this.users.set(key, { ...this.users.get(key), level });
80
- this.dirty = true;
81
- this.logger.debug(`设置用户权限: ${key} → ${level}`);
82
- }
83
- removeUser(platform, userId) {
84
- if (this.users.delete(`${platform}:${userId}`)) {
85
- this.dirty = true;
86
- this.rebuildLinkIndex();
87
- this.logger.debug(`删除用户权限记录: ${platform}:${userId}`);
88
- }
89
- }
90
- // ── 跨平台身份绑定 ──────────────────────────────────────
91
- /** 从 users[].links 重建反向索引(表很小,全量重建即可) */
92
- rebuildLinkIndex() {
93
- this.linkIndex.clear();
94
- for (const [key, record] of this.users) {
95
- for (const linked of record.links ?? [])
96
- this.linkIndex.set(linked, key);
97
- }
98
- }
99
- createBindCode(platform, userId) {
100
- if (platform !== 'webui')
101
- throw new Error('绑定码只能由 WebUI 主账户发起');
102
- const account = `${platform}:${userId}`;
103
- // 同账户重新生成作废旧码;顺手清理过期码
104
- const now = Date.now();
105
- for (const [code, pending] of this.bindCodes) {
106
- if (pending.account === account || pending.expiresAt <= now)
107
- this.bindCodes.delete(code);
108
- }
109
- const code = generateBindCode();
110
- const expiresAt = now + BIND_CODE_TTL_MS;
111
- this.bindCodes.set(code, { account, expiresAt });
112
- this.logger.info(`生成绑定码: 账户 ${account}(5 分钟有效)`);
113
- return { code, expiresAt };
114
- }
115
- consumeBindCode(code, identity) {
116
- if (identity.platform === 'webui' || identity.platform === 'cli') {
117
- throw new Error('请在外部平台(如 QQ)私聊中向机器人发送绑定码');
118
- }
119
- const pending = this.bindCodes.get(code);
120
- if (!pending || pending.expiresAt <= Date.now()) {
121
- this.bindCodes.delete(code);
122
- throw new Error('绑定码无效或已过期,请在 WebUI 重新生成');
123
- }
124
- const identityKey = `${identity.platform}:${identity.userId}`;
125
- const existing = this.linkIndex.get(identityKey);
126
- if (existing)
127
- throw new Error(`该平台身份已绑定到 ${existing},请先解绑`);
128
- this.bindCodes.delete(code); // 一次性
129
- const accountRecord = { ...this.users.get(pending.account) };
130
- // 绑时一次性合并(运行时零合并的前提):等级取 max、grants/denies 并集
131
- // 写入账户;平台身份原记录原样留底,解绑即还原。
132
- const identityRecord = this.users.get(identityKey);
133
- if (identityRecord) {
134
- if (identityRecord.level !== undefined && identityRecord.level > (accountRecord.level ?? 0)) {
135
- accountRecord.level = identityRecord.level;
136
- }
137
- const union = (a, b) => {
138
- const merged = [...new Set([...(a ?? []), ...(b ?? [])])];
139
- return merged.length > 0 ? merged : undefined;
140
- };
141
- accountRecord.grants = union(accountRecord.grants, identityRecord.grants);
142
- accountRecord.denies = union(accountRecord.denies, identityRecord.denies);
143
- }
144
- accountRecord.links = [...new Set([...(accountRecord.links ?? []), identityKey])];
145
- this.users.set(pending.account, accountRecord);
146
- this.dirty = true;
147
- this.rebuildLinkIndex();
148
- const idx = pending.account.indexOf(':');
149
- this.logger.info(`身份绑定成功: ${identityKey} → ${pending.account}`);
150
- return { platform: pending.account.slice(0, idx), userId: pending.account.slice(idx + 1) };
151
- }
152
- unlinkIdentity(platform, userId) {
153
- const identityKey = `${platform}:${userId}`;
154
- const accountKey = this.linkIndex.get(identityKey);
155
- if (!accountKey)
156
- return false;
157
- const record = this.users.get(accountKey);
158
- if (record?.links) {
159
- const links = record.links.filter(k => k !== identityKey);
160
- this.users.set(accountKey, { ...record, links: links.length > 0 ? links : undefined });
161
- }
162
- this.dirty = true;
163
- this.rebuildLinkIndex();
164
- this.logger.info(`身份解绑: ${identityKey} ↮ ${accountKey}`);
165
- return true;
166
- }
167
- async setPassword(platform, userId, password) {
168
- if (!password)
169
- throw new Error('密码不能为空');
170
- const key = `${platform}:${userId}`;
171
- const salt = Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('hex');
172
- const hash = await deriveHash(password, salt, PBKDF2_ITERATIONS);
173
- this.users.set(key, { ...this.users.get(key), secret: `pbkdf2:${PBKDF2_ITERATIONS}:${salt}:${hash}` });
174
- this.dirty = true;
175
- this.logger.info(`账户密码已更新: ${key}`);
176
- }
177
- async verifyPassword(platform, userId, password) {
178
- const secret = this.users.get(`${platform}:${userId}`)?.secret;
179
- if (!secret || !password)
180
- return false;
181
- const [scheme, iterStr, salt, hash] = secret.split(':');
182
- const iterations = Number(iterStr);
183
- if (scheme !== 'pbkdf2' || !Number.isFinite(iterations) || iterations < 1 || !salt || !hash)
184
- return false;
185
- const actual = await deriveHash(password, salt, iterations);
186
- return timingSafeEqualStr(actual, hash);
187
- }
188
- hasPassword(platform, userId) {
189
- return !!this.users.get(`${platform}:${userId}`)?.secret;
190
- }
191
- setUserCapabilities(platform, userId, overrides) {
192
- const key = `${platform}:${userId}`;
193
- const normalize = (list) => {
194
- const cleaned = [...new Set((list ?? []).map(p => p.trim()).filter(Boolean))];
195
- return cleaned.length > 0 ? cleaned : undefined;
196
- };
197
- const next = {
198
- ...this.users.get(key),
199
- grants: normalize(overrides.grants),
200
- denies: normalize(overrides.denies),
201
- };
202
- if (next.level === undefined && !next.grants && !next.denies && !next.secret && !next.links) {
203
- this.users.delete(key);
204
- }
205
- else {
206
- this.users.set(key, next);
207
- }
208
- this.dirty = true;
209
- this.logger.debug(`设置用户 capability 覆盖: ${key} grants=${next.grants?.length ?? 0} denies=${next.denies?.length ?? 0}`);
210
- }
211
- /**
212
- * capability 中心统一闸。裁决优先级(per-capability):
213
- * 全局 permissionPolicy > 用户 deny > 用户 grant > 角色链等级门槛。
214
- *
215
- * 等级门槛 = max(declaredAuthority, requiredAuthorityFor([cap]))——
216
- * 即"操作声明的基础等级"与"capability 归属角色包"取较高者,只升不降。
217
- */
218
- authorize(identity, request) {
219
- const level = this.getAuthority(identity.platform, identity.userId);
220
- const declared = request.declaredAuthority ?? 0;
221
- if (request.capabilities.length === 0) {
222
- if (level < declared)
223
- return `权限不足: 需要权限等级 ${declared},当前用户等级 ${level}`;
224
- return null;
225
- }
226
- const policyDenied = this.checkPermissionPolicy(request.capabilities);
227
- if (policyDenied)
228
- return policyDenied;
229
- // 被绑身份零合并解析:grants 以主账户为唯一真源;denies 取自身∪账户并集
230
- // (自身记录的 deny 在绑定后仍生效——防"绑定洗白封禁")。
231
- const ownKey = identity.userId ? `${identity.platform}:${identity.userId}` : undefined;
232
- const ownRecord = ownKey ? this.users.get(ownKey) : undefined;
233
- const accountKey = ownKey ? this.linkIndex.get(ownKey) : undefined;
234
- const accountRecord = accountKey ? this.users.get(accountKey) : undefined;
235
- const grants = accountKey ? accountRecord?.grants : ownRecord?.grants;
236
- const denies = [...(ownRecord?.denies ?? []), ...(accountRecord?.denies ?? [])];
237
- for (const cap of request.capabilities) {
238
- if (denies.length > 0 && this.matchAny(denies, [cap])) {
239
- return `已被禁止: ${cap}`;
240
- }
241
- if (grants && this.matchAny(grants, [cap]))
242
- continue;
243
- const required = Math.max(declared, this.requiredAuthorityFor([cap]));
244
- if (level < required) {
245
- return `权限不足: "${cap}" 需要权限等级 ${required},当前用户等级 ${level}`;
246
- }
247
- }
248
- return null;
249
- }
250
- isDangerousAllowed(name, permissions = []) {
251
- const policy = this.config.get('dangerousPolicy');
252
- if (!policy?.allow || policy.allow.length === 0)
253
- return false;
254
- // 有限时策略时检查过期;未记录 enabledAt 视为未启用(重启后自动失效)
255
- if (policy.duration && policy.duration > 0) {
256
- if (!this.dangerousEnabledAt)
257
- return false;
258
- const elapsed = (Date.now() - this.dangerousEnabledAt) / 1000;
259
- if (elapsed > policy.duration) {
260
- this.logger.info('dangerous 白名单已过期');
261
- return false;
262
- }
263
- }
264
- return this.matchAny(policy.allow, [name, ...permissions]);
265
- }
266
- /** 刷新 dangerous 策略启动时间戳(运行时状态) */
267
- markDangerousEnabled() {
268
- this.dangerousEnabledAt = Date.now();
269
- }
270
- /** 清除 dangerous 策略启动时间戳 */
271
- clearDangerousEnabled() {
272
- this.dangerousEnabledAt = null;
273
- }
274
- setConfirmHandler(platform, handler) {
275
- this.confirmHandlers.set(platform, handler);
276
- }
277
- async confirmDangerous(request) {
278
- if (this.isDangerousAllowed(request.name, request.permissions))
279
- return true;
280
- const grant = this.consumeDangerousGrant(request);
281
- if (grant) {
282
- this.logger.info(`命中高危会话授权: ${request.type}:${request.name} session=${request.sessionId} grant=${grant.id} used=${grant.used}${grant.maxUses ? `/${grant.maxUses}` : ''}`);
283
- return true;
284
- }
285
- const handler = this.confirmHandlers.get(request.platform);
286
- if (handler) {
287
- try {
288
- const result = await handler(request);
289
- const normalized = this.normalizeConfirmResult(result);
290
- if (normalized.allowed && normalized.grant?.scope === 'session') {
291
- this.createDangerousGrant(request, normalized);
292
- }
293
- return normalized.allowed;
294
- }
295
- catch (err) {
296
- this.logger.warn(`高危确认回调异常: ${err}`);
297
- return false;
298
- }
299
- }
300
- return false;
301
- }
302
- /**
303
- * 计算一组细粒度权限所要求的最低权限等级(参数级动态提权)。
304
- *
305
- * 例如 file_write 写普通文件只需声明的 authority:3,但写 data:/users.json
306
- * (用户权限表)或 data:/scheduler-jobs.json(计划任务,可注入 owner 身份的
307
- * actor)这类敏感文件、或写 aalis:/ 源码根(重启后即任意代码执行)时要求
308
- * owner 等级,防止低权限用户借文件写入自我提权。
309
- *
310
- * 默认保护清单可被 config.permissionAuthority 覆盖/扩展(同模式取配置值,
311
- * 新模式叠加;命中多个模式时取最大要求)。只提高门槛,不降低声明值。
312
- */
313
- /** 参数级提权完整清单(内置保护 + config.permissionAuthority 合并后;展示与裁决共用同一真源) */
314
- getEscalationMap() {
315
- const ownerLevel = this.config.get('ownerAuthority') ?? 5;
316
- return {
317
- 'storage:path:data:/users.json:write': ownerLevel,
318
- 'storage:path:data:/users.json:delete': ownerLevel,
319
- 'storage:path:data:/scheduler-jobs.json:write': ownerLevel,
320
- 'storage:path:data:/scheduler-jobs.json:delete': ownerLevel,
321
- 'storage:aalis:write': ownerLevel,
322
- 'storage:aalis:delete': ownerLevel,
323
- ...(this.config.get('permissionAuthority') ?? {}),
324
- };
325
- }
326
- requiredAuthorityFor(permissions) {
327
- if (permissions.length === 0)
328
- return 0;
329
- let required = 0;
330
- for (const [pattern, level] of Object.entries(this.getEscalationMap())) {
331
- if (level > required && this.matchAny([pattern], permissions))
332
- required = level;
333
- }
334
- return required;
335
- }
336
- checkPermissionPolicy(permissions) {
337
- const policy = this.config.get('permissionPolicy');
338
- if (!policy)
339
- return null;
340
- const deny = policy.deny ?? [];
341
- if (deny.length > 0 && this.matchAny(deny, permissions)) {
342
- return `权限策略拒绝: ${permissions.join(', ')}`;
343
- }
344
- const allow = policy.allow ?? [];
345
- if (allow.length > 0 && !this.matchAny(allow, permissions)) {
346
- return `权限策略未允许: ${permissions.join(', ')}`;
347
- }
348
- return null;
349
- }
350
- matchAny(patterns, values) {
351
- return patterns.some(pattern => values.some(value => this.matchPattern(pattern, value)));
352
- }
353
- matchPattern(pattern, value) {
354
- if (pattern === '*' || pattern === value)
355
- return true;
356
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*');
357
- return new RegExp(`^${escaped}$`).test(value);
358
- }
359
- listDangerousGrants() {
360
- this.pruneDangerousGrants();
361
- return [...this.dangerousGrants.values()].map(grant => ({ ...grant }));
362
- }
363
- revokeDangerousGrant(id) {
364
- const ok = this.dangerousGrants.delete(id);
365
- if (ok)
366
- this.logger.info(`已撤销高危会话授权: ${id}`);
367
- return ok;
368
- }
369
- normalizeConfirmResult(result) {
370
- return typeof result === 'boolean' ? { allowed: result } : result;
371
- }
372
- consumeDangerousGrant(request) {
373
- this.pruneDangerousGrants();
374
- for (const grant of this.dangerousGrants.values()) {
375
- if (grant.type !== request.type)
376
- continue;
377
- if (grant.name !== request.name)
378
- continue;
379
- if (grant.sessionId !== request.sessionId)
380
- continue;
381
- if (grant.platform !== request.platform)
382
- continue;
383
- if (grant.userId && request.userId && grant.userId !== request.userId)
384
- continue;
385
- if (!this.samePermissions(grant.permissions, request.permissions))
386
- continue;
387
- grant.used++;
388
- if (grant.maxUses && grant.used >= grant.maxUses) {
389
- this.dangerousGrants.delete(grant.id);
390
- }
391
- return grant;
392
- }
393
- return undefined;
394
- }
395
- createDangerousGrant(request, result) {
396
- const grantRequest = result.grant;
397
- if (!grantRequest || grantRequest.scope !== 'session')
398
- return;
399
- // 创建新授权时顺带清扫过期/用尽的(对称 bindCodes 的发码即清扫——审计 MEDIUM #11)
400
- this.pruneDangerousGrants();
401
- const durationSeconds = Math.max(1, Math.min(grantRequest.durationSeconds ?? 600, 3600));
402
- const grant = {
403
- id: `grant_${Date.now()}_${++this.grantSeq}`,
404
- name: request.name,
405
- type: request.type,
406
- permissions: request.permissions,
407
- sessionId: request.sessionId,
408
- platform: request.platform,
409
- userId: request.userId,
410
- expiresAt: Date.now() + durationSeconds * 1000,
411
- maxUses: grantRequest.maxUses,
412
- used: 0,
413
- createdAt: Date.now(),
414
- };
415
- this.dangerousGrants.set(grant.id, grant);
416
- this.logger.info(`创建高危会话授权: ${request.type}:${request.name} session=${request.sessionId} duration=${durationSeconds}s maxUses=${grant.maxUses ?? 'unlimited'} grant=${grant.id}`);
417
- }
418
- pruneDangerousGrants() {
419
- const now = Date.now();
420
- for (const [id, grant] of this.dangerousGrants) {
421
- if (grant.expiresAt <= now || (grant.maxUses && grant.used >= grant.maxUses)) {
422
- this.dangerousGrants.delete(id);
423
- this.logger.debug(`高危会话授权已过期: ${id}`);
424
- }
425
- }
426
- }
427
- samePermissions(a, b) {
428
- const left = [...new Set(a ?? [])].sort();
429
- const right = [...new Set(b ?? [])].sort();
430
- if (left.length !== right.length)
431
- return false;
432
- return left.every((value, index) => value === right[index]);
433
- }
434
- isOwner(platform, userId) {
435
- if (!userId)
436
- return false;
437
- if ((platform === 'webui' || platform === 'cli') && userId === 'console')
438
- return true;
439
- const owners = this.config.get('owners') ?? [];
440
- return owners.some((o) => o.platform === platform && o.userId === userId);
441
- }
442
- listUsers() {
443
- const result = [];
444
- const defaultLevel = this.config.get('defaultAuthority') ?? 1;
445
- for (const [key, record] of this.users) {
446
- const idx = key.indexOf(':');
447
- const platform = key.slice(0, idx);
448
- const userId = key.slice(idx + 1);
449
- const linkedTo = this.linkIndex.get(key);
450
- result.push({
451
- platform,
452
- userId,
453
- // 被绑身份显示运行时有效等级(解析到主账户);自身记录被遮蔽留底
454
- authority: linkedTo ? this.getAuthority(platform, userId) : (record.level ?? defaultLevel),
455
- grants: record.grants,
456
- denies: record.denies,
457
- hasPassword: record.secret ? true : undefined,
458
- links: record.links,
459
- linkedTo,
460
- });
461
- }
462
- // 无自身记录的被绑身份也要可见(绑定关系本身就是一条用户事实)
463
- for (const [identityKey, accountKey] of this.linkIndex) {
464
- if (this.users.has(identityKey))
465
- continue;
466
- const idx = identityKey.indexOf(':');
467
- const platform = identityKey.slice(0, idx);
468
- const userId = identityKey.slice(idx + 1);
469
- result.push({ platform, userId, authority: this.getAuthority(platform, userId), linkedTo: accountKey });
470
- }
471
- return result;
472
- }
473
- save() {
474
- if (!this.dirty)
475
- return;
476
- const users = {};
477
- for (const [key, record] of this.users)
478
- users[key] = record;
479
- const payload = JSON.stringify({ version: 2, users }, null, 2);
480
- this.dirty = false;
481
- this.saveChain = this.saveChain
482
- .then(() => this.storage.writeFile(this.fileUri, payload))
483
- .then(() => {
484
- this.logger.debug('用户权限数据已保存');
485
- }, err => {
486
- this.logger.warn(`保存用户权限数据失败: ${err}`);
487
- this.dirty = true;
488
- });
489
- }
490
- async init() {
491
- try {
492
- let raw;
493
- try {
494
- raw = (await this.storage.readFile(this.fileUri, 'utf-8'));
495
- }
496
- catch {
497
- return;
498
- }
499
- const data = JSON.parse(raw);
500
- if (data.version === 2 && typeof data.users === 'object' && data.users !== null) {
501
- for (const [key, record] of Object.entries(data.users)) {
502
- if (record && typeof record === 'object')
503
- this.users.set(key, record);
504
- }
505
- }
506
- else {
507
- // v1 平面格式({"platform:userId": level}):就地迁移,下次 save 写 v2
508
- for (const [key, level] of Object.entries(data)) {
509
- if (typeof level === 'number')
510
- this.users.set(key, { level });
511
- }
512
- if (this.users.size > 0) {
513
- this.dirty = true;
514
- this.logger.info(`users.json v1 → v2 迁移:${this.users.size} 条记录`);
515
- }
516
- }
517
- this.rebuildLinkIndex();
518
- this.logger.debug(`加载了 ${this.users.size} 条用户权限记录(绑定 ${this.linkIndex.size} 条)`);
519
- }
520
- catch (err) {
521
- this.logger.warn(`加载用户权限数据失败: ${err}`);
522
- }
523
- }
524
- }
5
+ import { AuthorityManager } from './authority-manager.js';
6
+ export { AuthorityManager } from './authority-manager.js';
525
7
  // ===== 插件元数据 =====
526
8
  export const name = '@aalis/plugin-authority';
527
9
  export const displayName = '权限管理';
@@ -530,38 +12,32 @@ export const provides = ['authority'];
530
12
  export const inject = {
531
13
  optional: ['commands', 'tools'],
532
14
  };
15
+ // 权限管理页(自定义 renderer 在 webui-client)+ 委托关系图(声明式 graph 组件,
16
+ // 复用通用 cytoscape 渲染器):能力委托模型下"上层分发下层"天然是一张图,比扁平列表直观。
533
17
  const webuiPages = [
534
18
  { key: 'authority', label: '权限管理', icon: 'authority', order: 50, renderer: 'authority' },
535
19
  {
536
20
  key: 'authority-graph',
537
- label: '权限图',
538
- icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z"/><circle cx="12" cy="10" r="1.6"/><circle cx="8.5" cy="14.5" r="1.3"/><circle cx="15.5" cy="14.5" r="1.3"/><line x1="12" y1="11.5" x2="8.5" y2="13.3"/><line x1="12" y1="11.5" x2="15.5" y2="13.3"/></svg>',
21
+ label: '委托关系图',
22
+ icon: 'authority',
539
23
  order: 51,
540
24
  content: [
541
25
  {
542
26
  type: 'graph',
543
- label: '权限依赖图:用户角色链 capability / 指令 / 工具(点击节点查看详情)',
544
- source: 'getPermissionGraph',
545
- detailSource: 'getPermissionNode',
27
+ label: '委托关系图:owner 委托链 + 授予/拒绝能力 + 跨平台绑定(点节点看详情)',
28
+ source: 'getDelegationGraph',
29
+ detailSource: 'getDelegationNode',
546
30
  defaultMaxDepth: 2,
547
- defaultMaxBreadth: 30,
548
- refresh: 0,
549
- // 权限图自有图例(声明后组件不再用人物关系图的 person/event/entity 语义)
550
31
  nodeKinds: [
32
+ { kind: 'owner', label: 'Owner(*)', shape: 'diamond', color: '#fbbf24' },
551
33
  { kind: 'user', label: '用户', shape: 'circle', color: '#60a5fa' },
552
- { kind: 'role', label: '角色(等级)', shape: 'round-rect', color: '#f59e0b' },
553
- { kind: 'capability', label: 'capability', shape: 'diamond', color: '#34d399' },
554
- { kind: 'command', label: '指令', shape: 'round-rect', color: '#a855f7' },
555
- { kind: 'tool', label: '工具', shape: 'round-rect', color: '#06b6d4' },
34
+ { kind: 'cap', label: '能力', shape: 'round-rect', color: '#9ca3af' },
556
35
  ],
557
36
  edgeKinds: [
558
- { kind: 'inherit', label: '继承', color: '#f59e0b' },
559
- { kind: 'level', label: '等级归属', color: '#60a5fa' },
560
- { kind: 'bind', label: '绑定', color: '#f472b6', dashed: true },
561
- { kind: 'grant', label: '授予', color: '#34d399', dashed: true },
562
- { kind: 'deny', label: '拒绝', color: '#ef4444' },
563
- { kind: 'escalation', label: '提权要求', color: '#ef4444', dashed: true },
564
- { kind: 'belongs', label: '归入角色包', color: '#6b7280' },
37
+ { kind: 'delegate', label: '委托', color: '#34d399' },
38
+ { kind: 'grant', label: '授予', color: '#60a5fa' },
39
+ { kind: 'deny', label: '拒绝', color: '#ef4444', dashed: true },
40
+ { kind: 'bind', label: '绑定', color: '#a78bfa', dashed: true },
565
41
  ],
566
42
  },
567
43
  ],
@@ -569,7 +45,6 @@ const webuiPages = [
569
45
  ];
570
46
  // ===== 插件入口 =====
571
47
  export async function apply(ctx, _config) {
572
- // 注册 WebUI 页面
573
48
  const webui = useWebuiService(ctx);
574
49
  for (const page of webuiPages)
575
50
  webui.registerPage(page);
@@ -578,33 +53,32 @@ export async function apply(ctx, _config) {
578
53
  const authority = new AuthorityManager(ctx.config, ctx.logger, storage);
579
54
  await authority.init();
580
55
  ctx.provide('authority', authority);
581
- // ===== tools/commands 注入执行守卫 =====
582
- const guard = async (guardCtx) => {
583
- // ExecutionGuard tool/command surface 的适配器:等级门槛、参数级提权、
584
- // 全局策略与用户 grant/deny 全部收进 authorize 统一闸;dangerous 确认是
585
- // 交互流程(弹窗/会话授权),保留在适配器层。
586
- const capabilities = guardCtx.permissions?.length ? guardCtx.permissions : [`${guardCtx.type}:${guardCtx.name}`];
587
- const denied = authority.authorize({ platform: guardCtx.platform, userId: guardCtx.userId }, { capabilities, declaredAuthority: guardCtx.authority });
588
- if (denied)
56
+ // ===== 执行守卫:能力统一闸 + 受限能力的临时委托确认 =====
57
+ const guard = async (g) => {
58
+ const capability = `${g.type}:${g.name}`;
59
+ const overrides = (ctx.config.get('visibilityOverrides') ?? {});
60
+ const visibility = overrides[g.name] ?? g.visibility;
61
+ const identity = { platform: g.platform, userId: g.userId };
62
+ // authorize 永远先评估(含系统源)——防"桥接/系统调用"绕过能力检查提权
63
+ const denied = authority.authorize(identity, { capability, visibility, resourceCapabilities: g.permissions });
64
+ if (!denied)
65
+ return null;
66
+ // 受限被拒:系统/受信源无人确认,直接返回拒绝;否则走交互确认(白名单/会话授予/回调)
67
+ if (g.skipConfirm)
589
68
  return denied;
590
- if (guardCtx.safety === 'dangerous' && !guardCtx.skipSafetyCheck) {
591
- const confirmed = await authority.confirmDangerous({
592
- name: guardCtx.name,
593
- type: guardCtx.type,
594
- args: guardCtx.args,
595
- permissions: guardCtx.permissions,
596
- sessionId: guardCtx.sessionId,
597
- platform: guardCtx.platform,
598
- userId: guardCtx.userId,
599
- });
600
- if (!confirmed) {
601
- return `已取消执行${guardCtx.type === 'command' ? '指令' : '工具'} ${guardCtx.name}。`;
602
- }
603
- }
604
- return null;
69
+ const granted = await authority.requestAccess({
70
+ name: g.name,
71
+ type: g.type,
72
+ capability,
73
+ resourceCapabilities: g.permissions,
74
+ args: g.args,
75
+ sessionId: g.sessionId,
76
+ platform: g.platform,
77
+ userId: g.userId,
78
+ });
79
+ return granted ? null : denied;
605
80
  };
606
- // 注入到 commands / tools 服务。whenService 会在 provider 上线(含 bounce 后
607
- // 重新 provide)时各调一次,自动覆盖"authority 早于 provider"和"provider 重启"两种场景。
81
+ // 注入到 commands / toolswhenService provider 上线/重启时各调一次)
608
82
  ctx.whenService('commands', svc => {
609
83
  if (svc.setExecutionGuard) {
610
84
  svc.setExecutionGuard(guard);
@@ -616,106 +90,117 @@ export async function apply(ctx, _config) {
616
90
  svc.setExecutionGuard(guard);
617
91
  ctx.logger.debug('权限守卫已注入: tools');
618
92
  }
619
- const toolOvr = ctx.config.get('toolOverrides');
620
- if (toolOvr && svc.loadOverrides) {
621
- svc.loadOverrides(toolOvr);
622
- }
623
- });
624
- // ===== 应用停止时保存 =====
625
- ctx.on('app:stopping', () => {
626
- authority.save();
627
93
  });
94
+ ctx.on('app:stopping', () => authority.save());
628
95
  // ===== 权限指令 =====
629
- // /grant设置用户权限等级
630
- cmds
631
- .command('grant <target:string> <level:number>', '设置用户权限等级', { authority: 2 })
632
- .example('/grant onebot:12345 2')
633
- .action(async (argv, target, level) => {
634
- const t = target;
635
- const lvl = level;
636
- if (Number.isNaN(lvl) || lvl < 0)
637
- return '权限等级必须是非负整数。';
638
- const callerAuth = authority.getAuthority(argv.session.platform, argv.session.userId);
639
- if (lvl >= callerAuth)
640
- return `不能将权限设置为 >= 您自身的等级 (${callerAuth})。`;
641
- const sep = t.indexOf(':');
642
- if (sep < 1)
643
- return '目标格式: <platform:userId>,例如 onebot:12345';
644
- const platform = t.slice(0, sep);
645
- const userId = t.slice(sep + 1);
646
- authority.setAuthority(platform, userId, lvl);
647
- authority.save();
648
- return `已将 ${t} 的权限等级设置为 ${lvl}。`;
649
- });
650
- // /authority — 查看权限等级 + 个别授予/拒绝 + 绑定关系
651
- const describeIdentity = (platform, userId, self) => {
652
- const level = authority.getAuthority(platform, userId);
653
- const isOwner = authority.isOwner(platform, userId);
654
- const lines = [`${self ? '您' : `${platform}:${userId}`}的权限等级: ${level}${isOwner ? ' (owner)' : ''}`];
655
- const entry = userId ? authority.listUsers().find(u => u.platform === platform && u.userId === userId) : undefined;
656
- if (entry?.linkedTo)
657
- lines.push(`已绑定到主账户 ${entry.linkedTo}(权限以账户为准)`);
658
- if (entry?.links?.length)
659
- lines.push(`已绑定身份: ${entry.links.join(', ')}`);
660
- if (entry?.grants?.length)
661
- lines.push(`个别授予: ${entry.grants.join(', ')}`);
662
- if (entry?.denies?.length)
663
- lines.push(`个别拒绝: ${entry.denies.join(', ')}`);
664
- return lines.join('\n');
665
- };
666
- cmds.command('authority [target:string]', '查看自己或指定用户的权限等级与授予').action(async (argv, target) => {
96
+ // /authority [target] 查看自己或指定用户的能力
97
+ cmds.command('authority [target:string]', '查看自己或指定用户的能力授予').action(async (argv, target) => {
98
+ const describe = (platform, userId, self) => {
99
+ const isOwner = authority.isOwner(platform, userId);
100
+ const who = self ? '您' : `${platform}:${userId}`;
101
+ const lines = [`${who}${isOwner ? '(owner,拥有全部能力)' : ''}`];
102
+ const entry = userId
103
+ ? authority.listUsers().find(u => u.platform === platform && u.userId === userId)
104
+ : undefined;
105
+ if (entry?.linkedTo)
106
+ lines.push(`已绑定到主账户 ${entry.linkedTo}(能力以账户为准)`);
107
+ if (entry?.links?.length)
108
+ lines.push(`已绑定身份: ${entry.links.join(', ')}`);
109
+ if (entry?.grant?.length)
110
+ lines.push(`授予能力: ${entry.grant.join(', ')}`);
111
+ if (entry?.deny?.length)
112
+ lines.push(`禁用能力: ${entry.deny.join(', ')}`);
113
+ if (entry?.grantedBy)
114
+ lines.push(`委托自: ${entry.grantedBy}`);
115
+ if (!isOwner && !entry?.grant?.length)
116
+ lines.push('(默认拥有全部 public 能力)');
117
+ return lines.join('\n');
118
+ };
667
119
  const t = target;
668
120
  if (t) {
669
121
  const sep = t.indexOf(':');
670
122
  if (sep < 1)
671
123
  return '目标格式: <platform:userId>';
672
- return describeIdentity(t.slice(0, sep), t.slice(sep + 1), false);
124
+ return describe(t.slice(0, sep), t.slice(sep + 1), false);
673
125
  }
674
- return describeIdentity(argv.session.platform, argv.session.userId, true);
126
+ return describe(argv.session.platform, argv.session.userId, true);
675
127
  });
676
- // /bind 把当前平台账号绑定到 WebUI 主账户(码在 WebUI 权限页生成)。
677
- // 仅限私聊:群聊发码会把绑定码暴露给旁观者;公开信道(群聊)安全性弱,
678
- // 故直接限定私聊信道完成绑定握手。
128
+ // /grant <target> <capability> 委托一个能力(子集约束在 manager 内校验)
129
+ cmds
130
+ .command('grant <target:string> <capability:string>', '授予用户一个能力', { visibility: 'restricted' })
131
+ .example('/grant onebot:12345 tool:weather')
132
+ .action(async (argv, target, capability) => editCaps(argv, target, capability, 'grant'));
133
+ // /deny <target> <capability> — 禁用一个能力
679
134
  cmds
680
- .command('bind <code:string>', '将当前平台账号绑定到 WebUI 账户', { authority: 1 })
135
+ .command('deny <target:string> <capability:string>', '禁用用户一个能力', { visibility: 'restricted' })
136
+ .example('/deny onebot:12345 tool:shell.exec')
137
+ .action(async (argv, target, capability) => editCaps(argv, target, capability, 'deny'));
138
+ /** /grant、/deny 共用:往目标用户的 grant/deny 集追加一条能力(委托子集校验在 setUserCapabilities) */
139
+ function editCaps(argv, target, capability, field) {
140
+ const t = String(target);
141
+ const cap = String(capability).trim();
142
+ const sep = t.indexOf(':');
143
+ if (sep < 1)
144
+ return '目标格式: <platform:userId>';
145
+ if (!cap)
146
+ return '能力不能为空';
147
+ const granter = { platform: argv.session.platform, userId: argv.session.userId ?? '' };
148
+ const targetId = { platform: t.slice(0, sep), userId: t.slice(sep + 1) };
149
+ const cur = authority.listUsers().find(u => u.platform === targetId.platform && u.userId === targetId.userId);
150
+ const next = [...new Set([...(cur?.[field] ?? []), cap])];
151
+ try {
152
+ authority.setUserCapabilities(granter, targetId, {
153
+ grant: field === 'grant' ? next : cur?.grant,
154
+ deny: field === 'deny' ? next : cur?.deny,
155
+ });
156
+ authority.save();
157
+ return `已${field === 'grant' ? '授予' : '禁用'} ${t}: ${cap}`;
158
+ }
159
+ catch (err) {
160
+ return err instanceof Error ? err.message : String(err);
161
+ }
162
+ }
163
+ // /bind <code> — 把当前平台账号绑定到 WebUI 主账户(码在 WebUI 权限页生成)。
164
+ // 仅限私聊:群聊发码会暴露给旁观者。
165
+ cmds
166
+ .command('bind <code:string>', '将当前平台账号绑定到 WebUI 账户', { visibility: 'public' })
681
167
  .example('/bind AB12CD34')
682
168
  .action(async (argv, code) => {
683
169
  const { platform, userId, sessionType } = argv.session;
684
170
  if (!userId)
685
171
  return '无法识别您的身份,无法绑定。';
686
- if (platform === 'webui' || platform === 'cli') {
172
+ if (platform === 'webui' || platform === 'cli')
687
173
  return '请在外部平台(如 QQ)私聊中向机器人发送本指令。';
688
- }
689
- if (sessionType !== 'private') {
174
+ if (sessionType !== 'private')
690
175
  return '为防止绑定码泄露,请在私聊中使用本指令。';
691
- }
692
176
  try {
693
177
  const account = authority.consumeBindCode(String(code).trim().toUpperCase(), { platform, userId });
694
178
  authority.save();
695
- return `绑定成功:${platform}:${userId} ↔ ${account.platform}:${account.userId}。您现在以该账户的权限行事,可在 WebUI 权限页解绑。`;
179
+ return `绑定成功:${platform}:${userId} ↔ ${account.platform}:${account.userId}。可在 WebUI 权限页解绑。`;
696
180
  }
697
181
  catch (err) {
698
182
  return err instanceof Error ? err.message : String(err);
699
183
  }
700
184
  });
701
185
  }
702
- // ===== WebUI 操作处理器 =====
186
+ // ===== WebUI 操作处理器(最小新模型集;委托树/图 Phase 4 充实)=====
187
+ function asStringList(v, label) {
188
+ if (v === undefined || v === null)
189
+ return undefined;
190
+ if (!Array.isArray(v) || v.some(x => typeof x !== 'string'))
191
+ throw new Error(`${label} 必须是字符串数组`);
192
+ return v;
193
+ }
703
194
  export const actions = {
704
- /** 获取权限概览 */
195
+ /** 权限概览:用户能力委托 + owner + 操作可见性 + 临时委托 + 受限/禁用清单 */
705
196
  async getOverview(ctx) {
706
197
  const auth = ctx.getService('authority');
707
198
  const users = auth?.listUsers() ?? [];
708
199
  const owners = ctx.config.get('owners') ?? [];
709
- const overrides = ctx.getService('commands')?.getOverrides() ?? {};
710
- // 扁平化所有指令节点,按 dot 名顺序排列,便于 UI 表格渲染
711
- const cmdNodes = ctx.getService('commands')?.getAll() ?? [];
712
- const commandPrefix = ctx.getService('commands')?.prefix ?? '/';
713
- const cmdNames = new Set(cmdNodes.map(n => n.name));
200
+ const commandsSvc = ctx.getService('commands');
201
+ const commandPrefix = commandsSvc?.prefix ?? '/';
202
+ const cmdNodes = commandsSvc?.getAll() ?? [];
714
203
  const tools = ctx.getService('tools')?.getAll() ?? [];
715
- // 平台候选(WebUI 下拉用):身份系统的平台名 = 消息上的 platform 字段。
716
- // 取 adapter.platform(getPlatformNames)而非服务提供者 contextId——后者是
717
- // 插件实例名(如 @aalis/plugin-adapter-onebot),按它设的权限永远不会命中
718
- // 任何真实调用者。webui/cli 是内置 surface,无 adapter,显式列入。
719
204
  const platforms = Array.from(new Set([
720
205
  ...getPlatformNames(ctx),
721
206
  'webui',
@@ -727,398 +212,215 @@ export const actions = {
727
212
  users,
728
213
  owners,
729
214
  platforms,
730
- defaultAuthority: ctx.config.get('defaultAuthority') ?? 1,
731
- ownerAuthority: ctx.config.get('ownerAuthority') ?? 5,
732
- dangerousPolicy: ctx.config.get('dangerousPolicy') ?? {},
733
- permissionPolicy: ctx.config.get('permissionPolicy') ?? {},
734
- // 参数级动态提权清单(glob→等级;内置保护清单见 requiredAuthorityFor 文档)
735
- permissionAuthority: ctx.config.get('permissionAuthority') ?? {},
736
- dangerousGrants: auth?.listDangerousGrants() ?? [],
215
+ restrictedCapabilities: ctx.config.get('restrictedCapabilities') ?? [],
216
+ deniedCapabilities: ctx.config.get('deniedCapabilities') ?? [],
217
+ visibilityOverrides: ctx.config.get('visibilityOverrides') ?? {},
218
+ restrictedPolicy: ctx.config.get('restrictedPolicy') ?? {},
219
+ temporaryGrants: auth?.listTemporaryGrants() ?? [],
737
220
  commandPrefix,
738
- commands: cmdNodes.map(n => {
739
- const path = n.name.split('.');
740
- const depth = path.length - 1;
741
- const hasSubcommands = cmdNodes.some(other => other.name.startsWith(`${n.name}.`));
742
- return {
743
- // key 同时是 override 的查找键与 setCommandOverride 的入参;如 'profile.clear.nuke'
744
- key: n.name,
745
- // 兼容旧前端:以 name 作 React key
746
- name: n.name,
747
- // 用于显示,如 '/profile clear nuke'
748
- displayName: `${commandPrefix}${path.join(' ')}`,
749
- // 叶子段名('nuke')用于子行紧凑显示
750
- leafName: path[path.length - 1],
751
- path,
752
- depth,
753
- isRoot: depth === 0,
754
- hasSubcommands,
755
- hasAction: !!n.handler,
756
- description: n.description,
757
- authority: n.authority,
758
- safety: n.safety,
759
- permissions: n.permissions,
760
- baseAuthority: n.baseAuthority,
761
- baseSafety: n.baseSafety,
762
- basePermissions: n.basePermissions,
763
- overridden: n.overridden,
764
- pluginName: n.pluginName,
765
- };
766
- }),
767
- commandOverrides: overrides,
768
- orphanCommandOverrides: Object.keys(overrides).filter(k => !cmdNames.has(k)),
769
- tools,
770
- toolOverrides: ctx.getService('tools')?.getOverrides?.() ?? {},
221
+ commands: cmdNodes.map(n => ({
222
+ key: n.name,
223
+ name: n.name,
224
+ displayName: `${commandPrefix}${n.name.split('.').join(' ')}`,
225
+ visibility: n.visibility ?? 'public',
226
+ })),
227
+ tools: tools.map(t => ({ key: t.name, name: t.name, visibility: t.visibility ?? 'public' })),
771
228
  };
772
229
  },
773
- /** 设置用户权限等级 */
774
- async setUser(ctx, args, caller) {
775
- const { platform, userId, authority } = args;
776
- if (!platform || !userId || typeof authority !== 'number') {
777
- throw new Error('platform, userId, authority(number) 必填');
778
- }
779
- if (authority < 0)
780
- throw new Error('权限等级必须 >= 0');
781
- const auth = ctx.getService('authority');
782
- // 与 /grant 指令同语义的防越权检查:不能把任何人设到 >= 自身等级
783
- // (caller 为登录账户的真实身份;单 token 模式为 webui:console=owner)。
784
- if (caller && auth) {
785
- const callerLevel = auth.getAuthority(caller.platform, caller.userId);
786
- if (authority >= callerLevel) {
787
- throw new Error(`不能将权限设置为 >= 您自身的等级 (${callerLevel})`);
788
- }
789
- }
790
- auth?.setAuthority(platform, userId, authority);
791
- auth?.save();
792
- return { message: `${platform}:${userId} 权限已设为 ${authority}` };
793
- },
794
- /** 设置用户的 capability 个别授予/拒绝(deny > grant > 角色链) */
795
- async setUserCapabilities(ctx, args, caller) {
796
- const { platform, userId, grants, denies } = args;
797
- if (!platform || !userId)
798
- throw new Error('platform, userId 必填');
799
- const asList = (v, label) => {
800
- if (v === undefined || v === null)
801
- return undefined;
802
- if (!Array.isArray(v) || v.some(x => typeof x !== 'string'))
803
- throw new Error(`${label} 必须是字符串数组`);
804
- return v;
805
- };
806
- const auth = ctx.getService('authority');
807
- if (!auth)
808
- throw new Error('Authority 服务不可用');
809
- // 防越权:不能改动等级 >= 自身的用户(与 setUser 同思路;改授予=改实际权力)
810
- if (caller) {
811
- const callerLevel = auth.getAuthority(caller.platform, caller.userId);
812
- const targetLevel = auth.getAuthority(platform, userId);
813
- const isSelf = caller.platform === platform && caller.userId === userId;
814
- if (!isSelf && targetLevel >= callerLevel) {
815
- throw new Error(`不能修改等级 >= 您自身 (${callerLevel}) 的用户的 capability 授予`);
816
- }
817
- }
818
- auth.setUserCapabilities(platform, userId, {
819
- grants: asList(grants, 'grants'),
820
- denies: asList(denies, 'denies'),
821
- });
822
- auth.save();
823
- return { message: `${platform}:${userId} 的 capability 授予已更新` };
824
- },
825
- /** 设置/重置账户密码(webui 登录凭据;仅本人或更高等级者可操作) */
826
- async setPassword(ctx, args, caller) {
827
- const { platform, userId, password } = args;
828
- if (!platform || !userId || typeof password !== 'string')
829
- throw new Error('platform, userId, password 必填');
830
- if (password.length < 6)
831
- throw new Error('密码长度至少 6 位');
832
- const auth = ctx.getService('authority');
833
- if (!auth)
834
- throw new Error('Authority 服务不可用');
835
- if (caller) {
836
- const callerLevel = auth.getAuthority(caller.platform, caller.userId);
837
- const targetLevel = auth.getAuthority(platform, userId);
838
- const isSelf = caller.platform === platform && caller.userId === userId;
839
- if (!isSelf && targetLevel >= callerLevel) {
840
- throw new Error(`不能为等级 >= 您自身 (${callerLevel}) 的用户设置密码`);
841
- }
842
- }
843
- await auth.setPassword(platform, userId, password);
844
- auth.save();
845
- return { message: `${platform}:${userId} 密码已更新` };
846
- },
847
- /** 权限依赖图(graph 组件数据源):用户 → 角色链 ← capability / 指令 / 工具 */
848
- async getPermissionGraph(ctx) {
230
+ /**
231
+ * 委托关系图数据(喂通用 cytoscape graph 组件,协议与 user-relation getRelationGraph 对齐):
232
+ * 用户节点(owner/user)+ 能力节点,边 = 委托(父→子) / 授予 / 拒绝 / 绑定(被绑身份→主账户)。
233
+ * 保证每条边两端节点都存在;支持焦点子图导航(args.focusId 为节点或边 id + maxDepth/maxBreadth),
234
+ * 点边时回 focusEdge(详情卡片用)。无 focusId 返回全图。
235
+ */
236
+ async getDelegationGraph(ctx, args) {
849
237
  const auth = ctx.getService('authority');
850
- if (!auth)
851
- throw new Error('Authority 服务不可用');
852
- const ownerLevel = ctx.config.get('ownerAuthority') ?? 5;
853
- const clamp = (n) => Math.max(0, Math.min(ownerLevel, Math.round(n)));
854
- const nodes = [];
238
+ const users = auth?.listUsers() ?? [];
239
+ const owners = ctx.config.get('owners') ?? [];
240
+ const nodes = new Map();
855
241
  const edges = [];
856
- // 角色链(kind=role:圆角矩形;尺寸随等级增大)
857
- for (let n = 0; n <= ownerLevel; n++) {
858
- nodes.push({
859
- data: {
860
- id: `role:${n}`,
861
- label: n === ownerLevel ? `owner (${n})` : `等级 ${n}`,
862
- kind: 'role',
863
- pageRankScale: 0.35 + (0.65 * n) / ownerLevel,
864
- },
865
- });
866
- if (n > 0) {
867
- edges.push({
868
- data: {
869
- id: `inherit:${n}`,
870
- source: `role:${n}`,
871
- target: `role:${n - 1}`,
872
- label: '继承',
873
- kind: 'inherit',
874
- directed: true,
875
- },
876
- });
877
- }
878
- }
879
- // capability 节点(kind=capability:菱形),按模式去重
880
- const capIds = new Set();
881
- const capNode = (pattern) => {
882
- if (!capIds.has(pattern)) {
883
- capIds.add(pattern);
884
- nodes.push({ data: { id: `cap:${pattern}`, label: pattern, kind: 'capability', pageRankScale: 0.3 } });
885
- }
886
- return `cap:${pattern}`;
242
+ const ensureUser = (key) => {
243
+ const id = `user:${key}`;
244
+ if (nodes.has(id))
245
+ return id;
246
+ const i = key.indexOf(':');
247
+ const isOwner = i > 0 ? (auth?.isOwner(key.slice(0, i), key.slice(i + 1)) ?? false) : false;
248
+ nodes.set(id, { data: { id, label: key, kind: isOwner ? 'owner' : 'user', pageRankScale: isOwner ? 0.7 : 0.5 } });
249
+ return id;
887
250
  };
888
- // 用户(users.json + owners 配置 + 单 token 模式的 console)
889
- const users = auth.listUsers();
890
- const owners = ctx.config.get('owners') ?? [];
891
- const userIds = new Set();
892
- const userNode = (platform, userId) => {
893
- const key = `${platform}:${userId}`;
894
- if (!userIds.has(key)) {
895
- userIds.add(key);
896
- nodes.push({ data: { id: `user:${key}`, label: key, kind: 'user', pageRankScale: 0.55 } });
897
- }
898
- return `user:${key}`;
251
+ const ensureCap = (pat) => {
252
+ const id = `cap:${pat}`;
253
+ if (!nodes.has(id))
254
+ nodes.set(id, { data: { id, label: pat, kind: 'cap', pageRankScale: 0.3 } });
255
+ return id;
899
256
  };
257
+ for (const o of owners)
258
+ ensureUser(`${o.platform}:${o.userId}`);
900
259
  for (const u of users) {
901
- const id = userNode(u.platform, u.userId);
902
- if (u.linkedTo) {
903
- // 被绑身份:权限随主账户走,只画绑定边(等级边由账户承担)
904
- const idx = u.linkedTo.indexOf(':');
260
+ const key = `${u.platform}:${u.userId}`;
261
+ const src = ensureUser(key);
262
+ for (const g of u.grant ?? [])
905
263
  edges.push({
906
- data: {
907
- id: `bind:${u.platform}:${u.userId}`,
908
- source: id,
909
- target: userNode(u.linkedTo.slice(0, idx), u.linkedTo.slice(idx + 1)),
910
- label: '绑定',
911
- kind: 'bind',
912
- directed: true,
913
- },
264
+ data: { id: `grant:${key}:${g}`, source: src, target: ensureCap(g), label: '授予', kind: 'grant' },
914
265
  });
915
- }
916
- else {
266
+ for (const d of u.deny ?? [])
917
267
  edges.push({
918
- data: {
919
- id: `lvl:${u.platform}:${u.userId}`,
920
- source: id,
921
- target: `role:${clamp(u.authority)}`,
922
- label: '等级',
923
- kind: 'level',
924
- directed: true,
925
- },
268
+ data: { id: `deny:${key}:${d}`, source: src, target: ensureCap(d), label: '拒绝', kind: 'deny' },
926
269
  });
927
- }
928
- for (const g of u.grants ?? []) {
270
+ if (u.grantedBy)
929
271
  edges.push({
930
272
  data: {
931
- id: `grant:${u.platform}:${u.userId}:${g}`,
932
- source: id,
933
- target: capNode(g),
934
- label: '授予',
935
- kind: 'grant',
273
+ id: `delegate:${key}`,
274
+ source: ensureUser(u.grantedBy),
275
+ target: src,
276
+ label: '委托',
277
+ kind: 'delegate',
936
278
  directed: true,
937
279
  },
938
280
  });
939
- }
940
- for (const d of u.denies ?? []) {
281
+ if (u.linkedTo)
941
282
  edges.push({
942
283
  data: {
943
- id: `deny:${u.platform}:${u.userId}:${d}`,
944
- source: id,
945
- target: capNode(d),
946
- label: '拒绝',
947
- kind: 'deny',
284
+ id: `bind:${key}`,
285
+ source: src,
286
+ target: ensureUser(u.linkedTo),
287
+ label: '绑定',
288
+ kind: 'bind',
948
289
  directed: true,
949
290
  },
950
291
  });
951
- }
952
292
  }
953
- for (const o of owners) {
954
- edges.push({
955
- data: {
956
- id: `owner:${o.platform}:${o.userId}`,
957
- source: userNode(o.platform, o.userId),
958
- target: `role:${ownerLevel}`,
959
- label: 'owner',
960
- kind: 'level',
961
- directed: true,
962
- },
963
- });
964
- }
965
- edges.push({
966
- data: {
967
- id: 'console-owner',
968
- source: userNode('webui', 'console'),
969
- target: `role:${ownerLevel}`,
970
- label: '单 token/本地',
971
- kind: 'level',
972
- directed: true,
973
- },
974
- });
975
- // 参数级提权清单(内置保护 + 配置,与裁决同源)
976
- for (const [pattern, level] of Object.entries(auth.getEscalationMap())) {
977
- edges.push({
978
- data: {
979
- id: `esc:${pattern}`,
980
- source: capNode(pattern),
981
- target: `role:${clamp(level)}`,
982
- label: '需等级',
983
- kind: 'escalation',
984
- directed: true,
985
- },
986
- });
987
- }
988
- // 指令与工具(仅根指令控制规模)
989
- const cmds = (ctx.getService('commands')?.getAll() ?? []).filter(c => !c.name.includes('.'));
990
- for (const c of cmds) {
991
- nodes.push({
992
- data: {
993
- id: `cmd:${c.name}`,
994
- label: `/${c.name}${c.safety === 'dangerous' ? ' ⚠' : ''}`,
995
- kind: 'command',
996
- pageRankScale: 0.12,
997
- },
998
- });
999
- edges.push({
1000
- data: {
1001
- id: `cmd-lvl:${c.name}`,
1002
- source: `cmd:${c.name}`,
1003
- target: `role:${clamp(c.authority ?? 1)}`,
1004
- label: '归入',
1005
- kind: 'belongs',
1006
- directed: true,
1007
- },
1008
- });
293
+ // owner 「* 全部能力」:owner 持有 `*`,不逐条 grant,否则会是孤立节点。连一个
294
+ // `*` 能力节点直观表达"拥有一切",也让焦点/邻域有内容可展开。
295
+ const ownerIds = [...nodes.values()].filter(n => n.data.kind === 'owner').map(n => String(n.data.id));
296
+ if (ownerIds.length > 0) {
297
+ const allCap = 'cap:*';
298
+ if (!nodes.has(allCap))
299
+ nodes.set(allCap, { data: { id: allCap, label: '★ 全部能力 (*)', kind: 'cap', pageRankScale: 0.6 } });
300
+ for (const id of ownerIds)
301
+ edges.push({
302
+ data: { id: `own:${id}`, source: id, target: allCap, label: '拥有全部', kind: 'grant', directed: true },
303
+ });
1009
304
  }
1010
- const toolDefs = ctx.getService('tools')?.getAll() ?? [];
1011
- for (const t of toolDefs) {
1012
- nodes.push({
1013
- data: {
1014
- id: `tool:${t.name}`,
1015
- label: `${t.name}${t.safety === 'dangerous' ? ' ⚠' : ''}`,
1016
- kind: 'tool',
1017
- pageRankScale: 0.12,
1018
- },
1019
- });
1020
- edges.push({
1021
- data: {
1022
- id: `tool-lvl:${t.name}`,
1023
- source: `tool:${t.name}`,
1024
- target: `role:${clamp(t.authority ?? 1)}`,
1025
- label: '归入',
1026
- kind: 'belongs',
1027
- directed: true,
1028
- },
1029
- });
305
+ const stats = {
306
+ 用户: users.length,
307
+ owner: owners.length,
308
+ 能力节点: [...nodes.keys()].filter(k => k.startsWith('cap:')).length,
309
+ };
310
+ // 焦点子图导航:无 focusId 全图;有则从焦点(节点或边两端)BFS maxDepth/maxBreadth。
311
+ const focusId = typeof args?.focusId === 'string' && args.focusId.trim() ? args.focusId.trim() : undefined;
312
+ if (!focusId)
313
+ return { nodes: [...nodes.values()], edges, stats };
314
+ const maxDepth = Number.isFinite(Number(args?.maxDepth)) ? Number(args?.maxDepth) : 2;
315
+ const maxBreadth = Number.isFinite(Number(args?.maxBreadth)) ? Number(args?.maxBreadth) : 10;
316
+ const edgeMatch = edges.find(e => e.data.id === focusId);
317
+ const starts = edgeMatch
318
+ ? [String(edgeMatch.data.source), String(edgeMatch.data.target)]
319
+ : nodes.has(focusId)
320
+ ? [focusId]
321
+ : [];
322
+ const focusEdge = edgeMatch
323
+ ? {
324
+ id: String(edgeMatch.data.id),
325
+ kind: String(edgeMatch.data.kind ?? ''),
326
+ description: String(edgeMatch.data.label ?? ''),
327
+ endpoints: [String(edgeMatch.data.source), String(edgeMatch.data.target)],
328
+ directed: edgeMatch.data.directed === true,
329
+ }
330
+ : undefined;
331
+ // 无向邻接:节点 id → [{edgeId, other}]
332
+ const adj = new Map();
333
+ for (const e of edges) {
334
+ const s = String(e.data.source);
335
+ const t = String(e.data.target);
336
+ const id = String(e.data.id);
337
+ (adj.get(s) ?? adj.set(s, []).get(s))?.push({ edgeId: id, other: t });
338
+ (adj.get(t) ?? adj.set(t, []).get(t))?.push({ edgeId: id, other: s });
339
+ }
340
+ const keptNodes = new Set(starts.filter(id => nodes.has(id)));
341
+ const keptEdges = new Set(edgeMatch ? [focusId] : []);
342
+ let frontier = [...keptNodes];
343
+ for (let d = 0; d < maxDepth && frontier.length > 0; d++) {
344
+ const next = [];
345
+ for (const id of frontier) {
346
+ for (const { edgeId, other } of (adj.get(id) ?? []).slice(0, maxBreadth)) {
347
+ keptEdges.add(edgeId);
348
+ if (!keptNodes.has(other)) {
349
+ keptNodes.add(other);
350
+ next.push(other);
351
+ }
352
+ }
353
+ }
354
+ frontier = next;
1030
355
  }
1031
356
  return {
1032
- nodes,
1033
- edges,
1034
- stats: {
1035
- 用户: userIds.size,
1036
- 角色: ownerLevel + 1,
1037
- 指令根: cmds.length,
1038
- 工具: toolDefs.length,
1039
- capability: capIds.size,
1040
- },
357
+ focusId,
358
+ focusEdge,
359
+ nodes: [...nodes.values()].filter(n => keptNodes.has(String(n.data.id))),
360
+ edges: edges.filter(e => keptEdges.has(String(e.data.id)) &&
361
+ keptNodes.has(String(e.data.source)) &&
362
+ keptNodes.has(String(e.data.target))),
363
+ stats,
1041
364
  };
1042
365
  },
1043
- /** 权限图节点详情(graph 组件 detailSource */
1044
- async getPermissionNode(ctx, args) {
366
+ /** 委托关系图节点详情(detailSource;点节点时调用) */
367
+ async getDelegationNode(ctx, args) {
1045
368
  const nodeId = String(args.nodeId ?? '');
1046
369
  const auth = ctx.getService('authority');
1047
- if (!auth)
1048
- throw new Error('Authority 服务不可用');
1049
- const ownerLevel = ctx.config.get('ownerAuthority') ?? 5;
370
+ const users = auth?.listUsers() ?? [];
1050
371
  if (nodeId.startsWith('user:')) {
1051
372
  const key = nodeId.slice(5);
1052
- const idx = key.indexOf(':');
1053
- const platform = key.slice(0, idx);
1054
- const userId = key.slice(idx + 1);
1055
- const entry = auth.listUsers().find(u => u.platform === platform && u.userId === userId);
373
+ const i = key.indexOf(':');
374
+ const isOwner = i > 0 ? (auth?.isOwner(key.slice(0, i), key.slice(i + 1)) ?? false) : false;
375
+ const u = users.find(x => `${x.platform}:${x.userId}` === key);
1056
376
  return {
1057
377
  身份: key,
1058
- 有效等级: auth.getAuthority(platform, userId),
1059
- owner: auth.isOwner(platform, userId) || undefined,
1060
- 可登录账户: entry?.hasPassword || undefined,
1061
- 绑定到: entry?.linkedTo,
1062
- 已绑身份: entry?.links?.join(', '),
1063
- 个别授予: entry?.grants?.join(', '),
1064
- 个别拒绝: entry?.denies?.join(', '),
1065
- };
1066
- }
1067
- if (nodeId.startsWith('role:')) {
1068
- const n = Number(nodeId.slice(5));
1069
- const holders = auth.listUsers().filter(u => !u.linkedTo && u.authority === n).length;
1070
- return {
1071
- 角色: n === ownerLevel ? `owner(等级 ${n})` : `等级 ${n}`,
1072
- 语义: '内置角色链:高等级继承低等级的全部授予;capability 图为唯一裁决',
1073
- 显式持有用户数: holders,
378
+ 类型: isOwner ? 'owner(拥有一切能力)' : '用户',
379
+ 授予: u?.grant?.join('、') || '(无)',
380
+ 拒绝: u?.deny?.join('、') || '(无)',
381
+ 委托自: u?.grantedBy || '(顶层 / owner 直接)',
382
+ 可登录账户: u?.hasPassword ? '是' : '否',
383
+ 绑定: u?.links?.join('、') || (u?.linkedTo ? `→ ${u.linkedTo}` : '(无)'),
1074
384
  };
1075
385
  }
1076
386
  if (nodeId.startsWith('cap:')) {
1077
- const pattern = nodeId.slice(4);
1078
- const escalation = auth.getEscalationMap()[pattern];
1079
- return {
1080
- capability: pattern,
1081
- 提权要求: escalation !== undefined ? `等级 ${escalation}` : undefined,
1082
- 说明: 'glob 模式,按 PermissionId 匹配;裁决优先级 deny > grant > 角色链',
1083
- };
387
+ const pat = nodeId.slice(4);
388
+ const granters = users.filter(u => (u.grant ?? []).some(g => g === pat)).map(u => `${u.platform}:${u.userId}`);
389
+ const deniers = users.filter(u => (u.deny ?? []).some(d => d === pat)).map(u => `${u.platform}:${u.userId}`);
390
+ return { 能力: pat, 授予给: granters.join('、') || '(无)', 拒绝于: deniers.join('、') || '(无)' };
1084
391
  }
1085
- if (nodeId.startsWith('cmd:')) {
1086
- const cmdName = nodeId.slice(4);
1087
- const c = ctx
1088
- .getService('commands')
1089
- ?.getAll()
1090
- .find(x => x.name === cmdName);
1091
- if (!c)
1092
- return { error: `指令 ${cmdName} 不存在` };
1093
- return {
1094
- 指令: `/${cmdName}`,
1095
- 描述: c.description,
1096
- 所需等级: c.authority,
1097
- 安全等级: c.safety,
1098
- capability: c.permissions?.join(', '),
1099
- 来源插件: c.pluginName,
1100
- };
1101
- }
1102
- if (nodeId.startsWith('tool:')) {
1103
- const toolName = nodeId.slice(5);
1104
- const t = ctx
1105
- .getService('tools')
1106
- ?.getAll()
1107
- .find(x => x.name === toolName);
1108
- if (!t)
1109
- return { error: `工具 ${toolName} 不存在` };
1110
- return {
1111
- 工具: toolName,
1112
- 描述: t.description,
1113
- 所需等级: t.authority ?? 1,
1114
- 安全等级: t.safety ?? 'safe',
1115
- capability: t.permissions?.join(', '),
1116
- 来源插件: t.pluginName,
1117
- };
392
+ return { id: nodeId };
393
+ },
394
+ /** 委托:设置用户能力 grant/deny(caller 为授予方,非 owner 时子集校验在 manager 内) */
395
+ async setUserCapabilities(ctx, args, caller) {
396
+ const { platform, userId, grant, deny } = args;
397
+ if (!platform || !userId)
398
+ throw new Error('platform, userId 必填');
399
+ const auth = ctx.getService('authority');
400
+ if (!auth)
401
+ throw new Error('Authority 服务不可用');
402
+ auth.setUserCapabilities(caller ?? null, { platform: platform, userId: userId }, { grant: asStringList(grant, 'grant'), deny: asStringList(deny, 'deny') });
403
+ auth.save();
404
+ return { message: `${platform}:${userId} 的能力委托已更新` };
405
+ },
406
+ /** 设置/重置账户密码(owner 或本人) */
407
+ async setPassword(ctx, args, caller) {
408
+ const { platform, userId, password } = args;
409
+ if (!platform || !userId || typeof password !== 'string')
410
+ throw new Error('platform, userId, password 必填');
411
+ if (password.length < 6)
412
+ throw new Error('密码长度至少 6 位');
413
+ const auth = ctx.getService('authority');
414
+ if (!auth)
415
+ throw new Error('Authority 服务不可用');
416
+ const isSelf = caller && caller.platform === platform && caller.userId === userId;
417
+ if (caller && !isSelf && !auth.isOwner(caller.platform, caller.userId)) {
418
+ throw new Error('只有 owner 或本人可设置密码');
1118
419
  }
1119
- return { error: `未知节点: ${nodeId}` };
420
+ await auth.setPassword(platform, userId, password);
421
+ auth.save();
422
+ return { message: `${platform}:${userId} 密码已更新` };
1120
423
  },
1121
- /** 生成跨平台绑定码(绑定到调用者自己的账户;5 分钟内在外部平台私聊发 /bind <码>) */
1122
424
  async createBindCode(ctx, _args, caller) {
1123
425
  if (!caller)
1124
426
  throw new Error('无法识别调用者身份');
@@ -1127,13 +429,8 @@ export const actions = {
1127
429
  throw new Error('Authority 服务不可用');
1128
430
  const { code, expiresAt } = auth.createBindCode(caller.platform, caller.userId);
1129
431
  const prefix = ctx.getService('commands')?.prefix ?? '/';
1130
- return {
1131
- code,
1132
- expiresAt,
1133
- hint: `请在 5 分钟内,用要绑定的平台账号(如 QQ)私聊向机器人发送:${prefix}bind ${code}`,
1134
- };
432
+ return { code, expiresAt, hint: `请在 5 分钟内用要绑定的平台账号私聊机器人发送:${prefix}bind ${code}` };
1135
433
  },
1136
- /** 解绑平台身份(owner 或该绑定所属账户本人) */
1137
434
  async unlinkIdentity(ctx, args, caller) {
1138
435
  const { platform, userId } = args;
1139
436
  if (!platform || !userId)
@@ -1142,12 +439,9 @@ export const actions = {
1142
439
  if (!auth)
1143
440
  throw new Error('Authority 服务不可用');
1144
441
  if (caller) {
1145
- const identityKey = `${platform}:${userId}`;
1146
- const ownerLevel = ctx.config.get('ownerAuthority') ?? 5;
1147
- const callerLevel = auth.getAuthority(caller.platform, caller.userId);
1148
- const owningAccount = auth.listUsers().find(u => u.links?.includes(identityKey));
1149
- const isSelf = owningAccount && owningAccount.platform === caller.platform && owningAccount.userId === caller.userId;
1150
- if (!isSelf && callerLevel < ownerLevel) {
442
+ const owning = auth.listUsers().find(u => u.links?.includes(`${platform}:${userId}`));
443
+ const isSelf = owning && owning.platform === caller.platform && owning.userId === caller.userId;
444
+ if (!isSelf && !auth.isOwner(caller.platform, caller.userId)) {
1151
445
  throw new Error('只有绑定所属账户本人或 owner 可以解绑');
1152
446
  }
1153
447
  }
@@ -1155,7 +449,7 @@ export const actions = {
1155
449
  auth.save();
1156
450
  return { ok, message: ok ? `${platform}:${userId} 已解绑` : '该身份没有绑定记录' };
1157
451
  },
1158
- /** 删除用户权限记录(等级回退默认,grants/denies/密码一并清除) */
452
+ /** 删除用户记录 */
1159
453
  async deleteUser(ctx, args) {
1160
454
  const { platform, userId } = args;
1161
455
  if (!platform || !userId)
@@ -1163,7 +457,7 @@ export const actions = {
1163
457
  const auth = ctx.getService('authority');
1164
458
  auth?.removeUser(platform, userId);
1165
459
  auth?.save();
1166
- return { message: `${platform}:${userId} 权限已重置` };
460
+ return { message: `${platform}:${userId} 记录已删除` };
1167
461
  },
1168
462
  /** 更新 owner 列表 */
1169
463
  async setOwners(ctx, args) {
@@ -1177,132 +471,68 @@ export const actions = {
1177
471
  app.saveConfig();
1178
472
  return { message: 'Owner 列表已更新' };
1179
473
  },
1180
- /** 更新 dangerousPolicy */
1181
- async setDangerousPolicy(ctx, args) {
474
+ /** 更新受限能力的临时放行策略(restrictedPolicy) */
475
+ async setRestrictedPolicy(ctx, args) {
1182
476
  const policy = args.policy;
1183
477
  if (!policy || typeof policy !== 'object')
1184
478
  throw new Error('policy 必须是对象');
1185
479
  const app = ctx.getService('app');
1186
480
  if (!app)
1187
481
  throw new Error('App 不可用');
1188
- ctx.config.set('dangerousPolicy', policy);
482
+ ctx.config.set('restrictedPolicy', policy);
1189
483
  app.saveConfig();
1190
- // 启用限时策略时,标记运行时启动时间戳(不写入 config)
1191
484
  if (Array.isArray(policy.allow) && policy.allow.length > 0) {
1192
- const auth = ctx.getService('authority');
1193
- auth?.markDangerousEnabled?.();
485
+ ctx.getService('authority')?.markPolicyEnabled?.();
1194
486
  }
1195
- return { message: '高危策略已更新' };
487
+ return { message: '临时放行策略已更新' };
1196
488
  },
1197
- /** 撤销一个高危会话授权 */
1198
- async revokeDangerousGrant(ctx, args) {
489
+ /** 撤销一个临时能力委托 */
490
+ async revokeTemporaryGrant(ctx, args) {
1199
491
  const id = args.id;
1200
492
  if (!id)
1201
- throw new Error('id 必须是字符串');
493
+ throw new Error('id 必填');
1202
494
  const auth = ctx.getService('authority');
1203
495
  if (!auth)
1204
496
  throw new Error('Authority 服务不可用');
1205
- const ok = auth.revokeDangerousGrant(id);
1206
- return { ok, message: ok ? '授权已撤销' : '授权不存在或已过期' };
1207
- },
1208
- /** 更新全局权限配置(defaultAuthority, ownerAuthority) */
1209
- async setConfig(ctx, args) {
1210
- const { defaultAuthority, ownerAuthority } = args;
1211
- const app = ctx.getService('app');
1212
- if (!app)
1213
- throw new Error('App 不可用');
1214
- if (typeof defaultAuthority === 'number')
1215
- ctx.config.set('defaultAuthority', defaultAuthority);
1216
- if (typeof ownerAuthority === 'number')
1217
- ctx.config.set('ownerAuthority', ownerAuthority);
1218
- app.saveConfig();
1219
- return { message: '权限配置已更新' };
497
+ const ok = auth.revokeTemporaryGrant(id);
498
+ return { ok, message: ok ? '临时委托已撤销' : '不存在或已过期' };
1220
499
  },
1221
- /** 更新单条指令的权限覆盖 */
1222
- async setCommandOverride(ctx, args) {
1223
- const { name, authority, safety } = args;
500
+ /** owner 覆盖单条操作的可见性(public ↔ restricted),无需改插件声明 */
501
+ async setVisibilityOverride(ctx, args) {
502
+ const { name, visibility } = args;
1224
503
  if (!name || typeof name !== 'string')
1225
504
  throw new Error('name 必填');
1226
505
  const app = ctx.getService('app');
1227
506
  if (!app)
1228
507
  throw new Error('App 不可用');
1229
- const override = {};
1230
- if (typeof authority === 'number')
1231
- override.authority = authority;
1232
- if (typeof safety === 'string' && (safety === 'safe' || safety === 'dangerous'))
1233
- override.safety = safety;
1234
- if (Object.keys(override).length === 0) {
1235
- ctx.getService('commands')?.removeOverride(name);
1236
- }
1237
- else {
1238
- ctx.getService('commands')?.setOverride(name, override);
1239
- }
1240
- ctx.config.set('commandOverrides', ctx.getService('commands')?.getOverrides() ?? {});
508
+ const overrides = { ...(ctx.config.get('visibilityOverrides') ?? {}) };
509
+ if (visibility === 'public' || visibility === 'restricted')
510
+ overrides[name] = visibility;
511
+ else
512
+ delete overrides[name];
513
+ ctx.config.set('visibilityOverrides', overrides);
1241
514
  app.saveConfig();
1242
- return { message: `指令 ${name} 权限已更新` };
515
+ return { message: `操作 ${name} 可见性已更新` };
1243
516
  },
1244
- /** 重置指令覆盖 */
1245
- async resetCommandOverride(ctx, args) {
1246
- const { name } = args;
1247
- if (!name || typeof name !== 'string')
1248
- throw new Error('name 必填');
1249
- const app = ctx.getService('app');
1250
- if (!app)
1251
- throw new Error('App 不可用');
1252
- ctx.getService('commands')?.removeOverride(name);
1253
- ctx.config.set('commandOverrides', ctx.getService('commands')?.getOverrides() ?? {});
1254
- app.saveConfig();
1255
- return { message: `指令 ${name} 覆盖已重置` };
1256
- },
1257
- /** 更新单个工具的权限覆盖 */
1258
- async setToolOverride(ctx, args) {
1259
- const { name, authority, safety } = args;
1260
- if (!name || typeof name !== 'string')
1261
- throw new Error('name 必填');
1262
- const app = ctx.getService('app');
1263
- const tools = ctx.getService('tools');
1264
- if (!app)
1265
- throw new Error('App 不可用');
1266
- if (!tools?.setOverride)
1267
- throw new Error('ToolService 未支持 override');
1268
- const override = {};
1269
- if (typeof authority === 'number')
1270
- override.authority = authority;
1271
- if (typeof safety === 'string' && (safety === 'safe' || safety === 'dangerous'))
1272
- override.safety = safety;
1273
- if (Object.keys(override).length === 0) {
1274
- tools.removeOverride?.(name);
1275
- }
1276
- else {
1277
- tools.setOverride(name, override);
1278
- }
1279
- ctx.config.set('toolOverrides', tools.getOverrides?.() ?? {});
1280
- app.saveConfig();
1281
- return { message: `工具 ${name} 权限已更新` };
1282
- },
1283
- /** 重置工具覆盖 */
1284
- async resetToolOverride(ctx, args) {
1285
- const { name } = args;
1286
- if (!name || typeof name !== 'string')
1287
- throw new Error('name 必填');
517
+ /** 更新受限/禁用能力清单 */
518
+ async setConfig(ctx, args) {
1288
519
  const app = ctx.getService('app');
1289
- const tools = ctx.getService('tools');
1290
520
  if (!app)
1291
521
  throw new Error('App 不可用');
1292
- tools?.removeOverride?.(name);
1293
- ctx.config.set('toolOverrides', tools?.getOverrides?.() ?? {});
522
+ const restricted = asStringList(args.restrictedCapabilities, 'restrictedCapabilities');
523
+ const denied = asStringList(args.deniedCapabilities, 'deniedCapabilities');
524
+ if (restricted)
525
+ ctx.config.set('restrictedCapabilities', restricted);
526
+ if (denied)
527
+ ctx.config.set('deniedCapabilities', denied);
1294
528
  app.saveConfig();
1295
- return { message: `工具 ${name} 覆盖已重置` };
529
+ return { message: '权限配置已更新' };
1296
530
  },
1297
531
  };
1298
- // actions 权限标注:createBindCode / unlinkIdentity 对任何登录账户开放
1299
- // (绑码只能绑到调用者自己;解绑有 handler 内的本人/owner 业务检查);
1300
- // 权限图为管理读档(含用户表信息,与 REST 管理读同档=4)。
1301
- // 其余 action 不声明 → 默认要求 owner(默认拒绝)。
532
+ // createBindCode / unlinkIdentity 对任何登录账户开放(绑码只能绑自己;解绑有 handler 内本人/owner 检查);
533
+ // 其余 action 不声明 → 默认 restricted(仅 owner / 被委托)。
1302
534
  export const actionsMeta = {
1303
- createBindCode: { authority: 1 },
1304
- unlinkIdentity: { authority: 1 },
1305
- getPermissionGraph: { authority: 4 },
1306
- getPermissionNode: { authority: 4 },
535
+ createBindCode: { visibility: 'public' },
536
+ unlinkIdentity: { visibility: 'public' },
1307
537
  };
1308
538
  //# sourceMappingURL=index.js.map