diff --git a/tg-bot.js b/tg-bot.js new file mode 100644 index 0000000..bbcd4cf --- /dev/null +++ b/tg-bot.js @@ -0,0 +1,898 @@ +/** + * Telegram Bot Worker v3.67 (No-Receipt Mod & No-Username Fix) + * 完整功能版:保留所有备份、配置面板、辅助函数 + * * 修改 1: 修复了无用户名用户无法推送卡片的问题 + * * 修改 2: 彻底移除了管理员回复的“✅ 已回复”提示 + * * 修改 3: 彻底移除了用户发送消息后的“✅ 已送达”回执 + */ + +// --- 1. 静态配置与常量 --- +// 缓存系统,用于减少数据库读写压力,降低 Worker KV/D1 计费 +const CACHE = { data: {}, ts: 0, ttl: 60000, user_locks: {}, warn_cd: {} }; + +const DEFAULTS = { + // 基础设置 + welcome_msg: "欢迎 {name}!使用前请先完成验证。", + + // 验证设置 + enable_verify: "true", + enable_qa_verify: "true", + captcha_mode: "turnstile", + verif_q: "1+1=?\n提示:答案在简介中。", + verif_a: "3", + + // 风控设置 + block_threshold: "5", + enable_admin_receipt: "false", // 默认关闭管理员回执 + + // 转发类型开关配置 + enable_image_forwarding: "true", enable_link_forwarding: "true", enable_text_forwarding: "true", + enable_channel_forwarding: "true", enable_forward_forwarding: "true", enable_audio_forwarding: "true", enable_sticker_forwarding: "true", + + // 话题与列表存储占位 + backup_group_id: "", blocked_topic_id: "", + busy_mode: "false", busy_msg: "当前是非营业时间,消息已收到,管理员稍后回复。", + block_keywords: "[]", keyword_responses: "[]", authorized_admins: "[]" +}; + +// 消息类型检查与映射字典 +const MSG_TYPES = [ + { 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 }, + { check: m => m.audio || m.voice, key: 'enable_audio_forwarding', name: "语音/音频" }, + { check: m => m.sticker || m.animation, key: 'enable_sticker_forwarding', name: "贴纸/GIF" }, + { check: m => m.photo || m.video || m.document, key: 'enable_image_forwarding', name: "媒体文件" }, + { check: m => (m.entities||[]).some(e => ['url','text_link'].includes(e.type)), key: 'enable_link_forwarding', name: "链接" }, + { check: m => m.text, key: 'enable_text_forwarding', name: "纯文本" } +]; + +// --- 2. 核心入口 (Entry Point) --- +export default { + async fetch(req, env, ctx) { + // 确保数据库初始化完毕,waitUntil不会阻塞主线程的即时响应 + ctx.waitUntil(dbInit(env).catch(err => console.error("DB Init Failed:", err))); + const url = new URL(req.url); + + try { + // GET 请求处理:验证页面加载或连通性测试 + if (req.method === "GET") { + if (url.pathname === "/verify") return handleVerifyPage(url, env); + if (url.pathname === "/") return new Response("Bot v3.67 Active", { status: 200 }); + } + // POST 请求处理:Telegram Webhook 核心逻辑接收端 + if (req.method === "POST") { + if (url.pathname === "/submit_token") return handleTokenSubmit(req, env); + try { + const update = await req.json(); + ctx.waitUntil(handleUpdate(update, env, ctx)); + return new Response("OK"); + } catch (jsonErr) { + console.error("Invalid JSON Update:", jsonErr); + return new Response("Bad Request", { status: 400 }); + } + } + } catch (e) { + console.error("Critical Worker Error:", e); + return new Response("Internal Server Error", { status: 500 }); + } + return new Response("404 Not Found", { status: 404 }); + } +}; + +// --- 3. 数据库与工具函数 --- +// 安全解析JSON,避免非规范格式导致脚本执行中断 +const safeParse = (str, fallback = {}) => { + if (!str) return fallback; + try { return JSON.parse(str); } + catch (e) { console.error("JSON Parse Error:", e); return fallback; } +}; + +// SQL 执行封装,支持不同的运行类型 (run, all, first) +const sql = async (env, query, args = [], type = 'run') => { + try { + const stmt = env.TG_BOT_DB.prepare(query).bind(...(Array.isArray(args) ? args : [args])); + return type === 'run' ? await stmt.run() : await stmt[type](); + } catch (e) { + console.error(`SQL Error [${query}]:`, e); + return null; + } +}; + +// 获取配置项:优先命中内存缓存以提升响应速度 +async function getCfg(key, env) { + const now = Date.now(); + if (CACHE.ts && (now - CACHE.ts) < CACHE.ttl && CACHE.data[key] !== undefined) return CACHE.data[key]; + + const rows = await sql(env, "SELECT * FROM config", [], 'all'); + if (rows && rows.results) { + CACHE.data = {}; + rows.results.forEach(r => CACHE.data[r.key] = r.value); + CACHE.ts = now; + } + const envKey = key.toUpperCase().replace(/_MSG|_Q|_A/, m => ({'_MSG':'_MESSAGE','_Q':'_QUESTION','_A':'_ANSWER'}[m])); + return CACHE.data[key] !== undefined ? CACHE.data[key] : (env[envKey] || DEFAULTS[key] || ""); +} + +// 设置配置项:同步使当前内存缓存失效 +async function setCfg(key, val, env) { + await sql(env, "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", [key, val]); + CACHE.ts = 0; +} + +// 获取或初始化用户信息实体 +async function getUser(id, env) { + let u = await sql(env, "SELECT * FROM users WHERE user_id = ?", id, 'first'); + if (!u) { + try { await sql(env, "INSERT OR IGNORE INTO users (user_id, user_state) VALUES (?, 'new')", id); } catch {} + u = await sql(env, "SELECT * FROM users WHERE user_id = ?", id, 'first'); + } + 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: "{}" }; + + // 布尔状态类型转换及附属 JSON 解析 + u.is_blocked = !!u.is_blocked; + u.first_message_sent = !!u.first_message_sent; + u.user_info = safeParse(u.user_info_json); + return u; +} + +// 增量更新用户信息记录 +async function updUser(id, data, env) { + if (data.user_info) { + data.user_info_json = JSON.stringify(data.user_info); + delete data.user_info; + } + const keys = Object.keys(data); + if (!keys.length) return; + const query = `UPDATE users SET ${keys.map(k => `${k}=?`).join(',')} WHERE user_id=?`; + const values = [...keys.map(k => typeof data[k] === 'boolean' ? (data[k]?1:0) : data[k]), id]; + await sql(env, query, values); +} + +// 数据库表结构初始化与防御性兼容处理 +async function dbInit(env) { + if (!env.TG_BOT_DB) return; + await env.TG_BOT_DB.batch([ + env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`), + 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)`), + 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))`) + ]); + try { await sql(env, "ALTER TABLE messages ADD COLUMN topic_message_id TEXT"); } catch (e) {} +} + +// --- 4. 业务逻辑 (核心流) --- + +// Telegram Bot API 原生请求封装 +async function api(token, method, body) { + try { + const r = await fetch(`https://api.telegram.org/bot${token}/${method}`, { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) + }); + const d = await r.json(); + if (!d.ok) { console.warn(`TG API Error [${method}]:`, d.description); throw new Error(d.description); } + return d.result; + } catch (e) { throw e; } +} + +// 自动向 Telegram 注册快捷菜单命令 +async function registerCommands(env) { + try { + await api(env.BOT_TOKEN, "deleteMyCommands", { scope: { type: "default" } }); + await api(env.BOT_TOKEN, "setMyCommands", { commands: [{ command: "start", description: "开始 / Start" }], scope: { type: "default" } }); + const list = [...(env.ADMIN_IDS||"").split(/[,,]/), ...(safeParse(await getCfg('authorized_admins', env), []))]; + const admins = [...new Set(list.map(i=>i.trim()).filter(Boolean))]; + for (const id of admins) await api(env.BOT_TOKEN, "setMyCommands", { commands: [{ command: "start", description: "⚙️ 管理面板" }, { command: "help", description: "📄 帮助说明" }], scope: { type: "chat", chat_id: id } }); + } catch (e) { console.error("Register Commands Failed:", e); } +} + +// 全局更新对象分发调度中心 +async function handleUpdate(update, env, ctx) { + const msg = update.message || update.edited_message; + if (!msg) return update.callback_query ? handleCallback(update.callback_query, env) : null; + + // 监听管理员侧的消息变更事件 + if (update.edited_message && msg.chat.id.toString() === env.ADMIN_GROUP_ID) { + return handleAdminEdit(msg, env); + } + + // 监听用户侧的消息变更事件 + if (update.edited_message) return (msg.chat.type === "private") ? handleEdit(msg, env) : null; + + // 会话路由 + if (msg.chat.type === "private") await handlePrivate(msg, env, ctx); + else if (msg.chat.id.toString() === env.ADMIN_GROUP_ID) await handleAdminReply(msg, env); +} + +// 管理员编辑群组内消息时的逻辑,主动通知用户变更内容 +async function handleAdminEdit(msg, env) { + if (!msg.message_thread_id) return; + const u = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'); + if (!u) return; + + const newText = msg.text || msg.caption || "[媒体消息]"; + await api(env.BOT_TOKEN, "sendMessage", { + chat_id: u.user_id, + text: `✏️ 对方修改了消息\n内容: ${escape(newText)}`, + parse_mode: "HTML" + }); +} + +// 私聊消息处理总线 (用户侧逻辑入口) +async function handlePrivate(msg, env, ctx) { + const id = msg.chat.id.toString(); + const text = msg.text || ""; + const isAdm = (env.ADMIN_IDS || "").includes(id); + const u = await getUser(id, env); + + // 人机验证拦截器 (非管理人员未完成验证则阻断) + if (text !== "/start" && !isAdm) { + const isCaptchaOn = await getBool('enable_verify', env); + const isQAOn = await getBool('enable_qa_verify', env); + + if ((isCaptchaOn || isQAOn) && u.user_state !== 'verified') { + const now = Date.now(); + const lastWarn = CACHE.warn_cd[id] || 0; + // 设定3秒冷却限流阈值,防止恶意并发攻击刷量 + if (now - lastWarn < 3000) return; + CACHE.warn_cd[id] = now; + return api(env.BOT_TOKEN, "sendMessage", { + chat_id: id, + text: "❗️❗️❗️请先进行验证,再发送消息", + reply_to_message_id: msg.message_id + }); + } + } + + // 1. 命令显式路由处理 + if (text === "/start") { + if (isAdm && ctx) ctx.waitUntil(registerCommands(env)); + if (!isAdm) { + await updUser(id, { user_state: 'new' }, env); + u.user_state = 'new'; + } + return isAdm ? handleAdminConfig(id, null, 'menu', null, null, env) : sendStart(id, msg, env); + } + if (text === "/help" && isAdm) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "ℹ️ 帮助\n• 回复消息即对话\n• /start 打开面板", parse_mode: "HTML" }); + + // 2. 封禁拦截层 + if (u.is_blocked) { + return; + } + + // 3. 授权状态补偿验证 + if (await isAuthAdmin(id, env)) { + if(u.user_state !== "verified") await updUser(id, { user_state: "verified" }, env); + if(text === "/start" && ctx) ctx.waitUntil(registerCommands(env)); + } + + // 4. 管理员配置状态机判定(面板输入模式截取) + if (isAdm) { + const stateStr = await getCfg(`admin_state:${id}`, env); + if (stateStr) { + const state = safeParse(stateStr); + if (state.action === 'input') return handleAdminInput(id, msg, state, env); + } + } + + // 5. 常规状态验证路由 + const isCaptchaOn = await getBool('enable_verify', env); + const isQAOn = await getBool('enable_qa_verify', env); + + if (!isCaptchaOn && !isQAOn) { + if (u.user_state !== 'verified') await updUser(id, { user_state: "verified" }, env); + return handleVerifiedMsg(msg, u, env); + } + + const state = u.user_state; + if (state === 'pending_verification') return verifyAnswer(id, text, env); + if (state === 'verified') return handleVerifiedMsg(msg, u, env); + + return sendStart(id, msg, env); +} + +// 首次接入:渲染欢迎页面与验证环节 +async function sendStart(id, msg, env) { + const u = await getUser(id, env); + if (u.topic_id) { + try { + const success = await sendInfoCardToTopic(env, u, msg.from, u.topic_id); + if (!success) await updUser(id, { topic_id: null }, env); + } catch (e) { await updUser(id, { topic_id: null }, env); } + } + + let welcomeRaw = await getCfg('welcome_msg', env); + const firstName = (msg.from.first_name || "用户").replace(/&/g,'&').replace(//g,'>'); + const nameDisplay = escape(firstName); + + let mediaConfig = null; + let welcomeText = welcomeRaw; + try { + if (welcomeRaw.trim().startsWith('{')) { + mediaConfig = safeParse(welcomeRaw, null); + if(mediaConfig) welcomeText = mediaConfig.caption || ""; + } + } catch {} + + welcomeText = welcomeText.replace(/{name}|{user}/g, nameDisplay); + + try { + if (mediaConfig && mediaConfig.type) { + const method = `send${mediaConfig.type.charAt(0).toUpperCase() + mediaConfig.type.slice(1)}`; + let body = { chat_id: id, caption: welcomeText, parse_mode: "HTML" }; + if (mediaConfig.type === 'photo') body.photo = mediaConfig.file_id; + else if (mediaConfig.type === 'video') body.video = mediaConfig.file_id; + else if (mediaConfig.type === 'animation') body.animation = mediaConfig.file_id; + else body = { chat_id: id, text: welcomeText, parse_mode: "HTML" }; + await api(env.BOT_TOKEN, method, body); + } else { + await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: welcomeText, parse_mode: "HTML" }); + } + } catch (e) { + await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "Welcome!", parse_mode: "HTML" }); + } + + const url = (env.WORKER_URL || "").replace(/\/$/, ''); + const mode = await getCfg('captcha_mode', env); + const hasKey = mode === 'recaptcha' ? env.RECAPTCHA_SITE_KEY : env.TURNSTILE_SITE_KEY; + const isCaptchaOn = await getBool('enable_verify', env); + const isQAOn = await getBool('enable_qa_verify', env); + + if (isCaptchaOn && url && hasKey) { + return api(env.BOT_TOKEN, "sendMessage", { + chat_id: id, + text: "🛡️ 安全验证\n请点击下方按钮完成人机验证以继续。", + parse_mode: "HTML", + reply_markup: { inline_keyboard: [[{ text: "点击进行验证", web_app: { url: `${url}/verify?user_id=${id}` } }]] } + }); + } else if (!isCaptchaOn && isQAOn) { + await updUser(id, { user_state: "pending_verification" }, env); + return api(env.BOT_TOKEN, "sendMessage", { + chat_id: id, + text: "❓ 安全提问\n请回答:\n" + await getCfg('verif_q', env), + parse_mode: "HTML" + }); + } +} + +// 正常态用户消息防线:敏感词与类型拦截器 +async function handleVerifiedMsg(msg, u, env) { + const id = u.user_id, text = msg.text || ""; + + // 敏感词屏蔽预检系统 + if (text) { + const kws = await getJsonCfg('block_keywords', env); + if (kws.some(k => new RegExp(k, 'gi').test(text))) { + const c = u.block_count + 1, max = parseInt(await getCfg('block_threshold', env)) || 5; + const willBlock = c >= max; + await updUser(id, { block_count: c, is_blocked: willBlock }, env); + if (willBlock) { + await manageBlacklist(env, u, msg.from, true); + return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 已封禁 (发送 /start 可申请解封)" }); + } + return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `⚠️ 屏蔽词 (${c}/${max})` }); + } + } + + // 负载类型过滤体系 + for (const t of MSG_TYPES) { + if (t.check(msg)) { + const isAdmin = await isAuthAdmin(id, env); + if ((t.extra && !(await getBool(t.extra(msg), env)) && !isAdmin) || + (!t.extra && !(await getBool(t.key, env)) && !isAdmin)) { + return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `⚠️ 不接收 ${t.name}` }); + } + break; + } + } + + // 勿扰状态静默应答逻辑 + if (await getBool('busy_mode', env)) { + const now = Date.now(); + if (now - (u.user_info.last_busy_reply || 0) > 300000) { + await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "🌙 " + await getCfg('busy_msg', env) }); + await updUser(id, { user_info: { ...u.user_info, last_busy_reply: now } }, env); + } + } + + // 关键词自动回复钩子 + if (text) { + const rules = await getJsonCfg('keyword_responses', env); + const match = rules.find(r => new RegExp(r.keywords, 'gi').test(text)); + if (match) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "自动回复:\n" + match.response }); + } + + // 所有前置校验通过,放行进入转发链路 + await relayToTopic(msg, u, env); +} + +// --- 核心:消息转发与话题流控引擎 --- +async function relayToTopic(msg, u, env) { + const uMeta = getUMeta(msg.from, u, msg.date), uid = u.user_id; + let tid = u.topic_id; + + // 1. 动态检测并同步用户基础标识信息变更 + if (u.user_info.name !== uMeta.name || u.user_info.username !== uMeta.username) { + u.user_info.name = uMeta.name; u.user_info.username = uMeta.username; + await updUser(uid, { user_info: u.user_info }, env); + if (u.topic_id) api(env.BOT_TOKEN, "editForumTopic", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: u.topic_id, name: uMeta.topicName }).catch(e=>{}); + } + + // 2. 线程池资源申请:建立独立话题 + if (!tid) { + if (CACHE.user_locks[uid]) return; + CACHE.user_locks[uid] = true; + try { + const freshU = await getUser(uid, env); + if (freshU.topic_id) tid = freshU.topic_id; + else { + const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: uMeta.topicName }); + tid = t.message_thread_id.toString(); + u.user_info.card_msg_id = null; + const dummy = await api(env.BOT_TOKEN, "sendMessage", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: "✨ 正在加载用户资料...", disable_notification: true }); + u.user_info.dummy_msg_id = dummy.message_id; + await updUser(uid, { topic_id: tid, user_info: u.user_info }, env); + } + } catch (e) { + console.error("Topic Creation Failed:", e); + delete CACHE.user_locks[uid]; + return api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "系统忙,请稍后再试" }); + } + delete CACHE.user_locks[uid]; + } + + try { + let forwardedMsg; + const rawText = msg.text || ""; + + // 特殊引用语法降级渲染支持 + if (rawText && (rawText.startsWith('>') || rawText.startsWith('》') || rawText.startsWith('>'))) { + let cleanText = rawText.replace(/^[>》]\s?/, '').replace(/^>\s?/, ''); + cleanText = escape(cleanText); + + const customHtml = `
${cleanText}`; + + forwardedMsg = await api(env.BOT_TOKEN, "sendMessage", { + chat_id: env.ADMIN_GROUP_ID, + message_thread_id: tid, + text: customHtml, + parse_mode: "HTML" + }); + } else { + // 标准转发处理尝试,若触碰受限隐私配置则回退到原生复制 + try { + forwardedMsg = await api(env.BOT_TOKEN, "forwardMessage", { + chat_id: env.ADMIN_GROUP_ID, from_chat_id: uid, message_id: msg.message_id, message_thread_id: tid + }); + } catch(fwdErr) { + forwardedMsg = await api(env.BOT_TOKEN, "copyMessage", { + chat_id: env.ADMIN_GROUP_ID, from_chat_id: uid, message_id: msg.message_id, message_thread_id: tid + }); + } + } + + // 推送资料卡片流程补偿机制 + try { + let infoDirty = false; + if (!u.user_info.card_msg_id) { + try { + const cardId = await sendInfoCardToTopic(env, u, msg.from, tid, msg.date); + if (cardId) { + u.user_info.card_msg_id = cardId; + u.user_info.join_date = msg.date || (Date.now()/1000); + infoDirty = true; + } + } catch (innerErr) { + if (innerErr.message && (innerErr.message.includes("thread") || innerErr.message.includes("not found"))) { + throw innerErr; + } + } + } + // 回收临时占位符提升界面整洁度 + if (u.user_info.dummy_msg_id) { + await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.dummy_msg_id }).catch(() => {}); + delete u.user_info.dummy_msg_id; infoDirty = true; + } + if (infoDirty) await updUser(uid, { user_info: u.user_info }, env); + } catch (processErr) { + if (processErr.message && (processErr.message.includes("thread") || processErr.message.includes("not found"))) { + await updUser(uid, { topic_id: null }, env); + u.topic_id = null; + return relayToTopic(msg, u, env); // 进行一次自愈递归重试 + } + } + + // --------------------------------------------------------------------- + // * 修改点:注释/移除了面向用户的 “✅ 已送达” 反馈回执 API 调用。 + // 此动作去除了多余的底层 fetch() 开销。 + // 原逻辑: api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "✅ 已送达", ... }).catch(()=>{}); + // --------------------------------------------------------------------- + + // 构建映射关联池,支撑双向回复寻址 + if (forwardedMsg && forwardedMsg.message_id) { + const storeText = msg.text || "[Media]"; + await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date, topic_message_id) VALUES (?,?,?,?,?)", + [uid, msg.message_id, storeText, msg.date, forwardedMsg.message_id.toString()]); + } + + // 下游归档链路:数据备份 + await handleBackup(msg, uMeta, env); + + } catch (e) { + console.error("Relay Failed:", e); + if (e.message && (e.message.includes("thread") || e.message.includes("not found") || e.message.includes("Bad Request"))) { + await updUser(uid, { topic_id: null }, env); u.topic_id = null; return relayToTopic(msg, u, env); + } else { + api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "❌ 发送失败,系统异常" }); + } + } +} + +// --- 核心:发送用户信息复合卡片 --- +async function sendInfoCardToTopic(env, u, tgUser, tid, date) { + const meta = getUMeta(tgUser, u, date || (Date.now()/1000)); + let bestPhoto = null; + + // 1. [容错防护] 非阻塞尝试获取目标用户头像 + try { + const photos = await api(env.BOT_TOKEN, "getUserProfilePhotos", { user_id: u.user_id, limit: 1 }); + if (photos && photos.photos && photos.photos.length > 0) { + bestPhoto = photos.photos[0][photos.photos[0].length - 1].file_id; + } + } catch (e) {} + + try { + let cardMsg; + // 2. [边界防护] 校验配文长度防止超出 Telegram 1024字符图片 Caption 上限 + const isCaptionTooLong = meta.card.length > 1000; + + // 根据上下文存在性选择装配发送方式 + if (bestPhoto && !isCaptionTooLong) { + try { + cardMsg = await api(env.BOT_TOKEN, "sendPhoto", { + chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, photo: bestPhoto, caption: meta.card, parse_mode: "HTML", reply_markup: getBtns(u.user_id, u.is_blocked, meta.username) + }); + } catch (err) { + if (err.message && (err.message.includes("parse") || err.message.includes("MEDIA"))) { + cardMsg = await api(env.BOT_TOKEN, "sendMessage", { + chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: meta.card, parse_mode: "HTML", reply_markup: getBtns(u.user_id, u.is_blocked, meta.username) + }); + } else { + throw err; + } + } + } else { + cardMsg = await api(env.BOT_TOKEN, "sendMessage", { + chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: meta.card, parse_mode: "HTML", reply_markup: getBtns(u.user_id, u.is_blocked, meta.username) + }); + } + try { await api(env.BOT_TOKEN, "pinChatMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: cardMsg.message_id, message_thread_id: tid }); } catch (pinErr) {} + return cardMsg.message_id; + } catch (e) { + console.error("Send Info Card Failed:", e); + if (e.message && (e.message.includes("thread") || e.message.includes("not found"))) { + throw e; + } + return null; + } +} + +// --- 5. 通用/黑名单 (管理黑名单控制域) --- +async function manageBlacklist(env, u, tgUser, isBlocking) { + let bid = await getCfg('blocked_topic_id', env); + if (!bid && isBlocking) { + try { + const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: "🚫 黑名单" }); + bid = t.message_thread_id.toString(); + await setCfg('blocked_topic_id', bid, env); + } catch { return; } + } + if (!bid) return; + if (isBlocking) { + const meta = getUMeta(tgUser, u, Date.now()/1000); + const msg = await api(env.BOT_TOKEN, "sendMessage", { + chat_id: env.ADMIN_GROUP_ID, message_thread_id: bid, text: `🚫 用户已屏蔽\n${meta.card}`, parse_mode: "HTML", + reply_markup: { inline_keyboard: [[{ text: "✅ 解除屏蔽", callback_data: `unblock:${u.user_id}` }]] } + }); + await updUser(u.user_id, { user_info: { ...u.user_info, blacklist_msg_id: msg.message_id } }, env); + } else { + if (u.user_info.blacklist_msg_id) { + try { await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.blacklist_msg_id }); } catch (e) { if(e.message && e.message.includes("thread")) await setCfg('blocked_topic_id', "", env); } + await updUser(u.user_id, { user_info: { ...u.user_info, blacklist_msg_id: null } }, env); + } + } +} + +// 下游归档链路:异步数据备份到冷频道 +async function handleBackup(msg, meta, env) { + const bid = await getCfg('backup_group_id', env); + if (!bid) return; + try { + const header = `📨 备份 ${meta.name} (${meta.userId})`; + if (msg.text) await api(env.BOT_TOKEN, "sendMessage", { chat_id: bid, text: header + "\n" + msg.text, parse_mode: "HTML" }); + else { + await api(env.BOT_TOKEN, "sendMessage", { chat_id: bid, text: header, parse_mode: "HTML" }); + await api(env.BOT_TOKEN, "copyMessage", { chat_id: bid, from_chat_id: msg.chat.id, message_id: msg.message_id }); + } + } catch (e) {} +} + +// --- 6. 管理员功能模块 (双向交互枢纽) --- +async function handleAdminReply(msg, env) { + if (!msg.message_thread_id || msg.from.is_bot || !(await isAuthAdmin(msg.from.id, env))) return; + + const stateStr = await getCfg(`admin_state:${msg.from.id}`, env); + if (stateStr) { + const state = safeParse(stateStr); + if (state.action === 'input_note') { + const targetUid = state.target; + const u = await getUser(targetUid, env); + if (msg.text === '/clear' || msg.text === '清除') delete u.user_info.note; + else u.user_info.note = msg.text; + const mockTgUser = { id: targetUid, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" }; + const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000)); + // 更新带有新附注的资料实体卡片 + if (u.topic_id && u.user_info.card_msg_id) { + try { + await api(env.BOT_TOKEN, "editMessageCaption", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, caption: newMeta.card, parse_mode: "HTML", reply_markup: getBtns(targetUid, u.is_blocked, newMeta.username) }); + } catch (e) { + try { await api(env.BOT_TOKEN, "editMessageText", { 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, newMeta.username) }); } catch(e2) {} + } + } + await updUser(targetUid, { user_info: u.user_info }, env); + await setCfg(`admin_state:${msg.from.id}`, "", env); + return api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: `✅ 备注已更新` }); + } + } + + const uid = (await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'))?.user_id; + if (!uid) return; + + let replyToMsgId = undefined; + if (msg.reply_to_message) { + const ref = await sql(env, "SELECT message_id FROM messages WHERE topic_message_id = ?", msg.reply_to_message.message_id.toString(), 'first'); + if (ref) replyToMsgId = ref.message_id; + } + + try { + await api(env.BOT_TOKEN, "copyMessage", { chat_id: uid, from_chat_id: msg.chat.id, message_id: msg.message_id, reply_to_message_id: replyToMsgId }); + // 此处为管理员端给用户下发消息的主逻辑。根据之前的版本,管理员侧发送成功后的回执代码也已经去除 + } catch (e) { api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "❌ 内部投递失败" }); } +} + +async function handleEdit(msg, env) { + const u = await getUser(msg.from.id.toString(), env); + if (!u.topic_id) return; + + const old = await sql(env, "SELECT text FROM messages WHERE user_id=? AND message_id=?", [u.user_id, msg.message_id], 'first'); + const newTxt = msg.text || msg.caption || "[非文本]"; + + const logText = `✏️ 消息修改\n前: ${escape(old?.text||"?")}\n后: ${escape(newTxt)}`; + + await api(env.BOT_TOKEN, "sendMessage", { + chat_id: env.ADMIN_GROUP_ID, + message_thread_id: u.topic_id, + text: logText, + parse_mode: "HTML" + }); + + if (old) { + await sql(env, "UPDATE messages SET text=? WHERE user_id=? AND message_id=?", [newTxt, u.user_id, msg.message_id]); + } +} + +// --- 7. Web验证外设接口组件 --- +async function handleVerifyPage(url, env) { + const uid = url.searchParams.get('user_id'); + const mode = await getCfg('captcha_mode', env); + const siteKey = mode === 'recaptcha' ? env.RECAPTCHA_SITE_KEY : env.TURNSTILE_SITE_KEY; + if (!uid || !siteKey) return new Response("Miss Config (Check Mode/Key)", { status: 400 }); + const scriptUrl = mode === 'recaptcha' ? "https://www.google.com/recaptcha/api.js" : "https://challenges.cloudflare.com/turnstile/v0/api.js"; + const divClass = mode === 'recaptcha' ? "g-recaptcha" : "cf-turnstile"; + const html = `
${id}${note}\n🕒: ${timeStr}`
+ };
+};
+
+// 动态键盘矩阵构建:受限 API 规避检查点
+const getBtns = (id, blk, username) => {
+ const btns = [];
+
+ // username 条件控制路由:缺少公开标识符强制屏蔽按钮注册
+ if (username) {
+ btns.push([{ text: "👤 访问个人主页", url: `https://t.me/${username}` }]);
+ }
+
+ btns.push([{ text: blk ? "✅ 执行解封" : "🚫 执行屏蔽", callback_data: `${blk ? 'unblock' : 'block'}:${id}` }]);
+ btns.push([{ text: "✏️ 录入备注", callback_data: `note:set:${id}` }, { text: "📌 提升置顶", callback_data: `pin_card:${id}` }]);
+
+ return { inline_keyboard: btns };
+};
\ No newline at end of file