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.
Files changed (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. 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 cfgForResume = loadConfig();
252
- const chatAgentMapForResume = { ...(cfgForResume.telegram ? cfgForResume.telegram.chat_agent_map : {}), ...(cfgForResume.feishu ? cfgForResume.feishu.chat_agent_map : {}) };
253
- const boundKeyForResume = chatAgentMapForResume[String(chatId)];
254
- const boundProjForResume = boundKeyForResume && cfgForResume.projects ? cfgForResume.projects[boundKeyForResume] : null;
255
- const boundCwdForResume = (boundProjForResume && boundProjForResume.cwd) ? normalizeCwd(boundProjForResume.cwd) : null;
256
- const curSession = getSession(chatId);
257
- const curCwd = boundCwdForResume || (curSession ? curSession.cwd : null);
258
- const recentSessions = listRecentSessions(5, curCwd);
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 (recentSessions.length === 0) {
262
- await bot.sendMessage(chatId, `No sessions found${curCwd ? ' in ' + path.basename(curCwd) : ''}. Try /new first.`);
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
- if (bot.sendRawCard) {
267
- await bot.sendRawCard(chatId, headerTitle, buildSessionCardElements(recentSessions));
268
- } else if (bot.sendButtons) {
269
- const buttons = recentSessions.map(s => {
270
- return [{ text: sessionLabel(s), callback_data: `/resume ${s.sessionId}` }];
271
- });
272
- await bot.sendButtons(chatId, headerTitle, buttons);
273
- } else {
274
- const _tags2 = loadSessionTags();
275
- let msg = `${headerTitle}\n\n`;
276
- recentSessions.forEach((s, i) => {
277
- msg += sessionRichLabel(s, i + 1, _tags2) + '\n';
278
- });
279
- await bot.sendMessage(chatId, msg);
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 by name, then by session ID prefix
285
- const allSessions = listRecentSessions(50);
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 = allSessions.find(s => s.customTitle && s.customTitle.toLowerCase() === argLower);
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 || (getSession(chatId) && getSession(chatId).cwd) || HOME;
431
+ const cwd = fullMatch.projectPath || (curSession && curSession.cwd) || HOME;
302
432
 
303
433
  const state2 = loadState();
304
434
  const cfgForEngine = loadConfig();
305
- const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd) || getDefaultEngine();
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
- engines: { ...existingEngines, [engineByTargetCwd]: { id: sessionId, started: true } },
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) || sessionId.slice(0, 8);
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(sessionId) : null;
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