metame-cli 1.5.4 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/index.js +277 -55
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +17 -5
- package/scripts/daemon-admin-commands.js +264 -62
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +447 -48
- package/scripts/daemon-claude-engine.js +650 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +34 -2
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +610 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +8 -1
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +150 -11
- package/scripts/hooks/intent-agent-manage.js +0 -50
- package/scripts/hooks/intent-hook-config.js +0 -28
|
@@ -4,6 +4,8 @@ let userAcl = null;
|
|
|
4
4
|
try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
|
|
5
5
|
const { findTeamMember: _findTeamMember } = require('./team-dispatch');
|
|
6
6
|
const { isRemoteMember } = require('./daemon-remote-dispatch');
|
|
7
|
+
const imessageIO = (() => { try { return require('./daemon-siri-imessage'); } catch { return null; } })();
|
|
8
|
+
const siriBridgeMod = (() => { try { return require('./daemon-siri-bridge'); } catch { return null; } })();
|
|
7
9
|
|
|
8
10
|
function createBridgeStarter(deps) {
|
|
9
11
|
const {
|
|
@@ -16,6 +18,7 @@ function createBridgeStarter(deps) {
|
|
|
16
18
|
loadState,
|
|
17
19
|
saveState,
|
|
18
20
|
getSession,
|
|
21
|
+
restoreSessionFromReply,
|
|
19
22
|
handleCommand,
|
|
20
23
|
pendingActivations, // optional — used to show smart activation hint
|
|
21
24
|
activeProcesses, // optional — used for auto-dispatch to clones
|
|
@@ -82,7 +85,7 @@ function createBridgeStarter(deps) {
|
|
|
82
85
|
return latest;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function unauthorizedMsg(chatId
|
|
88
|
+
function unauthorizedMsg(chatId) {
|
|
86
89
|
const pending = getPendingActivationForChat(chatId);
|
|
87
90
|
if (pending) {
|
|
88
91
|
return `⚠️ 此群未授权\n\n发送以下命令激活 Agent「${pending.agentName}」:\n\`/activate\``;
|
|
@@ -90,11 +93,71 @@ function createBridgeStarter(deps) {
|
|
|
90
93
|
return '⚠️ 此群未授权\n\n如已创建 Agent,发送 `/activate` 完成绑定。\n否则请先在主群创建 Agent。';
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
function extractFeishuReplyMessageId(event) {
|
|
97
|
+
const candidates = [
|
|
98
|
+
event && event.message && event.message.parent_id,
|
|
99
|
+
event && event.message && event.message.parent_message_id,
|
|
100
|
+
event && event.message && event.message.root_id,
|
|
101
|
+
event && event.message && event.message.reply_in_thread_id,
|
|
102
|
+
event && event.event && event.event.message && event.event.message.parent_id,
|
|
103
|
+
event && event.event && event.event.message && event.event.message.parent_message_id,
|
|
104
|
+
event && event.event && event.event.message && event.event.message.root_id,
|
|
105
|
+
event && event.event && event.event.message && event.event.message.reply_in_thread_id,
|
|
106
|
+
];
|
|
107
|
+
for (const value of candidates) {
|
|
108
|
+
const text = String(value || '').trim();
|
|
109
|
+
if (text) return text;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function trackBridgeReplyMapping(messageId, payload = {}) {
|
|
115
|
+
const safeMessageId = String(messageId || '').trim();
|
|
116
|
+
if (!safeMessageId) return;
|
|
117
|
+
const state = loadState();
|
|
118
|
+
if (!state.msg_sessions) state.msg_sessions = {};
|
|
119
|
+
state.msg_sessions[safeMessageId] = {
|
|
120
|
+
...(state.msg_sessions[safeMessageId] || {}),
|
|
121
|
+
...payload,
|
|
122
|
+
};
|
|
123
|
+
saveState(state);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function inferSessionMapping(logicalChatId, fallback = {}) {
|
|
127
|
+
const chatKey = String(logicalChatId || '').trim();
|
|
128
|
+
if (!chatKey) return { ...fallback };
|
|
129
|
+
const state = loadState();
|
|
130
|
+
const raw = state.sessions && state.sessions[chatKey];
|
|
131
|
+
if (!raw || typeof raw !== 'object') {
|
|
132
|
+
return {
|
|
133
|
+
logicalChatId: chatKey,
|
|
134
|
+
...fallback,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const engines = raw.engines && typeof raw.engines === 'object' ? raw.engines : {};
|
|
138
|
+
const preferredEngine = String(fallback.engine || '').trim().toLowerCase();
|
|
139
|
+
const slot = (preferredEngine && engines[preferredEngine])
|
|
140
|
+
|| engines.codex
|
|
141
|
+
|| engines.claude
|
|
142
|
+
|| null;
|
|
143
|
+
return {
|
|
144
|
+
...(slot && slot.id ? { id: String(slot.id) } : {}),
|
|
145
|
+
cwd: raw.cwd || fallback.cwd,
|
|
146
|
+
engine: preferredEngine || (engines.codex ? 'codex' : 'claude'),
|
|
147
|
+
logicalChatId: chatKey,
|
|
148
|
+
...((slot && slot.sandboxMode) ? { sandboxMode: slot.sandboxMode } : {}),
|
|
149
|
+
...((slot && slot.approvalPolicy) ? { approvalPolicy: slot.approvalPolicy } : {}),
|
|
150
|
+
...((slot && slot.permissionMode) ? { permissionMode: slot.permissionMode } : {}),
|
|
151
|
+
...fallback,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
93
155
|
// ── Team group helpers ─────────────────────────────────────────────────
|
|
94
156
|
function _getBoundProject(chatId, cfg) {
|
|
95
157
|
const map = {
|
|
96
|
-
...(cfg.telegram
|
|
97
|
-
...(cfg.feishu
|
|
158
|
+
...(cfg.telegram ? cfg.telegram.chat_agent_map || {} : {}),
|
|
159
|
+
...(cfg.feishu ? cfg.feishu.chat_agent_map || {} : {}),
|
|
160
|
+
...(cfg.imessage ? cfg.imessage.chat_agent_map || {} : {}),
|
|
98
161
|
};
|
|
99
162
|
const key = map[String(chatId)];
|
|
100
163
|
const proj = key && cfg.projects ? cfg.projects[key] : null;
|
|
@@ -104,7 +167,7 @@ function createBridgeStarter(deps) {
|
|
|
104
167
|
|
|
105
168
|
// Creates a bot proxy that redirects all send methods to replyChatId
|
|
106
169
|
function _createTeamProxyBot(bot, replyChatId) {
|
|
107
|
-
const SEND = new Set(['sendMessage', 'sendMarkdown', 'sendCard', 'editMessage', 'deleteMessage', 'sendTyping', 'sendFile', 'sendButtonCard']);
|
|
170
|
+
const SEND = new Set(['sendMessage', 'sendMarkdown', 'sendCard', 'editMessage', 'deleteMessage', 'sendTyping', 'sendFile', 'sendButtons', 'sendButtonCard']);
|
|
108
171
|
return new Proxy(bot, {
|
|
109
172
|
get(target, prop) {
|
|
110
173
|
const orig = target[prop];
|
|
@@ -201,6 +264,7 @@ function createBridgeStarter(deps) {
|
|
|
201
264
|
prompt: text,
|
|
202
265
|
source_chat_id: String(realChatId),
|
|
203
266
|
source_sender_key: acl.senderId || 'user',
|
|
267
|
+
source_sender_id: acl.senderId || '',
|
|
204
268
|
}, cfg).then(res => {
|
|
205
269
|
if (res.success) {
|
|
206
270
|
bot.sendMessage(realChatId, `📡 已发送给 ${member.icon || '🤖'} ${member.name} (${member.peer})`).catch(() => {});
|
|
@@ -260,12 +324,16 @@ function createBridgeStarter(deps) {
|
|
|
260
324
|
|
|
261
325
|
let offset = 0;
|
|
262
326
|
let running = true;
|
|
263
|
-
|
|
327
|
+
let abortController = new AbortController();
|
|
328
|
+
let pollLoopActive = false;
|
|
329
|
+
let reconnectTimer = null;
|
|
264
330
|
|
|
265
|
-
const pollLoop = async () => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
const pollLoop = async (signal) => {
|
|
332
|
+
pollLoopActive = true;
|
|
333
|
+
try {
|
|
334
|
+
while (running && signal === abortController.signal) {
|
|
335
|
+
try {
|
|
336
|
+
const updates = await bot.getUpdates(offset, 30, signal);
|
|
269
337
|
for (const update of updates) {
|
|
270
338
|
offset = update.update_id + 1;
|
|
271
339
|
|
|
@@ -377,11 +445,29 @@ function createBridgeStarter(deps) {
|
|
|
377
445
|
|
|
378
446
|
// Team group routing for Telegram (same logic as Feishu)
|
|
379
447
|
const trimmedText = text.trim();
|
|
448
|
+
const parentId = msg.reply_to_message && msg.reply_to_message.message_id
|
|
449
|
+
? String(msg.reply_to_message.message_id)
|
|
450
|
+
: null;
|
|
451
|
+
let _replyAgentKey = null;
|
|
380
452
|
const { key: _boundKey, project: _boundProj } = _getBoundProject(chatId, liveCfg);
|
|
381
453
|
const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
|
|
382
454
|
|
|
383
455
|
// Load sticky state
|
|
384
456
|
const _st = loadState();
|
|
457
|
+
if (parentId) {
|
|
458
|
+
const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
|
|
459
|
+
if (mapped) {
|
|
460
|
+
if (typeof restoreSessionFromReply === 'function') {
|
|
461
|
+
restoreSessionFromReply(chatId, mapped);
|
|
462
|
+
} else {
|
|
463
|
+
if (!_st.sessions) _st.sessions = {};
|
|
464
|
+
_st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
|
|
465
|
+
saveState(_st);
|
|
466
|
+
}
|
|
467
|
+
log('INFO', `Telegram session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
468
|
+
_replyAgentKey = mapped.agentKey || null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
385
471
|
const _chatKey = String(chatId);
|
|
386
472
|
const _setSticky = (key) => {
|
|
387
473
|
if (!_st.team_sticky) _st.team_sticky = {};
|
|
@@ -399,24 +485,55 @@ function createBridgeStarter(deps) {
|
|
|
399
485
|
const _stopMatch = trimmedText && trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
|
|
400
486
|
if (_stopMatch) {
|
|
401
487
|
const _stopArg = (_stopMatch[1] || '').trim();
|
|
402
|
-
|
|
488
|
+
let _targetKey = null;
|
|
489
|
+
if (_replyAgentKey) {
|
|
490
|
+
const m = _boundProj.team.find(t => t.key === _replyAgentKey);
|
|
491
|
+
if (m) _targetKey = m.key;
|
|
492
|
+
}
|
|
493
|
+
if (!_targetKey && _stopArg) {
|
|
403
494
|
const _sa = _stopArg.toLowerCase();
|
|
404
495
|
const m = _boundProj.team.find(t =>
|
|
405
496
|
(t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
|
|
406
497
|
);
|
|
407
|
-
if (m)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
498
|
+
if (m) _targetKey = m.key;
|
|
499
|
+
}
|
|
500
|
+
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
501
|
+
if (_targetKey) {
|
|
502
|
+
const vid = `_agent_${_targetKey}`;
|
|
503
|
+
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
504
|
+
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
505
|
+
if (messageQueue.has(vid)) {
|
|
506
|
+
const vq = messageQueue.get(vid);
|
|
507
|
+
if (vq && vq.timer) clearTimeout(vq.timer);
|
|
508
|
+
messageQueue.delete(vid);
|
|
509
|
+
}
|
|
510
|
+
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
511
|
+
if (vproc && vproc.child) {
|
|
512
|
+
vproc.aborted = true;
|
|
513
|
+
const sig = vproc.killSignal || 'SIGTERM';
|
|
514
|
+
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
515
|
+
await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
|
|
411
516
|
} else {
|
|
412
|
-
await bot.sendMessage(chatId,
|
|
517
|
+
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
413
518
|
}
|
|
414
519
|
continue;
|
|
415
520
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
521
|
+
if (_stopArg) {
|
|
522
|
+
await bot.sendMessage(chatId, `❌ 未找到团队成员: ${_stopArg}`).catch(() => {});
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 0. Quoted reply → force route + set sticky
|
|
528
|
+
if (_replyAgentKey) {
|
|
529
|
+
const member = _boundProj.team.find(m => m.key === _replyAgentKey);
|
|
530
|
+
if (member) {
|
|
531
|
+
_setSticky(member.key);
|
|
532
|
+
log('INFO', `Telegram quoted reply → force route to ${_replyAgentKey} (sticky set)`);
|
|
533
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
log('INFO', `Telegram quoted reply agentKey=${_replyAgentKey} not in team, falling through`);
|
|
420
537
|
}
|
|
421
538
|
|
|
422
539
|
// 1. Explicit nickname → route + set sticky
|
|
@@ -472,16 +589,21 @@ function createBridgeStarter(deps) {
|
|
|
472
589
|
});
|
|
473
590
|
}
|
|
474
591
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
592
|
+
} catch (e) {
|
|
593
|
+
if (e.message === 'aborted') break;
|
|
594
|
+
log('ERROR', `Telegram poll error: ${e.message}`);
|
|
595
|
+
await sleep(5000);
|
|
596
|
+
}
|
|
479
597
|
}
|
|
598
|
+
} finally {
|
|
599
|
+
pollLoopActive = false;
|
|
480
600
|
}
|
|
481
601
|
};
|
|
482
602
|
|
|
483
603
|
const startPoll = () => {
|
|
484
|
-
|
|
604
|
+
if (!running || pollLoopActive) return;
|
|
605
|
+
const signal = abortController.signal;
|
|
606
|
+
pollLoop(signal).catch(e => {
|
|
485
607
|
if (e.message === 'aborted') return;
|
|
486
608
|
log('ERROR', `pollLoop crashed: ${e.message} — restarting in 5s`);
|
|
487
609
|
if (running) setTimeout(startPoll, 5000);
|
|
@@ -490,7 +612,24 @@ function createBridgeStarter(deps) {
|
|
|
490
612
|
startPoll();
|
|
491
613
|
|
|
492
614
|
return {
|
|
493
|
-
stop() {
|
|
615
|
+
stop() {
|
|
616
|
+
running = false;
|
|
617
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
618
|
+
abortController.abort();
|
|
619
|
+
},
|
|
620
|
+
reconnect() {
|
|
621
|
+
if (!running) return;
|
|
622
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
623
|
+
try { abortController.abort(); } catch { /* ignore */ }
|
|
624
|
+
abortController = new AbortController();
|
|
625
|
+
reconnectTimer = setTimeout(() => {
|
|
626
|
+
reconnectTimer = null;
|
|
627
|
+
startPoll();
|
|
628
|
+
}, 150);
|
|
629
|
+
},
|
|
630
|
+
isAlive() {
|
|
631
|
+
return running && (pollLoopActive || !abortController.signal.aborted);
|
|
632
|
+
},
|
|
494
633
|
bot,
|
|
495
634
|
};
|
|
496
635
|
}
|
|
@@ -508,6 +647,12 @@ function createBridgeStarter(deps) {
|
|
|
508
647
|
try {
|
|
509
648
|
const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
|
|
510
649
|
const liveCfg = loadConfig();
|
|
650
|
+
const relayCfg = liveCfg && liveCfg.feishu && liveCfg.feishu.remote_dispatch;
|
|
651
|
+
const relayChatId = relayCfg && relayCfg.chat_id ? String(relayCfg.chat_id) : '';
|
|
652
|
+
if (relayChatId && String(chatId) === relayChatId) {
|
|
653
|
+
const preview = String(text || '').slice(0, 80).replace(/\s+/g, ' ');
|
|
654
|
+
log('INFO', `Feishu relay event chat=${chatId} sender=${senderId || 'unknown'} preview=${preview}`);
|
|
655
|
+
}
|
|
511
656
|
|
|
512
657
|
// ── Remote dispatch interception (before ACL) ──
|
|
513
658
|
if (handleRemoteDispatchMessage && text) {
|
|
@@ -575,18 +720,31 @@ function createBridgeStarter(deps) {
|
|
|
575
720
|
});
|
|
576
721
|
if (acl.blocked) return;
|
|
577
722
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|
|
578
|
-
const parentId = event
|
|
723
|
+
const parentId = extractFeishuReplyMessageId(event);
|
|
579
724
|
let _replyAgentKey = null;
|
|
580
725
|
// Load state once for the entire routing block
|
|
581
726
|
const _st = loadState();
|
|
727
|
+
if (parentId) {
|
|
728
|
+
log('INFO', `Feishu reply metadata detected chat=${chatId} parentId=${parentId}`);
|
|
729
|
+
}
|
|
582
730
|
if (parentId) {
|
|
583
731
|
const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
|
|
584
732
|
if (mapped) {
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
733
|
+
if (typeof restoreSessionFromReply === 'function') {
|
|
734
|
+
restoreSessionFromReply(chatId, mapped);
|
|
735
|
+
} else {
|
|
736
|
+
if (mapped.id) {
|
|
737
|
+
if (!_st.sessions) _st.sessions = {};
|
|
738
|
+
_st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
|
|
739
|
+
saveState(_st);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (mapped.id) {
|
|
743
|
+
log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
744
|
+
}
|
|
589
745
|
_replyAgentKey = mapped.agentKey || null;
|
|
746
|
+
} else {
|
|
747
|
+
log('INFO', `Feishu reply parentId=${parentId} had no msg_sessions mapping`);
|
|
590
748
|
}
|
|
591
749
|
}
|
|
592
750
|
|
|
@@ -670,18 +828,28 @@ function createBridgeStarter(deps) {
|
|
|
670
828
|
}
|
|
671
829
|
// 1. Explicit nickname → route + set sticky
|
|
672
830
|
const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
831
|
+
if (teamMatch) {
|
|
832
|
+
const { member, rest } = teamMatch;
|
|
833
|
+
_setSticky(member.key);
|
|
834
|
+
if (!rest) {
|
|
835
|
+
// Pure nickname, no task — confirm member is online
|
|
836
|
+
log('INFO', `Sticky set (pure nickname): ${_chatKey.slice(-8)} → ${member.key}`);
|
|
837
|
+
bot.sendMarkdown(chatId, `${member.icon || '🤖'} **${member.name}** 在线`)
|
|
838
|
+
.then((msg) => {
|
|
839
|
+
if (msg && msg.message_id) {
|
|
840
|
+
trackBridgeReplyMapping(msg.message_id, inferSessionMapping(`_agent_${member.key}`, {
|
|
841
|
+
agentKey: member.key,
|
|
842
|
+
cwd: member.cwd || _boundProj.cwd,
|
|
843
|
+
engine: member.engine || _boundProj.engine || 'claude',
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
846
|
+
})
|
|
847
|
+
.catch(() => {});
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
log('INFO', `Sticky set: ${_chatKey.slice(-8)} → ${member.key}`);
|
|
851
|
+
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
852
|
+
return;
|
|
685
853
|
}
|
|
686
854
|
|
|
687
855
|
// 1.5. Main project nickname → clear sticky, route to main
|
|
@@ -691,11 +859,22 @@ function createBridgeStarter(deps) {
|
|
|
691
859
|
if (_mainMatch) {
|
|
692
860
|
_clearSticky();
|
|
693
861
|
const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
862
|
+
log('INFO', `Main nickname → cleared sticky, routing to main${rest ? ` (task: ${rest.slice(0, 30)})` : ''}`);
|
|
863
|
+
if (!rest) {
|
|
864
|
+
bot.sendMarkdown(chatId, `${_boundProj.icon || '🤖'} **${_boundProj.name}** 在线`)
|
|
865
|
+
.then((msg) => {
|
|
866
|
+
if (msg && msg.message_id) {
|
|
867
|
+
trackBridgeReplyMapping(msg.message_id, inferSessionMapping(String(chatId), {
|
|
868
|
+
agentKey: _boundKey || null,
|
|
869
|
+
cwd: _boundProj.cwd,
|
|
870
|
+
engine: _boundProj.engine || 'claude',
|
|
871
|
+
logicalChatId: _boundKey ? `_bound_${_boundKey}` : String(chatId),
|
|
872
|
+
}));
|
|
873
|
+
}
|
|
874
|
+
})
|
|
875
|
+
.catch(() => {});
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
699
878
|
try {
|
|
700
879
|
await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
701
880
|
} catch (e) {
|
|
@@ -729,7 +908,227 @@ function createBridgeStarter(deps) {
|
|
|
729
908
|
}
|
|
730
909
|
}
|
|
731
910
|
|
|
732
|
-
|
|
911
|
+
// ── iMessage Bridge ─────────────────────────────────────────────────────────
|
|
912
|
+
async function startImessageBridge(config, executeTaskByName) {
|
|
913
|
+
const cfg = config.imessage || {};
|
|
914
|
+
if (!cfg.enabled) return null;
|
|
915
|
+
if (!imessageIO) { log('WARN', '[IMESSAGE] daemon-siri-imessage module not found'); return null; }
|
|
916
|
+
if (!imessageIO.isAvailable()) { log('WARN', '[IMESSAGE] chat.db not found — bridge disabled'); return null; }
|
|
917
|
+
|
|
918
|
+
const selfId = cfg.self_id || '';
|
|
919
|
+
const allowedSenders = cfg.allowed_senders || (selfId ? [selfId] : []);
|
|
920
|
+
const allowedChats = cfg.allowed_chat_ids || [];
|
|
921
|
+
const pollMs = cfg.poll_ms || 2000;
|
|
922
|
+
|
|
923
|
+
if (!selfId) { log('WARN', '[IMESSAGE] self_id not configured — bridge disabled'); return null; }
|
|
924
|
+
|
|
925
|
+
let lastRowId = imessageIO.getMaxRowId();
|
|
926
|
+
let processing = false;
|
|
927
|
+
let running = true;
|
|
928
|
+
|
|
929
|
+
// Per-chat persistent bot instances (preserve state across polls)
|
|
930
|
+
const chatBots = new Map();
|
|
931
|
+
const getBot = (chatTarget) => {
|
|
932
|
+
if (!chatBots.has(chatTarget)) {
|
|
933
|
+
const bot = imessageIO.createImessageBot(chatTarget, log);
|
|
934
|
+
// After bot sends a reply, advance lastRowId immediately + again after delay
|
|
935
|
+
if (bot.setOnAfterSend) {
|
|
936
|
+
bot.setOnAfterSend(() => {
|
|
937
|
+
// Immediate advance — covers fast echo
|
|
938
|
+
const freshNow = imessageIO.getMaxRowId();
|
|
939
|
+
if (freshNow > lastRowId) {
|
|
940
|
+
log('INFO', `[IMESSAGE] Advanced lastRowId ${lastRowId}→${freshNow} (echo skip immediate)`);
|
|
941
|
+
lastRowId = freshNow;
|
|
942
|
+
}
|
|
943
|
+
// Delayed advance — covers slow iCloud sync echo
|
|
944
|
+
setTimeout(() => {
|
|
945
|
+
const freshLater = imessageIO.getMaxRowId();
|
|
946
|
+
if (freshLater > lastRowId) {
|
|
947
|
+
log('INFO', `[IMESSAGE] Advanced lastRowId ${lastRowId}→${freshLater} (echo skip delayed)`);
|
|
948
|
+
lastRowId = freshLater;
|
|
949
|
+
}
|
|
950
|
+
}, 3000);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
chatBots.set(chatTarget, bot);
|
|
954
|
+
}
|
|
955
|
+
return chatBots.get(chatTarget);
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
log('INFO', `[IMESSAGE] Bridge started (poll=${pollMs}ms, self=${selfId}, lastRowId=${lastRowId})`);
|
|
959
|
+
|
|
960
|
+
const timer = setInterval(async () => {
|
|
961
|
+
if (!running || processing) return;
|
|
962
|
+
processing = true;
|
|
963
|
+
try {
|
|
964
|
+
const rows = imessageIO.queryNewMessages(lastRowId);
|
|
965
|
+
if (!rows) { processing = false; return; }
|
|
966
|
+
|
|
967
|
+
for (const row of rows.split('\n').filter(Boolean)) {
|
|
968
|
+
const parts = row.split('\t');
|
|
969
|
+
const rowId = parseInt(parts[0], 10);
|
|
970
|
+
const text = (parts[1] || '').trim();
|
|
971
|
+
const sender = (parts[2] || '').trim();
|
|
972
|
+
const chatGuid = (parts[3] || '').trim();
|
|
973
|
+
const chatIdentifier = (parts[4] || '').trim();
|
|
974
|
+
const chatName = (parts[5] || '').trim();
|
|
975
|
+
const chatTarget = chatGuid || chatIdentifier || sender;
|
|
976
|
+
|
|
977
|
+
if (!rowId || rowId <= lastRowId) continue;
|
|
978
|
+
lastRowId = rowId;
|
|
979
|
+
if (!text) continue;
|
|
980
|
+
if (!chatTarget) continue;
|
|
981
|
+
|
|
982
|
+
if (allowedSenders.length && !allowedSenders.includes(sender)) {
|
|
983
|
+
log('INFO', `[IMESSAGE] Ignored message from ${sender} (not in allowed_senders)`);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (allowedChats.length && !allowedChats.includes(chatTarget) && !allowedChats.includes(chatIdentifier)) {
|
|
987
|
+
log('INFO', `[IMESSAGE] Ignored chat ${chatTarget} (${chatName || sender || 'unknown'}) not in allowed_chat_ids`);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const chatId = chatTarget;
|
|
992
|
+
const liveCfg = loadConfig();
|
|
993
|
+
const bot = getBot(chatTarget);
|
|
994
|
+
|
|
995
|
+
// Echo fingerprint check — skip if this text matches something we recently sent
|
|
996
|
+
if (bot.isEcho && bot.isEcho(text)) {
|
|
997
|
+
log('INFO', `[IMESSAGE] Skipped echo: "${text.slice(0, 40)}"`);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const trimmedText = text.trim();
|
|
1002
|
+
let commandText = text;
|
|
1003
|
+
|
|
1004
|
+
log('INFO', `[IMESSAGE] Received chat=${chatTarget} sender=${sender || 'unknown'} name=${chatName || '-'}: "${text.slice(0, 60)}"`);
|
|
1005
|
+
|
|
1006
|
+
const acl = await applyUserAcl({
|
|
1007
|
+
bot,
|
|
1008
|
+
chatId,
|
|
1009
|
+
text,
|
|
1010
|
+
config: liveCfg,
|
|
1011
|
+
senderId: sender,
|
|
1012
|
+
bypassAcl: false,
|
|
1013
|
+
});
|
|
1014
|
+
if (acl.blocked) continue;
|
|
1015
|
+
|
|
1016
|
+
const { project: _boundProj } = _getBoundProject(chatId, liveCfg);
|
|
1017
|
+
const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
|
|
1018
|
+
const _st = loadState();
|
|
1019
|
+
const _chatKey = String(chatId);
|
|
1020
|
+
const _setSticky = (key) => {
|
|
1021
|
+
if (!_st.team_sticky) _st.team_sticky = {};
|
|
1022
|
+
_st.team_sticky[_chatKey] = key;
|
|
1023
|
+
saveState(_st);
|
|
1024
|
+
};
|
|
1025
|
+
const _clearSticky = () => {
|
|
1026
|
+
if (_st.team_sticky) delete _st.team_sticky[_chatKey];
|
|
1027
|
+
saveState(_st);
|
|
1028
|
+
};
|
|
1029
|
+
const _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
|
|
1030
|
+
|
|
1031
|
+
if (_boundProj && Array.isArray(_boundProj.team) && _boundProj.team.length > 0 && !_isTeamSlashCmd) {
|
|
1032
|
+
const _stopMatch = trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
|
|
1033
|
+
if (_stopMatch) {
|
|
1034
|
+
const _stopArg = (_stopMatch[1] || '').trim();
|
|
1035
|
+
let _targetKey = null;
|
|
1036
|
+
if (_stopArg) {
|
|
1037
|
+
const _sa = _stopArg.toLowerCase();
|
|
1038
|
+
const m = _boundProj.team.find(t =>
|
|
1039
|
+
(t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
|
|
1040
|
+
);
|
|
1041
|
+
if (m) _targetKey = m.key;
|
|
1042
|
+
}
|
|
1043
|
+
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
1044
|
+
if (_targetKey) {
|
|
1045
|
+
const vid = `_agent_${_targetKey}`;
|
|
1046
|
+
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
1047
|
+
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
1048
|
+
if (messageQueue.has(vid)) {
|
|
1049
|
+
const vq = messageQueue.get(vid);
|
|
1050
|
+
if (vq && vq.timer) clearTimeout(vq.timer);
|
|
1051
|
+
messageQueue.delete(vid);
|
|
1052
|
+
}
|
|
1053
|
+
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
1054
|
+
if (vproc && vproc.child) {
|
|
1055
|
+
vproc.aborted = true;
|
|
1056
|
+
const sig = vproc.killSignal || 'SIGTERM';
|
|
1057
|
+
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
1058
|
+
await bot.sendMessage(chatId, `Stopping ${label}...`);
|
|
1059
|
+
} else {
|
|
1060
|
+
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
1061
|
+
}
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (_stopArg) {
|
|
1065
|
+
await bot.sendMessage(chatId, `未找到团队成员: ${_stopArg}`);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
|
|
1071
|
+
if (teamMatch) {
|
|
1072
|
+
const { member, rest } = teamMatch;
|
|
1073
|
+
_setSticky(member.key);
|
|
1074
|
+
if (!rest) {
|
|
1075
|
+
await bot.sendMessage(chatId, `${member.icon || '🤖'} ${member.name} 在线`);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
log('INFO', `[IMESSAGE] Team route ${chatId} -> ${member.key}`);
|
|
1079
|
+
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const _mainNicks = Array.isArray(_boundProj.nicknames) ? _boundProj.nicknames : [];
|
|
1084
|
+
const _trimLower = trimmedText.toLowerCase();
|
|
1085
|
+
const _mainMatch = _mainNicks.find(n =>
|
|
1086
|
+
_trimLower === n.toLowerCase()
|
|
1087
|
+
|| _trimLower.startsWith(n.toLowerCase() + ' ')
|
|
1088
|
+
|| _trimLower.startsWith(n.toLowerCase() + ',')
|
|
1089
|
+
|| _trimLower.startsWith(n.toLowerCase() + ',')
|
|
1090
|
+
);
|
|
1091
|
+
if (_mainMatch) {
|
|
1092
|
+
_clearSticky();
|
|
1093
|
+
const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
|
|
1094
|
+
if (!rest) {
|
|
1095
|
+
await bot.sendMessage(chatId, `${_boundProj.icon || '🤖'} ${_boundProj.name || 'Agent'} 在线`);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
commandText = rest;
|
|
1099
|
+
} else if (_stickyKey) {
|
|
1100
|
+
const member = _boundProj.team.find(m => m.key === _stickyKey);
|
|
1101
|
+
if (member) {
|
|
1102
|
+
log('INFO', `[IMESSAGE] Sticky route ${chatId} -> ${member.key}`);
|
|
1103
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
handleCommand(bot, chatId, commandText, liveCfg, executeTaskByName, sender, false)
|
|
1110
|
+
.catch(e => log('ERROR', `[IMESSAGE] handleCommand error: ${e.message}`));
|
|
1111
|
+
}
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
log('WARN', `[IMESSAGE] poll error: ${e.message}`);
|
|
1114
|
+
}
|
|
1115
|
+
processing = false;
|
|
1116
|
+
}, pollMs);
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
stop: () => { running = false; clearInterval(timer); },
|
|
1120
|
+
bot: imessageIO.createImessageBot(selfId, log),
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ── Siri HTTP Bridge ────────────────────────────────────────────────────────
|
|
1125
|
+
function startSiriBridge(config, executeTaskByName) {
|
|
1126
|
+
if (!siriBridgeMod) { log('WARN', '[SIRI] daemon-siri-bridge module not found'); return null; }
|
|
1127
|
+
const bridge = siriBridgeMod.createSiriBridge({ log, loadConfig, handleCommand });
|
|
1128
|
+
return bridge.startSiriBridge(config, executeTaskByName);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge };
|
|
733
1132
|
}
|
|
734
1133
|
|
|
735
1134
|
module.exports = { createBridgeStarter };
|