metame-cli 1.5.21 → 1.5.23

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.
@@ -1,6 +1,23 @@
1
1
  'use strict';
2
2
 
3
3
  const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
+ const {
5
+ getBoundProject,
6
+ createWorkspaceAgent,
7
+ bindAgentToChat,
8
+ editAgentRole,
9
+ listAgents,
10
+ unbindAgent,
11
+ handleActivateCommand,
12
+ } = require('./daemon-agent-workflow');
13
+ const {
14
+ startNewAgentWizard,
15
+ completeAgentCreation,
16
+ readAgentRolePreview,
17
+ resetAgentRoleSection,
18
+ handleSoulCommand,
19
+ } = require('./daemon-agent-lifecycle');
20
+ const { parseTeamMembers, createTeamWorkspace } = require('./daemon-team-workflow');
4
21
 
5
22
  function createAgentCommandHandler(deps) {
6
23
  const {
@@ -22,6 +39,7 @@ function createAgentCommandHandler(deps) {
22
39
  loadSessionTags,
23
40
  sessionRichLabel,
24
41
  getSessionRecentContext,
42
+ getSessionRecentDialogue,
25
43
  pendingBinds,
26
44
  pendingAgentFlows,
27
45
  pendingTeamFlows,
@@ -163,21 +181,6 @@ function createAgentCommandHandler(deps) {
163
181
  return true;
164
182
  }
165
183
 
166
- // Pending activations have no TTL — they persist until consumed.
167
- // The creating chatId is stored to prevent self-activation.
168
-
169
- // Returns the latest pending activation, excluding the creating chat
170
- function getLatestActivationForChat(chatId) {
171
- if (!pendingActivations || pendingActivations.size === 0) return null;
172
- const cid = String(chatId);
173
- let latest = null;
174
- for (const rec of pendingActivations.values()) {
175
- if (rec.createdByChatId === cid) continue; // creating chat cannot self-activate
176
- if (!latest || rec.createdAt > latest.createdAt) latest = rec;
177
- }
178
- return latest;
179
- }
180
-
181
184
  function resolveTtl(valueOrGetter, fallbackMs) {
182
185
  const raw = typeof valueOrGetter === 'function' ? valueOrGetter() : valueOrGetter;
183
186
  const num = Number(raw);
@@ -206,6 +209,29 @@ function createAgentCommandHandler(deps) {
206
209
  pendingAgentFlows.set(flowKey, { ...flow, __ts: Date.now() });
207
210
  }
208
211
 
212
+ function getFreshTimedFlow(flowMap, flowKey) {
213
+ if (!flowMap) return null;
214
+ const flow = flowMap.get(flowKey);
215
+ if (!flow) return null;
216
+ const FLOW_TTL_MS = resolveTtl(agentFlowTtlMs, 10 * 60 * 1000);
217
+ const ts = Number(flow.__ts || 0);
218
+ if (!(ts > 0) && flow && typeof flow === 'object') {
219
+ const stamped = { ...flow, __ts: Date.now() };
220
+ flowMap.set(flowKey, stamped);
221
+ return stamped;
222
+ }
223
+ if (ts > 0 && (Date.now() - ts) > FLOW_TTL_MS) {
224
+ flowMap.delete(flowKey);
225
+ return null;
226
+ }
227
+ return flow;
228
+ }
229
+
230
+ function setTimedFlow(flowMap, flowKey, flow) {
231
+ if (!flowMap) return;
232
+ flowMap.set(flowKey, { ...flow, __ts: Date.now() });
233
+ }
234
+
209
235
  function setPendingBind(chatKey, agentName) {
210
236
  pendingBinds.set(chatKey, { name: agentName, __ts: Date.now() });
211
237
  }
@@ -229,101 +255,31 @@ function createAgentCommandHandler(deps) {
229
255
  return raw.name || null;
230
256
  }
231
257
 
232
- function getBoundProject(chatId, cfg) {
233
- const agentMap = {
234
- ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
235
- ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
236
- };
237
- const boundKey = agentMap[String(chatId)];
238
- const boundProj = boundKey && cfg.projects && cfg.projects[boundKey];
239
- return { boundKey: boundKey || null, boundProj: boundProj || null };
240
- }
241
-
242
258
  async function bindViaUnifiedApi(bot, chatId, agentName, agentCwd) {
243
- if (agentTools && typeof agentTools.bindAgentToChat === 'function') {
244
- const res = await agentTools.bindAgentToChat(chatId, agentName, agentCwd);
245
- if (!res.ok) {
246
- await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
247
- return { ok: false };
248
- }
249
- const p = res.data.project || {};
250
- const icon = p.icon || '🤖';
251
- const action = res.data.isNewProject ? '绑定成功' : '重新绑定';
252
- const displayCwd = String(res.data.cwd || '').replace(HOME, '~');
253
- if (res.data.cwd && typeof attachOrCreateSession === 'function') {
254
- attachOrCreateSession(
255
- chatId,
256
- normalizeCwd(res.data.cwd),
257
- p.name || agentName || res.data.projectKey || '',
258
- p.engine || getDefaultEngine()
259
- );
260
- }
261
- await bot.sendMessage(chatId, `${icon} ${p.name || agentName} ${action}\n目录: ${displayCwd}`);
262
- return { ok: true, data: res.data };
263
- }
264
-
265
- // Backward-compatible fallback
266
- const fallback = await doBindAgent(bot, chatId, agentName, agentCwd);
267
- if (!fallback || fallback.ok === false) {
268
- return { ok: false, error: (fallback && fallback.error) || 'bind failed' };
269
- }
270
- const fallbackCwd = (fallback.data && fallback.data.cwd) || agentCwd;
271
- if (fallbackCwd && typeof attachOrCreateSession === 'function') {
272
- attachOrCreateSession(chatId, normalizeCwd(fallbackCwd), agentName || '', getDefaultEngine());
273
- }
274
- return {
275
- ok: true,
276
- data: {
277
- cwd: fallbackCwd,
278
- projectKey: fallback && fallback.data ? fallback.data.projectKey : null,
279
- project: fallback && fallback.data ? fallback.data.project : null,
280
- },
281
- };
282
- }
283
-
284
- async function editRoleViaUnifiedApi(workspaceDir, deltaText) {
285
- if (agentTools && typeof agentTools.editAgentRoleDefinition === 'function') {
286
- return agentTools.editAgentRoleDefinition(workspaceDir, deltaText);
287
- }
288
- const legacy = await mergeAgentRole(workspaceDir, deltaText);
289
- if (legacy.error) return { ok: false, error: legacy.error };
290
- return { ok: true, data: legacy };
259
+ return bindAgentToChat({
260
+ agentTools,
261
+ doBindAgent,
262
+ bot,
263
+ chatId,
264
+ agentName,
265
+ agentCwd,
266
+ HOME,
267
+ attachOrCreateSession,
268
+ normalizeCwd,
269
+ getDefaultEngine,
270
+ });
291
271
  }
292
272
 
293
273
  async function listAgentsViaUnifiedApi(chatId) {
294
- if (agentTools && typeof agentTools.listAllAgents === 'function') {
295
- return agentTools.listAllAgents(chatId);
296
- }
297
-
298
- const cfg = loadConfig();
299
- const projects = cfg.projects || {};
300
- const entries = Object.entries(projects)
301
- .filter(([, p]) => p && p.cwd)
302
- .map(([key, p]) => ({
303
- key,
304
- name: p.name || key,
305
- cwd: p.cwd,
306
- icon: p.icon || '🤖',
307
- }));
308
- const { boundKey } = getBoundProject(chatId, cfg);
309
- return { ok: true, data: { agents: entries, boundKey } };
274
+ return listAgents({ agentTools, chatId, loadConfig });
310
275
  }
311
276
 
312
277
  async function unbindViaUnifiedApi(chatId) {
313
- if (agentTools && typeof agentTools.unbindCurrentAgent === 'function') {
314
- return agentTools.unbindCurrentAgent(chatId);
315
- }
278
+ return unbindAgent({ agentTools, chatId, loadConfig, writeConfigSafe, backupConfig });
279
+ }
316
280
 
317
- const cfg = loadConfig();
318
- const isTg = typeof chatId === 'number';
319
- const ak = isTg ? 'telegram' : 'feishu';
320
- if (!cfg[ak]) cfg[ak] = {};
321
- if (!cfg[ak].chat_agent_map) cfg[ak].chat_agent_map = {};
322
- const old = cfg[ak].chat_agent_map[String(chatId)] || null;
323
- if (old) {
324
- delete cfg[ak].chat_agent_map[String(chatId)];
325
- }
326
- return { ok: true, data: { unbound: !!old, previousProjectKey: old } };
281
+ async function editRoleViaUnifiedApi(workspaceDir, deltaText) {
282
+ return editAgentRole({ agentTools, mergeAgentRole, workspaceDir, deltaText });
327
283
  }
328
284
 
329
285
  async function handleAgentCommand(ctx) {
@@ -466,11 +422,19 @@ function createAgentCommandHandler(deps) {
466
422
 
467
423
  // 读取最近对话片段,帮助确认是否切换到正确的 session
468
424
  const recentCtx = getSessionRecentContext ? getSessionRecentContext(targetSessionId) : null;
425
+ const recentDialogue = getSessionRecentDialogue ? getSessionRecentDialogue(targetSessionId, 4) : null;
469
426
  let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}`;
470
427
  if (selectedLogicalCurrent) {
471
428
  msg += '\n\n已恢复当前智能体会话。';
472
429
  }
473
- if (recentCtx) {
430
+ if (Array.isArray(recentDialogue) && recentDialogue.length > 0) {
431
+ msg += '\n\n最近对话:';
432
+ for (const item of recentDialogue) {
433
+ const marker = item.role === 'assistant' ? '🤖' : '👤';
434
+ const snippet = String(item.text || '').replace(/\n/g, ' ').slice(0, 120);
435
+ if (snippet) msg += `\n${marker} ${snippet}`;
436
+ }
437
+ } else if (recentCtx) {
474
438
  if (recentCtx.lastUser) {
475
439
  const snippet = recentCtx.lastUser.replace(/\n/g, ' ').slice(0, 80);
476
440
  msg += `\n\n💬 上次你说: _${snippet}${recentCtx.lastUser.length > 80 ? '…' : ''}_`;
@@ -492,48 +456,68 @@ function createAgentCommandHandler(deps) {
492
456
 
493
457
  // /agent new 多步向导状态机(name/desc 步骤)
494
458
  if (pendingAgentFlows) {
495
- const flow = pendingAgentFlows.get(String(chatId));
459
+ const flow = getFreshTimedFlow(pendingAgentFlows, String(chatId));
496
460
  if (flow && text && !text.startsWith('/')) {
497
461
  if (flow.step === 'name') {
498
462
  flow.name = text.trim();
463
+ if (flow.isClone) {
464
+ pendingAgentFlows.delete(String(chatId));
465
+ return completeAgentCreation({
466
+ bot,
467
+ chatId,
468
+ flow,
469
+ description: '',
470
+ createWorkspaceAgent: ({ chatId: createChatId, agentName, workspaceDir, roleDescription }) => createWorkspaceAgent({
471
+ agentTools,
472
+ chatId: createChatId,
473
+ agentName,
474
+ workspaceDir,
475
+ roleDescription,
476
+ attachOrCreateSession,
477
+ normalizeCwd,
478
+ getDefaultEngine,
479
+ }),
480
+ doBindAgent,
481
+ mergeAgentRole,
482
+ });
483
+ }
499
484
  flow.step = 'desc';
500
- pendingAgentFlows.set(String(chatId), flow);
485
+ setTimedFlow(pendingAgentFlows, String(chatId), flow);
501
486
  await bot.sendMessage(chatId, `好的,Agent 名称是「${flow.name}」\n\n步骤3/3:请描述这个 Agent 的角色和职责(用自然语言):`);
502
487
  return true;
503
488
  }
504
489
  if (flow.step === 'desc') {
505
490
  pendingAgentFlows.delete(String(chatId));
506
- const { dir, name, isClone, parentCwd } = flow;
507
- const description = text.trim();
508
- await bot.sendMessage(chatId, `⏳ 正在配置 Agent「${name}」,稍等...`);
509
- try {
510
- await doBindAgent(bot, chatId, name, dir);
511
- const mergeResult = await mergeAgentRole(dir, description, isClone, parentCwd);
512
- if (mergeResult && mergeResult.error) {
513
- await bot.sendMessage(chatId, `⚠️ CLAUDE.md 合并失败: ${mergeResult.error},其他配置已保存`);
514
- } else if (mergeResult && mergeResult.symlinked) {
515
- await bot.sendMessage(chatId, `🔗 CLAUDE.md 已链接到父 Agent(分身模式)\n✅ Agent「${name}」创建完成`);
516
- } else if (mergeResult && mergeResult.created) {
517
- await bot.sendMessage(chatId, `📝 已创建 CLAUDE.md\n✅ Agent「${name}」创建完成`);
518
- } else {
519
- await bot.sendMessage(chatId, `📝 已更新 CLAUDE.md\n✅ Agent「${name}」创建完成`);
520
- }
521
- } catch (e) {
522
- await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${e.message}`);
523
- }
524
- return true;
491
+ return completeAgentCreation({
492
+ bot,
493
+ chatId,
494
+ flow,
495
+ description: text.trim(),
496
+ createWorkspaceAgent: ({ chatId: createChatId, agentName, workspaceDir, roleDescription }) => createWorkspaceAgent({
497
+ agentTools,
498
+ chatId: createChatId,
499
+ agentName,
500
+ workspaceDir,
501
+ roleDescription,
502
+ attachOrCreateSession,
503
+ normalizeCwd,
504
+ getDefaultEngine,
505
+ }),
506
+ doBindAgent,
507
+ mergeAgentRole,
508
+ });
525
509
  }
526
510
  }
527
511
  }
528
512
 
529
513
  // /agent new team 多步向导状态机
530
514
  if (pendingTeamFlows) {
531
- const teamFlow = pendingTeamFlows.get(String(chatId));
515
+ const teamFlow = getFreshTimedFlow(pendingTeamFlows, String(chatId));
532
516
  if (teamFlow && text && !text.startsWith('/')) {
533
517
  if (teamFlow.step === 'name') {
534
518
  teamFlow.name = text.trim();
535
519
  teamFlow.step = 'members';
536
- pendingTeamFlows.set(String(chatId), teamFlow);
520
+ setTimedFlow(pendingTeamFlows, String(chatId), teamFlow);
537
521
  await bot.sendMessage(chatId, `团队名称:「${teamFlow.name}」
538
522
 
539
523
  请输入成员列表,格式:
@@ -549,26 +533,14 @@ function createAgentCommandHandler(deps) {
549
533
  }
550
534
 
551
535
  if (teamFlow.step === 'members') {
552
- const validColors = ['green', 'yellow', 'red', 'blue', 'purple', 'orange', 'pink', 'indigo'];
553
- const memberLines = text.split(/[,,\n]/).filter(l => l.trim());
554
- const members = [];
555
- for (const line of memberLines) {
556
- const parts = line.trim().split(':');
557
- const name = parts[0] && parts[0].trim();
558
- const icon = (parts[1] && parts[1].trim()) || '🤖';
559
- const rawColor = parts[2] && parts[2].trim().toLowerCase();
560
- const color = validColors.includes(rawColor) ? rawColor : validColors[members.length % validColors.length];
561
- if (name) {
562
- members.push({ key: name, name: `${teamFlow.name} · ${name}`, icon, color, nicknames: [name] });
563
- }
564
- }
536
+ const members = parseTeamMembers(text, teamFlow.name);
565
537
  if (members.length === 0) {
566
538
  await bot.sendMessage(chatId, '⚠️ 请至少添加一个成员,格式:名称:icon:颜色');
567
539
  return true;
568
540
  }
569
541
  teamFlow.members = members;
570
542
  teamFlow.step = 'cwd';
571
- pendingTeamFlows.set(String(chatId), teamFlow);
543
+ setTimedFlow(pendingTeamFlows, String(chatId), teamFlow);
572
544
  const memberList = members.map(m => `${m.icon} ${m.name} (${m.color})`).join('\n');
573
545
  await bot.sendMessage(chatId, `✅ 成员配置:\n\n${memberList}\n\n正在选择父目录...`);
574
546
  await sendBrowse(bot, chatId, 'team-new', HOME, `为「${teamFlow.name}」选择父工作目录`);
@@ -601,37 +573,17 @@ function createAgentCommandHandler(deps) {
601
573
 
602
574
  // /agent new [team] — 创建新 Agent 或团队
603
575
  if (agentSub === 'new') {
604
- const secondArg = agentParts[1];
605
- if (secondArg === 'team') {
606
- if (!pendingTeamFlows) {
607
- await bot.sendMessage(chatId, '❌ 团队向导暂不可用');
608
- return true;
609
- }
610
- pendingTeamFlows.set(String(chatId), { step: 'name' });
611
- await bot.sendMessage(chatId, `🏗️ **团队创建向导**
612
-
613
- 请输入团队名称(如:短剧团队、销售团队):
614
-
615
- 输入 /cancel 可取消`);
616
- return true;
617
- }
618
- // /agent new clone — 分身模式
619
- const isClone = secondArg === 'clone';
620
- let parentCwd = null;
621
- if (isClone) {
622
- const cfg = loadConfig();
623
- const agentMap = {
624
- ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
625
- ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
626
- };
627
- const boundKey = agentMap[String(chatId)];
628
- const boundProj = boundKey && cfg.projects && cfg.projects[boundKey];
629
- if (boundProj && boundProj.cwd) parentCwd = normalizeCwd(boundProj.cwd);
630
- }
631
- pendingAgentFlows.set(String(chatId), { step: 'dir', isClone, parentCwd, __ts: Date.now() });
632
- const hint = isClone ? `(${parentCwd ? '分身模式:将链接父 Agent 的 CLAUDE.md' : '⚠️ 当前群未绑定 Agent'})` : '';
633
- await sendBrowse(bot, chatId, 'agent-new', HOME, `步骤1/3:选择 Agent 的工作目录${hint}`);
634
- return true;
576
+ return startNewAgentWizard({
577
+ bot,
578
+ chatId,
579
+ secondArg: agentParts[1],
580
+ pendingTeamFlows,
581
+ pendingAgentFlows,
582
+ loadConfig,
583
+ normalizeCwd,
584
+ sendBrowse,
585
+ HOME,
586
+ });
635
587
  }
636
588
 
637
589
  // /agent bind <名称> [目录]
@@ -703,12 +655,7 @@ function createAgentCommandHandler(deps) {
703
655
  return true;
704
656
  }
705
657
 
706
- const claudeMdPath = path.join(cwd, 'CLAUDE.md');
707
- let currentContent = '(CLAUDE.md 不存在)';
708
- if (fs.existsSync(claudeMdPath)) {
709
- currentContent = fs.readFileSync(claudeMdPath, 'utf8');
710
- if (currentContent.length > 500) currentContent = currentContent.slice(0, 500) + '\n...(已截断)';
711
- }
658
+ const currentContent = readAgentRolePreview({ fs, path, cwd });
712
659
  setFlow(String(chatId) + ':edit', { cwd });
713
660
  await bot.sendMessage(chatId, `📄 当前 CLAUDE.md 内容:\n\`\`\`\n${currentContent}\n\`\`\`\n\n请描述你想做的修改(用自然语言,例如:「把角色改成后端工程师,专注 Python」):`);
714
661
  return true;
@@ -738,18 +685,15 @@ function createAgentCommandHandler(deps) {
738
685
  return true;
739
686
  }
740
687
  const cwd = normalizeCwd(boundProj.cwd);
741
- const claudeMdPath = path.join(cwd, 'CLAUDE.md');
742
- if (!fs.existsSync(claudeMdPath)) {
688
+ const resetResult = resetAgentRoleSection({ fs, path, cwd });
689
+ if (resetResult.status === 'missing') {
743
690
  await bot.sendMessage(chatId, '⚠️ CLAUDE.md 不存在,无需重置');
744
691
  return true;
745
692
  }
746
- const before = fs.readFileSync(claudeMdPath, 'utf8');
747
- const after = before.replace(/(?:^|\n)## Agent 角色\n[\s\S]*?(?=\n## |$)/, '').trimStart();
748
- if (after === before.trimStart()) {
693
+ if (resetResult.status === 'unchanged') {
749
694
  await bot.sendMessage(chatId, '⚠️ 未找到「## Agent 角色」section,CLAUDE.md 未修改');
750
695
  return true;
751
696
  }
752
- fs.writeFileSync(claudeMdPath, after, 'utf8');
753
697
  await bot.sendMessage(chatId, '✅ 已删除角色 section,请重新发送角色描述(/agent edit 或自然语言修改)');
754
698
  return true;
755
699
  }
@@ -768,66 +712,16 @@ function createAgentCommandHandler(deps) {
768
712
  await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先 /agent bind <名称> <目录>');
769
713
  return true;
770
714
  }
771
- const cwd = normalizeCwd(boundProj.cwd);
772
- const soulPath = path.join(cwd, 'SOUL.md');
773
-
774
- if (soulAction === 'repair') {
775
- if (agentTools && typeof agentTools.repairAgentSoul === 'function') {
776
- const res = await agentTools.repairAgentSoul(cwd);
777
- if (!res.ok) {
778
- await bot.sendMessage(chatId, '❌ Soul 修复失败: ' + res.error);
779
- } else {
780
- const viewModes = res.data.views
781
- ? Object.entries(res.data.views).map(([k, v]) => k + ':' + v).join(', ')
782
- : '—';
783
- await bot.sendMessage(chatId, [
784
- '✅ Agent Soul 层已就绪',
785
- 'agent_id: ' + res.data.agentId,
786
- '链接方式: ' + viewModes,
787
- '',
788
- '文件位置:',
789
- ' SOUL.md → ~/.metame/agents/' + res.data.agentId + '/soul.md',
790
- ' MEMORY.md → ~/.metame/agents/' + res.data.agentId + '/memory-snapshot.md',
791
- ].join('\n'));
792
- }
793
- } else {
794
- await bot.sendMessage(chatId, '❌ agentTools 不可用');
795
- }
796
- return true;
797
- }
798
-
799
- if (soulAction === 'edit') {
800
- const soulText = agentParts.slice(2).join(' ').trim();
801
- if (!soulText) {
802
- await bot.sendMessage(chatId, '用法: /agent soul edit <新内容>\n当前内容: /agent soul');
803
- return true;
804
- }
805
- try {
806
- fs.writeFileSync(soulPath, soulText, 'utf8');
807
- await bot.sendMessage(chatId, '✅ SOUL.md 已更新');
808
- } catch (e) {
809
- await bot.sendMessage(chatId, '❌ 写入失败: ' + e.message);
810
- }
811
- return true;
812
- }
813
-
814
- // Default: show current SOUL.md content
815
- if (!fs.existsSync(soulPath)) {
816
- await bot.sendMessage(chatId, [
817
- '⚠️ SOUL.md 不存在',
818
- '',
819
- '老项目或刚绑定的 Agent 可能尚未建立 Soul 层。',
820
- '运行 /agent soul repair 自动生成。',
821
- ].join('\n'));
822
- return true;
823
- }
824
- try {
825
- const soulContent = fs.readFileSync(soulPath, 'utf8').trim().slice(0, 2000);
826
- await bot.sendMessage(chatId, '📋 当前 Soul:\n\n' + soulContent);
827
- } catch (e) {
828
- await bot.sendMessage(chatId, '❌ 读取 SOUL.md 失败: ' + e.message);
829
- }
830
- return true;
715
+ return handleSoulCommand({
716
+ bot,
717
+ chatId,
718
+ soulAction,
719
+ soulText: agentParts.slice(2).join(' ').trim(),
720
+ cwd: normalizeCwd(boundProj.cwd),
721
+ fs,
722
+ path,
723
+ agentTools,
724
+ });
831
725
  }
832
726
 
833
727
 
@@ -853,66 +747,13 @@ function createAgentCommandHandler(deps) {
853
747
 
854
748
  // /activate — bind this unbound chat to the most recently created pending agent
855
749
  if (text === '/activate' || text.startsWith('/activate ')) {
856
- const cfg = loadConfig();
857
- const { boundKey } = getBoundProject(chatId, cfg);
858
- if (boundKey) {
859
- await bot.sendMessage(chatId, `此群已绑定到「${boundKey}」,无需激活。如需更换请先 /agent unbind`);
860
- return true;
861
- }
862
- const activation = getLatestActivationForChat(chatId);
863
- if (!activation) {
864
- // Check if this chat was the creator (self-activate attempt)
865
- if (pendingActivations) {
866
- for (const rec of pendingActivations.values()) {
867
- if (rec.createdByChatId === String(chatId)) {
868
- await bot.sendMessage(chatId,
869
- `❌ 不能在创建来源群激活。\n请在你新建的目标群里发送 \`/activate\`\n\n` +
870
- `或在任意群用: \`/agent bind ${rec.agentName} ${rec.cwd}\``
871
- );
872
- return true;
873
- }
874
- }
875
- }
876
- // No pending activation — fall back to scanning daemon.yaml for unbound projects
877
- const allBoundKeys = new Set(Object.values({
878
- ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
879
- ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
880
- }));
881
- const unboundProjects = Object.entries(cfg.projects || {})
882
- .filter(([key, p]) => p && p.cwd && !allBoundKeys.has(key))
883
- .map(([key, p]) => ({ key, name: p.name || key, cwd: p.cwd, icon: p.icon || '🤖' }));
884
-
885
- if (unboundProjects.length === 1) {
886
- // Exactly one unbound project — auto-bind using project KEY (not display name)
887
- // to ensure toProjectKey() resolves to the correct existing key in daemon.yaml
888
- const proj = unboundProjects[0];
889
- const bindRes2 = await bindViaUnifiedApi(bot, chatId, proj.key, proj.cwd);
890
- if (bindRes2.ok) pendingActivations && pendingActivations.delete(proj.key);
891
- return true;
892
- }
893
-
894
- if (unboundProjects.length > 1) {
895
- // Multiple unbound projects — show pick list using project keys
896
- const lines = ['请选择要激活的 Agent:', ''];
897
- for (const p of unboundProjects) {
898
- lines.push(`${p.icon} ${p.name} → \`/agent bind ${p.key} ${p.cwd}\``);
899
- }
900
- lines.push('\n发送对应命令即可绑定此群。');
901
- await bot.sendMessage(chatId, lines.join('\n'));
902
- return true;
903
- }
904
-
905
- // Truly nothing to activate
906
- await bot.sendMessage(chatId,
907
- '没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
908
- );
909
- return true;
910
- }
911
- const bindRes = await bindViaUnifiedApi(bot, chatId, activation.agentName, activation.cwd);
912
- if (bindRes.ok) {
913
- pendingActivations && pendingActivations.delete(activation.agentKey);
914
- }
915
- return true;
750
+ return handleActivateCommand({
751
+ bot,
752
+ chatId,
753
+ loadConfig,
754
+ pendingActivations,
755
+ bindAgent: (agentName, agentCwd) => bindViaUnifiedApi(bot, chatId, agentName, agentCwd),
756
+ });
916
757
  }
917
758
 
918
759
  // /agent-dir <path>: /agent new 向导的目录选择回调(步骤1→步骤2)
@@ -928,7 +769,7 @@ function createAgentCommandHandler(deps) {
928
769
  pendingAgentFlows.set(String(chatId), flow);
929
770
  const displayPath = dirPath.replace(HOME, '~');
930
771
  const cloneHint = flow.isClone ? '(分身模式)' : '';
931
- await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}${cloneHint}\n\n步骤2/3:给这个 Agent 起个名字?`);
772
+ await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}${cloneHint}\n\n${flow.isClone ? '步骤2/2' : '步骤2/3'}:给这个 Agent 起个名字?`);
932
773
  return true;
933
774
  }
934
775
 
@@ -948,65 +789,30 @@ function createAgentCommandHandler(deps) {
948
789
  // /agent-team-dir <path>: directory picker callback for team creation
949
790
  if (text.startsWith('/agent-team-dir ')) {
950
791
  const dirPath = expandPath(text.slice(16).trim());
951
- const teamFlow = pendingTeamFlows && pendingTeamFlows.get(String(chatId));
792
+ const teamFlow = getFreshTimedFlow(pendingTeamFlows, String(chatId));
952
793
  if (!teamFlow || teamFlow.step !== 'cwd') {
953
794
  await bot.sendMessage(chatId, '❌ 没有待完成的团队创建,请重新发送 /agent new team');
954
795
  return true;
955
796
  }
956
797
  teamFlow.step = 'creating';
957
- pendingTeamFlows.set(String(chatId), teamFlow);
798
+ setTimedFlow(pendingTeamFlows, String(chatId), teamFlow);
958
799
  await bot.sendMessage(chatId, `⏳ 正在创建团队「${teamFlow.name}」...`);
959
800
 
960
801
  try {
961
- const teamDir = path.join(dirPath, 'team');
962
- if (!fs.existsSync(teamDir)) fs.mkdirSync(teamDir, { recursive: true });
963
-
964
802
  const members = Array.isArray(teamFlow.members) ? teamFlow.members : [];
965
- const results = [];
966
- for (const member of members) {
967
- const memberDir = path.join(teamDir, member.key);
968
- if (!fs.existsSync(memberDir)) fs.mkdirSync(memberDir, { recursive: true });
969
- const claudeMdPath = path.join(memberDir, 'CLAUDE.md');
970
- if (!fs.existsSync(claudeMdPath)) {
971
- fs.writeFileSync(claudeMdPath, `# ${member.name}\n\n(团队成员:${teamFlow.name})\n`, 'utf8');
972
- }
973
- // Init git repo for checkpoint support
974
- try {
975
- if (execSync) execSync('git init -q', { cwd: memberDir, stdio: 'ignore' });
976
- } catch { /* non-critical */ }
977
- results.push(`✅ ${member.icon} ${member.key}: ${memberDir.replace(HOME, '~')}`);
978
- }
979
-
980
- // Register in daemon.yaml under parent project's team array
981
- const cfg = loadConfig();
982
- let parentProjectKey = null;
983
- if (cfg.projects) {
984
- for (const [projKey, proj] of Object.entries(cfg.projects)) {
985
- if (normalizeCwd(proj.cwd || '') === normalizeCwd(dirPath)) {
986
- parentProjectKey = projKey;
987
- break;
988
- }
989
- }
990
- }
991
-
992
- if (parentProjectKey && cfg.projects[parentProjectKey]) {
993
- const proj = cfg.projects[parentProjectKey];
994
- if (!proj.team) proj.team = [];
995
- for (const member of members) {
996
- if (!proj.team.some(m => m.key === member.key)) {
997
- proj.team.push({
998
- key: member.key,
999
- name: member.name,
1000
- icon: member.icon,
1001
- color: member.color,
1002
- cwd: path.join(teamDir, member.key),
1003
- nicknames: member.nicknames,
1004
- });
1005
- }
1006
- }
1007
- if (writeConfigSafe) writeConfigSafe(cfg);
1008
- if (backupConfig) backupConfig();
1009
- }
803
+ const { teamDir, parentProjectKey } = createTeamWorkspace({
804
+ fs,
805
+ path,
806
+ execSync,
807
+ dirPath,
808
+ teamName: teamFlow.name,
809
+ members,
810
+ loadConfig,
811
+ normalizeCwd,
812
+ writeConfigSafe,
813
+ backupConfig,
814
+ HOME,
815
+ });
1010
816
 
1011
817
  const memberList = members.map(m => `${m.icon} ${m.key}`).join(' | ');
1012
818
  const yamlNote = parentProjectKey