metame-cli 1.5.3 → 1.5.5
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 +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
|
@@ -13,6 +13,7 @@ function createAgentCommandHandler(deps) {
|
|
|
13
13
|
sendBrowse,
|
|
14
14
|
sendDirPicker,
|
|
15
15
|
getSession,
|
|
16
|
+
getSessionForEngine,
|
|
16
17
|
listRecentSessions,
|
|
17
18
|
buildSessionCardElements,
|
|
18
19
|
sessionLabel,
|
|
@@ -21,7 +22,11 @@ function createAgentCommandHandler(deps) {
|
|
|
21
22
|
getSessionRecentContext,
|
|
22
23
|
pendingBinds,
|
|
23
24
|
pendingAgentFlows,
|
|
25
|
+
pendingTeamFlows,
|
|
24
26
|
pendingActivations,
|
|
27
|
+
writeConfigSafe,
|
|
28
|
+
backupConfig,
|
|
29
|
+
execSync,
|
|
25
30
|
doBindAgent,
|
|
26
31
|
mergeAgentRole,
|
|
27
32
|
agentTools,
|
|
@@ -36,6 +41,103 @@ function createAgentCommandHandler(deps) {
|
|
|
36
41
|
return n === 'codex' ? 'codex' : getDefaultEngine();
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function inferStoredEngine(rawSession) {
|
|
45
|
+
if (!rawSession || typeof rawSession !== 'object') return getDefaultEngine();
|
|
46
|
+
if (rawSession.engine) return normalizeEngineName(rawSession.engine);
|
|
47
|
+
const slots = rawSession.engines && typeof rawSession.engines === 'object' ? rawSession.engines : null;
|
|
48
|
+
if (!slots) return getDefaultEngine();
|
|
49
|
+
const started = Object.entries(slots).find(([, slot]) => slot && slot.started);
|
|
50
|
+
if (started) return normalizeEngineName(started[0]);
|
|
51
|
+
const available = Object.keys(slots);
|
|
52
|
+
return available.length === 1 ? normalizeEngineName(available[0]) : getDefaultEngine();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildBoundSessionChatId(projectKey) {
|
|
56
|
+
const key = String(projectKey || '').trim();
|
|
57
|
+
return key ? `_bound_${key}` : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getSessionRoute(chatId) {
|
|
61
|
+
const cfg = loadConfig();
|
|
62
|
+
const state = loadState();
|
|
63
|
+
const chatKey = String(chatId);
|
|
64
|
+
const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
|
|
65
|
+
const boundKey = agentMap[chatKey] || null;
|
|
66
|
+
const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
|
|
67
|
+
const stickyKey = state && state.team_sticky ? state.team_sticky[chatKey] : null;
|
|
68
|
+
const stickyMember = stickyKey && boundProj && Array.isArray(boundProj.team)
|
|
69
|
+
? boundProj.team.find((m) => m && m.key === stickyKey)
|
|
70
|
+
: null;
|
|
71
|
+
|
|
72
|
+
if (stickyMember) {
|
|
73
|
+
return {
|
|
74
|
+
sessionChatId: `_agent_${stickyMember.key}`,
|
|
75
|
+
cwd: stickyMember.cwd ? normalizeCwd(stickyMember.cwd) : (boundProj && boundProj.cwd ? normalizeCwd(boundProj.cwd) : null),
|
|
76
|
+
engine: normalizeEngineName(stickyMember.engine || (boundProj && boundProj.engine)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (boundProj) {
|
|
81
|
+
return {
|
|
82
|
+
sessionChatId: buildBoundSessionChatId(boundKey),
|
|
83
|
+
cwd: boundProj.cwd ? normalizeCwd(boundProj.cwd) : null,
|
|
84
|
+
engine: normalizeEngineName(boundProj.engine),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rawSession = getSession(chatId);
|
|
89
|
+
return {
|
|
90
|
+
sessionChatId: String(chatId),
|
|
91
|
+
cwd: rawSession && rawSession.cwd ? normalizeCwd(rawSession.cwd) : null,
|
|
92
|
+
engine: inferStoredEngine(rawSession),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getCurrentEngine(chatId) {
|
|
97
|
+
return getSessionRoute(chatId).engine;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getLogicalSessionForRoute(route) {
|
|
101
|
+
if (!route || !route.sessionChatId) return null;
|
|
102
|
+
if (typeof getSessionForEngine === 'function') {
|
|
103
|
+
const engineSession = getSessionForEngine(route.sessionChatId, route.engine);
|
|
104
|
+
if (engineSession && engineSession.id) return engineSession;
|
|
105
|
+
}
|
|
106
|
+
const raw = getSession(route.sessionChatId);
|
|
107
|
+
if (!raw) return null;
|
|
108
|
+
const slot = raw.engines && raw.engines[route.engine];
|
|
109
|
+
if (slot && slot.id) return { cwd: raw.cwd, engine: route.engine, ...slot };
|
|
110
|
+
if (raw.id) return { cwd: raw.cwd, engine: route.engine, id: raw.id, started: !!raw.started };
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildResumeChoices({ recentSessions, currentLogical, curCwd, currentEngine, isLogicalRoute }) {
|
|
115
|
+
const items = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
if (
|
|
118
|
+
isLogicalRoute
|
|
119
|
+
&& currentLogical
|
|
120
|
+
&& currentLogical.id
|
|
121
|
+
&& currentLogical.started
|
|
122
|
+
) {
|
|
123
|
+
items.push({
|
|
124
|
+
sessionId: currentLogical.id,
|
|
125
|
+
projectPath: currentLogical.cwd || curCwd || HOME,
|
|
126
|
+
engine: currentEngine,
|
|
127
|
+
customTitle: '当前会话',
|
|
128
|
+
summary: '优先续接当前智能体会话',
|
|
129
|
+
});
|
|
130
|
+
seen.add(String(currentLogical.id));
|
|
131
|
+
}
|
|
132
|
+
for (const session of recentSessions || []) {
|
|
133
|
+
const key = String(session && session.sessionId || '');
|
|
134
|
+
if (!key || seen.has(key)) continue;
|
|
135
|
+
items.push(session);
|
|
136
|
+
seen.add(key);
|
|
137
|
+
}
|
|
138
|
+
return items;
|
|
139
|
+
}
|
|
140
|
+
|
|
39
141
|
function inferEngineByCwd(cfg, cwd) {
|
|
40
142
|
if (!cfg || !cfg.projects || !cwd) return null;
|
|
41
143
|
const targetCwd = normalizeCwd(cwd);
|
|
@@ -48,18 +150,20 @@ function createAgentCommandHandler(deps) {
|
|
|
48
150
|
return null;
|
|
49
151
|
}
|
|
50
152
|
|
|
153
|
+
async function autoCreateSessionOnEmptyResume(bot, chatId, cwd, engine) {
|
|
154
|
+
const resolvedCwd = cwd ? normalizeCwd(cwd) : null;
|
|
155
|
+
if (!resolvedCwd || !fs.existsSync(resolvedCwd) || typeof attachOrCreateSession !== 'function') {
|
|
156
|
+
await bot.sendMessage(chatId, `No sessions found${resolvedCwd ? ' in ' + path.basename(resolvedCwd) : ''}. Try /new first.`);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
attachOrCreateSession(getSessionRoute(chatId).sessionChatId, resolvedCwd, '', engine || getDefaultEngine());
|
|
160
|
+
await bot.sendMessage(chatId, `📁 ${path.basename(resolvedCwd)}\n✅ 已自动创建新会话`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
51
164
|
// Pending activations have no TTL — they persist until consumed.
|
|
52
165
|
// The creating chatId is stored to prevent self-activation.
|
|
53
166
|
|
|
54
|
-
function storePendingActivation(agentKey, agentName, cwd, createdByChatId) {
|
|
55
|
-
if (!pendingActivations) return;
|
|
56
|
-
pendingActivations.set(agentKey, {
|
|
57
|
-
agentKey, agentName, cwd,
|
|
58
|
-
createdByChatId: String(createdByChatId),
|
|
59
|
-
createdAt: Date.now(),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
167
|
// Returns the latest pending activation, excluding the creating chat
|
|
64
168
|
function getLatestActivationForChat(chatId) {
|
|
65
169
|
if (!pendingActivations || pendingActivations.size === 0) return null;
|
|
@@ -184,25 +288,6 @@ function createAgentCommandHandler(deps) {
|
|
|
184
288
|
return { ok: true, data: legacy };
|
|
185
289
|
}
|
|
186
290
|
|
|
187
|
-
async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc, opts = {}) {
|
|
188
|
-
// Default: skip binding the creating chat — let the target group activate via /activate
|
|
189
|
-
const { skipChatBinding = true, engine = null } = opts;
|
|
190
|
-
if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
|
|
191
|
-
const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding, engine });
|
|
192
|
-
if (res.ok && skipChatBinding && res.data && res.data.projectKey) {
|
|
193
|
-
storePendingActivation(res.data.projectKey, name, res.data.cwd, chatId);
|
|
194
|
-
}
|
|
195
|
-
return res;
|
|
196
|
-
}
|
|
197
|
-
const bound = await doBindAgent({ sendMessage: async () => {} }, chatId, name, dir);
|
|
198
|
-
if (!bound || bound.ok === false) {
|
|
199
|
-
return { ok: false, error: (bound && bound.error) || 'bind failed' };
|
|
200
|
-
}
|
|
201
|
-
const merged = await mergeAgentRole(dir, roleDesc);
|
|
202
|
-
if (merged.error) return { ok: false, error: merged.error };
|
|
203
|
-
return { ok: true, data: { cwd: dir, project: { name }, role: merged } };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
291
|
async function listAgentsViaUnifiedApi(chatId) {
|
|
207
292
|
if (agentTools && typeof agentTools.listAllAgents === 'function') {
|
|
208
293
|
return agentTools.listAllAgents(chatId);
|
|
@@ -244,47 +329,89 @@ function createAgentCommandHandler(deps) {
|
|
|
244
329
|
const config = ctx.config || {};
|
|
245
330
|
const text = ctx.text || '';
|
|
246
331
|
|
|
332
|
+
// /cancel — 取消任何挂起的向导流
|
|
333
|
+
if (text === '/cancel') {
|
|
334
|
+
let cancelled = false;
|
|
335
|
+
if (pendingTeamFlows && pendingTeamFlows.has(String(chatId))) {
|
|
336
|
+
pendingTeamFlows.delete(String(chatId));
|
|
337
|
+
cancelled = true;
|
|
338
|
+
}
|
|
339
|
+
if (pendingAgentFlows && pendingAgentFlows.has(String(chatId))) {
|
|
340
|
+
pendingAgentFlows.delete(String(chatId));
|
|
341
|
+
cancelled = true;
|
|
342
|
+
}
|
|
343
|
+
if (pendingBinds && pendingBinds.has(String(chatId))) {
|
|
344
|
+
pendingBinds.delete(String(chatId));
|
|
345
|
+
cancelled = true;
|
|
346
|
+
}
|
|
347
|
+
await bot.sendMessage(chatId, cancelled ? '✅ 已取消当前操作' : '没有进行中的操作');
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
247
351
|
if (text === '/resume' || text.startsWith('/resume ')) {
|
|
248
352
|
const arg = text.slice(7).trim();
|
|
249
353
|
|
|
250
354
|
// Get current workdir to scope session list — prefer bound project cwd over session cwd
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
355
|
+
const route = getSessionRoute(chatId);
|
|
356
|
+
const isLogicalRoute = route.sessionChatId !== String(chatId);
|
|
357
|
+
const currentLogical = getLogicalSessionForRoute(route);
|
|
358
|
+
const curSession = getSession(route.sessionChatId) || getSession(chatId);
|
|
359
|
+
const curCwd = route.cwd || (curSession ? curSession.cwd : null);
|
|
360
|
+
const currentEngine = getCurrentEngine(chatId);
|
|
361
|
+
const recentSessions = listRecentSessions(5, curCwd, currentEngine);
|
|
362
|
+
const resumeChoices = buildResumeChoices({
|
|
363
|
+
recentSessions,
|
|
364
|
+
currentLogical,
|
|
365
|
+
curCwd,
|
|
366
|
+
currentEngine,
|
|
367
|
+
isLogicalRoute,
|
|
368
|
+
});
|
|
259
369
|
|
|
260
370
|
if (!arg) {
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
return true;
|
|
371
|
+
if (resumeChoices.length === 0) {
|
|
372
|
+
return autoCreateSessionOnEmptyResume(bot, chatId, curCwd, currentEngine);
|
|
264
373
|
}
|
|
265
374
|
const headerTitle = curCwd ? `📋 Sessions in ${path.basename(curCwd)}` : '📋 Recent Sessions';
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
375
|
+
try {
|
|
376
|
+
if (bot.sendRawCard) {
|
|
377
|
+
await bot.sendRawCard(chatId, headerTitle, buildSessionCardElements(resumeChoices));
|
|
378
|
+
} else {
|
|
379
|
+
throw new Error('raw-card-unavailable');
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
try {
|
|
383
|
+
if (bot.sendButtons) {
|
|
384
|
+
const buttons = resumeChoices.map(s => {
|
|
385
|
+
return [{ text: sessionLabel(s), callback_data: `/resume ${s.sessionId}` }];
|
|
386
|
+
});
|
|
387
|
+
await bot.sendButtons(chatId, headerTitle, buttons);
|
|
388
|
+
} else {
|
|
389
|
+
throw new Error('buttons-unavailable');
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
const _tags2 = loadSessionTags();
|
|
393
|
+
let msg = `${headerTitle}\n`;
|
|
394
|
+
msg += '\n';
|
|
395
|
+
resumeChoices.forEach((s, i) => {
|
|
396
|
+
msg += sessionRichLabel(s, i + 1, _tags2) + '\n';
|
|
397
|
+
});
|
|
398
|
+
await bot.sendMessage(chatId, msg);
|
|
399
|
+
}
|
|
280
400
|
}
|
|
281
401
|
return true;
|
|
282
402
|
}
|
|
283
403
|
|
|
284
|
-
// Argument given -> match
|
|
285
|
-
|
|
404
|
+
// Argument given -> match current resume choices first (includes synthetic
|
|
405
|
+
// "当前会话" entry for logical routes), then fall back to global history.
|
|
406
|
+
const allSessions = listRecentSessions(50, null, currentEngine);
|
|
286
407
|
const argLower = arg.toLowerCase();
|
|
287
|
-
let fullMatch =
|
|
408
|
+
let fullMatch = resumeChoices.find(s => s.customTitle && s.customTitle.toLowerCase() === argLower);
|
|
409
|
+
if (!fullMatch) {
|
|
410
|
+
fullMatch = allSessions.find(s => s.customTitle && s.customTitle.toLowerCase() === argLower);
|
|
411
|
+
}
|
|
412
|
+
if (!fullMatch) {
|
|
413
|
+
fullMatch = resumeChoices.find(s => s.customTitle && s.customTitle.toLowerCase().includes(argLower));
|
|
414
|
+
}
|
|
288
415
|
if (!fullMatch) {
|
|
289
416
|
fullMatch = allSessions.find(s => s.customTitle && s.customTitle.toLowerCase().includes(argLower));
|
|
290
417
|
}
|
|
@@ -292,35 +419,53 @@ function createAgentCommandHandler(deps) {
|
|
|
292
419
|
fullMatch = recentSessions.find(s => s.sessionId.startsWith(arg))
|
|
293
420
|
|| allSessions.find(s => s.sessionId.startsWith(arg));
|
|
294
421
|
}
|
|
422
|
+
if (!fullMatch) {
|
|
423
|
+
fullMatch = resumeChoices.find(s => s.sessionId.startsWith(arg));
|
|
424
|
+
}
|
|
295
425
|
if (!fullMatch) {
|
|
296
426
|
// keep historical behavior:
|
|
297
427
|
// "/resume 看到的session信息太少了" should be treated as normal text
|
|
298
428
|
return null;
|
|
299
429
|
}
|
|
300
430
|
const sessionId = fullMatch.sessionId;
|
|
301
|
-
const cwd = fullMatch.projectPath || (
|
|
431
|
+
const cwd = fullMatch.projectPath || (curSession && curSession.cwd) || HOME;
|
|
302
432
|
|
|
303
433
|
const state2 = loadState();
|
|
304
434
|
const cfgForEngine = loadConfig();
|
|
305
|
-
const
|
|
306
|
-
// For bound chats, write session to virtual chatId (_agent_{key}) so askClaude picks it up
|
|
307
|
-
const resumeChatAgentMap = { ...(cfgForEngine.telegram ? cfgForEngine.telegram.chat_agent_map : {}), ...(cfgForEngine.feishu ? cfgForEngine.feishu.chat_agent_map : {}) };
|
|
308
|
-
const resumeBoundKey = resumeChatAgentMap[String(chatId)];
|
|
309
|
-
const sessionKey = resumeBoundKey ? `_agent_${resumeBoundKey}` : String(chatId);
|
|
435
|
+
const sessionKey = route.sessionChatId;
|
|
310
436
|
const existing = state2.sessions[sessionKey] || {};
|
|
437
|
+
const existingEngine = normalizeEngineName(
|
|
438
|
+
existing.engine
|
|
439
|
+
|| (existing.engines && Object.entries(existing.engines).find(([, slot]) => slot && slot.started)?.[0])
|
|
440
|
+
);
|
|
441
|
+
const engineByTargetCwd = normalizeEngineName(fullMatch.engine)
|
|
442
|
+
|| inferEngineByCwd(cfgForEngine, cwd)
|
|
443
|
+
|| existingEngine;
|
|
444
|
+
const selectedLogicalCurrent = isLogicalRoute
|
|
445
|
+
&& currentLogical
|
|
446
|
+
&& currentLogical.id
|
|
447
|
+
&& sessionId === currentLogical.id;
|
|
448
|
+
const targetSessionId = sessionId;
|
|
449
|
+
const targetCwd = cwd;
|
|
311
450
|
const existingEngines = existing.engines || {};
|
|
312
451
|
state2.sessions[sessionKey] = {
|
|
313
452
|
...existing,
|
|
314
|
-
cwd,
|
|
315
|
-
|
|
453
|
+
cwd: targetCwd,
|
|
454
|
+
id: targetSessionId,
|
|
455
|
+
started: true,
|
|
456
|
+
engine: engineByTargetCwd,
|
|
457
|
+
engines: { ...existingEngines, [engineByTargetCwd]: { id: targetSessionId, started: true } },
|
|
316
458
|
};
|
|
317
459
|
saveState(state2);
|
|
318
460
|
const name = fullMatch.customTitle;
|
|
319
|
-
const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) ||
|
|
461
|
+
const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) || targetSessionId.slice(0, 8);
|
|
320
462
|
|
|
321
463
|
// 读取最近对话片段,帮助确认是否切换到正确的 session
|
|
322
|
-
const recentCtx = getSessionRecentContext ? getSessionRecentContext(
|
|
464
|
+
const recentCtx = getSessionRecentContext ? getSessionRecentContext(targetSessionId) : null;
|
|
323
465
|
let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}`;
|
|
466
|
+
if (selectedLogicalCurrent) {
|
|
467
|
+
msg += '\n\n已恢复当前智能体会话。';
|
|
468
|
+
}
|
|
324
469
|
if (recentCtx) {
|
|
325
470
|
if (recentCtx.lastUser) {
|
|
326
471
|
const snippet = recentCtx.lastUser.replace(/\n/g, ' ').slice(0, 80);
|
|
@@ -341,6 +486,93 @@ function createAgentCommandHandler(deps) {
|
|
|
341
486
|
|
|
342
487
|
// wizard state machine removed — use natural language to create agents
|
|
343
488
|
|
|
489
|
+
// /agent new 多步向导状态机(name/desc 步骤)
|
|
490
|
+
if (pendingAgentFlows) {
|
|
491
|
+
const flow = pendingAgentFlows.get(String(chatId));
|
|
492
|
+
if (flow && text && !text.startsWith('/')) {
|
|
493
|
+
if (flow.step === 'name') {
|
|
494
|
+
flow.name = text.trim();
|
|
495
|
+
flow.step = 'desc';
|
|
496
|
+
pendingAgentFlows.set(String(chatId), flow);
|
|
497
|
+
await bot.sendMessage(chatId, `好的,Agent 名称是「${flow.name}」\n\n步骤3/3:请描述这个 Agent 的角色和职责(用自然语言):`);
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (flow.step === 'desc') {
|
|
501
|
+
pendingAgentFlows.delete(String(chatId));
|
|
502
|
+
const { dir, name, isClone, parentCwd } = flow;
|
|
503
|
+
const description = text.trim();
|
|
504
|
+
await bot.sendMessage(chatId, `⏳ 正在配置 Agent「${name}」,稍等...`);
|
|
505
|
+
try {
|
|
506
|
+
await doBindAgent(bot, chatId, name, dir);
|
|
507
|
+
const mergeResult = await mergeAgentRole(dir, description, isClone, parentCwd);
|
|
508
|
+
if (mergeResult && mergeResult.error) {
|
|
509
|
+
await bot.sendMessage(chatId, `⚠️ CLAUDE.md 合并失败: ${mergeResult.error},其他配置已保存`);
|
|
510
|
+
} else if (mergeResult && mergeResult.symlinked) {
|
|
511
|
+
await bot.sendMessage(chatId, `🔗 CLAUDE.md 已链接到父 Agent(分身模式)\n✅ Agent「${name}」创建完成`);
|
|
512
|
+
} else if (mergeResult && mergeResult.created) {
|
|
513
|
+
await bot.sendMessage(chatId, `📝 已创建 CLAUDE.md\n✅ Agent「${name}」创建完成`);
|
|
514
|
+
} else {
|
|
515
|
+
await bot.sendMessage(chatId, `📝 已更新 CLAUDE.md\n✅ Agent「${name}」创建完成`);
|
|
516
|
+
}
|
|
517
|
+
} catch (e) {
|
|
518
|
+
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${e.message}`);
|
|
519
|
+
}
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// /agent new team 多步向导状态机
|
|
526
|
+
if (pendingTeamFlows) {
|
|
527
|
+
const teamFlow = pendingTeamFlows.get(String(chatId));
|
|
528
|
+
if (teamFlow && text && !text.startsWith('/')) {
|
|
529
|
+
if (teamFlow.step === 'name') {
|
|
530
|
+
teamFlow.name = text.trim();
|
|
531
|
+
teamFlow.step = 'members';
|
|
532
|
+
pendingTeamFlows.set(String(chatId), teamFlow);
|
|
533
|
+
await bot.sendMessage(chatId, `团队名称:「${teamFlow.name}」
|
|
534
|
+
|
|
535
|
+
请输入成员列表,格式:
|
|
536
|
+
名称:icon:颜色
|
|
537
|
+
|
|
538
|
+
可用颜色:green, yellow, red, blue, purple, orange, pink, indigo
|
|
539
|
+
|
|
540
|
+
示例:
|
|
541
|
+
编剧:✍️:green, 审核:🔍:yellow, 推广:📢:red
|
|
542
|
+
|
|
543
|
+
一行一个成员,或用逗号分隔多个`);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (teamFlow.step === 'members') {
|
|
548
|
+
const validColors = ['green', 'yellow', 'red', 'blue', 'purple', 'orange', 'pink', 'indigo'];
|
|
549
|
+
const memberLines = text.split(/[,,\n]/).filter(l => l.trim());
|
|
550
|
+
const members = [];
|
|
551
|
+
for (const line of memberLines) {
|
|
552
|
+
const parts = line.trim().split(':');
|
|
553
|
+
const name = parts[0] && parts[0].trim();
|
|
554
|
+
const icon = (parts[1] && parts[1].trim()) || '🤖';
|
|
555
|
+
const rawColor = parts[2] && parts[2].trim().toLowerCase();
|
|
556
|
+
const color = validColors.includes(rawColor) ? rawColor : validColors[members.length % validColors.length];
|
|
557
|
+
if (name) {
|
|
558
|
+
members.push({ key: name, name: `${teamFlow.name} · ${name}`, icon, color, nicknames: [name] });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (members.length === 0) {
|
|
562
|
+
await bot.sendMessage(chatId, '⚠️ 请至少添加一个成员,格式:名称:icon:颜色');
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
teamFlow.members = members;
|
|
566
|
+
teamFlow.step = 'cwd';
|
|
567
|
+
pendingTeamFlows.set(String(chatId), teamFlow);
|
|
568
|
+
const memberList = members.map(m => `${m.icon} ${m.name} (${m.color})`).join('\n');
|
|
569
|
+
await bot.sendMessage(chatId, `✅ 成员配置:\n\n${memberList}\n\n正在选择父目录...`);
|
|
570
|
+
await sendBrowse(bot, chatId, 'team-new', HOME, `为「${teamFlow.name}」选择父工作目录`);
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
344
576
|
// /agent edit wait-input flow (kept for command compatibility)
|
|
345
577
|
{
|
|
346
578
|
const editFlow = getFreshFlow(String(chatId) + ':edit');
|
|
@@ -363,6 +595,41 @@ function createAgentCommandHandler(deps) {
|
|
|
363
595
|
const agentParts = agentArg.split(/\s+/).filter(Boolean);
|
|
364
596
|
const agentSub = agentParts[0] || ''; // bind / list / new / edit / reset / unbind / ''
|
|
365
597
|
|
|
598
|
+
// /agent new [team] — 创建新 Agent 或团队
|
|
599
|
+
if (agentSub === 'new') {
|
|
600
|
+
const secondArg = agentParts[1];
|
|
601
|
+
if (secondArg === 'team') {
|
|
602
|
+
if (!pendingTeamFlows) {
|
|
603
|
+
await bot.sendMessage(chatId, '❌ 团队向导暂不可用');
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
pendingTeamFlows.set(String(chatId), { step: 'name' });
|
|
607
|
+
await bot.sendMessage(chatId, `🏗️ **团队创建向导**
|
|
608
|
+
|
|
609
|
+
请输入团队名称(如:短剧团队、销售团队):
|
|
610
|
+
|
|
611
|
+
输入 /cancel 可取消`);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
// /agent new clone — 分身模式
|
|
615
|
+
const isClone = secondArg === 'clone';
|
|
616
|
+
let parentCwd = null;
|
|
617
|
+
if (isClone) {
|
|
618
|
+
const cfg = loadConfig();
|
|
619
|
+
const agentMap = {
|
|
620
|
+
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
621
|
+
...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
622
|
+
};
|
|
623
|
+
const boundKey = agentMap[String(chatId)];
|
|
624
|
+
const boundProj = boundKey && cfg.projects && cfg.projects[boundKey];
|
|
625
|
+
if (boundProj && boundProj.cwd) parentCwd = normalizeCwd(boundProj.cwd);
|
|
626
|
+
}
|
|
627
|
+
pendingAgentFlows.set(String(chatId), { step: 'dir', isClone, parentCwd, __ts: Date.now() });
|
|
628
|
+
const hint = isClone ? `(${parentCwd ? '分身模式:将链接父 Agent 的 CLAUDE.md' : '⚠️ 当前群未绑定 Agent'})` : '';
|
|
629
|
+
await sendBrowse(bot, chatId, 'agent-new', HOME, `步骤1/3:选择 Agent 的工作目录${hint}`);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
|
|
366
633
|
// /agent bind <名称> [目录]
|
|
367
634
|
if (agentSub === 'bind') {
|
|
368
635
|
const bindName = agentParts[1];
|
|
@@ -644,6 +911,23 @@ function createAgentCommandHandler(deps) {
|
|
|
644
911
|
return true;
|
|
645
912
|
}
|
|
646
913
|
|
|
914
|
+
// /agent-dir <path>: /agent new 向导的目录选择回调(步骤1→步骤2)
|
|
915
|
+
if (text.startsWith('/agent-dir ')) {
|
|
916
|
+
const dirPath = expandPath(text.slice(11).trim());
|
|
917
|
+
const flow = pendingAgentFlows && pendingAgentFlows.get(String(chatId));
|
|
918
|
+
if (!flow || flow.step !== 'dir') {
|
|
919
|
+
await bot.sendMessage(chatId, '❌ 没有待完成的 /agent new,请重新发送 /agent new');
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
flow.dir = dirPath;
|
|
923
|
+
flow.step = 'name';
|
|
924
|
+
pendingAgentFlows.set(String(chatId), flow);
|
|
925
|
+
const displayPath = dirPath.replace(HOME, '~');
|
|
926
|
+
const cloneHint = flow.isClone ? '(分身模式)' : '';
|
|
927
|
+
await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}${cloneHint}\n\n步骤2/3:给这个 Agent 起个名字?`);
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
|
|
647
931
|
// /agent-bind-dir <path>: internal callback for bind picker
|
|
648
932
|
if (text.startsWith('/agent-bind-dir ')) {
|
|
649
933
|
const dirPath = expandPath(text.slice(16).trim());
|
|
@@ -657,6 +941,90 @@ function createAgentCommandHandler(deps) {
|
|
|
657
941
|
return true;
|
|
658
942
|
}
|
|
659
943
|
|
|
944
|
+
// /agent-team-dir <path>: directory picker callback for team creation
|
|
945
|
+
if (text.startsWith('/agent-team-dir ')) {
|
|
946
|
+
const dirPath = expandPath(text.slice(16).trim());
|
|
947
|
+
const teamFlow = pendingTeamFlows && pendingTeamFlows.get(String(chatId));
|
|
948
|
+
if (!teamFlow || teamFlow.step !== 'cwd') {
|
|
949
|
+
await bot.sendMessage(chatId, '❌ 没有待完成的团队创建,请重新发送 /agent new team');
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
teamFlow.step = 'creating';
|
|
953
|
+
pendingTeamFlows.set(String(chatId), teamFlow);
|
|
954
|
+
await bot.sendMessage(chatId, `⏳ 正在创建团队「${teamFlow.name}」...`);
|
|
955
|
+
|
|
956
|
+
try {
|
|
957
|
+
const teamDir = path.join(dirPath, 'team');
|
|
958
|
+
if (!fs.existsSync(teamDir)) fs.mkdirSync(teamDir, { recursive: true });
|
|
959
|
+
|
|
960
|
+
const members = Array.isArray(teamFlow.members) ? teamFlow.members : [];
|
|
961
|
+
const results = [];
|
|
962
|
+
for (const member of members) {
|
|
963
|
+
const memberDir = path.join(teamDir, member.key);
|
|
964
|
+
if (!fs.existsSync(memberDir)) fs.mkdirSync(memberDir, { recursive: true });
|
|
965
|
+
const claudeMdPath = path.join(memberDir, 'CLAUDE.md');
|
|
966
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
967
|
+
fs.writeFileSync(claudeMdPath, `# ${member.name}\n\n(团队成员:${teamFlow.name})\n`, 'utf8');
|
|
968
|
+
}
|
|
969
|
+
// Init git repo for checkpoint support
|
|
970
|
+
try {
|
|
971
|
+
if (execSync) execSync('git init -q', { cwd: memberDir, stdio: 'ignore' });
|
|
972
|
+
} catch { /* non-critical */ }
|
|
973
|
+
results.push(`✅ ${member.icon} ${member.key}: ${memberDir.replace(HOME, '~')}`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Register in daemon.yaml under parent project's team array
|
|
977
|
+
const cfg = loadConfig();
|
|
978
|
+
let parentProjectKey = null;
|
|
979
|
+
if (cfg.projects) {
|
|
980
|
+
for (const [projKey, proj] of Object.entries(cfg.projects)) {
|
|
981
|
+
if (normalizeCwd(proj.cwd || '') === normalizeCwd(dirPath)) {
|
|
982
|
+
parentProjectKey = projKey;
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (parentProjectKey && cfg.projects[parentProjectKey]) {
|
|
989
|
+
const proj = cfg.projects[parentProjectKey];
|
|
990
|
+
if (!proj.team) proj.team = [];
|
|
991
|
+
for (const member of members) {
|
|
992
|
+
if (!proj.team.some(m => m.key === member.key)) {
|
|
993
|
+
proj.team.push({
|
|
994
|
+
key: member.key,
|
|
995
|
+
name: member.name,
|
|
996
|
+
icon: member.icon,
|
|
997
|
+
color: member.color,
|
|
998
|
+
cwd: path.join(teamDir, member.key),
|
|
999
|
+
nicknames: member.nicknames,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (writeConfigSafe) writeConfigSafe(cfg);
|
|
1004
|
+
if (backupConfig) backupConfig();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const memberList = members.map(m => `${m.icon} ${m.key}`).join(' | ');
|
|
1008
|
+
const yamlNote = parentProjectKey
|
|
1009
|
+
? `📝 已更新 daemon.yaml:${parentProjectKey}.team`
|
|
1010
|
+
: '⚠️ 未找到父项目,请手动在 daemon.yaml 中注册 team 段';
|
|
1011
|
+
await bot.sendMessage(chatId, `🎉 **团队创建完成!**
|
|
1012
|
+
|
|
1013
|
+
**${teamFlow.name}**
|
|
1014
|
+
${memberList}
|
|
1015
|
+
|
|
1016
|
+
📁 目录:${teamDir.replace(HOME, '~')}/
|
|
1017
|
+
${yamlNote}
|
|
1018
|
+
|
|
1019
|
+
💡 发 \`/agent\` 可切换到成员对话`);
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
await bot.sendMessage(chatId, `❌ 创建失败: ${e.message}`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
pendingTeamFlows.delete(String(chatId));
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
660
1028
|
return false;
|
|
661
1029
|
}
|
|
662
1030
|
|