metame-cli 1.5.3 → 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 +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- 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 +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- 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 +315 -0
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,11 +69,14 @@ 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,
|
|
75
77
|
decodePacket: decodeRemoteDispatchPacket,
|
|
76
78
|
verifyPacket: verifyRemoteDispatchPacket,
|
|
79
|
+
isDuplicate: isRemoteDispatchDuplicate,
|
|
77
80
|
} = require('./daemon-remote-dispatch');
|
|
78
81
|
|
|
79
82
|
// ---------------------------------------------------------
|
|
@@ -83,18 +86,15 @@ function isMacLocalOrchestratorIntent(prompt) {
|
|
|
83
86
|
const text = String(prompt || '').trim();
|
|
84
87
|
if (!text) return false;
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
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;
|
|
93
95
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
const hasTarget = /(?:微信|WeChat|飞书|Feishu|Finder|Safari|Terminal|iTerm|系统设置|System Settings|电脑|System Events|mac)/i.test(text);
|
|
97
|
-
return hasAction && hasTarget;
|
|
96
|
+
// Natural-language control only triggers when both the action and the macOS target are explicit.
|
|
97
|
+
return hasAutomationVerb && hasMacTarget;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const SKILL_ROUTES = [
|
|
@@ -147,11 +147,13 @@ const { createSessionCommandHandler } = require('./daemon-session-commands');
|
|
|
147
147
|
const { createSessionStore } = require('./daemon-session-store');
|
|
148
148
|
const { createCheckpointUtils } = require('./daemon-checkpoints');
|
|
149
149
|
const { createBridgeStarter } = require('./daemon-bridges');
|
|
150
|
+
const { buildTeamRosterHint, buildEnrichedPrompt, resolveDispatchActor, updateDispatchContextFiles } = require('./team-dispatch');
|
|
150
151
|
const { createFileBrowser } = require('./daemon-file-browser');
|
|
151
152
|
const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
|
|
153
|
+
const { repairAgentLayer } = require('./agent-layer');
|
|
152
154
|
const { createNotifier } = require('./daemon-notify');
|
|
153
155
|
const { createClaudeEngine } = require('./daemon-claude-engine');
|
|
154
|
-
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG
|
|
156
|
+
const { createEngineRuntimeFactory, detectDefaultEngine, resolveEngineModel, ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
|
|
155
157
|
const { createCommandRouter } = require('./daemon-command-router');
|
|
156
158
|
const { createTaskScheduler } = require('./daemon-task-scheduler');
|
|
157
159
|
const { createAgentTools } = require('./daemon-agent-tools');
|
|
@@ -409,6 +411,24 @@ function saveState(state) {
|
|
|
409
411
|
if (currentUsageUpdated && currentUsageUpdated > nextUsageUpdated) {
|
|
410
412
|
next.usage.updated_at = currentUsageUpdated;
|
|
411
413
|
}
|
|
414
|
+
|
|
415
|
+
// Merge sessions: prevent concurrent agents from wiping each other's session data.
|
|
416
|
+
// When a stale state object is saved (e.g. after a long spawnClaudeStreaming await),
|
|
417
|
+
// preserve any sessions that were added/updated by other agents in the interim.
|
|
418
|
+
if (current.sessions && typeof current.sessions === 'object') {
|
|
419
|
+
if (!next.sessions || typeof next.sessions !== 'object') next.sessions = {};
|
|
420
|
+
for (const [key, curSession] of Object.entries(current.sessions)) {
|
|
421
|
+
if (!next.sessions[key]) {
|
|
422
|
+
// Session exists in cache but not in incoming state → preserve it
|
|
423
|
+
next.sessions[key] = curSession;
|
|
424
|
+
} else {
|
|
425
|
+
// Both have it → keep whichever has newer last_active
|
|
426
|
+
const curActive = Number(curSession && curSession.last_active) || 0;
|
|
427
|
+
const nextActive = Number(next.sessions[key] && next.sessions[key].last_active) || 0;
|
|
428
|
+
if (curActive > nextActive) next.sessions[key] = curSession;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
412
432
|
}
|
|
413
433
|
|
|
414
434
|
_cachedState = next;
|
|
@@ -499,15 +519,6 @@ function recordTokens(state, tokens, meta = null) {
|
|
|
499
519
|
}
|
|
500
520
|
|
|
501
521
|
|
|
502
|
-
function getBudgetWarning(config, state) {
|
|
503
|
-
const limit = (config.budget && config.budget.daily_limit) || 50000;
|
|
504
|
-
const threshold = (config.budget && config.budget.warning_threshold) || 0.8;
|
|
505
|
-
const ratio = state.budget.tokens_used / limit;
|
|
506
|
-
if (ratio >= 1) return 'exceeded';
|
|
507
|
-
if (ratio >= threshold) return 'warning';
|
|
508
|
-
return 'ok';
|
|
509
|
-
}
|
|
510
|
-
|
|
511
522
|
const taskBoard = createTaskBoard({
|
|
512
523
|
logger: (msg) => log('WARN', msg),
|
|
513
524
|
});
|
|
@@ -519,12 +530,58 @@ const taskBoard = createTaskBoard({
|
|
|
519
530
|
// Late-bound reference to handleCommand (defined later in file)
|
|
520
531
|
let _handleCommand = null;
|
|
521
532
|
let _dispatchBridgeRef = null; // Store bridge (not bot) so .bot is always the live object after reconnects
|
|
533
|
+
const _pendingRemoteDispatches = new Map();
|
|
522
534
|
function setDispatchHandler(fn) { _handleCommand = fn; }
|
|
523
535
|
|
|
524
536
|
function getRemoteDispatchConfig(config) {
|
|
525
537
|
return normalizeRemoteDispatchConfig(config || {});
|
|
526
538
|
}
|
|
527
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
|
+
|
|
528
585
|
async function sendRemoteDispatch(packet, config) {
|
|
529
586
|
const rd = getRemoteDispatchConfig(config);
|
|
530
587
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
@@ -538,8 +595,13 @@ async function sendRemoteDispatch(packet, config) {
|
|
|
538
595
|
id,
|
|
539
596
|
ts,
|
|
540
597
|
...packet,
|
|
598
|
+
from_peer: rd.selfPeer,
|
|
541
599
|
}, rd.secret);
|
|
542
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
|
+
}
|
|
543
605
|
return { success: true, id };
|
|
544
606
|
} catch (e) {
|
|
545
607
|
return { success: false, error: e.message };
|
|
@@ -565,38 +627,91 @@ function createNullBot(onOutput) {
|
|
|
565
627
|
};
|
|
566
628
|
}
|
|
567
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
|
+
|
|
568
645
|
/**
|
|
569
646
|
* Forward bot: routes all calls to a real bot with a fixed chatId.
|
|
570
647
|
* Used for dispatch tasks so Claude's streaming output appears in the target's Feishu channel.
|
|
571
648
|
*/
|
|
572
|
-
function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
649
|
+
function createStreamForwardBot(realBot, chatId, onOutput = null, opts = {}) {
|
|
573
650
|
// Track edit-broken state independently so dispatch failures don't poison realBot's flag
|
|
574
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
|
+
}
|
|
575
676
|
return {
|
|
576
677
|
sendMessage: async (_, text) => {
|
|
678
|
+
await waitUntilReady();
|
|
577
679
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] msg: ${String(text).slice(0, 80)}`);
|
|
578
|
-
|
|
579
|
-
return realBot.sendMessage(chatId, text);
|
|
680
|
+
return deliver(text, text);
|
|
580
681
|
},
|
|
581
682
|
sendMarkdown: async (_, text) => {
|
|
683
|
+
await waitUntilReady();
|
|
582
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);
|
|
583
695
|
if (onOutput) onOutput(text);
|
|
584
|
-
return realBot.sendMarkdown(chatId,
|
|
696
|
+
return realBot.sendMarkdown(chatId, displayText);
|
|
585
697
|
},
|
|
586
698
|
sendCard: async (_, card) => {
|
|
699
|
+
await waitUntilReady();
|
|
587
700
|
const title = typeof card === 'object' ? (card.title || card.body || '').slice(0, 60) : String(card).slice(0, 60);
|
|
588
701
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] card: ${title}`);
|
|
589
702
|
if (onOutput) onOutput(typeof card === 'object' ? (card.body || card.title || JSON.stringify(card)) : card);
|
|
590
703
|
return realBot.sendCard(chatId, card);
|
|
591
704
|
},
|
|
592
705
|
sendRawCard: async (_, header, elements) => {
|
|
706
|
+
await waitUntilReady();
|
|
593
707
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] rawcard: ${String(header).slice(0, 60)}`);
|
|
594
708
|
if (onOutput) onOutput(header);
|
|
595
709
|
return realBot.sendRawCard(chatId, header, elements);
|
|
596
710
|
},
|
|
597
|
-
sendButtons: async (_, text, buttons) => realBot.sendButtons(chatId, text, buttons),
|
|
598
|
-
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); },
|
|
599
713
|
editMessage: async (_, msgId, text) => {
|
|
714
|
+
await waitUntilReady();
|
|
600
715
|
if (_editBroken) return false;
|
|
601
716
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] edit ${String(msgId).slice(-8)}: ${String(text).slice(0, 60)}`);
|
|
602
717
|
try {
|
|
@@ -609,8 +724,8 @@ function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
|
609
724
|
return false;
|
|
610
725
|
}
|
|
611
726
|
},
|
|
612
|
-
deleteMessage: async (_, msgId) => realBot.deleteMessage(chatId, msgId),
|
|
613
|
-
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); },
|
|
614
729
|
downloadFile: async (...args) => realBot.downloadFile(...args),
|
|
615
730
|
};
|
|
616
731
|
}
|
|
@@ -780,6 +895,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
780
895
|
const fullMsg = {
|
|
781
896
|
id: `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
782
897
|
from: message.from || 'unknown',
|
|
898
|
+
source_sender_id: String(message.source_sender_id || '').trim() || '',
|
|
783
899
|
to: targetProject,
|
|
784
900
|
type: message.type || 'task',
|
|
785
901
|
priority: message.priority || 'normal',
|
|
@@ -792,6 +908,17 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
792
908
|
created_at: new Date().toISOString(),
|
|
793
909
|
};
|
|
794
910
|
|
|
911
|
+
// Inject team roster hint if target is a team member and hint not already present
|
|
912
|
+
if (!message.team_roster_injected && config && config.projects && fullMsg.payload.prompt) {
|
|
913
|
+
for (const [parentKey, parent] of Object.entries(config.projects)) {
|
|
914
|
+
if (Array.isArray(parent.team) && parent.team.some(m => m.key === targetProject)) {
|
|
915
|
+
const hint = buildTeamRosterHint(parentKey, targetProject, config.projects);
|
|
916
|
+
if (hint) fullMsg.payload.prompt = `${hint}\n\n---\n${fullMsg.payload.prompt}`;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
795
922
|
if (envelope && taskBoard) {
|
|
796
923
|
const nowIso = new Date().toISOString();
|
|
797
924
|
taskBoard.upsertTask({
|
|
@@ -826,79 +953,23 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
826
953
|
if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
827
954
|
fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
|
|
828
955
|
|
|
829
|
-
// Auto-update
|
|
956
|
+
// Auto-update scoped dispatch context files; only TeamTask writes shared state.
|
|
830
957
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const fromProj = config && config.projects ? config.projects[fullMsg.from] : null;
|
|
842
|
-
const fromName = fromProj ? (fromProj.name || fullMsg.from) : (fullMsg.from || 'unknown');
|
|
843
|
-
const fromIcon = fromProj ? (fromProj.icon || '🤖') : '🤖';
|
|
844
|
-
|
|
845
|
-
// Get target display name
|
|
846
|
-
const toProj = config && config.projects ? config.projects[targetProject] : null;
|
|
847
|
-
const toName = toProj ? (toProj.name || targetProject) : targetProject;
|
|
848
|
-
const toIcon = toProj ? (toProj.icon || '🤖') : '🤖';
|
|
849
|
-
|
|
850
|
-
const taskTitle = payload.title || '';
|
|
851
|
-
const taskPrompt = payload.prompt || '';
|
|
852
|
-
|
|
853
|
-
// Update shared.md
|
|
854
|
-
const content = `# 共享当前状态
|
|
855
|
-
**最后更新**: ${timeStr} **更新者**: ${fromName} (${fullMsg.from})
|
|
856
|
-
|
|
857
|
-
## 当前任务
|
|
858
|
-
- **派发给**: ${toIcon} ${toName} (${targetProject})
|
|
859
|
-
- **任务**: ${taskTitle || taskPrompt.slice(0, 60)}
|
|
860
|
-
- **时间**: ${timeStr}
|
|
861
|
-
|
|
862
|
-
## 任务链
|
|
863
|
-
${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProject}
|
|
864
|
-
`;
|
|
865
|
-
fs.writeFileSync(SHARED_FILE, content, 'utf8');
|
|
866
|
-
|
|
867
|
-
// Update tasks.md if shared directory exists
|
|
868
|
-
const tasksFile = path.join(SHARED_DIR, 'tasks.md');
|
|
869
|
-
if (fs.existsSync(SHARED_DIR)) {
|
|
870
|
-
const taskLine = `- [${dateStr}] ${fromIcon} ${fromName} → ${toIcon} ${toName}: ${taskTitle || taskPrompt.slice(0, 40)}`;
|
|
871
|
-
let tasksContent = '';
|
|
872
|
-
if (fs.existsSync(tasksFile)) {
|
|
873
|
-
tasksContent = fs.readFileSync(tasksFile, 'utf8');
|
|
874
|
-
} else {
|
|
875
|
-
tasksContent = '# 任务看板\n\n## 🔄 进行中\n\n## ✅ 已完成\n\n## 📅 待开始\n';
|
|
876
|
-
}
|
|
877
|
-
// Insert task under "进行中" section
|
|
878
|
-
if (!tasksContent.includes(taskLine)) {
|
|
879
|
-
const lines = tasksContent.split('\n');
|
|
880
|
-
const newLines = [];
|
|
881
|
-
let inProgress = false;
|
|
882
|
-
for (const line of lines) {
|
|
883
|
-
newLines.push(line);
|
|
884
|
-
if (line.includes('## 🔄 进行中')) {
|
|
885
|
-
inProgress = true;
|
|
886
|
-
} else if (inProgress && line.startsWith('## ')) {
|
|
887
|
-
newLines.push(taskLine);
|
|
888
|
-
inProgress = false;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
if (inProgress) newLines.push(taskLine);
|
|
892
|
-
fs.writeFileSync(tasksFile, newLines.join('\n'), 'utf8');
|
|
893
|
-
}
|
|
894
|
-
}
|
|
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
|
+
});
|
|
895
968
|
} catch (e) {
|
|
896
|
-
log('WARN', `Failed to update
|
|
969
|
+
log('WARN', `Failed to update dispatch context files: ${e.message}`);
|
|
897
970
|
}
|
|
898
971
|
|
|
899
|
-
const rawPrompt = envelope
|
|
900
|
-
? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
|
|
901
|
-
: (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
|
|
972
|
+
const rawPrompt = buildDispatchPrompt(targetProject, fullMsg, envelope);
|
|
902
973
|
|
|
903
974
|
// Inject sender identity when dispatched by another agent (not directly from user)
|
|
904
975
|
const userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
@@ -924,10 +995,25 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
924
995
|
const dispatchChatId = buildDispatchChatId(targetProject, envelope && envelope.scope_id);
|
|
925
996
|
const sessionMode = forceNew ? 'fresh session (forced)' : 'existing virtual session';
|
|
926
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();
|
|
927
1012
|
|
|
928
1013
|
let _taskFinalized = false;
|
|
929
1014
|
const outputHandler = (output) => {
|
|
930
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;
|
|
931
1017
|
log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
|
|
932
1018
|
if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
|
|
933
1019
|
const status = inferTaskStatusFromOutput(outStr);
|
|
@@ -946,7 +1032,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
946
1032
|
_taskFinalized = true;
|
|
947
1033
|
}
|
|
948
1034
|
if (replyFn && outStr.trim().length > 2) {
|
|
949
|
-
replyFn(
|
|
1035
|
+
replyFn(displayOut);
|
|
950
1036
|
} else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
|
|
951
1037
|
// Write result to sender's inbox before dispatching callback
|
|
952
1038
|
try {
|
|
@@ -961,7 +1047,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
961
1047
|
`TS: ${new Date().toISOString()}`,
|
|
962
1048
|
`SUBJECT: ${subject}`,
|
|
963
1049
|
'',
|
|
964
|
-
|
|
1050
|
+
displayOut.slice(0, 2000),
|
|
965
1051
|
].join('\n');
|
|
966
1052
|
fs.writeFileSync(inboxFile, body, 'utf8');
|
|
967
1053
|
} catch (e) {
|
|
@@ -969,12 +1055,13 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
969
1055
|
}
|
|
970
1056
|
dispatchTask(fullMsg.from, {
|
|
971
1057
|
from: targetProject,
|
|
1058
|
+
source_sender_id: fullMsg.source_sender_id || '',
|
|
972
1059
|
type: 'callback',
|
|
973
1060
|
priority: 'normal',
|
|
974
1061
|
payload: {
|
|
975
1062
|
title: `任务完成: ${fullMsg.payload.title || fullMsg.id}`,
|
|
976
1063
|
original_id: fullMsg.id,
|
|
977
|
-
output:
|
|
1064
|
+
output: displayOut.slice(0, 500),
|
|
978
1065
|
},
|
|
979
1066
|
chain: [], // reset chain for callbacks
|
|
980
1067
|
}, config);
|
|
@@ -983,11 +1070,14 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
983
1070
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
984
1071
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
985
1072
|
const nullBot = streamOptions?.bot && streamOptions?.chatId
|
|
986
|
-
? 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
|
+
})
|
|
987
1078
|
: createNullBot(outputHandler);
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
// 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.
|
|
991
1081
|
// When forceNew=true, clear any cached session for this virtual chatId so
|
|
992
1082
|
// attachOrCreateSession in handleCommand actually creates a fresh Claude session.
|
|
993
1083
|
if (forceNew) {
|
|
@@ -997,7 +1087,7 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
997
1087
|
saveState(st);
|
|
998
1088
|
}
|
|
999
1089
|
}
|
|
1000
|
-
const dispatchReadOnly =
|
|
1090
|
+
const dispatchReadOnly = resolveDispatchReadOnly(message, config, targetProject);
|
|
1001
1091
|
if (envelope && taskBoard) {
|
|
1002
1092
|
taskBoard.markTaskStatus(envelope.task_id, 'running', { summary: `dispatched via ${sessionMode}` });
|
|
1003
1093
|
taskBoard.appendTaskEvent(envelope.task_id, 'task_started', targetProject, { session_mode: sessionMode });
|
|
@@ -1010,7 +1100,12 @@ ${fullMsg.chain ? fullMsg.chain.join(' → ') : fullMsg.from} → ${targetProjec
|
|
|
1010
1100
|
}
|
|
1011
1101
|
});
|
|
1012
1102
|
|
|
1013
|
-
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
|
+
};
|
|
1014
1109
|
}
|
|
1015
1110
|
|
|
1016
1111
|
/**
|
|
@@ -1111,28 +1206,265 @@ function _findTeamBroadcastContext(fromKey, targetKey, config) {
|
|
|
1111
1206
|
return null;
|
|
1112
1207
|
}
|
|
1113
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
|
+
|
|
1114
1441
|
function handleDispatchItem(item, config) {
|
|
1115
1442
|
if (!item.target || !item.prompt) return;
|
|
1116
|
-
|
|
1443
|
+
const resolvedTarget = resolveDispatchTarget(item.target, config);
|
|
1444
|
+
if (!resolvedTarget) {
|
|
1117
1445
|
log('WARN', `dispatch: unknown target "${item.target}"`);
|
|
1118
|
-
|
|
1446
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, { success: false, error: 'unknown_target' }));
|
|
1447
|
+
return { success: false, error: 'unknown_target' };
|
|
1119
1448
|
}
|
|
1449
|
+
const targetKey = resolvedTarget.key;
|
|
1120
1450
|
// 安全护栏:禁止 agent 主动 dispatch 到 personal(防止 LLM 幻觉乱发消息给小美)
|
|
1121
1451
|
// personal 只允许用户本人触发,或来源为 user/unknown 的系统任务
|
|
1122
1452
|
const _agentSources = new Set(Object.keys((config.projects) || {}));
|
|
1123
1453
|
const isFromAgent = _agentSources.has(item.from) || item.from === '_claude_session';
|
|
1124
|
-
const targetProject = config.projects?.[
|
|
1454
|
+
const targetProject = config.projects?.[targetKey] || {};
|
|
1125
1455
|
if (isFromAgent && targetProject.guard === 'user-only') {
|
|
1126
|
-
log('WARN', `dispatch: blocked agent "${item.from}" → "${
|
|
1127
|
-
|
|
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' };
|
|
1128
1459
|
}
|
|
1129
|
-
log('INFO', `Dispatch: ${item.from || '?'} → ${
|
|
1460
|
+
log('INFO', `Dispatch: ${item.from || '?'} → ${targetKey}: ${item.prompt.slice(0, 60)}`);
|
|
1130
1461
|
|
|
1131
1462
|
// ── Team broadcast: intra-team dispatch → show in group chat ──
|
|
1132
1463
|
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1133
|
-
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from,
|
|
1464
|
+
const teamCtx = liveBot ? _findTeamBroadcastContext(item.from, targetKey, config) : null;
|
|
1465
|
+
const responseCard = buildDispatchResponseCard(targetKey, config);
|
|
1134
1466
|
if (teamCtx && teamCtx.groupChatId) {
|
|
1135
|
-
const { senderMember, targetMember, groupChatId
|
|
1467
|
+
const { senderMember, targetMember, groupChatId } = teamCtx;
|
|
1136
1468
|
const sIcon = senderMember.icon || '🤖';
|
|
1137
1469
|
const sName = senderMember.name || senderMember.key;
|
|
1138
1470
|
const tIcon = targetMember.icon || '🤖';
|
|
@@ -1141,71 +1473,48 @@ function handleDispatchItem(item, config) {
|
|
|
1141
1473
|
const cardTitle = `${sIcon} ${sName} → ${tIcon} ${tName}`;
|
|
1142
1474
|
const cardBody = item.prompt.slice(0, 300) + (item.prompt.length > 300 ? '…' : '');
|
|
1143
1475
|
const cardColor = senderMember.color || 'blue';
|
|
1144
|
-
const
|
|
1476
|
+
const sendTaskNotice = liveBot.sendCard
|
|
1145
1477
|
? () => liveBot.sendCard(groupChatId, { title: cardTitle, body: cardBody, color: cardColor })
|
|
1146
1478
|
: () => liveBot.sendMarkdown(groupChatId, `**${cardTitle}**\n\n> ${cardBody}`);
|
|
1147
|
-
|
|
1148
|
-
//
|
|
1149
|
-
const streamOptions = {
|
|
1150
|
-
|
|
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, {
|
|
1151
1490
|
from: item.from || 'claude_session',
|
|
1491
|
+
source_sender_id: item.source_sender_id || '',
|
|
1152
1492
|
type: 'task', priority: 'normal',
|
|
1153
1493
|
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1154
1494
|
callback: false,
|
|
1155
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 || '',
|
|
1156
1499
|
}, config, null, streamOptions);
|
|
1157
|
-
|
|
1500
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
|
|
1501
|
+
return result;
|
|
1158
1502
|
}
|
|
1159
1503
|
|
|
1160
1504
|
// ── Normal dispatch (non-team or broadcast off) ──
|
|
1161
|
-
let pendingReplyFn = null;
|
|
1505
|
+
let pendingReplyFn = typeof item._replyFn === 'function' ? item._replyFn : null;
|
|
1162
1506
|
let streamOptions = null;
|
|
1163
1507
|
if (liveBot) {
|
|
1164
1508
|
const feishuMap = (config.feishu && config.feishu.chat_agent_map) || {};
|
|
1165
|
-
const
|
|
1166
|
-
const agentChatIds = new Set(Object.keys(feishuMap));
|
|
1167
|
-
const targetChatId = Object.entries(feishuMap).find(([, v]) => v === item.target)?.[0] || null;
|
|
1509
|
+
const targetChatId = Object.entries(feishuMap).find(([, v]) => v === targetKey)?.[0] || null;
|
|
1168
1510
|
if (targetChatId) {
|
|
1169
|
-
streamOptions = { bot: liveBot, chatId: targetChatId };
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
liveBot.sendMessage(targetChatId, ackText.replace(/\*\*/g, '')).catch(e =>
|
|
1173
|
-
log('WARN', `Dispatch ack failed: ${e.message}`)
|
|
1174
|
-
)
|
|
1175
|
-
);
|
|
1176
|
-
} else {
|
|
1177
|
-
const _userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
1178
|
-
let senderChatId = null;
|
|
1179
|
-
if (!_userSources.has(item.from)) {
|
|
1180
|
-
// Direct match: sender is a bound agent
|
|
1181
|
-
senderChatId = Object.entries(feishuMap).find(([, v]) => v === item.from)?.[0] || null;
|
|
1182
|
-
// Team member fallback: if sender is a team member (e.g., jarvis_c), find parent project's chatId
|
|
1183
|
-
if (!senderChatId) {
|
|
1184
|
-
const projects = config.projects || {};
|
|
1185
|
-
for (const [projKey, proj] of Object.entries(projects)) {
|
|
1186
|
-
if (proj.team && Array.isArray(proj.team)) {
|
|
1187
|
-
const member = proj.team.find(m => m.key === item.from);
|
|
1188
|
-
if (member && feishuMap[projKey]) {
|
|
1189
|
-
senderChatId = feishuMap[projKey];
|
|
1190
|
-
break;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
if (!senderChatId) {
|
|
1197
|
-
senderChatId = allowedFeishuIds.map(String).find(id => !agentChatIds.has(id)) || null;
|
|
1198
|
-
}
|
|
1511
|
+
streamOptions = { bot: liveBot, chatId: targetChatId, stripPlan: true, responseCard };
|
|
1512
|
+
} else if (!item._suppressDefaultReplyRouting) {
|
|
1513
|
+
const senderChatId = resolveDispatchSenderChatId(item, config);
|
|
1199
1514
|
if (senderChatId) {
|
|
1200
|
-
const targetProj = (config
|
|
1201
|
-
const ackText = `📬 已接收,转发给 ${targetProj.icon || '🤖'} **${targetProj.name || item.target}**...\n\n> ${item.prompt.slice(0, 100)}${item.prompt.length > 100 ? '...' : ''}`;
|
|
1202
|
-
liveBot.sendMarkdown(senderChatId, ackText).catch(() =>
|
|
1203
|
-
liveBot.sendMessage(senderChatId, ackText.replace(/\*\*/g, '')).catch(e =>
|
|
1204
|
-
log('WARN', `Dispatch ack to sender failed: ${e.message}`)
|
|
1205
|
-
)
|
|
1206
|
-
);
|
|
1515
|
+
const targetProj = resolveDispatchTarget(targetKey, config) || {};
|
|
1207
1516
|
pendingReplyFn = (output) => {
|
|
1208
|
-
const text = `${targetProj.icon || '📬'} **${targetProj.name ||
|
|
1517
|
+
const text = `${targetProj.icon || '📬'} **${targetProj.name || targetKey}** 回复:\n\n${output.slice(0, 2000)}`;
|
|
1209
1518
|
liveBot.sendMarkdown(senderChatId, text).catch(e => {
|
|
1210
1519
|
log('WARN', `Dispatch reply (markdown) failed: ${e.message}`);
|
|
1211
1520
|
liveBot.sendMessage(senderChatId, text.replace(/\*\*/g, '')).catch(e2 =>
|
|
@@ -1214,31 +1523,52 @@ function handleDispatchItem(item, config) {
|
|
|
1214
1523
|
});
|
|
1215
1524
|
};
|
|
1216
1525
|
// Also set streamOptions so target agent's streaming replies go to the sender's group
|
|
1217
|
-
streamOptions = { bot: liveBot, chatId: senderChatId };
|
|
1526
|
+
streamOptions = { bot: liveBot, chatId: senderChatId, stripPlan: true, responseCard };
|
|
1218
1527
|
}
|
|
1219
1528
|
}
|
|
1220
1529
|
}
|
|
1221
|
-
dispatchTask(
|
|
1530
|
+
const result = dispatchTask(targetKey, {
|
|
1222
1531
|
from: item.from || 'claude_session',
|
|
1532
|
+
source_sender_id: item.source_sender_id || '',
|
|
1223
1533
|
type: 'task', priority: 'normal',
|
|
1224
1534
|
payload: { title: item.prompt.slice(0, 60), prompt: item.prompt },
|
|
1225
1535
|
callback: false,
|
|
1226
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 || '',
|
|
1227
1540
|
}, config, pendingReplyFn, streamOptions);
|
|
1541
|
+
sendDispatchReceipt(item, config, buildDispatchReceipt(item, config, result));
|
|
1542
|
+
return result;
|
|
1228
1543
|
}
|
|
1229
1544
|
|
|
1230
1545
|
async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
1231
1546
|
const rd = getRemoteDispatchConfig(config);
|
|
1232
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, ' ')}`);
|
|
1233
1549
|
|
|
1234
1550
|
const packet = decodeRemoteDispatchPacket(text);
|
|
1235
|
-
if (!packet)
|
|
1551
|
+
if (!packet) {
|
|
1552
|
+
log('INFO', 'Remote dispatch decode miss');
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1236
1555
|
if (!verifyRemoteDispatchPacket(packet, rd.secret)) {
|
|
1237
1556
|
log('WARN', 'Remote dispatch ignored: invalid signature');
|
|
1238
1557
|
return true;
|
|
1239
1558
|
}
|
|
1240
|
-
if (packet.from_peer === rd.selfPeer)
|
|
1241
|
-
|
|
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
|
+
}
|
|
1567
|
+
if (isRemoteDispatchDuplicate(packet.id)) {
|
|
1568
|
+
log('DEBUG', `Remote dispatch ignored: duplicate id=${packet.id}`);
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
log('INFO', `Remote dispatch received type=${packet.type} id=${packet.id || ''} from=${packet.from_peer}:${packet.target_project || 'unknown'}`);
|
|
1242
1572
|
|
|
1243
1573
|
if (packet.type === 'task') {
|
|
1244
1574
|
const replyFn = async (output) => {
|
|
@@ -1249,24 +1579,89 @@ async function handleRemoteDispatchMessage({ chatId, text, config }) {
|
|
|
1249
1579
|
target_project: packet.target_project,
|
|
1250
1580
|
source_chat_id: packet.source_chat_id,
|
|
1251
1581
|
source_sender_key: packet.source_sender_key || 'user',
|
|
1582
|
+
source_sender_id: packet.source_sender_id || '',
|
|
1252
1583
|
request_id: packet.id,
|
|
1253
1584
|
result: String(output || '').slice(0, 4000),
|
|
1254
1585
|
}, config);
|
|
1255
1586
|
if (!res.success) log('WARN', `Remote dispatch result send failed: ${res.error}`);
|
|
1256
1587
|
};
|
|
1257
1588
|
|
|
1258
|
-
handleDispatchItem({
|
|
1589
|
+
const dispatchRes = handleDispatchItem({
|
|
1259
1590
|
target: packet.target_project,
|
|
1260
1591
|
prompt: packet.prompt,
|
|
1261
1592
|
from: packet.source_sender_key || `${packet.from_peer}:remote`,
|
|
1262
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 || '',
|
|
1263
1597
|
_replyFn: replyFn,
|
|
1264
1598
|
_suppressDefaultReplyRouting: true,
|
|
1265
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
|
+
});
|
|
1266
1659
|
return true;
|
|
1267
1660
|
}
|
|
1268
1661
|
|
|
1269
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'}`);
|
|
1270
1665
|
const targetChatId = String(packet.source_chat_id || '').trim();
|
|
1271
1666
|
if (!targetChatId) {
|
|
1272
1667
|
const inboxTarget = String(packet.source_sender_key || '').trim();
|
|
@@ -1324,8 +1719,8 @@ function startDispatchSocket(getConfig) {
|
|
|
1324
1719
|
try {
|
|
1325
1720
|
const item = JSON.parse(buf);
|
|
1326
1721
|
const liveCfg = typeof getConfig === 'function' ? getConfig() : getConfig;
|
|
1327
|
-
handleDispatchItem(item, liveCfg || {});
|
|
1328
|
-
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');
|
|
1329
1724
|
} catch (e) {
|
|
1330
1725
|
try { conn.write(JSON.stringify({ ok: false, error: e.message }) + '\n'); } catch { /* ignore */ }
|
|
1331
1726
|
}
|
|
@@ -1369,6 +1764,41 @@ function physiologicalHeartbeat(config) {
|
|
|
1369
1764
|
log('WARN', `Pending dispatch drain failed: ${e.message}`);
|
|
1370
1765
|
}
|
|
1371
1766
|
|
|
1767
|
+
// 2b. Drain remote-pending.jsonl — remote dispatch packets written by dispatch_to CLI
|
|
1768
|
+
const REMOTE_PENDING = path.join(DISPATCH_DIR, 'remote-pending.jsonl');
|
|
1769
|
+
const REMOTE_PENDING_TMP = REMOTE_PENDING + '.processing';
|
|
1770
|
+
try {
|
|
1771
|
+
if (fs.existsSync(REMOTE_PENDING)) {
|
|
1772
|
+
fs.renameSync(REMOTE_PENDING, REMOTE_PENDING_TMP);
|
|
1773
|
+
const content = fs.readFileSync(REMOTE_PENDING_TMP, 'utf8').trim();
|
|
1774
|
+
fs.unlinkSync(REMOTE_PENDING_TMP);
|
|
1775
|
+
if (content) {
|
|
1776
|
+
const items = content.split('\n').filter(Boolean)
|
|
1777
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
1778
|
+
const liveBot = _dispatchBridgeRef && _dispatchBridgeRef.bot;
|
|
1779
|
+
for (const item of items) {
|
|
1780
|
+
if (item.relay_chat_id && item.body && liveBot && typeof liveBot.sendMessage === 'function') {
|
|
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
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
} catch (e) {
|
|
1799
|
+
log('WARN', `Remote pending dispatch drain failed: ${e.message}`);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1372
1802
|
// 2. Rotate dispatch-log if > 512KB (keep 7 days)
|
|
1373
1803
|
try {
|
|
1374
1804
|
if (fs.existsSync(DISPATCH_LOG)) {
|
|
@@ -1391,7 +1821,6 @@ function physiologicalHeartbeat(config) {
|
|
|
1391
1821
|
const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
|
|
1392
1822
|
const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
|
|
1393
1823
|
const FALLBACK_THROTTLE_MS = 8000; // 8s between fallback status updates
|
|
1394
|
-
const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
|
|
1395
1824
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1396
1825
|
|
|
1397
1826
|
// Rate limiter for /ask and /run — prevents rapid-fire Claude calls
|
|
@@ -1445,12 +1874,23 @@ function attachOrCreateSession(chatId, projCwd, name, engine) {
|
|
|
1445
1874
|
* 主路径已迁移到 daemon-agent-tools.editAgentRoleDefinition。
|
|
1446
1875
|
* 保留该实现仅用于兼容回退路径。
|
|
1447
1876
|
*/
|
|
1448
|
-
async function mergeAgentRole(cwd, description) {
|
|
1877
|
+
async function mergeAgentRole(cwd, description, isClone = false, parentCwd = null) {
|
|
1449
1878
|
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
1450
1879
|
// Sanitize user input: strip control chars, cap length to prevent prompt stuffing
|
|
1451
1880
|
const safeDesc = String(description || '').replace(/[\x00-\x1F\x7F]/g, ' ').slice(0, 500);
|
|
1452
1881
|
if (!fs.existsSync(claudeMdPath)) {
|
|
1453
|
-
//
|
|
1882
|
+
// 分身模式:symlink 到父 Agent 的 CLAUDE.md
|
|
1883
|
+
if (isClone) {
|
|
1884
|
+
const sourceCwd = parentCwd || path.dirname(cwd);
|
|
1885
|
+
const parentClaudeMd = path.join(sourceCwd, 'CLAUDE.md');
|
|
1886
|
+
if (fs.existsSync(parentClaudeMd)) {
|
|
1887
|
+
try {
|
|
1888
|
+
fs.symlinkSync(parentClaudeMd, claudeMdPath, 'file');
|
|
1889
|
+
return { created: true, symlinked: true };
|
|
1890
|
+
} catch { /* fall through to normal creation */ }
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
// 普通模式:直接创建
|
|
1454
1894
|
const content = `## Agent 角色\n\n${safeDesc}\n`;
|
|
1455
1895
|
fs.writeFileSync(claudeMdPath, content, 'utf8');
|
|
1456
1896
|
return { created: true };
|
|
@@ -1556,6 +1996,7 @@ async function doBindAgent(bot, chatId, agentName, agentCwd) {
|
|
|
1556
1996
|
// ---------------------------------------------------------
|
|
1557
1997
|
const {
|
|
1558
1998
|
findSessionFile,
|
|
1999
|
+
findCodexSessionFile,
|
|
1559
2000
|
clearSessionFileCache,
|
|
1560
2001
|
truncateSessionToCheckpoint,
|
|
1561
2002
|
listRecentSessions,
|
|
@@ -1565,15 +2006,17 @@ const {
|
|
|
1565
2006
|
sessionRichLabel,
|
|
1566
2007
|
getSessionRecentContext,
|
|
1567
2008
|
buildSessionCardElements,
|
|
1568
|
-
listProjectDirs,
|
|
1569
2009
|
getSession,
|
|
1570
2010
|
getSessionForEngine,
|
|
1571
2011
|
createSession,
|
|
2012
|
+
restoreSessionFromReply,
|
|
1572
2013
|
getSessionName,
|
|
1573
2014
|
writeSessionName,
|
|
1574
2015
|
markSessionStarted,
|
|
1575
2016
|
watchSessionFiles,
|
|
1576
2017
|
isEngineSessionValid,
|
|
2018
|
+
getCodexSessionSandboxProfile,
|
|
2019
|
+
getCodexSessionPermissionMode,
|
|
1577
2020
|
} = createSessionStore({
|
|
1578
2021
|
fs,
|
|
1579
2022
|
path,
|
|
@@ -1726,6 +2169,8 @@ const getEngineRuntime = createEngineRuntimeFactory({
|
|
|
1726
2169
|
getActiveProviderEnv,
|
|
1727
2170
|
});
|
|
1728
2171
|
|
|
2172
|
+
let wakeRecoveryHook = null;
|
|
2173
|
+
|
|
1729
2174
|
const {
|
|
1730
2175
|
checkPrecondition,
|
|
1731
2176
|
executeTask,
|
|
@@ -1754,6 +2199,7 @@ const {
|
|
|
1754
2199
|
isInSleepMode: () => _inSleepMode,
|
|
1755
2200
|
setSleepMode: (next) => { _inSleepMode = !!next; },
|
|
1756
2201
|
spawnSessionSummaries,
|
|
2202
|
+
getWakeRecoveryHook: () => wakeRecoveryHook,
|
|
1757
2203
|
skillEvolution,
|
|
1758
2204
|
});
|
|
1759
2205
|
|
|
@@ -1765,6 +2211,10 @@ const pendingBinds = new Map(); // chatId -> agentName
|
|
|
1765
2211
|
// chatId -> { step: 'dir'|'name'|'desc', dir: string, name: string }
|
|
1766
2212
|
const pendingAgentFlows = new Map();
|
|
1767
2213
|
|
|
2214
|
+
// Pending /agent new team 多步向导状态机
|
|
2215
|
+
// chatId -> { step: 'name'|'members'|'cwd'|'creating', name, members, parentCwd }
|
|
2216
|
+
const pendingTeamFlows = new Map();
|
|
2217
|
+
|
|
1768
2218
|
// Pending activation: after creating an agent with skipChatBinding=true,
|
|
1769
2219
|
// store here so any new unbound group can activate it with /activate
|
|
1770
2220
|
// { agentKey, agentName, cwd, createdAt }
|
|
@@ -1856,7 +2306,10 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
|
|
|
1856
2306
|
sendFileButtons,
|
|
1857
2307
|
findSessionFile,
|
|
1858
2308
|
listRecentSessions,
|
|
2309
|
+
getSessionRecentContext,
|
|
1859
2310
|
isEngineSessionValid,
|
|
2311
|
+
getCodexSessionSandboxProfile,
|
|
2312
|
+
getCodexSessionPermissionMode,
|
|
1860
2313
|
getSession,
|
|
1861
2314
|
getSessionForEngine,
|
|
1862
2315
|
createSession,
|
|
@@ -1916,6 +2369,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1916
2369
|
sendBrowse,
|
|
1917
2370
|
sendDirPicker,
|
|
1918
2371
|
getSession,
|
|
2372
|
+
getSessionForEngine,
|
|
1919
2373
|
listRecentSessions,
|
|
1920
2374
|
buildSessionCardElements,
|
|
1921
2375
|
sessionLabel,
|
|
@@ -1924,6 +2378,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1924
2378
|
getSessionRecentContext,
|
|
1925
2379
|
pendingBinds,
|
|
1926
2380
|
pendingAgentFlows,
|
|
2381
|
+
pendingTeamFlows,
|
|
1927
2382
|
pendingActivations,
|
|
1928
2383
|
doBindAgent,
|
|
1929
2384
|
mergeAgentRole,
|
|
@@ -1932,6 +2387,9 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1932
2387
|
agentFlowTtlMs: getAgentFlowTtlMs,
|
|
1933
2388
|
agentBindTtlMs: getAgentBindTtlMs,
|
|
1934
2389
|
getDefaultEngine,
|
|
2390
|
+
writeConfigSafe,
|
|
2391
|
+
backupConfig,
|
|
2392
|
+
execSync,
|
|
1935
2393
|
});
|
|
1936
2394
|
|
|
1937
2395
|
// Caffeinate process for /nosleep toggle (macOS only)
|
|
@@ -1953,11 +2411,14 @@ const { handleExecCommand } = createExecCommandHandler({
|
|
|
1953
2411
|
loadState,
|
|
1954
2412
|
saveState,
|
|
1955
2413
|
getSession,
|
|
2414
|
+
getSessionForEngine,
|
|
1956
2415
|
getSessionName,
|
|
1957
2416
|
createSession,
|
|
1958
2417
|
findSessionFile,
|
|
2418
|
+
findCodexSessionFile,
|
|
1959
2419
|
loadConfig,
|
|
1960
2420
|
getDistillModel,
|
|
2421
|
+
getDefaultEngine,
|
|
1961
2422
|
});
|
|
1962
2423
|
|
|
1963
2424
|
const { handleOpsCommand } = createOpsCommandHandler({
|
|
@@ -1966,9 +2427,12 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
1966
2427
|
spawn,
|
|
1967
2428
|
execSync,
|
|
1968
2429
|
log,
|
|
2430
|
+
loadConfig,
|
|
2431
|
+
loadState,
|
|
1969
2432
|
messageQueue,
|
|
1970
2433
|
activeProcesses,
|
|
1971
2434
|
getSession,
|
|
2435
|
+
getSessionForEngine,
|
|
1972
2436
|
listCheckpoints,
|
|
1973
2437
|
cpDisplayLabel,
|
|
1974
2438
|
truncateSessionToCheckpoint,
|
|
@@ -1979,6 +2443,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
1979
2443
|
cleanupCheckpoints,
|
|
1980
2444
|
getNoSleepProcess: () => caffeinateProcess,
|
|
1981
2445
|
setNoSleepProcess: (p) => { caffeinateProcess = p || null; },
|
|
2446
|
+
getDefaultEngine,
|
|
1982
2447
|
});
|
|
1983
2448
|
|
|
1984
2449
|
const { handleCommand } = createCommandRouter({
|
|
@@ -2015,7 +2480,7 @@ setDispatchHandler(handleCommand);
|
|
|
2015
2480
|
// ---------------------------------------------------------
|
|
2016
2481
|
// BOT BRIDGES
|
|
2017
2482
|
// ---------------------------------------------------------
|
|
2018
|
-
const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
2483
|
+
const { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge } = createBridgeStarter({
|
|
2019
2484
|
fs,
|
|
2020
2485
|
path,
|
|
2021
2486
|
HOME,
|
|
@@ -2025,10 +2490,13 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
|
|
|
2025
2490
|
loadState,
|
|
2026
2491
|
saveState,
|
|
2027
2492
|
getSession,
|
|
2493
|
+
restoreSessionFromReply,
|
|
2028
2494
|
handleCommand,
|
|
2029
2495
|
pendingActivations,
|
|
2030
2496
|
activeProcesses,
|
|
2031
2497
|
messageQueue,
|
|
2498
|
+
sendRemoteDispatch,
|
|
2499
|
+
handleRemoteDispatchMessage,
|
|
2032
2500
|
});
|
|
2033
2501
|
|
|
2034
2502
|
const { killExistingDaemon, writePid, cleanPid } = createPidManager({
|
|
@@ -2147,7 +2615,7 @@ async function main() {
|
|
|
2147
2615
|
}
|
|
2148
2616
|
|
|
2149
2617
|
// Config validation: warn on unknown/suspect fields
|
|
2150
|
-
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
|
|
2618
|
+
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects', 'imessage', 'siri_bridge'];
|
|
2151
2619
|
const KNOWN_DAEMON = [
|
|
2152
2620
|
'model', // legacy (still valid as fallback)
|
|
2153
2621
|
'models', // per-engine model map: { claude, codex }
|
|
@@ -2238,6 +2706,7 @@ async function main() {
|
|
|
2238
2706
|
// Bridges
|
|
2239
2707
|
let telegramBridge = null;
|
|
2240
2708
|
let feishuBridge = null;
|
|
2709
|
+
let lastWakeBridgeRecoveryAt = 0;
|
|
2241
2710
|
|
|
2242
2711
|
const notifier = createNotifier({
|
|
2243
2712
|
log,
|
|
@@ -2251,6 +2720,25 @@ async function main() {
|
|
|
2251
2720
|
// Start dispatch socket server (low-latency IPC, fallback: file polling still works)
|
|
2252
2721
|
const dispatchSocket = startDispatchSocket(() => config);
|
|
2253
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
|
+
|
|
2254
2742
|
// Start heartbeat scheduler
|
|
2255
2743
|
let heartbeatTimer = startHeartbeat(config, notifyFn, notifyPersonalFn);
|
|
2256
2744
|
|
|
@@ -2303,6 +2791,11 @@ async function main() {
|
|
|
2303
2791
|
// Reuse full shutdown logic, then self-spawn replacement.
|
|
2304
2792
|
shutdown({ restartReason: 'daemon-script-changed' }).catch(() => process.exit(1));
|
|
2305
2793
|
},
|
|
2794
|
+
// Agent soul layer auto-repair on config hot-reload
|
|
2795
|
+
repairAgentLayer,
|
|
2796
|
+
writeConfigSafe,
|
|
2797
|
+
expandPath,
|
|
2798
|
+
HOME,
|
|
2306
2799
|
});
|
|
2307
2800
|
// Expose reloadConfig to handleCommand via closure
|
|
2308
2801
|
global._metameReload = runtimeWatchers.reloadConfig;
|
|
@@ -2310,6 +2803,8 @@ async function main() {
|
|
|
2310
2803
|
// Start bridges (both can run simultaneously)
|
|
2311
2804
|
telegramBridge = await startTelegramBridge(config, executeTaskByName);
|
|
2312
2805
|
feishuBridge = await startFeishuBridge(config, executeTaskByName);
|
|
2806
|
+
await startImessageBridge(config, executeTaskByName);
|
|
2807
|
+
await startSiriBridge(config, executeTaskByName);
|
|
2313
2808
|
if (feishuBridge) _dispatchBridgeRef = feishuBridge; // store bridge, not bot, so .bot stays live after reconnects
|
|
2314
2809
|
|
|
2315
2810
|
// Notify once on startup (single message, no duplicates)
|
|
@@ -2356,6 +2851,8 @@ async function main() {
|
|
|
2356
2851
|
try { require('./qmd-client').stopDaemon(); } catch { /* ignore */ }
|
|
2357
2852
|
// Kill all tracked engine process groups before exiting (covers sub-agents too)
|
|
2358
2853
|
for (const [cid, proc] of activeProcesses) {
|
|
2854
|
+
proc.aborted = true;
|
|
2855
|
+
proc.abortReason = opts.restartReason ? 'daemon-restart' : 'shutdown';
|
|
2359
2856
|
try { process.kill(-proc.child.pid, 'SIGKILL'); } catch { try { proc.child.kill('SIGKILL'); } catch { } }
|
|
2360
2857
|
log('INFO', `Shutdown: killed engine process group for chatId ${cid}`);
|
|
2361
2858
|
}
|
|
@@ -2369,6 +2866,10 @@ async function main() {
|
|
|
2369
2866
|
process.exit(0);
|
|
2370
2867
|
};
|
|
2371
2868
|
|
|
2869
|
+
process.on('SIGUSR2', () => {
|
|
2870
|
+
shutdown({ restartReason: process.env.METAME_DEPLOY_RESTART_REASON || 'external-restart' })
|
|
2871
|
+
.catch(() => process.exit(1));
|
|
2872
|
+
});
|
|
2372
2873
|
process.on('SIGTERM', () => { shutdown().catch(() => process.exit(0)); });
|
|
2373
2874
|
process.on('SIGINT', () => { shutdown().catch(() => process.exit(0)); });
|
|
2374
2875
|
|
|
@@ -2429,4 +2930,21 @@ if (process.argv.includes('--run')) {
|
|
|
2429
2930
|
}
|
|
2430
2931
|
|
|
2431
2932
|
// Export for testing & cross-bot dispatch
|
|
2432
|
-
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
|
+
};
|