metame-cli 1.5.2 → 1.5.3
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 +36 -0
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +2 -1
- package/scripts/daemon-admin-commands.js +164 -9
- package/scripts/daemon-agent-commands.js +17 -8
- package/scripts/daemon-bridges.js +385 -6
- package/scripts/daemon-claude-engine.js +172 -51
- package/scripts/daemon-command-router.js +33 -0
- package/scripts/daemon-default.yaml +4 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +2 -2
- package/scripts/daemon-remote-dispatch.js +60 -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 +272 -6
- package/scripts/daemon.yaml +349 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/maintenance-manual.md +62 -1
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/memory-extract.js +1 -1
- package/scripts/memory-write.js +21 -4
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
|
@@ -35,11 +35,26 @@ function createSessionCommandHandler(deps) {
|
|
|
35
35
|
return n === 'codex' ? 'codex' : getDefaultEngine();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function getBoundCwd(chatId) {
|
|
39
|
+
try {
|
|
40
|
+
const cfg = loadConfig();
|
|
41
|
+
const chatAgentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
|
|
42
|
+
const mappedKey = chatAgentMap[String(chatId)];
|
|
43
|
+
const proj = mappedKey && cfg.projects ? cfg.projects[mappedKey] : null;
|
|
44
|
+
return (proj && proj.cwd) ? normalizeCwd(proj.cwd) : null;
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
// Write per-engine session slot, preserving cwd and other engine slots.
|
|
39
49
|
function attachEngineSession(state, chatId, engine, sessionId, cwd) {
|
|
40
|
-
|
|
50
|
+
// For bound chats, resolve to virtual chatId so askClaude picks up the session
|
|
51
|
+
const cfg = loadConfig();
|
|
52
|
+
const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
|
|
53
|
+
const boundKey = agentMap[String(chatId)];
|
|
54
|
+
const effectiveId = boundKey ? `_agent_${boundKey}` : String(chatId);
|
|
55
|
+
const existing = state.sessions[effectiveId] || {};
|
|
41
56
|
const existingEngines = existing.engines || {};
|
|
42
|
-
state.sessions[
|
|
57
|
+
state.sessions[effectiveId] = {
|
|
43
58
|
...existing,
|
|
44
59
|
cwd: cwd || existing.cwd || HOME,
|
|
45
60
|
engines: { ...existingEngines, [engine]: { id: sessionId, started: true } },
|
|
@@ -254,7 +269,7 @@ function createSessionCommandHandler(deps) {
|
|
|
254
269
|
if (text === '/sessions') {
|
|
255
270
|
const currentEngine = getDefaultEngine();
|
|
256
271
|
const codexLimitTip = '⚠️ 当前为 Codex 会话:`/sessions` 列表暂仅展示 Claude 本地会话,Codex 会话暂不可见。';
|
|
257
|
-
const allSessions = listRecentSessions(15);
|
|
272
|
+
const allSessions = listRecentSessions(15, getBoundCwd(chatId));
|
|
258
273
|
if (allSessions.length === 0) {
|
|
259
274
|
const base = 'No sessions found. Try /new first.';
|
|
260
275
|
await bot.sendMessage(chatId, currentEngine === 'codex' ? `${base}\n\n${codexLimitTip}` : base);
|
|
@@ -365,14 +380,7 @@ function createSessionCommandHandler(deps) {
|
|
|
365
380
|
|
|
366
381
|
// For bound chats, prefer sessions from the same project to avoid
|
|
367
382
|
// the bound-chat guard (handleCommand) immediately overwriting with a new session.
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
const cfg = loadConfig();
|
|
371
|
-
const chatAgentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
|
|
372
|
-
const mappedKey = chatAgentMap[String(chatId)];
|
|
373
|
-
const proj = mappedKey && cfg.projects ? cfg.projects[mappedKey] : null;
|
|
374
|
-
if (proj && proj.cwd) boundCwd = normalizeCwd(proj.cwd);
|
|
375
|
-
} catch { /* ignore */ }
|
|
383
|
+
const boundCwd = getBoundCwd(chatId);
|
|
376
384
|
|
|
377
385
|
let candidates = filtered;
|
|
378
386
|
if (boundCwd) {
|
|
@@ -182,6 +182,11 @@ function createSessionStore(deps) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
// Fallback: decode projectPath from directory name (e.g. -Users-yaron-AGI-AChat → /Users/yaron/AGI/AChat)
|
|
186
|
+
if (!projPathCache.has(proj) && proj.startsWith('-')) {
|
|
187
|
+
const decoded = proj.replace(/-/g, '/');
|
|
188
|
+
if (fs.existsSync(decoded)) projPathCache.set(proj, decoded);
|
|
189
|
+
}
|
|
185
190
|
} catch { /* skip */ }
|
|
186
191
|
|
|
187
192
|
try {
|
|
@@ -283,8 +288,7 @@ function createSessionStore(deps) {
|
|
|
283
288
|
function listRecentSessions(limit, cwd) {
|
|
284
289
|
let all = scanAllSessions();
|
|
285
290
|
if (cwd) {
|
|
286
|
-
|
|
287
|
-
if (matched.length > 0) all = matched;
|
|
291
|
+
all = all.filter(s => s.projectPath === cwd);
|
|
288
292
|
}
|
|
289
293
|
return all.slice(0, limit || 10);
|
|
290
294
|
}
|
|
@@ -578,10 +582,24 @@ function createSessionStore(deps) {
|
|
|
578
582
|
}
|
|
579
583
|
|
|
580
584
|
// Claude backend: JSONL files under ~/.claude/projects/<hash>/
|
|
585
|
+
// Best approach: read cwd directly from session file content (not from dir name)
|
|
581
586
|
function _isClaudeSessionValid(sessionId, normCwd) {
|
|
582
587
|
try {
|
|
583
588
|
const sessionFile = findSessionFile(sessionId);
|
|
584
589
|
if (!sessionFile) return false;
|
|
590
|
+
|
|
591
|
+
// Try to read cwd from session JSONL file content (most reliable)
|
|
592
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
593
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
594
|
+
for (const line of lines.slice(0, 20)) { // Check first 20 lines
|
|
595
|
+
try {
|
|
596
|
+
const entry = JSON.parse(line);
|
|
597
|
+
const fileCwd = entry.cwd || (entry.message && entry.message.cwd);
|
|
598
|
+
if (fileCwd && path.resolve(fileCwd) === normCwd) return true;
|
|
599
|
+
} catch { /* skip non-JSON lines */ }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Fallback: check sessions-index.json if exists
|
|
585
603
|
const projectDir = path.dirname(sessionFile);
|
|
586
604
|
const indexFile = path.join(projectDir, 'sessions-index.json');
|
|
587
605
|
if (fs.existsSync(indexFile)) {
|
|
@@ -592,15 +610,15 @@ function createSessionStore(deps) {
|
|
|
592
610
|
const anyPath = (entries.find(e => e && e.projectPath) || {}).projectPath;
|
|
593
611
|
if (anyPath) return path.resolve(anyPath) === normCwd;
|
|
594
612
|
}
|
|
595
|
-
|
|
596
|
-
//
|
|
597
|
-
//
|
|
613
|
+
|
|
614
|
+
// Last resort fallback: dir name match (less reliable, skip for worktree paths)
|
|
615
|
+
// Skip this for paths containing .worktree to avoid edge cases
|
|
616
|
+
if (normCwd.includes('.worktree')) return true; // trust the session exists
|
|
598
617
|
const actualDir = path.basename(projectDir).toLowerCase();
|
|
599
618
|
const expectedDir = process.platform === 'win32'
|
|
600
619
|
? normCwd.replace(/[:\\\/_ ]/g, '-').toLowerCase()
|
|
601
|
-
: ('-' + normCwd.replace(/^\//, '').replace(/[\/_ ]/g, '-')).toLowerCase();
|
|
602
|
-
|
|
603
|
-
return false; // dir name mismatch — session belongs to a different project
|
|
620
|
+
: ('-' + normCwd.replace(/^\//, '').replace(/[\/_. ]/g, '-')).toLowerCase();
|
|
621
|
+
return actualDir === expectedDir;
|
|
604
622
|
} catch {
|
|
605
623
|
return true; // conservative: infra failure ≠ invalid session
|
|
606
624
|
}
|
|
@@ -741,12 +741,12 @@ function createTaskScheduler(deps) {
|
|
|
741
741
|
lastTickTime = tickNow;
|
|
742
742
|
|
|
743
743
|
if (tickElapsed > WAKE_THRESHOLD) {
|
|
744
|
-
log('
|
|
744
|
+
log('WARN', `[WAKE-DETECT] System resumed after ${Math.round(tickElapsed / 1000)}s sleep — letting connections auto-reconnect`);
|
|
745
745
|
const st = loadState();
|
|
746
746
|
st.wake_restart = new Date().toISOString();
|
|
747
747
|
st.wake_sleep_seconds = Math.round(tickElapsed / 1000);
|
|
748
748
|
saveState(st);
|
|
749
|
-
|
|
749
|
+
// Don't exit — Feishu and Telegram have built-in auto-reconnect
|
|
750
750
|
}
|
|
751
751
|
|
|
752
752
|
// ① Physiological heartbeat (zero token, pure awareness)
|
package/scripts/daemon.js
CHANGED
|
@@ -69,6 +69,12 @@ const CLAUDE_BIN = (() => {
|
|
|
69
69
|
// Skill evolution module (hot path + cold path)
|
|
70
70
|
let skillEvolution = null;
|
|
71
71
|
try { skillEvolution = require('./skill-evolution'); } catch { /* graceful fallback */ }
|
|
72
|
+
const {
|
|
73
|
+
normalizeRemoteDispatchConfig,
|
|
74
|
+
encodePacket: encodeRemoteDispatchPacket,
|
|
75
|
+
decodePacket: decodeRemoteDispatchPacket,
|
|
76
|
+
verifyPacket: verifyRemoteDispatchPacket,
|
|
77
|
+
} = require('./daemon-remote-dispatch');
|
|
72
78
|
|
|
73
79
|
// ---------------------------------------------------------
|
|
74
80
|
// SKILL ROUTING (keyword → /skillname prefix, like metame-desktop)
|
|
@@ -145,7 +151,7 @@ const { createFileBrowser } = require('./daemon-file-browser');
|
|
|
145
151
|
const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
|
|
146
152
|
const { createNotifier } = require('./daemon-notify');
|
|
147
153
|
const { createClaudeEngine } = require('./daemon-claude-engine');
|
|
148
|
-
const { createEngineRuntimeFactory, detectDefaultEngine, ENGINE_MODEL_CONFIG, ENGINE_DISTILL_MAP, ENGINE_DEFAULT_MODEL } = require('./daemon-engine-runtime');
|
|
154
|
+
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG, ENGINE_DISTILL_MAP, ENGINE_DEFAULT_MODEL } = require('./daemon-engine-runtime');
|
|
149
155
|
const { createCommandRouter } = require('./daemon-command-router');
|
|
150
156
|
const { createTaskScheduler } = require('./daemon-task-scheduler');
|
|
151
157
|
const { createAgentTools } = require('./daemon-agent-tools');
|
|
@@ -515,6 +521,31 @@ let _handleCommand = null;
|
|
|
515
521
|
let _dispatchBridgeRef = null; // Store bridge (not bot) so .bot is always the live object after reconnects
|
|
516
522
|
function setDispatchHandler(fn) { _handleCommand = fn; }
|
|
517
523
|
|
|
524
|
+
function getRemoteDispatchConfig(config) {
|
|
525
|
+
return normalizeRemoteDispatchConfig(config || {});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function sendRemoteDispatch(packet, config) {
|
|
529
|
+
const rd = getRemoteDispatchConfig(config);
|
|
530
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
531
|
+
if (!rd) return { success: false, error: 'feishu.remote_dispatch not configured' };
|
|
532
|
+
if (!liveBot || typeof liveBot.sendMessage !== 'function') return { success: false, error: 'feishu bot not connected' };
|
|
533
|
+
const ts = new Date().toISOString();
|
|
534
|
+
const id = `${rd.selfPeer}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
535
|
+
try {
|
|
536
|
+
const body = encodeRemoteDispatchPacket({
|
|
537
|
+
v: 1,
|
|
538
|
+
id,
|
|
539
|
+
ts,
|
|
540
|
+
...packet,
|
|
541
|
+
}, rd.secret);
|
|
542
|
+
await liveBot.sendMessage(rd.chatId, body);
|
|
543
|
+
return { success: true, id };
|
|
544
|
+
} catch (e) {
|
|
545
|
+
return { success: false, error: e.message };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
518
549
|
/**
|
|
519
550
|
* Create a null bot that captures Claude's output without sending to Feishu/Telegram.
|
|
520
551
|
*/
|
|
@@ -795,6 +826,76 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
795
826
|
if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
796
827
|
fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
|
|
797
828
|
|
|
829
|
+
// Auto-update now/shared.md and team shared files for cross-agent visibility
|
|
830
|
+
try {
|
|
831
|
+
const NOW_DIR = path.join(HOME, '.metame', 'memory', 'now');
|
|
832
|
+
const SHARED_FILE = path.join(NOW_DIR, 'shared.md');
|
|
833
|
+
const SHARED_DIR = path.join(HOME, '.metame', 'memory', 'shared');
|
|
834
|
+
if (!fs.existsSync(NOW_DIR)) fs.mkdirSync(NOW_DIR, { recursive: true });
|
|
835
|
+
|
|
836
|
+
const now = new Date();
|
|
837
|
+
const timeStr = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
|
838
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
839
|
+
|
|
840
|
+
// Get sender display name
|
|
841
|
+
const fromProj = config && config.projects ? config.projects[fullMsg.from] : null;
|
|
842
|
+
const fromName = fromProj ? (fromProj.name || fullMsg.from) : (fullMsg.from || 'unknown');
|
|
843
|
+
const fromIcon = fromProj ? (fromProj.icon || '🤖') : '🤖';
|
|
844
|
+
|
|
845
|
+
// Get target display name
|
|
846
|
+
const toProj = config && config.projects ? config.projects[targetProject] : null;
|
|
847
|
+
const toName = toProj ? (toProj.name || targetProject) : targetProject;
|
|
848
|
+
const toIcon = toProj ? (toProj.icon || '🤖') : '🤖';
|
|
849
|
+
|
|
850
|
+
const taskTitle = payload.title || '';
|
|
851
|
+
const taskPrompt = payload.prompt || '';
|
|
852
|
+
|
|
853
|
+
// Update shared.md
|
|
854
|
+
const content = `# 共享当前状态
|
|
855
|
+
**最后更新**: ${timeStr} **更新者**: ${fromName} (${fullMsg.from})
|
|
856
|
+
|
|
857
|
+
## 当前任务
|
|
858
|
+
- **派发给**: ${toIcon} ${toName} (${targetProject})
|
|
859
|
+
- **任务**: ${taskTitle || taskPrompt.slice(0, 60)}
|
|
860
|
+
- **时间**: ${timeStr}
|
|
861
|
+
|
|
862
|
+
## 任务链
|
|
863
|
+
${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProject}
|
|
864
|
+
`;
|
|
865
|
+
fs.writeFileSync(SHARED_FILE, content, 'utf8');
|
|
866
|
+
|
|
867
|
+
// Update tasks.md if shared directory exists
|
|
868
|
+
const tasksFile = path.join(SHARED_DIR, 'tasks.md');
|
|
869
|
+
if (fs.existsSync(SHARED_DIR)) {
|
|
870
|
+
const taskLine = `- [${dateStr}] ${fromIcon} ${fromName} → ${toIcon} ${toName}: ${taskTitle || taskPrompt.slice(0, 40)}`;
|
|
871
|
+
let tasksContent = '';
|
|
872
|
+
if (fs.existsSync(tasksFile)) {
|
|
873
|
+
tasksContent = fs.readFileSync(tasksFile, 'utf8');
|
|
874
|
+
} else {
|
|
875
|
+
tasksContent = '# 任务看板\n\n## 🔄 进行中\n\n## ✅ 已完成\n\n## 📅 待开始\n';
|
|
876
|
+
}
|
|
877
|
+
// Insert task under "进行中" section
|
|
878
|
+
if (!tasksContent.includes(taskLine)) {
|
|
879
|
+
const lines = tasksContent.split('\n');
|
|
880
|
+
const newLines = [];
|
|
881
|
+
let inProgress = false;
|
|
882
|
+
for (const line of lines) {
|
|
883
|
+
newLines.push(line);
|
|
884
|
+
if (line.includes('## 🔄 进行中')) {
|
|
885
|
+
inProgress = true;
|
|
886
|
+
} else if (inProgress && line.startsWith('## ')) {
|
|
887
|
+
newLines.push(taskLine);
|
|
888
|
+
inProgress = false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (inProgress) newLines.push(taskLine);
|
|
892
|
+
fs.writeFileSync(tasksFile, newLines.join('\n'), 'utf8');
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} catch (e) {
|
|
896
|
+
log('WARN', `Failed to update shared files: ${e.message}`);
|
|
897
|
+
}
|
|
898
|
+
|
|
798
899
|
const rawPrompt = envelope
|
|
799
900
|
? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
|
|
800
901
|
: (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
|
|
@@ -980,6 +1081,36 @@ function spawnSessionSummaries() {
|
|
|
980
1081
|
/**
|
|
981
1082
|
* Handle a single dispatch message (from socket or pending.jsonl fallback).
|
|
982
1083
|
*/
|
|
1084
|
+
/**
|
|
1085
|
+
* Find if both sender and target belong to the same team group.
|
|
1086
|
+
* Returns { parentKey, parentProject, senderMember, targetMember, groupChatId } or null.
|
|
1087
|
+
*/
|
|
1088
|
+
function _findTeamBroadcastContext(fromKey, targetKey, config) {
|
|
1089
|
+
if (!config || !config.projects) return null;
|
|
1090
|
+
const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
1091
|
+
for (const [projKey, proj] of Object.entries(config.projects)) {
|
|
1092
|
+
if (!proj || !Array.isArray(proj.team) || proj.team.length === 0) continue;
|
|
1093
|
+
if (!proj.broadcast) continue; // broadcast switch must be on
|
|
1094
|
+
const senderMember = proj.team.find(m => m.key === fromKey);
|
|
1095
|
+
const targetMember = proj.team.find(m => m.key === targetKey);
|
|
1096
|
+
// Also check if sender/target is the parent project itself
|
|
1097
|
+
const senderIsParent = fromKey === projKey;
|
|
1098
|
+
const targetIsParent = targetKey === projKey;
|
|
1099
|
+
if ((senderMember || senderIsParent) && (targetMember || targetIsParent)) {
|
|
1100
|
+
// Find group chatId for this project
|
|
1101
|
+
const groupChatId = Object.entries(feishuMap).find(([, v]) => v === projKey)?.[0] || null;
|
|
1102
|
+
return {
|
|
1103
|
+
parentKey: projKey,
|
|
1104
|
+
parentProject: proj,
|
|
1105
|
+
senderMember: senderMember || { key: projKey, name: proj.name, icon: proj.icon || '🤖' },
|
|
1106
|
+
targetMember: targetMember || { key: projKey, name: proj.name, icon: proj.icon || '🤖' },
|
|
1107
|
+
groupChatId,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
983
1114
|
function handleDispatchItem(item, config) {
|
|
984
1115
|
if (!item.target || !item.prompt) return;
|
|
985
1116
|
if (!(config && config.projects && config.projects[item.target])) {
|
|
@@ -996,9 +1127,39 @@ function handleDispatchItem(item, config) {
|
|
|
996
1127
|
return;
|
|
997
1128
|
}
|
|
998
1129
|
log('INFO', `Dispatch: ${item.from || '?'} → ${item.target}: ${item.prompt.slice(0, 60)}`);
|
|
1130
|
+
|
|
1131
|
+
// ── Team broadcast: intra-team dispatch → show in group chat ──
|
|
1132
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1133
|
+
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, item.target, config) : null;
|
|
1134
|
+
if (teamCtx && teamCtx.groupChatId) {
|
|
1135
|
+
const { senderMember, targetMember, groupChatId, parentProject } = teamCtx;
|
|
1136
|
+
const sIcon = senderMember.icon || '🤖';
|
|
1137
|
+
const sName = senderMember.name || senderMember.key;
|
|
1138
|
+
const tIcon = targetMember.icon || '🤖';
|
|
1139
|
+
const tName = targetMember.name || targetMember.key;
|
|
1140
|
+
// Broadcast the handoff message to group as a card
|
|
1141
|
+
const cardTitle = `${sIcon} ${sName} → ${tIcon} ${tName}`;
|
|
1142
|
+
const cardBody = item.prompt.slice(0, 300) + (item.prompt.length > 300 ? '…' : '');
|
|
1143
|
+
const cardColor = senderMember.color || 'blue';
|
|
1144
|
+
const sendFn = liveBot.sendCard
|
|
1145
|
+
? () => liveBot.sendCard(groupChatId, { title: cardTitle, body: cardBody, color: cardColor })
|
|
1146
|
+
: () => liveBot.sendMarkdown(groupChatId, `**${cardTitle}**\n\n> ${cardBody}`);
|
|
1147
|
+
sendFn().catch(e => log('WARN', `Team broadcast failed: ${e.message}`));
|
|
1148
|
+
// Use streamForwardBot so target's reply also shows in group
|
|
1149
|
+
const streamOptions = { bot: liveBot, chatId: groupChatId };
|
|
1150
|
+
dispatchTask(item.target, {
|
|
1151
|
+
from: item.from || 'claude_session',
|
|
1152
|
+
type: 'task', priority: 'normal',
|
|
1153
|
+
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1154
|
+
callback: false,
|
|
1155
|
+
new_session: !!item.new_session,
|
|
1156
|
+
}, config, null, streamOptions);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// ── Normal dispatch (non-team or broadcast off) ──
|
|
999
1161
|
let pendingReplyFn = null;
|
|
1000
1162
|
let streamOptions = null;
|
|
1001
|
-
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1002
1163
|
if (liveBot) {
|
|
1003
1164
|
const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
1004
1165
|
const allowedFeishuIds = (config.feishu && config.feishu.allowed_chat_ids) || [];
|
|
@@ -1016,7 +1177,21 @@ function handleDispatchItem(item, config) {
|
|
|
1016
1177
|
const _userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
1017
1178
|
let senderChatId = null;
|
|
1018
1179
|
if (!_userSources.has(item.from)) {
|
|
1180
|
+
// Direct match: sender is a bound agent
|
|
1019
1181
|
senderChatId = Object.entries(feishuMap).find(([, v]) => v === item.from)?.[0] || null;
|
|
1182
|
+
// Team member fallback: if sender is a team member (e.g., jarvis_c), find parent project's chatId
|
|
1183
|
+
if (!senderChatId) {
|
|
1184
|
+
const projects = config.projects || {};
|
|
1185
|
+
for (const [projKey, proj] of Object.entries(projects)) {
|
|
1186
|
+
if (proj.team && Array.isArray(proj.team)) {
|
|
1187
|
+
const member = proj.team.find(m => m.key === item.from);
|
|
1188
|
+
if (member && feishuMap[projKey]) {
|
|
1189
|
+
senderChatId = feishuMap[projKey];
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1020
1195
|
}
|
|
1021
1196
|
if (!senderChatId) {
|
|
1022
1197
|
senderChatId = allowedFeishuIds.map(String).find(id => !agentChatIds.has(id)) || null;
|
|
@@ -1038,6 +1213,8 @@ function handleDispatchItem(item, config) {
|
|
|
1038
1213
|
);
|
|
1039
1214
|
});
|
|
1040
1215
|
};
|
|
1216
|
+
// Also set streamOptions so target agent's streaming replies go to the sender's group
|
|
1217
|
+
streamOptions = { bot: liveBot, chatId: senderChatId };
|
|
1041
1218
|
}
|
|
1042
1219
|
}
|
|
1043
1220
|
}
|
|
@@ -1050,6 +1227,90 @@ function handleDispatchItem(item, config) {
|
|
|
1050
1227
|
}, config, pendingReplyFn, streamOptions);
|
|
1051
1228
|
}
|
|
1052
1229
|
|
|
1230
|
+
async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
1231
|
+
const rd = getRemoteDispatchConfig(config);
|
|
1232
|
+
if (!rd || String(chatId) !== rd.chatId) return false;
|
|
1233
|
+
|
|
1234
|
+
const packet = decodeRemoteDispatchPacket(text);
|
|
1235
|
+
if (!packet) return true;
|
|
1236
|
+
if (!verifyRemoteDispatchPacket(packet, rd.secret)) {
|
|
1237
|
+
log('WARN', 'Remote dispatch ignored: invalid signature');
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
if (packet.from_peer === rd.selfPeer) return true;
|
|
1241
|
+
if (packet.to_peer !== rd.selfPeer) return true;
|
|
1242
|
+
|
|
1243
|
+
if (packet.type === 'task') {
|
|
1244
|
+
const replyFn = async (output) => {
|
|
1245
|
+
const res = await sendRemoteDispatch({
|
|
1246
|
+
type: 'result',
|
|
1247
|
+
from_peer: rd.selfPeer,
|
|
1248
|
+
to_peer: packet.from_peer,
|
|
1249
|
+
target_project: packet.target_project,
|
|
1250
|
+
source_chat_id: packet.source_chat_id,
|
|
1251
|
+
source_sender_key: packet.source_sender_key || 'user',
|
|
1252
|
+
request_id: packet.id,
|
|
1253
|
+
result: String(output || '').slice(0, 4000),
|
|
1254
|
+
}, config);
|
|
1255
|
+
if (!res.success) log('WARN', `Remote dispatch result send failed: ${res.error}`);
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
handleDispatchItem({
|
|
1259
|
+
target: packet.target_project,
|
|
1260
|
+
prompt: packet.prompt,
|
|
1261
|
+
from: packet.source_sender_key || `${packet.from_peer}:remote`,
|
|
1262
|
+
new_session: !!packet.new_session,
|
|
1263
|
+
_replyFn: replyFn,
|
|
1264
|
+
_suppressDefaultReplyRouting: true,
|
|
1265
|
+
}, config);
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (packet.type === 'result') {
|
|
1270
|
+
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
1271
|
+
if (!targetChatId) {
|
|
1272
|
+
const inboxTarget = String(packet.source_sender_key || '').trim();
|
|
1273
|
+
if (!inboxTarget || inboxTarget === 'user' || inboxTarget === '_claude_session') {
|
|
1274
|
+
log('WARN', 'Remote dispatch result dropped: no source_chat_id/source_sender_key');
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', inboxTarget);
|
|
1279
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
1280
|
+
const tsStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15);
|
|
1281
|
+
const inboxFile = path.join(inboxDir, `${tsStr}_${packet.from_peer}_${packet.target_project || 'remote'}_result.md`);
|
|
1282
|
+
const body = [
|
|
1283
|
+
`FROM: ${packet.from_peer}:${packet.target_project || 'unknown'}`,
|
|
1284
|
+
`TO: ${inboxTarget}`,
|
|
1285
|
+
`TS: ${new Date().toISOString()}`,
|
|
1286
|
+
'',
|
|
1287
|
+
String(packet.result || '').trim() || '(empty result)',
|
|
1288
|
+
].join('\n');
|
|
1289
|
+
fs.writeFileSync(inboxFile, body, 'utf8');
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
log('WARN', `Remote dispatch inbox write failed: ${e.message}`);
|
|
1292
|
+
}
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1296
|
+
if (!liveBot) {
|
|
1297
|
+
log('WARN', 'Remote dispatch result dropped: no live bot');
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
const header = `${packet.from_peer}:${packet.target_project || 'unknown'}`;
|
|
1301
|
+
const body = `${header}\n\n${String(packet.result || '').trim() || '(empty result)'}`;
|
|
1302
|
+
try {
|
|
1303
|
+
if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, body);
|
|
1304
|
+
else await liveBot.sendMessage(targetChatId, body);
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
log('WARN', `Remote dispatch result delivery failed: ${e.message}`);
|
|
1307
|
+
}
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1053
1314
|
/**
|
|
1054
1315
|
* Start Unix Domain Socket server for low-latency dispatch.
|
|
1055
1316
|
*/
|
|
@@ -1528,6 +1789,7 @@ const { handleAdminCommand } = createAdminCommandHandler({
|
|
|
1528
1789
|
skillEvolution,
|
|
1529
1790
|
taskBoard,
|
|
1530
1791
|
taskEnvelope,
|
|
1792
|
+
sendRemoteDispatch,
|
|
1531
1793
|
getActiveProcesses: () => activeProcesses,
|
|
1532
1794
|
getMessageQueue: () => messageQueue,
|
|
1533
1795
|
loadState,
|
|
@@ -1765,6 +2027,8 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
|
1765
2027
|
getSession,
|
|
1766
2028
|
handleCommand,
|
|
1767
2029
|
pendingActivations,
|
|
2030
|
+
activeProcesses,
|
|
2031
|
+
messageQueue,
|
|
1768
2032
|
});
|
|
1769
2033
|
|
|
1770
2034
|
const { killExistingDaemon, writePid, cleanPid } = createPidManager({
|
|
@@ -1900,7 +2164,9 @@ async function main() {
|
|
|
1900
2164
|
'enable_nl_mac_fallback',
|
|
1901
2165
|
];
|
|
1902
2166
|
// All known models across all engines (for legacy daemon.model validation only)
|
|
1903
|
-
const BUILTIN_CLAUDE_MODELS = ENGINE_MODEL_CONFIG.claude.options
|
|
2167
|
+
const BUILTIN_CLAUDE_MODELS = (ENGINE_MODEL_CONFIG.claude.options || []).map(option =>
|
|
2168
|
+
typeof option === 'string' ? option : option.value
|
|
2169
|
+
).filter(Boolean);
|
|
1904
2170
|
for (const key of Object.keys(config)) {
|
|
1905
2171
|
if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
|
|
1906
2172
|
}
|
|
@@ -1914,7 +2180,7 @@ async function main() {
|
|
|
1914
2180
|
if (activeProv === 'anthropic' && _defaultEngine === 'claude') {
|
|
1915
2181
|
log('WARN', `Config: daemon.model="${config.daemon.model}" is not a known Claude model`);
|
|
1916
2182
|
} else {
|
|
1917
|
-
log('INFO', `Config: model
|
|
2183
|
+
log('INFO', `Config: legacy daemon.model="${config.daemon.model}" retained; active ${_defaultEngine} model resolves to "${resolveEngineModel(_defaultEngine, config.daemon)}" (${activeProv})`);
|
|
1918
2184
|
}
|
|
1919
2185
|
}
|
|
1920
2186
|
}
|
|
@@ -2162,5 +2428,5 @@ if (process.argv.includes('--run')) {
|
|
|
2162
2428
|
});
|
|
2163
2429
|
}
|
|
2164
2430
|
|
|
2165
|
-
// Export for testing
|
|
2166
|
-
module.exports = { executeTask, loadConfig, loadState, buildProfilePreamble, parseInterval };
|
|
2431
|
+
// Export for testing & cross-bot dispatch
|
|
2432
|
+
module.exports = { executeTask, loadConfig, loadState, buildProfilePreamble, parseInterval, handleRemoteDispatchMessage, sendRemoteDispatch };
|