From c5fbb3b3c99e6d42df271edfe2ec959b58b13bf8 Mon Sep 17 00:00:00 2001
From: xiaochengshiguduo <114392164+xiaochengshiguduo@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:17:31 +0800
Subject: [PATCH 1/5] feat: add fbi plugin - cross-group message tracking
---
fbi/fbi.ts | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++
plugins.json | 4 +
2 files changed, 474 insertions(+)
create mode 100644 fbi/fbi.ts
diff --git a/fbi/fbi.ts b/fbi/fbi.ts
new file mode 100644
index 0000000..210a503
--- /dev/null
+++ b/fbi/fbi.ts
@@ -0,0 +1,470 @@
+import { Plugin } from "@utils/pluginBase";
+import { getGlobalClient } from "@utils/globalClient";
+import { getPrefixes } from "@utils/pluginManager";
+import { safeGetReplyMessage } from "@utils/safeGetMessages";
+import { createDirectoryInAssets } from "@utils/pathHelpers";
+import { Api } from "teleproto";
+import { JSONFilePreset } from "lowdb/node";
+import * as path from "path";
+
+const PREFIX = getPrefixes()[0];
+const HELP = `🕵️ FBI 跨群组追踪
+
+• ${PREFIX}fbi cs [目标] — 搜索目标最新消息
+• ${PREFIX}fbi sv [目标] — 蹲守目标下一条消息
+• ${PREFIX}fbi ds [目标] — 分析目标最活跃群组
+• ${PREFIX}fbi ssv — 终止所有蹲守
+• ${PREFIX}fbi cache — 查看/管理消息缓存
+• ${PREFIX}fbi help — 本帮助
+
+目标可为 @用户名、用户ID,或回复消息自动取被回复者。`;
+
+const CACHE_MSG_LIMIT = 200; // max messages stored per group
+const CACHE_LIMIT_DEF = 300; // default max groups to cache
+const CACHE_LIMIT_MIN = 10;
+const CACHE_LIMIT_MAX = 1000;
+
+const htmlEsc = (s: string) =>
+ s.replace(/[&<>"']/g, (m) => {
+ if (m === "&") return "&";
+ if (m === "<") return "<";
+ if (m === ">") return ">";
+ if (m === '"') return """;
+ return "'";
+ });
+
+const peelChatId = (id: any) => String(typeof id === "bigint" ? id.toString() : id).replace(/^-100/, "");
+
+function msgPreview(msg: Api.Message): string {
+ let t = msg.text || "";
+ if (!t && msg.media) t = "[媒体消息]";
+ return t.length > 50 ? t.slice(0, 50) + "…" : t || "[空消息]";
+}
+
+/** random delay between min and max ms */
+function rndDelay(min = 500, max = 2000): Promise {
+ const ms = Math.floor(Math.random() * (max - min + 1)) + min;
+ return new Promise((r) => setTimeout(r, ms));
+}
+
+interface CachedChat {
+ username?: string;
+ title?: string;
+ msgs: Api.Message[];
+}
+
+interface SvEntry {
+ targetId: string;
+ targetName: string;
+ triggerPeer: string;
+ triggerMsgId: number;
+}
+
+interface FbiDB {
+ surveillance: Record;
+ cacheLimit: number;
+}
+
+const DB_DEF: FbiDB = { surveillance: {}, cacheLimit: CACHE_LIMIT_DEF };
+
+class FbiPlugin extends Plugin {
+ description = `FBI 跨群组追踪\n\n${HELP}`;
+
+ cmdHandlers = { fbi: this.onCmd.bind(this) };
+ listenMessageHandler = this.onMsg.bind(this);
+
+ private db: any;
+ private sv = new Map();
+ private chatCache = new Map();
+ private cacheReady = false;
+
+ constructor() {
+ super();
+ this.initDB().catch((e) => console.error("[fbi] initDB error", e));
+ }
+
+ /* ====== bootstrap ====== */
+
+ private async initDB() {
+ const p = path.join(createDirectoryInAssets("fbi"), "db.json");
+ this.db = await JSONFilePreset(p, DB_DEF);
+ // ensure cacheLimit has a default
+ if (typeof this.db.data.cacheLimit !== "number") {
+ this.db.data.cacheLimit = CACHE_LIMIT_DEF;
+ await this.db.write();
+ }
+ for (const [k, v] of Object.entries(this.db.data.surveillance)) this.sv.set(k, v);
+ // cache populated live via onMsg — no startup scan needed
+ this.cacheReady = true;
+ }
+
+ private async persistDb() {
+ this.db.data.surveillance = Object.fromEntries(this.sv);
+ await this.db.write();
+ }
+
+ /** scan public groups one-by-one with random delay, fill chatCache */
+ private async buildCache() {
+ const cl = await getGlobalClient();
+ if (!cl) return;
+
+ const limit = this.db.data.cacheLimit;
+ const dialogs = ((await cl.getDialogs({})) as any[])
+ .filter((d: any) => d.isGroup || d.isChat)
+ .slice(0, limit);
+
+ let count = 0;
+ for (const d of dialogs) {
+ const msgs: Api.Message[] = [];
+ for await (const m of cl.iterMessages(d.id, { limit: CACHE_MSG_LIMIT })) {
+ msgs.push(m);
+ }
+ // get entity for username/title
+ let username: string | undefined;
+ let title: string | undefined;
+ try {
+ const entity = await cl.getEntity(d.id);
+ username = entity.username;
+ title = entity.title;
+ } catch { /* best-effort */ }
+ this.chatCache.set(String(d.id), { username, title, msgs });
+ count++;
+ await rndDelay();
+ }
+
+ this.cacheReady = true;
+ console.log(`[fbi] cache ready: ${count} groups (limit ${limit})`);
+ }
+
+ /* ====== router ====== */
+
+ private async onCmd(msg: Api.Message) {
+ const parts = msg.text?.trim().split(/\s+/) || [];
+ const sub = parts[1]?.toLowerCase();
+ const args = parts.slice(2);
+
+ try {
+ switch (sub) {
+ case "cs": return this.doCs(msg, args);
+ case "sv": return this.doSv(msg, args);
+ case "ds": return this.doDs(msg, args);
+ case "ssv": return this.doSsv(msg);
+ case "cache": return this.doCache(msg, args);
+ default: return msg.edit({ text: HELP, parseMode: "html" });
+ }
+ } catch (e: any) {
+ await msg.edit({ text: `❌ ${htmlEsc(e.message || "未知错误")}`, parseMode: "html" });
+ }
+ }
+
+ /* ====== target resolver ====== */
+
+ private async resolveTarget(msg: Api.Message, args: string[]): Promise<{ id: string; name: string } | null> {
+ const cl = await getGlobalClient();
+ if (!cl) return null;
+ let targetId: string | undefined;
+ let name = "";
+
+ if (args.length) {
+ const raw = args[0];
+ try {
+ const e = await cl.getEntity(raw);
+ targetId = String(e.id);
+ name = e.username ? `@${e.username}` : e.firstName || e.title || targetId;
+ } catch {
+ name = raw;
+ targetId = raw.replace(/^@/, "");
+ }
+ } else if (msg.isReply) {
+ const r = await safeGetReplyMessage(msg);
+ if (r?.senderId) {
+ targetId = String(r.senderId);
+ try {
+ const u = await cl.getEntity(r.senderId as any);
+ name = u.username ? `@${u.username}` : u.firstName || targetId;
+ } catch {
+ name = targetId;
+ }
+ }
+ }
+ return targetId ? { id: targetId, name } : null;
+ }
+
+ /* ====== link builder ====== */
+
+ private async msgLink(cl: any, msg: Api.Message): Promise {
+ const chat = await cl.getEntity(msg.peerId);
+ const text = htmlEsc(msgPreview(msg));
+ const mid = msg.id;
+ if (chat.username) return `${text}`;
+ return `${text}`;
+ }
+
+ /* ====== cs (zero-request, reads cache) ====== */
+
+ private async doCs(msg: Api.Message, args: string[]) {
+ if (!this.cacheReady) {
+ await msg.edit({ text: "⏳ 缓存正在初始化,请稍后再试。", parseMode: "html" });
+ return;
+ }
+ const target = await this.resolveTarget(msg, args);
+ if (!target) {
+ await msg.edit({ text: "❌ 无法识别目标,请回复消息或提供用户名/ID", parseMode: "html" });
+ return;
+ }
+
+ await msg.edit({ text: `🔍 正在搜索嫌疑人 ${target.name} 的蛛丝马迹...`, parseMode: "html" });
+
+ let found: { msg: Api.Message; peer: string } | null = null;
+
+ for (const [peer, chat] of this.chatCache) {
+ for (const m of chat.msgs) {
+ if (String(m.senderId) === target.id) {
+ if (!found || m.date > found.msg.date) found = { msg: m, peer };
+ break; // newest msg in this group, move on
+ }
+ }
+ }
+
+ await msg.delete().catch(() => {});
+
+ if (!found) {
+ await this.sendReply(msg, `🤦♀ 暂时没发现 ${target.name} 有作案嫌疑。`);
+ return;
+ }
+
+ const cl = await getGlobalClient();
+ if (!cl) return;
+ // debug: log what we matched
+ console.log(`[fbi:cs] target.id=${target.id} target.name=${target.name}`);
+ console.log(`[fbi:cs] found msg: chat=${found.peer} mid=${found.msg.id} senderId=${String(found.msg.senderId)} date=${found.msg.date} text="${found.msg.text?.slice(0,60)}"`);
+ const link = await this.msgLink(cl, found.msg);
+ const userTag = `${htmlEsc(target.name)}`;
+ await this.sendReply(msg, `👀 发现嫌疑人 ${userTag} 的作案现场:\n\n${link}\n\n要想人不知除非己莫为。`);
+ }
+
+ /* ====== sv ====== */
+
+ private async doSv(msg: Api.Message, args: string[]) {
+ const cl = await getGlobalClient();
+ if (!cl) return;
+ const target = await this.resolveTarget(msg, args);
+ if (!target) {
+ await msg.edit({ text: "❌ 无法识别目标", parseMode: "html" });
+ return;
+ }
+
+ await msg.edit({ text: `👁️ 正在对嫌疑人 ${target.name} 进行蹲守...`, parseMode: "html" });
+
+ this.sv.set(target.id, {
+ targetId: target.id,
+ targetName: target.name,
+ triggerPeer: String(msg.chatId),
+ triggerMsgId: msg.id,
+ });
+ await this.persistDb();
+ }
+
+ /* ====== listen — update cache + check surveillance ====== */
+
+ private async onMsg(msg: Api.Message) {
+ // 1) update cache — only public groups, auto-vivify on first sighting
+ if (msg.chatId) {
+ const peer = String(msg.chatId);
+ let chat = this.chatCache.get(peer);
+ if (!chat) {
+ // first sighting → check if public, cache only public groups
+ const cl = await getGlobalClient();
+ if (cl) {
+ try {
+ const entity = await cl.getEntity(msg.chatId);
+ if (entity.username && (entity.className === 'Channel' || entity.className === 'Chat')) {
+ chat = { username: entity.username, title: entity.title, msgs: [] };
+ this.chatCache.set(peer, chat);
+ }
+ } catch { /* getEntity failed → skip */ }
+ }
+ }
+ if (chat) {
+ chat.msgs.unshift(msg);
+ if (chat.msgs.length > CACHE_MSG_LIMIT) chat.msgs.length = CACHE_MSG_LIMIT;
+ }
+ }
+
+ // 2) sv surveillance
+ if (this.sv.size === 0) return;
+ const sid = msg.senderId ? String(msg.senderId) : "";
+ if (!sid || !this.sv.has(sid)) return;
+ const entry = this.sv.get(sid)!;
+
+ // prevent self-trigger
+ if (String(msg.chatId) === entry.triggerPeer && msg.id === entry.triggerMsgId) return;
+
+ this.sv.delete(sid);
+ this.persistDb().catch(() => {});
+
+ const cl = await getGlobalClient();
+ if (!cl) return;
+
+ const link = await this.msgLink(cl, msg);
+ const userTag = `${htmlEsc(entry.targetName)}`;
+ const result = `🚨 发现嫌疑人 ${userTag} 最新动向\n\n${link}\n\n天网恢恢疏而不漏。`;
+
+ // ponytail: client handles channel/normal, one call suffices
+ try { await cl.deleteMessages(entry.triggerPeer, [entry.triggerMsgId], { revoke: false }); } catch {}
+
+ await cl.sendMessage(entry.triggerPeer, { message: result, parseMode: "html", linkPreview: false });
+ await cl.sendMessage("me", { message: result, parseMode: "html", linkPreview: false });
+ }
+
+ /* ====== ds (zero-request, reads cache) ====== */
+
+ private async doDs(msg: Api.Message, args: string[]) {
+ if (!this.cacheReady) {
+ await msg.edit({ text: "⏳ 缓存正在初始化,请稍后再试。", parseMode: "html" });
+ return;
+ }
+ const target = await this.resolveTarget(msg, args);
+ if (!target) {
+ await msg.edit({ text: "❌ 无法识别目标", parseMode: "html" });
+ return;
+ }
+
+ await msg.edit({ text: `🧭 正在摸排嫌疑人 ${target.name} 的窝点...`, parseMode: "html" });
+
+ let bestPeer: string | null = null;
+ let bestCount = 0;
+ let bestMsg: Api.Message | null = null;
+
+ for (const [peer, chat] of this.chatCache) {
+ let cnt = 0;
+ let latest: Api.Message | null = null;
+ for (const m of chat.msgs) {
+ if (String(m.senderId) === target.id) {
+ cnt++;
+ if (!latest) latest = m; // first hit = newest (msgs newest-first)
+ }
+ }
+ if (cnt > bestCount) {
+ bestCount = cnt;
+ bestPeer = peer;
+ bestMsg = latest;
+ }
+ }
+
+ await msg.delete().catch(() => {});
+
+ if (!bestPeer || bestCount === 0) {
+ await this.sendReply(msg, `🤦♀ 摸排结果不尽人意,嫌疑人 ${target.name} 藏的很深。`);
+ return;
+ }
+
+ // debug: log ds match
+ if (bestMsg) {
+ console.log(`[fbi:ds] target.id=${target.id} target.name=${target.name}`);
+ console.log(`[fbi:ds] best group=${bestPeer} count=${bestCount} mid=${bestMsg.id} senderId=${String(bestMsg.senderId)} date=${bestMsg.date} text="${bestMsg.text?.slice(0,60)}"`);
+ }
+ // ponytail: link text = group name, href points to target's latest msg in that group
+ let link = `https://t.me/c/${peelChatId(bestPeer)}`;
+ if (bestMsg) {
+ const chat = this.chatCache.get(bestPeer)!;
+ const mid = bestMsg.id;
+ const href = chat.username ? `https://t.me/${chat.username}/${mid}` : `https://t.me/c/${peelChatId(bestPeer)}/${mid}`;
+ link = `${htmlEsc(chat.title || chat.username || bestPeer)}`;
+ }
+ const userTag = `${htmlEsc(target.name)}`;
+ await this.sendReply(msg, `🏚 发现嫌疑人 ${userTag} 的窝点:\n\n${link}\n\n跑得了和尚跑不了庙。`);
+ }
+
+ /* ====== ssv ====== */
+
+ private async doSsv(msg: Api.Message) {
+ if (this.sv.size === 0) {
+ await msg.edit({ text: "❌ 当前没有活跃的蹲守任务。", parseMode: "html" });
+ return;
+ }
+
+ const cl = await getGlobalClient();
+
+ for (const entry of this.sv.values()) {
+ if (cl) {
+ try {
+ await cl.invoke(
+ new Api.messages.EditMessage({
+ peer: entry.triggerPeer,
+ id: entry.triggerMsgId,
+ message: `🤦♀ 蹲守过程中没有发现嫌疑人 ${entry.targetName} 的行踪。`,
+ parseMode: "html" as any,
+ }),
+ );
+ } catch { /* trigger may be deleted — fine */ }
+ }
+ }
+
+ this.sv.clear();
+ await this.persistDb();
+ await msg.edit({ text: "✅ 已终止所有蹲守任务。", parseMode: "html" });
+ }
+
+ /* ====== cache management ====== */
+
+ private async doCache(msg: Api.Message, args: string[]) {
+ const sub = args[0]?.toLowerCase();
+
+ if (sub === "limit") {
+ const n = parseInt(args[1], 10);
+ if (isNaN(n) || n < CACHE_LIMIT_MIN || n > CACHE_LIMIT_MAX) {
+ await msg.edit({
+ text: `❌ 缓存上限必须为 ${CACHE_LIMIT_MIN}~${CACHE_LIMIT_MAX} 之间的整数。当前:${this.db.data.cacheLimit}`,
+ parseMode: "html",
+ });
+ return;
+ }
+ this.db.data.cacheLimit = n;
+ await this.db.write();
+ await msg.edit({
+ text: `✅ 缓存上限已设为 ${n} 个群组。使用 ${PREFIX}fbi cache rebuild 重新构建。`,
+ parseMode: "html",
+ });
+ return;
+ }
+
+ if (sub === "rebuild") {
+ await msg.edit({ text: "🔄 正在重建缓存,可能需要几分钟...", parseMode: "html" });
+ this.cacheReady = false;
+ this.chatCache.clear();
+ await this.buildCache();
+ await msg.edit({
+ text: `✅ 缓存重建完成,共缓存 ${this.chatCache.size} 个群组。`,
+ parseMode: "html",
+ });
+ return;
+ }
+
+ // status
+ const limit = this.db.data.cacheLimit;
+ await msg.edit({
+ text: `📦 FBI 缓存状态
+
+• 已缓存群组:${this.chatCache.size}
+• 缓存上限:${limit}
+• 每群最大消息:${CACHE_MSG_LIMIT}
+• 状态:${this.cacheReady ? "✅ 就绪" : "⏳ 初始化中"}
+
+子命令
+• ${PREFIX}fbi cache limit [数量] — 设置缓存上限(${CACHE_LIMIT_MIN}~${CACHE_LIMIT_MAX})
+• ${PREFIX}fbi cache rebuild — 重建缓存`,
+ parseMode: "html",
+ });
+ }
+
+ /* ====== helper ====== */
+
+ private async sendReply(original: Api.Message, text: string) {
+ const cl = await getGlobalClient();
+ if (!cl) return;
+ await cl.sendMessage(original.peerId, { message: text, parseMode: "html", linkPreview: false });
+ }
+}
+
+export default new FbiPlugin();
diff --git a/plugins.json b/plugins.json
index ee297fa..8de9176 100644
--- a/plugins.json
+++ b/plugins.json
@@ -175,6 +175,10 @@
"url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/epic/epic.ts?raw=true",
"desc": "检查Epic Games喜加一优惠"
},
+ "fbi": {
+ "url": "https://github.com/xiaochengshiguduo/TeleBox_Plugins/blob/main/fbi/fbi.ts?raw=true",
+ "desc": "FBI - 跨群组消息追踪 / 蹲守 / 活跃群组分析"
+ },
"fadian": {
"url": "https://github.com/TeleBoxDev/TeleBox_Plugins/blob/main/fadian/fadian.ts?raw=true",
"desc": "fadian语录"
From b335ff32384ff710b84f4bf99b46346bea6046a2 Mon Sep 17 00:00:00 2001
From: xiaochengshiguduo <114392164+xiaochengshiguduo@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:20:00 +0800
Subject: [PATCH 2/5] fix: use upstream URL in plugins.json
---
plugins.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins.json b/plugins.json
index 8de9176..fd93055 100644
--- a/plugins.json
+++ b/plugins.json
@@ -176,7 +176,7 @@
"desc": "检查Epic Games喜加一优惠"
},
"fbi": {
- "url": "https://github.com/xiaochengshiguduo/TeleBox_Plugins/blob/main/fbi/fbi.ts?raw=true",
+ "url": "https://github.com/TeleBoxOrg/TeleBox_Plugins/blob/main/fbi/fbi.ts?raw=true",
"desc": "FBI - 跨群组消息追踪 / 蹲守 / 活跃群组分析"
},
"fadian": {
From 2276ca05bc4a3474c0671a315de2534694f666df Mon Sep 17 00:00:00 2001
From: xiaochengshiguduo <114392164+xiaochengshiguduo@users.noreply.github.com>
Date: Thu, 2 Jul 2026 20:30:07 +0800
Subject: [PATCH 3/5] feat: persist cache to lowdb, strip messages
---
fbi/fbi.ts | 78 ++++++++++++++++++++++++++++++++----------------------
1 file changed, 47 insertions(+), 31 deletions(-)
diff --git a/fbi/fbi.ts b/fbi/fbi.ts
index 210a503..ca9305e 100644
--- a/fbi/fbi.ts
+++ b/fbi/fbi.ts
@@ -35,10 +35,8 @@ const htmlEsc = (s: string) =>
const peelChatId = (id: any) => String(typeof id === "bigint" ? id.toString() : id).replace(/^-100/, "");
-function msgPreview(msg: Api.Message): string {
- let t = msg.text || "";
- if (!t && msg.media) t = "[媒体消息]";
- return t.length > 50 ? t.slice(0, 50) + "…" : t || "[空消息]";
+function stripMsg(m: Api.Message): CachedMsg {
+ return { id: m.id, senderId: Number(m.senderId), date: m.date, text: m.text || "" };
}
/** random delay between min and max ms */
@@ -47,10 +45,18 @@ function rndDelay(min = 500, max = 2000): Promise {
return new Promise((r) => setTimeout(r, ms));
}
+/** Minimal serializable representation of a chat message */
+interface CachedMsg {
+ id: number;
+ senderId: number;
+ date: number;
+ text: string;
+}
+
interface CachedChat {
username?: string;
title?: string;
- msgs: Api.Message[];
+ msgs: CachedMsg[];
}
interface SvEntry {
@@ -63,9 +69,10 @@ interface SvEntry {
interface FbiDB {
surveillance: Record;
cacheLimit: number;
+ cache: Record;
}
-const DB_DEF: FbiDB = { surveillance: {}, cacheLimit: CACHE_LIMIT_DEF };
+const DB_DEF: FbiDB = { surveillance: {}, cacheLimit: CACHE_LIMIT_DEF, cache: {} };
class FbiPlugin extends Plugin {
description = `FBI 跨群组追踪\n\n${HELP}`;
@@ -94,7 +101,12 @@ class FbiPlugin extends Plugin {
await this.db.write();
}
for (const [k, v] of Object.entries(this.db.data.surveillance)) this.sv.set(k, v);
- // cache populated live via onMsg — no startup scan needed
+ // load persisted cache into memory
+ if (this.db.data.cache) {
+ for (const [k, v] of Object.entries(this.db.data.cache))
+ this.chatCache.set(k, v as CachedChat);
+ console.log(`[fbi] cache loaded: ${this.chatCache.size} groups`);
+ }
this.cacheReady = true;
}
@@ -115,9 +127,9 @@ class FbiPlugin extends Plugin {
let count = 0;
for (const d of dialogs) {
- const msgs: Api.Message[] = [];
+ const msgs: CachedMsg[] = [];
for await (const m of cl.iterMessages(d.id, { limit: CACHE_MSG_LIMIT })) {
- msgs.push(m);
+ msgs.push(stripMsg(m));
}
// get entity for username/title
let username: string | undefined;
@@ -127,11 +139,17 @@ class FbiPlugin extends Plugin {
username = entity.username;
title = entity.title;
} catch { /* best-effort */ }
- this.chatCache.set(String(d.id), { username, title, msgs });
- count++;
+ // ponytail: skip private groups (no username)
+ if (username) {
+ this.chatCache.set(String(d.id), { username, title, msgs });
+ count++;
+ }
await rndDelay();
}
+ // persist to disk
+ this.db.data.cache = Object.fromEntries(this.chatCache);
+ await this.db.write();
this.cacheReady = true;
console.log(`[fbi] cache ready: ${count} groups (limit ${limit})`);
}
@@ -190,16 +208,6 @@ class FbiPlugin extends Plugin {
return targetId ? { id: targetId, name } : null;
}
- /* ====== link builder ====== */
-
- private async msgLink(cl: any, msg: Api.Message): Promise {
- const chat = await cl.getEntity(msg.peerId);
- const text = htmlEsc(msgPreview(msg));
- const mid = msg.id;
- if (chat.username) return `${text}`;
- return `${text}`;
- }
-
/* ====== cs (zero-request, reads cache) ====== */
private async doCs(msg: Api.Message, args: string[]) {
@@ -215,7 +223,7 @@ class FbiPlugin extends Plugin {
await msg.edit({ text: `🔍 正在搜索嫌疑人 ${target.name} 的蛛丝马迹...`, parseMode: "html" });
- let found: { msg: Api.Message; peer: string } | null = null;
+ let found: { msg: CachedMsg; peer: string } | null = null;
for (const [peer, chat] of this.chatCache) {
for (const m of chat.msgs) {
@@ -233,12 +241,15 @@ class FbiPlugin extends Plugin {
return;
}
- const cl = await getGlobalClient();
- if (!cl) return;
// debug: log what we matched
console.log(`[fbi:cs] target.id=${target.id} target.name=${target.name}`);
- console.log(`[fbi:cs] found msg: chat=${found.peer} mid=${found.msg.id} senderId=${String(found.msg.senderId)} date=${found.msg.date} text="${found.msg.text?.slice(0,60)}"`);
- const link = await this.msgLink(cl, found.msg);
+ console.log(`[fbi:cs] found msg: chat=${found.peer} mid=${found.msg.id} senderId=${found.msg.senderId} date=${found.msg.date} text="${found.msg.text.slice(0,60)}"`);
+ // ponytail: build link from cache, no API call needed
+ const chat = this.chatCache.get(found.peer)!;
+ const text = htmlEsc(found.msg.text.slice(0, 50));
+ const link = chat.username
+ ? `${text}`
+ : `${text}`;
const userTag = `${htmlEsc(target.name)}`;
await this.sendReply(msg, `👀 发现嫌疑人 ${userTag} 的作案现场:\n\n${link}\n\n要想人不知除非己莫为。`);
}
@@ -286,7 +297,7 @@ class FbiPlugin extends Plugin {
}
}
if (chat) {
- chat.msgs.unshift(msg);
+ chat.msgs.unshift(stripMsg(msg));
if (chat.msgs.length > CACHE_MSG_LIMIT) chat.msgs.length = CACHE_MSG_LIMIT;
}
}
@@ -306,7 +317,12 @@ class FbiPlugin extends Plugin {
const cl = await getGlobalClient();
if (!cl) return;
- const link = await this.msgLink(cl, msg);
+ // ponytail: link from live msg entity (need peerId for unknown groups)
+ const chatEntity = await cl.getEntity(msg.peerId);
+ const preview = htmlEsc((msg.text || "").slice(0, 50) || "[空消息]");
+ const link = chatEntity.username
+ ? `${preview}`
+ : `${preview}`;
const userTag = `${htmlEsc(entry.targetName)}`;
const result = `🚨 发现嫌疑人 ${userTag} 最新动向\n\n${link}\n\n天网恢恢疏而不漏。`;
@@ -334,11 +350,11 @@ class FbiPlugin extends Plugin {
let bestPeer: string | null = null;
let bestCount = 0;
- let bestMsg: Api.Message | null = null;
+ let bestMsg: CachedMsg | null = null;
for (const [peer, chat] of this.chatCache) {
let cnt = 0;
- let latest: Api.Message | null = null;
+ let latest: CachedMsg | null = null;
for (const m of chat.msgs) {
if (String(m.senderId) === target.id) {
cnt++;
@@ -362,7 +378,7 @@ class FbiPlugin extends Plugin {
// debug: log ds match
if (bestMsg) {
console.log(`[fbi:ds] target.id=${target.id} target.name=${target.name}`);
- console.log(`[fbi:ds] best group=${bestPeer} count=${bestCount} mid=${bestMsg.id} senderId=${String(bestMsg.senderId)} date=${bestMsg.date} text="${bestMsg.text?.slice(0,60)}"`);
+ console.log(`[fbi:ds] best group=${bestPeer} count=${bestCount} mid=${bestMsg.id} senderId=${bestMsg.senderId} date=${bestMsg.date} text="${bestMsg.text.slice(0,60)}"`);
}
// ponytail: link text = group name, href points to target's latest msg in that group
let link = `https://t.me/c/${peelChatId(bestPeer)}`;
From 05d56a2cf971039735d6271227f7fb10bb67676f Mon Sep 17 00:00:00 2001
From: xiaochengshiguduo <114392164+xiaochengshiguduo@users.noreply.github.com>
Date: Thu, 2 Jul 2026 20:49:45 +0800
Subject: [PATCH 4/5] fix: auto-persist cache on onMsg via debounced writes
---
fbi/fbi.ts | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/fbi/fbi.ts b/fbi/fbi.ts
index ca9305e..1f80b57 100644
--- a/fbi/fbi.ts
+++ b/fbi/fbi.ts
@@ -84,6 +84,8 @@ class FbiPlugin extends Plugin {
private sv = new Map();
private chatCache = new Map();
private cacheReady = false;
+ private cacheDirty = false;
+ private cachePersistTimer: ReturnType | null = null;
constructor() {
super();
@@ -115,6 +117,19 @@ class FbiPlugin extends Plugin {
await this.db.write();
}
+ /** debounced cache persist — at most once every 10s */
+ private schedulePersistCache() {
+ this.cacheDirty = true;
+ if (this.cachePersistTimer) return; // already queued
+ this.cachePersistTimer = setTimeout(async () => {
+ this.cachePersistTimer = null;
+ if (!this.cacheDirty) return;
+ this.cacheDirty = false;
+ this.db.data.cache = Object.fromEntries(this.chatCache);
+ await this.db.write();
+ }, 10_000);
+ }
+
/** scan public groups one-by-one with random delay, fill chatCache */
private async buildCache() {
const cl = await getGlobalClient();
@@ -299,6 +314,7 @@ class FbiPlugin extends Plugin {
if (chat) {
chat.msgs.unshift(stripMsg(msg));
if (chat.msgs.length > CACHE_MSG_LIMIT) chat.msgs.length = CACHE_MSG_LIMIT;
+ this.schedulePersistCache();
}
}
From a22fa72bcee677f65466c11336337294a6a2cdb8 Mon Sep 17 00:00:00 2001
From: xiaochengshiguduo <114392164+xiaochengshiguduo@users.noreply.github.com>
Date: Thu, 2 Jul 2026 20:55:32 +0800
Subject: [PATCH 5/5] chore: remove debug console.log from cs/ds
---
fbi/fbi.ts | 12 ++----------
1 file changed, 2 insertions(+), 10 deletions(-)
diff --git a/fbi/fbi.ts b/fbi/fbi.ts
index 1f80b57..59a13b8 100644
--- a/fbi/fbi.ts
+++ b/fbi/fbi.ts
@@ -256,10 +256,7 @@ class FbiPlugin extends Plugin {
return;
}
- // debug: log what we matched
- console.log(`[fbi:cs] target.id=${target.id} target.name=${target.name}`);
- console.log(`[fbi:cs] found msg: chat=${found.peer} mid=${found.msg.id} senderId=${found.msg.senderId} date=${found.msg.date} text="${found.msg.text.slice(0,60)}"`);
- // ponytail: build link from cache, no API call needed
+ // build link from cache, no API call needed
const chat = this.chatCache.get(found.peer)!;
const text = htmlEsc(found.msg.text.slice(0, 50));
const link = chat.username
@@ -391,12 +388,7 @@ class FbiPlugin extends Plugin {
return;
}
- // debug: log ds match
- if (bestMsg) {
- console.log(`[fbi:ds] target.id=${target.id} target.name=${target.name}`);
- console.log(`[fbi:ds] best group=${bestPeer} count=${bestCount} mid=${bestMsg.id} senderId=${bestMsg.senderId} date=${bestMsg.date} text="${bestMsg.text.slice(0,60)}"`);
- }
- // ponytail: link text = group name, href points to target's latest msg in that group
+ // link text = group name, href points to target's latest msg in that group
let link = `https://t.me/c/${peelChatId(bestPeer)}`;
if (bestMsg) {
const chat = this.chatCache.get(bestPeer)!;