metame-cli 1.5.4 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/index.js +277 -55
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +17 -5
- package/scripts/daemon-admin-commands.js +264 -62
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +447 -48
- package/scripts/daemon-claude-engine.js +650 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +34 -2
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +610 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +8 -1
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +150 -11
- package/scripts/hooks/intent-agent-manage.js +0 -50
- package/scripts/hooks/intent-hook-config.js +0 -28
package/scripts/daemon.js
CHANGED
|
@@ -47,7 +47,7 @@ const LOG_FILE = path.join(METAME_DIR, 'daemon.log');
|
|
|
47
47
|
const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
|
|
48
48
|
const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
|
|
49
49
|
const DISPATCH_LOG = path.join(DISPATCH_DIR, 'dispatch-log.jsonl');
|
|
50
|
-
const { socketPath, needsSocketCleanup } = require('./platform');
|
|
50
|
+
const { sleepSync, socketPath, needsSocketCleanup } = require('./platform');
|
|
51
51
|
const SOCK_PATH = socketPath(METAME_DIR);
|
|
52
52
|
|
|
53
53
|
// Resolve claude binary path (daemon may not inherit user's full PATH)
|
|
@@ -69,6 +69,8 @@ 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
|
+
let userAcl = null;
|
|
73
|
+
try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
|
|
72
74
|
const {
|
|
73
75
|
normalizeRemoteDispatchConfig,
|
|
74
76
|
encodePacket: encodeRemoteDispatchPacket,
|
|
@@ -84,18 +86,15 @@ function isMacLocalOrchestratorIntent(prompt) {
|
|
|
84
86
|
const text = String(prompt || '').trim();
|
|
85
87
|
if (!text) return false;
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
89
|
+
const hasAutomationVerb = /(?:自动化|脚本|控制|操作|执行|设置|调整|打开|关闭|启动|退出|切到|唤起|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|调(?:高|低|整)?音量|open|launch|quit|activate|lock\s*screen|sleep|mute|unmute|set\s+volume|run\s+(?:an?\s+)?script)/i.test(text);
|
|
90
|
+
const hasMacTool = /\b(?:mac|macos|applescript|osascript|jxa|hammerspoon|aerospace|yabai|skhd|raycast|launchctl|keyboard maestro|shortcuts)\b/i.test(text);
|
|
91
|
+
const hasMacTarget = /(?:微信|WeChat|飞书|Feishu|Finder|Safari|Terminal|iTerm|系统设置|System Settings|辅助功能|隐私|权限|屏幕录制|自动化|电脑|桌面|访达|System Events|LaunchAgent|快捷指令|锁屏|锁定屏幕|睡眠|休眠|静音|音量|mac)/i.test(text);
|
|
92
|
+
|
|
93
|
+
// Require an actual automation ask. Mentioning "macOS" or "权限" alone should not route.
|
|
94
|
+
if (hasMacTool && hasAutomationVerb) return true;
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
const hasTarget = /(?:微信|WeChat|飞书|Feishu|Finder|Safari|Terminal|iTerm|系统设置|System Settings|电脑|System Events|mac)/i.test(text);
|
|
98
|
-
return hasAction && hasTarget;
|
|
96
|
+
// Natural-language control only triggers when both the action and the macOS target are explicit.
|
|
97
|
+
return hasAutomationVerb && hasMacTarget;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
const SKILL_ROUTES = [
|
|
@@ -148,13 +147,13 @@ const { createSessionCommandHandler } = require('./daemon-session-commands');
|
|
|
148
147
|
const { createSessionStore } = require('./daemon-session-store');
|
|
149
148
|
const { createCheckpointUtils } = require('./daemon-checkpoints');
|
|
150
149
|
const { createBridgeStarter } = require('./daemon-bridges');
|
|
151
|
-
const { buildTeamRosterHint } = require('./team-dispatch');
|
|
150
|
+
const { buildTeamRosterHint, buildEnrichedPrompt, resolveDispatchActor, updateDispatchContextFiles } = require('./team-dispatch');
|
|
152
151
|
const { createFileBrowser } = require('./daemon-file-browser');
|
|
153
152
|
const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
|
|
154
153
|
const { repairAgentLayer } = require('./agent-layer');
|
|
155
154
|
const { createNotifier } = require('./daemon-notify');
|
|
156
155
|
const { createClaudeEngine } = require('./daemon-claude-engine');
|
|
157
|
-
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG
|
|
156
|
+
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
|
|
158
157
|
const { createCommandRouter } = require('./daemon-command-router');
|
|
159
158
|
const { createTaskScheduler } = require('./daemon-task-scheduler');
|
|
160
159
|
const { createAgentTools } = require('./daemon-agent-tools');
|
|
@@ -520,15 +519,6 @@ function recordTokens(state, tokens, meta = null) {
|
|
|
520
519
|
}
|
|
521
520
|
|
|
522
521
|
|
|
523
|
-
function getBudgetWarning(config, state) {
|
|
524
|
-
const limit = (config.budget && config.budget.daily_limit) || 50000;
|
|
525
|
-
const threshold = (config.budget && config.budget.warning_threshold) || 0.8;
|
|
526
|
-
const ratio = state.budget.tokens_used / limit;
|
|
527
|
-
if (ratio >= 1) return 'exceeded';
|
|
528
|
-
if (ratio >= threshold) return 'warning';
|
|
529
|
-
return 'ok';
|
|
530
|
-
}
|
|
531
|
-
|
|
532
522
|
const taskBoard = createTaskBoard({
|
|
533
523
|
logger: (msg) => log('WARN', msg),
|
|
534
524
|
});
|
|
@@ -540,12 +530,58 @@ const taskBoard = createTaskBoard({
|
|
|
540
530
|
// Late-bound reference to handleCommand (defined later in file)
|
|
541
531
|
let _handleCommand = null;
|
|
542
532
|
let _dispatchBridgeRef = null; // Store bridge (not bot) so .bot is always the live object after reconnects
|
|
533
|
+
const _pendingRemoteDispatches = new Map();
|
|
543
534
|
function setDispatchHandler(fn) { _handleCommand = fn; }
|
|
544
535
|
|
|
545
536
|
function getRemoteDispatchConfig(config) {
|
|
546
537
|
return normalizeRemoteDispatchConfig(config || {});
|
|
547
538
|
}
|
|
548
539
|
|
|
540
|
+
function trackRemoteDispatch(packet) {
|
|
541
|
+
if (!packet || packet.type !== 'task') return;
|
|
542
|
+
const requestId = String(packet.id || '').trim();
|
|
543
|
+
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
544
|
+
if (!requestId || !targetChatId) return;
|
|
545
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
546
|
+
const timeoutMs = 15000;
|
|
547
|
+
const existing = _pendingRemoteDispatches.get(requestId);
|
|
548
|
+
if (existing && existing.timer) clearTimeout(existing.timer);
|
|
549
|
+
const timer = setTimeout(async () => {
|
|
550
|
+
_pendingRemoteDispatches.delete(requestId);
|
|
551
|
+
const text = [
|
|
552
|
+
'⏱️ 远端 Dispatch 超时',
|
|
553
|
+
'',
|
|
554
|
+
`目标: ${packet.to_peer}:${packet.target_project || 'unknown'}`,
|
|
555
|
+
`请求: ${requestId}`,
|
|
556
|
+
`状态: 15s 内未收到回执`,
|
|
557
|
+
].join('\n');
|
|
558
|
+
log('WARN', `Remote dispatch timeout id=${requestId} target=${packet.to_peer}:${packet.target_project || 'unknown'}`);
|
|
559
|
+
if (!liveBot) return;
|
|
560
|
+
try {
|
|
561
|
+
if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, text);
|
|
562
|
+
else await liveBot.sendMessage(targetChatId, text);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
log('WARN', `Remote dispatch timeout delivery failed: ${e.message}`);
|
|
565
|
+
}
|
|
566
|
+
}, timeoutMs);
|
|
567
|
+
_pendingRemoteDispatches.set(requestId, {
|
|
568
|
+
id: requestId,
|
|
569
|
+
targetChatId,
|
|
570
|
+
targetPeer: String(packet.to_peer || '').trim(),
|
|
571
|
+
targetProject: String(packet.target_project || '').trim(),
|
|
572
|
+
timer,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function resolveTrackedRemoteDispatch(requestId) {
|
|
577
|
+
const key = String(requestId || '').trim();
|
|
578
|
+
if (!key) return null;
|
|
579
|
+
const tracked = _pendingRemoteDispatches.get(key) || null;
|
|
580
|
+
if (tracked && tracked.timer) clearTimeout(tracked.timer);
|
|
581
|
+
if (tracked) _pendingRemoteDispatches.delete(key);
|
|
582
|
+
return tracked;
|
|
583
|
+
}
|
|
584
|
+
|
|
549
585
|
async function sendRemoteDispatch(packet, config) {
|
|
550
586
|
const rd = getRemoteDispatchConfig(config);
|
|
551
587
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
@@ -562,6 +598,10 @@ async function sendRemoteDispatch(packet, config) {
|
|
|
562
598
|
from_peer: rd.selfPeer,
|
|
563
599
|
}, rd.secret);
|
|
564
600
|
await liveBot.sendMessage(rd.chatId, body);
|
|
601
|
+
log('INFO', `Remote dispatch sent type=${packet.type} id=${id} to=${packet.to_peer}:${packet.target_project || 'unknown'} via=${rd.chatId}`);
|
|
602
|
+
if (packet.type === 'task') {
|
|
603
|
+
trackRemoteDispatch({ ...packet, id }, config);
|
|
604
|
+
}
|
|
565
605
|
return { success: true, id };
|
|
566
606
|
} catch (e) {
|
|
567
607
|
return { success: false, error: e.message };
|
|
@@ -587,38 +627,91 @@ function createNullBot(onOutput) {
|
|
|
587
627
|
};
|
|
588
628
|
}
|
|
589
629
|
|
|
630
|
+
function stripLeadingPlanSection(text) {
|
|
631
|
+
const src = String(text || '');
|
|
632
|
+
if (!src.trim()) return '';
|
|
633
|
+
const normalized = src.replace(/\r\n/g, '\n');
|
|
634
|
+
const paragraphs = normalized.split(/\n\s*\n/);
|
|
635
|
+
if (paragraphs.length === 0) return normalized.trim();
|
|
636
|
+
const first = String(paragraphs[0] || '').trim();
|
|
637
|
+
if (!/^计划[::]/.test(first)) return normalized.trim();
|
|
638
|
+
const rest = paragraphs.slice(1).join('\n\n').trim();
|
|
639
|
+
if (rest) return rest;
|
|
640
|
+
const lines = normalized.split('\n');
|
|
641
|
+
const remaining = lines.slice(1).join('\n').trim();
|
|
642
|
+
return remaining || first.replace(/^计划[::]\s*/, '').trim();
|
|
643
|
+
}
|
|
644
|
+
|
|
590
645
|
/**
|
|
591
646
|
* Forward bot: routes all calls to a real bot with a fixed chatId.
|
|
592
647
|
* Used for dispatch tasks so Claude's streaming output appears in the target's Feishu channel.
|
|
593
648
|
*/
|
|
594
|
-
function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
649
|
+
function createStreamForwardBot(realBot, chatId, onOutput = null, opts = {}) {
|
|
595
650
|
// Track edit-broken state independently so dispatch failures don't poison realBot's flag
|
|
596
651
|
let _editBroken = false;
|
|
652
|
+
const ready = opts && opts.ready && typeof opts.ready.then === 'function'
|
|
653
|
+
? opts.ready.catch(() => {})
|
|
654
|
+
: Promise.resolve();
|
|
655
|
+
async function waitUntilReady() {
|
|
656
|
+
await ready;
|
|
657
|
+
}
|
|
658
|
+
function normalizeOutput(payload) {
|
|
659
|
+
const text = typeof payload === 'object'
|
|
660
|
+
? (payload.body || payload.title || JSON.stringify(payload))
|
|
661
|
+
: String(payload);
|
|
662
|
+
return opts.stripPlan !== false ? stripLeadingPlanSection(text) : text;
|
|
663
|
+
}
|
|
664
|
+
async function deliver(text, rawText = text) {
|
|
665
|
+
const displayText = normalizeOutput(text);
|
|
666
|
+
if (onOutput) onOutput(rawText);
|
|
667
|
+
if (opts.responseCard && realBot.sendCard) {
|
|
668
|
+
return realBot.sendCard(chatId, {
|
|
669
|
+
title: opts.responseCard.title,
|
|
670
|
+
body: displayText,
|
|
671
|
+
color: opts.responseCard.color || 'blue',
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return realBot.sendMessage(chatId, displayText);
|
|
675
|
+
}
|
|
597
676
|
return {
|
|
598
677
|
sendMessage: async (_, text) => {
|
|
678
|
+
await waitUntilReady();
|
|
599
679
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] msg: ${String(text).slice(0, 80)}`);
|
|
600
|
-
|
|
601
|
-
return realBot.sendMessage(chatId, text);
|
|
680
|
+
return deliver(text, text);
|
|
602
681
|
},
|
|
603
682
|
sendMarkdown: async (_, text) => {
|
|
683
|
+
await waitUntilReady();
|
|
604
684
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] md: ${String(text).slice(0, 80)}`);
|
|
685
|
+
if (opts.responseCard && realBot.sendCard) {
|
|
686
|
+
const displayText = normalizeOutput(text);
|
|
687
|
+
if (onOutput) onOutput(text);
|
|
688
|
+
return realBot.sendCard(chatId, {
|
|
689
|
+
title: opts.responseCard.title,
|
|
690
|
+
body: displayText,
|
|
691
|
+
color: opts.responseCard.color || 'blue',
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const displayText = normalizeOutput(text);
|
|
605
695
|
if (onOutput) onOutput(text);
|
|
606
|
-
return realBot.sendMarkdown(chatId,
|
|
696
|
+
return realBot.sendMarkdown(chatId, displayText);
|
|
607
697
|
},
|
|
608
698
|
sendCard: async (_, card) => {
|
|
699
|
+
await waitUntilReady();
|
|
609
700
|
const title = typeof card === 'object' ? (card.title || card.body || '').slice(0, 60) : String(card).slice(0, 60);
|
|
610
701
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] card: ${title}`);
|
|
611
702
|
if (onOutput) onOutput(typeof card === 'object' ? (card.body || card.title || JSON.stringify(card)) : card);
|
|
612
703
|
return realBot.sendCard(chatId, card);
|
|
613
704
|
},
|
|
614
705
|
sendRawCard: async (_, header, elements) => {
|
|
706
|
+
await waitUntilReady();
|
|
615
707
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] rawcard: ${String(header).slice(0, 60)}`);
|
|
616
708
|
if (onOutput) onOutput(header);
|
|
617
709
|
return realBot.sendRawCard(chatId, header, elements);
|
|
618
710
|
},
|
|
619
|
-
sendButtons: async (_, text, buttons) => realBot.sendButtons(chatId, text, buttons),
|
|
620
|
-
sendTyping: async () => realBot.sendTyping(chatId),
|
|
711
|
+
sendButtons: async (_, text, buttons) => { await waitUntilReady(); return realBot.sendButtons(chatId, text, buttons); },
|
|
712
|
+
sendTyping: async () => { await waitUntilReady(); return realBot.sendTyping(chatId); },
|
|
621
713
|
editMessage: async (_, msgId, text) => {
|
|
714
|
+
await waitUntilReady();
|
|
622
715
|
if (_editBroken) return false;
|
|
623
716
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] edit ${String(msgId).slice(-8)}: ${String(text).slice(0, 60)}`);
|
|
624
717
|
try {
|
|
@@ -631,8 +724,8 @@ function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
|
631
724
|
return false;
|
|
632
725
|
}
|
|
633
726
|
},
|
|
634
|
-
deleteMessage: async (_, msgId) => realBot.deleteMessage(chatId, msgId),
|
|
635
|
-
sendFile: async (_, filePath, caption) => realBot.sendFile(chatId, filePath, caption),
|
|
727
|
+
deleteMessage: async (_, msgId) => { await waitUntilReady(); return realBot.deleteMessage(chatId, msgId); },
|
|
728
|
+
sendFile: async (_, filePath, caption) => { await waitUntilReady(); return realBot.sendFile(chatId, filePath, caption); },
|
|
636
729
|
downloadFile: async (...args) => realBot.downloadFile(...args),
|
|
637
730
|
};
|
|
638
731
|
}
|
|
@@ -802,6 +895,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
802
895
|
const fullMsg = {
|
|
803
896
|
id: `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
804
897
|
from: message.from || 'unknown',
|
|
898
|
+
source_sender_id: String(message.source_sender_id || '').trim() || '',
|
|
805
899
|
to: targetProject,
|
|
806
900
|
type: message.type || 'task',
|
|
807
901
|
priority: message.priority || 'normal',
|
|
@@ -859,79 +953,23 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
859
953
|
if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
860
954
|
fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
|
|
861
955
|
|
|
862
|
-
// Auto-update
|
|
956
|
+
// Auto-update scoped dispatch context files; only TeamTask writes shared state.
|
|
863
957
|
try {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const fromProj = config && config.projects ? config.projects[fullMsg.from] : null;
|
|
875
|
-
const fromName = fromProj ? (fromProj.name || fullMsg.from) : (fullMsg.from || 'unknown');
|
|
876
|
-
const fromIcon = fromProj ? (fromProj.icon || '🤖') : '🤖';
|
|
877
|
-
|
|
878
|
-
// Get target display name
|
|
879
|
-
const toProj = config && config.projects ? config.projects[targetProject] : null;
|
|
880
|
-
const toName = toProj ? (toProj.name || targetProject) : targetProject;
|
|
881
|
-
const toIcon = toProj ? (toProj.icon || '🤖') : '🤖';
|
|
882
|
-
|
|
883
|
-
const taskTitle = payload.title || '';
|
|
884
|
-
const taskPrompt = payload.prompt || '';
|
|
885
|
-
|
|
886
|
-
// Update shared.md
|
|
887
|
-
const content = `# 共享当前状态
|
|
888
|
-
**最后更新**: ${timeStr} **更新者**: ${fromName} (${fullMsg.from})
|
|
889
|
-
|
|
890
|
-
## 当前任务
|
|
891
|
-
- **派发给**: ${toIcon} ${toName} (${targetProject})
|
|
892
|
-
- **任务**: ${taskTitle || taskPrompt.slice(0, 60)}
|
|
893
|
-
- **时间**: ${timeStr}
|
|
894
|
-
|
|
895
|
-
## 任务链
|
|
896
|
-
${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProject}
|
|
897
|
-
`;
|
|
898
|
-
fs.writeFileSync(SHARED_FILE, content, 'utf8');
|
|
899
|
-
|
|
900
|
-
// Update tasks.md if shared directory exists
|
|
901
|
-
const tasksFile = path.join(SHARED_DIR, 'tasks.md');
|
|
902
|
-
if (fs.existsSync(SHARED_DIR)) {
|
|
903
|
-
const taskLine = `- [${dateStr}] ${fromIcon} ${fromName} → ${toIcon} ${toName}: ${taskTitle || taskPrompt.slice(0, 40)}`;
|
|
904
|
-
let tasksContent = '';
|
|
905
|
-
if (fs.existsSync(tasksFile)) {
|
|
906
|
-
tasksContent = fs.readFileSync(tasksFile, 'utf8');
|
|
907
|
-
} else {
|
|
908
|
-
tasksContent = '# 任务看板\n\n## 🔄 进行中\n\n## ✅ 已完成\n\n## 📅 待开始\n';
|
|
909
|
-
}
|
|
910
|
-
// Insert task under "进行中" section
|
|
911
|
-
if (!tasksContent.includes(taskLine)) {
|
|
912
|
-
const lines = tasksContent.split('\n');
|
|
913
|
-
const newLines = [];
|
|
914
|
-
let inProgress = false;
|
|
915
|
-
for (const line of lines) {
|
|
916
|
-
newLines.push(line);
|
|
917
|
-
if (line.includes('## 🔄 进行中')) {
|
|
918
|
-
inProgress = true;
|
|
919
|
-
} else if (inProgress && line.startsWith('## ')) {
|
|
920
|
-
newLines.push(taskLine);
|
|
921
|
-
inProgress = false;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
if (inProgress) newLines.push(taskLine);
|
|
925
|
-
fs.writeFileSync(tasksFile, newLines.join('\n'), 'utf8');
|
|
926
|
-
}
|
|
927
|
-
}
|
|
958
|
+
updateDispatchContextFiles({
|
|
959
|
+
fs,
|
|
960
|
+
path,
|
|
961
|
+
baseDir: METAME_DIR,
|
|
962
|
+
fullMsg,
|
|
963
|
+
targetProject,
|
|
964
|
+
config,
|
|
965
|
+
envelope,
|
|
966
|
+
logger: (msg) => log('WARN', msg),
|
|
967
|
+
});
|
|
928
968
|
} catch (e) {
|
|
929
|
-
log('WARN', `Failed to update
|
|
969
|
+
log('WARN', `Failed to update dispatch context files: ${e.message}`);
|
|
930
970
|
}
|
|
931
971
|
|
|
932
|
-
const rawPrompt = envelope
|
|
933
|
-
? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
|
|
934
|
-
: (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
|
|
972
|
+
const rawPrompt = buildDispatchPrompt(targetProject, fullMsg, envelope);
|
|
935
973
|
|
|
936
974
|
// Inject sender identity when dispatched by another agent (not directly from user)
|
|
937
975
|
const userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
@@ -957,10 +995,25 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
957
995
|
const dispatchChatId = buildDispatchChatId(targetProject, envelope && envelope.scope_id);
|
|
958
996
|
const sessionMode = forceNew ? 'fresh session (forced)' : 'existing virtual session';
|
|
959
997
|
log('INFO', `Dispatching ${fullMsg.type} to ${targetProject} via ${sessionMode}: ${rawPrompt.slice(0, 80)}`);
|
|
998
|
+
const streamReady = streamOptions?.bot && streamOptions?.chatId
|
|
999
|
+
? (() => {
|
|
1000
|
+
if (typeof streamOptions.preDispatch === 'function') {
|
|
1001
|
+
return Promise.resolve()
|
|
1002
|
+
.then(() => streamOptions.preDispatch())
|
|
1003
|
+
.catch(e => log('WARN', `Dispatch prelude failed: ${e.message}`));
|
|
1004
|
+
}
|
|
1005
|
+
if (streamOptions.sendTaskCard === false) return Promise.resolve();
|
|
1006
|
+
const card = buildDispatchTaskCard(fullMsg, targetProject, config);
|
|
1007
|
+
return Promise.resolve()
|
|
1008
|
+
.then(() => sendDispatchTaskCard(streamOptions.bot, streamOptions.chatId, card))
|
|
1009
|
+
.catch(e => log('WARN', `Dispatch task card failed: ${e.message}`));
|
|
1010
|
+
})()
|
|
1011
|
+
: Promise.resolve();
|
|
960
1012
|
|
|
961
1013
|
let _taskFinalized = false;
|
|
962
1014
|
const outputHandler = (output) => {
|
|
963
1015
|
const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
|
|
1016
|
+
const displayOut = envelope ? appendTeamTaskResumeHint(outStr, envelope.task_id, envelope.scope_id) : outStr;
|
|
964
1017
|
log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
|
|
965
1018
|
if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
|
|
966
1019
|
const status = inferTaskStatusFromOutput(outStr);
|
|
@@ -979,7 +1032,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
979
1032
|
_taskFinalized = true;
|
|
980
1033
|
}
|
|
981
1034
|
if (replyFn && outStr.trim().length > 2) {
|
|
982
|
-
replyFn(
|
|
1035
|
+
replyFn(displayOut);
|
|
983
1036
|
} else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
|
|
984
1037
|
// Write result to sender's inbox before dispatching callback
|
|
985
1038
|
try {
|
|
@@ -994,7 +1047,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
994
1047
|
`TS: ${new Date().toISOString()}`,
|
|
995
1048
|
`SUBJECT: ${subject}`,
|
|
996
1049
|
'',
|
|
997
|
-
|
|
1050
|
+
displayOut.slice(0, 2000),
|
|
998
1051
|
].join('\n');
|
|
999
1052
|
fs.writeFileSync(inboxFile, body, 'utf8');
|
|
1000
1053
|
} catch (e) {
|
|
@@ -1002,12 +1055,13 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1002
1055
|
}
|
|
1003
1056
|
dispatchTask(fullMsg.from, {
|
|
1004
1057
|
from: targetProject,
|
|
1058
|
+
source_sender_id: fullMsg.source_sender_id || '',
|
|
1005
1059
|
type: 'callback',
|
|
1006
1060
|
priority: 'normal',
|
|
1007
1061
|
payload: {
|
|
1008
1062
|
title: `任务完成: ${fullMsg.payload.title || fullMsg.id}`,
|
|
1009
1063
|
original_id: fullMsg.id,
|
|
1010
|
-
output:
|
|
1064
|
+
output: displayOut.slice(0, 500),
|
|
1011
1065
|
},
|
|
1012
1066
|
chain: [], // reset chain for callbacks
|
|
1013
1067
|
}, config);
|
|
@@ -1016,11 +1070,14 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1016
1070
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
1017
1071
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
1018
1072
|
const nullBot = streamOptions?.bot && streamOptions?.chatId
|
|
1019
|
-
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler
|
|
1073
|
+
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler, {
|
|
1074
|
+
ready: streamReady,
|
|
1075
|
+
stripPlan: streamOptions.stripPlan !== false,
|
|
1076
|
+
responseCard: streamOptions.responseCard || null,
|
|
1077
|
+
})
|
|
1020
1078
|
: createNullBot(outputHandler);
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
// Otherwise fall back to readOnly (safe default for untrusted daemon configs).
|
|
1079
|
+
// Trusted dispatches (user / bound agent / team member) keep write access.
|
|
1080
|
+
// Only unknown senders are downgraded to read-only.
|
|
1024
1081
|
// When forceNew=true, clear any cached session for this virtual chatId so
|
|
1025
1082
|
// attachOrCreateSession in handleCommand actually creates a fresh Claude session.
|
|
1026
1083
|
if (forceNew) {
|
|
@@ -1030,7 +1087,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1030
1087
|
saveState(st);
|
|
1031
1088
|
}
|
|
1032
1089
|
}
|
|
1033
|
-
const dispatchReadOnly =
|
|
1090
|
+
const dispatchReadOnly = resolveDispatchReadOnly(message, config, targetProject);
|
|
1034
1091
|
if (envelope && taskBoard) {
|
|
1035
1092
|
taskBoard.markTaskStatus(envelope.task_id, 'running', { summary: `dispatched via ${sessionMode}` });
|
|
1036
1093
|
taskBoard.appendTaskEvent(envelope.task_id, 'task_started', targetProject, { session_mode: sessionMode });
|
|
@@ -1043,7 +1100,12 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1043
1100
|
}
|
|
1044
1101
|
});
|
|
1045
1102
|
|
|
1046
|
-
return {
|
|
1103
|
+
return {
|
|
1104
|
+
success: true,
|
|
1105
|
+
id: fullMsg.id,
|
|
1106
|
+
task_id: envelope ? envelope.task_id : null,
|
|
1107
|
+
scope_id: envelope ? envelope.scope_id : null,
|
|
1108
|
+
};
|
|
1047
1109
|
}
|
|
1048
1110
|
|
|
1049
1111
|
/**
|
|
@@ -1144,28 +1206,265 @@ function _findTeamBroadcastContext(fromKey, targetKey, config) {
|
|
|
1144
1206
|
return null;
|
|
1145
1207
|
}
|
|
1146
1208
|
|
|
1209
|
+
function resolveDispatchSenderChatId(item, config) {
|
|
1210
|
+
const requestedChatId = String(item && item.source_chat_id || '').trim();
|
|
1211
|
+
if (requestedChatId) return requestedChatId;
|
|
1212
|
+
|
|
1213
|
+
const feishuMap = (config && config.feishu && config.feishu.chat_agent_map) || {};
|
|
1214
|
+
const allowedFeishuIds = ((config && config.feishu && config.feishu.allowed_chat_ids) || []).map(String);
|
|
1215
|
+
const agentChatIds = new Set(Object.keys(feishuMap).map(String));
|
|
1216
|
+
const senderKey = String(item && (item.source_sender_key || item.from) || '').trim();
|
|
1217
|
+
const userSources = new Set(['', 'unknown', 'claude_session', '_claude_session', 'user']);
|
|
1218
|
+
|
|
1219
|
+
if (!userSources.has(senderKey)) {
|
|
1220
|
+
const directChatId = Object.entries(feishuMap).find(([, v]) => v === senderKey)?.[0] || null;
|
|
1221
|
+
if (directChatId) return String(directChatId);
|
|
1222
|
+
|
|
1223
|
+
const projects = (config && config.projects) || {};
|
|
1224
|
+
for (const [projKey, proj] of Object.entries(projects)) {
|
|
1225
|
+
if (!Array.isArray(proj && proj.team)) continue;
|
|
1226
|
+
const member = proj.team.find(m => m && m.key === senderKey);
|
|
1227
|
+
if (!member) continue;
|
|
1228
|
+
const groupChatId = Object.entries(feishuMap).find(([, v]) => v === projKey)?.[0] || null;
|
|
1229
|
+
if (groupChatId) return String(groupChatId);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return allowedFeishuIds.find(id => !agentChatIds.has(id)) || null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function writeDispatchReceiptInbox(item, receipt) {
|
|
1237
|
+
const senderKey = String(item && (item.source_sender_key || item.from) || '').trim();
|
|
1238
|
+
if (!senderKey || ['user', 'unknown', 'claude_session', '_claude_session'].includes(senderKey)) return;
|
|
1239
|
+
try {
|
|
1240
|
+
const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', senderKey);
|
|
1241
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
1242
|
+
const tsStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15);
|
|
1243
|
+
const targetKey = String(receipt && receipt.targetKey || item.target || 'unknown').trim() || 'unknown';
|
|
1244
|
+
const inboxFile = path.join(inboxDir, `${tsStr}_${targetKey}_dispatch_receipt.md`);
|
|
1245
|
+
const body = [
|
|
1246
|
+
`TYPE: dispatch_receipt`,
|
|
1247
|
+
`STATUS: ${receipt && receipt.status ? receipt.status : 'accepted'}`,
|
|
1248
|
+
`TARGET: ${targetKey}`,
|
|
1249
|
+
`DISPATCH_ID: ${receipt && receipt.dispatchId ? receipt.dispatchId : ''}`,
|
|
1250
|
+
`TS: ${new Date().toISOString()}`,
|
|
1251
|
+
'',
|
|
1252
|
+
String(receipt && receipt.text || '').trim() || '(empty receipt)',
|
|
1253
|
+
].join('\n');
|
|
1254
|
+
fs.writeFileSync(inboxFile, body, 'utf8');
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
log('WARN', `Dispatch receipt inbox write failed: ${e.message}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function sendDispatchReceipt(item, config, receipt) {
|
|
1261
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1262
|
+
const senderChatId = resolveDispatchSenderChatId(item, config);
|
|
1263
|
+
const text = String(receipt && receipt.text || '').trim();
|
|
1264
|
+
if (!text) return;
|
|
1265
|
+
|
|
1266
|
+
if (liveBot && senderChatId) {
|
|
1267
|
+
const send = liveBot.sendMarkdown
|
|
1268
|
+
? liveBot.sendMarkdown(senderChatId, text)
|
|
1269
|
+
: liveBot.sendMessage(senderChatId, text);
|
|
1270
|
+
send.catch((e) => {
|
|
1271
|
+
log('WARN', `Dispatch receipt delivery failed: ${e.message}`);
|
|
1272
|
+
writeDispatchReceiptInbox(item, receipt);
|
|
1273
|
+
});
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
writeDispatchReceiptInbox(item, receipt);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function buildTeamTaskResumeHint(taskId, scopeId) {
|
|
1281
|
+
const safeTaskId = String(taskId || '').trim();
|
|
1282
|
+
if (!safeTaskId) return '';
|
|
1283
|
+
const safeScopeId = String(scopeId || '').trim();
|
|
1284
|
+
const lines = [
|
|
1285
|
+
'',
|
|
1286
|
+
`TeamTask: ${safeTaskId}`,
|
|
1287
|
+
];
|
|
1288
|
+
if (safeScopeId && safeScopeId !== safeTaskId) lines.push(`Scope: ${safeScopeId}`);
|
|
1289
|
+
lines.push(`如需复工,请使用: /TeamTask resume ${safeTaskId}`);
|
|
1290
|
+
return lines.join('\n');
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function appendTeamTaskResumeHint(text, taskId, scopeId) {
|
|
1294
|
+
const base = String(text || '').trim();
|
|
1295
|
+
const hint = buildTeamTaskResumeHint(taskId, scopeId);
|
|
1296
|
+
if (!hint) return base;
|
|
1297
|
+
return `${base}${hint}`;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function buildDispatchPrompt(targetProject, fullMsg, envelope, metameDir = METAME_DIR) {
|
|
1301
|
+
const promptBody = buildEnrichedPrompt(
|
|
1302
|
+
targetProject,
|
|
1303
|
+
fullMsg && fullMsg.payload ? (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided') : 'No prompt provided',
|
|
1304
|
+
metameDir,
|
|
1305
|
+
{ includeShared: !!(envelope && envelope.task_kind === 'team') }
|
|
1306
|
+
);
|
|
1307
|
+
return envelope
|
|
1308
|
+
? buildPromptFromTaskEnvelope(envelope, promptBody)
|
|
1309
|
+
: promptBody;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function resolveDispatchTarget(targetKey, config) {
|
|
1313
|
+
const rawKey = String(targetKey || '').trim();
|
|
1314
|
+
const projects = (config && config.projects) || {};
|
|
1315
|
+
if (!rawKey) return null;
|
|
1316
|
+
if (projects[rawKey]) {
|
|
1317
|
+
const proj = projects[rawKey];
|
|
1318
|
+
return {
|
|
1319
|
+
key: rawKey,
|
|
1320
|
+
name: proj.name || rawKey,
|
|
1321
|
+
icon: proj.icon || '🤖',
|
|
1322
|
+
color: proj.color || 'blue',
|
|
1323
|
+
parentKey: rawKey,
|
|
1324
|
+
parentProject: proj,
|
|
1325
|
+
member: null,
|
|
1326
|
+
isTeamMember: false,
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
for (const [parentKey, parent] of Object.entries(projects)) {
|
|
1330
|
+
if (!Array.isArray(parent && parent.team)) continue;
|
|
1331
|
+
const member = parent.team.find(m => m && m.key === rawKey);
|
|
1332
|
+
if (!member) continue;
|
|
1333
|
+
return {
|
|
1334
|
+
key: rawKey,
|
|
1335
|
+
name: member.name || rawKey,
|
|
1336
|
+
icon: member.icon || parent.icon || '🤖',
|
|
1337
|
+
color: member.color || parent.color || 'blue',
|
|
1338
|
+
parentKey,
|
|
1339
|
+
parentProject: parent,
|
|
1340
|
+
member,
|
|
1341
|
+
isTeamMember: true,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function buildDispatchResponseCard(targetKey, config) {
|
|
1348
|
+
const target = resolveDispatchTarget(targetKey, config);
|
|
1349
|
+
if (!target) return null;
|
|
1350
|
+
return {
|
|
1351
|
+
title: `${target.icon} ${target.name}`,
|
|
1352
|
+
color: target.color || 'blue',
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function buildDispatchTaskCard(fullMsg, targetProject, config) {
|
|
1357
|
+
const projects = (config && config.projects) || {};
|
|
1358
|
+
const actor = resolveDispatchActor(
|
|
1359
|
+
(fullMsg && fullMsg.source_sender_key) || (fullMsg && fullMsg.from),
|
|
1360
|
+
projects
|
|
1361
|
+
);
|
|
1362
|
+
const target = resolveDispatchTarget(targetProject, config) || {
|
|
1363
|
+
icon: '🤖',
|
|
1364
|
+
name: targetProject,
|
|
1365
|
+
color: 'blue',
|
|
1366
|
+
};
|
|
1367
|
+
const prompt = String(fullMsg && fullMsg.payload && (fullMsg.payload.prompt || fullMsg.payload.title) || '').trim();
|
|
1368
|
+
const preview = prompt ? `${prompt.slice(0, 300)}${prompt.length > 300 ? '…' : ''}` : '(empty)';
|
|
1369
|
+
const lines = [
|
|
1370
|
+
`发起: ${actor.icon} ${actor.name}`,
|
|
1371
|
+
`目标: ${target.icon} ${target.name}`,
|
|
1372
|
+
`编号: ${fullMsg.id}`,
|
|
1373
|
+
];
|
|
1374
|
+
if (fullMsg.task_id) lines.push(`TeamTask: ${fullMsg.task_id}`);
|
|
1375
|
+
if (fullMsg.scope_id && fullMsg.scope_id !== fullMsg.task_id) lines.push(`Scope: ${fullMsg.scope_id}`);
|
|
1376
|
+
lines.push('', preview);
|
|
1377
|
+
return {
|
|
1378
|
+
title: '📬 新任务',
|
|
1379
|
+
body: lines.join('\n'),
|
|
1380
|
+
color: target.color || 'blue',
|
|
1381
|
+
markdown: `## 📬 新任务\n\n${lines.join('\n')}\n\n---\n${preview}`,
|
|
1382
|
+
text: `📬 新任务\n\n${lines.join('\n')}\n\n${preview}`,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function resolveDispatchReadOnly(message, config, targetProject) {
|
|
1387
|
+
if (message && typeof message.readOnly === 'boolean') return message.readOnly;
|
|
1388
|
+
const senderId = String((message && message.source_sender_id) || '').trim();
|
|
1389
|
+
if (senderId && userAcl && typeof userAcl.resolveUserCtx === 'function') {
|
|
1390
|
+
try {
|
|
1391
|
+
const userCtx = userAcl.resolveUserCtx(senderId, config || {});
|
|
1392
|
+
return !!userCtx.readOnly;
|
|
1393
|
+
} catch { /* fall through to safe default */ }
|
|
1394
|
+
}
|
|
1395
|
+
void targetProject;
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function sendDispatchTaskCard(bot, chatId, card) {
|
|
1400
|
+
if (!bot || !chatId || !card) return null;
|
|
1401
|
+
if (bot.sendCard) return bot.sendCard(chatId, { title: card.title, body: card.body, color: card.color || 'blue' });
|
|
1402
|
+
if (bot.sendMarkdown) return bot.sendMarkdown(chatId, card.markdown);
|
|
1403
|
+
return bot.sendMessage(chatId, card.text);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function buildDispatchReceipt(item, config, result, opts = {}) {
|
|
1407
|
+
const targetKey = String(opts.targetKey || item.target || '').trim() || 'unknown';
|
|
1408
|
+
const target = resolveDispatchTarget(targetKey, config) || {
|
|
1409
|
+
icon: '🤖',
|
|
1410
|
+
name: targetKey,
|
|
1411
|
+
};
|
|
1412
|
+
const actor = resolveDispatchActor(
|
|
1413
|
+
String(item && (item.source_sender_key || item.from) || 'user').trim() || 'user',
|
|
1414
|
+
(config && config.projects) || {}
|
|
1415
|
+
);
|
|
1416
|
+
const prompt = String(item && item.prompt || '').trim();
|
|
1417
|
+
const preview = prompt ? `${prompt.slice(0, 120)}${prompt.length > 120 ? '...' : ''}` : '(empty)';
|
|
1418
|
+
const isFailed = !result || !result.success;
|
|
1419
|
+
const title = isFailed ? '❌ Dispatch 回执' : '📮 Dispatch 回执';
|
|
1420
|
+
const statusLine = isFailed
|
|
1421
|
+
? `状态: 入队失败 (${String(result && result.error || 'unknown_error').slice(0, 120)})`
|
|
1422
|
+
: '状态: 目标端已接收并入队';
|
|
1423
|
+
const lines = [
|
|
1424
|
+
title,
|
|
1425
|
+
'',
|
|
1426
|
+
statusLine,
|
|
1427
|
+
`发起: ${actor.icon} ${actor.name}`,
|
|
1428
|
+
`目标: ${target.icon} ${target.name}`,
|
|
1429
|
+
];
|
|
1430
|
+
if (result && result.id) lines.push(`编号: ${result.id}`);
|
|
1431
|
+
lines.push(`摘要: ${preview}`);
|
|
1432
|
+
if (result && result.task_id) lines.push(buildTeamTaskResumeHint(result.task_id, result.scope_id));
|
|
1433
|
+
return {
|
|
1434
|
+
status: isFailed ? 'failed' : 'accepted',
|
|
1435
|
+
dispatchId: result && result.id ? result.id : '',
|
|
1436
|
+
targetKey,
|
|
1437
|
+
text: lines.join('\n'),
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1147
1441
|
function handleDispatchItem(item, config) {
|
|
1148
1442
|
if (!item.target || !item.prompt) return;
|
|
1149
|
-
|
|
1443
|
+
const resolvedTarget = resolveDispatchTarget(item.target, config);
|
|
1444
|
+
if (!resolvedTarget) {
|
|
1150
1445
|
log('WARN', `dispatch: unknown target "${item.target}"`);
|
|
1151
|
-
|
|
1446
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'unknown_target' }));
|
|
1447
|
+
return { success: false, error: 'unknown_target' };
|
|
1152
1448
|
}
|
|
1449
|
+
const targetKey = resolvedTarget.key;
|
|
1153
1450
|
// 安全护栏:禁止 agent 主动 dispatch 到 personal(防止 LLM 幻觉乱发消息给小美)
|
|
1154
1451
|
// personal 只允许用户本人触发,或来源为 user/unknown 的系统任务
|
|
1155
1452
|
const _agentSources = new Set(Object.keys((config.projects) || {}));
|
|
1156
1453
|
const isFromAgent = _agentSources.has(item.from) || item.from === '_claude_session';
|
|
1157
|
-
const targetProject = config.projects?.[
|
|
1454
|
+
const targetProject = config.projects?.[targetKey] || {};
|
|
1158
1455
|
if (isFromAgent && targetProject.guard === 'user-only') {
|
|
1159
|
-
log('WARN', `dispatch: blocked agent "${item.from}" → "${
|
|
1160
|
-
|
|
1456
|
+
log('WARN', `dispatch: blocked agent "${item.from}" → "${targetKey}" (user-only guard)`);
|
|
1457
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'target_guard_user_only' }));
|
|
1458
|
+
return { success: false, error: 'target_guard_user_only' };
|
|
1161
1459
|
}
|
|
1162
|
-
log('INFO', `Dispatch: ${item.from || '?'} → ${
|
|
1460
|
+
log('INFO', `Dispatch: ${item.from || '?'} → ${targetKey}: ${item.prompt.slice(0, 60)}`);
|
|
1163
1461
|
|
|
1164
1462
|
// ── Team broadcast: intra-team dispatch → show in group chat ──
|
|
1165
1463
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1166
|
-
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from,
|
|
1464
|
+
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, targetKey, config) : null;
|
|
1465
|
+
const responseCard = buildDispatchResponseCard(targetKey, config);
|
|
1167
1466
|
if (teamCtx && teamCtx.groupChatId) {
|
|
1168
|
-
const { senderMember, targetMember, groupChatId
|
|
1467
|
+
const { senderMember, targetMember, groupChatId } = teamCtx;
|
|
1169
1468
|
const sIcon = senderMember.icon || '🤖';
|
|
1170
1469
|
const sName = senderMember.name || senderMember.key;
|
|
1171
1470
|
const tIcon = targetMember.icon || '🤖';
|
|
@@ -1174,71 +1473,48 @@ function handleDispatchItem(item, config) {
|
|
|
1174
1473
|
const cardTitle = `${sIcon} ${sName} → ${tIcon} ${tName}`;
|
|
1175
1474
|
const cardBody = item.prompt.slice(0, 300) + (item.prompt.length > 300 ? '…' : '');
|
|
1176
1475
|
const cardColor = senderMember.color || 'blue';
|
|
1177
|
-
const
|
|
1476
|
+
const sendTaskNotice = liveBot.sendCard
|
|
1178
1477
|
? () => liveBot.sendCard(groupChatId, { title: cardTitle, body: cardBody, color: cardColor })
|
|
1179
1478
|
: () => liveBot.sendMarkdown(groupChatId, `**${cardTitle}**\n\n> ${cardBody}`);
|
|
1180
|
-
|
|
1181
|
-
//
|
|
1182
|
-
const streamOptions = {
|
|
1183
|
-
|
|
1479
|
+
// Use streamForwardBot so target's reply also shows in group.
|
|
1480
|
+
// Gate the worker output behind the task notice so the group always sees the task card first.
|
|
1481
|
+
const streamOptions = {
|
|
1482
|
+
bot: liveBot,
|
|
1483
|
+
chatId: groupChatId,
|
|
1484
|
+
preDispatch: () => sendTaskNotice().catch(e => log('WARN', `Team broadcast failed: ${e.message}`)),
|
|
1485
|
+
sendTaskCard: false,
|
|
1486
|
+
stripPlan: true,
|
|
1487
|
+
responseCard,
|
|
1488
|
+
};
|
|
1489
|
+
const result = dispatchTask(targetKey, {
|
|
1184
1490
|
from: item.from || 'claude_session',
|
|
1491
|
+
source_sender_id: item.source_sender_id || '',
|
|
1185
1492
|
type: 'task', priority: 'normal',
|
|
1186
1493
|
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1187
1494
|
callback: false,
|
|
1188
1495
|
new_session: !!item.new_session,
|
|
1496
|
+
source_chat_id: item.source_chat_id || '',
|
|
1497
|
+
source_sender_key: item.source_sender_key || item.from || '',
|
|
1498
|
+
source_sender_id: item.source_sender_id || '',
|
|
1189
1499
|
}, config, null, streamOptions);
|
|
1190
|
-
|
|
1500
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
|
|
1501
|
+
return result;
|
|
1191
1502
|
}
|
|
1192
1503
|
|
|
1193
1504
|
// ── Normal dispatch (non-team or broadcast off) ──
|
|
1194
|
-
let pendingReplyFn = null;
|
|
1505
|
+
let pendingReplyFn = typeof item._replyFn === 'function' ? item._replyFn : null;
|
|
1195
1506
|
let streamOptions = null;
|
|
1196
1507
|
if (liveBot) {
|
|
1197
1508
|
const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
1198
|
-
const
|
|
1199
|
-
const agentChatIds = new Set(Object.keys(feishuMap));
|
|
1200
|
-
const targetChatId = Object.entries(feishuMap).find(([, v]) => v === item.target)?.[0] || null;
|
|
1509
|
+
const targetChatId = Object.entries(feishuMap).find(([, v]) => v === targetKey)?.[0] || null;
|
|
1201
1510
|
if (targetChatId) {
|
|
1202
|
-
streamOptions = { bot: liveBot, chatId: targetChatId };
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
liveBot.sendMessage(targetChatId, ackText.replace(/\*\*/g, '')).catch(e =>
|
|
1206
|
-
log('WARN', `Dispatch ack failed: ${e.message}`)
|
|
1207
|
-
)
|
|
1208
|
-
);
|
|
1209
|
-
} else {
|
|
1210
|
-
const _userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
1211
|
-
let senderChatId = null;
|
|
1212
|
-
if (!_userSources.has(item.from)) {
|
|
1213
|
-
// Direct match: sender is a bound agent
|
|
1214
|
-
senderChatId = Object.entries(feishuMap).find(([, v]) => v === item.from)?.[0] || null;
|
|
1215
|
-
// Team member fallback: if sender is a team member (e.g., jarvis_c), find parent project's chatId
|
|
1216
|
-
if (!senderChatId) {
|
|
1217
|
-
const projects = config.projects || {};
|
|
1218
|
-
for (const [projKey, proj] of Object.entries(projects)) {
|
|
1219
|
-
if (proj.team && Array.isArray(proj.team)) {
|
|
1220
|
-
const member = proj.team.find(m => m.key === item.from);
|
|
1221
|
-
if (member && feishuMap[projKey]) {
|
|
1222
|
-
senderChatId = feishuMap[projKey];
|
|
1223
|
-
break;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (!senderChatId) {
|
|
1230
|
-
senderChatId = allowedFeishuIds.map(String).find(id => !agentChatIds.has(id)) || null;
|
|
1231
|
-
}
|
|
1511
|
+
streamOptions = { bot: liveBot, chatId: targetChatId, stripPlan: true, responseCard };
|
|
1512
|
+
} else if (!item._suppressDefaultReplyRouting) {
|
|
1513
|
+
const senderChatId = resolveDispatchSenderChatId(item, config);
|
|
1232
1514
|
if (senderChatId) {
|
|
1233
|
-
const targetProj = (config
|
|
1234
|
-
const ackText = `📬 已接收,转发给 ${targetProj.icon || '🤖'} **${targetProj.name || item.target}**...\n\n> ${item.prompt.slice(0, 100)}${item.prompt.length > 100 ? '...' : ''}`;
|
|
1235
|
-
liveBot.sendMarkdown(senderChatId, ackText).catch(() =>
|
|
1236
|
-
liveBot.sendMessage(senderChatId, ackText.replace(/\*\*/g, '')).catch(e =>
|
|
1237
|
-
log('WARN', `Dispatch ack to sender failed: ${e.message}`)
|
|
1238
|
-
)
|
|
1239
|
-
);
|
|
1515
|
+
const targetProj = resolveDispatchTarget(targetKey, config) || {};
|
|
1240
1516
|
pendingReplyFn = (output) => {
|
|
1241
|
-
const text = `${targetProj.icon || '📬'} **${targetProj.name ||
|
|
1517
|
+
const text = `${targetProj.icon || '📬'} **${targetProj.name || targetKey}** 回复:\n\n${output.slice(0, 2000)}`;
|
|
1242
1518
|
liveBot.sendMarkdown(senderChatId, text).catch(e => {
|
|
1243
1519
|
log('WARN', `Dispatch reply (markdown) failed: ${e.message}`);
|
|
1244
1520
|
liveBot.sendMessage(senderChatId, text.replace(/\*\*/g, '')).catch(e2 =>
|
|
@@ -1247,35 +1523,52 @@ function handleDispatchItem(item, config) {
|
|
|
1247
1523
|
});
|
|
1248
1524
|
};
|
|
1249
1525
|
// Also set streamOptions so target agent's streaming replies go to the sender's group
|
|
1250
|
-
streamOptions = { bot: liveBot, chatId: senderChatId };
|
|
1526
|
+
streamOptions = { bot: liveBot, chatId: senderChatId, stripPlan: true, responseCard };
|
|
1251
1527
|
}
|
|
1252
1528
|
}
|
|
1253
1529
|
}
|
|
1254
|
-
dispatchTask(
|
|
1530
|
+
const result = dispatchTask(targetKey, {
|
|
1255
1531
|
from: item.from || 'claude_session',
|
|
1532
|
+
source_sender_id: item.source_sender_id || '',
|
|
1256
1533
|
type: 'task', priority: 'normal',
|
|
1257
1534
|
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1258
1535
|
callback: false,
|
|
1259
1536
|
new_session: !!item.new_session,
|
|
1537
|
+
source_chat_id: item.source_chat_id || '',
|
|
1538
|
+
source_sender_key: item.source_sender_key || item.from || '',
|
|
1539
|
+
source_sender_id: item.source_sender_id || '',
|
|
1260
1540
|
}, config, pendingReplyFn, streamOptions);
|
|
1541
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
|
|
1542
|
+
return result;
|
|
1261
1543
|
}
|
|
1262
1544
|
|
|
1263
1545
|
async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
1264
1546
|
const rd = getRemoteDispatchConfig(config);
|
|
1265
1547
|
if (!rd || String(chatId) !== rd.chatId) return false;
|
|
1548
|
+
log('INFO', `Remote dispatch intercept chat=${chatId} preview=${String(text || '').slice(0, 48).replace(/\s+/g, ' ')}`);
|
|
1266
1549
|
|
|
1267
1550
|
const packet = decodeRemoteDispatchPacket(text);
|
|
1268
|
-
if (!packet)
|
|
1551
|
+
if (!packet) {
|
|
1552
|
+
log('INFO', 'Remote dispatch decode miss');
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1269
1555
|
if (!verifyRemoteDispatchPacket(packet, rd.secret)) {
|
|
1270
1556
|
log('WARN', 'Remote dispatch ignored: invalid signature');
|
|
1271
1557
|
return true;
|
|
1272
1558
|
}
|
|
1273
|
-
if (packet.from_peer === rd.selfPeer)
|
|
1274
|
-
|
|
1559
|
+
if (packet.from_peer === rd.selfPeer) {
|
|
1560
|
+
log('INFO', `Remote dispatch ignored: self echo id=${packet.id || ''}`);
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1563
|
+
if (packet.to_peer !== rd.selfPeer) {
|
|
1564
|
+
log('INFO', `Remote dispatch ignored: peer mismatch id=${packet.id || ''} to=${packet.to_peer || ''} self=${rd.selfPeer}`);
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1275
1567
|
if (isRemoteDispatchDuplicate(packet.id)) {
|
|
1276
1568
|
log('DEBUG', `Remote dispatch ignored: duplicate id=${packet.id}`);
|
|
1277
1569
|
return true;
|
|
1278
1570
|
}
|
|
1571
|
+
log('INFO', `Remote dispatch received type=${packet.type} id=${packet.id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1279
1572
|
|
|
1280
1573
|
if (packet.type === 'task') {
|
|
1281
1574
|
const replyFn = async (output) => {
|
|
@@ -1286,24 +1579,89 @@ async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
|
1286
1579
|
target_project: packet.target_project,
|
|
1287
1580
|
source_chat_id: packet.source_chat_id,
|
|
1288
1581
|
source_sender_key: packet.source_sender_key || 'user',
|
|
1582
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1289
1583
|
request_id: packet.id,
|
|
1290
1584
|
result: String(output || '').slice(0, 4000),
|
|
1291
1585
|
}, config);
|
|
1292
1586
|
if (!res.success) log('WARN', `Remote dispatch result send failed: ${res.error}`);
|
|
1293
1587
|
};
|
|
1294
1588
|
|
|
1295
|
-
handleDispatchItem({
|
|
1589
|
+
const dispatchRes = handleDispatchItem({
|
|
1296
1590
|
target: packet.target_project,
|
|
1297
1591
|
prompt: packet.prompt,
|
|
1298
1592
|
from: packet.source_sender_key || `${packet.from_peer}:remote`,
|
|
1299
1593
|
new_session: !!packet.new_session,
|
|
1594
|
+
source_chat_id: packet.source_chat_id || '',
|
|
1595
|
+
source_sender_key: packet.source_sender_key || '',
|
|
1596
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1300
1597
|
_replyFn: replyFn,
|
|
1301
1598
|
_suppressDefaultReplyRouting: true,
|
|
1302
1599
|
}, config);
|
|
1600
|
+
const ackRes = await sendRemoteDispatch({
|
|
1601
|
+
type: 'ack',
|
|
1602
|
+
to_peer: packet.from_peer,
|
|
1603
|
+
target_project: packet.target_project,
|
|
1604
|
+
source_chat_id: packet.source_chat_id,
|
|
1605
|
+
source_sender_key: packet.source_sender_key || 'user',
|
|
1606
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1607
|
+
request_id: packet.id,
|
|
1608
|
+
dispatch_id: dispatchRes && dispatchRes.id ? dispatchRes.id : '',
|
|
1609
|
+
task_id: dispatchRes && dispatchRes.task_id ? dispatchRes.task_id : '',
|
|
1610
|
+
scope_id: dispatchRes && dispatchRes.scope_id ? dispatchRes.scope_id : '',
|
|
1611
|
+
status: dispatchRes && dispatchRes.success ? 'accepted' : 'failed',
|
|
1612
|
+
error: dispatchRes && dispatchRes.success ? '' : String(dispatchRes && dispatchRes.error || 'dispatch_failed'),
|
|
1613
|
+
}, config);
|
|
1614
|
+
if (!ackRes.success) log('WARN', `Remote dispatch ack send failed: ${ackRes.error}`);
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if (packet.type === 'ack') {
|
|
1619
|
+
resolveTrackedRemoteDispatch(packet.request_id);
|
|
1620
|
+
log('INFO', `Remote dispatch ack id=${packet.request_id || ''} status=${packet.status} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1621
|
+
const text = String(packet.status) === 'accepted'
|
|
1622
|
+
? [
|
|
1623
|
+
'📮 远端 Dispatch 回执',
|
|
1624
|
+
'',
|
|
1625
|
+
`状态: ${packet.from_peer}:${packet.target_project || 'unknown'} 已接收并入队`,
|
|
1626
|
+
packet.dispatch_id ? `编号: ${packet.dispatch_id}` : '',
|
|
1627
|
+
packet.task_id ? buildTeamTaskResumeHint(packet.task_id, packet.scope_id) : '',
|
|
1628
|
+
].filter(Boolean).join('\n')
|
|
1629
|
+
: [
|
|
1630
|
+
'❌ 远端 Dispatch 回执',
|
|
1631
|
+
'',
|
|
1632
|
+
`状态: ${packet.from_peer}:${packet.target_project || 'unknown'} 入队失败`,
|
|
1633
|
+
packet.error ? `错误: ${String(packet.error).slice(0, 200)}` : '',
|
|
1634
|
+
].filter(Boolean).join('\n');
|
|
1635
|
+
|
|
1636
|
+
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
1637
|
+
if (targetChatId) {
|
|
1638
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1639
|
+
if (!liveBot) {
|
|
1640
|
+
writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, { status: packet.status, targetKey: packet.target_project, dispatchId: packet.dispatch_id, text });
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
try {
|
|
1644
|
+
if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, text);
|
|
1645
|
+
else await liveBot.sendMessage(targetChatId, text);
|
|
1646
|
+
} catch (e) {
|
|
1647
|
+
log('WARN', `Remote dispatch ack delivery failed: ${e.message}`);
|
|
1648
|
+
writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, { status: packet.status, targetKey: packet.target_project, dispatchId: packet.dispatch_id, text });
|
|
1649
|
+
}
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, {
|
|
1654
|
+
status: packet.status,
|
|
1655
|
+
targetKey: packet.target_project,
|
|
1656
|
+
dispatchId: packet.dispatch_id,
|
|
1657
|
+
text,
|
|
1658
|
+
});
|
|
1303
1659
|
return true;
|
|
1304
1660
|
}
|
|
1305
1661
|
|
|
1306
1662
|
if (packet.type === 'result') {
|
|
1663
|
+
resolveTrackedRemoteDispatch(packet.request_id);
|
|
1664
|
+
log('INFO', `Remote dispatch result id=${packet.request_id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1307
1665
|
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
1308
1666
|
if (!targetChatId) {
|
|
1309
1667
|
const inboxTarget = String(packet.source_sender_key || '').trim();
|
|
@@ -1361,8 +1719,8 @@ function startDispatchSocket(getConfig) {
|
|
|
1361
1719
|
try {
|
|
1362
1720
|
const item = JSON.parse(buf);
|
|
1363
1721
|
const liveCfg = typeof getConfig === 'function' ? getConfig() : getConfig;
|
|
1364
|
-
handleDispatchItem(item, liveCfg || {});
|
|
1365
|
-
conn.write(JSON.stringify({ ok:
|
|
1722
|
+
const result = handleDispatchItem(item, liveCfg || {});
|
|
1723
|
+
conn.write(JSON.stringify({ ok: !!(result && result.success), id: result && result.id ? result.id : null, error: result && result.error ? result.error : null }) + '\n');
|
|
1366
1724
|
} catch (e) {
|
|
1367
1725
|
try { conn.write(JSON.stringify({ ok: false, error: e.message }) + '\n'); } catch { /* ignore */ }
|
|
1368
1726
|
}
|
|
@@ -1420,9 +1778,19 @@ function physiologicalHeartbeat(config) {
|
|
|
1420
1778
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1421
1779
|
for (const item of items) {
|
|
1422
1780
|
if (item.relay_chat_id && item.body && liveBot && typeof liveBot.sendMessage === 'function') {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1781
|
+
const packet = decodeRemoteDispatchPacket(item.body);
|
|
1782
|
+
liveBot.sendMessage(item.relay_chat_id, item.body)
|
|
1783
|
+
.then(() => {
|
|
1784
|
+
if (packet) {
|
|
1785
|
+
log('INFO', `Remote dispatch queue sent type=${packet.type} id=${packet.id || ''} to=${packet.to_peer}:${packet.target_project || 'unknown'} via=${item.relay_chat_id}`);
|
|
1786
|
+
if (packet.type === 'task') trackRemoteDispatch(packet, config);
|
|
1787
|
+
} else {
|
|
1788
|
+
log('INFO', `Remote dispatch queue sent raw via=${item.relay_chat_id}`);
|
|
1789
|
+
}
|
|
1790
|
+
})
|
|
1791
|
+
.catch(e2 =>
|
|
1792
|
+
log('WARN', `Remote dispatch relay send failed: ${e2.message}`)
|
|
1793
|
+
);
|
|
1426
1794
|
}
|
|
1427
1795
|
}
|
|
1428
1796
|
}
|
|
@@ -1453,7 +1821,6 @@ function physiologicalHeartbeat(config) {
|
|
|
1453
1821
|
const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
|
|
1454
1822
|
const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
|
|
1455
1823
|
const FALLBACK_THROTTLE_MS = 8000; // 8s between fallback status updates
|
|
1456
|
-
const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
|
|
1457
1824
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1458
1825
|
|
|
1459
1826
|
// Rate limiter for /ask and /run — prevents rapid-fire Claude calls
|
|
@@ -1629,6 +1996,7 @@ async function doBindAgent(bot, chatId, agentName, agentCwd) {
|
|
|
1629
1996
|
// ---------------------------------------------------------
|
|
1630
1997
|
const {
|
|
1631
1998
|
findSessionFile,
|
|
1999
|
+
findCodexSessionFile,
|
|
1632
2000
|
clearSessionFileCache,
|
|
1633
2001
|
truncateSessionToCheckpoint,
|
|
1634
2002
|
listRecentSessions,
|
|
@@ -1638,15 +2006,17 @@ const {
|
|
|
1638
2006
|
sessionRichLabel,
|
|
1639
2007
|
getSessionRecentContext,
|
|
1640
2008
|
buildSessionCardElements,
|
|
1641
|
-
listProjectDirs,
|
|
1642
2009
|
getSession,
|
|
1643
2010
|
getSessionForEngine,
|
|
1644
2011
|
createSession,
|
|
2012
|
+
restoreSessionFromReply,
|
|
1645
2013
|
getSessionName,
|
|
1646
2014
|
writeSessionName,
|
|
1647
2015
|
markSessionStarted,
|
|
1648
2016
|
watchSessionFiles,
|
|
1649
2017
|
isEngineSessionValid,
|
|
2018
|
+
getCodexSessionSandboxProfile,
|
|
2019
|
+
getCodexSessionPermissionMode,
|
|
1650
2020
|
} = createSessionStore({
|
|
1651
2021
|
fs,
|
|
1652
2022
|
path,
|
|
@@ -1799,6 +2169,8 @@ const getEngineRuntime = createEngineRuntimeFactory({
|
|
|
1799
2169
|
getActiveProviderEnv,
|
|
1800
2170
|
});
|
|
1801
2171
|
|
|
2172
|
+
let wakeRecoveryHook = null;
|
|
2173
|
+
|
|
1802
2174
|
const {
|
|
1803
2175
|
checkPrecondition,
|
|
1804
2176
|
executeTask,
|
|
@@ -1827,6 +2199,7 @@ const {
|
|
|
1827
2199
|
isInSleepMode: () => _inSleepMode,
|
|
1828
2200
|
setSleepMode: (next) => { _inSleepMode = !!next; },
|
|
1829
2201
|
spawnSessionSummaries,
|
|
2202
|
+
getWakeRecoveryHook: () => wakeRecoveryHook,
|
|
1830
2203
|
skillEvolution,
|
|
1831
2204
|
});
|
|
1832
2205
|
|
|
@@ -1933,7 +2306,10 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
|
|
|
1933
2306
|
sendFileButtons,
|
|
1934
2307
|
findSessionFile,
|
|
1935
2308
|
listRecentSessions,
|
|
2309
|
+
getSessionRecentContext,
|
|
1936
2310
|
isEngineSessionValid,
|
|
2311
|
+
getCodexSessionSandboxProfile,
|
|
2312
|
+
getCodexSessionPermissionMode,
|
|
1937
2313
|
getSession,
|
|
1938
2314
|
getSessionForEngine,
|
|
1939
2315
|
createSession,
|
|
@@ -1993,6 +2369,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1993
2369
|
sendBrowse,
|
|
1994
2370
|
sendDirPicker,
|
|
1995
2371
|
getSession,
|
|
2372
|
+
getSessionForEngine,
|
|
1996
2373
|
listRecentSessions,
|
|
1997
2374
|
buildSessionCardElements,
|
|
1998
2375
|
sessionLabel,
|
|
@@ -2038,8 +2415,10 @@ const { handleExecCommand } = createExecCommandHandler({
|
|
|
2038
2415
|
getSessionName,
|
|
2039
2416
|
createSession,
|
|
2040
2417
|
findSessionFile,
|
|
2418
|
+
findCodexSessionFile,
|
|
2041
2419
|
loadConfig,
|
|
2042
2420
|
getDistillModel,
|
|
2421
|
+
getDefaultEngine,
|
|
2043
2422
|
});
|
|
2044
2423
|
|
|
2045
2424
|
const { handleOpsCommand } = createOpsCommandHandler({
|
|
@@ -2048,9 +2427,12 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2048
2427
|
spawn,
|
|
2049
2428
|
execSync,
|
|
2050
2429
|
log,
|
|
2430
|
+
loadConfig,
|
|
2431
|
+
loadState,
|
|
2051
2432
|
messageQueue,
|
|
2052
2433
|
activeProcesses,
|
|
2053
2434
|
getSession,
|
|
2435
|
+
getSessionForEngine,
|
|
2054
2436
|
listCheckpoints,
|
|
2055
2437
|
cpDisplayLabel,
|
|
2056
2438
|
truncateSessionToCheckpoint,
|
|
@@ -2061,6 +2443,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2061
2443
|
cleanupCheckpoints,
|
|
2062
2444
|
getNoSleepProcess: () => caffeinateProcess,
|
|
2063
2445
|
setNoSleepProcess: (p) => { caffeinateProcess = p || null; },
|
|
2446
|
+
getDefaultEngine,
|
|
2064
2447
|
});
|
|
2065
2448
|
|
|
2066
2449
|
const { handleCommand } = createCommandRouter({
|
|
@@ -2097,7 +2480,7 @@ setDispatchHandler(handleCommand);
|
|
|
2097
2480
|
// ---------------------------------------------------------
|
|
2098
2481
|
// BOT BRIDGES
|
|
2099
2482
|
// ---------------------------------------------------------
|
|
2100
|
-
const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
2483
|
+
const { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge } = createBridgeStarter({
|
|
2101
2484
|
fs,
|
|
2102
2485
|
path,
|
|
2103
2486
|
HOME,
|
|
@@ -2107,6 +2490,7 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
|
2107
2490
|
loadState,
|
|
2108
2491
|
saveState,
|
|
2109
2492
|
getSession,
|
|
2493
|
+
restoreSessionFromReply,
|
|
2110
2494
|
handleCommand,
|
|
2111
2495
|
pendingActivations,
|
|
2112
2496
|
activeProcesses,
|
|
@@ -2231,7 +2615,7 @@ async function main() {
|
|
|
2231
2615
|
}
|
|
2232
2616
|
|
|
2233
2617
|
// Config validation: warn on unknown/suspect fields
|
|
2234
|
-
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
|
|
2618
|
+
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects', 'imessage', 'siri_bridge'];
|
|
2235
2619
|
const KNOWN_DAEMON = [
|
|
2236
2620
|
'model', // legacy (still valid as fallback)
|
|
2237
2621
|
'models', // per-engine model map: { claude, codex }
|
|
@@ -2322,6 +2706,7 @@ async function main() {
|
|
|
2322
2706
|
// Bridges
|
|
2323
2707
|
let telegramBridge = null;
|
|
2324
2708
|
let feishuBridge = null;
|
|
2709
|
+
let lastWakeBridgeRecoveryAt = 0;
|
|
2325
2710
|
|
|
2326
2711
|
const notifier = createNotifier({
|
|
2327
2712
|
log,
|
|
@@ -2335,6 +2720,25 @@ async function main() {
|
|
|
2335
2720
|
// Start dispatch socket server (low-latency IPC, fallback: file polling still works)
|
|
2336
2721
|
const dispatchSocket = startDispatchSocket(() => config);
|
|
2337
2722
|
|
|
2723
|
+
wakeRecoveryHook = async ({ sleepSeconds }) => {
|
|
2724
|
+
const now = Date.now();
|
|
2725
|
+
if (now - lastWakeBridgeRecoveryAt < 60 * 1000) {
|
|
2726
|
+
log('INFO', `[WAKE-DETECT] bridge recovery skipped — cooldown active (${Math.round((now - lastWakeBridgeRecoveryAt) / 1000)}s since last)`);
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
lastWakeBridgeRecoveryAt = now;
|
|
2730
|
+
const tasks = [];
|
|
2731
|
+
if (telegramBridge && typeof telegramBridge.reconnect === 'function') {
|
|
2732
|
+
log('INFO', `[WAKE-DETECT] reconnecting Telegram bridge after ${sleepSeconds}s sleep`);
|
|
2733
|
+
tasks.push(Promise.resolve().then(() => telegramBridge.reconnect()));
|
|
2734
|
+
}
|
|
2735
|
+
if (feishuBridge && typeof feishuBridge.reconnect === 'function') {
|
|
2736
|
+
log('INFO', `[WAKE-DETECT] reconnecting Feishu bridge after ${sleepSeconds}s sleep`);
|
|
2737
|
+
tasks.push(Promise.resolve().then(() => feishuBridge.reconnect()));
|
|
2738
|
+
}
|
|
2739
|
+
await Promise.allSettled(tasks);
|
|
2740
|
+
};
|
|
2741
|
+
|
|
2338
2742
|
// Start heartbeat scheduler
|
|
2339
2743
|
let heartbeatTimer = startHeartbeat(config, notifyFn, notifyPersonalFn);
|
|
2340
2744
|
|
|
@@ -2399,6 +2803,8 @@ async function main() {
|
|
|
2399
2803
|
// Start bridges (both can run simultaneously)
|
|
2400
2804
|
telegramBridge = await startTelegramBridge(config, executeTaskByName);
|
|
2401
2805
|
feishuBridge = await startFeishuBridge(config, executeTaskByName);
|
|
2806
|
+
await startImessageBridge(config, executeTaskByName);
|
|
2807
|
+
await startSiriBridge(config, executeTaskByName);
|
|
2402
2808
|
if (feishuBridge) _dispatchBridgeRef = feishuBridge; // store bridge, not bot, so .bot stays live after reconnects
|
|
2403
2809
|
|
|
2404
2810
|
// Notify once on startup (single message, no duplicates)
|
|
@@ -2445,6 +2851,8 @@ async function main() {
|
|
|
2445
2851
|
try { require('./qmd-client').stopDaemon(); } catch { /* ignore */ }
|
|
2446
2852
|
// Kill all tracked engine process groups before exiting (covers sub-agents too)
|
|
2447
2853
|
for (const [cid, proc] of activeProcesses) {
|
|
2854
|
+
proc.aborted = true;
|
|
2855
|
+
proc.abortReason = opts.restartReason ? 'daemon-restart' : 'shutdown';
|
|
2448
2856
|
try { process.kill(-proc.child.pid, 'SIGKILL'); } catch { try { proc.child.kill('SIGKILL'); } catch { } }
|
|
2449
2857
|
log('INFO', `Shutdown: killed engine process group for chatId ${cid}`);
|
|
2450
2858
|
}
|
|
@@ -2458,6 +2866,10 @@ async function main() {
|
|
|
2458
2866
|
process.exit(0);
|
|
2459
2867
|
};
|
|
2460
2868
|
|
|
2869
|
+
process.on('SIGUSR2', () => {
|
|
2870
|
+
shutdown({ restartReason: process.env.METAME_DEPLOY_RESTART_REASON || 'external-restart' })
|
|
2871
|
+
.catch(() => process.exit(1));
|
|
2872
|
+
});
|
|
2461
2873
|
process.on('SIGTERM', () => { shutdown().catch(() => process.exit(0)); });
|
|
2462
2874
|
process.on('SIGINT', () => { shutdown().catch(() => process.exit(0)); });
|
|
2463
2875
|
|
|
@@ -2518,4 +2930,21 @@ if (process.argv.includes('--run')) {
|
|
|
2518
2930
|
}
|
|
2519
2931
|
|
|
2520
2932
|
// Export for testing & cross-bot dispatch
|
|
2521
|
-
module.exports = {
|
|
2933
|
+
module.exports = {
|
|
2934
|
+
executeTask,
|
|
2935
|
+
loadConfig,
|
|
2936
|
+
loadState,
|
|
2937
|
+
buildProfilePreamble,
|
|
2938
|
+
parseInterval,
|
|
2939
|
+
handleRemoteDispatchMessage,
|
|
2940
|
+
sendRemoteDispatch,
|
|
2941
|
+
__test: {
|
|
2942
|
+
buildDispatchPrompt,
|
|
2943
|
+
createStreamForwardBot,
|
|
2944
|
+
buildDispatchTaskCard,
|
|
2945
|
+
stripLeadingPlanSection,
|
|
2946
|
+
resolveDispatchTarget,
|
|
2947
|
+
resolveDispatchReadOnly,
|
|
2948
|
+
isMacLocalOrchestratorIntent,
|
|
2949
|
+
},
|
|
2950
|
+
};
|