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.
Files changed (40) hide show
  1. package/README.md +6 -1
  2. package/index.js +277 -55
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +17 -5
  6. package/scripts/daemon-admin-commands.js +264 -62
  7. package/scripts/daemon-agent-commands.js +188 -66
  8. package/scripts/daemon-bridges.js +447 -48
  9. package/scripts/daemon-claude-engine.js +650 -103
  10. package/scripts/daemon-command-router.js +134 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +2 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +106 -50
  15. package/scripts/daemon-file-browser.js +63 -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 +34 -2
  19. package/scripts/daemon-session-commands.js +102 -45
  20. package/scripts/daemon-session-store.js +497 -66
  21. package/scripts/daemon-siri-bridge.js +234 -0
  22. package/scripts/daemon-siri-imessage.js +209 -0
  23. package/scripts/daemon-task-scheduler.js +10 -2
  24. package/scripts/daemon.js +610 -181
  25. package/scripts/docs/hook-config.md +7 -4
  26. package/scripts/docs/maintenance-manual.md +8 -1
  27. package/scripts/feishu-adapter.js +7 -15
  28. package/scripts/hooks/doc-router.js +29 -0
  29. package/scripts/hooks/intent-doc-router.js +54 -0
  30. package/scripts/hooks/intent-engine.js +9 -40
  31. package/scripts/intent-registry.js +59 -0
  32. package/scripts/memory-extract.js +59 -0
  33. package/scripts/mentor-engine.js +6 -0
  34. package/scripts/schema.js +1 -0
  35. package/scripts/self-reflect.js +110 -12
  36. package/scripts/session-analytics.js +160 -0
  37. package/scripts/signal-capture.js +1 -1
  38. package/scripts/team-dispatch.js +150 -11
  39. package/scripts/hooks/intent-agent-manage.js +0 -50
  40. 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, useSend) {
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 ? cfg.telegram.chat_agent_map || {} : {}),
97
- ...(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 || {} : {}),
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
- const abortController = new AbortController();
327
+ let abortController = new AbortController();
328
+ let pollLoopActive = false;
329
+ let reconnectTimer = null;
264
330
 
265
- const pollLoop = async () => {
266
- while (running) {
267
- try {
268
- 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);
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
- 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) {
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
- _clearSticky();
409
- log('INFO', `Team /stop: ${_chatKey.slice(-8)} cleared sticky`);
410
- 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}...`);
411
516
  } else {
412
- await bot.sendMessage(chatId, `❌ 未找到团队成员: ${_stopArg}`).catch(() => {});
517
+ await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
413
518
  }
414
519
  continue;
415
520
  }
416
- // Bare /stop, clear sticky
417
- _clearSticky();
418
- await bot.sendMessage(chatId, `⏹ 已切换回主 Agent`).catch(() => {});
419
- 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`);
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
- } catch (e) {
476
- if (e.message === 'aborted') break;
477
- log('ERROR', `Telegram poll error: ${e.message}`);
478
- 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
+ }
479
597
  }
598
+ } finally {
599
+ pollLoopActive = false;
480
600
  }
481
601
  };
482
602
 
483
603
  const startPoll = () => {
484
- pollLoop().catch(e => {
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() { 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
+ },
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?.message?.parent_id;
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 (!_st.sessions) _st.sessions = {};
586
- _st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
587
- saveState(_st);
588
- 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
+ }
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
- if (teamMatch) {
674
- const { member, rest } = teamMatch;
675
- _setSticky(member.key);
676
- if (!rest) {
677
- // Pure nickname, no task — confirm member is online
678
- log('INFO', `Sticky set (pure nickname): ${_chatKey.slice(-8)} → ${member.key}`);
679
- bot.sendMarkdown(chatId, `${member.icon || '🤖'} **${member.name}** 在线`).catch(() => {});
680
- return;
681
- }
682
- log('INFO', `Sticky set: ${_chatKey.slice(-8)} → ${member.key}`);
683
- _dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
684
- 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;
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
- log('INFO', `Main nickname → cleared sticky, routing to main${rest ? ` (task: ${rest.slice(0, 30)})` : ''}`);
695
- if (!rest) {
696
- bot.sendMarkdown(chatId, `${_boundProj.icon || '🤖'} **${_boundProj.name}** 在线`).catch(() => {});
697
- return;
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
- 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 };
733
1132
  }
734
1133
 
735
1134
  module.exports = { createBridgeStarter };