metame-cli 1.4.17 → 1.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -34
- package/index.js +12 -5
- package/package.json +2 -2
- package/scripts/check-macos-control-capabilities.sh +77 -0
- package/scripts/daemon-admin-commands.js +350 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-agent-commands.js +20 -1
- package/scripts/daemon-claude-engine.js +62 -12
- package/scripts/daemon-command-router.js +257 -12
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-session-store.js +176 -41
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +213 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon-user-acl.js +399 -0
- package/scripts/daemon.js +376 -26
- package/scripts/distill.js +184 -34
- package/scripts/memory-extract.js +13 -5
- package/scripts/memory.js +239 -60
- package/scripts/providers.js +1 -1
- package/scripts/reliability-core.test.js +268 -0
- package/scripts/session-analytics.js +123 -35
- package/scripts/signal-capture.js +171 -11
- package/scripts/skill-evolution.js +158 -19
- package/scripts/task-board.js +398 -0
- package/scripts/task-board.test.js +83 -0
- package/scripts/usage-classifier.js +139 -0
- package/scripts/utils.js +107 -0
- package/scripts/utils.test.js +61 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-user-acl.js — MetaMe 多用户权限管理模块
|
|
3
|
+
*
|
|
4
|
+
* 角色体系:
|
|
5
|
+
* admin — 全部权限(王总)
|
|
6
|
+
* member — 可配置白名单操作
|
|
7
|
+
* stranger — 仅基础问答,无系统操作
|
|
8
|
+
*
|
|
9
|
+
* 配置文件:~/.metame/users.yaml(热更新,独立于 daemon.yaml)
|
|
10
|
+
* 格式:
|
|
11
|
+
* users:
|
|
12
|
+
* ou_abc123: { role: admin, name: 王总 }
|
|
13
|
+
* ou_def456: { role: member, name: 老马, allowed_actions: [feedback, status, query] }
|
|
14
|
+
* default_role: stranger
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
|
|
23
|
+
const USERS_FILE = path.join(os.homedir(), '.metame', 'users.yaml');
|
|
24
|
+
|
|
25
|
+
// ─── YAML 轻量解析(无依赖) ─────────────────────────────────────────────────
|
|
26
|
+
// 只解析本文件需要的简单结构,不引入 js-yaml 依赖
|
|
27
|
+
function parseSimpleYaml(content) {
|
|
28
|
+
const result = {};
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
let currentSection = null;
|
|
31
|
+
let currentUserId = null;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const line = lines[i];
|
|
35
|
+
const trimmed = line.trimEnd();
|
|
36
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
37
|
+
|
|
38
|
+
const indent = line.length - line.trimStart().length;
|
|
39
|
+
const stripped = trimmed.trim();
|
|
40
|
+
|
|
41
|
+
if (indent === 0) {
|
|
42
|
+
const m = stripped.match(/^(\w[\w_]*):\s*(.*)$/);
|
|
43
|
+
if (m) {
|
|
44
|
+
currentSection = m[1];
|
|
45
|
+
if (m[2]) result[currentSection] = m[2];
|
|
46
|
+
else result[currentSection] = {};
|
|
47
|
+
currentUserId = null;
|
|
48
|
+
}
|
|
49
|
+
} else if (indent === 2 && currentSection === 'users') {
|
|
50
|
+
const m = stripped.match(/^([\w_-]+):\s*\{?(.*)\}?$/);
|
|
51
|
+
if (m) {
|
|
52
|
+
currentUserId = m[1];
|
|
53
|
+
result.users = result.users || {};
|
|
54
|
+
result.users[currentUserId] = parseInlineObj(m[2]);
|
|
55
|
+
}
|
|
56
|
+
} else if (indent === 4 && currentSection === 'users' && currentUserId) {
|
|
57
|
+
const m = stripped.match(/^([\w_]+):\s*(.+)$/);
|
|
58
|
+
if (m) {
|
|
59
|
+
result.users[currentUserId][m[1]] = parseYamlValue(m[2]);
|
|
60
|
+
}
|
|
61
|
+
} else if (indent === 2 && currentSection !== 'users') {
|
|
62
|
+
const m = stripped.match(/^([\w_]+):\s*(.+)$/);
|
|
63
|
+
if (m && typeof result[currentSection] === 'object') {
|
|
64
|
+
result[currentSection][m[1]] = parseYamlValue(m[2]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseInlineObj(str) {
|
|
72
|
+
const obj = {};
|
|
73
|
+
str = str.replace(/^\{|\}$/g, '').trim();
|
|
74
|
+
if (!str) return obj;
|
|
75
|
+
const parts = str.split(',').map(s => s.trim());
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
const m = part.match(/^([\w_]+):\s*(.+)$/);
|
|
78
|
+
if (m) obj[m[1]] = parseYamlValue(m[2]);
|
|
79
|
+
}
|
|
80
|
+
return obj;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseYamlValue(val) {
|
|
84
|
+
val = val.trim();
|
|
85
|
+
if (val === 'true') return true;
|
|
86
|
+
if (val === 'false') return false;
|
|
87
|
+
if (/^\d+$/.test(val)) return parseInt(val, 10);
|
|
88
|
+
// Array like [a, b, c]
|
|
89
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
90
|
+
return val.slice(1, -1).split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
// Quoted string
|
|
93
|
+
return val.replace(/^['"]|['"]$/g, '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── 序列化为 YAML ────────────────────────────────────────────────────────────
|
|
97
|
+
// name 字段转义:去除可能破坏行内 YAML 的特殊字符
|
|
98
|
+
function sanitizeYamlScalar(str) {
|
|
99
|
+
return String(str || '').replace(/[,:{}\[\]\r\n#]/g, '_').slice(0, 40);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function serializeUsers(data) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
// 始终写出 default_role,确保 reload 时语义明确
|
|
105
|
+
lines.push(`default_role: ${data.default_role || 'stranger'}`);
|
|
106
|
+
lines.push('users:');
|
|
107
|
+
for (const [uid, info] of Object.entries(data.users || {})) {
|
|
108
|
+
const actions = info.allowed_actions
|
|
109
|
+
? `, allowed_actions: [${info.allowed_actions.join(', ')}]`
|
|
110
|
+
: '';
|
|
111
|
+
const safeName = info.name ? `, name: ${sanitizeYamlScalar(info.name)}` : '';
|
|
112
|
+
lines.push(` ${uid}: { role: ${info.role}${safeName}${actions} }`);
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n') + '\n';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 飞书 open_id 格式校验(字母数字下划线,10-64 位)
|
|
118
|
+
function isValidOpenId(uid) {
|
|
119
|
+
return typeof uid === 'string' && /^[a-zA-Z0-9_-]{10,64}$/.test(uid);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── 加载用户配置 ─────────────────────────────────────────────────────────────
|
|
123
|
+
let _cachedUsers = null;
|
|
124
|
+
let _cachedMtime = 0;
|
|
125
|
+
|
|
126
|
+
function loadUsers() {
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.statSync(USERS_FILE);
|
|
129
|
+
if (stat.mtimeMs !== _cachedMtime) {
|
|
130
|
+
const content = fs.readFileSync(USERS_FILE, 'utf8');
|
|
131
|
+
_cachedUsers = parseSimpleYaml(content);
|
|
132
|
+
_cachedMtime = stat.mtimeMs;
|
|
133
|
+
}
|
|
134
|
+
return _cachedUsers || { users: {}, default_role: 'stranger' };
|
|
135
|
+
} catch {
|
|
136
|
+
return { users: {}, default_role: 'stranger' };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function saveUsers(data) {
|
|
141
|
+
const dir = path.dirname(USERS_FILE);
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
// [M3] 原子写入:先写临时文件再 rename,防止崩溃时损坏 users.yaml
|
|
144
|
+
const tmp = USERS_FILE + '.tmp';
|
|
145
|
+
fs.writeFileSync(tmp, serializeUsers(data), 'utf8');
|
|
146
|
+
fs.renameSync(tmp, USERS_FILE);
|
|
147
|
+
_cachedMtime = 0; // 强制下次重新加载
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── 权限动作定义 ─────────────────────────────────────────────────────────────
|
|
151
|
+
// 每个 action 代表一类权限门控点
|
|
152
|
+
const ACTION_GROUPS = {
|
|
153
|
+
// member 可开放的安全操作
|
|
154
|
+
feedback: { desc: '提交反馈' },
|
|
155
|
+
status: { desc: '查询系统状态 (/status, /tasks)' },
|
|
156
|
+
query: { desc: '自然语言问答(只读)' },
|
|
157
|
+
file_read: { desc: '查看文件' },
|
|
158
|
+
|
|
159
|
+
// admin 专属(不可赋予 member)
|
|
160
|
+
system: { desc: '系统操作 (/sh, /mac, /fix)' },
|
|
161
|
+
agent: { desc: 'Agent 调度与管理' },
|
|
162
|
+
config: { desc: '配置修改 (/reload, /model)' },
|
|
163
|
+
admin_acl: { desc: '用户权限管理 (/user *)' },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const ROLE_DEFAULT_ACTIONS = {
|
|
167
|
+
admin: Object.keys(ACTION_GROUPS), // 全部权限
|
|
168
|
+
member: ['query'], // 默认只能问答
|
|
169
|
+
stranger: [], // 无系统权限,但允许基础问答由 askClaude readOnly 处理
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// 不可赋予 member 的 admin 专属 action
|
|
173
|
+
const ADMIN_ONLY_ACTIONS = new Set(['system', 'agent', 'config', 'admin_acl']);
|
|
174
|
+
|
|
175
|
+
// ─── 核心 API ─────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 根据 senderId 解析用户上下文
|
|
179
|
+
* @param {string|null} senderId 飞书 open_id
|
|
180
|
+
* @param {object} config daemon 配置(兼容旧 operator_ids)
|
|
181
|
+
* @returns {object} userCtx { senderId, role, name, allowedActions, can(action) }
|
|
182
|
+
*/
|
|
183
|
+
function resolveUserCtx(senderId, config) {
|
|
184
|
+
const userData = loadUsers();
|
|
185
|
+
|
|
186
|
+
let role, name, allowedActions;
|
|
187
|
+
|
|
188
|
+
if (!senderId) {
|
|
189
|
+
// 无 ID(Telegram 等)— 兼容旧逻辑,视为 admin
|
|
190
|
+
role = 'admin';
|
|
191
|
+
name = 'unknown';
|
|
192
|
+
allowedActions = ROLE_DEFAULT_ACTIONS.admin;
|
|
193
|
+
} else {
|
|
194
|
+
const userInfo = (userData.users || {})[senderId];
|
|
195
|
+
|
|
196
|
+
if (userInfo) {
|
|
197
|
+
role = userInfo.role || 'member';
|
|
198
|
+
name = userInfo.name || senderId.slice(-6);
|
|
199
|
+
if (role === 'admin') {
|
|
200
|
+
allowedActions = ROLE_DEFAULT_ACTIONS.admin;
|
|
201
|
+
} else if (role === 'member') {
|
|
202
|
+
// member 的 allowed_actions = 默认 member 权限 ∪ 配置的扩展权限(过滤 admin-only)
|
|
203
|
+
const extra = (userInfo.allowed_actions || []).filter(a => !ADMIN_ONLY_ACTIONS.has(a));
|
|
204
|
+
allowedActions = [...new Set([...ROLE_DEFAULT_ACTIONS.member, ...extra])];
|
|
205
|
+
} else {
|
|
206
|
+
allowedActions = [];
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// 兼容旧 operator_ids:若 senderId 在 operator_ids 中,视为 admin
|
|
210
|
+
const operatorIds = (config && config.feishu && config.feishu.operator_ids) || [];
|
|
211
|
+
if (operatorIds.includes(senderId)) {
|
|
212
|
+
role = 'admin';
|
|
213
|
+
name = senderId.slice(-6);
|
|
214
|
+
allowedActions = ROLE_DEFAULT_ACTIONS.admin;
|
|
215
|
+
} else {
|
|
216
|
+
role = userData.default_role || 'stranger';
|
|
217
|
+
name = senderId.slice(-6);
|
|
218
|
+
allowedActions = ROLE_DEFAULT_ACTIONS[role] || [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
senderId,
|
|
225
|
+
role,
|
|
226
|
+
name,
|
|
227
|
+
allowedActions,
|
|
228
|
+
isAdmin: role === 'admin',
|
|
229
|
+
isMember: role === 'member',
|
|
230
|
+
isStranger: role === 'stranger',
|
|
231
|
+
can(action) { return allowedActions.includes(action); },
|
|
232
|
+
readOnly: role !== 'admin',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 所有人(包括 stranger)均可使用的公开命令,集中维护
|
|
237
|
+
const PUBLIC_COMMANDS = ['/myid', '/chatid', '/user whoami'];
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 判断命令文本对应的 action 类型
|
|
241
|
+
*/
|
|
242
|
+
function classifyCommandAction(text) {
|
|
243
|
+
if (!text) return 'query';
|
|
244
|
+
const t = text.trim().toLowerCase();
|
|
245
|
+
if (t.startsWith('/sh ') || t.startsWith('/mac ') || t.startsWith('/fix') || t.startsWith('/reset') || t.startsWith('/doctor')) return 'system';
|
|
246
|
+
// /run 执行任务,副作用等级与 system 相同,不应归入 status
|
|
247
|
+
if (t.startsWith('/run ') || t.startsWith('/run\n')) return 'system';
|
|
248
|
+
if (t.startsWith('/agent ') || t.startsWith('/dispatch')) return 'agent';
|
|
249
|
+
if (t.startsWith('/model') || t.startsWith('/reload') || t.startsWith('/budget')) return 'config';
|
|
250
|
+
if (t.startsWith('/user ')) return 'admin_acl';
|
|
251
|
+
if (t.startsWith('/status') || t.startsWith('/tasks')) return 'status';
|
|
252
|
+
if (t.startsWith('/')) return 'system'; // 未知 slash 命令默认需要 system 权限
|
|
253
|
+
return 'query';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── /user 管理命令处理 ───────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 处理 /user 系列命令(仅 admin 可调用)
|
|
260
|
+
* 返回 { handled: boolean, reply: string }
|
|
261
|
+
*/
|
|
262
|
+
function handleUserCommand(text, userCtx) {
|
|
263
|
+
if (!text || !text.startsWith('/user')) return { handled: false };
|
|
264
|
+
|
|
265
|
+
const args = text.trim().split(/\s+/);
|
|
266
|
+
// args[0] = '/user', args[1] = subcommand
|
|
267
|
+
|
|
268
|
+
const sub = args[1];
|
|
269
|
+
|
|
270
|
+
if (!sub || sub === 'help') {
|
|
271
|
+
return {
|
|
272
|
+
handled: true,
|
|
273
|
+
reply: `**用户权限管理**\n\n` +
|
|
274
|
+
`/user list — 列出所有用户\n` +
|
|
275
|
+
`/user add <open_id> <role> [name] — 添加用户 (role: admin/member)\n` +
|
|
276
|
+
`/user role <open_id> <role> — 修改角色\n` +
|
|
277
|
+
`/user grant <open_id> <action> — 赋予 member 额外权限\n` +
|
|
278
|
+
`/user revoke <open_id> <action> — 撤销 member 权限\n` +
|
|
279
|
+
`/user remove <open_id> — 移除用户\n` +
|
|
280
|
+
`/user actions — 列出可用 action\n` +
|
|
281
|
+
`/user whoami — 查看当前身份`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (sub === 'whoami') {
|
|
286
|
+
return {
|
|
287
|
+
handled: true,
|
|
288
|
+
reply: `**你的身份**\n\nID: \`${userCtx.senderId || 'N/A'}\`\n角色: ${userCtx.role}\n名称: ${userCtx.name}\n权限: ${userCtx.allowedActions.join(', ') || '无'}`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (sub === 'actions') {
|
|
293
|
+
const lines = Object.entries(ACTION_GROUPS).map(([k, v]) => `- \`${k}\` — ${v.desc}`);
|
|
294
|
+
return { handled: true, reply: `**可用 Actions**\n\n${lines.join('\n')}` };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (sub === 'list') {
|
|
298
|
+
const data = loadUsers();
|
|
299
|
+
const users = Object.entries(data.users || {});
|
|
300
|
+
if (!users.length) return { handled: true, reply: '暂无用户配置(仅依赖 operator_ids)' };
|
|
301
|
+
const lines = users.map(([uid, info]) => {
|
|
302
|
+
const actions = info.allowed_actions ? ` | ${info.allowed_actions.join(',')}` : '';
|
|
303
|
+
return `- \`${uid}\` [${info.role}] ${info.name || ''}${actions}`;
|
|
304
|
+
});
|
|
305
|
+
return { handled: true, reply: `**用户列表** (default: ${data.default_role || 'stranger'})\n\n${lines.join('\n')}` };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// [S1] 破坏性子命令内部守卫(防止外部绕过调用方校验)
|
|
309
|
+
const ADMIN_REQUIRED_SUBS = new Set(['add', 'role', 'grant', 'revoke', 'remove']);
|
|
310
|
+
if (ADMIN_REQUIRED_SUBS.has(sub) && !userCtx.isAdmin) {
|
|
311
|
+
return { handled: true, reply: '⚠️ 此操作需要 admin 权限,请联系管理员。' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (sub === 'add') {
|
|
315
|
+
// /user add <open_id> <role> [name...]
|
|
316
|
+
const [, , , uid, role, ...nameParts] = args;
|
|
317
|
+
if (!uid || !role) return { handled: true, reply: '用法: /user add <open_id> <role> [name]' };
|
|
318
|
+
// [S2] open_id 格式校验
|
|
319
|
+
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法(应为 10-64 位字母数字下划线)' };
|
|
320
|
+
if (!['admin', 'member', 'stranger'].includes(role)) {
|
|
321
|
+
return { handled: true, reply: '角色必须是 admin / member / stranger' };
|
|
322
|
+
}
|
|
323
|
+
const data = loadUsers();
|
|
324
|
+
data.users = data.users || {};
|
|
325
|
+
data.users[uid] = { role, name: nameParts.join(' ') || uid.slice(-6) };
|
|
326
|
+
saveUsers(data);
|
|
327
|
+
return { handled: true, reply: `✅ 已添加用户 \`${uid}\` → ${role}` };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (sub === 'role') {
|
|
331
|
+
const [, , , uid, role] = args;
|
|
332
|
+
if (!uid || !role) return { handled: true, reply: '用法: /user role <open_id> <role>' };
|
|
333
|
+
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
334
|
+
if (!['admin', 'member', 'stranger'].includes(role)) {
|
|
335
|
+
return { handled: true, reply: '角色必须是 admin / member / stranger' };
|
|
336
|
+
}
|
|
337
|
+
const data = loadUsers();
|
|
338
|
+
data.users = data.users || {};
|
|
339
|
+
if (!data.users[uid]) data.users[uid] = {};
|
|
340
|
+
data.users[uid].role = role;
|
|
341
|
+
saveUsers(data);
|
|
342
|
+
return { handled: true, reply: `✅ 用户 \`${uid}\` 角色已更新为 ${role}` };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (sub === 'grant') {
|
|
346
|
+
const [, , , uid, action] = args;
|
|
347
|
+
if (!uid || !action) return { handled: true, reply: '用法: /user grant <open_id> <action>' };
|
|
348
|
+
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
349
|
+
if (ADMIN_ONLY_ACTIONS.has(action)) {
|
|
350
|
+
return { handled: true, reply: `❌ \`${action}\` 是 admin 专属权限,不可赋予 member` };
|
|
351
|
+
}
|
|
352
|
+
if (!ACTION_GROUPS[action]) {
|
|
353
|
+
return { handled: true, reply: `❌ 未知 action: ${action},用 /user actions 查看可用列表` };
|
|
354
|
+
}
|
|
355
|
+
const data = loadUsers();
|
|
356
|
+
data.users = data.users || {};
|
|
357
|
+
if (!data.users[uid]) data.users[uid] = { role: 'member' };
|
|
358
|
+
const existing = data.users[uid].allowed_actions || [];
|
|
359
|
+
if (!existing.includes(action)) {
|
|
360
|
+
data.users[uid].allowed_actions = [...existing, action];
|
|
361
|
+
saveUsers(data);
|
|
362
|
+
}
|
|
363
|
+
return { handled: true, reply: `✅ 已授权 \`${uid}\` → ${action}` };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (sub === 'revoke') {
|
|
367
|
+
const [, , , uid, action] = args;
|
|
368
|
+
if (!uid || !action) return { handled: true, reply: '用法: /user revoke <open_id> <action>' };
|
|
369
|
+
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
370
|
+
const data = loadUsers();
|
|
371
|
+
const userInfo = (data.users || {})[uid];
|
|
372
|
+
if (!userInfo) return { handled: true, reply: `❌ 未找到用户 \`${uid}\`` };
|
|
373
|
+
userInfo.allowed_actions = (userInfo.allowed_actions || []).filter(a => a !== action);
|
|
374
|
+
saveUsers(data);
|
|
375
|
+
return { handled: true, reply: `✅ 已撤销 \`${uid}\` 的 ${action} 权限` };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (sub === 'remove') {
|
|
379
|
+
const [, , , uid] = args;
|
|
380
|
+
if (!uid) return { handled: true, reply: '用法: /user remove <open_id>' };
|
|
381
|
+
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
382
|
+
const data = loadUsers();
|
|
383
|
+
delete (data.users || {})[uid];
|
|
384
|
+
saveUsers(data);
|
|
385
|
+
return { handled: true, reply: `✅ 已移除用户 \`${uid}\`` };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { handled: true, reply: `未知子命令: ${sub},用 /user help 查看帮助` };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
resolveUserCtx,
|
|
393
|
+
classifyCommandAction,
|
|
394
|
+
handleUserCommand,
|
|
395
|
+
loadUsers,
|
|
396
|
+
saveUsers,
|
|
397
|
+
ACTION_GROUPS,
|
|
398
|
+
PUBLIC_COMMANDS,
|
|
399
|
+
};
|