├── README.md
└── TG_Chat_Bot-D1.js
/README.md:
--------------------------------------------------------------------------------
1 | # 🚀 Telegram 双向机器人 Cloudflare Worker
2 |
3 | [](https://workers.cloudflare.com/)
4 | [](https://core.telegram.org/bots/api)
5 | [](https://developers.cloudflare.com/d1)
6 |
7 | > **企业级私聊托管与风控解决方案** - 通过 Cloudflare Worker + D1 数据库构建的高性能 Telegram 双向机器人,支持三模态验证、可视化协管系统和智能 CRM 管理
8 |
9 | ## ✨ 项目简介
10 |
11 | 这是一个基于 **Cloudflare Worker** 和 **D1 数据库** 构建的高性能 Telegram 双向机器人。
12 | **Telegram 双向机器人 Cloudflare Worker 混合验证版** 带来了质的飞跃:完美继承私聊消息转发到群组话题的 CRM 核心能力,更引入 **三模态混合验证系统**(Cloudflare/Google/关闭)与 **独立问题验证** 机制,配合全新可视化协管管理,为您提供企业级私聊托管与风控解决方案。
13 |
14 | ---
15 |
16 | ## 🔥 版本特性
17 |
18 | | 特性 | 说明 |
19 | |------|------|
20 | | 🛡️ **混合验证架构** | 首创验证模式热切换,支持 Cloudflare Turnstile、Google Recaptcha 及关闭模式一键轮询,应对不同网络环境 |
21 | | 🧩 **模块化风控** | “人机验证(Captcha)”与“问题验证(Q&A)”逻辑解耦,可双重开启加强防护,或仅开启问答降低门槛 |
22 | | 👮 **可视化协管** | 重构协管管理逻辑,列表直接显示管理员 ID,支持精准删除与添加,权限管理更透明 |
23 | | ⚡ **CRM 增强** | 支持通过回复 `/clear` 快速撤销用户备注,管理更灵活 |
24 | | 🧠 **智能维护** | 保留数据库自动初始化、单人单卡聚合、双向状态同步等经典特性 |
25 |
26 | ---
27 |
28 | ## 🧰 核心功能详解
29 |
30 | ### 1. 🛡️ 多维安全验证系统(核心升级)
31 | - **三模态一键切换**:在控制面板中随时切换 Cloudflare/Google Recaptcha/关闭验证模式,无需重新部署
32 | - **独立问答验证**:即使关闭人机验证码,也能单独开启“数学题”或“自定义问答”拦截基础脚本攻击
33 | - **组合防御**:支持“验证码 + 问答”双重验证,只有全部通过后用户才能发送消息
34 |
35 | ### 2. 👮 协管权限系统(优化)
36 | - **权限下放**:主管理员可添加多名协管员,拥有回复消息、查看面板、管理黑名单权限
37 | - **可视化管理**:在协管面板直观展示所有协管的 Telegram ID,点击 ID 一键删除,告别盲猜
38 |
39 | ### 3. 📨 双向消息中继
40 | - **自动话题**:每个用户的私聊消息自动在管理员群组创建独立话题(Topic)
41 | - **无感回复**:管理员在话题内直接回复,机器人自动转发给用户;用户回复自动转入对应话题
42 |
43 | ### 4. 📇 CRM 客户管理系统
44 | - **智能备注**:管理员点击资料卡 ✏️ 按钮为用户打标签
45 | - **新特性**:备注模式下回复 `/clear` 可直接删除当前备注
46 | - **全局同步**:修改备注后,该用户所有历史资料卡(话题顶部、通知消息)自动同步更新
47 | - **资料卡追踪**:话题顶部始终置顶最新用户资料卡(含 ID、用户名、注册时间及备注)
48 |
49 | ### 5. 📥 聚合收件箱 (One Card Policy)
50 | - **防刷屏机制**:无论用户发送多少消息,“🔔 未读消息”话题中每个用户只保留一张最新通知卡片
51 | - **阅后即焚**:点击 ✅ 已阅/删除,卡片即刻消失并重置通知冷却时间
52 | - **一键直达**:通知卡片包含跳转按钮,点击直达用户专属聊天话题
53 |
54 | ### 6. 🚫 黑名单隔离系统
55 | - **双向同步**:手动屏蔽或触发关键词自动封禁后,在“🚫 黑名单”话题生成卡片
56 | - **一键解封**:在个人话题或黑名单话题点击解封,状态双向同步,黑名单卡片自动销毁
57 | - **自助重置**:被封禁用户发送 `/start` 可触发重置流程,重新进行验证
58 |
59 | ### 7. 🌙 营业状态管理
60 | - **一键切换**:在面板中切换“营业中”或“休息中”
61 | - **自动回复**:休息模式下用户发消息收到预设忙碌提示(内置防抖机制避免重复打扰)
62 |
63 | ---
64 |
65 | ## 🛠️ 部署指南(保姆级教程)
66 |
67 | > **📺 视频教程**:[点击访问部署视频教程](https://t.me/yinhai_notify/371?comment=136740)
68 |
69 | ### 📋 准备工作
70 | 1. [Cloudflare 账号](https://dash.cloudflare.com/)
71 | 2. Telegram Bot Token(通过 [@BotFather](https://t.me/BotFather) 获取)
72 | 3. Telegram 管理员群组 ID(必须是**开启话题功能的超级群组**,ID 以 `-100` 开头,通过 [@raw_data_bot](https://t.me/raw_data_bot) 获取)
73 | 4. 管理员 ID(你自己的 TG ID,通过 [@raw_data_bot](https://t.me/raw_data_bot) 获取)
74 |
75 | > 💡 **升级超级群组技巧**(不公开群组方法):
76 | > 1. 将群组的 **新成员是否可见消息记录** 设置为 **可见**
77 | > 2. 在 **管理员权限** 中细分权限,关闭 bot 用不上的权限
78 |
79 | ---
80 |
81 | ### 步骤一:创建 D1 数据库
82 | 1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
83 | 2. 导航至 **存储和数据库 → D1 数据库**
84 | 3. 点击 **创建数据库**,命名为 `tg-bot-db`(或自定义名称)
85 | 4. **无需**进行其他操作(代码会自动建表)
86 |
87 | ### 步骤二:创建 Worker
88 | 1. 进入 **Workers 和 Pages → 创建 Worker**
89 | 2. 命名 Worker(例如 `tg-contact-bot`)→ 点击 **部署**
90 | 3. 点击 **编辑代码**
91 | 4. **全量覆盖**:删除所有默认代码,将 [`worker.js`](https://github.com/your-repo/telegram-cf-worker/blob/main/worker.js) 的完整代码粘贴进去
92 | 5. **点击部署**,进行下一步配置
93 |
94 | ### 步骤三:绑定 D1 数据库
95 | 1. 在代码编辑页面左侧/上方找到 **设置 (Settings) 或 绑定 (Bindings)**
96 | 2. 添加 D1 数据库绑定:
97 | - **变量名称 (Variable Name)**:`TG_BOT_DB`(必须严格匹配大小写)
98 | - **数据库**:选择步骤一创建的数据库
99 | 3. 保存设置
100 |
101 | ### 步骤四:配置 Turnstile 验证
102 | 1. 在 Cloudflare 侧边栏选择 **Turnstile → 添加站点**
103 | 2. 填写配置:
104 | - **站点名称**:任意(如 `tg-bot-verification`)
105 | - **域**:填写 Worker 域名(例如 `your-worker.your-subdomain.workers.dev` 或 `workers.dev`)
106 | - **模式**:选择 **托管 (Managed)**
107 | 3. 创建后复制 **站点密钥 (Site Key)** 和 **密钥 (Secret Key)** 备用
108 |
109 | ### 步骤五:配置环境变量
110 | 在 Worker 的 **设置 → 变量** 中,添加以下 **8 个必备变量**:
111 |
112 | | 变量名称 | 示例值 | 说明 |
113 | |----------|--------|------|
114 | | `BOT_TOKEN` | `12345:AAH...` | 你的 Bot Token |
115 | | `ADMIN_IDS` | `123456,789012` | 管理员ID(多人用英文逗号分隔,**无空格**) |
116 | | `ADMIN_GROUP_ID` | `-100123456789` | 开启话题的超级群组 ID |
117 | | `WORKER_URL` | `https://xxx.workers.dev` | Worker 完整访问链接(**不带末尾斜杠**) |
118 | | `TURNSTILE_SITE_KEY` | `0x4AAAA...` | 步骤四获取的 Turnstile 站点密钥 |
119 | | `TURNSTILE_SECRET_KEY` | `0x4AAAA...` | 步骤四获取的 Turnstile 密钥 |
120 | | `RECAPTCHA_SITE_KEY` | `6LAAAAABBCCDDBGHYDD_cDmgjUtEbpF` | [Google reCAPTCHA v2](https://www.google.com/recaptcha/admin) 站点密钥 |
121 | | `RECAPTCHA_SECRET_KEY` | `6LAAAAABDDCCFGTTH-AIMK6z-H4aE` | [Google reCAPTCHA v2](https://www.google.com/recaptcha/admin) 密钥 |
122 |
123 | > ⚠️ **重要**:
124 | > - Google reCAPTCHA 需自行在 [Google reCAPTCHA Admin Console](https://www.google.com/recaptcha/admin) 创建(选择 **v2 Checkbox** 类型)
125 | > - 所有变量值**不要包含空格或引号**
126 | > - 点击 **部署 (Deploy)** 使代码和配置生效
127 |
128 | ### 步骤六:设置 Webhook
129 | 在浏览器地址栏输入以下 URL 并回车(替换 `<你的BOT_TOKEN>` 和 `<你的WORKER_URL>`):
130 | ```bash
131 | https://api.telegram.org/bot<你的BOT_TOKEN>/setWebhook?url=<你的WORKER_URL>
132 | ```
133 | ✅ **成功响应**:
134 | ```json
135 | {"ok":true,"result":true,"description":"Webhook was set"}
136 | ```
137 |
138 | ---
139 |
140 | ## ❓ 常见问题解答
141 |
142 | | 问题现象 | 可能原因 | 解决方案 |
143 | |----------|----------|----------|
144 | | `[说明1] 系统忙,请稍后再试` | 1. 机器人未获得足够权限
2. 群组ID错误
3. 群组未升级为超级群组
4. 未开启话题功能 | 1. 检查群组是否为超级群组
2. 确认群组设置中 **开启话题**
3. 通过 [@raw_data_bot](https://t.me/raw_data_bot) 检查群组状态 |
145 | | `[说明2] 私聊BOT/start无反应` | `BOT_TOKEN` 配置错误 | 1. 重新从 @BotFather 获取 Token
2. 检查环境变量是否有拼写错误
3. 重新设置 webhook |
146 | | `[说明3] 回复消息无反应` | `ADMIN_IDS` 配置错误 | 1. 通过 [@raw_data_bot](https://t.me/raw_data_bot) 确认你的 TG ID
2. 检查环境变量中 ID 是否正确且无空格 |
147 | | `[说明4] 点击配置菜单出现ERROR` | D1 数据库未绑定或变量名错误 | 1. 检查绑定变量名是否为 `TG_BOT_DB`(大小写敏感)
2. 确认数据库已正确创建 |
148 | | `[说明5] 点击配置菜单无反应` | D1 数据库配置错误 | 1. 重新绑定数据库
2. 检查 Worker 代码是否包含最新 D1 初始化逻辑 |
149 |
150 | ---
151 |
152 | ## 📜 许可证
153 | 本项目采用 [MIT 许可证](LICENSE) - 详情请参阅 LICENSE 文件
154 |
155 | ---
156 |
157 | > **💡 提示**:部署完成后,向机器人发送 `/start` 即可体验完整功能!
158 | > 遇到问题?请在 [Issues](https://github.com/huliyoudiangou/TG_Chat_Bot-D1/issues) 提交详细日志,我们将快速响应!
159 |
160 | **🌟 给项目一个 Star 吧!您的支持是我们持续更新的动力!**
161 | [](https://github.com/huliyoudiangou/TG_Chat_Bot-D1)
162 |
--------------------------------------------------------------------------------
/TG_Chat_Bot-D1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Telegram Bot Worker v3.62 (Final Hardened)
3 | * 优化重点: 话题名称实时同步、无头像渲染修复、高并发数据库写合并、隐私穿透加固
4 | */
5 |
6 | // --- 1. 静态配置与常量 ---
7 | const CACHE = { data: {}, ts: 0, ttl: 60000, user_locks: {} };
8 | const DEFAULTS = {
9 | // 基础
10 | welcome_msg: "欢迎 {name}!使用前请先完成验证。",
11 |
12 | // 验证
13 | enable_verify: "true",
14 | enable_qa_verify: "true",
15 | captcha_mode: "turnstile",
16 | verif_q: "1+1=?\n提示:答案在简介中。",
17 | verif_a: "3",
18 |
19 | // 风控
20 | block_threshold: "5",
21 | enable_admin_receipt: "true",
22 |
23 | // 转发开关
24 | enable_image_forwarding: "true", enable_link_forwarding: "true", enable_text_forwarding: "true",
25 | enable_channel_forwarding: "true", enable_forward_forwarding: "true", enable_audio_forwarding: "true", enable_sticker_forwarding: "true",
26 |
27 | // 话题与列表
28 | backup_group_id: "", unread_topic_id: "", blocked_topic_id: "",
29 | busy_mode: "false", busy_msg: "当前是非营业时间,消息已收到,管理员稍后回复。",
30 | block_keywords: "[]", keyword_responses: "[]", authorized_admins: "[]"
31 | };
32 |
33 | const MSG_TYPES = [
34 | { check: m => m.forward_from || m.forward_from_chat, key: 'enable_forward_forwarding', name: "转发消息", extra: m => m.forward_from_chat?.type === 'channel' ? 'enable_channel_forwarding' : null },
35 | { check: m => m.audio || m.voice, key: 'enable_audio_forwarding', name: "语音/音频" },
36 | { check: m => m.sticker || m.animation, key: 'enable_sticker_forwarding', name: "贴纸/GIF" },
37 | { check: m => m.photo || m.video || m.document, key: 'enable_image_forwarding', name: "媒体文件" },
38 | { check: m => (m.entities||[]).some(e => ['url','text_link'].includes(e.type)), key: 'enable_link_forwarding', name: "链接" },
39 | { check: m => m.text, key: 'enable_text_forwarding', name: "纯文本" }
40 | ];
41 |
42 | // --- 2. 核心入口 ---
43 | export default {
44 | async fetch(req, env, ctx) {
45 | // 数据库初始化 (Non-blocking)
46 | ctx.waitUntil(dbInit(env).catch(err => console.error("DB Init Failed:", err)));
47 |
48 | const url = new URL(req.url);
49 |
50 | try {
51 | if (req.method === "GET") {
52 | if (url.pathname === "/verify") return handleVerifyPage(url, env);
53 | if (url.pathname === "/") return new Response("Bot v3.62 Hardened Active", { status: 200 });
54 | }
55 | if (req.method === "POST") {
56 | if (url.pathname === "/submit_token") return handleTokenSubmit(req, env);
57 |
58 | // 处理 Update
59 | try {
60 | const update = await req.json();
61 | ctx.waitUntil(handleUpdate(update, env, ctx));
62 | return new Response("OK");
63 | } catch (jsonErr) {
64 | console.error("Invalid JSON Update:", jsonErr);
65 | return new Response("Bad Request", { status: 400 });
66 | }
67 | }
68 | } catch (e) {
69 | console.error("Critical Worker Error:", e);
70 | return new Response("Internal Server Error", { status: 500 });
71 | }
72 | return new Response("404 Not Found", { status: 404 });
73 | }
74 | };
75 |
76 | // --- 3. 数据库与工具函数 ---
77 |
78 | const safeParse = (str, fallback = {}) => {
79 | if (!str) return fallback;
80 | try { return JSON.parse(str); }
81 | catch (e) { console.error("JSON Parse Error:", e); return fallback; }
82 | };
83 |
84 | const sql = async (env, query, args = [], type = 'run') => {
85 | try {
86 | const stmt = env.TG_BOT_DB.prepare(query).bind(...(Array.isArray(args) ? args : [args]));
87 | return type === 'run' ? await stmt.run() : await stmt[type]();
88 | } catch (e) {
89 | console.error(`SQL Error [${query}]:`, e);
90 | return null;
91 | }
92 | };
93 |
94 | async function getCfg(key, env) {
95 | const now = Date.now();
96 | if (CACHE.ts && (now - CACHE.ts) < CACHE.ttl && CACHE.data[key] !== undefined) return CACHE.data[key];
97 |
98 | const rows = await sql(env, "SELECT * FROM config", [], 'all');
99 | if (rows && rows.results) {
100 | CACHE.data = {};
101 | rows.results.forEach(r => CACHE.data[r.key] = r.value);
102 | CACHE.ts = now;
103 | }
104 |
105 | const envKey = key.toUpperCase().replace(/_MSG|_Q|_A/, m => ({'_MSG':'_MESSAGE','_Q':'_QUESTION','_A':'_ANSWER'}[m]));
106 | return CACHE.data[key] !== undefined ? CACHE.data[key] : (env[envKey] || DEFAULTS[key] || "");
107 | }
108 |
109 | async function setCfg(key, val, env) {
110 | await sql(env, "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", [key, val]);
111 | CACHE.ts = 0;
112 | }
113 |
114 | async function getUser(id, env) {
115 | let u = await sql(env, "SELECT * FROM users WHERE user_id = ?", id, 'first');
116 | if (!u) {
117 | try { await sql(env, "INSERT OR IGNORE INTO users (user_id, user_state) VALUES (?, 'new')", id); } catch {}
118 | u = await sql(env, "SELECT * FROM users WHERE user_id = ?", id, 'first');
119 | }
120 | if (!u) u = { user_id: id, user_state: 'new', is_blocked: 0, block_count: 0, first_message_sent: 0, topic_id: null, user_info_json: "{}" };
121 |
122 | u.is_blocked = !!u.is_blocked;
123 | u.first_message_sent = !!u.first_message_sent;
124 | u.user_info = safeParse(u.user_info_json);
125 | return u;
126 | }
127 |
128 | async function updUser(id, data, env) {
129 | if (data.user_info) {
130 | data.user_info_json = JSON.stringify(data.user_info);
131 | delete data.user_info;
132 | }
133 | const keys = Object.keys(data);
134 | if (!keys.length) return;
135 |
136 | const query = `UPDATE users SET ${keys.map(k => `${k}=?`).join(',')} WHERE user_id=?`;
137 | const values = [...keys.map(k => typeof data[k] === 'boolean' ? (data[k]?1:0) : data[k]), id];
138 | await sql(env, query, values);
139 | }
140 |
141 | async function dbInit(env) {
142 | if (!env.TG_BOT_DB) return;
143 | await env.TG_BOT_DB.batch([
144 | env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`),
145 | env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS users (user_id TEXT PRIMARY KEY, user_state TEXT DEFAULT 'new', is_blocked INTEGER DEFAULT 0, block_count INTEGER DEFAULT 0, first_message_sent INTEGER DEFAULT 0, topic_id TEXT, user_info_json TEXT)`),
146 | env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message_id TEXT, text TEXT, date INTEGER, PRIMARY KEY (user_id, message_id))`)
147 | ]);
148 | }
149 |
150 | // --- 4. 业务逻辑 (核心流) ---
151 |
152 | async function api(token, method, body) {
153 | try {
154 | const r = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
155 | method: "POST",
156 | headers: { "Content-Type": "application/json" },
157 | body: JSON.stringify(body)
158 | });
159 | const d = await r.json();
160 | if (!d.ok) {
161 | console.warn(`TG API Error [${method}]:`, d.description);
162 | throw new Error(d.description);
163 | }
164 | return d.result;
165 | } catch (e) {
166 | throw e;
167 | }
168 | }
169 |
170 | async function registerCommands(env) {
171 | try {
172 | await api(env.BOT_TOKEN, "deleteMyCommands", { scope: { type: "default" } });
173 | await api(env.BOT_TOKEN, "setMyCommands", { commands: [{ command: "start", description: "开始 / Start" }], scope: { type: "default" } });
174 | const list = [...(env.ADMIN_IDS||"").split(/[,,]/), ...(safeParse(await getCfg('authorized_admins', env), []))];
175 | const admins = [...new Set(list.map(i=>i.trim()).filter(Boolean))];
176 | for (const id of admins) await api(env.BOT_TOKEN, "setMyCommands", { commands: [{ command: "start", description: "⚙️ 管理面板" }, { command: "help", description: "📄 帮助说明" }], scope: { type: "chat", chat_id: id } });
177 | } catch (e) { console.error("Register Commands Failed:", e); }
178 | }
179 |
180 | async function handleUpdate(update, env, ctx) {
181 | const msg = update.message || update.edited_message;
182 | if (!msg) return update.callback_query ? handleCallback(update.callback_query, env) : null;
183 |
184 | // 编辑消息处理
185 | if (update.edited_message) return (msg.chat.type === "private") ? handleEdit(msg, env) : null;
186 |
187 | if (msg.chat.type === "private") await handlePrivate(msg, env, ctx);
188 | else if (msg.chat.id.toString() === env.ADMIN_GROUP_ID) await handleAdminReply(msg, env);
189 | }
190 |
191 | async function handlePrivate(msg, env, ctx) {
192 | const id = msg.chat.id.toString();
193 | const text = msg.text || "";
194 | const isAdm = (env.ADMIN_IDS || "").includes(id);
195 |
196 | // 1. 命令处理
197 | if (text === "/start") {
198 | if (isAdm && ctx) ctx.waitUntil(registerCommands(env));
199 | return isAdm ? handleAdminConfig(id, null, 'menu', null, null, env) : sendStart(id, msg, env);
200 | }
201 | if (text === "/help" && isAdm) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "ℹ️ 帮助\n• 回复消息即对话\n• /start 打开面板", parse_mode: "HTML" });
202 |
203 | // 2. 获取用户状态
204 | const u = await getUser(id, env);
205 |
206 | // 3. [自愈逻辑] 封禁用户重置
207 | if (u.is_blocked) {
208 | if (text === "/start") {
209 | await updUser(id, { is_blocked: 0, user_state: 'new', block_count: 0 }, env);
210 | const mockMeta = { id: id, username: u.user_info.username, first_name: u.user_info.name };
211 | await manageBlacklist(env, u, mockMeta, false);
212 | return sendStart(id, msg, env);
213 | }
214 | return;
215 | }
216 |
217 | // 4. 管理员特权:自动通过验证
218 | if (await isAuthAdmin(id, env)) {
219 | if(u.user_state !== "verified" && !u.user_state.startsWith("pending_")) {
220 | await updUser(id, { user_state: "verified" }, env);
221 | u.user_state = "verified";
222 | }
223 | if(text === "/start" && ctx) ctx.waitUntil(registerCommands(env));
224 | }
225 |
226 | // 5. 管理员状态机输入
227 | if (isAdm) {
228 | const stateStr = await getCfg(`admin_state:${id}`, env);
229 | if (stateStr) {
230 | const state = safeParse(stateStr);
231 | if (state.action === 'input') return handleAdminInput(id, msg, state, env);
232 | }
233 | }
234 |
235 | // 6. 验证逻辑路由
236 | const isCaptchaOn = await getBool('enable_verify', env);
237 | const isQAOn = await getBool('enable_qa_verify', env);
238 |
239 | if (!isCaptchaOn && !isQAOn) {
240 | if (u.user_state !== 'verified') {
241 | await updUser(id, { user_state: "verified" }, env);
242 | u.user_state = "verified";
243 | }
244 | return handleVerifiedMsg(msg, u, env);
245 | }
246 |
247 | if (!isCaptchaOn && isQAOn && (u.user_state === 'new' || u.user_state === 'pending_turnstile')) {
248 | await updUser(id, { user_state: "pending_verification" }, env);
249 | return sendStart(id, msg, env);
250 | }
251 |
252 | const state = u.user_state;
253 | if (['new','pending_turnstile'].includes(state)) {
254 | return sendStart(id, msg, env);
255 | }
256 |
257 | if (state === 'pending_verification') return verifyAnswer(id, text, env);
258 | if (state === 'verified') return handleVerifiedMsg(msg, u, env);
259 | }
260 |
261 | async function sendStart(id, msg, env) {
262 | const u = await getUser(id, env);
263 |
264 | if (u.topic_id) {
265 | const success = await sendInfoCardToTopic(env, u, msg.from, u.topic_id);
266 | if (!success) await updUser(id, { topic_id: null }, env);
267 | }
268 |
269 | let welcomeRaw = await getCfg('welcome_msg', env);
270 | const firstName = (msg.from.first_name || "用户").replace(/&/g,'&').replace(//g,'>');
271 | const nameDisplay = escape(firstName);
272 |
273 | let mediaConfig = null;
274 | let welcomeText = welcomeRaw;
275 | try {
276 | if (welcomeRaw.trim().startsWith('{')) {
277 | mediaConfig = safeParse(welcomeRaw, null);
278 | if(mediaConfig) welcomeText = mediaConfig.caption || "";
279 | }
280 | } catch {}
281 |
282 | welcomeText = welcomeText.replace(/{name}|{user}/g, nameDisplay);
283 |
284 | try {
285 | if (mediaConfig && mediaConfig.type) {
286 | const method = `send${mediaConfig.type.charAt(0).toUpperCase() + mediaConfig.type.slice(1)}`;
287 | let body = { chat_id: id, caption: welcomeText, parse_mode: "HTML" };
288 | if (mediaConfig.type === 'photo') body.photo = mediaConfig.file_id;
289 | else if (mediaConfig.type === 'video') body.video = mediaConfig.file_id;
290 | else if (mediaConfig.type === 'animation') body.animation = mediaConfig.file_id;
291 | else body = { chat_id: id, text: welcomeText, parse_mode: "HTML" };
292 |
293 | await api(env.BOT_TOKEN, method, body);
294 | } else {
295 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: welcomeText, parse_mode: "HTML" });
296 | }
297 | } catch (e) {
298 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "Welcome!", parse_mode: "HTML" });
299 | }
300 |
301 | const url = (env.WORKER_URL || "").replace(/\/$/, '');
302 | const mode = await getCfg('captcha_mode', env);
303 | const hasKey = mode === 'recaptcha' ? env.RECAPTCHA_SITE_KEY : env.TURNSTILE_SITE_KEY;
304 | const isCaptchaOn = await getBool('enable_verify', env);
305 | const isQAOn = await getBool('enable_qa_verify', env);
306 |
307 | if (isCaptchaOn && url && hasKey) {
308 | return api(env.BOT_TOKEN, "sendMessage", {
309 | chat_id: id,
310 | text: "🛡️ 安全验证\n请点击下方按钮完成人机验证以继续。",
311 | parse_mode: "HTML",
312 | reply_markup: { inline_keyboard: [[{ text: "点击进行验证", web_app: { url: `${url}/verify?user_id=${id}` } }]] }
313 | });
314 | } else if (!isCaptchaOn && isQAOn) {
315 | await updUser(id, { user_state: "pending_verification" }, env);
316 | return api(env.BOT_TOKEN, "sendMessage", {
317 | chat_id: id,
318 | text: "❓ 安全提问\n请回答:\n" + await getCfg('verif_q', env),
319 | parse_mode: "HTML"
320 | });
321 | }
322 | }
323 |
324 | async function handleVerifiedMsg(msg, u, env) {
325 | const id = u.user_id, text = msg.text || "";
326 |
327 | // 1. 屏蔽词检测
328 | if (text) {
329 | const kws = await getJsonCfg('block_keywords', env);
330 | if (kws.some(k => new RegExp(k, 'gi').test(text))) {
331 | const c = u.block_count + 1, max = parseInt(await getCfg('block_threshold', env)) || 5;
332 | const willBlock = c >= max;
333 | await updUser(id, { block_count: c, is_blocked: willBlock }, env);
334 | if (willBlock) {
335 | await manageBlacklist(env, u, msg.from, true);
336 | return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 已封禁 (发送 /start 可申请解封)" });
337 | }
338 | return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `⚠️ 屏蔽词 (${c}/${max})` });
339 | }
340 | }
341 |
342 | // 2. 消息类型过滤
343 | for (const t of MSG_TYPES) {
344 | if (t.check(msg)) {
345 | const isAdmin = await isAuthAdmin(id, env);
346 | if ((t.extra && !(await getBool(t.extra(msg), env)) && !isAdmin) ||
347 | (!t.extra && !(await getBool(t.key, env)) && !isAdmin)) {
348 | return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `⚠️ 不接收 ${t.name}` });
349 | }
350 | break;
351 | }
352 | }
353 |
354 | // 3. 忙碌回复
355 | if (await getBool('busy_mode', env)) {
356 | const now = Date.now();
357 | if (now - (u.user_info.last_busy_reply || 0) > 300000) {
358 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "🌙 " + await getCfg('busy_msg', env) });
359 | await updUser(id, { user_info: { ...u.user_info, last_busy_reply: now } }, env);
360 | }
361 | }
362 |
363 | // 4. 自动回复
364 | if (text) {
365 | const rules = await getJsonCfg('keyword_responses', env);
366 | const match = rules.find(r => new RegExp(r.keywords, 'gi').test(text));
367 | if (match) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "自动回复:\n" + match.response });
368 | }
369 |
370 | // 5. 核心转发
371 | await relayToTopic(msg, u, env);
372 | }
373 |
374 | // 核心:消息转发与话题管理 (V3.62 Optimized)
375 | async function relayToTopic(msg, u, env) {
376 | const uMeta = getUMeta(msg.from, u, msg.date), uid = u.user_id;
377 | let tid = u.topic_id;
378 |
379 | // [New] 更新用户信息 & 话题名同步
380 | if (u.user_info.name !== uMeta.name || u.user_info.username !== uMeta.username) {
381 | u.user_info.name = uMeta.name;
382 | u.user_info.username = uMeta.username;
383 | await updUser(uid, { user_info: u.user_info }, env);
384 |
385 | // 关键增强:同步修改话题名称,防止身份漂移
386 | if (u.topic_id) {
387 | api(env.BOT_TOKEN, "editForumTopic", {
388 | chat_id: env.ADMIN_GROUP_ID,
389 | message_thread_id: u.topic_id,
390 | name: uMeta.topicName
391 | }).catch(e => console.warn("Sync Topic Name Failed (Non-fatal):", e));
392 | }
393 | }
394 |
395 | // 1. 创建话题 (如果不存在)
396 | if (!tid) {
397 | if (CACHE.user_locks[uid]) return;
398 | CACHE.user_locks[uid] = true;
399 |
400 | try {
401 | const freshU = await getUser(uid, env);
402 | if (freshU.topic_id) {
403 | tid = freshU.topic_id;
404 | } else {
405 | const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: uMeta.topicName });
406 | tid = t.message_thread_id.toString();
407 |
408 | const dummy = await api(env.BOT_TOKEN, "sendMessage", {
409 | chat_id: env.ADMIN_GROUP_ID,
410 | message_thread_id: tid,
411 | text: "✨ 正在加载用户资料...",
412 | disable_notification: true
413 | });
414 | u.user_info.dummy_msg_id = dummy.message_id;
415 |
416 | await updUser(uid, { topic_id: tid, user_info: u.user_info }, env);
417 | }
418 | } catch (e) {
419 | console.error("Topic Creation Failed:", e);
420 | delete CACHE.user_locks[uid];
421 | return api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "系统忙,请稍后再试" });
422 | }
423 | delete CACHE.user_locks[uid];
424 | }
425 |
426 | // 2. 消息处理与哑消息清理 (顺序优化: 先发卡片 -> 再删哑消息)
427 | try {
428 | // A. 转发用户消息
429 | await api(env.BOT_TOKEN, "forwardMessage", {
430 | chat_id: env.ADMIN_GROUP_ID,
431 | from_chat_id: uid,
432 | message_id: msg.message_id,
433 | message_thread_id: tid
434 | });
435 |
436 | // B. 处理资料卡与哑消息
437 | try {
438 | let infoDirty = false;
439 |
440 | // 1. 优先发送/更新资料卡 (保证话题内有内容)
441 | if (!u.user_info.card_msg_id) {
442 | const cardId = await sendInfoCardToTopic(env, u, msg.from, tid, msg.date);
443 | if (cardId) {
444 | u.user_info.card_msg_id = cardId;
445 | u.user_info.join_date = msg.date || (Date.now()/1000);
446 | infoDirty = true;
447 | }
448 | }
449 |
450 | // 2. 只有在确保有内容后,才清理哑消息
451 | if (u.user_info.dummy_msg_id) {
452 | await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.dummy_msg_id }).catch(() => {});
453 | delete u.user_info.dummy_msg_id;
454 | infoDirty = true;
455 | }
456 |
457 | // 3. 统一入库 (合并写入,减少DB并发)
458 | if (infoDirty) {
459 | await updUser(uid, { user_info: u.user_info }, env);
460 | }
461 |
462 | } catch (cardErr) {
463 | console.warn("Card/Dummy Msg Handling Error:", cardErr);
464 | }
465 |
466 | // C. 后续
467 | api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "✅ 已送达", reply_to_message_id: msg.message_id, disable_notification: true }).catch(()=>{});
468 |
469 | if (msg.text) {
470 | await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date) VALUES (?,?,?,?)", [uid, msg.message_id, msg.text, msg.date]);
471 | }
472 |
473 | await Promise.all([
474 | handleBackup(msg, uMeta, env),
475 | handleInbox(env, msg, u, tid, uMeta)
476 | ]);
477 |
478 | } catch (e) {
479 | console.error("Relay Failed:", e);
480 | if (e.message && e.message.includes("thread")) {
481 | await updUser(uid, { topic_id: null }, env);
482 | api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "会话过期,请重发" });
483 | }
484 | }
485 | }
486 |
487 | // 纯API操作,无副作用,带容错
488 | async function sendInfoCardToTopic(env, u, tgUser, tid, date) {
489 | const meta = getUMeta(tgUser, u, date || (Date.now()/1000));
490 | try {
491 | const card = await api(env.BOT_TOKEN, "sendMessage", {
492 | chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: meta.card, parse_mode: "HTML",
493 | reply_markup: getBtns(u.user_id, u.is_blocked)
494 | });
495 |
496 | // 独立 try-catch,置顶失败不影响流程
497 | try {
498 | await api(env.BOT_TOKEN, "pinChatMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: card.message_id, message_thread_id: tid });
499 | } catch (pinErr) {
500 | console.warn("Pin failed (non-fatal):", pinErr);
501 | }
502 |
503 | return card.message_id;
504 | } catch (e) {
505 | console.error("Send Info Card Failed:", e);
506 | return null;
507 | }
508 | }
509 |
510 | // --- 5. 通知与黑名单 ---
511 | async function handleInbox(env, msg, u, tid, uMeta) {
512 | let inboxId = await getCfg('unread_topic_id', env);
513 | if (!inboxId) {
514 | try {
515 | const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: "🔔 未读消息" });
516 | inboxId = t.message_thread_id.toString();
517 | await setCfg('unread_topic_id', inboxId, env);
518 | } catch { return; }
519 | }
520 |
521 | const now = Date.now();
522 | if (CACHE.user_locks[`in_${u.user_id}`] && now - CACHE.user_locks[`in_${u.user_id}`] < 5000) return;
523 | if (now - (u.user_info.last_notify || 0) < 300000) return;
524 | CACHE.user_locks[`in_${u.user_id}`] = now;
525 |
526 | if (u.user_info.inbox_msg_id) await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.inbox_msg_id }).catch(()=>{});
527 |
528 | const gid = env.ADMIN_GROUP_ID.toString().replace(/^-100/, '');
529 | const preview = msg.text ? (msg.text.length > 20 ? msg.text.substring(0, 20)+"..." : msg.text) : "[媒体]";
530 | const card = `🔔 新消息\n${uMeta.card}\n📝 预览: ${escape(preview)}`;
531 |
532 | try {
533 | const nm = await api(env.BOT_TOKEN, "sendMessage", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: inboxId, text: card, parse_mode: "HTML", reply_markup: { inline_keyboard: [[{ text: "🚀 直达回复", url: `https://t.me/c/${gid}/${tid}` }, { text: "✅ 已阅/删除", callback_data: `inbox:del:${u.user_id}` }]] } });
534 | await updUser(u.user_id, { user_info: { ...u.user_info, last_notify: now, inbox_msg_id: nm.message_id } }, env);
535 | } catch (e) { if(e.message && e.message.includes("thread")) await setCfg('unread_topic_id', "", env); }
536 | }
537 |
538 | async function manageBlacklist(env, u, tgUser, isBlocking) {
539 | let bid = await getCfg('blocked_topic_id', env);
540 | if (!bid && isBlocking) {
541 | try {
542 | const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: "🚫 黑名单" });
543 | bid = t.message_thread_id.toString();
544 | await setCfg('blocked_topic_id', bid, env);
545 | } catch { return; }
546 | }
547 | if (!bid) return;
548 | if (isBlocking) {
549 | const meta = getUMeta(tgUser, u, Date.now()/1000);
550 | const msg = await api(env.BOT_TOKEN, "sendMessage", {
551 | chat_id: env.ADMIN_GROUP_ID, message_thread_id: bid, text: `🚫 用户已屏蔽\n${meta.card}`, parse_mode: "HTML",
552 | reply_markup: { inline_keyboard: [[{ text: "✅ 解除屏蔽", callback_data: `unblock:${u.user_id}` }]] }
553 | });
554 | await updUser(u.user_id, { user_info: { ...u.user_info, blacklist_msg_id: msg.message_id } }, env);
555 | } else {
556 | if (u.user_info.blacklist_msg_id) {
557 | try {
558 | await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.blacklist_msg_id });
559 | } catch (e) { if(e.message && e.message.includes("thread")) await setCfg('blocked_topic_id', "", env); }
560 | await updUser(u.user_id, { user_info: { ...u.user_info, blacklist_msg_id: null } }, env);
561 | }
562 | }
563 | }
564 |
565 | async function handleBackup(msg, meta, env) {
566 | const bid = await getCfg('backup_group_id', env);
567 | if (!bid) return;
568 | try {
569 | const header = `📨 备份 ${meta.name} (${meta.userId})`;
570 | if (msg.text) await api(env.BOT_TOKEN, "sendMessage", { chat_id: bid, text: header + "\n" + msg.text, parse_mode: "HTML" });
571 | else {
572 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: bid, text: header, parse_mode: "HTML" });
573 | await api(env.BOT_TOKEN, "copyMessage", { chat_id: bid, from_chat_id: msg.chat.id, message_id: msg.message_id });
574 | }
575 | } catch (e) { console.warn("Backup failed:", e); }
576 | }
577 |
578 | // --- 6. 管理员功能与状态机 ---
579 | async function handleAdminReply(msg, env) {
580 | if (!msg.message_thread_id || msg.from.is_bot || !(await isAuthAdmin(msg.from.id, env))) return;
581 |
582 | // 检查状态机:是否在输入备注
583 | const stateStr = await getCfg(`admin_state:${msg.from.id}`, env);
584 | if (stateStr) {
585 | const state = safeParse(stateStr);
586 | if (state.action === 'input_note') {
587 | const targetUid = state.target;
588 | const u = await getUser(targetUid, env);
589 |
590 | if (msg.text === '/clear' || msg.text === '清除') delete u.user_info.note;
591 | else u.user_info.note = msg.text;
592 |
593 | const mockTgUser = { id: targetUid, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" };
594 | const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000));
595 |
596 | if (u.topic_id) {
597 | let updated = false;
598 | if (u.user_info.card_msg_id) try {
599 | await api(env.BOT_TOKEN, "editMessageText", {
600 | chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, text: newMeta.card, parse_mode: "HTML", reply_markup: getBtns(targetUid, u.is_blocked)
601 | });
602 | updated = true;
603 | } catch {}
604 | if (!updated) {
605 | const cid = await sendInfoCardToTopic(env, u, mockTgUser, u.topic_id, u.user_info.join_date);
606 | if(cid) u.user_info.card_msg_id = cid;
607 | }
608 | }
609 |
610 | if (u.user_info.inbox_msg_id) {
611 | const gid = env.ADMIN_GROUP_ID.toString().replace(/^-100/, '');
612 | await api(env.BOT_TOKEN, "editMessageText", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.inbox_msg_id, text: `🔔 新消息\n${newMeta.card}\n📝 备注更新`, parse_mode: "HTML", reply_markup: { inline_keyboard: [[{ text: "🚀 直达回复", url: `https://t.me/c/${gid}/${u.topic_id}` }, { text: "✅ 已阅/删除", callback_data: `inbox:del:${targetUid}` }]] } }).catch(()=>{});
613 | }
614 |
615 | await updUser(targetUid, { user_info: u.user_info }, env);
616 | await setCfg(`admin_state:${msg.from.id}`, "", env);
617 | return api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: `✅ 备注已更新` });
618 | }
619 | }
620 |
621 | // 常规回复转发
622 | const uid = (await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'))?.user_id;
623 | if (!uid) return;
624 | try {
625 | await api(env.BOT_TOKEN, "copyMessage", { chat_id: uid, from_chat_id: msg.chat.id, message_id: msg.message_id });
626 | if (await getBool('enable_admin_receipt', env)) api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "✅ 已回复", reply_to_message_id: msg.message_id, disable_notification: true }).catch(()=>{});
627 | } catch (e) { api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "❌ 发送失败" }); }
628 | }
629 |
630 | async function handleEdit(msg, env) {
631 | const u = await getUser(msg.from.id.toString(), env);
632 | if (!u.topic_id) return;
633 | const old = await sql(env, "SELECT text FROM messages WHERE user_id=? AND message_id=?", [u.user_id, msg.message_id], 'first');
634 | const newTxt = msg.text || msg.caption || "[非文本]";
635 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: u.topic_id, text: `✏️ 消息修改\n前: ${escape(old?.text||"?")}\n后: ${escape(newTxt)}`, parse_mode: "HTML" });
636 | }
637 |
638 | // --- 7. Web验证接口 ---
639 | async function handleVerifyPage(url, env) {
640 | const uid = url.searchParams.get('user_id');
641 | const mode = await getCfg('captcha_mode', env);
642 | const siteKey = mode === 'recaptcha' ? env.RECAPTCHA_SITE_KEY : env.TURNSTILE_SITE_KEY;
643 | if (!uid || !siteKey) return new Response("Miss Config (Check Mode/Key)", { status: 400 });
644 | const scriptUrl = mode === 'recaptcha' ? "https://www.google.com/recaptcha/api.js" : "https://challenges.cloudflare.com/turnstile/v0/api.js";
645 | const divClass = mode === 'recaptcha' ? "g-recaptcha" : "cf-turnstile";
646 |
647 | const html = `
${escape(name)}`;
896 | const note = dbUser.user_info && dbUser.user_info.note ? `\n📝 备注: ${escape(dbUser.user_info.note)}` : "";
897 | const labelDisplay = tgUser.username ? `@${tgUser.username}` : "无用户名";
898 | const timeStr = new Date(d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
899 | return {
900 | userId: id, name, username: tgUser.username,
901 | topicName: `${name} | ${id}`.substring(0, 128),
902 | card: `🪪 用户资料\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: ${id}${note}\n🕒: ${timeStr}`
903 | };
904 | };
905 |
906 | const getBtns = (id, blk) => ({ inline_keyboard: [[{ text: "👤 打开主页", url: `tg://user?id=${id}` }], [{ text: blk?"✅ 解封":"🚫 屏蔽", callback_data: `${blk?'unblock':'block'}:${id}` }], [{ text: "✏️ 备注", callback_data: `note:set:${id}` }, { text: "📌 置顶", callback_data: `pin_card:${id}` }]] });
907 |
--------------------------------------------------------------------------------