metame-cli 1.5.2 → 1.5.4
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 +90 -17
- package/index.js +76 -25
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +167 -90
- package/scripts/daemon-admin-commands.js +225 -24
- package/scripts/daemon-agent-commands.js +263 -8
- package/scripts/daemon-bridges.js +395 -6
- package/scripts/daemon-claude-engine.js +749 -582
- package/scripts/daemon-command-router.js +104 -0
- package/scripts/daemon-default.yaml +9 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +8 -5
- package/scripts/daemon-file-browser.js +1 -0
- package/scripts/daemon-remote-dispatch.js +82 -0
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +19 -11
- package/scripts/daemon-session-store.js +26 -8
- package/scripts/daemon-task-scheduler.js +2 -2
- package/scripts/daemon.js +363 -8
- package/scripts/daemon.yaml +356 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +131 -0
- package/scripts/docs/maintenance-manual.md +214 -3
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-agent-manage.js +50 -0
- package/scripts/hooks/intent-engine.js +103 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-hook-config.js +28 -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/memory-extract.js +1 -1
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory-write.js +21 -4
- package/scripts/memory.js +55 -17
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
- package/scripts/team-dispatch.js +176 -0
|
@@ -6,7 +6,9 @@ const {
|
|
|
6
6
|
USAGE_CATEGORY_LABEL,
|
|
7
7
|
} = require('./usage-classifier');
|
|
8
8
|
const { IS_WIN } = require('./platform');
|
|
9
|
-
const { ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
|
|
9
|
+
const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
|
|
10
|
+
const { resolveProjectKey: _resolveProjectKey } = require('./team-dispatch');
|
|
11
|
+
const { parseRemoteTargetRef, normalizeRemoteDispatchConfig } = require('./daemon-remote-dispatch');
|
|
10
12
|
let mentorEngine = null;
|
|
11
13
|
try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
|
|
12
14
|
|
|
@@ -39,15 +41,26 @@ function createAdminCommandHandler(deps) {
|
|
|
39
41
|
getDistillModel = () => 'haiku',
|
|
40
42
|
} = deps;
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// resolveProjectKey: imported from team-dispatch.js (shared with dispatch_to and daemon.js)
|
|
45
|
+
const resolveProjectKey = _resolveProjectKey;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a target name to { dispatchKey, projInfo }.
|
|
49
|
+
* resolveProjectKey returns 'parent/member' for team members; this splits
|
|
50
|
+
* it into the bare dispatch key and looks up the project/member config.
|
|
51
|
+
*/
|
|
52
|
+
function resolveDispatchTarget(targetName, projects) {
|
|
53
|
+
const resolved = resolveProjectKey(targetName, projects || {});
|
|
54
|
+
if (!resolved) return null;
|
|
55
|
+
if (!resolved.includes('/')) {
|
|
56
|
+
return { dispatchKey: resolved, projInfo: (projects || {})[resolved] || {} };
|
|
49
57
|
}
|
|
50
|
-
|
|
58
|
+
const [parentKey, memberKey] = resolved.split('/');
|
|
59
|
+
const parent = (projects || {})[parentKey] || {};
|
|
60
|
+
const member = Array.isArray(parent.team)
|
|
61
|
+
? (parent.team.find(m => m.key === memberKey) || {})
|
|
62
|
+
: {};
|
|
63
|
+
return { dispatchKey: memberKey, projInfo: member };
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
function resolveSenderKey(chatId, config) {
|
|
@@ -611,23 +624,81 @@ function createAdminCommandHandler(deps) {
|
|
|
611
624
|
return { handled: true, config };
|
|
612
625
|
}
|
|
613
626
|
|
|
627
|
+
// /dispatch peers — show remote dispatch config
|
|
628
|
+
if (args === 'peers') {
|
|
629
|
+
const rd = normalizeRemoteDispatchConfig(config);
|
|
630
|
+
if (!rd) {
|
|
631
|
+
await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
|
|
632
|
+
return { handled: true, config };
|
|
633
|
+
}
|
|
634
|
+
let msg = `📡 远端 Dispatch 配置\n─────────────\nself: ${rd.selfPeer}\nrelay chat: ${rd.chatId}\n\n远端成员:\n`;
|
|
635
|
+
let hasRemote = false;
|
|
636
|
+
for (const [key, proj] of Object.entries(config.projects || {})) {
|
|
637
|
+
if (!Array.isArray(proj.team)) continue;
|
|
638
|
+
for (const m of proj.team) {
|
|
639
|
+
if (m.peer) {
|
|
640
|
+
hasRemote = true;
|
|
641
|
+
msg += `- ${m.icon || '🤖'} ${m.name || m.key} → peer:${m.peer} (${key}/${m.key})\n`;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (!hasRemote) msg += '(无远端成员)\n';
|
|
646
|
+
await bot.sendMessage(chatId, msg.trim());
|
|
647
|
+
return { handled: true, config };
|
|
648
|
+
}
|
|
649
|
+
|
|
614
650
|
// /dispatch to <agent> <prompt>
|
|
615
651
|
const toMatch = args.match(/^to\s+(\S+)\s+(.+)$/s);
|
|
616
652
|
if (toMatch) {
|
|
617
653
|
const targetName = toMatch[1];
|
|
618
654
|
const prompt = toMatch[2].trim();
|
|
655
|
+
const senderKey = resolveSenderKey(chatId, config);
|
|
619
656
|
|
|
620
|
-
//
|
|
621
|
-
const
|
|
622
|
-
if (
|
|
623
|
-
|
|
657
|
+
// Check for remote target (peer:project format)
|
|
658
|
+
const remoteTarget = parseRemoteTargetRef(targetName);
|
|
659
|
+
if (remoteTarget && deps.sendRemoteDispatch) {
|
|
660
|
+
const res = await deps.sendRemoteDispatch({
|
|
661
|
+
type: 'task',
|
|
662
|
+
to_peer: remoteTarget.peer,
|
|
663
|
+
target_project: remoteTarget.project,
|
|
664
|
+
prompt,
|
|
665
|
+
source_chat_id: String(chatId),
|
|
666
|
+
source_sender_key: senderKey,
|
|
667
|
+
}, config);
|
|
668
|
+
if (res.success) {
|
|
669
|
+
await bot.sendMessage(chatId, `📡 已发送给 ${remoteTarget.peer}:${remoteTarget.project}`);
|
|
670
|
+
} else {
|
|
671
|
+
await bot.sendMessage(chatId, `❌ 远端派发失败: ${res.error}`);
|
|
672
|
+
}
|
|
624
673
|
return { handled: true, config };
|
|
625
674
|
}
|
|
626
675
|
|
|
627
|
-
//
|
|
628
|
-
const
|
|
676
|
+
// Resolve target by project key or nickname (handles team members via compound key)
|
|
677
|
+
const resolved = resolveDispatchTarget(targetName, config.projects || {});
|
|
678
|
+
if (!resolved) {
|
|
679
|
+
await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
|
|
680
|
+
return { handled: true, config };
|
|
681
|
+
}
|
|
682
|
+
const { dispatchKey: targetKey, projInfo } = resolved;
|
|
683
|
+
|
|
684
|
+
// Check if resolved target is a remote team member
|
|
685
|
+
if (projInfo.peer && deps.sendRemoteDispatch) {
|
|
686
|
+
const res = await deps.sendRemoteDispatch({
|
|
687
|
+
type: 'task',
|
|
688
|
+
to_peer: projInfo.peer,
|
|
689
|
+
target_project: targetKey,
|
|
690
|
+
prompt,
|
|
691
|
+
source_chat_id: String(chatId),
|
|
692
|
+
source_sender_key: senderKey,
|
|
693
|
+
}, config);
|
|
694
|
+
if (res.success) {
|
|
695
|
+
await bot.sendMessage(chatId, `📡 已发送给 ${projInfo.icon || '🤖'} ${projInfo.name || targetKey} (${projInfo.peer})`);
|
|
696
|
+
} else {
|
|
697
|
+
await bot.sendMessage(chatId, `❌ 远端派发失败: ${res.error}`);
|
|
698
|
+
}
|
|
699
|
+
return { handled: true, config };
|
|
700
|
+
}
|
|
629
701
|
|
|
630
|
-
const projInfo = config.projects[targetKey] || {};
|
|
631
702
|
// Find the target project's own Feishu chat (reverse lookup of chat_agent_map)
|
|
632
703
|
const feishuChatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
633
704
|
const targetChatId = Object.entries(feishuChatAgentMap).find(([, v]) => v === targetKey)?.[0] || null;
|
|
@@ -662,13 +733,52 @@ function createAdminCommandHandler(deps) {
|
|
|
662
733
|
'用法:',
|
|
663
734
|
'/dispatch status — 查看状态',
|
|
664
735
|
'/dispatch log — 查看记录',
|
|
736
|
+
'/dispatch peers — 查看远端配置',
|
|
665
737
|
'/dispatch to <agent> <任务内容> — 直接跨 agent 派发',
|
|
738
|
+
'/dispatch to <peer:project> <任务内容> — 跨设备派发',
|
|
666
739
|
'/TeamTask create <agent> <目标> [--scope <id>] [--parent <id>] — 创建/续接 TeamTask',
|
|
667
740
|
'/TeamTask — 查看 TeamTask 列表',
|
|
668
741
|
].join('\n'));
|
|
669
742
|
return { handled: true, config };
|
|
670
743
|
}
|
|
671
744
|
|
|
745
|
+
// /msg — team internal messaging (like sessions_send)
|
|
746
|
+
if (text.startsWith('/msg ')) {
|
|
747
|
+
const args = text.slice('/msg '.length).trim();
|
|
748
|
+
const msgMatch = args.match(/^(\S+)\s+(.+)$/s);
|
|
749
|
+
if (!msgMatch) {
|
|
750
|
+
await bot.sendMessage(chatId, '用法: /msg <agent> <消息内容>\n示例: /msg 甲 帮我看看这个文档');
|
|
751
|
+
return { handled: true, config };
|
|
752
|
+
}
|
|
753
|
+
const targetName = msgMatch[1];
|
|
754
|
+
const message = msgMatch[2].trim();
|
|
755
|
+
|
|
756
|
+
// Resolve target by nickname or key (handles team members via compound key)
|
|
757
|
+
const senderKey = resolveSenderKey(chatId, config);
|
|
758
|
+
const resolved = resolveDispatchTarget(targetName, config.projects || {});
|
|
759
|
+
|
|
760
|
+
if (!resolved) {
|
|
761
|
+
await bot.sendMessage(chatId, `未找到 agent: ${targetName}`);
|
|
762
|
+
return { handled: true, config };
|
|
763
|
+
}
|
|
764
|
+
const { dispatchKey: targetKey, projInfo: toProj } = resolved;
|
|
765
|
+
|
|
766
|
+
const result = dispatchTask(targetKey, {
|
|
767
|
+
from: senderKey,
|
|
768
|
+
type: 'message',
|
|
769
|
+
priority: 'normal',
|
|
770
|
+
payload: { title: 'team message', prompt: `[来自团队的消息]\n\n${message}` },
|
|
771
|
+
callback: false,
|
|
772
|
+
}, config, null, null);
|
|
773
|
+
|
|
774
|
+
if (result.success) {
|
|
775
|
+
await bot.sendMessage(chatId, `📬 已发送消息给 ${toProj.icon || '🤖'} ${toProj.name || targetKey}`);
|
|
776
|
+
} else {
|
|
777
|
+
await bot.sendMessage(chatId, `❌ 发送失败: ${result.error}`);
|
|
778
|
+
}
|
|
779
|
+
return { handled: true, config };
|
|
780
|
+
}
|
|
781
|
+
|
|
672
782
|
if (text === '/budget') {
|
|
673
783
|
const limit = (config.budget && config.budget.daily_limit) || 50000;
|
|
674
784
|
const used = state.budget.tokens_used;
|
|
@@ -676,6 +786,102 @@ function createAdminCommandHandler(deps) {
|
|
|
676
786
|
return { handled: true, config };
|
|
677
787
|
}
|
|
678
788
|
|
|
789
|
+
if (text === '/reset-budget') {
|
|
790
|
+
if (!state.budget) state.budget = {};
|
|
791
|
+
state.budget.tokens_used = 0;
|
|
792
|
+
state.budget.date = new Date().toISOString().slice(0, 10);
|
|
793
|
+
await bot.sendMessage(chatId, `✅ Budget 已重置 (${state.budget.date})`);
|
|
794
|
+
return { handled: true, config };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (text === '/toggle' || text.startsWith('/toggle ')) {
|
|
798
|
+
const arg = text.slice('/toggle'.length).trim();
|
|
799
|
+
const cfg = config;
|
|
800
|
+
const tasks = (cfg.heartbeat && cfg.heartbeat.tasks) || [];
|
|
801
|
+
|
|
802
|
+
// Group mapping: friendly name → task names
|
|
803
|
+
const groups = {
|
|
804
|
+
cognition: ['cognitive-distill', 'self-reflect'],
|
|
805
|
+
memory: ['memory-extract', 'nightly-reflect', 'memory-gc', 'memory-index'],
|
|
806
|
+
skill: ['skill-evolve'],
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
if (!arg) {
|
|
810
|
+
// Show status
|
|
811
|
+
const lines = ['⚙️ 后台任务开关:'];
|
|
812
|
+
for (const [group, names] of Object.entries(groups)) {
|
|
813
|
+
const statuses = names.map(n => {
|
|
814
|
+
const t = tasks.find(t2 => t2.name === n);
|
|
815
|
+
return t ? (t.enabled !== false ? '✅' : '❌') : '⚠️';
|
|
816
|
+
});
|
|
817
|
+
const allOn = statuses.every(s => s === '✅');
|
|
818
|
+
const allOff = statuses.every(s => s === '❌');
|
|
819
|
+
lines.push(` ${allOn ? '✅' : allOff ? '❌' : '⚠️'} ${group}`);
|
|
820
|
+
}
|
|
821
|
+
lines.push('', '用法: /toggle <cognition|memory|skill> <on|off>');
|
|
822
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
823
|
+
return { handled: true, config };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const parts = arg.split(/\s+/);
|
|
827
|
+
const groupName = parts[0];
|
|
828
|
+
const action = parts[1];
|
|
829
|
+
|
|
830
|
+
if (!groups[groupName]) {
|
|
831
|
+
await bot.sendMessage(chatId, `未知分组: ${groupName}\n可选: cognition, memory, skill`);
|
|
832
|
+
return { handled: true, config };
|
|
833
|
+
}
|
|
834
|
+
if (action !== 'on' && action !== 'off') {
|
|
835
|
+
await bot.sendMessage(chatId, `用法: /toggle ${groupName} <on|off>`);
|
|
836
|
+
return { handled: true, config };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const enabled = action === 'on';
|
|
840
|
+
const affected = [];
|
|
841
|
+
for (const name of groups[groupName]) {
|
|
842
|
+
const t = tasks.find(t2 => t2.name === name);
|
|
843
|
+
if (t) {
|
|
844
|
+
t.enabled = enabled;
|
|
845
|
+
affected.push(name);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (affected.length === 0) {
|
|
849
|
+
await bot.sendMessage(chatId, `⚠️ 未找到 ${groupName} 相关任务,请检查 heartbeat 配置`);
|
|
850
|
+
return { handled: true, config };
|
|
851
|
+
}
|
|
852
|
+
writeConfigSafe(cfg);
|
|
853
|
+
config = loadConfig();
|
|
854
|
+
await bot.sendMessage(chatId, `${enabled ? '✅' : '❌'} ${groupName} ${enabled ? 'ON' : 'OFF'} (${affected.join(', ')})`);
|
|
855
|
+
return { handled: true, config };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// /broadcast [on|off] — toggle team broadcast for the current chat's bound project
|
|
859
|
+
if (text === '/broadcast' || text.startsWith('/broadcast ')) {
|
|
860
|
+
const arg = text.slice('/broadcast'.length).trim();
|
|
861
|
+
const cfg = config;
|
|
862
|
+
const feishuMap = { ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}), ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}) };
|
|
863
|
+
const boundKey = feishuMap[String(chatId)];
|
|
864
|
+
const boundProj = boundKey && cfg.projects ? cfg.projects[boundKey] : null;
|
|
865
|
+
if (!boundProj || !Array.isArray(boundProj.team) || boundProj.team.length === 0) {
|
|
866
|
+
await bot.sendMessage(chatId, '⚠️ 当前群没有绑定 team 项目');
|
|
867
|
+
return { handled: true, config };
|
|
868
|
+
}
|
|
869
|
+
if (!arg) {
|
|
870
|
+
const status = boundProj.broadcast ? '✅ ON' : '❌ OFF';
|
|
871
|
+
await bot.sendMessage(chatId, `📢 团队广播: ${status}\n\n用法: /broadcast on|off\n开启后 team 成员间的传话会在群里可见`);
|
|
872
|
+
return { handled: true, config };
|
|
873
|
+
}
|
|
874
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
875
|
+
await bot.sendMessage(chatId, '用法: /broadcast on|off');
|
|
876
|
+
return { handled: true, config };
|
|
877
|
+
}
|
|
878
|
+
cfg.projects[boundKey].broadcast = arg === 'on';
|
|
879
|
+
writeConfigSafe(cfg);
|
|
880
|
+
config = loadConfig();
|
|
881
|
+
await bot.sendMessage(chatId, `📢 团队广播已${arg === 'on' ? '开启' : '关闭'}`);
|
|
882
|
+
return { handled: true, config };
|
|
883
|
+
}
|
|
884
|
+
|
|
679
885
|
if (text === '/usage' || text.startsWith('/usage ')) {
|
|
680
886
|
const arg = text.slice('/usage'.length).trim() || 'today';
|
|
681
887
|
const usage = state.usage || {};
|
|
@@ -999,9 +1205,7 @@ function createAdminCommandHandler(deps) {
|
|
|
999
1205
|
);
|
|
1000
1206
|
const optionValues = optionEntries.map(o => o.value);
|
|
1001
1207
|
const daemonCfg = config.daemon || {};
|
|
1002
|
-
const currentModel = (
|
|
1003
|
-
|| daemonCfg.model // legacy fallback
|
|
1004
|
-
|| engineCfg.main;
|
|
1208
|
+
const currentModel = resolveEngineModel(currentEngine, daemonCfg);
|
|
1005
1209
|
// providerMod manages Claude providers only — for codex use engineCfg.provider
|
|
1006
1210
|
const activeProvider = (currentEngine === 'claude' && providerMod)
|
|
1007
1211
|
? providerMod.getActiveName()
|
|
@@ -1089,8 +1293,7 @@ function createAdminCommandHandler(deps) {
|
|
|
1089
1293
|
: curEngineCfg.provider;
|
|
1090
1294
|
const distill = getDistillModel();
|
|
1091
1295
|
const daemonCfg = config.daemon || {};
|
|
1092
|
-
const currentModel = (
|
|
1093
|
-
|| daemonCfg.model || curEngineCfg.main;
|
|
1296
|
+
const currentModel = resolveEngineModel(cur, daemonCfg);
|
|
1094
1297
|
await bot.sendMessage(chatId, [
|
|
1095
1298
|
`🔧 引擎: ${cur} | Provider: ${activeProvider}`,
|
|
1096
1299
|
`🤖 会话模型: ${currentModel} | 后台轻量: ${distill}`,
|
|
@@ -1111,9 +1314,7 @@ function createAdminCommandHandler(deps) {
|
|
|
1111
1314
|
const distill = getDistillModel();
|
|
1112
1315
|
const freshCfg = loadConfig();
|
|
1113
1316
|
const freshDaemon = freshCfg.daemon || {};
|
|
1114
|
-
const
|
|
1115
|
-
const syncedModel = (freshDaemon.models && freshDaemon.models[arg])
|
|
1116
|
-
|| freshDaemon.model || targetEngineCfg.main;
|
|
1317
|
+
const syncedModel = resolveEngineModel(arg, freshDaemon);
|
|
1117
1318
|
|
|
1118
1319
|
// Auto-switch provider if the preferred one exists in providers.yaml
|
|
1119
1320
|
let providerNote = '';
|
|
@@ -21,7 +21,11 @@ function createAgentCommandHandler(deps) {
|
|
|
21
21
|
getSessionRecentContext,
|
|
22
22
|
pendingBinds,
|
|
23
23
|
pendingAgentFlows,
|
|
24
|
+
pendingTeamFlows,
|
|
24
25
|
pendingActivations,
|
|
26
|
+
writeConfigSafe,
|
|
27
|
+
backupConfig,
|
|
28
|
+
execSync,
|
|
25
29
|
doBindAgent,
|
|
26
30
|
mergeAgentRole,
|
|
27
31
|
agentTools,
|
|
@@ -244,12 +248,36 @@ function createAgentCommandHandler(deps) {
|
|
|
244
248
|
const config = ctx.config || {};
|
|
245
249
|
const text = ctx.text || '';
|
|
246
250
|
|
|
251
|
+
// /cancel — 取消任何挂起的向导流
|
|
252
|
+
if (text === '/cancel') {
|
|
253
|
+
let cancelled = false;
|
|
254
|
+
if (pendingTeamFlows && pendingTeamFlows.has(String(chatId))) {
|
|
255
|
+
pendingTeamFlows.delete(String(chatId));
|
|
256
|
+
cancelled = true;
|
|
257
|
+
}
|
|
258
|
+
if (pendingAgentFlows && pendingAgentFlows.has(String(chatId))) {
|
|
259
|
+
pendingAgentFlows.delete(String(chatId));
|
|
260
|
+
cancelled = true;
|
|
261
|
+
}
|
|
262
|
+
if (pendingBinds && pendingBinds.has(String(chatId))) {
|
|
263
|
+
pendingBinds.delete(String(chatId));
|
|
264
|
+
cancelled = true;
|
|
265
|
+
}
|
|
266
|
+
await bot.sendMessage(chatId, cancelled ? '✅ 已取消当前操作' : '没有进行中的操作');
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
247
270
|
if (text === '/resume' || text.startsWith('/resume ')) {
|
|
248
271
|
const arg = text.slice(7).trim();
|
|
249
272
|
|
|
250
|
-
// Get current workdir to scope session list
|
|
273
|
+
// Get current workdir to scope session list — prefer bound project cwd over session cwd
|
|
274
|
+
const cfgForResume = loadConfig();
|
|
275
|
+
const chatAgentMapForResume = { ...(cfgForResume.telegram ? cfgForResume.telegram.chat_agent_map : {}), ...(cfgForResume.feishu ? cfgForResume.feishu.chat_agent_map : {}) };
|
|
276
|
+
const boundKeyForResume = chatAgentMapForResume[String(chatId)];
|
|
277
|
+
const boundProjForResume = boundKeyForResume && cfgForResume.projects ? cfgForResume.projects[boundKeyForResume] : null;
|
|
278
|
+
const boundCwdForResume = (boundProjForResume && boundProjForResume.cwd) ? normalizeCwd(boundProjForResume.cwd) : null;
|
|
251
279
|
const curSession = getSession(chatId);
|
|
252
|
-
const curCwd = curSession ? curSession.cwd : null;
|
|
280
|
+
const curCwd = boundCwdForResume || (curSession ? curSession.cwd : null);
|
|
253
281
|
const recentSessions = listRecentSessions(5, curCwd);
|
|
254
282
|
|
|
255
283
|
if (!arg) {
|
|
@@ -297,13 +325,17 @@ function createAgentCommandHandler(deps) {
|
|
|
297
325
|
|
|
298
326
|
const state2 = loadState();
|
|
299
327
|
const cfgForEngine = loadConfig();
|
|
300
|
-
const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
328
|
+
const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd) || getDefaultEngine();
|
|
329
|
+
// For bound chats, write session to virtual chatId (_agent_{key}) so askClaude picks it up
|
|
330
|
+
const resumeChatAgentMap = { ...(cfgForEngine.telegram ? cfgForEngine.telegram.chat_agent_map : {}), ...(cfgForEngine.feishu ? cfgForEngine.feishu.chat_agent_map : {}) };
|
|
331
|
+
const resumeBoundKey = resumeChatAgentMap[String(chatId)];
|
|
332
|
+
const sessionKey = resumeBoundKey ? `_agent_${resumeBoundKey}` : String(chatId);
|
|
333
|
+
const existing = state2.sessions[sessionKey] || {};
|
|
334
|
+
const existingEngines = existing.engines || {};
|
|
335
|
+
state2.sessions[sessionKey] = {
|
|
336
|
+
...existing,
|
|
304
337
|
cwd,
|
|
305
|
-
started: true,
|
|
306
|
-
engine: engineByTargetCwd || currentEngine,
|
|
338
|
+
engines: { ...existingEngines, [engineByTargetCwd]: { id: sessionId, started: true } },
|
|
307
339
|
};
|
|
308
340
|
saveState(state2);
|
|
309
341
|
const name = fullMatch.customTitle;
|
|
@@ -332,6 +364,93 @@ function createAgentCommandHandler(deps) {
|
|
|
332
364
|
|
|
333
365
|
// wizard state machine removed — use natural language to create agents
|
|
334
366
|
|
|
367
|
+
// /agent new 多步向导状态机(name/desc 步骤)
|
|
368
|
+
if (pendingAgentFlows) {
|
|
369
|
+
const flow = pendingAgentFlows.get(String(chatId));
|
|
370
|
+
if (flow && text && !text.startsWith('/')) {
|
|
371
|
+
if (flow.step === 'name') {
|
|
372
|
+
flow.name = text.trim();
|
|
373
|
+
flow.step = 'desc';
|
|
374
|
+
pendingAgentFlows.set(String(chatId), flow);
|
|
375
|
+
await bot.sendMessage(chatId, `好的,Agent 名称是「${flow.name}」\n\n步骤3/3:请描述这个 Agent 的角色和职责(用自然语言):`);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (flow.step === 'desc') {
|
|
379
|
+
pendingAgentFlows.delete(String(chatId));
|
|
380
|
+
const { dir, name, isClone, parentCwd } = flow;
|
|
381
|
+
const description = text.trim();
|
|
382
|
+
await bot.sendMessage(chatId, `⏳ 正在配置 Agent「${name}」,稍等...`);
|
|
383
|
+
try {
|
|
384
|
+
await doBindAgent(bot, chatId, name, dir);
|
|
385
|
+
const mergeResult = await mergeAgentRole(dir, description, isClone, parentCwd);
|
|
386
|
+
if (mergeResult && mergeResult.error) {
|
|
387
|
+
await bot.sendMessage(chatId, `⚠️ CLAUDE.md 合并失败: ${mergeResult.error},其他配置已保存`);
|
|
388
|
+
} else if (mergeResult && mergeResult.symlinked) {
|
|
389
|
+
await bot.sendMessage(chatId, `🔗 CLAUDE.md 已链接到父 Agent(分身模式)\n✅ Agent「${name}」创建完成`);
|
|
390
|
+
} else if (mergeResult && mergeResult.created) {
|
|
391
|
+
await bot.sendMessage(chatId, `📝 已创建 CLAUDE.md\n✅ Agent「${name}」创建完成`);
|
|
392
|
+
} else {
|
|
393
|
+
await bot.sendMessage(chatId, `📝 已更新 CLAUDE.md\n✅ Agent「${name}」创建完成`);
|
|
394
|
+
}
|
|
395
|
+
} catch (e) {
|
|
396
|
+
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${e.message}`);
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// /agent new team 多步向导状态机
|
|
404
|
+
if (pendingTeamFlows) {
|
|
405
|
+
const teamFlow = pendingTeamFlows.get(String(chatId));
|
|
406
|
+
if (teamFlow && text && !text.startsWith('/')) {
|
|
407
|
+
if (teamFlow.step === 'name') {
|
|
408
|
+
teamFlow.name = text.trim();
|
|
409
|
+
teamFlow.step = 'members';
|
|
410
|
+
pendingTeamFlows.set(String(chatId), teamFlow);
|
|
411
|
+
await bot.sendMessage(chatId, `团队名称:「${teamFlow.name}」
|
|
412
|
+
|
|
413
|
+
请输入成员列表,格式:
|
|
414
|
+
名称:icon:颜色
|
|
415
|
+
|
|
416
|
+
可用颜色:green, yellow, red, blue, purple, orange, pink, indigo
|
|
417
|
+
|
|
418
|
+
示例:
|
|
419
|
+
编剧:✍️:green, 审核:🔍:yellow, 推广:📢:red
|
|
420
|
+
|
|
421
|
+
一行一个成员,或用逗号分隔多个`);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (teamFlow.step === 'members') {
|
|
426
|
+
const validColors = ['green', 'yellow', 'red', 'blue', 'purple', 'orange', 'pink', 'indigo'];
|
|
427
|
+
const memberLines = text.split(/[,,\n]/).filter(l => l.trim());
|
|
428
|
+
const members = [];
|
|
429
|
+
for (const line of memberLines) {
|
|
430
|
+
const parts = line.trim().split(':');
|
|
431
|
+
const name = parts[0] && parts[0].trim();
|
|
432
|
+
const icon = (parts[1] && parts[1].trim()) || '🤖';
|
|
433
|
+
const rawColor = parts[2] && parts[2].trim().toLowerCase();
|
|
434
|
+
const color = validColors.includes(rawColor) ? rawColor : validColors[members.length % validColors.length];
|
|
435
|
+
if (name) {
|
|
436
|
+
members.push({ key: name, name: `${teamFlow.name} · ${name}`, icon, color, nicknames: [name] });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (members.length === 0) {
|
|
440
|
+
await bot.sendMessage(chatId, '⚠️ 请至少添加一个成员,格式:名称:icon:颜色');
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
teamFlow.members = members;
|
|
444
|
+
teamFlow.step = 'cwd';
|
|
445
|
+
pendingTeamFlows.set(String(chatId), teamFlow);
|
|
446
|
+
const memberList = members.map(m => `${m.icon} ${m.name} (${m.color})`).join('\n');
|
|
447
|
+
await bot.sendMessage(chatId, `✅ 成员配置:\n\n${memberList}\n\n正在选择父目录...`);
|
|
448
|
+
await sendBrowse(bot, chatId, 'team-new', HOME, `为「${teamFlow.name}」选择父工作目录`);
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
335
454
|
// /agent edit wait-input flow (kept for command compatibility)
|
|
336
455
|
{
|
|
337
456
|
const editFlow = getFreshFlow(String(chatId) + ':edit');
|
|
@@ -354,6 +473,41 @@ function createAgentCommandHandler(deps) {
|
|
|
354
473
|
const agentParts = agentArg.split(/\s+/).filter(Boolean);
|
|
355
474
|
const agentSub = agentParts[0] || ''; // bind / list / new / edit / reset / unbind / ''
|
|
356
475
|
|
|
476
|
+
// /agent new [team] — 创建新 Agent 或团队
|
|
477
|
+
if (agentSub === 'new') {
|
|
478
|
+
const secondArg = agentParts[1];
|
|
479
|
+
if (secondArg === 'team') {
|
|
480
|
+
if (!pendingTeamFlows) {
|
|
481
|
+
await bot.sendMessage(chatId, '❌ 团队向导暂不可用');
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
pendingTeamFlows.set(String(chatId), { step: 'name' });
|
|
485
|
+
await bot.sendMessage(chatId, `🏗️ **团队创建向导**
|
|
486
|
+
|
|
487
|
+
请输入团队名称(如:短剧团队、销售团队):
|
|
488
|
+
|
|
489
|
+
输入 /cancel 可取消`);
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
// /agent new clone — 分身模式
|
|
493
|
+
const isClone = secondArg === 'clone';
|
|
494
|
+
let parentCwd = null;
|
|
495
|
+
if (isClone) {
|
|
496
|
+
const cfg = loadConfig();
|
|
497
|
+
const agentMap = {
|
|
498
|
+
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
499
|
+
...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
500
|
+
};
|
|
501
|
+
const boundKey = agentMap[String(chatId)];
|
|
502
|
+
const boundProj = boundKey && cfg.projects && cfg.projects[boundKey];
|
|
503
|
+
if (boundProj && boundProj.cwd) parentCwd = normalizeCwd(boundProj.cwd);
|
|
504
|
+
}
|
|
505
|
+
pendingAgentFlows.set(String(chatId), { step: 'dir', isClone, parentCwd, __ts: Date.now() });
|
|
506
|
+
const hint = isClone ? `(${parentCwd ? '分身模式:将链接父 Agent 的 CLAUDE.md' : '⚠️ 当前群未绑定 Agent'})` : '';
|
|
507
|
+
await sendBrowse(bot, chatId, 'agent-new', HOME, `步骤1/3:选择 Agent 的工作目录${hint}`);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
357
511
|
// /agent bind <名称> [目录]
|
|
358
512
|
if (agentSub === 'bind') {
|
|
359
513
|
const bindName = agentParts[1];
|
|
@@ -635,6 +789,23 @@ function createAgentCommandHandler(deps) {
|
|
|
635
789
|
return true;
|
|
636
790
|
}
|
|
637
791
|
|
|
792
|
+
// /agent-dir <path>: /agent new 向导的目录选择回调(步骤1→步骤2)
|
|
793
|
+
if (text.startsWith('/agent-dir ')) {
|
|
794
|
+
const dirPath = expandPath(text.slice(11).trim());
|
|
795
|
+
const flow = pendingAgentFlows && pendingAgentFlows.get(String(chatId));
|
|
796
|
+
if (!flow || flow.step !== 'dir') {
|
|
797
|
+
await bot.sendMessage(chatId, '❌ 没有待完成的 /agent new,请重新发送 /agent new');
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
flow.dir = dirPath;
|
|
801
|
+
flow.step = 'name';
|
|
802
|
+
pendingAgentFlows.set(String(chatId), flow);
|
|
803
|
+
const displayPath = dirPath.replace(HOME, '~');
|
|
804
|
+
const cloneHint = flow.isClone ? '(分身模式)' : '';
|
|
805
|
+
await bot.sendMessage(chatId, `✓ 已选择目录:${displayPath}${cloneHint}\n\n步骤2/3:给这个 Agent 起个名字?`);
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
|
|
638
809
|
// /agent-bind-dir <path>: internal callback for bind picker
|
|
639
810
|
if (text.startsWith('/agent-bind-dir ')) {
|
|
640
811
|
const dirPath = expandPath(text.slice(16).trim());
|
|
@@ -648,6 +819,90 @@ function createAgentCommandHandler(deps) {
|
|
|
648
819
|
return true;
|
|
649
820
|
}
|
|
650
821
|
|
|
822
|
+
// /agent-team-dir <path>: directory picker callback for team creation
|
|
823
|
+
if (text.startsWith('/agent-team-dir ')) {
|
|
824
|
+
const dirPath = expandPath(text.slice(16).trim());
|
|
825
|
+
const teamFlow = pendingTeamFlows && pendingTeamFlows.get(String(chatId));
|
|
826
|
+
if (!teamFlow || teamFlow.step !== 'cwd') {
|
|
827
|
+
await bot.sendMessage(chatId, '❌ 没有待完成的团队创建,请重新发送 /agent new team');
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
teamFlow.step = 'creating';
|
|
831
|
+
pendingTeamFlows.set(String(chatId), teamFlow);
|
|
832
|
+
await bot.sendMessage(chatId, `⏳ 正在创建团队「${teamFlow.name}」...`);
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
const teamDir = path.join(dirPath, 'team');
|
|
836
|
+
if (!fs.existsSync(teamDir)) fs.mkdirSync(teamDir, { recursive: true });
|
|
837
|
+
|
|
838
|
+
const members = Array.isArray(teamFlow.members) ? teamFlow.members : [];
|
|
839
|
+
const results = [];
|
|
840
|
+
for (const member of members) {
|
|
841
|
+
const memberDir = path.join(teamDir, member.key);
|
|
842
|
+
if (!fs.existsSync(memberDir)) fs.mkdirSync(memberDir, { recursive: true });
|
|
843
|
+
const claudeMdPath = path.join(memberDir, 'CLAUDE.md');
|
|
844
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
845
|
+
fs.writeFileSync(claudeMdPath, `# ${member.name}\n\n(团队成员:${teamFlow.name})\n`, 'utf8');
|
|
846
|
+
}
|
|
847
|
+
// Init git repo for checkpoint support
|
|
848
|
+
try {
|
|
849
|
+
if (execSync) execSync('git init -q', { cwd: memberDir, stdio: 'ignore' });
|
|
850
|
+
} catch { /* non-critical */ }
|
|
851
|
+
results.push(`✅ ${member.icon} ${member.key}: ${memberDir.replace(HOME, '~')}`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Register in daemon.yaml under parent project's team array
|
|
855
|
+
const cfg = loadConfig();
|
|
856
|
+
let parentProjectKey = null;
|
|
857
|
+
if (cfg.projects) {
|
|
858
|
+
for (const [projKey, proj] of Object.entries(cfg.projects)) {
|
|
859
|
+
if (normalizeCwd(proj.cwd || '') === normalizeCwd(dirPath)) {
|
|
860
|
+
parentProjectKey = projKey;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (parentProjectKey && cfg.projects[parentProjectKey]) {
|
|
867
|
+
const proj = cfg.projects[parentProjectKey];
|
|
868
|
+
if (!proj.team) proj.team = [];
|
|
869
|
+
for (const member of members) {
|
|
870
|
+
if (!proj.team.some(m => m.key === member.key)) {
|
|
871
|
+
proj.team.push({
|
|
872
|
+
key: member.key,
|
|
873
|
+
name: member.name,
|
|
874
|
+
icon: member.icon,
|
|
875
|
+
color: member.color,
|
|
876
|
+
cwd: path.join(teamDir, member.key),
|
|
877
|
+
nicknames: member.nicknames,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (writeConfigSafe) writeConfigSafe(cfg);
|
|
882
|
+
if (backupConfig) backupConfig();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const memberList = members.map(m => `${m.icon} ${m.key}`).join(' | ');
|
|
886
|
+
const yamlNote = parentProjectKey
|
|
887
|
+
? `📝 已更新 daemon.yaml:${parentProjectKey}.team`
|
|
888
|
+
: '⚠️ 未找到父项目,请手动在 daemon.yaml 中注册 team 段';
|
|
889
|
+
await bot.sendMessage(chatId, `🎉 **团队创建完成!**
|
|
890
|
+
|
|
891
|
+
**${teamFlow.name}**
|
|
892
|
+
${memberList}
|
|
893
|
+
|
|
894
|
+
📁 目录:${teamDir.replace(HOME, '~')}/
|
|
895
|
+
${yamlNote}
|
|
896
|
+
|
|
897
|
+
💡 发 \`/agent\` 可切换到成员对话`);
|
|
898
|
+
} catch (e) {
|
|
899
|
+
await bot.sendMessage(chatId, `❌ 创建失败: ${e.message}`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
pendingTeamFlows.delete(String(chatId));
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
|
|
651
906
|
return false;
|
|
652
907
|
}
|
|
653
908
|
|