@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/authority-manager.d.ts +14 -19
- package/dist/authority-manager.d.ts.map +1 -1
- package/dist/authority-manager.js +64 -124
- package/dist/authority-manager.js.map +1 -1
- package/dist/authority-model.d.ts +51 -0
- package/dist/authority-model.d.ts.map +1 -0
- package/dist/authority-model.js +85 -0
- package/dist/authority-model.js.map +1 -0
- package/dist/capability-model.d.ts +0 -8
- package/dist/capability-model.d.ts.map +1 -1
- package/dist/capability-model.js +2 -15
- package/dist/capability-model.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +182 -345
- package/dist/index.js.map +1 -1
- package/dist/tier-model.d.ts +42 -0
- package/dist/tier-model.d.ts.map +1 -0
- package/dist/tier-model.js +83 -0
- package/dist/tier-model.js.map +1 -0
- package/dist/user-store.d.ts +5 -31
- package/dist/user-store.d.ts.map +1 -1
- package/dist/user-store.js +19 -154
- package/dist/user-store.js.map +1 -1
- package/package.json +12 -10
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
|
|
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
|
-
|
|
60
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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]', '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// /
|
|
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('
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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('
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (platform
|
|
173
|
-
return '
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
367
|
-
async
|
|
368
|
-
const
|
|
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
|
-
|
|
443
|
-
|
|
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 {
|
|
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
|
|
501
|
-
async
|
|
502
|
-
const { name,
|
|
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('
|
|
509
|
-
if (
|
|
510
|
-
overrides[name] =
|
|
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('
|
|
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
|