metame-cli 1.4.34 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -94
- package/index.js +312 -57
- package/package.json +8 -4
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +328 -28
- package/scripts/daemon-agent-commands.js +145 -6
- package/scripts/daemon-agent-tools.js +163 -7
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +849 -358
- package/scripts/daemon-command-router.js +31 -10
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +328 -0
- package/scripts/daemon-exec-commands.js +15 -7
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +129 -5
- package/scripts/daemon-session-commands.js +60 -25
- package/scripts/daemon-session-store.js +121 -13
- package/scripts/daemon-task-scheduler.js +129 -49
- package/scripts/daemon-user-acl.js +35 -9
- package/scripts/daemon.js +268 -33
- package/scripts/distill.js +327 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +155 -0
- package/scripts/docs/pointer-map.md +110 -0
- package/scripts/feishu-adapter.js +42 -13
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +105 -6
- package/scripts/memory-nightly-reflect.js +199 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +24 -0
- package/scripts/providers.js +182 -22
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/telegram-adapter.js +12 -8
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
createAgentId,
|
|
5
|
+
ensureClaudeMdSoulImport,
|
|
6
|
+
ensureAgentLayer,
|
|
7
|
+
repairAgentLayer,
|
|
8
|
+
refreshMemorySnapshot,
|
|
9
|
+
buildMemorySnapshotContent,
|
|
10
|
+
normalizeEngine: normalizeLayerEngine,
|
|
11
|
+
} = require('./agent-layer');
|
|
12
|
+
|
|
3
13
|
function createAgentTools(deps) {
|
|
4
14
|
const {
|
|
5
15
|
fs,
|
|
@@ -31,13 +41,35 @@ function createAgentTools(deps) {
|
|
|
31
41
|
return (String(agentName || '').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase() || String(chatId));
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
function ensureAgentMetadata({ cfg, projectKey, project, safeName, resolvedDir, engine }) {
|
|
45
|
+
const agentId = String(project && project.agent_id ? project.agent_id : createAgentId({
|
|
46
|
+
projectKey,
|
|
47
|
+
agentName: safeName,
|
|
48
|
+
cwd: resolvedDir,
|
|
49
|
+
}));
|
|
50
|
+
const ensured = ensureAgentLayer({
|
|
51
|
+
agentId,
|
|
52
|
+
projectKey,
|
|
53
|
+
agentName: safeName,
|
|
54
|
+
workspaceDir: resolvedDir,
|
|
55
|
+
engine: normalizeLayerEngine(engine || (project && project.engine)),
|
|
56
|
+
aliases: [safeName],
|
|
57
|
+
homeDir: HOME,
|
|
58
|
+
});
|
|
59
|
+
cfg.projects[projectKey] = {
|
|
60
|
+
...cfg.projects[projectKey],
|
|
61
|
+
agent_id: ensured.agentId,
|
|
62
|
+
};
|
|
63
|
+
return ensured;
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
function ensureAdapterConfig(cfg, adapterKey) {
|
|
35
67
|
if (!cfg[adapterKey]) cfg[adapterKey] = {};
|
|
36
68
|
if (!cfg[adapterKey].allowed_chat_ids) cfg[adapterKey].allowed_chat_ids = [];
|
|
37
69
|
if (!cfg[adapterKey].chat_agent_map) cfg[adapterKey].chat_agent_map = {};
|
|
38
70
|
}
|
|
39
71
|
|
|
40
|
-
async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false } = {}) {
|
|
72
|
+
async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false, engine = null } = {}) {
|
|
41
73
|
try {
|
|
42
74
|
const safeName = sanitizeText(agentName, 120);
|
|
43
75
|
if (!safeName) return { ok: false, error: 'agentName is required' };
|
|
@@ -49,6 +81,7 @@ function createAgentTools(deps) {
|
|
|
49
81
|
|
|
50
82
|
const projectKey = toProjectKey(safeName, chatId);
|
|
51
83
|
let resolvedDir = resolveWorkspaceDir(workspaceDir);
|
|
84
|
+
const normalizedEngine = engine ? normalizeLayerEngine(engine) : null;
|
|
52
85
|
|
|
53
86
|
if (!resolvedDir) {
|
|
54
87
|
const existing = cfg.projects[projectKey];
|
|
@@ -80,7 +113,13 @@ function createAgentTools(deps) {
|
|
|
80
113
|
cfg[adapterKey].chat_agent_map[String(chatId)] = projectKey;
|
|
81
114
|
const existed = !!cfg.projects[projectKey];
|
|
82
115
|
if (!existed) {
|
|
83
|
-
cfg.projects[projectKey] = {
|
|
116
|
+
cfg.projects[projectKey] = {
|
|
117
|
+
name: safeName,
|
|
118
|
+
cwd: resolvedDir,
|
|
119
|
+
nicknames: [safeName],
|
|
120
|
+
agent_id: createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
|
|
121
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
122
|
+
};
|
|
84
123
|
} else {
|
|
85
124
|
const nicknames = Array.isArray(cfg.projects[projectKey].nicknames)
|
|
86
125
|
? cfg.projects[projectKey].nicknames
|
|
@@ -91,9 +130,20 @@ function createAgentTools(deps) {
|
|
|
91
130
|
name: safeName,
|
|
92
131
|
cwd: resolvedDir,
|
|
93
132
|
nicknames,
|
|
133
|
+
agent_id: cfg.projects[projectKey].agent_id || createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
|
|
134
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
94
135
|
};
|
|
95
136
|
}
|
|
96
137
|
|
|
138
|
+
const agentLayer = ensureAgentMetadata({
|
|
139
|
+
cfg,
|
|
140
|
+
projectKey,
|
|
141
|
+
project: cfg.projects[projectKey],
|
|
142
|
+
safeName,
|
|
143
|
+
resolvedDir,
|
|
144
|
+
engine: normalizedEngine,
|
|
145
|
+
});
|
|
146
|
+
|
|
97
147
|
writeConfigSafe(cfg);
|
|
98
148
|
backupConfig();
|
|
99
149
|
|
|
@@ -106,6 +156,7 @@ function createAgentTools(deps) {
|
|
|
106
156
|
cwd: resolvedDir,
|
|
107
157
|
isNewProject: !existed,
|
|
108
158
|
project: cfg.projects[projectKey],
|
|
159
|
+
agent: agentLayer,
|
|
109
160
|
},
|
|
110
161
|
};
|
|
111
162
|
} catch (e) {
|
|
@@ -127,6 +178,7 @@ function createAgentTools(deps) {
|
|
|
127
178
|
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
128
179
|
if (!fs.existsSync(claudeMdPath)) {
|
|
129
180
|
fs.writeFileSync(claudeMdPath, `## Agent 角色\n\n${safeDelta}\n`, 'utf8');
|
|
181
|
+
try { ensureClaudeMdSoulImport(cwd); } catch { /* non-critical */ }
|
|
130
182
|
return { ok: true, data: { created: true, merged: false, path: claudeMdPath } };
|
|
131
183
|
}
|
|
132
184
|
|
|
@@ -169,14 +221,16 @@ ${safeDelta}
|
|
|
169
221
|
cleanOutput = cleanOutput.replace(/^```[a-zA-Z]*\n/, '').replace(/\n```$/, '');
|
|
170
222
|
}
|
|
171
223
|
fs.writeFileSync(claudeMdPath, cleanOutput, 'utf8');
|
|
224
|
+
try { ensureClaudeMdSoulImport(cwd); } catch { /* non-critical */ }
|
|
172
225
|
return { ok: true, data: { created: false, merged: true, path: claudeMdPath } };
|
|
173
226
|
} catch (e) {
|
|
174
227
|
return { ok: false, error: e.message };
|
|
175
228
|
}
|
|
176
229
|
}
|
|
177
230
|
|
|
178
|
-
async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false } = {}) {
|
|
231
|
+
async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false, engine = null } = {}) {
|
|
179
232
|
let bindData;
|
|
233
|
+
const normalizedEngine = engine ? normalizeLayerEngine(engine) : null;
|
|
180
234
|
|
|
181
235
|
if (skipChatBinding) {
|
|
182
236
|
// Create the project entry without touching chat_agent_map
|
|
@@ -192,19 +246,40 @@ ${safeDelta}
|
|
|
192
246
|
const projectKey = toProjectKey(safeName, chatId);
|
|
193
247
|
const existed = !!cfg.projects[projectKey];
|
|
194
248
|
if (!existed) {
|
|
195
|
-
cfg.projects[projectKey] = {
|
|
196
|
-
|
|
197
|
-
|
|
249
|
+
cfg.projects[projectKey] = {
|
|
250
|
+
name: safeName,
|
|
251
|
+
cwd: resolvedDir,
|
|
252
|
+
nicknames: [safeName],
|
|
253
|
+
agent_id: createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
|
|
254
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
255
|
+
};
|
|
256
|
+
} else {
|
|
257
|
+
cfg.projects[projectKey] = {
|
|
258
|
+
...cfg.projects[projectKey],
|
|
259
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
260
|
+
agent_id: cfg.projects[projectKey].agent_id || createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
|
|
261
|
+
};
|
|
198
262
|
}
|
|
263
|
+
const agentLayer = ensureAgentMetadata({
|
|
264
|
+
cfg,
|
|
265
|
+
projectKey,
|
|
266
|
+
project: cfg.projects[projectKey],
|
|
267
|
+
safeName,
|
|
268
|
+
resolvedDir,
|
|
269
|
+
engine: normalizedEngine,
|
|
270
|
+
});
|
|
271
|
+
writeConfigSafe(cfg);
|
|
272
|
+
backupConfig();
|
|
199
273
|
bindData = {
|
|
200
274
|
projectKey,
|
|
201
275
|
cwd: resolvedDir,
|
|
202
276
|
isNewProject: !existed,
|
|
203
277
|
chatId: null, // not bound to any chat
|
|
204
278
|
project: cfg.projects[projectKey],
|
|
279
|
+
agent: agentLayer,
|
|
205
280
|
};
|
|
206
281
|
} else {
|
|
207
|
-
const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir);
|
|
282
|
+
const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir, { engine: normalizedEngine });
|
|
208
283
|
if (!bindResult.ok) return bindResult;
|
|
209
284
|
bindData = bindResult.data;
|
|
210
285
|
}
|
|
@@ -223,6 +298,10 @@ ${safeDelta}
|
|
|
223
298
|
};
|
|
224
299
|
}
|
|
225
300
|
|
|
301
|
+
// editAgentRoleDefinition may have just created CLAUDE.md for the first time.
|
|
302
|
+
// Ensure @SOUL.md import is present so Claude auto-loads soul on every future session.
|
|
303
|
+
try { ensureClaudeMdSoulImport(bindData.cwd); } catch { /* non-critical */ }
|
|
304
|
+
|
|
226
305
|
return {
|
|
227
306
|
ok: true,
|
|
228
307
|
data: { ...bindData, role: roleResult.data },
|
|
@@ -281,12 +360,89 @@ ${safeDelta}
|
|
|
281
360
|
}
|
|
282
361
|
}
|
|
283
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Lazy-migration repair: given a workspace directory, ensure the agent soul layer
|
|
365
|
+
* (~/.metame/agents/<id>/, SOUL.md, MEMORY.md) exists and is wired up.
|
|
366
|
+
* Persists agent_id back to daemon.yaml if it was missing.
|
|
367
|
+
* Safe to call repeatedly — idempotent.
|
|
368
|
+
*/
|
|
369
|
+
async function repairAgentSoul(workspaceDir) {
|
|
370
|
+
try {
|
|
371
|
+
const cwd = resolveWorkspaceDir(workspaceDir);
|
|
372
|
+
if (!cwd) return { ok: false, error: 'workspaceDir is required' };
|
|
373
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
374
|
+
return { ok: false, error: `workspaceDir not found: ${cwd}` };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const cfg = loadConfig();
|
|
378
|
+
let projectKey = null;
|
|
379
|
+
let project = null;
|
|
380
|
+
for (const [key, p] of Object.entries(cfg.projects || {})) {
|
|
381
|
+
if (!p || !p.cwd) continue;
|
|
382
|
+
const pCwd = normalizeCwd ? normalizeCwd(p.cwd) : p.cwd;
|
|
383
|
+
const r1 = path.resolve(pCwd);
|
|
384
|
+
const r2 = path.resolve(cwd);
|
|
385
|
+
const isMatch = process.platform === 'win32'
|
|
386
|
+
? r1.toLowerCase() === r2.toLowerCase()
|
|
387
|
+
: r1 === r2;
|
|
388
|
+
if (isMatch) {
|
|
389
|
+
projectKey = key;
|
|
390
|
+
project = p;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!projectKey) {
|
|
395
|
+
return { ok: false, error: `No registered agent found for: ${cwd}. Run /agent bind first.` };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const ensured = repairAgentLayer(projectKey, project, HOME);
|
|
399
|
+
if (!ensured) return { ok: false, error: 'repairAgentLayer returned null' };
|
|
400
|
+
|
|
401
|
+
// Persist agent_id back to config if it was missing
|
|
402
|
+
if (!project.agent_id) {
|
|
403
|
+
cfg.projects[projectKey] = { ...cfg.projects[projectKey], agent_id: ensured.agentId };
|
|
404
|
+
writeConfigSafe(cfg);
|
|
405
|
+
backupConfig();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
data: {
|
|
411
|
+
projectKey,
|
|
412
|
+
agentId: ensured.agentId,
|
|
413
|
+
views: ensured.views
|
|
414
|
+
? Object.fromEntries(Object.entries(ensured.views).map(([k, v]) => [k, v.mode]))
|
|
415
|
+
: null,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
} catch (e) {
|
|
419
|
+
return { ok: false, error: e.message };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Refresh memory-snapshot.md from fresh session+fact data.
|
|
425
|
+
* Called by the engine after first-message of a session; also callable directly.
|
|
426
|
+
*/
|
|
427
|
+
async function updateMemorySnapshot(agentId, sessions = [], facts = []) {
|
|
428
|
+
try {
|
|
429
|
+
if (!agentId) return { ok: false, error: 'agentId is required' };
|
|
430
|
+
const content = buildMemorySnapshotContent(sessions, facts);
|
|
431
|
+
const ok = refreshMemorySnapshot(agentId, content, HOME);
|
|
432
|
+
return { ok, error: ok ? null : 'agent directory not found or not yet created' };
|
|
433
|
+
} catch (e) {
|
|
434
|
+
return { ok: false, error: e.message };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
284
438
|
return {
|
|
285
439
|
bindAgentToChat,
|
|
286
440
|
createNewWorkspaceAgent,
|
|
287
441
|
editAgentRoleDefinition,
|
|
288
442
|
listAllAgents,
|
|
289
443
|
unbindCurrentAgent,
|
|
444
|
+
repairAgentSoul,
|
|
445
|
+
updateMemorySnapshot,
|
|
290
446
|
};
|
|
291
447
|
}
|
|
292
448
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
let userAcl = null;
|
|
4
|
+
try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
|
|
5
|
+
|
|
3
6
|
function createBridgeStarter(deps) {
|
|
4
7
|
const {
|
|
5
8
|
fs,
|
|
@@ -15,6 +18,52 @@ function createBridgeStarter(deps) {
|
|
|
15
18
|
pendingActivations, // optional — used to show smart activation hint
|
|
16
19
|
} = deps;
|
|
17
20
|
|
|
21
|
+
async function sendAclReply(bot, chatId, text) {
|
|
22
|
+
if (!text) return;
|
|
23
|
+
try {
|
|
24
|
+
if (bot.sendMarkdown) await bot.sendMarkdown(chatId, text);
|
|
25
|
+
else await bot.sendMessage(chatId, text.replace(/[*_`]/g, ''));
|
|
26
|
+
} catch { /* non-fatal */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSenderId(senderId) {
|
|
30
|
+
if (senderId === undefined || senderId === null) return null;
|
|
31
|
+
const text = String(senderId).trim();
|
|
32
|
+
return text || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl }) {
|
|
36
|
+
const trimmed = String(text || '').trim();
|
|
37
|
+
const normalizedSenderId = normalizeSenderId(senderId);
|
|
38
|
+
if (!trimmed || bypassAcl || !userAcl) {
|
|
39
|
+
return { blocked: false, readOnly: false, senderId: normalizedSenderId };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let userCtx;
|
|
43
|
+
try {
|
|
44
|
+
userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {});
|
|
45
|
+
} catch {
|
|
46
|
+
return { blocked: false, readOnly: false, senderId: normalizedSenderId };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userCmd = userAcl.handleUserCommand(trimmed, userCtx);
|
|
50
|
+
if (userCmd && userCmd.handled) {
|
|
51
|
+
await sendAclReply(bot, chatId, userCmd.reply);
|
|
52
|
+
return { blocked: true, readOnly: !!userCtx.readOnly, senderId: normalizedSenderId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const publicCmds = Array.isArray(userAcl.PUBLIC_COMMANDS) ? userAcl.PUBLIC_COMMANDS : [];
|
|
56
|
+
const isPublic = publicCmds.includes(trimmed.toLowerCase());
|
|
57
|
+
const action = userAcl.classifyCommandAction(trimmed);
|
|
58
|
+
const allowed = isPublic || (typeof userCtx.can === 'function' && userCtx.can(action));
|
|
59
|
+
if (!allowed) {
|
|
60
|
+
await sendAclReply(bot, chatId, `⚠️ 当前权限不足(角色: ${userCtx.role})\n命令类型: ${action}\n请联系管理员授权。`);
|
|
61
|
+
return { blocked: true, readOnly: true, senderId: normalizedSenderId };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { blocked: false, readOnly: !!userCtx.readOnly, senderId: normalizedSenderId };
|
|
65
|
+
}
|
|
66
|
+
|
|
18
67
|
// Returns the best pending activation for a given chatId (excludes self-created)
|
|
19
68
|
function getPendingActivationForChat(chatId) {
|
|
20
69
|
if (!pendingActivations || pendingActivations.size === 0) return null;
|
|
@@ -67,12 +116,26 @@ function createBridgeStarter(deps) {
|
|
|
67
116
|
if (update.callback_query) {
|
|
68
117
|
const cb = update.callback_query;
|
|
69
118
|
const chatId = cb.message && cb.message.chat.id;
|
|
119
|
+
const senderId = cb.from && cb.from.id ? String(cb.from.id) : null;
|
|
70
120
|
bot.answerCallback(cb.id).catch(() => { });
|
|
71
121
|
if (chatId && cb.data) {
|
|
72
122
|
const liveCfg = loadConfig();
|
|
73
123
|
const allowedIds = (liveCfg.telegram && liveCfg.telegram.allowed_chat_ids) || [];
|
|
74
124
|
if (!allowedIds.includes(chatId)) continue;
|
|
75
|
-
|
|
125
|
+
const isBindCmd = cb.data.startsWith('/agent bind')
|
|
126
|
+
|| cb.data.startsWith('/agent-bind-dir')
|
|
127
|
+
|| cb.data.startsWith('/browse bind')
|
|
128
|
+
|| cb.data === '/activate';
|
|
129
|
+
const acl = await applyUserAcl({
|
|
130
|
+
bot,
|
|
131
|
+
chatId,
|
|
132
|
+
text: cb.data,
|
|
133
|
+
config: liveCfg,
|
|
134
|
+
senderId,
|
|
135
|
+
bypassAcl: !allowedIds.includes(chatId) && !!isBindCmd,
|
|
136
|
+
});
|
|
137
|
+
if (acl.blocked) continue;
|
|
138
|
+
handleCommand(bot, chatId, cb.data, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
|
|
76
139
|
log('ERROR', `Telegram callback handler error: ${e.message}`);
|
|
77
140
|
});
|
|
78
141
|
}
|
|
@@ -83,6 +146,7 @@ function createBridgeStarter(deps) {
|
|
|
83
146
|
|
|
84
147
|
const msg = update.message;
|
|
85
148
|
const chatId = msg.chat.id;
|
|
149
|
+
const senderId = msg.from && msg.from.id ? String(msg.from.id) : null;
|
|
86
150
|
|
|
87
151
|
const liveCfg = loadConfig();
|
|
88
152
|
const allowedIds = (liveCfg.telegram && liveCfg.telegram.allowed_chat_ids) || [];
|
|
@@ -93,7 +157,8 @@ function createBridgeStarter(deps) {
|
|
|
93
157
|
|| trimmedText.startsWith('/browse bind')
|
|
94
158
|
|| trimmedText === '/activate'
|
|
95
159
|
);
|
|
96
|
-
|
|
160
|
+
const isAllowedChat = allowedIds.includes(chatId);
|
|
161
|
+
if (!isAllowedChat && !isBindCmd) {
|
|
97
162
|
log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
|
|
98
163
|
bot.sendMessage(chatId, unauthorizedMsg(chatId)).catch(() => {});
|
|
99
164
|
continue;
|
|
@@ -108,6 +173,15 @@ function createBridgeStarter(deps) {
|
|
|
108
173
|
const fileId = msg.document ? msg.document.file_id : msg.photo[msg.photo.length - 1].file_id;
|
|
109
174
|
const fileName = msg.document ? msg.document.file_name : `photo_${Date.now()}.jpg`;
|
|
110
175
|
const caption = msg.caption || '';
|
|
176
|
+
const acl = await applyUserAcl({
|
|
177
|
+
bot,
|
|
178
|
+
chatId,
|
|
179
|
+
text: caption || '[file-upload]',
|
|
180
|
+
config: liveCfg,
|
|
181
|
+
senderId,
|
|
182
|
+
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
183
|
+
});
|
|
184
|
+
if (acl.blocked) continue;
|
|
111
185
|
|
|
112
186
|
const session = getSession(chatId);
|
|
113
187
|
const cwd = session?.cwd || HOME;
|
|
@@ -123,7 +197,7 @@ function createBridgeStarter(deps) {
|
|
|
123
197
|
? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
|
|
124
198
|
: `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
|
|
125
199
|
|
|
126
|
-
handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName).catch(e => {
|
|
200
|
+
handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
|
|
127
201
|
log('ERROR', `Telegram file handler error: ${e.message}`);
|
|
128
202
|
});
|
|
129
203
|
} catch (err) {
|
|
@@ -134,7 +208,17 @@ function createBridgeStarter(deps) {
|
|
|
134
208
|
}
|
|
135
209
|
|
|
136
210
|
if (msg.text) {
|
|
137
|
-
|
|
211
|
+
const text = msg.text.trim();
|
|
212
|
+
const acl = await applyUserAcl({
|
|
213
|
+
bot,
|
|
214
|
+
chatId,
|
|
215
|
+
text,
|
|
216
|
+
config: liveCfg,
|
|
217
|
+
senderId,
|
|
218
|
+
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
219
|
+
});
|
|
220
|
+
if (acl.blocked) continue;
|
|
221
|
+
handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
|
|
138
222
|
log('ERROR', `Telegram handler error: ${e.message}`);
|
|
139
223
|
});
|
|
140
224
|
}
|
|
@@ -182,27 +266,24 @@ function createBridgeStarter(deps) {
|
|
|
182
266
|
|| trimmedText.startsWith('/browse bind')
|
|
183
267
|
|| trimmedText === '/activate'
|
|
184
268
|
);
|
|
185
|
-
|
|
269
|
+
const isAllowedChat = allowedIds.includes(chatId);
|
|
270
|
+
if (!isAllowedChat && !isBindCmd) {
|
|
186
271
|
log('WARN', `Feishu: rejected message from ${chatId}`);
|
|
187
272
|
const msg = unauthorizedMsg(chatId);
|
|
188
273
|
(bot.sendMarkdown ? bot.sendMarkdown(chatId, msg) : bot.sendMessage(chatId, msg)).catch(() => {});
|
|
189
274
|
return;
|
|
190
275
|
}
|
|
191
276
|
|
|
192
|
-
const operatorIds = (liveCfg.feishu && liveCfg.feishu.operator_ids) || [];
|
|
193
|
-
if (operatorIds.length > 0 && senderId && !operatorIds.includes(senderId) && !isBindCmd) {
|
|
194
|
-
log('INFO', `Feishu: read-only message from non-operator ${senderId} in ${chatId}: ${(text || '').slice(0, 50)}`);
|
|
195
|
-
if (text && text.startsWith('/')) {
|
|
196
|
-
await (bot.sendMarkdown ? bot.sendMarkdown(chatId, '⚠️ 该操作需要授权,请联系管理员。') : bot.sendMessage(chatId, '⚠️ 该操作需要授权,请联系管理员。'));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (text) {
|
|
200
|
-
await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, senderId, true);
|
|
201
|
-
}
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
277
|
if (fileInfo && fileInfo.fileKey) {
|
|
278
|
+
const acl = await applyUserAcl({
|
|
279
|
+
bot,
|
|
280
|
+
chatId,
|
|
281
|
+
text: text || '[file-upload]',
|
|
282
|
+
config: liveCfg,
|
|
283
|
+
senderId,
|
|
284
|
+
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
285
|
+
});
|
|
286
|
+
if (acl.blocked) return;
|
|
206
287
|
log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
|
|
207
288
|
const session = getSession(chatId);
|
|
208
289
|
const cwd = session?.cwd || HOME;
|
|
@@ -218,7 +299,7 @@ function createBridgeStarter(deps) {
|
|
|
218
299
|
? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
|
|
219
300
|
: `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
|
|
220
301
|
|
|
221
|
-
await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName);
|
|
302
|
+
await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
222
303
|
} catch (err) {
|
|
223
304
|
log('ERROR', `Feishu file download failed: ${err.message}`);
|
|
224
305
|
await bot.sendMessage(chatId, `❌ Download failed: ${err.message}`);
|
|
@@ -227,6 +308,15 @@ function createBridgeStarter(deps) {
|
|
|
227
308
|
}
|
|
228
309
|
|
|
229
310
|
if (text) {
|
|
311
|
+
const acl = await applyUserAcl({
|
|
312
|
+
bot,
|
|
313
|
+
chatId,
|
|
314
|
+
text,
|
|
315
|
+
config: liveCfg,
|
|
316
|
+
senderId,
|
|
317
|
+
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
318
|
+
});
|
|
319
|
+
if (acl.blocked) return;
|
|
230
320
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|
|
231
321
|
const parentId = event?.message?.parent_id;
|
|
232
322
|
if (parentId) {
|
|
@@ -238,7 +328,7 @@ function createBridgeStarter(deps) {
|
|
|
238
328
|
log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
239
329
|
}
|
|
240
330
|
}
|
|
241
|
-
await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, senderId);
|
|
331
|
+
await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
242
332
|
}
|
|
243
333
|
});
|
|
244
334
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
function createCheckpointUtils(deps) {
|
|
4
|
-
const { execSync, path, log } = deps;
|
|
4
|
+
const { execSync, execFile, path, log } = deps;
|
|
5
|
+
const { promisify } = require('util');
|
|
6
|
+
const execFileAsync = execFile ? promisify(execFile) : null;
|
|
5
7
|
|
|
6
8
|
const CHECKPOINT_PREFIX = '[metame-checkpoint]';
|
|
7
9
|
const MAX_CHECKPOINTS = 20;
|
|
@@ -35,19 +37,22 @@ function createCheckpointUtils(deps) {
|
|
|
35
37
|
return message.replace(CHECKPOINT_PREFIX, '').trim();
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// On Windows, git.exe is a console app — windowsHide:true prevents flash
|
|
41
|
+
const WIN_HIDE = process.platform === 'win32' ? { windowsHide: true } : {};
|
|
42
|
+
|
|
38
43
|
function gitCheckpoint(cwd, label) {
|
|
39
44
|
try {
|
|
40
|
-
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
|
|
41
|
-
execSync('git add -A', { cwd, stdio: 'ignore', timeout: 5000 });
|
|
42
|
-
const status = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
45
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore', ...WIN_HIDE });
|
|
46
|
+
execSync('git add -A', { cwd, stdio: 'ignore', timeout: 5000, ...WIN_HIDE });
|
|
47
|
+
const status = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }).trim();
|
|
43
48
|
if (!status) return null;
|
|
44
49
|
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
45
50
|
const safeLabel = label
|
|
46
51
|
? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
|
|
47
52
|
: '';
|
|
48
53
|
const msg = `${CHECKPOINT_PREFIX}${safeLabel} (${ts})`;
|
|
49
|
-
execSync(`git commit -m "${msg}" --no-verify`, { cwd, stdio: 'ignore', timeout: 10000 });
|
|
50
|
-
const hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000 }).trim();
|
|
54
|
+
execSync(`git commit -m "${msg}" --no-verify`, { cwd, stdio: 'ignore', timeout: 10000, ...WIN_HIDE });
|
|
55
|
+
const hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
|
|
51
56
|
log('INFO', `Git checkpoint: ${hash.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
|
|
52
57
|
return hash;
|
|
53
58
|
} catch {
|
|
@@ -55,11 +60,34 @@ function createCheckpointUtils(deps) {
|
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
// Async version: runs git commands without blocking the event loop.
|
|
64
|
+
// Call fire-and-forget before spawning Claude; completes well before Claude's first file write.
|
|
65
|
+
async function gitCheckpointAsync(cwd, label) {
|
|
66
|
+
if (!execFileAsync) return gitCheckpoint(cwd, label); // fallback
|
|
67
|
+
try {
|
|
68
|
+
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 3000, ...WIN_HIDE });
|
|
69
|
+
await execFileAsync('git', ['add', '-A'], { cwd, timeout: 5000, ...WIN_HIDE });
|
|
70
|
+
const { stdout: status } = await execFileAsync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE });
|
|
71
|
+
if (!status.trim()) return null;
|
|
72
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
73
|
+
const safeLabel = label
|
|
74
|
+
? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
|
|
75
|
+
: '';
|
|
76
|
+
const msg = `${CHECKPOINT_PREFIX}${safeLabel} (${ts})`;
|
|
77
|
+
await execFileAsync('git', ['commit', '-m', msg, '--no-verify'], { cwd, timeout: 10000, ...WIN_HIDE });
|
|
78
|
+
const { stdout: hash } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE });
|
|
79
|
+
log('INFO', `Git checkpoint: ${hash.trim().slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
|
|
80
|
+
return hash.trim();
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
function listCheckpoints(cwd, limit = 20) {
|
|
59
87
|
try {
|
|
60
88
|
const raw = execSync(
|
|
61
89
|
`git log --fixed-strings --oneline --all --grep="${CHECKPOINT_PREFIX}" -n ${limit} --format="%H %s"`,
|
|
62
|
-
{ cwd, encoding: 'utf8', timeout: 5000 }
|
|
90
|
+
{ cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }
|
|
63
91
|
).trim();
|
|
64
92
|
if (!raw) return [];
|
|
65
93
|
return raw.split('\n').map(line => {
|
|
@@ -81,6 +109,7 @@ function createCheckpointUtils(deps) {
|
|
|
81
109
|
cpExtractTimestamp,
|
|
82
110
|
cpDisplayLabel,
|
|
83
111
|
gitCheckpoint,
|
|
112
|
+
gitCheckpointAsync,
|
|
84
113
|
listCheckpoints,
|
|
85
114
|
cleanupCheckpoints,
|
|
86
115
|
};
|