metame-cli 1.3.2 → 1.3.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/index.js +132 -50
- package/package.json +1 -1
- package/scripts/daemon.js +84 -16
- package/scripts/feishu-adapter.js +23 -2
package/index.js
CHANGED
|
@@ -571,11 +571,12 @@ if (isDaemon) {
|
|
|
571
571
|
const DAEMON_SCRIPT = path.join(METAME_DIR, 'daemon.js');
|
|
572
572
|
|
|
573
573
|
if (subCmd === 'init') {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
574
|
+
const readline = require('readline');
|
|
575
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
576
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
577
|
+
|
|
578
|
+
// Create config from template if not exists
|
|
579
|
+
if (!fs.existsSync(DAEMON_CONFIG)) {
|
|
579
580
|
const templateSrc = fs.existsSync(DAEMON_DEFAULT)
|
|
580
581
|
? DAEMON_DEFAULT
|
|
581
582
|
: path.join(METAME_DIR, 'daemon-default.yaml');
|
|
@@ -585,45 +586,113 @@ if (isDaemon) {
|
|
|
585
586
|
console.error("❌ Template not found. Reinstall MetaMe.");
|
|
586
587
|
process.exit(1);
|
|
587
588
|
}
|
|
588
|
-
// Ensure directory permissions (700)
|
|
589
589
|
try { fs.chmodSync(METAME_DIR, 0o700); } catch { /* ignore on Windows */ }
|
|
590
|
-
console.log("✅
|
|
591
|
-
|
|
590
|
+
console.log("✅ Config created: ~/.metame/daemon.yaml\n");
|
|
591
|
+
} else {
|
|
592
|
+
console.log("✅ Config exists: ~/.metame/daemon.yaml\n");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const yaml = require(path.join(__dirname, 'node_modules', 'js-yaml'));
|
|
596
|
+
let cfg = yaml.load(fs.readFileSync(DAEMON_CONFIG, 'utf8')) || {};
|
|
597
|
+
|
|
598
|
+
// --- Telegram Setup ---
|
|
599
|
+
console.log("━━━ 📱 Telegram Setup ━━━");
|
|
600
|
+
console.log("Steps:");
|
|
601
|
+
console.log(" 1. Open Telegram, search @BotFather");
|
|
602
|
+
console.log(" 2. Send /newbot, follow prompts to create a bot");
|
|
603
|
+
console.log(" 3. Copy the bot token (looks like: 123456:ABC-DEF...)");
|
|
604
|
+
console.log("");
|
|
605
|
+
|
|
606
|
+
const tgToken = (await ask("Paste your Telegram bot token (Enter to skip): ")).trim();
|
|
607
|
+
if (tgToken) {
|
|
608
|
+
if (!cfg.telegram) cfg.telegram = {};
|
|
609
|
+
cfg.telegram.enabled = true;
|
|
610
|
+
cfg.telegram.bot_token = tgToken;
|
|
611
|
+
|
|
612
|
+
console.log("\nFinding your chat ID...");
|
|
613
|
+
console.log(" → Send any message to your bot in Telegram first, then press Enter.");
|
|
614
|
+
await ask("Press Enter after you've messaged your bot: ");
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const https = require('https');
|
|
618
|
+
const chatIds = await new Promise((resolve, reject) => {
|
|
619
|
+
https.get(`https://api.telegram.org/bot${tgToken}/getUpdates`, (res) => {
|
|
620
|
+
let body = '';
|
|
621
|
+
res.on('data', d => body += d);
|
|
622
|
+
res.on('end', () => {
|
|
623
|
+
try {
|
|
624
|
+
const data = JSON.parse(body);
|
|
625
|
+
const ids = new Set();
|
|
626
|
+
if (data.result) {
|
|
627
|
+
for (const u of data.result) {
|
|
628
|
+
if (u.message && u.message.chat) ids.add(u.message.chat.id);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
resolve([...ids]);
|
|
632
|
+
} catch { resolve([]); }
|
|
633
|
+
});
|
|
634
|
+
}).on('error', () => resolve([]));
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (chatIds.length > 0) {
|
|
638
|
+
cfg.telegram.allowed_chat_ids = chatIds;
|
|
639
|
+
console.log(` ✅ Found chat ID(s): ${chatIds.join(', ')}`);
|
|
640
|
+
} else {
|
|
641
|
+
console.log(" ⚠️ No messages found. Make sure you messaged the bot.");
|
|
642
|
+
console.log(" You can set allowed_chat_ids manually in daemon.yaml later.");
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
console.log(" ⚠️ Could not fetch chat ID. Set it manually in daemon.yaml.");
|
|
646
|
+
}
|
|
647
|
+
console.log(" ✅ Telegram configured!\n");
|
|
648
|
+
} else {
|
|
649
|
+
console.log(" Skipped.\n");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// --- Feishu Setup ---
|
|
653
|
+
console.log("━━━ 📘 Feishu (Lark) Setup ━━━");
|
|
654
|
+
console.log("Steps:");
|
|
655
|
+
console.log(" 1. Go to: https://open.feishu.cn/app");
|
|
656
|
+
console.log(" → Create App (企业自建应用)");
|
|
657
|
+
console.log(" 2. In 'Credentials' (凭证与基础信息), copy App ID & App Secret");
|
|
658
|
+
console.log(" 3. In 'Bot' (机器人), enable bot capability");
|
|
659
|
+
console.log(" 4. In 'Event Subscription' (事件订阅):");
|
|
660
|
+
console.log(" → Set mode to 'Long Connection' (使用长连接接收事件)");
|
|
661
|
+
console.log(" → Add event: im.message.receive_v1 (接收消息)");
|
|
662
|
+
console.log(" 5. In 'Permissions' (权限管理), add:");
|
|
663
|
+
console.log(" → im:message, im:message:send_as_bot, im:chat");
|
|
664
|
+
console.log(" 6. Publish the app version (创建版本 → 申请发布)");
|
|
665
|
+
console.log("");
|
|
666
|
+
|
|
667
|
+
const feishuAppId = (await ask("Paste your Feishu App ID (Enter to skip): ")).trim();
|
|
668
|
+
if (feishuAppId) {
|
|
669
|
+
const feishuSecret = (await ask("Paste your Feishu App Secret: ")).trim();
|
|
670
|
+
if (feishuSecret) {
|
|
671
|
+
if (!cfg.feishu) cfg.feishu = {};
|
|
672
|
+
cfg.feishu.enabled = true;
|
|
673
|
+
cfg.feishu.app_id = feishuAppId;
|
|
674
|
+
cfg.feishu.app_secret = feishuSecret;
|
|
675
|
+
if (!cfg.feishu.allowed_chat_ids) cfg.feishu.allowed_chat_ids = [];
|
|
676
|
+
console.log(" ✅ Feishu configured!");
|
|
677
|
+
console.log(" Note: allowed_chat_ids is empty = allow all users.");
|
|
678
|
+
console.log(" To restrict, add chat IDs to daemon.yaml later.\n");
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
console.log(" Skipped.\n");
|
|
592
682
|
}
|
|
593
683
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
console.log("
|
|
597
|
-
console.log(
|
|
598
|
-
console.log("
|
|
599
|
-
console.log("
|
|
600
|
-
console.log("
|
|
601
|
-
console.log(" allowed_chat_ids: [YOUR_CHAT_ID]");
|
|
602
|
-
console.log(" 4. To find your chat_id: message your bot, then run:");
|
|
603
|
-
console.log(" curl https://api.telegram.org/botYOUR_TOKEN/getUpdates");
|
|
604
|
-
console.log("\n📘 Feishu Setup (optional):");
|
|
605
|
-
console.log(" 1. Go to open.feishu.cn → Create App → get app_id & app_secret");
|
|
606
|
-
console.log(" 2. Enable Bot capability + im:message events");
|
|
607
|
-
console.log(" 3. Enable 'Long Connection' (长连接) mode in Event Subscription");
|
|
608
|
-
console.log(" 4. Edit ~/.metame/daemon.yaml:");
|
|
609
|
-
console.log(" feishu:");
|
|
610
|
-
console.log(" enabled: true");
|
|
611
|
-
console.log(" app_id: \"YOUR_APP_ID\"");
|
|
612
|
-
console.log(" app_secret: \"YOUR_APP_SECRET\"");
|
|
613
|
-
console.log(" allowed_chat_ids: [CHAT_ID]");
|
|
614
|
-
|
|
615
|
-
console.log("\n Then: metame daemon start");
|
|
616
|
-
|
|
617
|
-
// Optional launchd setup (macOS only)
|
|
684
|
+
// Write config
|
|
685
|
+
fs.writeFileSync(DAEMON_CONFIG, yaml.dump(cfg, { lineWidth: -1 }), 'utf8');
|
|
686
|
+
console.log("━━━ ✅ Setup Complete ━━━");
|
|
687
|
+
console.log(`Config saved: ${DAEMON_CONFIG}`);
|
|
688
|
+
console.log("\nNext steps:");
|
|
689
|
+
console.log(" metame daemon start — start the daemon");
|
|
690
|
+
console.log(" metame daemon status — check status");
|
|
618
691
|
if (process.platform === 'darwin') {
|
|
619
|
-
|
|
620
|
-
const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
|
|
621
|
-
console.log("\n🍎 Auto-start on macOS (optional):");
|
|
622
|
-
console.log(" To start daemon automatically on login:");
|
|
623
|
-
console.log(` metame daemon start (first time to verify it works)`);
|
|
624
|
-
console.log(` Then create: ${plistPath}`);
|
|
625
|
-
console.log(" Or run: metame daemon install-launchd");
|
|
692
|
+
console.log(" metame daemon install-launchd — auto-start on login");
|
|
626
693
|
}
|
|
694
|
+
|
|
695
|
+
rl.close();
|
|
627
696
|
process.exit(0);
|
|
628
697
|
}
|
|
629
698
|
|
|
@@ -676,18 +745,21 @@ if (isDaemon) {
|
|
|
676
745
|
}
|
|
677
746
|
|
|
678
747
|
if (subCmd === 'start') {
|
|
748
|
+
// Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
|
|
749
|
+
try {
|
|
750
|
+
const { execSync: es } = require('child_process');
|
|
751
|
+
const pids = es("pgrep -f 'node.*daemon\\.js' 2>/dev/null || true", { encoding: 'utf8' }).trim();
|
|
752
|
+
if (pids) {
|
|
753
|
+
for (const p of pids.split('\n').filter(Boolean)) {
|
|
754
|
+
const n = parseInt(p, 10);
|
|
755
|
+
if (n && n !== process.pid) try { process.kill(n, 'SIGKILL'); } catch { /* */ }
|
|
756
|
+
}
|
|
757
|
+
es('sleep 1');
|
|
758
|
+
}
|
|
759
|
+
} catch { /* ignore */ }
|
|
679
760
|
// Check if already running
|
|
680
761
|
if (fs.existsSync(DAEMON_PID)) {
|
|
681
|
-
|
|
682
|
-
try {
|
|
683
|
-
process.kill(existingPid, 0); // test if alive
|
|
684
|
-
console.log(`⚠️ Daemon already running (PID: ${existingPid})`);
|
|
685
|
-
console.log(" Use 'metame daemon stop' first.");
|
|
686
|
-
process.exit(1);
|
|
687
|
-
} catch {
|
|
688
|
-
// Stale PID file — clean up
|
|
689
|
-
fs.unlinkSync(DAEMON_PID);
|
|
690
|
-
}
|
|
762
|
+
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
691
763
|
}
|
|
692
764
|
if (!fs.existsSync(DAEMON_CONFIG)) {
|
|
693
765
|
console.error("❌ No config found. Run: metame daemon init");
|
|
@@ -717,11 +789,21 @@ if (isDaemon) {
|
|
|
717
789
|
const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
|
|
718
790
|
try {
|
|
719
791
|
process.kill(pid, 'SIGTERM');
|
|
792
|
+
// Wait for process to die (up to 3s), then force kill
|
|
793
|
+
let dead = false;
|
|
794
|
+
for (let i = 0; i < 6; i++) {
|
|
795
|
+
const { execSync: es } = require('child_process');
|
|
796
|
+
es('sleep 0.5');
|
|
797
|
+
try { process.kill(pid, 0); } catch { dead = true; break; }
|
|
798
|
+
}
|
|
799
|
+
if (!dead) {
|
|
800
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ }
|
|
801
|
+
}
|
|
720
802
|
console.log(`✅ Daemon stopped (PID: ${pid})`);
|
|
721
803
|
} catch (e) {
|
|
722
804
|
console.log(`⚠️ Process ${pid} not found (may have already exited).`);
|
|
723
|
-
fs.unlinkSync(DAEMON_PID);
|
|
724
805
|
}
|
|
806
|
+
try { fs.unlinkSync(DAEMON_PID); } catch { /* ignore */ }
|
|
725
807
|
process.exit(0);
|
|
726
808
|
}
|
|
727
809
|
|
package/package.json
CHANGED
package/scripts/daemon.js
CHANGED
|
@@ -615,12 +615,27 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
615
615
|
await sendDirPicker(bot, chatId, 'new', 'Pick a workdir:');
|
|
616
616
|
return;
|
|
617
617
|
}
|
|
618
|
-
if
|
|
619
|
-
|
|
620
|
-
|
|
618
|
+
// Parse: /new <path> [name] — if arg contains a space after a valid path, rest is name
|
|
619
|
+
let dirPath = arg;
|
|
620
|
+
let sessionName = '';
|
|
621
|
+
// Try full arg as path first; if not, split on spaces to find path + name
|
|
622
|
+
if (!fs.existsSync(dirPath)) {
|
|
623
|
+
const spaceIdx = arg.indexOf(' ');
|
|
624
|
+
if (spaceIdx > 0) {
|
|
625
|
+
const maybePath = arg.slice(0, spaceIdx);
|
|
626
|
+
if (fs.existsSync(maybePath)) {
|
|
627
|
+
dirPath = maybePath;
|
|
628
|
+
sessionName = arg.slice(spaceIdx + 1).trim();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (!fs.existsSync(dirPath)) {
|
|
632
|
+
await bot.sendMessage(chatId, `Path not found: ${dirPath}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
621
635
|
}
|
|
622
|
-
const session = createSession(chatId,
|
|
623
|
-
|
|
636
|
+
const session = createSession(chatId, dirPath, sessionName || '');
|
|
637
|
+
const label = sessionName ? `[${sessionName}]` : '';
|
|
638
|
+
await bot.sendMessage(chatId, `New session ${label}\nWorkdir: ${session.cwd}`);
|
|
624
639
|
return;
|
|
625
640
|
}
|
|
626
641
|
|
|
@@ -669,11 +684,26 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
669
684
|
return;
|
|
670
685
|
}
|
|
671
686
|
|
|
672
|
-
// Argument given → match by
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
687
|
+
// Argument given → match by name, then by session ID prefix
|
|
688
|
+
const allSessions = listRecentSessions(50);
|
|
689
|
+
const argLower = arg.toLowerCase();
|
|
690
|
+
// 1. Match by name (from session_names map)
|
|
691
|
+
let fullMatch = allSessions.find(s => {
|
|
692
|
+
const n = getSessionName(s.sessionId);
|
|
693
|
+
return n && n.toLowerCase() === argLower;
|
|
694
|
+
});
|
|
695
|
+
// 2. Partial name match
|
|
696
|
+
if (!fullMatch) {
|
|
697
|
+
fullMatch = allSessions.find(s => {
|
|
698
|
+
const n = getSessionName(s.sessionId);
|
|
699
|
+
return n && n.toLowerCase().includes(argLower);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
// 3. Session ID prefix match
|
|
703
|
+
if (!fullMatch) {
|
|
704
|
+
fullMatch = recentSessions.find(s => s.sessionId.startsWith(arg))
|
|
705
|
+
|| allSessions.find(s => s.sessionId.startsWith(arg));
|
|
706
|
+
}
|
|
677
707
|
const sessionId = fullMatch ? fullMatch.sessionId : arg;
|
|
678
708
|
const cwd = (fullMatch && fullMatch.projectPath) || (getSession(chatId) && getSession(chatId).cwd) || HOME;
|
|
679
709
|
|
|
@@ -684,8 +714,13 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
684
714
|
created: new Date().toISOString(),
|
|
685
715
|
started: true,
|
|
686
716
|
};
|
|
717
|
+
if (fullMatch) {
|
|
718
|
+
const n = getSessionName(sessionId);
|
|
719
|
+
if (n) state2.sessions[chatId].name = n;
|
|
720
|
+
}
|
|
687
721
|
saveState(state2);
|
|
688
|
-
const
|
|
722
|
+
const name = getSessionName(sessionId);
|
|
723
|
+
const label = name || (fullMatch ? (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) : sessionId.slice(0, 8));
|
|
689
724
|
await bot.sendMessage(chatId, `Resumed: ${label}\nWorkdir: ${cwd}`);
|
|
690
725
|
return;
|
|
691
726
|
}
|
|
@@ -711,12 +746,32 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
711
746
|
return;
|
|
712
747
|
}
|
|
713
748
|
|
|
749
|
+
if (text.startsWith('/name ')) {
|
|
750
|
+
const name = text.slice(6).trim();
|
|
751
|
+
if (!name) {
|
|
752
|
+
await bot.sendMessage(chatId, 'Usage: /name <session name>');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const state2 = loadState();
|
|
756
|
+
if (!state2.sessions[chatId]) {
|
|
757
|
+
await bot.sendMessage(chatId, 'No active session. Start one first.');
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
state2.sessions[chatId].name = name;
|
|
761
|
+
if (!state2.session_names) state2.session_names = {};
|
|
762
|
+
state2.session_names[state2.sessions[chatId].id] = name;
|
|
763
|
+
saveState(state2);
|
|
764
|
+
await bot.sendMessage(chatId, `Session named: ${name}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
714
768
|
if (text === '/session') {
|
|
715
769
|
const session = getSession(chatId);
|
|
716
770
|
if (!session) {
|
|
717
771
|
await bot.sendMessage(chatId, 'No active session. Send any message to start one.');
|
|
718
772
|
} else {
|
|
719
|
-
|
|
773
|
+
const nameTag = session.name ? ` [${session.name}]` : '';
|
|
774
|
+
await bot.sendMessage(chatId, `Session: ${session.id.slice(0, 8)}...${nameTag}\nWorkdir: ${session.cwd}\nStarted: ${session.created}`);
|
|
720
775
|
}
|
|
721
776
|
return;
|
|
722
777
|
}
|
|
@@ -796,9 +851,10 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
796
851
|
if (text.startsWith('/')) {
|
|
797
852
|
await bot.sendMessage(chatId, [
|
|
798
853
|
'Commands:',
|
|
799
|
-
'/new [path] — new session',
|
|
854
|
+
'/new [path] [name] — new session (optional name)',
|
|
800
855
|
'/continue — resume last computer session',
|
|
801
|
-
'/resume <id> — resume
|
|
856
|
+
'/resume <name|id> — resume by name or session ID',
|
|
857
|
+
'/name <name> — name current session',
|
|
802
858
|
'/cd <path> — change workdir',
|
|
803
859
|
'/session — current session info',
|
|
804
860
|
'/status /tasks /run /budget /quiet /reload',
|
|
@@ -865,8 +921,10 @@ function listRecentSessions(limit, cwd) {
|
|
|
865
921
|
* Format a session entry into a short, readable label for buttons
|
|
866
922
|
*/
|
|
867
923
|
function sessionLabel(s) {
|
|
924
|
+
const name = getSessionName(s.sessionId);
|
|
868
925
|
const proj = s.projectPath ? path.basename(s.projectPath) : '';
|
|
869
926
|
const date = new Date(s.modified).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' });
|
|
927
|
+
if (name) return `${date} [${name}] ${proj}`;
|
|
870
928
|
const title = (s.summary || '').slice(0, 28);
|
|
871
929
|
return `${date} ${proj ? proj + ': ' : ''}${title}`;
|
|
872
930
|
}
|
|
@@ -901,7 +959,7 @@ function getSession(chatId) {
|
|
|
901
959
|
return state.sessions[chatId] || null;
|
|
902
960
|
}
|
|
903
961
|
|
|
904
|
-
function createSession(chatId, cwd) {
|
|
962
|
+
function createSession(chatId, cwd, name) {
|
|
905
963
|
const state = loadState();
|
|
906
964
|
const sessionId = crypto.randomUUID();
|
|
907
965
|
state.sessions[chatId] = {
|
|
@@ -910,11 +968,21 @@ function createSession(chatId, cwd) {
|
|
|
910
968
|
created: new Date().toISOString(),
|
|
911
969
|
started: false, // true after first message sent
|
|
912
970
|
};
|
|
971
|
+
if (name) {
|
|
972
|
+
state.sessions[chatId].name = name;
|
|
973
|
+
if (!state.session_names) state.session_names = {};
|
|
974
|
+
state.session_names[sessionId] = name;
|
|
975
|
+
}
|
|
913
976
|
saveState(state);
|
|
914
|
-
log('INFO', `New session for ${chatId}: ${sessionId} (cwd: ${state.sessions[chatId].cwd})`);
|
|
977
|
+
log('INFO', `New session for ${chatId}: ${sessionId}${name ? ' [' + name + ']' : ''} (cwd: ${state.sessions[chatId].cwd})`);
|
|
915
978
|
return state.sessions[chatId];
|
|
916
979
|
}
|
|
917
980
|
|
|
981
|
+
function getSessionName(sessionId) {
|
|
982
|
+
const state = loadState();
|
|
983
|
+
return (state.session_names && state.session_names[sessionId]) || '';
|
|
984
|
+
}
|
|
985
|
+
|
|
918
986
|
function markSessionStarted(chatId) {
|
|
919
987
|
const state = loadState();
|
|
920
988
|
if (state.sessions[chatId]) {
|
|
@@ -121,12 +121,32 @@ function createBot(config) {
|
|
|
121
121
|
loggerLevel: Lark.LoggerLevel.info,
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
// Dedup: track recent message_ids (Feishu may redeliver on slow ack)
|
|
125
|
+
const _seenMsgIds = new Map(); // message_id → timestamp
|
|
126
|
+
const DEDUP_TTL = 60000; // 60s window
|
|
127
|
+
function isDuplicate(msgId) {
|
|
128
|
+
if (!msgId) return false;
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
// Cleanup old entries
|
|
131
|
+
if (_seenMsgIds.size > 200) {
|
|
132
|
+
for (const [k, t] of _seenMsgIds) {
|
|
133
|
+
if (now - t > DEDUP_TTL) _seenMsgIds.delete(k);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (_seenMsgIds.has(msgId)) return true;
|
|
137
|
+
_seenMsgIds.set(msgId, now);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
const eventDispatcher = new Lark.EventDispatcher({}).register({
|
|
125
142
|
'im.message.receive_v1': async (data) => {
|
|
126
143
|
try {
|
|
127
144
|
const msg = data.message;
|
|
128
145
|
if (!msg) return;
|
|
129
146
|
|
|
147
|
+
// Dedup by message_id
|
|
148
|
+
if (isDuplicate(msg.message_id)) return;
|
|
149
|
+
|
|
130
150
|
const chatId = msg.chat_id;
|
|
131
151
|
let text = '';
|
|
132
152
|
|
|
@@ -143,7 +163,8 @@ function createBot(config) {
|
|
|
143
163
|
text = text.replace(/@_user_\d+\s*/g, '').trim();
|
|
144
164
|
|
|
145
165
|
if (text) {
|
|
146
|
-
|
|
166
|
+
// Fire-and-forget: don't block the event loop (SDK needs fast ack)
|
|
167
|
+
Promise.resolve().then(() => onMessage(chatId, text, data)).catch(() => {});
|
|
147
168
|
}
|
|
148
169
|
} catch (e) {
|
|
149
170
|
// Non-fatal
|
|
@@ -159,7 +180,7 @@ function createBot(config) {
|
|
|
159
180
|
if (action && chatId) {
|
|
160
181
|
const cmd = action.value && action.value.cmd;
|
|
161
182
|
if (cmd) {
|
|
162
|
-
onMessage(chatId, cmd, data);
|
|
183
|
+
Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch(() => {});
|
|
163
184
|
}
|
|
164
185
|
}
|
|
165
186
|
} catch (e) {
|