├── README.md └── TG_Chat_Bot-D1.js /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Telegram 双向机器人 Cloudflare Worker 2 | 3 | [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-FFC425?logo=cloudflare)](https://workers.cloudflare.com/) 4 | [![Telegram Bot API](https://img.shields.io/badge/Telegram-Bot_API-3AB3E0?logo=telegram)](https://core.telegram.org/bots/api) 5 | [![D1 Database](https://img.shields.io/badge/D1-Database-FFC425?logo=cloudflare)](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 | [![GitHub stars](https://img.shields.io/github/stars/your-repo/telegram-cf-worker?style=social)](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 = `

🛡️ 安全验证

`; 648 | return new Response(html, { headers: { "Content-Type": "text/html" } }); 649 | } 650 | 651 | async function handleTokenSubmit(req, env) { 652 | try { 653 | const { token, userId } = await req.json(); 654 | const mode = await getCfg('captcha_mode', env); 655 | let success = false; 656 | 657 | const verifyUrl = mode === 'recaptcha' ? 'https://www.google.com/recaptcha/api/siteverify' : 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; 658 | const params = mode === 'recaptcha' 659 | ? new URLSearchParams({ secret: env.RECAPTCHA_SECRET_KEY, response: token }) 660 | : JSON.stringify({ secret: env.TURNSTILE_SECRET_KEY, response: token }); 661 | const headers = mode === 'recaptcha' 662 | ? { 'Content-Type': 'application/x-www-form-urlencoded' } 663 | : { 'Content-Type': 'application/json' }; 664 | 665 | const r = await fetch(verifyUrl, { method: 'POST', headers, body: params }); 666 | const d = await r.json(); 667 | success = d.success; 668 | 669 | if (!success) throw new Error("Invalid Token"); 670 | 671 | if (await getBool('enable_qa_verify', env)) { 672 | await updUser(userId, { user_state: "pending_verification" }, env); 673 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: userId, text: "✅ 验证通过!\n请回答:\n" + await getCfg('verif_q', env) }); 674 | } else { 675 | await updUser(userId, { user_state: "verified" }, env); 676 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: userId, text: "✅ 验证通过!\n现在您可以直接发送消息,我会帮您转达给管理员。" }); 677 | } 678 | return new Response(JSON.stringify({ success: true })); 679 | } catch { return new Response(JSON.stringify({ success: false }), { status: 400 }); } 680 | } 681 | 682 | async function verifyAnswer(id, ans, env) { 683 | if (ans.trim() === (await getCfg('verif_a', env)).trim()) { 684 | await updUser(id, { user_state: "verified" }, env); 685 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "✅ 验证通过!\n现在您可以直接发送消息,我会帮您转达给管理员。" }); 686 | } else await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 错误" }); 687 | } 688 | 689 | // --- 8. 菜单回调处理 --- 690 | async function handleCallback(cb, env) { 691 | const { data, message: msg, from } = cb; 692 | const [act, p1, p2, p3] = data.split(':'); 693 | 694 | // 收件箱操作 695 | if (act === 'inbox' && p1 === 'del') { 696 | await api(env.BOT_TOKEN, "deleteMessage", { chat_id: msg.chat.id, message_id: msg.message_id }).catch(()=>{}); 697 | if (p2) { const u = await getUser(p2, env); await updUser(p2, { user_info: { ...u.user_info, last_notify: 0 } }, env); } 698 | return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "已处理" }); 699 | } 700 | 701 | // 备注操作 702 | if (act === 'note' && p1 === 'set') { 703 | await setCfg(`admin_state:${from.id}`, JSON.stringify({ action: 'input_note', target: p2 }), env); 704 | return api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "⌨️ 请回复备注内容 (回复 /clear 清除):" }); 705 | } 706 | 707 | // 配置操作 708 | if (act === 'config') { 709 | if (!(env.ADMIN_IDS||"").includes(from.id.toString())) return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "无权", show_alert: true }); 710 | 711 | if (p1 === 'rotate_mode') { 712 | const currentMode = await getCfg('captcha_mode', env); 713 | const isEnabled = await getBool('enable_verify', env); 714 | 715 | let nextMode = 'turnstile'; 716 | let nextEnable = 'true'; 717 | let toast = "已切换: Cloudflare"; 718 | if (isEnabled) { 719 | if (currentMode === 'turnstile') { 720 | nextMode = 'recaptcha'; 721 | toast = "已切换: Google Recaptcha"; 722 | } else { 723 | nextEnable = 'false'; 724 | nextMode = currentMode; 725 | toast = "验证码功能已关闭"; 726 | } 727 | } else { 728 | nextMode = 'turnstile'; 729 | nextEnable = 'true'; 730 | toast = "已切换: Cloudflare"; 731 | } 732 | 733 | await setCfg('captcha_mode', nextMode, env); 734 | await setCfg('enable_verify', nextEnable, env); 735 | await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: toast }); 736 | return handleAdminConfig(msg.chat.id, msg.message_id, 'menu', 'base', null, env); 737 | } 738 | 739 | await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id }); 740 | return handleAdminConfig(msg.chat.id, msg.message_id, p1, p2, p3, env); 741 | } 742 | 743 | if (msg.chat.id.toString() === env.ADMIN_GROUP_ID) { 744 | await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id }); 745 | if (act === 'pin_card') api(env.BOT_TOKEN, "pinChatMessage", { chat_id: msg.chat.id, message_id: msg.message_id, message_thread_id: msg.message_thread_id }); 746 | else if (['block','unblock'].includes(act)) { 747 | const isB = act === 'block'; 748 | const uid = p1; 749 | const u = await getUser(uid, env); 750 | const bid = await getCfg('blocked_topic_id', env); 751 | await updUser(uid, { is_blocked: isB, block_count: 0 }, env); 752 | 753 | if (u.user_info.card_msg_id) { 754 | api(env.BOT_TOKEN, "editMessageReplyMarkup", { 755 | chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, reply_markup: getBtns(uid, isB) 756 | }).catch(()=>{}); 757 | } 758 | 759 | await manageBlacklist(env, u, { id: uid, username: u.user_info.username, first_name: u.user_info.name }, isB); 760 | if (!isB && msg.message_thread_id && bid && msg.message_thread_id.toString() === bid) { 761 | api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "✅ 已解除屏蔽" }); 762 | } else { 763 | api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: isB ? "❌ 已屏蔽" : "✅ 已解封" }); 764 | } 765 | } 766 | } 767 | } 768 | 769 | // --- 9. 管理配置面板逻辑 --- 770 | async function handleAdminConfig(cid, mid, type, key, val, env) { 771 | const render = (txt, kb) => api(env.BOT_TOKEN, mid?"editMessageText":"sendMessage", { chat_id: cid, message_id: mid, text: txt, parse_mode: "HTML", reply_markup: kb }); 772 | const back = { text: "🔙 返回", callback_data: "config:menu" }; 773 | try { 774 | if (!type || type === 'menu') { 775 | if (!key) return render("⚙️ 控制面板", { inline_keyboard: [[{text:"📝 基础",callback_data:"config:menu:base"},{text:"🤖 自动回复",callback_data:"config:menu:ar"}], [{text:"🚫 屏蔽词",callback_data:"config:menu:kw"},{text:"🛠 过滤",callback_data:"config:menu:fl"}], [{text:"👮 协管",callback_data:"config:menu:auth"},{text:"💾 备份/通知",callback_data:"config:menu:bak"}], [{text:"🌙 营业状态",callback_data:"config:menu:busy"}]] }); 776 | if (key === 'base') { 777 | const mode = await getCfg('captcha_mode', env); 778 | const captchaOn = await getBool('enable_verify', env); 779 | const qaOn = await getBool('enable_qa_verify', env); 780 | let statusText = "❌ 已关闭"; 781 | if (captchaOn) statusText = mode === 'recaptcha' ? "Google" : "Cloudflare"; 782 | return render(`基础配置\n验证码模式: ${statusText}\n问题验证: ${qaOn?"✅":"❌"}`, { inline_keyboard: [ 783 | [{text:"欢迎语",callback_data:"config:edit:welcome_msg"},{text:"问题",callback_data:"config:edit:verif_q"},{text:"答案",callback_data:"config:edit:verif_a"}], 784 | [{text: `验证码模式: ${statusText} (点击切换)`, callback_data:`config:rotate_mode`}], 785 | [{text: `问题验证: ${qaOn?"✅ 开启":"❌ 关闭"}`, callback_data:`config:toggle:enable_qa_verify:${!qaOn}`}], 786 | [back] 787 | ] }); 788 | } 789 | if (key === 'fl') return render("🛠 过滤设置", await getFilterKB(env)); 790 | if (['ar','kw','auth'].includes(key)) return render(`列表: ${key}`, await getListKB(key, env)); 791 | if (key === 'bak') { 792 | const bid = await getCfg('backup_group_id', env), uid = await getCfg('unread_topic_id', env), blk = await getCfg('blocked_topic_id', env); 793 | return render(`💾 备份与通知\n备份群: ${bid||"无"}\n未读话题: ${uid?`✅ (${uid})`:"⏳"}\n黑名单话题: ${blk?`✅ (${blk})`:"⏳"}`, { inline_keyboard: [[{text:"设备份群",callback_data:"config:edit:backup_group_id"},{text:"清备份",callback_data:"config:cl:backup_group_id"}],[{text:"重置聚合话题",callback_data:"config:cl:unread_topic_id"},{text:"重置黑名单",callback_data:"config:cl:blocked_topic_id"}],[back]] }); 794 | } 795 | if (key === 'busy') { 796 | const on = await getBool('busy_mode', env), msg = await getCfg('busy_msg', env); 797 | return render(`🌙 营业状态\n当前: ${on?"🔴 休息中":"🟢 营业中"}\n回复语: ${escape(msg)}`, { inline_keyboard: [[{text:`切换为 ${on?"🟢 营业":"🔴 休息"}`,callback_data:`config:toggle:busy_mode:${!on}`}], [{text:"✏️ 修改回复语",callback_data:"config:edit:busy_msg"}], [back]] }); 798 | } 799 | } 800 | 801 | if (type === 'toggle') { await setCfg(key, val, env); 802 | return key==='busy_mode' ? handleAdminConfig(cid,mid,'menu','busy',null,env) : (key==='enable_qa_verify' ? handleAdminConfig(cid,mid,'menu','base',null,env) : render("🛠 过滤设置", await getFilterKB(env))); 803 | } 804 | if (type === 'cl') { await setCfg(key, key==='authorized_admins'?'[]':'', env); 805 | return handleAdminConfig(cid, mid, 'menu', key==='unread_topic_id'||key==='blocked_topic_id'?'bak':(key==='authorized_admins'?'auth':'bak'), null, env); } 806 | if (type === 'del') { 807 | const realK = key==='kw'?'block_keywords':(key==='auth'?'authorized_admins':'keyword_responses'); 808 | let l = await getJsonCfg(realK, env); 809 | l = l.filter(i => (i.id||i).toString() !== val); 810 | await setCfg(realK, JSON.stringify(l), env); 811 | return render(`列表: ${key}`, await getListKB(key, env)); 812 | } 813 | if (type === 'edit' || type === 'add') { 814 | await setCfg(`admin_state:${cid}`, JSON.stringify({ action: 'input', key: key + (type==='add'?'_add':'') }), env); 815 | let promptText = `请输入 ${key} 的值 (/cancel 取消):`; 816 | if (key === 'ar' && type === 'add') promptText = `请输入自动回复规则,格式:\n关键词===回复内容\n\n例如:价格===请联系人工客服\n(/cancel 取消)`; 817 | if (key === 'welcome_msg') promptText = `请发送新的欢迎语 (/cancel 取消):\n\n• 支持 文字图片/视频/GIF\n• 支持占位符: {name}\n• 直接发送媒体即可`; 818 | return api(env.BOT_TOKEN, "editMessageText", { chat_id: cid, message_id: mid, text: promptText, parse_mode: "HTML" }); 819 | } 820 | } catch (e) { api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: mid, text: "Error", show_alert: true }); } 821 | } 822 | 823 | async function getFilterKB(env) { 824 | const s = async k => (await getBool(k, env)) ? "✅" : "❌"; 825 | const b = (t, k, v) => ({ text: `${t} ${v}`, callback_data: `config:toggle:${k}:${v==="❌"}` }); 826 | const keys = ['enable_admin_receipt', 'enable_forward_forwarding', 'enable_image_forwarding', 'enable_audio_forwarding', 'enable_sticker_forwarding', 'enable_link_forwarding', 'enable_channel_forwarding', 'enable_text_forwarding']; 827 | const vals = await Promise.all(keys.map(k => s(k))); 828 | return { inline_keyboard: [[b("回执", keys[0], vals[0]), b("转发", keys[1], vals[1])], [b("媒体", keys[2], vals[2]), b("语音", keys[3], vals[3])], [b("贴纸", keys[4], vals[4]), b("链接", keys[5], vals[5])], [b("频道", keys[6], vals[6]), b("文本", keys[7], vals[7])], [{ text: "🔙 返回", callback_data: "config:menu" }]] }; 829 | } 830 | 831 | async function getListKB(type, env) { 832 | const k = type==='ar'?'keyword_responses':(type==='kw'?'block_keywords':'authorized_admins'); 833 | const l = await getJsonCfg(k, env); 834 | const btns = l.map((i, idx) => [{ text: `🗑 ${type==='ar'?i.keywords:i}`, callback_data: `config:del:${type}:${i.id||i}` }]); 835 | btns.push([{ text: "➕ 添加", callback_data: `config:add:${type}` }], [{ text: "🔙 返回", callback_data: "config:menu" }]); 836 | return { inline_keyboard: btns }; 837 | } 838 | 839 | async function handleAdminInput(id, msg, state, env) { 840 | const txt = msg.text || ""; 841 | if (txt === '/cancel') { await sql(env, "DELETE FROM config WHERE key=?", `admin_state:${id}`); 842 | return handleAdminConfig(id, null, 'menu', null, null, env); } 843 | 844 | let k = state.key, val = txt; 845 | try { 846 | if (k === 'welcome_msg') { 847 | if (msg.photo || msg.video || msg.animation) { 848 | let fileId, type; 849 | if (msg.photo) { type = 'photo'; fileId = msg.photo[msg.photo.length - 1].file_id; } 850 | else if (msg.video) { type = 'video'; fileId = msg.video.file_id; } 851 | else if (msg.animation) { type = 'animation'; fileId = msg.animation.file_id; } 852 | val = JSON.stringify({ type: type, file_id: fileId, caption: msg.caption || "" }); 853 | } else { val = txt; } 854 | } 855 | else if (k.endsWith('_add')) { 856 | k = k.replace('_add', ''); 857 | const realK = k==='ar'?'keyword_responses':(k==='kw'?'block_keywords':'authorized_admins'); 858 | const list = await getJsonCfg(realK, env); 859 | if (k === 'ar') { 860 | const [kk, rr] = txt.split('==='); 861 | if(kk && rr) list.push({keywords:kk, response:rr, id:Date.now()}); 862 | else return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 格式错误,请使用:关键词===回复内容" }); 863 | } 864 | else list.push(txt); 865 | val = JSON.stringify(list); k = realK; 866 | } else if (k === 'authorized_admins') { 867 | val = JSON.stringify(txt.split(/[,,]/).map(s => s.trim()).filter(Boolean)); 868 | } 869 | 870 | await setCfg(k, val, env); 871 | await sql(env, "DELETE FROM config WHERE key=?", `admin_state:${id}`); 872 | const displayVal = (val.startsWith('{') && k === 'welcome_msg') ? "[媒体配置]" : val.substring(0,100); 873 | await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `✅ ${k} 已更新:\n${displayVal}` }); 874 | await handleAdminConfig(id, null, 'menu', null, null, env); 875 | } catch (e) { api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `❌ 失败: ${e.message}` }); } 876 | } 877 | 878 | // --- 10. 工具函数 (Pure Functions) --- 879 | const getBool = async (k, e) => (await getCfg(k, e)) === 'true'; 880 | const getJsonCfg = async (k, e) => safeParse(await getCfg(k, e), []); 881 | const escape = t => (t||"").toString().replace(/&/g,'&').replace(//g,'>'); 882 | const isAuthAdmin = async (id, e) => { 883 | const idStr = id.toString(); 884 | if ((e.ADMIN_IDS||"").includes(idStr)) return true; 885 | const list = await getJsonCfg('authorized_admins', e); 886 | return list.includes(idStr); 887 | }; 888 | 889 | const getUMeta = (tgUser, dbUser, d) => { 890 | const id = tgUser.id.toString(); 891 | const firstName = tgUser.first_name || ""; 892 | const lastName = tgUser.last_name || ""; 893 | let name = (firstName + " " + lastName).trim(); 894 | if (!name) name = "未命名用户"; 895 | const safeName = `${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 | --------------------------------------------------------------------------------