metame-cli 1.5.4 → 1.5.6
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 +3 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +18 -6
- package/scripts/bin/push-clean.sh +72 -0
- package/scripts/daemon-admin-commands.js +266 -64
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +475 -50
- package/scripts/daemon-checkpoints.js +84 -30
- package/scripts/daemon-claude-engine.js +651 -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-dispatch-cards.js +185 -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 +28 -6
- 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/{team-dispatch.js → daemon-team-dispatch.js} +150 -11
- package/scripts/daemon.js +484 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +10 -3
- package/scripts/docs/pointer-map.md +2 -2
- 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/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
|
-
if (/(自动化|辅助功能|系统设置|隐私|权限|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|音量)/.test(text)) {
|
|
92
|
-
return true;
|
|
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);
|
|
94
92
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
// Require an actual automation ask. Mentioning "macOS" or "权限" alone should not route.
|
|
94
|
+
if (hasMacTool && hasAutomationVerb) return true;
|
|
95
|
+
|
|
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,22 @@ 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, updateDispatchContextFiles } = require('./daemon-team-dispatch');
|
|
151
|
+
const {
|
|
152
|
+
resolveDispatchTarget,
|
|
153
|
+
buildTeamTaskResumeHint,
|
|
154
|
+
appendTeamTaskResumeHint,
|
|
155
|
+
buildDispatchResponseCard,
|
|
156
|
+
buildDispatchTaskCard,
|
|
157
|
+
buildDispatchReceipt,
|
|
158
|
+
sendDispatchTaskCard,
|
|
159
|
+
} = require('./daemon-dispatch-cards');
|
|
152
160
|
const { createFileBrowser } = require('./daemon-file-browser');
|
|
153
161
|
const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
|
|
154
162
|
const { repairAgentLayer } = require('./agent-layer');
|
|
155
163
|
const { createNotifier } = require('./daemon-notify');
|
|
156
164
|
const { createClaudeEngine } = require('./daemon-claude-engine');
|
|
157
|
-
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG
|
|
165
|
+
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
|
|
158
166
|
const { createCommandRouter } = require('./daemon-command-router');
|
|
159
167
|
const { createTaskScheduler } = require('./daemon-task-scheduler');
|
|
160
168
|
const { createAgentTools } = require('./daemon-agent-tools');
|
|
@@ -520,15 +528,6 @@ function recordTokens(state, tokens, meta = null) {
|
|
|
520
528
|
}
|
|
521
529
|
|
|
522
530
|
|
|
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
531
|
const taskBoard = createTaskBoard({
|
|
533
532
|
logger: (msg) => log('WARN', msg),
|
|
534
533
|
});
|
|
@@ -540,12 +539,58 @@ const taskBoard = createTaskBoard({
|
|
|
540
539
|
// Late-bound reference to handleCommand (defined later in file)
|
|
541
540
|
let _handleCommand = null;
|
|
542
541
|
let _dispatchBridgeRef = null; // Store bridge (not bot) so .bot is always the live object after reconnects
|
|
542
|
+
const _pendingRemoteDispatches = new Map();
|
|
543
543
|
function setDispatchHandler(fn) { _handleCommand = fn; }
|
|
544
544
|
|
|
545
545
|
function getRemoteDispatchConfig(config) {
|
|
546
546
|
return normalizeRemoteDispatchConfig(config || {});
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
function trackRemoteDispatch(packet) {
|
|
550
|
+
if (!packet || packet.type !== 'task') return;
|
|
551
|
+
const requestId = String(packet.id || '').trim();
|
|
552
|
+
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
553
|
+
if (!requestId || !targetChatId) return;
|
|
554
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
555
|
+
const timeoutMs = 15000;
|
|
556
|
+
const existing = _pendingRemoteDispatches.get(requestId);
|
|
557
|
+
if (existing && existing.timer) clearTimeout(existing.timer);
|
|
558
|
+
const timer = setTimeout(async () => {
|
|
559
|
+
_pendingRemoteDispatches.delete(requestId);
|
|
560
|
+
const text = [
|
|
561
|
+
'⏱️ 远端 Dispatch 超时',
|
|
562
|
+
'',
|
|
563
|
+
`目标: ${packet.to_peer}:${packet.target_project || 'unknown'}`,
|
|
564
|
+
`请求: ${requestId}`,
|
|
565
|
+
`状态: 15s 内未收到回执`,
|
|
566
|
+
].join('\n');
|
|
567
|
+
log('WARN', `Remote dispatch timeout id=${requestId} target=${packet.to_peer}:${packet.target_project || 'unknown'}`);
|
|
568
|
+
if (!liveBot) return;
|
|
569
|
+
try {
|
|
570
|
+
if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, text);
|
|
571
|
+
else await liveBot.sendMessage(targetChatId, text);
|
|
572
|
+
} catch (e) {
|
|
573
|
+
log('WARN', `Remote dispatch timeout delivery failed: ${e.message}`);
|
|
574
|
+
}
|
|
575
|
+
}, timeoutMs);
|
|
576
|
+
_pendingRemoteDispatches.set(requestId, {
|
|
577
|
+
id: requestId,
|
|
578
|
+
targetChatId,
|
|
579
|
+
targetPeer: String(packet.to_peer || '').trim(),
|
|
580
|
+
targetProject: String(packet.target_project || '').trim(),
|
|
581
|
+
timer,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function resolveTrackedRemoteDispatch(requestId) {
|
|
586
|
+
const key = String(requestId || '').trim();
|
|
587
|
+
if (!key) return null;
|
|
588
|
+
const tracked = _pendingRemoteDispatches.get(key) || null;
|
|
589
|
+
if (tracked && tracked.timer) clearTimeout(tracked.timer);
|
|
590
|
+
if (tracked) _pendingRemoteDispatches.delete(key);
|
|
591
|
+
return tracked;
|
|
592
|
+
}
|
|
593
|
+
|
|
549
594
|
async function sendRemoteDispatch(packet, config) {
|
|
550
595
|
const rd = getRemoteDispatchConfig(config);
|
|
551
596
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
@@ -562,6 +607,10 @@ async function sendRemoteDispatch(packet, config) {
|
|
|
562
607
|
from_peer: rd.selfPeer,
|
|
563
608
|
}, rd.secret);
|
|
564
609
|
await liveBot.sendMessage(rd.chatId, body);
|
|
610
|
+
log('INFO', `Remote dispatch sent type=${packet.type} id=${id} to=${packet.to_peer}:${packet.target_project || 'unknown'} via=${rd.chatId}`);
|
|
611
|
+
if (packet.type === 'task') {
|
|
612
|
+
trackRemoteDispatch({ ...packet, id }, config);
|
|
613
|
+
}
|
|
565
614
|
return { success: true, id };
|
|
566
615
|
} catch (e) {
|
|
567
616
|
return { success: false, error: e.message };
|
|
@@ -587,38 +636,91 @@ function createNullBot(onOutput) {
|
|
|
587
636
|
};
|
|
588
637
|
}
|
|
589
638
|
|
|
639
|
+
function stripLeadingPlanSection(text) {
|
|
640
|
+
const src = String(text || '');
|
|
641
|
+
if (!src.trim()) return '';
|
|
642
|
+
const normalized = src.replace(/\r\n/g, '\n');
|
|
643
|
+
const paragraphs = normalized.split(/\n\s*\n/);
|
|
644
|
+
if (paragraphs.length === 0) return normalized.trim();
|
|
645
|
+
const first = String(paragraphs[0] || '').trim();
|
|
646
|
+
if (!/^计划[::]/.test(first)) return normalized.trim();
|
|
647
|
+
const rest = paragraphs.slice(1).join('\n\n').trim();
|
|
648
|
+
if (rest) return rest;
|
|
649
|
+
const lines = normalized.split('\n');
|
|
650
|
+
const remaining = lines.slice(1).join('\n').trim();
|
|
651
|
+
return remaining || first.replace(/^计划[::]\s*/, '').trim();
|
|
652
|
+
}
|
|
653
|
+
|
|
590
654
|
/**
|
|
591
655
|
* Forward bot: routes all calls to a real bot with a fixed chatId.
|
|
592
656
|
* Used for dispatch tasks so Claude's streaming output appears in the target's Feishu channel.
|
|
593
657
|
*/
|
|
594
|
-
function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
658
|
+
function createStreamForwardBot(realBot, chatId, onOutput = null, opts = {}) {
|
|
595
659
|
// Track edit-broken state independently so dispatch failures don't poison realBot's flag
|
|
596
660
|
let _editBroken = false;
|
|
661
|
+
const ready = opts && opts.ready && typeof opts.ready.then === 'function'
|
|
662
|
+
? opts.ready.catch(() => {})
|
|
663
|
+
: Promise.resolve();
|
|
664
|
+
async function waitUntilReady() {
|
|
665
|
+
await ready;
|
|
666
|
+
}
|
|
667
|
+
function normalizeOutput(payload) {
|
|
668
|
+
const text = typeof payload === 'object'
|
|
669
|
+
? (payload.body || payload.title || JSON.stringify(payload))
|
|
670
|
+
: String(payload);
|
|
671
|
+
return opts.stripPlan !== false ? stripLeadingPlanSection(text) : text;
|
|
672
|
+
}
|
|
673
|
+
async function deliver(text, rawText = text) {
|
|
674
|
+
const displayText = normalizeOutput(text);
|
|
675
|
+
if (onOutput) onOutput(rawText);
|
|
676
|
+
if (opts.responseCard && realBot.sendCard) {
|
|
677
|
+
return realBot.sendCard(chatId, {
|
|
678
|
+
title: opts.responseCard.title,
|
|
679
|
+
body: displayText,
|
|
680
|
+
color: opts.responseCard.color || 'blue',
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return realBot.sendMessage(chatId, displayText);
|
|
684
|
+
}
|
|
597
685
|
return {
|
|
598
686
|
sendMessage: async (_, text) => {
|
|
687
|
+
await waitUntilReady();
|
|
599
688
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] msg: ${String(text).slice(0, 80)}`);
|
|
600
|
-
|
|
601
|
-
return realBot.sendMessage(chatId, text);
|
|
689
|
+
return deliver(text, text);
|
|
602
690
|
},
|
|
603
691
|
sendMarkdown: async (_, text) => {
|
|
692
|
+
await waitUntilReady();
|
|
604
693
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] md: ${String(text).slice(0, 80)}`);
|
|
694
|
+
if (opts.responseCard && realBot.sendCard) {
|
|
695
|
+
const displayText = normalizeOutput(text);
|
|
696
|
+
if (onOutput) onOutput(text);
|
|
697
|
+
return realBot.sendCard(chatId, {
|
|
698
|
+
title: opts.responseCard.title,
|
|
699
|
+
body: displayText,
|
|
700
|
+
color: opts.responseCard.color || 'blue',
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
const displayText = normalizeOutput(text);
|
|
605
704
|
if (onOutput) onOutput(text);
|
|
606
|
-
return realBot.sendMarkdown(chatId,
|
|
705
|
+
return realBot.sendMarkdown(chatId, displayText);
|
|
607
706
|
},
|
|
608
707
|
sendCard: async (_, card) => {
|
|
708
|
+
await waitUntilReady();
|
|
609
709
|
const title = typeof card === 'object' ? (card.title || card.body || '').slice(0, 60) : String(card).slice(0, 60);
|
|
610
710
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] card: ${title}`);
|
|
611
711
|
if (onOutput) onOutput(typeof card === 'object' ? (card.body || card.title || JSON.stringify(card)) : card);
|
|
612
712
|
return realBot.sendCard(chatId, card);
|
|
613
713
|
},
|
|
614
714
|
sendRawCard: async (_, header, elements) => {
|
|
715
|
+
await waitUntilReady();
|
|
615
716
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] rawcard: ${String(header).slice(0, 60)}`);
|
|
616
717
|
if (onOutput) onOutput(header);
|
|
617
718
|
return realBot.sendRawCard(chatId, header, elements);
|
|
618
719
|
},
|
|
619
|
-
sendButtons: async (_, text, buttons) => realBot.sendButtons(chatId, text, buttons),
|
|
620
|
-
sendTyping: async () => realBot.sendTyping(chatId),
|
|
720
|
+
sendButtons: async (_, text, buttons) => { await waitUntilReady(); return realBot.sendButtons(chatId, text, buttons); },
|
|
721
|
+
sendTyping: async () => { await waitUntilReady(); return realBot.sendTyping(chatId); },
|
|
621
722
|
editMessage: async (_, msgId, text) => {
|
|
723
|
+
await waitUntilReady();
|
|
622
724
|
if (_editBroken) return false;
|
|
623
725
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] edit ${String(msgId).slice(-8)}: ${String(text).slice(0, 60)}`);
|
|
624
726
|
try {
|
|
@@ -631,8 +733,8 @@ function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
|
631
733
|
return false;
|
|
632
734
|
}
|
|
633
735
|
},
|
|
634
|
-
deleteMessage: async (_, msgId) => realBot.deleteMessage(chatId, msgId),
|
|
635
|
-
sendFile: async (_, filePath, caption) => realBot.sendFile(chatId, filePath, caption),
|
|
736
|
+
deleteMessage: async (_, msgId) => { await waitUntilReady(); return realBot.deleteMessage(chatId, msgId); },
|
|
737
|
+
sendFile: async (_, filePath, caption) => { await waitUntilReady(); return realBot.sendFile(chatId, filePath, caption); },
|
|
636
738
|
downloadFile: async (...args) => realBot.downloadFile(...args),
|
|
637
739
|
};
|
|
638
740
|
}
|
|
@@ -802,6 +904,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
802
904
|
const fullMsg = {
|
|
803
905
|
id: `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
804
906
|
from: message.from || 'unknown',
|
|
907
|
+
source_sender_id: String(message.source_sender_id || '').trim() || '',
|
|
805
908
|
to: targetProject,
|
|
806
909
|
type: message.type || 'task',
|
|
807
910
|
priority: message.priority || 'normal',
|
|
@@ -859,79 +962,23 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
859
962
|
if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
860
963
|
fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
|
|
861
964
|
|
|
862
|
-
// Auto-update
|
|
965
|
+
// Auto-update scoped dispatch context files; only TeamTask writes shared state.
|
|
863
966
|
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
|
-
}
|
|
967
|
+
updateDispatchContextFiles({
|
|
968
|
+
fs,
|
|
969
|
+
path,
|
|
970
|
+
baseDir: METAME_DIR,
|
|
971
|
+
fullMsg,
|
|
972
|
+
targetProject,
|
|
973
|
+
config,
|
|
974
|
+
envelope,
|
|
975
|
+
logger: (msg) => log('WARN', msg),
|
|
976
|
+
});
|
|
928
977
|
} catch (e) {
|
|
929
|
-
log('WARN', `Failed to update
|
|
978
|
+
log('WARN', `Failed to update dispatch context files: ${e.message}`);
|
|
930
979
|
}
|
|
931
980
|
|
|
932
|
-
const rawPrompt = envelope
|
|
933
|
-
? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
|
|
934
|
-
: (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
|
|
981
|
+
const rawPrompt = buildDispatchPrompt(targetProject, fullMsg, envelope);
|
|
935
982
|
|
|
936
983
|
// Inject sender identity when dispatched by another agent (not directly from user)
|
|
937
984
|
const userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
@@ -957,10 +1004,25 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
957
1004
|
const dispatchChatId = buildDispatchChatId(targetProject, envelope && envelope.scope_id);
|
|
958
1005
|
const sessionMode = forceNew ? 'fresh session (forced)' : 'existing virtual session';
|
|
959
1006
|
log('INFO', `Dispatching ${fullMsg.type} to ${targetProject} via ${sessionMode}: ${rawPrompt.slice(0, 80)}`);
|
|
1007
|
+
const streamReady = streamOptions?.bot && streamOptions?.chatId
|
|
1008
|
+
? (() => {
|
|
1009
|
+
if (typeof streamOptions.preDispatch === 'function') {
|
|
1010
|
+
return Promise.resolve()
|
|
1011
|
+
.then(() => streamOptions.preDispatch())
|
|
1012
|
+
.catch(e => log('WARN', `Dispatch prelude failed: ${e.message}`));
|
|
1013
|
+
}
|
|
1014
|
+
if (streamOptions.sendTaskCard === false) return Promise.resolve();
|
|
1015
|
+
const card = buildDispatchTaskCard(fullMsg, targetProject, config);
|
|
1016
|
+
return Promise.resolve()
|
|
1017
|
+
.then(() => sendDispatchTaskCard(streamOptions.bot, streamOptions.chatId, card))
|
|
1018
|
+
.catch(e => log('WARN', `Dispatch task card failed: ${e.message}`));
|
|
1019
|
+
})()
|
|
1020
|
+
: Promise.resolve();
|
|
960
1021
|
|
|
961
1022
|
let _taskFinalized = false;
|
|
962
1023
|
const outputHandler = (output) => {
|
|
963
1024
|
const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
|
|
1025
|
+
const displayOut = envelope ? appendTeamTaskResumeHint(outStr, envelope.task_id, envelope.scope_id) : outStr;
|
|
964
1026
|
log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
|
|
965
1027
|
if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
|
|
966
1028
|
const status = inferTaskStatusFromOutput(outStr);
|
|
@@ -979,7 +1041,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
979
1041
|
_taskFinalized = true;
|
|
980
1042
|
}
|
|
981
1043
|
if (replyFn && outStr.trim().length > 2) {
|
|
982
|
-
replyFn(
|
|
1044
|
+
replyFn(displayOut);
|
|
983
1045
|
} else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
|
|
984
1046
|
// Write result to sender's inbox before dispatching callback
|
|
985
1047
|
try {
|
|
@@ -994,7 +1056,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
994
1056
|
`TS: ${new Date().toISOString()}`,
|
|
995
1057
|
`SUBJECT: ${subject}`,
|
|
996
1058
|
'',
|
|
997
|
-
|
|
1059
|
+
displayOut.slice(0, 2000),
|
|
998
1060
|
].join('\n');
|
|
999
1061
|
fs.writeFileSync(inboxFile, body, 'utf8');
|
|
1000
1062
|
} catch (e) {
|
|
@@ -1002,12 +1064,13 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1002
1064
|
}
|
|
1003
1065
|
dispatchTask(fullMsg.from, {
|
|
1004
1066
|
from: targetProject,
|
|
1067
|
+
source_sender_id: fullMsg.source_sender_id || '',
|
|
1005
1068
|
type: 'callback',
|
|
1006
1069
|
priority: 'normal',
|
|
1007
1070
|
payload: {
|
|
1008
1071
|
title: `任务完成: ${fullMsg.payload.title || fullMsg.id}`,
|
|
1009
1072
|
original_id: fullMsg.id,
|
|
1010
|
-
output:
|
|
1073
|
+
output: displayOut.slice(0, 500),
|
|
1011
1074
|
},
|
|
1012
1075
|
chain: [], // reset chain for callbacks
|
|
1013
1076
|
}, config);
|
|
@@ -1016,11 +1079,14 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1016
1079
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
1017
1080
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
1018
1081
|
const nullBot = streamOptions?.bot && streamOptions?.chatId
|
|
1019
|
-
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler
|
|
1082
|
+
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler, {
|
|
1083
|
+
ready: streamReady,
|
|
1084
|
+
stripPlan: streamOptions.stripPlan !== false,
|
|
1085
|
+
responseCard: streamOptions.responseCard || null,
|
|
1086
|
+
})
|
|
1020
1087
|
: createNullBot(outputHandler);
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
// Otherwise fall back to readOnly (safe default for untrusted daemon configs).
|
|
1088
|
+
// Trusted dispatches (user / bound agent / team member) keep write access.
|
|
1089
|
+
// Only unknown senders are downgraded to read-only.
|
|
1024
1090
|
// When forceNew=true, clear any cached session for this virtual chatId so
|
|
1025
1091
|
// attachOrCreateSession in handleCommand actually creates a fresh Claude session.
|
|
1026
1092
|
if (forceNew) {
|
|
@@ -1030,7 +1096,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1030
1096
|
saveState(st);
|
|
1031
1097
|
}
|
|
1032
1098
|
}
|
|
1033
|
-
const dispatchReadOnly =
|
|
1099
|
+
const dispatchReadOnly = resolveDispatchReadOnly(message, config, targetProject);
|
|
1034
1100
|
if (envelope && taskBoard) {
|
|
1035
1101
|
taskBoard.markTaskStatus(envelope.task_id, 'running', { summary: `dispatched via ${sessionMode}` });
|
|
1036
1102
|
taskBoard.appendTaskEvent(envelope.task_id, 'task_started', targetProject, { session_mode: sessionMode });
|
|
@@ -1043,7 +1109,12 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1043
1109
|
}
|
|
1044
1110
|
});
|
|
1045
1111
|
|
|
1046
|
-
return {
|
|
1112
|
+
return {
|
|
1113
|
+
success: true,
|
|
1114
|
+
id: fullMsg.id,
|
|
1115
|
+
task_id: envelope ? envelope.task_id : null,
|
|
1116
|
+
scope_id: envelope ? envelope.scope_id : null,
|
|
1117
|
+
};
|
|
1047
1118
|
}
|
|
1048
1119
|
|
|
1049
1120
|
/**
|
|
@@ -1144,28 +1215,130 @@ function _findTeamBroadcastContext(fromKey, targetKey, config) {
|
|
|
1144
1215
|
return null;
|
|
1145
1216
|
}
|
|
1146
1217
|
|
|
1218
|
+
function resolveDispatchSenderChatId(item, config) {
|
|
1219
|
+
const requestedChatId = String(item && item.source_chat_id || '').trim();
|
|
1220
|
+
if (requestedChatId) return requestedChatId;
|
|
1221
|
+
|
|
1222
|
+
const feishuMap = (config && config.feishu && config.feishu.chat_agent_map) || {};
|
|
1223
|
+
const allowedFeishuIds = ((config && config.feishu && config.feishu.allowed_chat_ids) || []).map(String);
|
|
1224
|
+
const agentChatIds = new Set(Object.keys(feishuMap).map(String));
|
|
1225
|
+
const senderKey = String(item && (item.source_sender_key || item.from) || '').trim();
|
|
1226
|
+
const userSources = new Set(['', 'unknown', 'claude_session', '_claude_session', 'user']);
|
|
1227
|
+
|
|
1228
|
+
if (!userSources.has(senderKey)) {
|
|
1229
|
+
const directChatId = Object.entries(feishuMap).find(([, v]) => v === senderKey)?.[0] || null;
|
|
1230
|
+
if (directChatId) return String(directChatId);
|
|
1231
|
+
|
|
1232
|
+
const projects = (config && config.projects) || {};
|
|
1233
|
+
for (const [projKey, proj] of Object.entries(projects)) {
|
|
1234
|
+
if (!Array.isArray(proj && proj.team)) continue;
|
|
1235
|
+
const member = proj.team.find(m => m && m.key === senderKey);
|
|
1236
|
+
if (!member) continue;
|
|
1237
|
+
const groupChatId = Object.entries(feishuMap).find(([, v]) => v === projKey)?.[0] || null;
|
|
1238
|
+
if (groupChatId) return String(groupChatId);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return allowedFeishuIds.find(id => !agentChatIds.has(id)) || null;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function writeDispatchReceiptInbox(item, receipt) {
|
|
1246
|
+
const senderKey = String(item && (item.source_sender_key || item.from) || '').trim();
|
|
1247
|
+
if (!senderKey || ['user', 'unknown', 'claude_session', '_claude_session'].includes(senderKey)) return;
|
|
1248
|
+
try {
|
|
1249
|
+
const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', senderKey);
|
|
1250
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
1251
|
+
const tsStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15);
|
|
1252
|
+
const targetKey = String(receipt && receipt.targetKey || item.target || 'unknown').trim() || 'unknown';
|
|
1253
|
+
const inboxFile = path.join(inboxDir, `${tsStr}_${targetKey}_dispatch_receipt.md`);
|
|
1254
|
+
const body = [
|
|
1255
|
+
`TYPE: dispatch_receipt`,
|
|
1256
|
+
`STATUS: ${receipt && receipt.status ? receipt.status : 'accepted'}`,
|
|
1257
|
+
`TARGET: ${targetKey}`,
|
|
1258
|
+
`DISPATCH_ID: ${receipt && receipt.dispatchId ? receipt.dispatchId : ''}`,
|
|
1259
|
+
`TS: ${new Date().toISOString()}`,
|
|
1260
|
+
'',
|
|
1261
|
+
String(receipt && receipt.text || '').trim() || '(empty receipt)',
|
|
1262
|
+
].join('\n');
|
|
1263
|
+
fs.writeFileSync(inboxFile, body, 'utf8');
|
|
1264
|
+
} catch (e) {
|
|
1265
|
+
log('WARN', `Dispatch receipt inbox write failed: ${e.message}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function sendDispatchReceipt(item, config, receipt) {
|
|
1270
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1271
|
+
const senderChatId = resolveDispatchSenderChatId(item, config);
|
|
1272
|
+
const text = String(receipt && receipt.text || '').trim();
|
|
1273
|
+
if (!text) return;
|
|
1274
|
+
|
|
1275
|
+
if (liveBot && senderChatId) {
|
|
1276
|
+
const send = liveBot.sendMarkdown
|
|
1277
|
+
? liveBot.sendMarkdown(senderChatId, text)
|
|
1278
|
+
: liveBot.sendMessage(senderChatId, text);
|
|
1279
|
+
send.catch((e) => {
|
|
1280
|
+
log('WARN', `Dispatch receipt delivery failed: ${e.message}`);
|
|
1281
|
+
writeDispatchReceiptInbox(item, receipt);
|
|
1282
|
+
});
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
writeDispatchReceiptInbox(item, receipt);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function buildDispatchPrompt(targetProject, fullMsg, envelope, metameDir = METAME_DIR) {
|
|
1290
|
+
const promptBody = buildEnrichedPrompt(
|
|
1291
|
+
targetProject,
|
|
1292
|
+
fullMsg && fullMsg.payload ? (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided') : 'No prompt provided',
|
|
1293
|
+
metameDir,
|
|
1294
|
+
{ includeShared: !!(envelope && envelope.task_kind === 'team') }
|
|
1295
|
+
);
|
|
1296
|
+
return envelope
|
|
1297
|
+
? buildPromptFromTaskEnvelope(envelope, promptBody)
|
|
1298
|
+
: promptBody;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
function resolveDispatchReadOnly(message, config, targetProject) {
|
|
1303
|
+
if (message && typeof message.readOnly === 'boolean') return message.readOnly;
|
|
1304
|
+
const senderId = String((message && message.source_sender_id) || '').trim();
|
|
1305
|
+
if (senderId && userAcl && typeof userAcl.resolveUserCtx === 'function') {
|
|
1306
|
+
try {
|
|
1307
|
+
const userCtx = userAcl.resolveUserCtx(senderId, config || {});
|
|
1308
|
+
return !!userCtx.readOnly;
|
|
1309
|
+
} catch { /* fall through to safe default */ }
|
|
1310
|
+
}
|
|
1311
|
+
void targetProject;
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1147
1315
|
function handleDispatchItem(item, config) {
|
|
1148
1316
|
if (!item.target || !item.prompt) return;
|
|
1149
|
-
|
|
1317
|
+
const resolvedTarget = resolveDispatchTarget(item.target, config);
|
|
1318
|
+
if (!resolvedTarget) {
|
|
1150
1319
|
log('WARN', `dispatch: unknown target "${item.target}"`);
|
|
1151
|
-
|
|
1320
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'unknown_target' }));
|
|
1321
|
+
return { success: false, error: 'unknown_target' };
|
|
1152
1322
|
}
|
|
1323
|
+
const targetKey = resolvedTarget.key;
|
|
1153
1324
|
// 安全护栏:禁止 agent 主动 dispatch 到 personal(防止 LLM 幻觉乱发消息给小美)
|
|
1154
1325
|
// personal 只允许用户本人触发,或来源为 user/unknown 的系统任务
|
|
1155
1326
|
const _agentSources = new Set(Object.keys((config.projects) || {}));
|
|
1156
1327
|
const isFromAgent = _agentSources.has(item.from) || item.from === '_claude_session';
|
|
1157
|
-
const targetProject = config.projects?.[
|
|
1328
|
+
const targetProject = config.projects?.[targetKey] || {};
|
|
1158
1329
|
if (isFromAgent && targetProject.guard === 'user-only') {
|
|
1159
|
-
log('WARN', `dispatch: blocked agent "${item.from}" → "${
|
|
1160
|
-
|
|
1330
|
+
log('WARN', `dispatch: blocked agent "${item.from}" → "${targetKey}" (user-only guard)`);
|
|
1331
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'target_guard_user_only' }));
|
|
1332
|
+
return { success: false, error: 'target_guard_user_only' };
|
|
1161
1333
|
}
|
|
1162
|
-
log('INFO', `Dispatch: ${item.from || '?'} → ${
|
|
1334
|
+
log('INFO', `Dispatch: ${item.from || '?'} → ${targetKey}: ${item.prompt.slice(0, 60)}`);
|
|
1163
1335
|
|
|
1164
1336
|
// ── Team broadcast: intra-team dispatch → show in group chat ──
|
|
1165
1337
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1166
|
-
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from,
|
|
1338
|
+
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, targetKey, config) : null;
|
|
1339
|
+
const responseCard = buildDispatchResponseCard(targetKey, config);
|
|
1167
1340
|
if (teamCtx && teamCtx.groupChatId) {
|
|
1168
|
-
const { senderMember, targetMember, groupChatId
|
|
1341
|
+
const { senderMember, targetMember, groupChatId } = teamCtx;
|
|
1169
1342
|
const sIcon = senderMember.icon || '🤖';
|
|
1170
1343
|
const sName = senderMember.name || senderMember.key;
|
|
1171
1344
|
const tIcon = targetMember.icon || '🤖';
|
|
@@ -1174,71 +1347,48 @@ function handleDispatchItem(item, config) {
|
|
|
1174
1347
|
const cardTitle = `${sIcon} ${sName} → ${tIcon} ${tName}`;
|
|
1175
1348
|
const cardBody = item.prompt.slice(0, 300) + (item.prompt.length > 300 ? '…' : '');
|
|
1176
1349
|
const cardColor = senderMember.color || 'blue';
|
|
1177
|
-
const
|
|
1350
|
+
const sendTaskNotice = liveBot.sendCard
|
|
1178
1351
|
? () => liveBot.sendCard(groupChatId, { title: cardTitle, body: cardBody, color: cardColor })
|
|
1179
1352
|
: () => liveBot.sendMarkdown(groupChatId, `**${cardTitle}**\n\n> ${cardBody}`);
|
|
1180
|
-
|
|
1181
|
-
//
|
|
1182
|
-
const streamOptions = {
|
|
1183
|
-
|
|
1353
|
+
// Use streamForwardBot so target's reply also shows in group.
|
|
1354
|
+
// Gate the worker output behind the task notice so the group always sees the task card first.
|
|
1355
|
+
const streamOptions = {
|
|
1356
|
+
bot: liveBot,
|
|
1357
|
+
chatId: groupChatId,
|
|
1358
|
+
preDispatch: () => sendTaskNotice().catch(e => log('WARN', `Team broadcast failed: ${e.message}`)),
|
|
1359
|
+
sendTaskCard: false,
|
|
1360
|
+
stripPlan: true,
|
|
1361
|
+
responseCard,
|
|
1362
|
+
};
|
|
1363
|
+
const result = dispatchTask(targetKey, {
|
|
1184
1364
|
from: item.from || 'claude_session',
|
|
1365
|
+
source_sender_id: item.source_sender_id || '',
|
|
1185
1366
|
type: 'task', priority: 'normal',
|
|
1186
1367
|
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1187
1368
|
callback: false,
|
|
1188
1369
|
new_session: !!item.new_session,
|
|
1370
|
+
source_chat_id: item.source_chat_id || '',
|
|
1371
|
+
source_sender_key: item.source_sender_key || item.from || '',
|
|
1372
|
+
source_sender_id: item.source_sender_id || '',
|
|
1189
1373
|
}, config, null, streamOptions);
|
|
1190
|
-
|
|
1374
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
|
|
1375
|
+
return result;
|
|
1191
1376
|
}
|
|
1192
1377
|
|
|
1193
1378
|
// ── Normal dispatch (non-team or broadcast off) ──
|
|
1194
|
-
let pendingReplyFn = null;
|
|
1379
|
+
let pendingReplyFn = typeof item._replyFn === 'function' ? item._replyFn : null;
|
|
1195
1380
|
let streamOptions = null;
|
|
1196
1381
|
if (liveBot) {
|
|
1197
1382
|
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;
|
|
1383
|
+
const targetChatId = Object.entries(feishuMap).find(([, v]) => v === targetKey)?.[0] || null;
|
|
1201
1384
|
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
|
-
}
|
|
1385
|
+
streamOptions = { bot: liveBot, chatId: targetChatId, stripPlan: true, responseCard };
|
|
1386
|
+
} else if (!item._suppressDefaultReplyRouting) {
|
|
1387
|
+
const senderChatId = resolveDispatchSenderChatId(item, config);
|
|
1232
1388
|
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
|
-
);
|
|
1389
|
+
const targetProj = resolveDispatchTarget(targetKey, config) || {};
|
|
1240
1390
|
pendingReplyFn = (output) => {
|
|
1241
|
-
const text = `${targetProj.icon || '📬'} **${targetProj.name ||
|
|
1391
|
+
const text = `${targetProj.icon || '📬'} **${targetProj.name || targetKey}** 回复:\n\n${output.slice(0, 2000)}`;
|
|
1242
1392
|
liveBot.sendMarkdown(senderChatId, text).catch(e => {
|
|
1243
1393
|
log('WARN', `Dispatch reply (markdown) failed: ${e.message}`);
|
|
1244
1394
|
liveBot.sendMessage(senderChatId, text.replace(/\*\*/g, '')).catch(e2 =>
|
|
@@ -1247,35 +1397,52 @@ function handleDispatchItem(item, config) {
|
|
|
1247
1397
|
});
|
|
1248
1398
|
};
|
|
1249
1399
|
// Also set streamOptions so target agent's streaming replies go to the sender's group
|
|
1250
|
-
streamOptions = { bot: liveBot, chatId: senderChatId };
|
|
1400
|
+
streamOptions = { bot: liveBot, chatId: senderChatId, stripPlan: true, responseCard };
|
|
1251
1401
|
}
|
|
1252
1402
|
}
|
|
1253
1403
|
}
|
|
1254
|
-
dispatchTask(
|
|
1404
|
+
const result = dispatchTask(targetKey, {
|
|
1255
1405
|
from: item.from || 'claude_session',
|
|
1406
|
+
source_sender_id: item.source_sender_id || '',
|
|
1256
1407
|
type: 'task', priority: 'normal',
|
|
1257
1408
|
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1258
1409
|
callback: false,
|
|
1259
1410
|
new_session: !!item.new_session,
|
|
1411
|
+
source_chat_id: item.source_chat_id || '',
|
|
1412
|
+
source_sender_key: item.source_sender_key || item.from || '',
|
|
1413
|
+
source_sender_id: item.source_sender_id || '',
|
|
1260
1414
|
}, config, pendingReplyFn, streamOptions);
|
|
1415
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
|
|
1416
|
+
return result;
|
|
1261
1417
|
}
|
|
1262
1418
|
|
|
1263
1419
|
async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
1264
1420
|
const rd = getRemoteDispatchConfig(config);
|
|
1265
1421
|
if (!rd || String(chatId) !== rd.chatId) return false;
|
|
1422
|
+
log('INFO', `Remote dispatch intercept chat=${chatId} preview=${String(text || '').slice(0, 48).replace(/\s+/g, ' ')}`);
|
|
1266
1423
|
|
|
1267
1424
|
const packet = decodeRemoteDispatchPacket(text);
|
|
1268
|
-
if (!packet)
|
|
1425
|
+
if (!packet) {
|
|
1426
|
+
log('INFO', 'Remote dispatch decode miss');
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1269
1429
|
if (!verifyRemoteDispatchPacket(packet, rd.secret)) {
|
|
1270
1430
|
log('WARN', 'Remote dispatch ignored: invalid signature');
|
|
1271
1431
|
return true;
|
|
1272
1432
|
}
|
|
1273
|
-
if (packet.from_peer === rd.selfPeer)
|
|
1274
|
-
|
|
1433
|
+
if (packet.from_peer === rd.selfPeer) {
|
|
1434
|
+
log('INFO', `Remote dispatch ignored: self echo id=${packet.id || ''}`);
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
if (packet.to_peer !== rd.selfPeer) {
|
|
1438
|
+
log('INFO', `Remote dispatch ignored: peer mismatch id=${packet.id || ''} to=${packet.to_peer || ''} self=${rd.selfPeer}`);
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1275
1441
|
if (isRemoteDispatchDuplicate(packet.id)) {
|
|
1276
1442
|
log('DEBUG', `Remote dispatch ignored: duplicate id=${packet.id}`);
|
|
1277
1443
|
return true;
|
|
1278
1444
|
}
|
|
1445
|
+
log('INFO', `Remote dispatch received type=${packet.type} id=${packet.id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1279
1446
|
|
|
1280
1447
|
if (packet.type === 'task') {
|
|
1281
1448
|
const replyFn = async (output) => {
|
|
@@ -1286,24 +1453,89 @@ async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
|
1286
1453
|
target_project: packet.target_project,
|
|
1287
1454
|
source_chat_id: packet.source_chat_id,
|
|
1288
1455
|
source_sender_key: packet.source_sender_key || 'user',
|
|
1456
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1289
1457
|
request_id: packet.id,
|
|
1290
1458
|
result: String(output || '').slice(0, 4000),
|
|
1291
1459
|
}, config);
|
|
1292
1460
|
if (!res.success) log('WARN', `Remote dispatch result send failed: ${res.error}`);
|
|
1293
1461
|
};
|
|
1294
1462
|
|
|
1295
|
-
handleDispatchItem({
|
|
1463
|
+
const dispatchRes = handleDispatchItem({
|
|
1296
1464
|
target: packet.target_project,
|
|
1297
1465
|
prompt: packet.prompt,
|
|
1298
1466
|
from: packet.source_sender_key || `${packet.from_peer}:remote`,
|
|
1299
1467
|
new_session: !!packet.new_session,
|
|
1468
|
+
source_chat_id: packet.source_chat_id || '',
|
|
1469
|
+
source_sender_key: packet.source_sender_key || '',
|
|
1470
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1300
1471
|
_replyFn: replyFn,
|
|
1301
1472
|
_suppressDefaultReplyRouting: true,
|
|
1302
1473
|
}, config);
|
|
1474
|
+
const ackRes = await sendRemoteDispatch({
|
|
1475
|
+
type: 'ack',
|
|
1476
|
+
to_peer: packet.from_peer,
|
|
1477
|
+
target_project: packet.target_project,
|
|
1478
|
+
source_chat_id: packet.source_chat_id,
|
|
1479
|
+
source_sender_key: packet.source_sender_key || 'user',
|
|
1480
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1481
|
+
request_id: packet.id,
|
|
1482
|
+
dispatch_id: dispatchRes && dispatchRes.id ? dispatchRes.id : '',
|
|
1483
|
+
task_id: dispatchRes && dispatchRes.task_id ? dispatchRes.task_id : '',
|
|
1484
|
+
scope_id: dispatchRes && dispatchRes.scope_id ? dispatchRes.scope_id : '',
|
|
1485
|
+
status: dispatchRes && dispatchRes.success ? 'accepted' : 'failed',
|
|
1486
|
+
error: dispatchRes && dispatchRes.success ? '' : String(dispatchRes && dispatchRes.error || 'dispatch_failed'),
|
|
1487
|
+
}, config);
|
|
1488
|
+
if (!ackRes.success) log('WARN', `Remote dispatch ack send failed: ${ackRes.error}`);
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (packet.type === 'ack') {
|
|
1493
|
+
resolveTrackedRemoteDispatch(packet.request_id);
|
|
1494
|
+
log('INFO', `Remote dispatch ack id=${packet.request_id || ''} status=${packet.status} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1495
|
+
const text = String(packet.status) === 'accepted'
|
|
1496
|
+
? [
|
|
1497
|
+
'📮 远端 Dispatch 回执',
|
|
1498
|
+
'',
|
|
1499
|
+
`状态: ${packet.from_peer}:${packet.target_project || 'unknown'} 已接收并入队`,
|
|
1500
|
+
packet.dispatch_id ? `编号: ${packet.dispatch_id}` : '',
|
|
1501
|
+
packet.task_id ? buildTeamTaskResumeHint(packet.task_id, packet.scope_id) : '',
|
|
1502
|
+
].filter(Boolean).join('\n')
|
|
1503
|
+
: [
|
|
1504
|
+
'❌ 远端 Dispatch 回执',
|
|
1505
|
+
'',
|
|
1506
|
+
`状态: ${packet.from_peer}:${packet.target_project || 'unknown'} 入队失败`,
|
|
1507
|
+
packet.error ? `错误: ${String(packet.error).slice(0, 200)}` : '',
|
|
1508
|
+
].filter(Boolean).join('\n');
|
|
1509
|
+
|
|
1510
|
+
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
1511
|
+
if (targetChatId) {
|
|
1512
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1513
|
+
if (!liveBot) {
|
|
1514
|
+
writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, { status: packet.status, targetKey: packet.target_project, dispatchId: packet.dispatch_id, text });
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
if (liveBot.sendMarkdown) await liveBot.sendMarkdown(targetChatId, text);
|
|
1519
|
+
else await liveBot.sendMessage(targetChatId, text);
|
|
1520
|
+
} catch (e) {
|
|
1521
|
+
log('WARN', `Remote dispatch ack delivery failed: ${e.message}`);
|
|
1522
|
+
writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, { status: packet.status, targetKey: packet.target_project, dispatchId: packet.dispatch_id, text });
|
|
1523
|
+
}
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
writeDispatchReceiptInbox({ from: packet.source_sender_key || 'user' }, {
|
|
1528
|
+
status: packet.status,
|
|
1529
|
+
targetKey: packet.target_project,
|
|
1530
|
+
dispatchId: packet.dispatch_id,
|
|
1531
|
+
text,
|
|
1532
|
+
});
|
|
1303
1533
|
return true;
|
|
1304
1534
|
}
|
|
1305
1535
|
|
|
1306
1536
|
if (packet.type === 'result') {
|
|
1537
|
+
resolveTrackedRemoteDispatch(packet.request_id);
|
|
1538
|
+
log('INFO', `Remote dispatch result id=${packet.request_id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1307
1539
|
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
1308
1540
|
if (!targetChatId) {
|
|
1309
1541
|
const inboxTarget = String(packet.source_sender_key || '').trim();
|
|
@@ -1361,8 +1593,8 @@ function startDispatchSocket(getConfig) {
|
|
|
1361
1593
|
try {
|
|
1362
1594
|
const item = JSON.parse(buf);
|
|
1363
1595
|
const liveCfg = typeof getConfig === 'function' ? getConfig() : getConfig;
|
|
1364
|
-
handleDispatchItem(item, liveCfg || {});
|
|
1365
|
-
conn.write(JSON.stringify({ ok:
|
|
1596
|
+
const result = handleDispatchItem(item, liveCfg || {});
|
|
1597
|
+
conn.write(JSON.stringify({ ok: !!(result && result.success), id: result && result.id ? result.id : null, error: result && result.error ? result.error : null }) + '\n');
|
|
1366
1598
|
} catch (e) {
|
|
1367
1599
|
try { conn.write(JSON.stringify({ ok: false, error: e.message }) + '\n'); } catch { /* ignore */ }
|
|
1368
1600
|
}
|
|
@@ -1420,9 +1652,19 @@ function physiologicalHeartbeat(config) {
|
|
|
1420
1652
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1421
1653
|
for (const item of items) {
|
|
1422
1654
|
if (item.relay_chat_id && item.body && liveBot && typeof liveBot.sendMessage === 'function') {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1655
|
+
const packet = decodeRemoteDispatchPacket(item.body);
|
|
1656
|
+
liveBot.sendMessage(item.relay_chat_id, item.body)
|
|
1657
|
+
.then(() => {
|
|
1658
|
+
if (packet) {
|
|
1659
|
+
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}`);
|
|
1660
|
+
if (packet.type === 'task') trackRemoteDispatch(packet, config);
|
|
1661
|
+
} else {
|
|
1662
|
+
log('INFO', `Remote dispatch queue sent raw via=${item.relay_chat_id}`);
|
|
1663
|
+
}
|
|
1664
|
+
})
|
|
1665
|
+
.catch(e2 =>
|
|
1666
|
+
log('WARN', `Remote dispatch relay send failed: ${e2.message}`)
|
|
1667
|
+
);
|
|
1426
1668
|
}
|
|
1427
1669
|
}
|
|
1428
1670
|
}
|
|
@@ -1453,7 +1695,6 @@ function physiologicalHeartbeat(config) {
|
|
|
1453
1695
|
const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
|
|
1454
1696
|
const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
|
|
1455
1697
|
const FALLBACK_THROTTLE_MS = 8000; // 8s between fallback status updates
|
|
1456
|
-
const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
|
|
1457
1698
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1458
1699
|
|
|
1459
1700
|
// Rate limiter for /ask and /run — prevents rapid-fire Claude calls
|
|
@@ -1629,6 +1870,7 @@ async function doBindAgent(bot, chatId, agentName, agentCwd) {
|
|
|
1629
1870
|
// ---------------------------------------------------------
|
|
1630
1871
|
const {
|
|
1631
1872
|
findSessionFile,
|
|
1873
|
+
findCodexSessionFile,
|
|
1632
1874
|
clearSessionFileCache,
|
|
1633
1875
|
truncateSessionToCheckpoint,
|
|
1634
1876
|
listRecentSessions,
|
|
@@ -1638,15 +1880,17 @@ const {
|
|
|
1638
1880
|
sessionRichLabel,
|
|
1639
1881
|
getSessionRecentContext,
|
|
1640
1882
|
buildSessionCardElements,
|
|
1641
|
-
listProjectDirs,
|
|
1642
1883
|
getSession,
|
|
1643
1884
|
getSessionForEngine,
|
|
1644
1885
|
createSession,
|
|
1886
|
+
restoreSessionFromReply,
|
|
1645
1887
|
getSessionName,
|
|
1646
1888
|
writeSessionName,
|
|
1647
1889
|
markSessionStarted,
|
|
1648
1890
|
watchSessionFiles,
|
|
1649
1891
|
isEngineSessionValid,
|
|
1892
|
+
getCodexSessionSandboxProfile,
|
|
1893
|
+
getCodexSessionPermissionMode,
|
|
1650
1894
|
} = createSessionStore({
|
|
1651
1895
|
fs,
|
|
1652
1896
|
path,
|
|
@@ -1799,6 +2043,8 @@ const getEngineRuntime = createEngineRuntimeFactory({
|
|
|
1799
2043
|
getActiveProviderEnv,
|
|
1800
2044
|
});
|
|
1801
2045
|
|
|
2046
|
+
let wakeRecoveryHook = null;
|
|
2047
|
+
|
|
1802
2048
|
const {
|
|
1803
2049
|
checkPrecondition,
|
|
1804
2050
|
executeTask,
|
|
@@ -1827,6 +2073,7 @@ const {
|
|
|
1827
2073
|
isInSleepMode: () => _inSleepMode,
|
|
1828
2074
|
setSleepMode: (next) => { _inSleepMode = !!next; },
|
|
1829
2075
|
spawnSessionSummaries,
|
|
2076
|
+
getWakeRecoveryHook: () => wakeRecoveryHook,
|
|
1830
2077
|
skillEvolution,
|
|
1831
2078
|
});
|
|
1832
2079
|
|
|
@@ -1933,7 +2180,10 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
|
|
|
1933
2180
|
sendFileButtons,
|
|
1934
2181
|
findSessionFile,
|
|
1935
2182
|
listRecentSessions,
|
|
2183
|
+
getSessionRecentContext,
|
|
1936
2184
|
isEngineSessionValid,
|
|
2185
|
+
getCodexSessionSandboxProfile,
|
|
2186
|
+
getCodexSessionPermissionMode,
|
|
1937
2187
|
getSession,
|
|
1938
2188
|
getSessionForEngine,
|
|
1939
2189
|
createSession,
|
|
@@ -1993,6 +2243,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1993
2243
|
sendBrowse,
|
|
1994
2244
|
sendDirPicker,
|
|
1995
2245
|
getSession,
|
|
2246
|
+
getSessionForEngine,
|
|
1996
2247
|
listRecentSessions,
|
|
1997
2248
|
buildSessionCardElements,
|
|
1998
2249
|
sessionLabel,
|
|
@@ -2038,8 +2289,10 @@ const { handleExecCommand } = createExecCommandHandler({
|
|
|
2038
2289
|
getSessionName,
|
|
2039
2290
|
createSession,
|
|
2040
2291
|
findSessionFile,
|
|
2292
|
+
findCodexSessionFile,
|
|
2041
2293
|
loadConfig,
|
|
2042
2294
|
getDistillModel,
|
|
2295
|
+
getDefaultEngine,
|
|
2043
2296
|
});
|
|
2044
2297
|
|
|
2045
2298
|
const { handleOpsCommand } = createOpsCommandHandler({
|
|
@@ -2048,9 +2301,12 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2048
2301
|
spawn,
|
|
2049
2302
|
execSync,
|
|
2050
2303
|
log,
|
|
2304
|
+
loadConfig,
|
|
2305
|
+
loadState,
|
|
2051
2306
|
messageQueue,
|
|
2052
2307
|
activeProcesses,
|
|
2053
2308
|
getSession,
|
|
2309
|
+
getSessionForEngine,
|
|
2054
2310
|
listCheckpoints,
|
|
2055
2311
|
cpDisplayLabel,
|
|
2056
2312
|
truncateSessionToCheckpoint,
|
|
@@ -2061,6 +2317,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2061
2317
|
cleanupCheckpoints,
|
|
2062
2318
|
getNoSleepProcess: () => caffeinateProcess,
|
|
2063
2319
|
setNoSleepProcess: (p) => { caffeinateProcess = p || null; },
|
|
2320
|
+
getDefaultEngine,
|
|
2064
2321
|
});
|
|
2065
2322
|
|
|
2066
2323
|
const { handleCommand } = createCommandRouter({
|
|
@@ -2097,7 +2354,7 @@ setDispatchHandler(handleCommand);
|
|
|
2097
2354
|
// ---------------------------------------------------------
|
|
2098
2355
|
// BOT BRIDGES
|
|
2099
2356
|
// ---------------------------------------------------------
|
|
2100
|
-
const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
2357
|
+
const { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge } = createBridgeStarter({
|
|
2101
2358
|
fs,
|
|
2102
2359
|
path,
|
|
2103
2360
|
HOME,
|
|
@@ -2107,6 +2364,7 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
|
2107
2364
|
loadState,
|
|
2108
2365
|
saveState,
|
|
2109
2366
|
getSession,
|
|
2367
|
+
restoreSessionFromReply,
|
|
2110
2368
|
handleCommand,
|
|
2111
2369
|
pendingActivations,
|
|
2112
2370
|
activeProcesses,
|
|
@@ -2231,7 +2489,7 @@ async function main() {
|
|
|
2231
2489
|
}
|
|
2232
2490
|
|
|
2233
2491
|
// Config validation: warn on unknown/suspect fields
|
|
2234
|
-
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
|
|
2492
|
+
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects', 'imessage', 'siri_bridge'];
|
|
2235
2493
|
const KNOWN_DAEMON = [
|
|
2236
2494
|
'model', // legacy (still valid as fallback)
|
|
2237
2495
|
'models', // per-engine model map: { claude, codex }
|
|
@@ -2322,6 +2580,7 @@ async function main() {
|
|
|
2322
2580
|
// Bridges
|
|
2323
2581
|
let telegramBridge = null;
|
|
2324
2582
|
let feishuBridge = null;
|
|
2583
|
+
let lastWakeBridgeRecoveryAt = 0;
|
|
2325
2584
|
|
|
2326
2585
|
const notifier = createNotifier({
|
|
2327
2586
|
log,
|
|
@@ -2335,6 +2594,25 @@ async function main() {
|
|
|
2335
2594
|
// Start dispatch socket server (low-latency IPC, fallback: file polling still works)
|
|
2336
2595
|
const dispatchSocket = startDispatchSocket(() => config);
|
|
2337
2596
|
|
|
2597
|
+
wakeRecoveryHook = async ({ sleepSeconds }) => {
|
|
2598
|
+
const now = Date.now();
|
|
2599
|
+
if (now - lastWakeBridgeRecoveryAt < 60 * 1000) {
|
|
2600
|
+
log('INFO', `[WAKE-DETECT] bridge recovery skipped — cooldown active (${Math.round((now - lastWakeBridgeRecoveryAt) / 1000)}s since last)`);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
lastWakeBridgeRecoveryAt = now;
|
|
2604
|
+
const tasks = [];
|
|
2605
|
+
if (telegramBridge && typeof telegramBridge.reconnect === 'function') {
|
|
2606
|
+
log('INFO', `[WAKE-DETECT] reconnecting Telegram bridge after ${sleepSeconds}s sleep`);
|
|
2607
|
+
tasks.push(Promise.resolve().then(() => telegramBridge.reconnect()));
|
|
2608
|
+
}
|
|
2609
|
+
if (feishuBridge && typeof feishuBridge.reconnect === 'function') {
|
|
2610
|
+
log('INFO', `[WAKE-DETECT] reconnecting Feishu bridge after ${sleepSeconds}s sleep`);
|
|
2611
|
+
tasks.push(Promise.resolve().then(() => feishuBridge.reconnect()));
|
|
2612
|
+
}
|
|
2613
|
+
await Promise.allSettled(tasks);
|
|
2614
|
+
};
|
|
2615
|
+
|
|
2338
2616
|
// Start heartbeat scheduler
|
|
2339
2617
|
let heartbeatTimer = startHeartbeat(config, notifyFn, notifyPersonalFn);
|
|
2340
2618
|
|
|
@@ -2399,6 +2677,8 @@ async function main() {
|
|
|
2399
2677
|
// Start bridges (both can run simultaneously)
|
|
2400
2678
|
telegramBridge = await startTelegramBridge(config, executeTaskByName);
|
|
2401
2679
|
feishuBridge = await startFeishuBridge(config, executeTaskByName);
|
|
2680
|
+
await startImessageBridge(config, executeTaskByName);
|
|
2681
|
+
await startSiriBridge(config, executeTaskByName);
|
|
2402
2682
|
if (feishuBridge) _dispatchBridgeRef = feishuBridge; // store bridge, not bot, so .bot stays live after reconnects
|
|
2403
2683
|
|
|
2404
2684
|
// Notify once on startup (single message, no duplicates)
|
|
@@ -2445,6 +2725,8 @@ async function main() {
|
|
|
2445
2725
|
try { require('./qmd-client').stopDaemon(); } catch { /* ignore */ }
|
|
2446
2726
|
// Kill all tracked engine process groups before exiting (covers sub-agents too)
|
|
2447
2727
|
for (const [cid, proc] of activeProcesses) {
|
|
2728
|
+
proc.aborted = true;
|
|
2729
|
+
proc.abortReason = opts.restartReason ? 'daemon-restart' : 'shutdown';
|
|
2448
2730
|
try { process.kill(-proc.child.pid, 'SIGKILL'); } catch { try { proc.child.kill('SIGKILL'); } catch { } }
|
|
2449
2731
|
log('INFO', `Shutdown: killed engine process group for chatId ${cid}`);
|
|
2450
2732
|
}
|
|
@@ -2458,6 +2740,10 @@ async function main() {
|
|
|
2458
2740
|
process.exit(0);
|
|
2459
2741
|
};
|
|
2460
2742
|
|
|
2743
|
+
process.on('SIGUSR2', () => {
|
|
2744
|
+
shutdown({ restartReason: process.env.METAME_DEPLOY_RESTART_REASON || 'external-restart' })
|
|
2745
|
+
.catch(() => process.exit(1));
|
|
2746
|
+
});
|
|
2461
2747
|
process.on('SIGTERM', () => { shutdown().catch(() => process.exit(0)); });
|
|
2462
2748
|
process.on('SIGINT', () => { shutdown().catch(() => process.exit(0)); });
|
|
2463
2749
|
|
|
@@ -2518,4 +2804,21 @@ if (process.argv.includes('--run')) {
|
|
|
2518
2804
|
}
|
|
2519
2805
|
|
|
2520
2806
|
// Export for testing & cross-bot dispatch
|
|
2521
|
-
module.exports = {
|
|
2807
|
+
module.exports = {
|
|
2808
|
+
executeTask,
|
|
2809
|
+
loadConfig,
|
|
2810
|
+
loadState,
|
|
2811
|
+
buildProfilePreamble,
|
|
2812
|
+
parseInterval,
|
|
2813
|
+
handleRemoteDispatchMessage,
|
|
2814
|
+
sendRemoteDispatch,
|
|
2815
|
+
__test: {
|
|
2816
|
+
buildDispatchPrompt,
|
|
2817
|
+
createStreamForwardBot,
|
|
2818
|
+
buildDispatchTaskCard,
|
|
2819
|
+
stripLeadingPlanSection,
|
|
2820
|
+
resolveDispatchTarget,
|
|
2821
|
+
resolveDispatchReadOnly,
|
|
2822
|
+
isMacLocalOrchestratorIntent,
|
|
2823
|
+
},
|
|
2824
|
+
};
|