metame-cli 1.5.2 → 1.5.3
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 +36 -0
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +2 -1
- package/scripts/daemon-admin-commands.js +164 -9
- package/scripts/daemon-agent-commands.js +17 -8
- package/scripts/daemon-bridges.js +385 -6
- package/scripts/daemon-claude-engine.js +172 -51
- package/scripts/daemon-command-router.js +33 -0
- package/scripts/daemon-default.yaml +4 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +2 -2
- package/scripts/daemon-remote-dispatch.js +60 -0
- package/scripts/daemon-session-commands.js +19 -11
- package/scripts/daemon-session-store.js +26 -8
- package/scripts/daemon-task-scheduler.js +2 -2
- package/scripts/daemon.js +272 -6
- package/scripts/daemon.yaml +349 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/maintenance-manual.md +62 -1
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/memory-extract.js +1 -1
- package/scripts/memory-write.js +21 -4
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
|
@@ -16,6 +16,8 @@ function createBridgeStarter(deps) {
|
|
|
16
16
|
getSession,
|
|
17
17
|
handleCommand,
|
|
18
18
|
pendingActivations, // optional — used to show smart activation hint
|
|
19
|
+
activeProcesses, // optional — used for auto-dispatch to clones
|
|
20
|
+
messageQueue, // optional — used for /stop to clear queued messages
|
|
19
21
|
} = deps;
|
|
20
22
|
|
|
21
23
|
async function sendAclReply(bot, chatId, text) {
|
|
@@ -84,6 +86,156 @@ function createBridgeStarter(deps) {
|
|
|
84
86
|
return '⚠️ 此群未授权\n\n如已创建 Agent,发送 `/activate` 完成绑定。\n否则请先在主群创建 Agent。';
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
// ── Team group helpers ─────────────────────────────────────────────────
|
|
90
|
+
function _getBoundProject(chatId, cfg) {
|
|
91
|
+
const map = {
|
|
92
|
+
...(cfg.telegram ? cfg.telegram.chat_agent_map || {} : {}),
|
|
93
|
+
...(cfg.feishu ? cfg.feishu.chat_agent_map || {} : {}),
|
|
94
|
+
};
|
|
95
|
+
const key = map[String(chatId)];
|
|
96
|
+
const proj = key && cfg.projects ? cfg.projects[key] : null;
|
|
97
|
+
return { key: key || null, project: proj || null };
|
|
98
|
+
}
|
|
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
|
+
}
|
|
119
|
+
|
|
120
|
+
// Creates a bot proxy that redirects all send methods to replyChatId
|
|
121
|
+
function _createTeamProxyBot(bot, replyChatId) {
|
|
122
|
+
const SEND = new Set(['sendMessage', 'sendMarkdown', 'sendCard', 'editMessage', 'deleteMessage', 'sendTyping', 'sendFile', 'sendButtonCard']);
|
|
123
|
+
return new Proxy(bot, {
|
|
124
|
+
get(target, prop) {
|
|
125
|
+
const orig = target[prop];
|
|
126
|
+
if (typeof orig !== 'function') return orig;
|
|
127
|
+
if (!SEND.has(prop)) return orig.bind(target);
|
|
128
|
+
return function(_chatId, ...args) { return orig.call(target, replyChatId, ...args); };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Get team member's working directory using subdir (not worktree).
|
|
133
|
+
// Creates agents/<key>/ directory and symlinks CLAUDE.md from parent.
|
|
134
|
+
function _getMemberCwd(parentCwd, key) {
|
|
135
|
+
const { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync } = require('fs');
|
|
136
|
+
const { execFileSync } = require('child_process');
|
|
137
|
+
const WIN_HIDE = process.platform === 'win32' ? { windowsHide: true } : {};
|
|
138
|
+
|
|
139
|
+
// Sanitize key to prevent path traversal
|
|
140
|
+
const safeKey = String(key).replace(/[^a-zA-Z0-9_\-]/g, '').slice(0, 50);
|
|
141
|
+
if (safeKey !== key) {
|
|
142
|
+
log('WARN', `Sanitized team member key: ${key} -> ${safeKey}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Use agents/<key>/ as the working directory
|
|
146
|
+
const agentsDir = path.join(parentCwd, 'agents');
|
|
147
|
+
const memberDir = path.join(agentsDir, safeKey);
|
|
148
|
+
|
|
149
|
+
// Create agents directory if not exists
|
|
150
|
+
if (!existsSync(agentsDir)) {
|
|
151
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create member directory if not exists
|
|
155
|
+
if (!existsSync(memberDir)) {
|
|
156
|
+
mkdirSync(memberDir, { recursive: true });
|
|
157
|
+
log('INFO', `Created agent directory: ${memberDir}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Initialize git for checkpoint support
|
|
161
|
+
const gitDir = path.join(memberDir, '.git');
|
|
162
|
+
if (!existsSync(gitDir)) {
|
|
163
|
+
try {
|
|
164
|
+
execFileSync('git', ['init'], { cwd: memberDir, stdio: 'ignore', ...WIN_HIDE });
|
|
165
|
+
log('INFO', `Git repo initialized: ${memberDir}`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
log('WARN', `Failed to init git for ${memberDir}: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Set up CLAUDE.md: use dedicated, or template, or symlink from parent
|
|
172
|
+
const claudeMd = path.join(memberDir, 'CLAUDE.md');
|
|
173
|
+
const parentClaudeMd = path.join(parentCwd, 'CLAUDE.md');
|
|
174
|
+
if (!existsSync(claudeMd)) {
|
|
175
|
+
// Priority 1: dedicated CLAUDE.md in agents/<key>/ directory
|
|
176
|
+
const dedicatedPath = path.join(parentCwd, 'agents', safeKey, 'CLAUDE.md');
|
|
177
|
+
if (existsSync(dedicatedPath)) {
|
|
178
|
+
try {
|
|
179
|
+
// Copy instead of symlink to avoid cross-device issues
|
|
180
|
+
const content = readFileSync(dedicatedPath, 'utf8');
|
|
181
|
+
writeFileSync(claudeMd, content, 'utf8');
|
|
182
|
+
log('INFO', `Copied dedicated CLAUDE.md for ${safeKey}`);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
log('WARN', `Failed to copy CLAUDE.md for ${safeKey}: ${e.message}`);
|
|
185
|
+
}
|
|
186
|
+
} else if (existsSync(parentClaudeMd)) {
|
|
187
|
+
// Priority 2: symlink to parent CLAUDE.md
|
|
188
|
+
try {
|
|
189
|
+
// Use 'junction' on Windows for directories, 'file' for files
|
|
190
|
+
const linkType = process.platform === 'win32' ? 'junction' : 'file';
|
|
191
|
+
symlinkSync(parentClaudeMd, claudeMd, linkType);
|
|
192
|
+
log('INFO', `Symlinked CLAUDE.md for ${safeKey}`);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// Fallback: copy file
|
|
195
|
+
try {
|
|
196
|
+
const content = readFileSync(parentClaudeMd, 'utf8');
|
|
197
|
+
writeFileSync(claudeMd, content, 'utf8');
|
|
198
|
+
log('INFO', `Copied CLAUDE.md for ${safeKey} (symlink failed)`);
|
|
199
|
+
} catch (e2) {
|
|
200
|
+
log('WARN', `Failed to create CLAUDE.md for ${safeKey}: ${e2.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return memberDir;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _dispatchToTeamMember(member, boundProj, text, cfg, bot, realChatId, executeTaskByName, acl) {
|
|
210
|
+
const virtualChatId = `_agent_${member.key}`;
|
|
211
|
+
const parentCwd = member.cwd || boundProj.cwd;
|
|
212
|
+
const resolvedParentCwd = parentCwd.replace(/^~/, require('os').homedir());
|
|
213
|
+
const memberCwd = _getMemberCwd(resolvedParentCwd, member.key);
|
|
214
|
+
if (!memberCwd) {
|
|
215
|
+
log('ERROR', `Team [${member.key}] cannot start: directory unavailable`);
|
|
216
|
+
bot.sendMessage(realChatId, `❌ ${member.icon || '🤖'} ${member.name} 启动失败:工作目录创建失败`).catch(() => {});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
log('INFO', `Team [${member.key}] using cwd: ${memberCwd}`);
|
|
220
|
+
const teamCfg = {
|
|
221
|
+
...cfg,
|
|
222
|
+
projects: {
|
|
223
|
+
...(cfg.projects || {}),
|
|
224
|
+
[member.key]: {
|
|
225
|
+
cwd: memberCwd,
|
|
226
|
+
name: member.name,
|
|
227
|
+
icon: member.icon || '🤖',
|
|
228
|
+
color: member.color || 'blue',
|
|
229
|
+
engine: member.engine || boundProj.engine,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const proxyBot = _createTeamProxyBot(bot, realChatId);
|
|
234
|
+
handleCommand(proxyBot, virtualChatId, text, teamCfg, executeTaskByName, acl.senderId, acl.readOnly)
|
|
235
|
+
.catch(e => log('ERROR', `Team [${member.key}] error: ${e.message}`));
|
|
236
|
+
}
|
|
237
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
87
239
|
async function startTelegramBridge(config, executeTaskByName) {
|
|
88
240
|
if (!config.telegram || !config.telegram.enabled) return null;
|
|
89
241
|
if (!config.telegram.bot_token) {
|
|
@@ -218,6 +370,99 @@ function createBridgeStarter(deps) {
|
|
|
218
370
|
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
219
371
|
});
|
|
220
372
|
if (acl.blocked) continue;
|
|
373
|
+
|
|
374
|
+
// Team group routing for Telegram (same logic as Feishu)
|
|
375
|
+
const trimmedText = text.trim();
|
|
376
|
+
const { key: _boundKey, project: _boundProj } = _getBoundProject(chatId, liveCfg);
|
|
377
|
+
const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
|
|
378
|
+
|
|
379
|
+
// Load sticky state
|
|
380
|
+
const _st = loadState();
|
|
381
|
+
const _chatKey = String(chatId);
|
|
382
|
+
const _setSticky = (key) => {
|
|
383
|
+
if (!_st.team_sticky) _st.team_sticky = {};
|
|
384
|
+
_st.team_sticky[_chatKey] = key;
|
|
385
|
+
saveState(_st);
|
|
386
|
+
};
|
|
387
|
+
const _clearSticky = () => {
|
|
388
|
+
if (_st.team_sticky) delete _st.team_sticky[_chatKey];
|
|
389
|
+
saveState(_st);
|
|
390
|
+
};
|
|
391
|
+
const _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
|
|
392
|
+
|
|
393
|
+
if (_boundProj && Array.isArray(_boundProj.team) && _boundProj.team.length > 0 && !_isTeamSlashCmd) {
|
|
394
|
+
// Team dispatch logic (same as Feishu)
|
|
395
|
+
const _stopMatch = trimmedText && trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
|
|
396
|
+
if (_stopMatch) {
|
|
397
|
+
const _stopArg = (_stopMatch[1] || '').trim();
|
|
398
|
+
if (_stopArg) {
|
|
399
|
+
const _sa = _stopArg.toLowerCase();
|
|
400
|
+
const m = _boundProj.team.find(t =>
|
|
401
|
+
(t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
|
|
402
|
+
);
|
|
403
|
+
if (m) {
|
|
404
|
+
_clearSticky();
|
|
405
|
+
log('INFO', `Team /stop: ${_chatKey.slice(-8)} → cleared sticky`);
|
|
406
|
+
await bot.sendMessage(chatId, `⏹ 已切换回主 Agent`).catch(() => {});
|
|
407
|
+
} else {
|
|
408
|
+
await bot.sendMessage(chatId, `❌ 未找到团队成员: ${_stopArg}`).catch(() => {});
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// Bare /stop, clear sticky
|
|
413
|
+
_clearSticky();
|
|
414
|
+
await bot.sendMessage(chatId, `⏹ 已切换回主 Agent`).catch(() => {});
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 1. Explicit nickname → route + set sticky
|
|
419
|
+
const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
|
|
420
|
+
if (teamMatch) {
|
|
421
|
+
const { member, rest } = teamMatch;
|
|
422
|
+
_setSticky(member.key);
|
|
423
|
+
if (!rest) {
|
|
424
|
+
log('INFO', `Sticky set (pure nickname): ${_chatKey.slice(-8)} → ${member.key}`);
|
|
425
|
+
bot.sendMarkdown(chatId, `${member.icon || '🤖'} **${member.name}** 在线`).catch(() => {});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
log('INFO', `Sticky set: ${_chatKey.slice(-8)} → ${member.key}`);
|
|
429
|
+
_dispatchToTeamMember(member, _boundProj, rest, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 1.5. Main project nickname → clear sticky, route to main
|
|
434
|
+
const _mainNicks = Array.isArray(_boundProj.nicknames) ? _boundProj.nicknames : [];
|
|
435
|
+
const _trimLower = trimmedText.toLowerCase();
|
|
436
|
+
const _mainMatch = _mainNicks.find(n => _trimLower === n.toLowerCase() || _trimLower.startsWith(n.toLowerCase() + ' ') || _trimLower.startsWith(n.toLowerCase() + ',') || _trimLower.startsWith(n.toLowerCase() + ','));
|
|
437
|
+
if (_mainMatch) {
|
|
438
|
+
_clearSticky();
|
|
439
|
+
const rest = trimmedText.slice(_mainMatch.length).replace(/^[\s,,::]+/, '');
|
|
440
|
+
log('INFO', `Main nickname → cleared sticky, routing to main${rest ? ` (task: ${rest.slice(0, 30)})` : ''}`);
|
|
441
|
+
if (!rest) {
|
|
442
|
+
bot.sendMarkdown(chatId, `${_boundProj.icon || '🤖'} **${_boundProj.name}** 在线`).catch(() => {});
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
|
|
449
|
+
bot.sendMessage(chatId, `❌ 执行失败: ${e.message}`).catch(() => {});
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 2. Sticky: no nickname given → route to last explicitly named member
|
|
455
|
+
if (_stickyKey) {
|
|
456
|
+
const member = _boundProj.team.find(m => m.key === _stickyKey);
|
|
457
|
+
if (member) {
|
|
458
|
+
log('INFO', `Sticky route: → ${_stickyKey}`);
|
|
459
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Default: route to main project
|
|
221
466
|
handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
|
|
222
467
|
log('ERROR', `Telegram handler error: ${e.message}`);
|
|
223
468
|
});
|
|
@@ -255,9 +500,11 @@ function createBridgeStarter(deps) {
|
|
|
255
500
|
|
|
256
501
|
const { createBot } = require('./feishu-adapter.js');
|
|
257
502
|
const bot = createBot(config.feishu);
|
|
503
|
+
|
|
258
504
|
try {
|
|
259
505
|
const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
|
|
260
506
|
const liveCfg = loadConfig();
|
|
507
|
+
|
|
261
508
|
const allowedIds = (liveCfg.feishu && liveCfg.feishu.allowed_chat_ids) || [];
|
|
262
509
|
const trimmedText = text && text.trim();
|
|
263
510
|
const isBindCmd = trimmedText && (
|
|
@@ -319,21 +566,153 @@ function createBridgeStarter(deps) {
|
|
|
319
566
|
if (acl.blocked) return;
|
|
320
567
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|
|
321
568
|
const parentId = event?.message?.parent_id;
|
|
569
|
+
let _replyAgentKey = null;
|
|
570
|
+
// Load state once for the entire routing block
|
|
571
|
+
const _st = loadState();
|
|
322
572
|
if (parentId) {
|
|
323
|
-
const
|
|
324
|
-
const mapped = st.msg_sessions && st.msg_sessions[parentId];
|
|
573
|
+
const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
|
|
325
574
|
if (mapped) {
|
|
326
|
-
|
|
327
|
-
|
|
575
|
+
if (!_st.sessions) _st.sessions = {};
|
|
576
|
+
_st.sessions[chatId] = { id: mapped.id, cwd: mapped.cwd, started: true };
|
|
577
|
+
saveState(_st);
|
|
328
578
|
log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
579
|
+
_replyAgentKey = mapped.agentKey || null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Helper: set/clear sticky on shared state object and persist
|
|
584
|
+
const _chatKey = String(chatId);
|
|
585
|
+
const _setSticky = (key) => {
|
|
586
|
+
if (!_st.team_sticky) _st.team_sticky = {};
|
|
587
|
+
_st.team_sticky[_chatKey] = key;
|
|
588
|
+
saveState(_st);
|
|
589
|
+
};
|
|
590
|
+
const _clearSticky = () => {
|
|
591
|
+
if (_st.team_sticky) delete _st.team_sticky[_chatKey];
|
|
592
|
+
saveState(_st);
|
|
593
|
+
};
|
|
594
|
+
const _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
|
|
595
|
+
|
|
596
|
+
// Team group routing: if bound project has a team array, check message for member nickname
|
|
597
|
+
// Non-/stop slash commands bypass team routing → handled by main project
|
|
598
|
+
const { key: _boundKey, project: _boundProj } = _getBoundProject(chatId, liveCfg);
|
|
599
|
+
const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
|
|
600
|
+
if (_boundProj && Array.isArray(_boundProj.team) && _boundProj.team.length > 0 && !_isTeamSlashCmd) {
|
|
601
|
+
// ── /stop precise routing for team groups ──
|
|
602
|
+
const _stopMatch = trimmedText && trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
|
|
603
|
+
if (_stopMatch) {
|
|
604
|
+
const _stopArg = (_stopMatch[1] || '').trim();
|
|
605
|
+
let _targetKey = null;
|
|
606
|
+
// Priority 1: quoted reply → stop that agent
|
|
607
|
+
if (_replyAgentKey) {
|
|
608
|
+
const m = _boundProj.team.find(t => t.key === _replyAgentKey);
|
|
609
|
+
if (m) _targetKey = m.key;
|
|
610
|
+
}
|
|
611
|
+
// Priority 2: /stop <nickname> → match team member (case-insensitive)
|
|
612
|
+
if (!_targetKey && _stopArg) {
|
|
613
|
+
const _sa = _stopArg.toLowerCase();
|
|
614
|
+
const m = _boundProj.team.find(t =>
|
|
615
|
+
(t.nicknames || []).some(n => n.toLowerCase() === _sa) || (t.name && t.name.toLowerCase() === _sa) || t.key === _sa
|
|
616
|
+
);
|
|
617
|
+
if (m) _targetKey = m.key;
|
|
618
|
+
}
|
|
619
|
+
// Priority 3: bare /stop → sticky
|
|
620
|
+
if (!_targetKey && !_stopArg) _targetKey = _stickyKey;
|
|
621
|
+
if (_targetKey) {
|
|
622
|
+
const vid = `_agent_${_targetKey}`;
|
|
623
|
+
const member = _boundProj.team.find(t => t.key === _targetKey);
|
|
624
|
+
const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
|
|
625
|
+
// Clear message queue for this virtual agent
|
|
626
|
+
if (messageQueue.has(vid)) {
|
|
627
|
+
const vq = messageQueue.get(vid);
|
|
628
|
+
if (vq && vq.timer) clearTimeout(vq.timer);
|
|
629
|
+
messageQueue.delete(vid);
|
|
630
|
+
}
|
|
631
|
+
const vproc = activeProcesses && activeProcesses.get(vid);
|
|
632
|
+
if (vproc && vproc.child) {
|
|
633
|
+
vproc.aborted = true;
|
|
634
|
+
const sig = vproc.killSignal || 'SIGTERM';
|
|
635
|
+
try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
|
|
636
|
+
await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
|
|
637
|
+
} else {
|
|
638
|
+
await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// /stop <bad-nickname> → no match, report error instead of falling through
|
|
643
|
+
if (_stopArg) {
|
|
644
|
+
await bot.sendMessage(chatId, `❌ 未找到团队成员: ${_stopArg}`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Bare /stop, no sticky set → fall through to handleCommand
|
|
329
648
|
}
|
|
649
|
+
|
|
650
|
+
// 0. Quoted reply → force route + set sticky
|
|
651
|
+
if (_replyAgentKey) {
|
|
652
|
+
const member = _boundProj.team.find(m => m.key === _replyAgentKey);
|
|
653
|
+
if (member) {
|
|
654
|
+
_setSticky(member.key);
|
|
655
|
+
log('INFO', `Quoted reply → force route to ${_replyAgentKey} (sticky set)`);
|
|
656
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
log('INFO', `Quoted reply agentKey=${_replyAgentKey} not in team, falling through`);
|
|
660
|
+
}
|
|
661
|
+
// 1. Explicit nickname → route + set sticky
|
|
662
|
+
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;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 1.5. Main project nickname → clear sticky, route to main
|
|
678
|
+
const _mainNicks = Array.isArray(_boundProj.nicknames) ? _boundProj.nicknames : [];
|
|
679
|
+
const _trimLower = trimmedText.toLowerCase();
|
|
680
|
+
const _mainMatch = _mainNicks.find(n => _trimLower === n.toLowerCase() || _trimLower.startsWith(n.toLowerCase() + ' ') || _trimLower.startsWith(n.toLowerCase() + ',') || _trimLower.startsWith(n.toLowerCase() + ','));
|
|
681
|
+
if (_mainMatch) {
|
|
682
|
+
_clearSticky();
|
|
683
|
+
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
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
691
|
+
} catch (e) {
|
|
692
|
+
log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
|
|
693
|
+
bot.sendMessage(chatId, `❌ 执行失败: ${e.message}`).catch(() => {});
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 2. Sticky: no nickname given → route to last explicitly named member
|
|
699
|
+
if (_stickyKey) {
|
|
700
|
+
const member = _boundProj.team.find(m => m.key === _stickyKey);
|
|
701
|
+
if (member) {
|
|
702
|
+
log('INFO', `Sticky route: → ${_stickyKey}`);
|
|
703
|
+
_dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
330
708
|
}
|
|
709
|
+
|
|
331
710
|
await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
|
|
332
711
|
}
|
|
333
|
-
});
|
|
712
|
+
}, { log: (lvl, msg) => log(lvl, msg) });
|
|
334
713
|
|
|
335
714
|
log('INFO', 'Feishu bot connected (WebSocket long connection)');
|
|
336
|
-
return { stop: () => receiver.stop(), bot };
|
|
715
|
+
return { stop: () => receiver.stop(), bot, reconnect: () => receiver.reconnect(), isAlive: () => receiver.isAlive() };
|
|
337
716
|
} catch (e) {
|
|
338
717
|
log('ERROR', `Feishu bridge failed: ${e.message}`);
|
|
339
718
|
return null;
|