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.
Files changed (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
@@ -2,6 +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');
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; } })();
5
9
 
6
10
  function createBridgeStarter(deps) {
7
11
  const {
@@ -14,10 +18,13 @@ function createBridgeStarter(deps) {
14
18
  loadState,
15
19
  saveState,
16
20
  getSession,
21
+ restoreSessionFromReply,
17
22
  handleCommand,
18
23
  pendingActivations, // optional — used to show smart activation hint
19
24
  activeProcesses, // optional — used for auto-dispatch to clones
20
25
  messageQueue, // optional — used for /stop to clear queued messages
26
+ sendRemoteDispatch, // optional — send packet to remote peer via relay chat
27
+ handleRemoteDispatchMessage, // optional — intercept relay chat messages
21
28
  } = deps;
22
29
 
23
30
  async function sendAclReply(bot, chatId, text) {
@@ -78,7 +85,7 @@ function createBridgeStarter(deps) {
78
85
  return latest;
79
86
  }
80
87
 
81
- function unauthorizedMsg(chatId, useSend) {
88
+ function unauthorizedMsg(chatId) {
82
89
  const pending = getPendingActivationForChat(chatId);
83
90
  if (pending) {
84
91
  return `⚠️ 此群未授权\n\n发送以下命令激活 Agent「${pending.agentName}」:\n\`/activate\``;
@@ -86,40 +93,81 @@ function createBridgeStarter(deps) {
86
93
  return '⚠️ 此群未授权\n\n如已创建 Agent,发送 `/activate` 完成绑定。\n否则请先在主群创建 Agent。';
87
94
  }
88
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
+
89
155
  // ── Team group helpers ─────────────────────────────────────────────────
90
156
  function _getBoundProject(chatId, cfg) {
91
157
  const map = {
92
- ...(cfg.telegram ? cfg.telegram.chat_agent_map || {} : {}),
93
- ...(cfg.feishu ? cfg.feishu.chat_agent_map || {} : {}),
158
+ ...(cfg.telegram ? cfg.telegram.chat_agent_map || {} : {}),
159
+ ...(cfg.feishu ? cfg.feishu.chat_agent_map || {} : {}),
160
+ ...(cfg.imessage ? cfg.imessage.chat_agent_map || {} : {}),
94
161
  };
95
162
  const key = map[String(chatId)];
96
163
  const proj = key && cfg.projects ? cfg.projects[key] : null;
97
164
  return { key: key || null, project: proj || null };
98
165
  }
99
-
100
- function _escapeRe(s) {
101
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
102
- }
103
-
104
- function _findTeamMember(text, team) {
105
- const t = String(text || '').trim();
106
- for (const member of team) {
107
- const nicks = Array.isArray(member.nicknames) ? member.nicknames : [];
108
- for (const nick of nicks) {
109
- const n = String(nick || '').trim();
110
- if (!n) continue;
111
- if (t.toLowerCase() === n.toLowerCase()) return { member, rest: '' };
112
- const re = new RegExp(`^${_escapeRe(n)}[\\s,,、::]+`, 'i');
113
- const m = t.match(re);
114
- if (m) return { member, rest: t.slice(m[0].length).trim() };
115
- }
116
- }
117
- return null;
118
- }
166
+ // _findTeamMember is imported from team-dispatch.js (shared with admin-commands)
119
167
 
120
168
  // Creates a bot proxy that redirects all send methods to replyChatId
121
169
  function _createTeamProxyBot(bot, replyChatId) {
122
- 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']);
123
171
  return new Proxy(bot, {
124
172
  get(target, prop) {
125
173
  const orig = target[prop];
@@ -207,6 +255,26 @@ function createBridgeStarter(deps) {
207
255
  }
208
256
 
209
257
  function _dispatchToTeamMember(member, boundProj, text, cfg, bot, realChatId, executeTaskByName, acl) {
258
+ // Remote member → send via relay chat
259
+ if (isRemoteMember(member) && sendRemoteDispatch) {
260
+ sendRemoteDispatch({
261
+ type: 'task',
262
+ to_peer: member.peer,
263
+ target_project: member.key,
264
+ prompt: text,
265
+ source_chat_id: String(realChatId),
266
+ source_sender_key: acl.senderId || 'user',
267
+ source_sender_id: acl.senderId || '',
268
+ }, cfg).then(res => {
269
+ if (res.success) {
270
+ bot.sendMessage(realChatId, `📡 已发送给 ${member.icon || '🤖'} ${member.name} (${member.peer})`).catch(() => {});
271
+ } else {
272
+ bot.sendMessage(realChatId, `❌ 远端派发失败: ${res.error}`).catch(() => {});
273
+ }
274
+ });
275
+ return;
276
+ }
277
+
210
278
  const virtualChatId = `_agent_${member.key}`;
211
279
  const parentCwd = member.cwd || boundProj.cwd;
212
280
  const resolvedParentCwd = parentCwd.replace(/^~/, require('os').homedir());
@@ -256,12 +324,16 @@ function createBridgeStarter(deps) {
256
324
 
257
325
  let offset = 0;
258
326
  let running = true;
259
- const abortController = new AbortController();
327
+ let abortController = new AbortController();
328
+ let pollLoopActive = false;
329
+ let reconnectTimer = null;
260
330
 
261
- const pollLoop = async () => {
262
- while (running) {
263
- try {
264
- const updates = await bot.getUpdates(offset, 30, abortController.signal);
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);
265
337
  for (const update of updates) {
266
338
  offset = update.update_id + 1;
267
339
 
@@ -373,11 +445,29 @@ function createBridgeStarter(deps) {
373
445
 
374
446
  // Team group routing for Telegram (same logic as Feishu)
375
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;
376
452
  const { key: _boundKey, project: _boundProj } = _getBoundProject(chatId, liveCfg);
377
453
  const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
378
454
 
379
455
  // Load sticky state
380
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
+ }
381
471
  const _chatKey = String(chatId);
382
472
  const _setSticky = (key) => {
383
473
  if (!_st.team_sticky) _st.team_sticky = {};
@@ -395,24 +485,55 @@ function createBridgeStarter(deps) {
395
485
  const _stopMatch = trimmedText && trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
396
486
  if (_stopMatch) {
397
487
  const _stopArg = (_stopMatch[1] || '').trim();
398
- if (_stopArg) {
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) {
399
494
  const _sa = _stopArg.toLowerCase();
400
495
  const m = _boundProj.team.find(t =>
401
496
  (t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
402
497
  );
403
- if (m) {
404
- _clearSticky();
405
- log('INFO', `Team /stop: ${_chatKey.slice(-8)} cleared sticky`);
406
- await bot.sendMessage(chatId, `⏹ 已切换回主 Agent`).catch(() => {});
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}...`);
407
516
  } else {
408
- await bot.sendMessage(chatId, `❌ 未找到团队成员: ${_stopArg}`).catch(() => {});
517
+ await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
409
518
  }
410
519
  continue;
411
520
  }
412
- // Bare /stop, clear sticky
413
- _clearSticky();
414
- await bot.sendMessage(chatId, `⏹ 已切换回主 Agent`).catch(() => {});
415
- continue;
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`);
416
537
  }
417
538
 
418
539
  // 1. Explicit nickname → route + set sticky
@@ -468,16 +589,21 @@ function createBridgeStarter(deps) {
468
589
  });
469
590
  }
470
591
  }
471
- } catch (e) {
472
- if (e.message === 'aborted') break;
473
- log('ERROR', `Telegram poll error: ${e.message}`);
474
- await sleep(5000);
592
+ } catch (e) {
593
+ if (e.message === 'aborted') break;
594
+ log('ERROR', `Telegram poll error: ${e.message}`);
595
+ await sleep(5000);
596
+ }
475
597
  }
598
+ } finally {
599
+ pollLoopActive = false;
476
600
  }
477
601
  };
478
602
 
479
603
  const startPoll = () => {
480
- pollLoop().catch(e => {
604
+ if (!running || pollLoopActive) return;
605
+ const signal = abortController.signal;
606
+ pollLoop(signal).catch(e => {
481
607
  if (e.message === 'aborted') return;
482
608
  log('ERROR', `pollLoop crashed: ${e.message} — restarting in 5s`);
483
609
  if (running) setTimeout(startPoll, 5000);
@@ -486,7 +612,24 @@ function createBridgeStarter(deps) {
486
612
  startPoll();
487
613
 
488
614
  return {
489
- stop() { running = false; abortController.abort(); },
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
+ },
490
633
  bot,
491
634
  };
492
635
  }
@@ -504,6 +647,18 @@ function createBridgeStarter(deps) {
504
647
  try {
505
648
  const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
506
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
+ }
656
+
657
+ // ── Remote dispatch interception (before ACL) ──
658
+ if (handleRemoteDispatchMessage && text) {
659
+ const handled = await handleRemoteDispatchMessage({ chatId, text, config: liveCfg });
660
+ if (handled) return;
661
+ }
507
662
 
508
663
  const allowedIds = (liveCfg.feishu && liveCfg.feishu.allowed_chat_ids) || [];
509
664
  const trimmedText = text && text.trim();
@@ -565,18 +720,31 @@ function createBridgeStarter(deps) {
565
720
  });
566
721
  if (acl.blocked) return;
567
722
  log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
568
- const parentId = event?.message?.parent_id;
723
+ const parentId = extractFeishuReplyMessageId(event);
569
724
  let _replyAgentKey = null;
570
725
  // Load state once for the entire routing block
571
726
  const _st = loadState();
727
+ if (parentId) {
728
+ log('INFO', `Feishu reply metadata detected chat=${chatId} parentId=${parentId}`);
729
+ }
572
730
  if (parentId) {
573
731
  const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
574
732
  if (mapped) {
575
- if (!_st.sessions) _st.sessions = {};
576
- _st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
577
- saveState(_st);
578
- log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
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
+ }
579
745
  _replyAgentKey = mapped.agentKey || null;
746
+ } else {
747
+ log('INFO', `Feishu reply parentId=${parentId} had no msg_sessions mapping`);
580
748
  }
581
749
  }
582
750
 
@@ -660,18 +828,28 @@ function createBridgeStarter(deps) {
660
828
  }
661
829
  // 1. Explicit nickname → route + set sticky
662
830
  const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
663
- if (teamMatch) {
664
- const { member, rest } = teamMatch;
665
- _setSticky(member.key);
666
- if (!rest) {
667
- // Pure nickname, no task — confirm member is online
668
- log('INFO', `Sticky set (pure nickname): ${_chatKey.slice(-8)} → ${member.key}`);
669
- bot.sendMarkdown(chatId, `${member.icon || '🤖'} **${member.name}** 在线`).catch(() => {});
670
- return;
671
- }
672
- log('INFO', `Sticky set: ${_chatKey.slice(-8)} → ${member.key}`);
673
- _dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
674
- return;
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;
675
853
  }
676
854
 
677
855
  // 1.5. Main project nickname → clear sticky, route to main
@@ -681,11 +859,22 @@ function createBridgeStarter(deps) {
681
859
  if (_mainMatch) {
682
860
  _clearSticky();
683
861
  const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
684
- log('INFO', `Main nickname → cleared sticky, routing to main${rest ? ` (task: ${rest.slice(0, 30)})` : ''}`);
685
- if (!rest) {
686
- bot.sendMarkdown(chatId, `${_boundProj.icon || '🤖'} **${_boundProj.name}** 在线`).catch(() => {});
687
- return;
688
- }
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
+ }
689
878
  try {
690
879
  await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
691
880
  } catch (e) {
@@ -719,7 +908,227 @@ function createBridgeStarter(deps) {
719
908
  }
720
909
  }
721
910
 
722
- return { startTelegramBridge, startFeishuBridge };
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 };
723
1132
  }
724
1133
 
725
1134
  module.exports = { createBridgeStarter };