@aalis/plugin-authority 0.2.0 → 0.5.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
@@ -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) {
@@ -53,30 +28,60 @@ export async function apply(ctx, _config) {
53
28
  const authority = new AuthorityManager(ctx.config, ctx.logger, storage);
54
29
  await authority.init();
55
30
  ctx.provide('authority', authority);
56
- // ===== 执行守卫:能力统一闸 + 受限能力的临时委托确认 =====
31
+ // 网络出口闸(SSRF):把 core 配置 network 注入进程级 safeFetch 策略(启动一次)。
32
+ // 安全归属在权限域;本地固定服务走裸 fetch、不过 safeFetch,故不受影响。
33
+ setNetworkPolicy(ctx.config.get('network') ?? {});
34
+ // ===== 执行守卫:两轴正交闸 —— 轴 A 授权(authorize)+ 轴 B 确认(confirm,owner 也吃)=====
57
35
  const guard = async (g) => {
58
36
  const capability = `${g.type}:${g.name}`;
59
- const overrides = (ctx.config.get('visibilityOverrides') ?? {});
60
- const visibility = overrides[g.name] ?? g.visibility;
37
+ // 确认覆盖:'off' 强制关确认;否则覆盖值优先,回退插件声明。
38
+ const confOv = (ctx.config.get('confirmOverrides') ?? {});
39
+ const cOv = confOv[capability];
40
+ const confirm = cOv === 'off' ? undefined : (cOv ?? g.confirm);
61
41
  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({
42
+ const accessBase = {
70
43
  name: g.name,
71
44
  type: g.type,
72
45
  capability,
73
- resourceCapabilities: g.permissions,
74
46
  args: g.args,
75
47
  sessionId: g.sessionId,
76
48
  platform: g.platform,
77
49
  userId: g.userId,
50
+ };
51
+ // ── 轴 A · 授权:数字等级裁决(minLevel 由 risk/visibility/authorityOverrides 在 manager 内派生)——系统源也评估,防绕过提权 ──
52
+ const denied = authority.authorize(identity, {
53
+ capability,
54
+ visibility: g.visibility,
55
+ risk: g.risk,
78
56
  });
79
- return granted ? null : denied;
57
+ if (denied) {
58
+ // 未授权(等级不够 / 硬禁 / 资源受限):**绝不**让发起者本人弹确认自我提权。
59
+ // 仅 owner 预先配置的放行(restrictedPolicy 白名单 / 该用户在本会话已有的授予)可救;否则硬拒。
60
+ // 注意:这里**不**调 requestAccess(那会询问发起者)——只查 isPreApproved(不问人)。
61
+ if (g.skipConfirm)
62
+ return denied;
63
+ return authority.isPreApproved(accessBase) ? null : denied;
64
+ }
65
+ // ── 轴 B · 确认:授权已过(含 owner / public / 已授予),但操作声明了 confirm 仍需「意图确认」 ──
66
+ // 仅对**已授权**操作做意图确认(owner 也吃,防注入借权);不再是提权入口。
67
+ // 跳过判定见 shouldSkipConfirm:always 永不跳(cron 无人确认即拒);非 always 可被
68
+ // skipConfirm(系统/受信源) 或 owner 本人 auto 模式 跳过。修 #6:旧实现用 `!g.skipConfirm`
69
+ // 门控整块 → skipConfirm 会连 always 一起绕过。
70
+ if (confirm) {
71
+ const skip = shouldSkipConfirm({
72
+ confirm,
73
+ skipConfirm: !!g.skipConfirm,
74
+ isOwner: authority.isOwner(g.platform, g.userId),
75
+ autoConfirmUntil: ctx.config.get('autoConfirmUntil') ?? 0,
76
+ now: Date.now(),
77
+ });
78
+ if (!skip) {
79
+ const ok = await authority.requestAccess({ ...accessBase, confirm });
80
+ if (!ok)
81
+ return `操作已取消:${capability} 需确认后执行`;
82
+ }
83
+ }
84
+ return null;
80
85
  };
81
86
  // 注入到 commands / tools(whenService 在 provider 上线/重启时各调一次)
82
87
  ctx.whenService('commands', svc => {
@@ -93,28 +98,17 @@ export async function apply(ctx, _config) {
93
98
  });
94
99
  ctx.on('app:stopping', () => authority.save());
95
100
  // ===== 权限指令 =====
96
- // /authority [target] — 查看自己或指定用户的能力
97
- cmds.command('authority [target:string]', '查看自己或指定用户的能力授予').action(async (argv, target) => {
101
+ // /authority [target] — 查看自己或指定用户的权限等级
102
+ cmds.command('authority [target:string]', '查看自己或指定用户的权限等级').action(async (argv, target) => {
98
103
  const describe = (platform, userId, self) => {
99
104
  const isOwner = authority.isOwner(platform, userId);
100
105
  const who = self ? '您' : `${platform}:${userId}`;
101
- const lines = [`${who}${isOwner ? '(owner,拥有全部能力)' : ''}`];
106
+ if (isOwner)
107
+ return `${who}(owner,等级 ∞,拥有全部权限)`;
102
108
  const entry = userId
103
109
  ? authority.listUsers().find(u => u.platform === platform && u.userId === userId)
104
110
  : 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');
111
+ return `${who} 等级: ${entry?.level ?? DEFAULT_AUTHORITY}`;
118
112
  };
119
113
  const t = target;
120
114
  if (t) {
@@ -125,65 +119,65 @@ export async function apply(ctx, _config) {
125
119
  }
126
120
  return describe(argv.session.platform, argv.session.userId, true);
127
121
  });
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> — 禁用一个能力
122
+ // /level <target> <整数>owner 给外部身份设等级(越大越高,0=默认,负数=封禁)。权限管理仅 owner 可达(防自授)。
134
123
  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) {
124
+ .command('level <target:string> <level:number>', '设置用户权限等级(整数,越大越高;0 默认,负数封禁)', {
125
+ visibility: 'restricted',
126
+ })
127
+ .example('/level onebot:12345 5')
128
+ .action(async (argv, target, level) => {
129
+ if (!authority.isOwner(argv.session.platform, argv.session.userId))
130
+ return '只有 owner 可管理权限';
140
131
  const t = String(target);
141
- const cap = String(capability).trim();
142
132
  const sep = t.indexOf(':');
143
133
  if (sep < 1)
144
134
  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
- // 仅限私聊:群聊发码会暴露给旁观者。
135
+ const lv = Number(level);
136
+ if (!Number.isInteger(lv))
137
+ return '等级必须是整数';
138
+ authority.setUserLevel({ platform: t.slice(0, sep), userId: t.slice(sep + 1) }, lv);
139
+ authority.save();
140
+ return `已设 ${t} 等级: ${lv}`;
141
+ });
142
+ // /auto [分钟|off|on] — owner 临时免 dangerous 二次确认(批处理便利)。on=一直, off=关, 数字=分钟。
165
143
  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 权限页解绑。`;
144
+ .command('auto [arg:string]', '自动确认模式:临时免 dangerous 二次确认(仅 owner 本人)', {
145
+ visibility: 'restricted',
146
+ })
147
+ .example('/auto 30')
148
+ .example('/auto off')
149
+ .action(async (argv, arg) => {
150
+ if (!authority.isOwner(argv.session.platform, argv.session.userId))
151
+ return '只有 owner 可管理权限';
152
+ const a = arg === undefined ? undefined : String(arg).trim().toLowerCase();
153
+ const setUntil = (u) => {
154
+ ctx.config.set('autoConfirmUntil', u);
155
+ ctx.getService('app')?.saveConfig();
156
+ };
157
+ if (a === undefined) {
158
+ const u = ctx.config.get('autoConfirmUntil') ?? 0;
159
+ if (u === -1)
160
+ return '自动确认:一直开启';
161
+ if (autoConfirmActive(u, Date.now()))
162
+ return `自动确认:开启中,剩 ${Math.ceil((u - Date.now()) / 60000)} 分钟`;
163
+ return '自动确认:关闭';
180
164
  }
181
- catch (err) {
182
- return err instanceof Error ? err.message : String(err);
165
+ if (a === 'off' || a === '0') {
166
+ setUntil(0);
167
+ return '已关闭自动确认';
183
168
  }
169
+ if (a === 'on') {
170
+ setUntil(-1);
171
+ return '已开启自动确认(一直,直到手动关闭)';
172
+ }
173
+ const m = Number(a);
174
+ if (!Number.isInteger(m) || m <= 0)
175
+ return '用法:/auto <分钟> | off | on';
176
+ setUntil(Date.now() + m * 60000);
177
+ return `已开启自动确认 ${m} 分钟`;
184
178
  });
185
179
  }
186
- // ===== WebUI 操作处理器(最小新模型集;委托树/图 Phase 4 充实)=====
180
+ // ===== WebUI 操作处理器(数字等级单轴:用户等级 + 操作门槛 + owner 列表 + 高级)=====
187
181
  function asStringList(v, label) {
188
182
  if (v === undefined || v === null)
189
183
  return undefined;
@@ -192,7 +186,7 @@ function asStringList(v, label) {
192
186
  return v;
193
187
  }
194
188
  export const actions = {
195
- /** 权限概览:用户能力委托 + owner + 操作可见性 + 临时委托 + 受限/禁用清单 */
189
+ /** 权限概览:用户等级 + owner + 操作门槛/确认 + 临时放行 + 受限/禁用清单 */
196
190
  async getOverview(ctx) {
197
191
  const auth = ctx.getService('authority');
198
192
  const users = auth?.listUsers() ?? [];
@@ -212,242 +206,52 @@ export const actions = {
212
206
  users,
213
207
  owners,
214
208
  platforms,
215
- restrictedCapabilities: ctx.config.get('restrictedCapabilities') ?? [],
216
209
  deniedCapabilities: ctx.config.get('deniedCapabilities') ?? [],
217
- visibilityOverrides: ctx.config.get('visibilityOverrides') ?? {},
210
+ authorityOverrides: ctx.config.get('authorityOverrides') ?? {},
211
+ defaultAuthority: DEFAULT_AUTHORITY,
212
+ confirmOverrides: ctx.config.get('confirmOverrides') ?? {},
213
+ autoConfirmUntil: ctx.config.get('autoConfirmUntil') ?? 0,
218
214
  restrictedPolicy: ctx.config.get('restrictedPolicy') ?? {},
219
215
  temporaryGrants: auth?.listTemporaryGrants() ?? [],
220
216
  commandPrefix,
217
+ // 操作清单:指令 + 工具统一带 pluginName/type/confirm,供前端「操作」视图按插件分组、显示两轴默认。
221
218
  commands: cmdNodes.map(n => ({
222
219
  key: n.name,
223
220
  name: n.name,
221
+ type: 'command',
224
222
  displayName: `${commandPrefix}${n.name.split('.').join(' ')}`,
223
+ pluginName: n.pluginName,
225
224
  visibility: n.visibility ?? 'public',
225
+ confirm: n.confirm,
226
+ risk: n.risk,
227
+ })),
228
+ tools: tools.map(t => ({
229
+ key: t.name,
230
+ name: t.name,
231
+ type: 'tool',
232
+ displayName: t.name,
233
+ pluginName: t.pluginName,
234
+ visibility: t.visibility ?? 'public',
235
+ confirm: t.confirm,
236
+ risk: t.risk,
226
237
  })),
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
238
  };
365
239
  },
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;
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 或本人可设置密码');
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;
240
+ /** 设置外部身份等级(覆盖式整数)。权限管理仅 owner 可达(防自我提权)。 */
241
+ async setUserLevel(ctx, args, caller) {
242
+ const { platform, userId, level } = args;
436
243
  if (!platform || !userId)
437
244
  throw new Error('platform, userId 必填');
245
+ if (typeof level !== 'number' || !Number.isInteger(level))
246
+ throw new Error('level 必须是整数');
438
247
  const auth = ctx.getService('authority');
439
248
  if (!auth)
440
249
  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);
250
+ if (caller && !auth.isOwner(caller.platform, caller.userId))
251
+ throw new Error('只有 owner 可管理权限');
252
+ auth.setUserLevel({ platform: platform, userId: userId }, level);
449
253
  auth.save();
450
- return { ok, message: ok ? `${platform}:${userId} 已解绑` : '该身份没有绑定记录' };
254
+ return { message: `${platform}:${userId} 等级已更新为 ${level}` };
451
255
  },
452
256
  /** 删除用户记录 */
453
257
  async deleteUser(ctx, args) {
@@ -459,11 +263,14 @@ export const actions = {
459
263
  auth?.save();
460
264
  return { message: `${platform}:${userId} 记录已删除` };
461
265
  },
462
- /** 更新 owner 列表 */
463
- async setOwners(ctx, args) {
266
+ /** 更新 owner 列表(仅 owner 可达:防非 owner 把自己加成 owner 提权) */
267
+ async setOwners(ctx, args, caller) {
464
268
  const owners = args.owners;
465
269
  if (!Array.isArray(owners))
466
270
  throw new Error('owners 必须是数组');
271
+ const auth = ctx.getService('authority');
272
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
273
+ throw new Error('只有 owner 可管理 owner 列表');
467
274
  const app = ctx.getService('app');
468
275
  if (!app)
469
276
  throw new Error('App 不可用');
@@ -497,42 +304,72 @@ export const actions = {
497
304
  const ok = auth.revokeTemporaryGrant(id);
498
305
  return { ok, message: ok ? '临时委托已撤销' : '不存在或已过期' };
499
306
  },
500
- /** owner 覆盖单条操作的可见性(public restricted),无需改插件声明 */
501
- async setVisibilityOverride(ctx, args) {
502
- const { name, visibility } = args;
307
+ /** owner 覆盖单条操作的最低等级(任意整数),无需改插件声明。key=能力键 `type:name`;传非整数则清除该条(回退默认派生)。 */
308
+ async setAuthorityOverride(ctx, args, caller) {
309
+ const { name, level } = args;
503
310
  if (!name || typeof name !== 'string')
504
311
  throw new Error('name 必填');
312
+ const auth = ctx.getService('authority');
313
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
314
+ throw new Error('只有 owner 可管理权限');
315
+ const app = ctx.getService('app');
316
+ if (!app)
317
+ throw new Error('App 不可用');
318
+ const overrides = { ...(ctx.config.get('authorityOverrides') ?? {}) };
319
+ if (typeof level === 'number' && Number.isInteger(level))
320
+ overrides[name] = level;
321
+ else
322
+ delete overrides[name];
323
+ ctx.config.set('authorityOverrides', overrides);
324
+ app.saveConfig();
325
+ return { message: `操作 ${name} 最低等级已更新` };
326
+ },
327
+ /** owner 覆盖单条操作的确认要求(session/always/off)。key=能力键 `type:name`;非法值清除该条。 */
328
+ async setConfirmOverride(ctx, args, caller) {
329
+ const { name, confirm } = args;
330
+ if (!name || typeof name !== 'string')
331
+ throw new Error('name 必填');
332
+ const auth = ctx.getService('authority');
333
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
334
+ throw new Error('只有 owner 可管理权限');
505
335
  const app = ctx.getService('app');
506
336
  if (!app)
507
337
  throw new Error('App 不可用');
508
- const overrides = { ...(ctx.config.get('visibilityOverrides') ?? {}) };
509
- if (visibility === 'public' || visibility === 'restricted')
510
- overrides[name] = visibility;
338
+ const overrides = { ...(ctx.config.get('confirmOverrides') ?? {}) };
339
+ if (confirm === 'session' || confirm === 'always' || confirm === 'off')
340
+ overrides[name] = confirm;
511
341
  else
512
342
  delete overrides[name];
513
- ctx.config.set('visibilityOverrides', overrides);
343
+ ctx.config.set('confirmOverrides', overrides);
514
344
  app.saveConfig();
515
- return { message: `操作 ${name} 可见性已更新` };
345
+ return { message: `操作 ${name} 确认要求已更新` };
516
346
  },
517
- /** 更新受限/禁用能力清单 */
347
+ /** owner 切换 auto 确认模式。minutes: -1=一直 / 0=关 / N=N 分钟。仅 owner 可达。 */
348
+ async setAutoConfirm(ctx, args, caller) {
349
+ const auth = ctx.getService('authority');
350
+ if (caller && !auth?.isOwner(caller.platform, caller.userId))
351
+ throw new Error('只有 owner 可管理权限');
352
+ const app = ctx.getService('app');
353
+ if (!app)
354
+ throw new Error('App 不可用');
355
+ const m = args.minutes;
356
+ if (typeof m !== 'number' || !Number.isInteger(m))
357
+ throw new Error('minutes 必须是整数(-1 一直 / 0 关 / N 分钟)');
358
+ const until = m === -1 ? -1 : m <= 0 ? 0 : Date.now() + m * 60000;
359
+ ctx.config.set('autoConfirmUntil', until);
360
+ app.saveConfig();
361
+ return { message: until === -1 ? '自动确认:一直' : until === 0 ? '自动确认:关' : `自动确认:${m} 分钟`, until };
362
+ },
363
+ /** 更新禁用能力清单 */
518
364
  async setConfig(ctx, args) {
519
365
  const app = ctx.getService('app');
520
366
  if (!app)
521
367
  throw new Error('App 不可用');
522
- const restricted = asStringList(args.restrictedCapabilities, 'restrictedCapabilities');
523
368
  const denied = asStringList(args.deniedCapabilities, 'deniedCapabilities');
524
- if (restricted)
525
- ctx.config.set('restrictedCapabilities', restricted);
526
369
  if (denied)
527
370
  ctx.config.set('deniedCapabilities', denied);
528
371
  app.saveConfig();
529
372
  return { message: '权限配置已更新' };
530
373
  },
531
374
  };
532
- // createBindCode / unlinkIdentity 对任何登录账户开放(绑码只能绑自己;解绑有 handler 内本人/owner 检查);
533
- // 其余 action 不声明 → 默认 restricted(仅 owner / 被委托)。
534
- export const actionsMeta = {
535
- createBindCode: { visibility: 'public' },
536
- unlinkIdentity: { visibility: 'public' },
537
- };
538
375
  //# sourceMappingURL=index.js.map