metame-cli 1.5.4 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/index.js +277 -55
- package/package.json +3 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +18 -6
- package/scripts/bin/push-clean.sh +72 -0
- package/scripts/daemon-admin-commands.js +266 -64
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +475 -50
- package/scripts/daemon-checkpoints.js +84 -30
- package/scripts/daemon-claude-engine.js +651 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-dispatch-cards.js +185 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +28 -6
- package/scripts/daemon-remote-dispatch.js +34 -2
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/{team-dispatch.js → daemon-team-dispatch.js} +150 -11
- package/scripts/daemon.js +484 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +10 -3
- package/scripts/docs/pointer-map.md +2 -2
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/hooks/intent-agent-manage.js +0 -50
- package/scripts/hooks/intent-hook-config.js +0 -28
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
let userAcl = null;
|
|
4
4
|
try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
|
|
5
|
-
const { findTeamMember: _findTeamMember } = require('./team-dispatch');
|
|
5
|
+
const { findTeamMember: _findTeamMember } = require('./daemon-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,21 +93,81 @@ 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;
|
|
101
164
|
return { key: key || null, project: proj || null };
|
|
102
165
|
}
|
|
103
|
-
// _findTeamMember is imported from team-dispatch.js (shared with admin-commands)
|
|
166
|
+
// _findTeamMember is imported from daemon-team-dispatch.js (shared with admin-commands)
|
|
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
|
|
|
@@ -353,6 +421,19 @@ function createBridgeStarter(deps) {
|
|
|
353
421
|
? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
|
|
354
422
|
: `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
|
|
355
423
|
|
|
424
|
+
// Respect team_sticky: route to active agent same as text messages
|
|
425
|
+
const _stFile = loadState();
|
|
426
|
+
const _chatKeyFile = String(chatId);
|
|
427
|
+
const { project: _boundProjFile } = _getBoundProject(chatId, liveCfg);
|
|
428
|
+
const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile];
|
|
429
|
+
if (_boundProjFile && Array.isArray(_boundProjFile.team) && _boundProjFile.team.length > 0 && _stickyKeyFile) {
|
|
430
|
+
const _stickyMember = _boundProjFile.team.find(m => m.key === _stickyKeyFile);
|
|
431
|
+
if (_stickyMember) {
|
|
432
|
+
log('INFO', `Telegram file → sticky route to ${_stickyKeyFile}`);
|
|
433
|
+
_dispatchToTeamMember(_stickyMember, _boundProjFile, prompt, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
356
437
|
handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
|
|
357
438
|
log('ERROR', `Telegram file handler error: ${e.message}`);
|
|
358
439
|
});
|
|
@@ -377,11 +458,29 @@ function createBridgeStarter(deps) {
|
|
|
377
458
|
|
|
378
459
|
// Team group routing for Telegram (same logic as Feishu)
|
|
379
460
|
const trimmedText = text.trim();
|
|
461
|
+
const parentId = msg.reply_to_message && msg.reply_to_message.message_id
|
|
462
|
+
? String(msg.reply_to_message.message_id)
|
|
463
|
+
: null;
|
|
464
|
+
let _replyAgentKey = null;
|
|
380
465
|
const { key: _boundKey, project: _boundProj } = _getBoundProject(chatId, liveCfg);
|
|
381
466
|
const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
|
|
382
467
|
|
|
383
468
|
// Load sticky state
|
|
384
469
|
const _st = loadState();
|
|
470
|
+
if (parentId) {
|
|
471
|
+
const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
|
|
472
|
+
if (mapped) {
|
|
473
|
+
if (typeof restoreSessionFromReply === 'function') {
|
|
474
|
+
restoreSessionFromReply(chatId, mapped);
|
|
475
|
+
} else {
|
|
476
|
+
if (!_st.sessions) _st.sessions = {};
|
|
477
|
+
_st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
|
|
478
|
+
saveState(_st);
|
|
479
|
+
}
|
|
480
|
+
log('INFO', `Telegram session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
481
|
+
_replyAgentKey = mapped.agentKey || null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
385
484
|
const _chatKey = String(chatId);
|
|
386
485
|
const _setSticky = (key) => {
|
|
387
486
|
if (!_st.team_sticky) _st.team_sticky = {};
|
|
@@ -399,24 +498,55 @@ function createBridgeStarter(deps) {
|
|
|
399
498
|
const _stopMatch = trimmedText && trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
|
|
400
499
|
if (_stopMatch) {
|
|
401
500
|
const _stopArg = (_stopMatch[1] || '').trim();
|
|
402
|
-
|
|
501
|
+
let _targetKey = null;
|
|
502
|
+
if (_replyAgentKey) {
|
|
503
|
+
const m = _boundProj.team.find(t => t.key === _replyAgentKey);
|
|
504
|
+
if (m) _targetKey = m.key;
|
|
505
|
+
}
|
|
506
|
+
if (!_targetKey && _stopArg) {
|
|
403
507
|
const _sa = _stopArg.toLowerCase();
|
|
404
508
|
const m = _boundProj.team.find(t =>
|
|
405
509
|
(t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
|
|
406
510
|
);
|
|
407
|
-
if (m)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
511
|
+
if (m) _targetKey = m.key;
|
|
512
|
+
}
|
|
513
|
+
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
514
|
+
if (_targetKey) {
|
|
515
|
+
const vid = `_agent_${_targetKey}`;
|
|
516
|
+
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
517
|
+
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
518
|
+
if (messageQueue.has(vid)) {
|
|
519
|
+
const vq = messageQueue.get(vid);
|
|
520
|
+
if (vq && vq.timer) clearTimeout(vq.timer);
|
|
521
|
+
messageQueue.delete(vid);
|
|
522
|
+
}
|
|
523
|
+
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
524
|
+
if (vproc && vproc.child) {
|
|
525
|
+
vproc.aborted = true;
|
|
526
|
+
const sig = vproc.killSignal || 'SIGTERM';
|
|
527
|
+
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
528
|
+
await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
|
|
411
529
|
} else {
|
|
412
|
-
await bot.sendMessage(chatId,
|
|
530
|
+
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
413
531
|
}
|
|
414
532
|
continue;
|
|
415
533
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
534
|
+
if (_stopArg) {
|
|
535
|
+
await bot.sendMessage(chatId, `❌ 未找到团队成员: ${_stopArg}`).catch(() => {});
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 0. Quoted reply → force route + set sticky
|
|
541
|
+
if (_replyAgentKey) {
|
|
542
|
+
const member = _boundProj.team.find(m => m.key === _replyAgentKey);
|
|
543
|
+
if (member) {
|
|
544
|
+
_setSticky(member.key);
|
|
545
|
+
log('INFO', `Telegram quoted reply → force route to ${_replyAgentKey} (sticky set)`);
|
|
546
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
log('INFO', `Telegram quoted reply agentKey=${_replyAgentKey} not in team, falling through`);
|
|
420
550
|
}
|
|
421
551
|
|
|
422
552
|
// 1. Explicit nickname → route + set sticky
|
|
@@ -472,16 +602,21 @@ function createBridgeStarter(deps) {
|
|
|
472
602
|
});
|
|
473
603
|
}
|
|
474
604
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
605
|
+
} catch (e) {
|
|
606
|
+
if (e.message === 'aborted') break;
|
|
607
|
+
log('ERROR', `Telegram poll error: ${e.message}`);
|
|
608
|
+
await sleep(5000);
|
|
609
|
+
}
|
|
479
610
|
}
|
|
611
|
+
} finally {
|
|
612
|
+
pollLoopActive = false;
|
|
480
613
|
}
|
|
481
614
|
};
|
|
482
615
|
|
|
483
616
|
const startPoll = () => {
|
|
484
|
-
|
|
617
|
+
if (!running || pollLoopActive) return;
|
|
618
|
+
const signal = abortController.signal;
|
|
619
|
+
pollLoop(signal).catch(e => {
|
|
485
620
|
if (e.message === 'aborted') return;
|
|
486
621
|
log('ERROR', `pollLoop crashed: ${e.message} — restarting in 5s`);
|
|
487
622
|
if (running) setTimeout(startPoll, 5000);
|
|
@@ -490,7 +625,24 @@ function createBridgeStarter(deps) {
|
|
|
490
625
|
startPoll();
|
|
491
626
|
|
|
492
627
|
return {
|
|
493
|
-
stop() {
|
|
628
|
+
stop() {
|
|
629
|
+
running = false;
|
|
630
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
631
|
+
abortController.abort();
|
|
632
|
+
},
|
|
633
|
+
reconnect() {
|
|
634
|
+
if (!running) return;
|
|
635
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
636
|
+
try { abortController.abort(); } catch { /* ignore */ }
|
|
637
|
+
abortController = new AbortController();
|
|
638
|
+
reconnectTimer = setTimeout(() => {
|
|
639
|
+
reconnectTimer = null;
|
|
640
|
+
startPoll();
|
|
641
|
+
}, 150);
|
|
642
|
+
},
|
|
643
|
+
isAlive() {
|
|
644
|
+
return running && (pollLoopActive || !abortController.signal.aborted);
|
|
645
|
+
},
|
|
494
646
|
bot,
|
|
495
647
|
};
|
|
496
648
|
}
|
|
@@ -508,6 +660,12 @@ function createBridgeStarter(deps) {
|
|
|
508
660
|
try {
|
|
509
661
|
const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
|
|
510
662
|
const liveCfg = loadConfig();
|
|
663
|
+
const relayCfg = liveCfg && liveCfg.feishu && liveCfg.feishu.remote_dispatch;
|
|
664
|
+
const relayChatId = relayCfg && relayCfg.chat_id ? String(relayCfg.chat_id) : '';
|
|
665
|
+
if (relayChatId && String(chatId) === relayChatId) {
|
|
666
|
+
const preview = String(text || '').slice(0, 80).replace(/\s+/g, ' ');
|
|
667
|
+
log('INFO', `Feishu relay event chat=${chatId} sender=${senderId || 'unknown'} preview=${preview}`);
|
|
668
|
+
}
|
|
511
669
|
|
|
512
670
|
// ── Remote dispatch interception (before ACL) ──
|
|
513
671
|
if (handleRemoteDispatchMessage && text) {
|
|
@@ -556,6 +714,19 @@ function createBridgeStarter(deps) {
|
|
|
556
714
|
? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
|
|
557
715
|
: `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
|
|
558
716
|
|
|
717
|
+
// Respect team_sticky: route to active agent same as text messages
|
|
718
|
+
const _stFile = loadState();
|
|
719
|
+
const _chatKeyFile = String(chatId);
|
|
720
|
+
const { project: _boundProjFile } = _getBoundProject(chatId, liveCfg);
|
|
721
|
+
const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile];
|
|
722
|
+
if (_boundProjFile && Array.isArray(_boundProjFile.team) && _boundProjFile.team.length > 0 && _stickyKeyFile) {
|
|
723
|
+
const _stickyMember = _boundProjFile.team.find(m => m.key === _stickyKeyFile);
|
|
724
|
+
if (_stickyMember) {
|
|
725
|
+
log('INFO', `Feishu file → sticky route to ${_stickyKeyFile}`);
|
|
726
|
+
_dispatchToTeamMember(_stickyMember, _boundProjFile, prompt, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
559
730
|
await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
560
731
|
} catch (err) {
|
|
561
732
|
log('ERROR', `Feishu file download failed: ${err.message}`);
|
|
@@ -575,18 +746,31 @@ function createBridgeStarter(deps) {
|
|
|
575
746
|
});
|
|
576
747
|
if (acl.blocked) return;
|
|
577
748
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|
|
578
|
-
const parentId = event
|
|
749
|
+
const parentId = extractFeishuReplyMessageId(event);
|
|
579
750
|
let _replyAgentKey = null;
|
|
580
751
|
// Load state once for the entire routing block
|
|
581
752
|
const _st = loadState();
|
|
753
|
+
if (parentId) {
|
|
754
|
+
log('INFO', `Feishu reply metadata detected chat=${chatId} parentId=${parentId}`);
|
|
755
|
+
}
|
|
582
756
|
if (parentId) {
|
|
583
757
|
const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
|
|
584
758
|
if (mapped) {
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
759
|
+
if (typeof restoreSessionFromReply === 'function') {
|
|
760
|
+
restoreSessionFromReply(chatId, mapped);
|
|
761
|
+
} else {
|
|
762
|
+
if (mapped.id) {
|
|
763
|
+
if (!_st.sessions) _st.sessions = {};
|
|
764
|
+
_st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
|
|
765
|
+
saveState(_st);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (mapped.id) {
|
|
769
|
+
log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
770
|
+
}
|
|
589
771
|
_replyAgentKey = mapped.agentKey || null;
|
|
772
|
+
} else {
|
|
773
|
+
log('INFO', `Feishu reply parentId=${parentId} had no msg_sessions mapping`);
|
|
590
774
|
}
|
|
591
775
|
}
|
|
592
776
|
|
|
@@ -670,18 +854,28 @@ function createBridgeStarter(deps) {
|
|
|
670
854
|
}
|
|
671
855
|
// 1. Explicit nickname → route + set sticky
|
|
672
856
|
const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
857
|
+
if (teamMatch) {
|
|
858
|
+
const { member, rest } = teamMatch;
|
|
859
|
+
_setSticky(member.key);
|
|
860
|
+
if (!rest) {
|
|
861
|
+
// Pure nickname, no task — confirm member is online
|
|
862
|
+
log('INFO', `Sticky set (pure nickname): ${_chatKey.slice(-8)} → ${member.key}`);
|
|
863
|
+
bot.sendMarkdown(chatId, `${member.icon || '🤖'} **${member.name}** 在线`)
|
|
864
|
+
.then((msg) => {
|
|
865
|
+
if (msg && msg.message_id) {
|
|
866
|
+
trackBridgeReplyMapping(msg.message_id, inferSessionMapping(`_agent_${member.key}`, {
|
|
867
|
+
agentKey: member.key,
|
|
868
|
+
cwd: member.cwd || _boundProj.cwd,
|
|
869
|
+
engine: member.engine || _boundProj.engine || 'claude',
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
.catch(() => {});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
log('INFO', `Sticky set: ${_chatKey.slice(-8)} → ${member.key}`);
|
|
877
|
+
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
878
|
+
return;
|
|
685
879
|
}
|
|
686
880
|
|
|
687
881
|
// 1.5. Main project nickname → clear sticky, route to main
|
|
@@ -691,11 +885,22 @@ function createBridgeStarter(deps) {
|
|
|
691
885
|
if (_mainMatch) {
|
|
692
886
|
_clearSticky();
|
|
693
887
|
const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
888
|
+
log('INFO', `Main nickname → cleared sticky, routing to main${rest ? ` (task: ${rest.slice(0, 30)})` : ''}`);
|
|
889
|
+
if (!rest) {
|
|
890
|
+
bot.sendMarkdown(chatId, `${_boundProj.icon || '🤖'} **${_boundProj.name}** 在线`)
|
|
891
|
+
.then((msg) => {
|
|
892
|
+
if (msg && msg.message_id) {
|
|
893
|
+
trackBridgeReplyMapping(msg.message_id, inferSessionMapping(String(chatId), {
|
|
894
|
+
agentKey: _boundKey || null,
|
|
895
|
+
cwd: _boundProj.cwd,
|
|
896
|
+
engine: _boundProj.engine || 'claude',
|
|
897
|
+
logicalChatId: _boundKey ? `_bound_${_boundKey}` : String(chatId),
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
})
|
|
901
|
+
.catch(() => {});
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
699
904
|
try {
|
|
700
905
|
await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
701
906
|
} catch (e) {
|
|
@@ -729,7 +934,227 @@ function createBridgeStarter(deps) {
|
|
|
729
934
|
}
|
|
730
935
|
}
|
|
731
936
|
|
|
732
|
-
|
|
937
|
+
// ── iMessage Bridge ─────────────────────────────────────────────────────────
|
|
938
|
+
async function startImessageBridge(config, executeTaskByName) {
|
|
939
|
+
const cfg = config.imessage || {};
|
|
940
|
+
if (!cfg.enabled) return null;
|
|
941
|
+
if (!imessageIO) { log('WARN', '[IMESSAGE] daemon-siri-imessage module not found'); return null; }
|
|
942
|
+
if (!imessageIO.isAvailable()) { log('WARN', '[IMESSAGE] chat.db not found — bridge disabled'); return null; }
|
|
943
|
+
|
|
944
|
+
const selfId = cfg.self_id || '';
|
|
945
|
+
const allowedSenders = cfg.allowed_senders || (selfId ? [selfId] : []);
|
|
946
|
+
const allowedChats = cfg.allowed_chat_ids || [];
|
|
947
|
+
const pollMs = cfg.poll_ms || 2000;
|
|
948
|
+
|
|
949
|
+
if (!selfId) { log('WARN', '[IMESSAGE] self_id not configured — bridge disabled'); return null; }
|
|
950
|
+
|
|
951
|
+
let lastRowId = imessageIO.getMaxRowId();
|
|
952
|
+
let processing = false;
|
|
953
|
+
let running = true;
|
|
954
|
+
|
|
955
|
+
// Per-chat persistent bot instances (preserve state across polls)
|
|
956
|
+
const chatBots = new Map();
|
|
957
|
+
const getBot = (chatTarget) => {
|
|
958
|
+
if (!chatBots.has(chatTarget)) {
|
|
959
|
+
const bot = imessageIO.createImessageBot(chatTarget, log);
|
|
960
|
+
// After bot sends a reply, advance lastRowId immediately + again after delay
|
|
961
|
+
if (bot.setOnAfterSend) {
|
|
962
|
+
bot.setOnAfterSend(() => {
|
|
963
|
+
// Immediate advance — covers fast echo
|
|
964
|
+
const freshNow = imessageIO.getMaxRowId();
|
|
965
|
+
if (freshNow > lastRowId) {
|
|
966
|
+
log('INFO', `[IMESSAGE] Advanced lastRowId ${lastRowId}→${freshNow} (echo skip immediate)`);
|
|
967
|
+
lastRowId = freshNow;
|
|
968
|
+
}
|
|
969
|
+
// Delayed advance — covers slow iCloud sync echo
|
|
970
|
+
setTimeout(() => {
|
|
971
|
+
const freshLater = imessageIO.getMaxRowId();
|
|
972
|
+
if (freshLater > lastRowId) {
|
|
973
|
+
log('INFO', `[IMESSAGE] Advanced lastRowId ${lastRowId}→${freshLater} (echo skip delayed)`);
|
|
974
|
+
lastRowId = freshLater;
|
|
975
|
+
}
|
|
976
|
+
}, 3000);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
chatBots.set(chatTarget, bot);
|
|
980
|
+
}
|
|
981
|
+
return chatBots.get(chatTarget);
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
log('INFO', `[IMESSAGE] Bridge started (poll=${pollMs}ms, self=${selfId}, lastRowId=${lastRowId})`);
|
|
985
|
+
|
|
986
|
+
const timer = setInterval(async () => {
|
|
987
|
+
if (!running || processing) return;
|
|
988
|
+
processing = true;
|
|
989
|
+
try {
|
|
990
|
+
const rows = imessageIO.queryNewMessages(lastRowId);
|
|
991
|
+
if (!rows) { processing = false; return; }
|
|
992
|
+
|
|
993
|
+
for (const row of rows.split('\n').filter(Boolean)) {
|
|
994
|
+
const parts = row.split('\t');
|
|
995
|
+
const rowId = parseInt(parts[0], 10);
|
|
996
|
+
const text = (parts[1] || '').trim();
|
|
997
|
+
const sender = (parts[2] || '').trim();
|
|
998
|
+
const chatGuid = (parts[3] || '').trim();
|
|
999
|
+
const chatIdentifier = (parts[4] || '').trim();
|
|
1000
|
+
const chatName = (parts[5] || '').trim();
|
|
1001
|
+
const chatTarget = chatGuid || chatIdentifier || sender;
|
|
1002
|
+
|
|
1003
|
+
if (!rowId || rowId <= lastRowId) continue;
|
|
1004
|
+
lastRowId = rowId;
|
|
1005
|
+
if (!text) continue;
|
|
1006
|
+
if (!chatTarget) continue;
|
|
1007
|
+
|
|
1008
|
+
if (allowedSenders.length && !allowedSenders.includes(sender)) {
|
|
1009
|
+
log('INFO', `[IMESSAGE] Ignored message from ${sender} (not in allowed_senders)`);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (allowedChats.length && !allowedChats.includes(chatTarget) && !allowedChats.includes(chatIdentifier)) {
|
|
1013
|
+
log('INFO', `[IMESSAGE] Ignored chat ${chatTarget} (${chatName || sender || 'unknown'}) not in allowed_chat_ids`);
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const chatId = chatTarget;
|
|
1018
|
+
const liveCfg = loadConfig();
|
|
1019
|
+
const bot = getBot(chatTarget);
|
|
1020
|
+
|
|
1021
|
+
// Echo fingerprint check — skip if this text matches something we recently sent
|
|
1022
|
+
if (bot.isEcho && bot.isEcho(text)) {
|
|
1023
|
+
log('INFO', `[IMESSAGE] Skipped echo: "${text.slice(0, 40)}"`);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const trimmedText = text.trim();
|
|
1028
|
+
let commandText = text;
|
|
1029
|
+
|
|
1030
|
+
log('INFO', `[IMESSAGE] Received chat=${chatTarget} sender=${sender || 'unknown'} name=${chatName || '-'}: "${text.slice(0, 60)}"`);
|
|
1031
|
+
|
|
1032
|
+
const acl = await applyUserAcl({
|
|
1033
|
+
bot,
|
|
1034
|
+
chatId,
|
|
1035
|
+
text,
|
|
1036
|
+
config: liveCfg,
|
|
1037
|
+
senderId: sender,
|
|
1038
|
+
bypassAcl: false,
|
|
1039
|
+
});
|
|
1040
|
+
if (acl.blocked) continue;
|
|
1041
|
+
|
|
1042
|
+
const { project: _boundProj } = _getBoundProject(chatId, liveCfg);
|
|
1043
|
+
const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
|
|
1044
|
+
const _st = loadState();
|
|
1045
|
+
const _chatKey = String(chatId);
|
|
1046
|
+
const _setSticky = (key) => {
|
|
1047
|
+
if (!_st.team_sticky) _st.team_sticky = {};
|
|
1048
|
+
_st.team_sticky[_chatKey] = key;
|
|
1049
|
+
saveState(_st);
|
|
1050
|
+
};
|
|
1051
|
+
const _clearSticky = () => {
|
|
1052
|
+
if (_st.team_sticky) delete _st.team_sticky[_chatKey];
|
|
1053
|
+
saveState(_st);
|
|
1054
|
+
};
|
|
1055
|
+
const _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
|
|
1056
|
+
|
|
1057
|
+
if (_boundProj && Array.isArray(_boundProj.team) && _boundProj.team.length > 0 && !_isTeamSlashCmd) {
|
|
1058
|
+
const _stopMatch = trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
|
|
1059
|
+
if (_stopMatch) {
|
|
1060
|
+
const _stopArg = (_stopMatch[1] || '').trim();
|
|
1061
|
+
let _targetKey = null;
|
|
1062
|
+
if (_stopArg) {
|
|
1063
|
+
const _sa = _stopArg.toLowerCase();
|
|
1064
|
+
const m = _boundProj.team.find(t =>
|
|
1065
|
+
(t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
|
|
1066
|
+
);
|
|
1067
|
+
if (m) _targetKey = m.key;
|
|
1068
|
+
}
|
|
1069
|
+
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
1070
|
+
if (_targetKey) {
|
|
1071
|
+
const vid = `_agent_${_targetKey}`;
|
|
1072
|
+
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
1073
|
+
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
1074
|
+
if (messageQueue.has(vid)) {
|
|
1075
|
+
const vq = messageQueue.get(vid);
|
|
1076
|
+
if (vq && vq.timer) clearTimeout(vq.timer);
|
|
1077
|
+
messageQueue.delete(vid);
|
|
1078
|
+
}
|
|
1079
|
+
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
1080
|
+
if (vproc && vproc.child) {
|
|
1081
|
+
vproc.aborted = true;
|
|
1082
|
+
const sig = vproc.killSignal || 'SIGTERM';
|
|
1083
|
+
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
1084
|
+
await bot.sendMessage(chatId, `Stopping ${label}...`);
|
|
1085
|
+
} else {
|
|
1086
|
+
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
1087
|
+
}
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (_stopArg) {
|
|
1091
|
+
await bot.sendMessage(chatId, `未找到团队成员: ${_stopArg}`);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
|
|
1097
|
+
if (teamMatch) {
|
|
1098
|
+
const { member, rest } = teamMatch;
|
|
1099
|
+
_setSticky(member.key);
|
|
1100
|
+
if (!rest) {
|
|
1101
|
+
await bot.sendMessage(chatId, `${member.icon || '🤖'} ${member.name} 在线`);
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
log('INFO', `[IMESSAGE] Team route ${chatId} -> ${member.key}`);
|
|
1105
|
+
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const _mainNicks = Array.isArray(_boundProj.nicknames) ? _boundProj.nicknames : [];
|
|
1110
|
+
const _trimLower = trimmedText.toLowerCase();
|
|
1111
|
+
const _mainMatch = _mainNicks.find(n =>
|
|
1112
|
+
_trimLower === n.toLowerCase()
|
|
1113
|
+
|| _trimLower.startsWith(n.toLowerCase() + ' ')
|
|
1114
|
+
|| _trimLower.startsWith(n.toLowerCase() + ',')
|
|
1115
|
+
|| _trimLower.startsWith(n.toLowerCase() + ',')
|
|
1116
|
+
);
|
|
1117
|
+
if (_mainMatch) {
|
|
1118
|
+
_clearSticky();
|
|
1119
|
+
const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
|
|
1120
|
+
if (!rest) {
|
|
1121
|
+
await bot.sendMessage(chatId, `${_boundProj.icon || '🤖'} ${_boundProj.name || 'Agent'} 在线`);
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
commandText = rest;
|
|
1125
|
+
} else if (_stickyKey) {
|
|
1126
|
+
const member = _boundProj.team.find(m => m.key === _stickyKey);
|
|
1127
|
+
if (member) {
|
|
1128
|
+
log('INFO', `[IMESSAGE] Sticky route ${chatId} -> ${member.key}`);
|
|
1129
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
handleCommand(bot, chatId, commandText, liveCfg, executeTaskByName, sender, false)
|
|
1136
|
+
.catch(e => log('ERROR', `[IMESSAGE] handleCommand error: ${e.message}`));
|
|
1137
|
+
}
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
log('WARN', `[IMESSAGE] poll error: ${e.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
processing = false;
|
|
1142
|
+
}, pollMs);
|
|
1143
|
+
|
|
1144
|
+
return {
|
|
1145
|
+
stop: () => { running = false; clearInterval(timer); },
|
|
1146
|
+
bot: imessageIO.createImessageBot(selfId, log),
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// ── Siri HTTP Bridge ────────────────────────────────────────────────────────
|
|
1151
|
+
function startSiriBridge(config, executeTaskByName) {
|
|
1152
|
+
if (!siriBridgeMod) { log('WARN', '[SIRI] daemon-siri-bridge module not found'); return null; }
|
|
1153
|
+
const bridge = siriBridgeMod.createSiriBridge({ log, loadConfig, handleCommand });
|
|
1154
|
+
return bridge.startSiriBridge(config, executeTaskByName);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return { startTelegramBridge, startFeishuBridge, startImessageBridge, startSiriBridge };
|
|
733
1158
|
}
|
|
734
1159
|
|
|
735
1160
|
module.exports = { createBridgeStarter };
|