@aalis/plugin-authority 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -2,7 +2,9 @@ import { useCommandService } from '@aalis/plugin-commands-api';
2
2
  import { getPlatformNames } from '@aalis/plugin-platform-api';
3
3
  import { createStorageGateway } from '@aalis/plugin-storage-api';
4
4
  import { useWebuiService } from '@aalis/plugin-webui-api';
5
+ import { setNetworkPolicy } from '@aalis/util-network-guard';
5
6
  import { AuthorityManager } from './authority-manager.js';
7
+ import { autoConfirmActive, DEFAULT_AUTHORITY, shouldSkipConfirm } from './authority-model.js';
6
8
  export { AuthorityManager } from './authority-manager.js';
7
9
  // ===== 插件元数据 =====
8
10
  export const name = '@aalis/plugin-authority';
@@ -12,36 +14,9 @@ export const provides = ['authority'];
12
14
  export const inject = {
13
15
  optional: ['commands', 'tools'],
14
16
  };
15
- // 权限管理页(自定义 renderer 在 webui-client)+ 委托关系图(声明式 graph 组件,
16
- // 复用通用 cytoscape 渲染器):能力委托模型下"上层分发下层"天然是一张图,比扁平列表直观。
17
+ // 权限管理页(自定义 renderer 在 webui-client)。单 owner 终态无委托树,故无委托关系图。
17
18
  const webuiPages = [
18
19
  { key: 'authority', label: '权限管理', icon: 'authority', order: 50, renderer: 'authority' },
19
- {
20
- key: 'authority-graph',
21
- label: '委托关系图',
22
- icon: 'authority',
23
- order: 51,
24
- content: [
25
- {
26
- type: 'graph',
27
- label: '委托关系图:owner → 子 → 孙 委托链 + 授予/拒绝能力 + 跨平台绑定(点节点看详情)',
28
- source: 'getDelegationGraph',
29
- detailSource: 'getDelegationNode',
30
- defaultMaxDepth: 2,
31
- nodeKinds: [
32
- { kind: 'owner', label: 'Owner(*)', shape: 'diamond', color: '#fbbf24' },
33
- { kind: 'user', label: '用户', shape: 'circle', color: '#60a5fa' },
34
- { kind: 'cap', label: '能力', shape: 'round-rect', color: '#9ca3af' },
35
- ],
36
- edgeKinds: [
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 },
41
- ],
42
- },
43
- ],
44
- },
45
20
  ];
46
21
  // ===== 插件入口 =====
47
22
  export async function apply(ctx, _config) {
@@ -51,32 +26,67 @@ export async function apply(ctx, _config) {
51
26
  const cmds = useCommandService(ctx);
52
27
  const storage = createStorageGateway(ctx);
53
28
  const authority = new AuthorityManager(ctx.config, ctx.logger, storage);
54
- await authority.init();
55
29
  ctx.provide('authority', authority);
56
- // ===== 执行守卫:能力统一闸 + 受限能力的临时委托确认 =====
30
+ // 用户等级存于 data:/users.json,读取依赖 storage 服务。storage provider 可能晚于本插件
31
+ // 上线(曾因此在 init 阶段 readFile 失败被静默吞 → 重启后等级不回载);改为 storage 就绪时再
32
+ // load,规避初始化时序竞态。whenService 对「已在线」的服务也会立即触发,故任意加载序都成立。
33
+ ctx.whenService('storage', () => {
34
+ void authority.init().then(() => ctx.logger.debug('授权用户等级已加载'), err => ctx.logger.warn(`授权用户等级加载失败: ${err}`));
35
+ });
36
+ // 网络出口闸(SSRF):把 core 配置 network 注入进程级 safeFetch 策略(启动一次)。
37
+ // 安全归属在权限域;本地固定服务走裸 fetch、不过 safeFetch,故不受影响。
38
+ setNetworkPolicy(ctx.config.get('network') ?? {});
39
+ // ===== 执行守卫:两轴正交闸 —— 轴 A 授权(authorize)+ 轴 B 确认(confirm,owner 也吃)=====
57
40
  const guard = async (g) => {
58
41
  const capability = `${g.type}:${g.name}`;
59
- const overrides = (ctx.config.get('visibilityOverrides') ?? {});
60
- const visibility = overrides[g.name] ?? g.visibility;
42
+ // 确认覆盖:'off' 强制关确认;否则覆盖值优先,回退插件声明。
43
+ const confOv = (ctx.config.get('confirmOverrides') ?? {});
44
+ const cOv = confOv[capability];
45
+ const confirm = cOv === 'off' ? undefined : (cOv ?? g.confirm);
61
46
  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)
68
- return denied;
69
- const granted = await authority.requestAccess({
47
+ const accessBase = {
70
48
  name: g.name,
71
49
  type: g.type,
72
50
  capability,
73
- resourceCapabilities: g.permissions,
74
51
  args: g.args,
75
52
  sessionId: g.sessionId,
76
53
  platform: g.platform,
77
54
  userId: g.userId,
55
+ };
56
+ // ── 轴 A · 授权:数字等级裁决(minLevel 由 risk/visibility/authorityOverrides 在 manager 内派生)——系统源也评估,防绕过提权 ──
57
+ const denied = authority.authorize(identity, {
58
+ capability,
59
+ visibility: g.visibility,
60
+ risk: g.risk,
78
61
  });
79
- return granted ? null : denied;
62
+ if (denied) {
63
+ // 未授权(等级不够 / 硬禁 / 资源受限):**绝不**让发起者本人弹确认自我提权。
64
+ // 仅 owner 预先配置的放行(restrictedPolicy 白名单 / 该用户在本会话已有的授予)可救;否则硬拒。
65
+ // 注意:这里**不**调 requestAccess(那会询问发起者)——只查 isPreApproved(不问人)。
66
+ if (g.skipConfirm)
67
+ return denied;
68
+ return authority.isPreApproved(accessBase) ? null : denied;
69
+ }
70
+ // ── 轴 B · 确认:授权已过(含 owner / public / 已授予),但操作声明了 confirm 仍需「意图确认」 ──
71
+ // 仅对**已授权**操作做意图确认(owner 也吃,防注入借权);不再是提权入口。
72
+ // 跳过判定见 shouldSkipConfirm:always 永不跳(cron 无人确认即拒);非 always 可被
73
+ // skipConfirm(系统/受信源) 或 owner 本人 auto 模式 跳过。修 #6:旧实现用 `!g.skipConfirm`
74
+ // 门控整块 → skipConfirm 会连 always 一起绕过。
75
+ if (confirm) {
76
+ const skip = shouldSkipConfirm({
77
+ confirm,
78
+ skipConfirm: !!g.skipConfirm,
79
+ isOwner: authority.isOwner(g.platform, g.userId),
80
+ autoConfirmUntil: ctx.config.get('autoConfirmUntil') ?? 0,
81
+ now: Date.now(),
82
+ });
83
+ if (!skip) {
84
+ const ok = await authority.requestAccess({ ...accessBase, confirm });
85
+ if (!ok)
86
+ return `操作已取消:${capability} 需确认后执行`;
87
+ }
88
+ }
89
+ return null;
80
90
  };
81
91
  // 注入到 commands / tools(whenService 在 provider 上线/重启时各调一次)
82
92
  ctx.whenService('commands', svc => {
@@ -93,28 +103,17 @@ export async function apply(ctx, _config) {
93
103
  });
94
104
  ctx.on('app:stopping', () => authority.save());
95
105
  // ===== 权限指令 =====
96
- // /authority [target] — 查看自己或指定用户的能力
97
- cmds.command('authority [target:string]', '查看自己或指定用户的能力授予').action(async (argv, target) => {
106
+ // /authority [target] — 查看自己或指定用户的权限等级
107
+ cmds.command('authority [target:string]', '查看自己或指定用户的权限等级').action(async (argv, target) => {
98
108
  const describe = (platform, userId, self) => {
99
109
  const isOwner = authority.isOwner(platform, userId);
100
110
  const who = self ? '您' : `${platform}:${userId}`;
101
- const lines = [`${who}${isOwner ? '(owner,拥有全部能力)' : ''}`];
111
+ if (isOwner)
112
+ return `${who}(owner,等级 ∞,拥有全部权限)`;
102
113
  const entry = userId
103
114
  ? authority.listUsers().find(u => u.platform === platform && u.userId === userId)
104
115
  : 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');
116
+ return `${who} 等级: ${entry?.level ?? DEFAULT_AUTHORITY}`;
118
117
  };
119
118
  const t = target;
120
119
  if (t) {
@@ -125,65 +124,65 @@ export async function apply(ctx, _config) {
125
124
  }
126
125
  return describe(argv.session.platform, argv.session.userId, true);
127
126
  });
128
- // /grant <target> <capability>委托一个能力(子集约束在 manager 内校验)
127
+ // /level <target> <整数>owner 给外部身份设等级(越大越高,0=默认,负数=封禁)。权限管理仅 owner 可达(防自授)。
129
128
  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> — 禁用一个能力
134
- cmds
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) {
129
+ .command('level <target:string> <level:number>', '设置用户权限等级(整数,越大越高;0 默认,负数封禁)', {
130
+ visibility: 'restricted',
131
+ })
132
+ .example('/level onebot:12345 5')
133
+ .action(async (argv, target, level) => {
134
+ if (!authority.isOwner(argv.session.platform, argv.session.userId))
135
+ return '只有 owner 可管理权限';
140
136
  const t = String(target);
141
- const cap = String(capability).trim();
142
137
  const sep = t.indexOf(':');
143
138
  if (sep < 1)
144
139
  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
- // 仅限私聊:群聊发码会暴露给旁观者。
140
+ const lv = Number(level);
141
+ if (!Number.isInteger(lv))
142
+ return '等级必须是整数';
143
+ authority.setUserLevel({ platform: t.slice(0, sep), userId: t.slice(sep + 1) }, lv);
144
+ authority.save();
145
+ return `已设 ${t} 等级: ${lv}`;
146
+ });
147
+ // /auto [分钟|off|on] — owner 临时免 dangerous 二次确认(批处理便利)。on=一直, off=关, 数字=分钟。
165
148
  cmds
166
- .command('bind <code:string>', '将当前平台账号绑定到 WebUI 账户', { visibility: 'public' })
167
- .example('/bind AB12CD34')
168
- .action(async (argv, code) => {
169
- const { platform, userId, sessionType } = argv.session;
170
- if (!userId)
171
- return '无法识别您的身份,无法绑定。';
172
- if (platform === 'webui' || platform === 'cli')
173
- return '请在外部平台(如 QQ)私聊中向机器人发送本指令。';
174
- if (sessionType !== 'private')
175
- return '为防止绑定码泄露,请在私聊中使用本指令。';
176
- try {
177
- const account = authority.consumeBindCode(String(code).trim().toUpperCase(), { platform, userId });
178
- authority.save();
179
- return `绑定成功:${platform}:${userId} ${account.platform}:${account.userId}。可在 WebUI 权限页解绑。`;
149
+ .command('auto [arg:string]', '自动确认模式:临时免 dangerous 二次确认(仅 owner 本人)', {
150
+ visibility: 'restricted',
151
+ })
152
+ .example('/auto 30')
153
+ .example('/auto off')
154
+ .action(async (argv, arg) => {
155
+ if (!authority.isOwner(argv.session.platform, argv.session.userId))
156
+ return '只有 owner 可管理权限';
157
+ const a = arg === undefined ? undefined : String(arg).trim().toLowerCase();
158
+ const setUntil = (u) => {
159
+ ctx.config.set('autoConfirmUntil', u);
160
+ ctx.getService('app')?.saveConfig();
161
+ };
162
+ if (a === undefined) {
163
+ const u = ctx.config.get('autoConfirmUntil') ?? 0;
164
+ if (u === -1)
165
+ return '自动确认:一直开启';
166
+ if (autoConfirmActive(u, Date.now()))
167
+ return `自动确认:开启中,剩 ${Math.ceil((u - Date.now()) / 60000)} 分钟`;
168
+ return '自动确认:关闭';
169
+ }
170
+ if (a === 'off' || a === '0') {
171
+ setUntil(0);
172
+ return '已关闭自动确认';
180
173
  }
181
- catch (err) {
182
- return err instanceof Error ? err.message : String(err);
174
+ if (a === 'on') {
175
+ setUntil(-1);
176
+ return '已开启自动确认(一直,直到手动关闭)';
183
177
  }
178
+ const m = Number(a);
179
+ if (!Number.isInteger(m) || m <= 0)
180
+ return '用法:/auto <分钟> | off | on';
181
+ setUntil(Date.now() + m * 60000);
182
+ return `已开启自动确认 ${m} 分钟`;
184
183
  });
185
184
  }
186
- // ===== WebUI 操作处理器(最小新模型集;委托树/图 Phase 4 充实)=====
185
+ // ===== WebUI 操作处理器(数字等级单轴:用户等级 + 操作门槛 + owner 列表 + 高级)=====
187
186
  function asStringList(v, label) {
188
187
  if (v === undefined || v === null)
189
188
  return undefined;
@@ -192,7 +191,7 @@ function asStringList(v, label) {
192
191
  return v;
193
192
  }
194
193
  export const actions = {
195
- /** 权限概览:用户能力委托 + owner + 操作可见性 + 临时委托 + 受限/禁用清单 */
194
+ /** 权限概览:用户等级 + owner + 操作门槛/确认 + 临时放行 + 受限/禁用清单 */
196
195
  async getOverview(ctx) {
197
196
  const auth = ctx.getService('authority');
198
197
  const users = auth?.listUsers() ?? [];
@@ -212,242 +211,52 @@ export const actions = {
212
211
  users,
213
212
  owners,
214
213
  platforms,
215
- restrictedCapabilities: ctx.config.get('restrictedCapabilities') ?? [],
216
214
  deniedCapabilities: ctx.config.get('deniedCapabilities') ?? [],
217
- visibilityOverrides: ctx.config.get('visibilityOverrides') ?? {},
215
+ authorityOverrides: ctx.config.get('authorityOverrides') ?? {},
216
+ defaultAuthority: DEFAULT_AUTHORITY,
217
+ confirmOverrides: ctx.config.get('confirmOverrides') ?? {},
218
+ autoConfirmUntil: ctx.config.get('autoConfirmUntil') ?? 0,
218
219
  restrictedPolicy: ctx.config.get('restrictedPolicy') ?? {},
219
220
  temporaryGrants: auth?.listTemporaryGrants() ?? [],
220
221
  commandPrefix,
222
+ // 操作清单:指令 + 工具统一带 pluginName/type/confirm,供前端「操作」视图按插件分组、显示两轴默认。
221
223
  commands: cmdNodes.map(n => ({
222
224
  key: n.name,
223
225
  name: n.name,
226
+ type: 'command',
224
227
  displayName: `${commandPrefix}${n.name.split('.').join(' ')}`,
228
+ pluginName: n.pluginName,
225
229
  visibility: n.visibility ?? 'public',
230
+ confirm: n.confirm,
231
+ risk: n.risk,
232
+ })),
233
+ tools: tools.map(t => ({
234
+ key: t.name,
235
+ name: t.name,
236
+ type: 'tool',
237
+ displayName: t.name,
238
+ pluginName: t.pluginName,
239
+ visibility: t.visibility ?? 'public',
240
+ confirm: t.confirm,
241
+ risk: t.risk,
226
242
  })),
227
- tools: tools.map(t => ({ key: t.name, name: t.name, visibility: t.visibility ?? 'public' })),
228
- };
229
- },
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) {
237
- const auth = ctx.getService('authority');
238
- const users = auth?.listUsers() ?? [];
239
- const owners = ctx.config.get('owners') ?? [];
240
- const nodes = new Map();
241
- const edges = [];
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;
250
- };
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;
256
- };
257
- for (const o of owners)
258
- ensureUser(`${o.platform}:${o.userId}`);
259
- for (const u of users) {
260
- const key = `${u.platform}:${u.userId}`;
261
- const src = ensureUser(key);
262
- for (const g of u.grant ?? [])
263
- edges.push({
264
- data: { id: `grant:${key}:${g}`, source: src, target: ensureCap(g), label: '授予', kind: 'grant' },
265
- });
266
- for (const d of u.deny ?? [])
267
- edges.push({
268
- data: { id: `deny:${key}:${d}`, source: src, target: ensureCap(d), label: '拒绝', kind: 'deny' },
269
- });
270
- if (u.grantedBy)
271
- edges.push({
272
- data: {
273
- id: `delegate:${key}`,
274
- source: ensureUser(u.grantedBy),
275
- target: src,
276
- label: '委托',
277
- kind: 'delegate',
278
- directed: true,
279
- },
280
- });
281
- if (u.linkedTo)
282
- edges.push({
283
- data: {
284
- id: `bind:${key}`,
285
- source: src,
286
- target: ensureUser(u.linkedTo),
287
- label: '绑定',
288
- kind: 'bind',
289
- directed: true,
290
- },
291
- });
292
- }
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
- });
304
- }
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;
355
- }
356
- return {
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,
364
243
  };
365
244
  },
366
- /** 委托关系图节点详情(detailSource;点节点时调用) */
367
- async getDelegationNode(ctx, args) {
368
- const nodeId = String(args.nodeId ?? '');
369
- const auth = ctx.getService('authority');
370
- const users = auth?.listUsers() ?? [];
371
- if (nodeId.startsWith('user:')) {
372
- const key = nodeId.slice(5);
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);
376
- return {
377
- 身份: key,
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}` : '(无)'),
384
- };
385
- }
386
- if (nodeId.startsWith('cap:')) {
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('、') || '(无)' };
391
- }
392
- return { id: nodeId };
393
- },
394
- /** 委托:设置用户能力 grant/deny(caller 为授予方,非 owner 时子集校验在 manager 内) */
395
- async setUserCapabilities(ctx, args, caller) {
396
- const { platform, userId, grant, deny } = args;
245
+ /** 设置外部身份等级(覆盖式整数)。权限管理仅 owner 可达(防自我提权)。 */
246
+ async setUserLevel(ctx, args, caller) {
247
+ const { platform, userId, level } = args;
397
248
  if (!platform || !userId)
398
249
  throw new Error('platform, userId 必填');
250
+ if (typeof level !== 'number' || !Number.isInteger(level))
251
+ throw new Error('level 必须是整数');
399
252
  const auth = ctx.getService('authority');
400
253
  if (!auth)
401
254
  throw new Error('Authority 服务不可用');
402
- auth.setUserCapabilities(caller ?? null, { platform: platform, userId: userId }, { grant: asStringList(grant, 'grant'), deny: asStringList(deny, 'deny') });
255
+ if (caller && !auth.isOwner(caller.platform, caller.userId))
256
+ throw new Error('只有 owner 可管理权限');
257
+ auth.setUserLevel({ platform: platform, userId: userId }, level);
403
258
  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 或本人可设置密码');
419
- }
420
- await auth.setPassword(platform, userId, password);
421
- auth.save();
422
- return { message: `${platform}:${userId} 密码已更新` };
423
- },
424
- async createBindCode(ctx, _args, caller) {
425
- if (!caller)
426
- throw new Error('无法识别调用者身份');
427
- const auth = ctx.getService('authority');
428
- if (!auth)
429
- throw new Error('Authority 服务不可用');
430
- const { code, expiresAt } = auth.createBindCode(caller.platform, caller.userId);
431
- const prefix = ctx.getService('commands')?.prefix ?? '/';
432
- return { code, expiresAt, hint: `请在 5 分钟内用要绑定的平台账号私聊机器人发送:${prefix}bind ${code}` };
433
- },
434
- async unlinkIdentity(ctx, args, caller) {
435
- const { platform, userId } = args;
436
- if (!platform || !userId)
437
- throw new Error('platform, userId 必填');
438
- const auth = ctx.getService('authority');
439
- if (!auth)
440
- throw new Error('Authority 服务不可用');
441
- if (caller) {
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)) {
445
- throw new Error('只有绑定所属账户本人或 owner 可以解绑');
446
- }
447
- }
448
- const ok = auth.unlinkIdentity(platform, userId);
449
- auth.save();
450
- return { ok, message: ok ? `${platform}:${userId} 已解绑` : '该身份没有绑定记录' };
259
+ return { message: `${platform}:${userId} 等级已更新为 ${level}` };
451
260
  },
452
261
  /** 删除用户记录 */
453
262
  async deleteUser(ctx, args) {
@@ -459,11 +268,14 @@ export const actions = {
459
268
  auth?.save();
460
269
  return { message: `${platform}:${userId} 记录已删除` };
461
270
  },
462
- /** 更新 owner 列表 */
463
- async setOwners(ctx, args) {
271
+ /** 更新 owner 列表(仅 owner 可达:防非 owner 把自己加成 owner 提权) */
272
+ async setOwners(ctx, args, caller) {
464
273
  const owners = args.owners;
465
274
  if (!Array.isArray(owners))
466
275
  throw new Error('owners 必须是数组');
276
+ const auth = ctx.getService('authority');
277
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
278
+ throw new Error('只有 owner 可管理 owner 列表');
467
279
  const app = ctx.getService('app');
468
280
  if (!app)
469
281
  throw new Error('App 不可用');
@@ -497,42 +309,72 @@ export const actions = {
497
309
  const ok = auth.revokeTemporaryGrant(id);
498
310
  return { ok, message: ok ? '临时委托已撤销' : '不存在或已过期' };
499
311
  },
500
- /** owner 覆盖单条操作的可见性(public restricted),无需改插件声明 */
501
- async setVisibilityOverride(ctx, args) {
502
- const { name, visibility } = args;
312
+ /** owner 覆盖单条操作的最低等级(任意整数),无需改插件声明。key=能力键 `type:name`;传非整数则清除该条(回退默认派生)。 */
313
+ async setAuthorityOverride(ctx, args, caller) {
314
+ const { name, level } = args;
503
315
  if (!name || typeof name !== 'string')
504
316
  throw new Error('name 必填');
317
+ const auth = ctx.getService('authority');
318
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
319
+ throw new Error('只有 owner 可管理权限');
320
+ const app = ctx.getService('app');
321
+ if (!app)
322
+ throw new Error('App 不可用');
323
+ const overrides = { ...(ctx.config.get('authorityOverrides') ?? {}) };
324
+ if (typeof level === 'number' && Number.isInteger(level))
325
+ overrides[name] = level;
326
+ else
327
+ delete overrides[name];
328
+ ctx.config.set('authorityOverrides', overrides);
329
+ app.saveConfig();
330
+ return { message: `操作 ${name} 最低等级已更新` };
331
+ },
332
+ /** owner 覆盖单条操作的确认要求(session/always/off)。key=能力键 `type:name`;非法值清除该条。 */
333
+ async setConfirmOverride(ctx, args, caller) {
334
+ const { name, confirm } = args;
335
+ if (!name || typeof name !== 'string')
336
+ throw new Error('name 必填');
337
+ const auth = ctx.getService('authority');
338
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
339
+ throw new Error('只有 owner 可管理权限');
505
340
  const app = ctx.getService('app');
506
341
  if (!app)
507
342
  throw new Error('App 不可用');
508
- const overrides = { ...(ctx.config.get('visibilityOverrides') ?? {}) };
509
- if (visibility === 'public' || visibility === 'restricted')
510
- overrides[name] = visibility;
343
+ const overrides = { ...(ctx.config.get('confirmOverrides') ?? {}) };
344
+ if (confirm === 'session' || confirm === 'always' || confirm === 'off')
345
+ overrides[name] = confirm;
511
346
  else
512
347
  delete overrides[name];
513
- ctx.config.set('visibilityOverrides', overrides);
348
+ ctx.config.set('confirmOverrides', overrides);
514
349
  app.saveConfig();
515
- return { message: `操作 ${name} 可见性已更新` };
350
+ return { message: `操作 ${name} 确认要求已更新` };
516
351
  },
517
- /** 更新受限/禁用能力清单 */
352
+ /** owner 切换 auto 确认模式。minutes: -1=一直 / 0=关 / N=N 分钟。仅 owner 可达。 */
353
+ async setAutoConfirm(ctx, args, caller) {
354
+ const auth = ctx.getService('authority');
355
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
356
+ throw new Error('只有 owner 可管理权限');
357
+ const app = ctx.getService('app');
358
+ if (!app)
359
+ throw new Error('App 不可用');
360
+ const m = args.minutes;
361
+ if (typeof m !== 'number' || !Number.isInteger(m))
362
+ throw new Error('minutes 必须是整数(-1 一直 / 0 关 / N 分钟)');
363
+ const until = m === -1 ? -1 : m <= 0 ? 0 : Date.now() + m * 60000;
364
+ ctx.config.set('autoConfirmUntil', until);
365
+ app.saveConfig();
366
+ return { message: until === -1 ? '自动确认:一直' : until === 0 ? '自动确认:关' : `自动确认:${m} 分钟`, until };
367
+ },
368
+ /** 更新禁用能力清单 */
518
369
  async setConfig(ctx, args) {
519
370
  const app = ctx.getService('app');
520
371
  if (!app)
521
372
  throw new Error('App 不可用');
522
- const restricted = asStringList(args.restrictedCapabilities, 'restrictedCapabilities');
523
373
  const denied = asStringList(args.deniedCapabilities, 'deniedCapabilities');
524
- if (restricted)
525
- ctx.config.set('restrictedCapabilities', restricted);
526
374
  if (denied)
527
375
  ctx.config.set('deniedCapabilities', denied);
528
376
  app.saveConfig();
529
377
  return { message: '权限配置已更新' };
530
378
  },
531
379
  };
532
- // createBindCode / unlinkIdentity 对任何登录账户开放(绑码只能绑自己;解绑有 handler 内本人/owner 检查);
533
- // 其余 action 不声明 → 默认 restricted(仅 owner / 被委托)。
534
- export const actionsMeta = {
535
- createBindCode: { visibility: 'public' },
536
- unlinkIdentity: { visibility: 'public' },
537
- };
538
380
  //# sourceMappingURL=index.js.map