metame-cli 1.5.21 → 1.5.22

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 {
@@ -163,21 +180,6 @@ function createAgentCommandHandler(deps) {
163
180
  return true;
164
181
  }
165
182
 
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
183
  function resolveTtl(valueOrGetter, fallbackMs) {
182
184
  const raw = typeof valueOrGetter === 'function' ? valueOrGetter() : valueOrGetter;
183
185
  const num = Number(raw);
@@ -206,6 +208,29 @@ function createAgentCommandHandler(deps) {
206
208
  pendingAgentFlows.set(flowKey, { ...flow, __ts: Date.now() });
207
209
  }
208
210
 
211
+ function getFreshTimedFlow(flowMap, flowKey) {
212
+ if (!flowMap) return null;
213
+ const flow = flowMap.get(flowKey);
214
+ if (!flow) return null;
215
+ const FLOW_TTL_MS = resolveTtl(agentFlowTtlMs, 10 * 60 * 1000);
216
+ const ts = Number(flow.__ts || 0);
217
+ if (!(ts > 0) && flow && typeof flow === 'object') {
218
+ const stamped = { ...flow, __ts: Date.now() };
219
+ flowMap.set(flowKey, stamped);
220
+ return stamped;
221
+ }
222
+ if (ts > 0 && (Date.now() - ts) > FLOW_TTL_MS) {
223
+ flowMap.delete(flowKey);
224
+ return null;
225
+ }
226
+ return flow;
227
+ }
228
+
229
+ function setTimedFlow(flowMap, flowKey, flow) {
230
+ if (!flowMap) return;
231
+ flowMap.set(flowKey, { ...flow, __ts: Date.now() });
232
+ }
233
+
209
234
  function setPendingBind(chatKey, agentName) {
210
235
  pendingBinds.set(chatKey, { name: agentName, __ts: Date.now() });
211
236
  }
@@ -229,101 +254,31 @@ function createAgentCommandHandler(deps) {
229
254
  return raw.name || null;
230
255
  }
231
256
 
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
257
  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 };
258
+ return bindAgentToChat({
259
+ agentTools,
260
+ doBindAgent,
261
+ bot,
262
+ chatId,
263
+ agentName,
264
+ agentCwd,
265
+ HOME,
266
+ attachOrCreateSession,
267
+ normalizeCwd,
268
+ getDefaultEngine,
269
+ });
291
270
  }
292
271
 
293
272
  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 } };
273
+ return listAgents({ agentTools, chatId, loadConfig });
310
274
  }
311
275
 
312
276
  async function unbindViaUnifiedApi(chatId) {
313
- if (agentTools && typeof agentTools.unbindCurrentAgent === 'function') {
314
- return agentTools.unbindCurrentAgent(chatId);
315
- }
277
+ return unbindAgent({ agentTools, chatId, loadConfig, writeConfigSafe, backupConfig });
278
+ }
316
279
 
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 } };
280
+ async function editRoleViaUnifiedApi(workspaceDir, deltaText) {
281
+ return editAgentRole({ agentTools, mergeAgentRole, workspaceDir, deltaText });
327
282
  }
328
283
 
329
284
  async function handleAgentCommand(ctx) {
@@ -492,48 +447,68 @@ function createAgentCommandHandler(deps) {
492
447
 
493
448
  // /agent new 多步向导状态机(name/desc 步骤)
494
449
  if (pendingAgentFlows) {
495
- const flow = pendingAgentFlows.get(String(chatId));
450
+ const flow = getFreshTimedFlow(pendingAgentFlows, String(chatId));
496
451
  if (flow && text && !text.startsWith('/')) {
497
452
  if (flow.step === 'name') {
498
453
  flow.name = text.trim();
454
+ if (flow.isClone) {
455
+ pendingAgentFlows.delete(String(chatId));
456
+ return completeAgentCreation({
457
+ bot,
458
+ chatId,
459
+ flow,
460
+ description: '',
461
+ createWorkspaceAgent: ({ chatId: createChatId, agentName, workspaceDir, roleDescription }) => createWorkspaceAgent({
462
+ agentTools,
463
+ chatId: createChatId,
464
+ agentName,
465
+ workspaceDir,
466
+ roleDescription,
467
+ attachOrCreateSession,
468
+ normalizeCwd,
469
+ getDefaultEngine,
470
+ }),
471
+ doBindAgent,
472
+ mergeAgentRole,
473
+ });
474
+ }
499
475
  flow.step = 'desc';
500
- pendingAgentFlows.set(String(chatId), flow);
476
+ setTimedFlow(pendingAgentFlows, String(chatId), flow);
501
477
  await bot.sendMessage(chatId, `好的,Agent 名称是「${flow.name}」\n\n步骤3/3:请描述这个 Agent 的角色和职责(用自然语言):`);
502
478
  return true;
503
479
  }
504
480
  if (flow.step === 'desc') {
505
481
  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;
482
+ return completeAgentCreation({
483
+ bot,
484
+ chatId,
485
+ flow,
486
+ description: text.trim(),
487
+ createWorkspaceAgent: ({ chatId: createChatId, agentName, workspaceDir, roleDescription }) => createWorkspaceAgent({
488
+ agentTools,
489
+ chatId: createChatId,
490
+ agentName,
491
+ workspaceDir,
492
+ roleDescription,
493
+ attachOrCreateSession,
494
+ normalizeCwd,
495
+ getDefaultEngine,
496
+ }),
497
+ doBindAgent,
498
+ mergeAgentRole,
499
+ });
525
500
  }
526
501
  }
527
502
  }
528
503
 
529
504
  // /agent new team 多步向导状态机
530
505
  if (pendingTeamFlows) {
531
- const teamFlow = pendingTeamFlows.get(String(chatId));
506
+ const teamFlow = getFreshTimedFlow(pendingTeamFlows, String(chatId));
532
507
  if (teamFlow && text && !text.startsWith('/')) {
533
508
  if (teamFlow.step === 'name') {
534
509
  teamFlow.name = text.trim();
535
510
  teamFlow.step = 'members';
536
- pendingTeamFlows.set(String(chatId), teamFlow);
511
+ setTimedFlow(pendingTeamFlows, String(chatId), teamFlow);
537
512
  await bot.sendMessage(chatId, `团队名称:「${teamFlow.name}」
538
513
 
539
514
  请输入成员列表,格式:
@@ -549,26 +524,14 @@ function createAgentCommandHandler(deps) {
549
524
  }
550
525
 
551
526
  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
- }
527
+ const members = parseTeamMembers(text, teamFlow.name);
565
528
  if (members.length === 0) {
566
529
  await bot.sendMessage(chatId, '⚠️ 请至少添加一个成员,格式:名称:icon:颜色');
567
530
  return true;
568
531
  }
569
532
  teamFlow.members = members;
570
533
  teamFlow.step = 'cwd';
571
- pendingTeamFlows.set(String(chatId), teamFlow);
534
+ setTimedFlow(pendingTeamFlows, String(chatId), teamFlow);
572
535
  const memberList = members.map(m => `${m.icon} ${m.name} (${m.color})`).join('\n');
573
536
  await bot.sendMessage(chatId, `✅ 成员配置:\n\n${memberList}\n\n正在选择父目录...`);
574
537
  await sendBrowse(bot, chatId, 'team-new', HOME, `为「${teamFlow.name}」选择父工作目录`);
@@ -601,37 +564,17 @@ function createAgentCommandHandler(deps) {
601
564
 
602
565
  // /agent new [team] — 创建新 Agent 或团队
603
566
  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;
567
+ return startNewAgentWizard({
568
+ bot,
569
+ chatId,
570
+ secondArg: agentParts[1],
571
+ pendingTeamFlows,
572
+ pendingAgentFlows,
573
+ loadConfig,
574
+ normalizeCwd,
575
+ sendBrowse,
576
+ HOME,
577
+ });
635
578
  }
636
579
 
637
580
  // /agent bind <名称> [目录]
@@ -703,12 +646,7 @@ function createAgentCommandHandler(deps) {
703
646
  return true;
704
647
  }
705
648
 
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
- }
649
+ const currentContent = readAgentRolePreview({ fs, path, cwd });
712
650
  setFlow(String(chatId) + ':edit', { cwd });
713
651
  await bot.sendMessage(chatId, `📄 当前 CLAUDE.md 内容:\n\`\`\`\n${currentContent}\n\`\`\`\n\n请描述你想做的修改(用自然语言,例如:「把角色改成后端工程师,专注 Python」):`);
714
652
  return true;
@@ -738,18 +676,15 @@ function createAgentCommandHandler(deps) {
738
676
  return true;
739
677
  }
740
678
  const cwd = normalizeCwd(boundProj.cwd);
741
- const claudeMdPath = path.join(cwd, 'CLAUDE.md');
742
- if (!fs.existsSync(claudeMdPath)) {
679
+ const resetResult = resetAgentRoleSection({ fs, path, cwd });
680
+ if (resetResult.status === 'missing') {
743
681
  await bot.sendMessage(chatId, '⚠️ CLAUDE.md 不存在,无需重置');
744
682
  return true;
745
683
  }
746
- const before = fs.readFileSync(claudeMdPath, 'utf8');
747
- const after = before.replace(/(?:^|\n)## Agent 角色\n[\s\S]*?(?=\n## |$)/, '').trimStart();
748
- if (after === before.trimStart()) {
684
+ if (resetResult.status === 'unchanged') {
749
685
  await bot.sendMessage(chatId, '⚠️ 未找到「## Agent 角色」section,CLAUDE.md 未修改');
750
686
  return true;
751
687
  }
752
- fs.writeFileSync(claudeMdPath, after, 'utf8');
753
688
  await bot.sendMessage(chatId, '✅ 已删除角色 section,请重新发送角色描述(/agent edit 或自然语言修改)');
754
689
  return true;
755
690
  }
@@ -768,66 +703,16 @@ function createAgentCommandHandler(deps) {
768
703
  await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先 /agent bind <名称> <目录>');
769
704
  return true;
770
705
  }
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;
706
+ return handleSoulCommand({
707
+ bot,
708
+ chatId,
709
+ soulAction,
710
+ soulText: agentParts.slice(2).join(' ').trim(),
711
+ cwd: normalizeCwd(boundProj.cwd),
712
+ fs,
713
+ path,
714
+ agentTools,
715
+ });
831
716
  }
832
717
 
833
718
 
@@ -853,66 +738,13 @@ function createAgentCommandHandler(deps) {
853
738
 
854
739
  // /activate — bind this unbound chat to the most recently created pending agent
855
740
  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;
741
+ return handleActivateCommand({
742
+ bot,
743
+ chatId,
744
+ loadConfig,
745
+ pendingActivations,
746
+ bindAgent: (agentName, agentCwd) => bindViaUnifiedApi(bot, chatId, agentName, agentCwd),
747
+ });
916
748
  }
917
749
 
918
750
  // /agent-dir <path>: /agent new 向导的目录选择回调(步骤1→步骤2)
@@ -928,7 +760,7 @@ function createAgentCommandHandler(deps) {
928
760
  pendingAgentFlows.set(String(chatId), flow);
929
761
  const displayPath = dirPath.replace(HOME, '~');
930
762
  const cloneHint = flow.isClone ? '(分身模式)' : '';
931
- await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}${cloneHint}\n\n步骤2/3:给这个 Agent 起个名字?`);
763
+ await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}${cloneHint}\n\n${flow.isClone ? '步骤2/2' : '步骤2/3'}:给这个 Agent 起个名字?`);
932
764
  return true;
933
765
  }
934
766
 
@@ -948,65 +780,30 @@ function createAgentCommandHandler(deps) {
948
780
  // /agent-team-dir <path>: directory picker callback for team creation
949
781
  if (text.startsWith('/agent-team-dir ')) {
950
782
  const dirPath = expandPath(text.slice(16).trim());
951
- const teamFlow = pendingTeamFlows && pendingTeamFlows.get(String(chatId));
783
+ const teamFlow = getFreshTimedFlow(pendingTeamFlows, String(chatId));
952
784
  if (!teamFlow || teamFlow.step !== 'cwd') {
953
785
  await bot.sendMessage(chatId, '❌ 没有待完成的团队创建,请重新发送 /agent new team');
954
786
  return true;
955
787
  }
956
788
  teamFlow.step = 'creating';
957
- pendingTeamFlows.set(String(chatId), teamFlow);
789
+ setTimedFlow(pendingTeamFlows, String(chatId), teamFlow);
958
790
  await bot.sendMessage(chatId, `⏳ 正在创建团队「${teamFlow.name}」...`);
959
791
 
960
792
  try {
961
- const teamDir = path.join(dirPath, 'team');
962
- if (!fs.existsSync(teamDir)) fs.mkdirSync(teamDir, { recursive: true });
963
-
964
793
  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
- }
794
+ const { teamDir, parentProjectKey } = createTeamWorkspace({
795
+ fs,
796
+ path,
797
+ execSync,
798
+ dirPath,
799
+ teamName: teamFlow.name,
800
+ members,
801
+ loadConfig,
802
+ normalizeCwd,
803
+ writeConfigSafe,
804
+ backupConfig,
805
+ HOME,
806
+ });
1010
807
 
1011
808
  const memberList = members.map(m => `${m.icon} ${m.key}`).join(' | ');
1012
809
  const yamlNote = parentProjectKey