metame-cli 1.5.3 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
|
@@ -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
|
|
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
|
|
93
|
-
...(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 || {} : {}),
|
|
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
|
-
|
|
327
|
+
let abortController = new AbortController();
|
|
328
|
+
let pollLoopActive = false;
|
|
329
|
+
let reconnectTimer = null;
|
|
260
330
|
|
|
261
|
-
const pollLoop = async () => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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,
|
|
517
|
+
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
409
518
|
}
|
|
410
519
|
continue;
|
|
411
520
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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() {
|
|
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
|
|
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 (
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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 };
|