metame-cli 1.4.18 → 1.4.20
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 +124 -38
- package/index.js +39 -1
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +86 -4
- package/scripts/daemon-agent-commands.js +91 -62
- package/scripts/daemon-agent-tools.js +49 -12
- package/scripts/daemon-bridges.js +26 -6
- package/scripts/daemon-claude-engine.js +111 -32
- package/scripts/daemon-command-router.js +32 -15
- package/scripts/daemon-default.yaml +18 -0
- package/scripts/daemon-exec-commands.js +6 -12
- package/scripts/daemon-file-browser.js +6 -5
- package/scripts/daemon-runtime-lifecycle.js +19 -5
- package/scripts/daemon-session-store.js +176 -41
- package/scripts/daemon-task-scheduler.js +30 -29
- package/scripts/daemon-user-acl.js +399 -0
- package/scripts/daemon.js +43 -6
- package/scripts/distill.js +11 -12
- package/scripts/memory-gc.js +239 -0
- package/scripts/memory-index.js +103 -0
- package/scripts/memory-nightly-reflect.js +299 -0
- package/scripts/memory-write.js +192 -0
- package/scripts/memory.js +144 -6
- package/scripts/schema.js +30 -9
- package/scripts/self-reflect.js +121 -5
- package/scripts/session-analytics.js +9 -10
- package/scripts/task-board.js +9 -3
- package/scripts/telegram-adapter.js +77 -9
|
@@ -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
|
+
};
|
package/scripts/daemon.js
CHANGED
|
@@ -248,11 +248,11 @@ function restoreConfig() {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
writeConfigSafe(bakCfg);
|
|
251
|
-
config = loadConfig();
|
|
251
|
+
config = loadConfig(); // eslint-disable-line no-undef -- config is declared in main() closure
|
|
252
252
|
return true;
|
|
253
253
|
} catch {
|
|
254
254
|
fs.copyFileSync(bak, CONFIG_FILE);
|
|
255
|
-
config = loadConfig();
|
|
255
|
+
config = loadConfig(); // eslint-disable-line no-undef
|
|
256
256
|
return true;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
@@ -1218,6 +1218,7 @@ const {
|
|
|
1218
1218
|
getSessionFileMtime,
|
|
1219
1219
|
sessionLabel,
|
|
1220
1220
|
sessionRichLabel,
|
|
1221
|
+
getSessionRecentContext,
|
|
1221
1222
|
buildSessionCardElements,
|
|
1222
1223
|
listProjectDirs,
|
|
1223
1224
|
getSession,
|
|
@@ -1225,6 +1226,7 @@ const {
|
|
|
1225
1226
|
getSessionName,
|
|
1226
1227
|
writeSessionName,
|
|
1227
1228
|
markSessionStarted,
|
|
1229
|
+
watchSessionFiles,
|
|
1228
1230
|
} = createSessionStore({
|
|
1229
1231
|
fs,
|
|
1230
1232
|
path,
|
|
@@ -1236,6 +1238,8 @@ const {
|
|
|
1236
1238
|
cpExtractTimestamp,
|
|
1237
1239
|
});
|
|
1238
1240
|
|
|
1241
|
+
watchSessionFiles(); // 热加载:手机端新建 session 后桌面无需重启
|
|
1242
|
+
|
|
1239
1243
|
// Active Claude processes per chat (for /stop)
|
|
1240
1244
|
const activeProcesses = new Map(); // chatId -> { child, aborted }
|
|
1241
1245
|
|
|
@@ -1348,6 +1352,11 @@ const pendingBinds = new Map(); // chatId -> agentName
|
|
|
1348
1352
|
// chatId -> { step: 'dir'|'name'|'desc', dir: string, name: string }
|
|
1349
1353
|
const pendingAgentFlows = new Map();
|
|
1350
1354
|
|
|
1355
|
+
// Pending activation: after creating an agent with skipChatBinding=true,
|
|
1356
|
+
// store here so any new unbound group can activate it with /activate
|
|
1357
|
+
// { agentKey, agentName, cwd, createdAt }
|
|
1358
|
+
const pendingActivations = new Map(); // key: agentKey -> activation record
|
|
1359
|
+
|
|
1351
1360
|
const { handleAdminCommand } = createAdminCommandHandler({
|
|
1352
1361
|
fs,
|
|
1353
1362
|
yaml,
|
|
@@ -1367,6 +1376,10 @@ const { handleAdminCommand } = createAdminCommandHandler({
|
|
|
1367
1376
|
skillEvolution,
|
|
1368
1377
|
taskBoard,
|
|
1369
1378
|
taskEnvelope,
|
|
1379
|
+
getActiveProcesses: () => activeProcesses,
|
|
1380
|
+
getMessageQueue: () => messageQueue,
|
|
1381
|
+
loadState,
|
|
1382
|
+
saveState,
|
|
1370
1383
|
});
|
|
1371
1384
|
|
|
1372
1385
|
const { handleSessionCommand } = createSessionCommandHandler({
|
|
@@ -1485,8 +1498,10 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1485
1498
|
sessionLabel,
|
|
1486
1499
|
loadSessionTags,
|
|
1487
1500
|
sessionRichLabel,
|
|
1501
|
+
getSessionRecentContext,
|
|
1488
1502
|
pendingBinds,
|
|
1489
1503
|
pendingAgentFlows,
|
|
1504
|
+
pendingActivations,
|
|
1490
1505
|
doBindAgent,
|
|
1491
1506
|
mergeAgentRole,
|
|
1492
1507
|
agentTools,
|
|
@@ -1564,6 +1579,7 @@ const { handleCommand } = createCommandRouter({
|
|
|
1564
1579
|
log,
|
|
1565
1580
|
agentTools,
|
|
1566
1581
|
pendingAgentFlows,
|
|
1582
|
+
pendingActivations,
|
|
1567
1583
|
agentFlowTtlMs: getAgentFlowTtlMs,
|
|
1568
1584
|
});
|
|
1569
1585
|
|
|
@@ -1584,6 +1600,7 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
|
1584
1600
|
saveState,
|
|
1585
1601
|
getSession,
|
|
1586
1602
|
handleCommand,
|
|
1603
|
+
pendingActivations,
|
|
1587
1604
|
});
|
|
1588
1605
|
|
|
1589
1606
|
const { killExistingDaemon, writePid, cleanPid } = createPidManager({
|
|
@@ -1735,7 +1752,10 @@ async function main() {
|
|
|
1735
1752
|
setConfig: (next) => { config = next; },
|
|
1736
1753
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
1737
1754
|
setHeartbeatTimer: (next) => { heartbeatTimer = next; },
|
|
1738
|
-
onRestartRequested: () =>
|
|
1755
|
+
onRestartRequested: () => {
|
|
1756
|
+
// Reuse full shutdown logic (kill child processes, clean PID, stop bridges)
|
|
1757
|
+
shutdown().catch(() => process.exit(0));
|
|
1758
|
+
},
|
|
1739
1759
|
});
|
|
1740
1760
|
// Expose reloadConfig to handleCommand via closure
|
|
1741
1761
|
global._metameReload = runtimeWatchers.reloadConfig;
|
|
@@ -1749,9 +1769,26 @@ async function main() {
|
|
|
1749
1769
|
await sleep(1500); // Let polling settle
|
|
1750
1770
|
await adminNotifyFn('✅ Daemon ready.').catch(() => { });
|
|
1751
1771
|
|
|
1772
|
+
// Notify active users before restart/shutdown
|
|
1773
|
+
async function notifyActiveUsers(reason) {
|
|
1774
|
+
if (activeProcesses.size === 0) return;
|
|
1775
|
+
const bots = [];
|
|
1776
|
+
if (feishuBridge && feishuBridge.bot) bots.push(feishuBridge.bot);
|
|
1777
|
+
if (telegramBridge && telegramBridge.bot) bots.push(telegramBridge.bot);
|
|
1778
|
+
if (bots.length === 0) return;
|
|
1779
|
+
const notifs = [];
|
|
1780
|
+
for (const [cid] of activeProcesses) {
|
|
1781
|
+
for (const bot of bots) {
|
|
1782
|
+
notifs.push(bot.sendMessage(cid, `⚠️ 系统正在重启(${reason}),任务已中断,请重新发送指令。`).catch(() => {}));
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
await Promise.race([Promise.all(notifs), new Promise(r => setTimeout(r, 3000))]);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1752
1788
|
// Graceful shutdown
|
|
1753
|
-
const shutdown = () => {
|
|
1789
|
+
const shutdown = async () => {
|
|
1754
1790
|
log('INFO', 'Daemon shutting down...');
|
|
1791
|
+
await notifyActiveUsers('关闭').catch(() => {});
|
|
1755
1792
|
runtimeWatchers.stop();
|
|
1756
1793
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1757
1794
|
if (dispatchSocket) try { dispatchSocket.close(); } catch { }
|
|
@@ -1774,8 +1811,8 @@ async function main() {
|
|
|
1774
1811
|
process.exit(0);
|
|
1775
1812
|
};
|
|
1776
1813
|
|
|
1777
|
-
process.on('SIGTERM', shutdown);
|
|
1778
|
-
process.on('SIGINT', shutdown);
|
|
1814
|
+
process.on('SIGTERM', () => { shutdown().catch(() => process.exit(0)); });
|
|
1815
|
+
process.on('SIGINT', () => { shutdown().catch(() => process.exit(0)); });
|
|
1779
1816
|
|
|
1780
1817
|
// Keep alive
|
|
1781
1818
|
log('INFO', 'Daemon running. Send SIGTERM to stop.');
|
package/scripts/distill.js
CHANGED
|
@@ -636,15 +636,6 @@ function strategicMerge(profile, updates, lockedKeys, pendingTraits, confidenceM
|
|
|
636
636
|
break;
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
-
case 'T4':
|
|
640
|
-
setNested(result, key, value);
|
|
641
|
-
|
|
642
|
-
// Auto-set focus_since when focus changes
|
|
643
|
-
if (key === 'context.focus') {
|
|
644
|
-
setNested(result, 'context.focus_since', new Date().toISOString().slice(0, 10));
|
|
645
|
-
}
|
|
646
|
-
break;
|
|
647
|
-
|
|
648
639
|
case 'T5':
|
|
649
640
|
setNested(result, key, value);
|
|
650
641
|
break;
|
|
@@ -717,9 +708,17 @@ function filterBySchema(obj, parentKey = '') {
|
|
|
717
708
|
const value = obj[key];
|
|
718
709
|
|
|
719
710
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
711
|
+
// map type: accept entire object as-is, do not recurse into sub-keys
|
|
712
|
+
const def = getDefinition(fullKey);
|
|
713
|
+
if (def && def.type === 'map') {
|
|
714
|
+
if (hasKey(fullKey) && !isLocked(fullKey)) {
|
|
715
|
+
result[key] = value;
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
const nested = filterBySchema(value, fullKey);
|
|
719
|
+
if (Object.keys(nested).length > 0) {
|
|
720
|
+
result[key] = nested;
|
|
721
|
+
}
|
|
723
722
|
}
|
|
724
723
|
} else {
|
|
725
724
|
// Check schema whitelist — allow if key exists and is not locked
|