// Cloudflare Worker:Telegram 双向机器人 (纯本地极速版 v4.0) // --- 本地题库 (15条) --- const LOCAL_QUESTIONS = [ {"question": "冰融化后会变成什么?", "correct_answer": "水", "incorrect_answers": ["石头", "木头", "火"]}, {"question": "正常人有几只眼睛?", "correct_answer": "2", "incorrect_answers": ["1", "3", "4"]}, {"question": "以下哪个属于水果?", "correct_answer": "香蕉", "incorrect_answers": ["白菜", "猪肉", "大米"]}, {"question": "1 加 2 等于几?", "correct_answer": "3", "incorrect_answers": ["2", "4", "5"]}, {"question": "5 减 2 等于几?", "correct_answer": "3", "incorrect_answers": ["1", "2", "4"]}, {"question": "2 乘以 3 等于几?", "correct_answer": "6", "incorrect_answers": ["4", "5", "7"]}, {"question": "10 加 5 等于几?", "correct_answer": "15", "incorrect_answers": ["10", "12", "20"]}, {"question": "8 减 4 等于几?", "correct_answer": "4", "incorrect_answers": ["2", "3", "5"]}, {"question": "在天上飞的交通工具是什么?", "correct_answer": "飞机", "incorrect_answers": ["汽车", "轮船", "自行车"]}, {"question": "星期一的后面是星期几?", "correct_answer": "星期二", "incorrect_answers": ["星期日", "星期五", "星期三"]}, {"question": "鱼通常生活在哪里?", "correct_answer": "水里", "incorrect_answers": ["树上", "土里", "火里"]}, {"question": "我们用什么器官来听声音?", "correct_answer": "耳朵", "incorrect_answers": ["眼睛", "鼻子", "嘴巴"]}, {"question": "晴朗的天空通常是什么颜色的?", "correct_answer": "蓝色", "incorrect_answers": ["绿色", "红色", "紫色"]}, {"question": "太阳从哪个方向升起?", "correct_answer": "东方", "incorrect_answers": ["西方", "南方", "北方"]}, {"question": "小狗发出的叫声通常是?", "correct_answer": "汪汪", "incorrect_answers": ["喵喵", "咩咩", "呱呱"]} ]; export default { async fetch(request, env, ctx) { // 环境自检 if (!env.TOPIC_MAP) return new Response("Error: KV 'TOPIC_MAP' not bound."); if (!env.BOT_TOKEN) return new Response("Error: BOT_TOKEN not set."); if (!env.SUPERGROUP_ID) return new Response("Error: SUPERGROUP_ID not set."); if (request.method !== "POST") return new Response("OK"); let update; try { update = await request.json(); } catch { return new Response("OK"); } if (update.callback_query) { await handleCallbackQuery(update.callback_query, env, ctx); return new Response("OK"); } const msg = update.message; if (!msg) return new Response("OK"); ctx.waitUntil(flushExpiredMediaGroups(env, Date.now())); if (msg.chat && msg.chat.type === "private") { try { await handlePrivateMessage(msg, env, ctx); } catch (e) { const errText = `⚠️ **系统错误**\n\n\`${e.message}\`\n\n请检查配置: SUPERGROUP_ID / BOT_TOKEN / TOPIC_MAP`; await tgCall(env, "sendMessage", { chat_id: msg.chat.id, text: errText, parse_mode: "Markdown" }); console.error(e); } return new Response("OK"); } const supergroupId = Number(env.SUPERGROUP_ID); if (msg.chat && Number(msg.chat.id) === supergroupId) { if (msg.forum_topic_closed && msg.message_thread_id) { await updateThreadStatus(msg.message_thread_id, true, env); return new Response("OK"); } if (msg.forum_topic_reopened && msg.message_thread_id) { await updateThreadStatus(msg.message_thread_id, false, env); return new Response("OK"); } if (msg.message_thread_id) { await handleAdminReply(msg, env, ctx); return new Response("OK"); } } return new Response("OK"); }, }; // ---------------- 核心业务逻辑 ---------------- async function handlePrivateMessage(msg, env, ctx) { const userId = msg.chat.id; const key = `user:${userId}`; // 拦截普通用户发送的指令 if (msg.text && msg.text.startsWith("/") && msg.text.trim() !== "/start") { return; } const isBanned = await env.TOPIC_MAP.get(`banned:${userId}`); if (isBanned) return; const verified = await env.TOPIC_MAP.get(`verified:${userId}`); if (!verified) { const isStart = msg.text && msg.text.trim() === "/start"; const pendingMsgId = isStart ? null : msg.message_id; await sendVerificationChallenge(userId, env, pendingMsgId); return; } await forwardToTopic(msg, userId, key, env, ctx); } async function forwardToTopic(msg, userId, key, env, ctx) { let rec = await env.TOPIC_MAP.get(key, { type: "json" }); if (rec && rec.closed) { await tgCall(env, "sendMessage", { chat_id: userId, text: "🚫 当前对话已被管理员关闭。" }); return; } if (!rec || !rec.thread_id) { rec = await createTopic(msg.from, key, env); } if (msg.media_group_id) { await handleMediaGroup(msg, env, ctx, { direction: "p2t", targetChat: env.SUPERGROUP_ID, threadId: rec.thread_id }); return; } const res = await tgCall(env, "forwardMessage", { chat_id: env.SUPERGROUP_ID, from_chat_id: userId, message_id: msg.message_id, message_thread_id: rec.thread_id, }); if (!res.ok) { const desc = (res.description || "").toLowerCase(); if (desc.includes("thread not found") || desc.includes("topic not found")) { const newRec = await createTopic(msg.from, key, env); await tgCall(env, "forwardMessage", { chat_id: env.SUPERGROUP_ID, from_chat_id: userId, message_id: msg.message_id, message_thread_id: newRec.thread_id, }); return; } if (desc.includes("chat not found")) throw new Error(`群组ID错误: ${env.SUPERGROUP_ID}`); if (desc.includes("not enough rights")) throw new Error("机器人权限不足 (需 Manage Topics)"); await tgCall(env, "copyMessage", { chat_id: env.SUPERGROUP_ID, from_chat_id: userId, message_id: msg.message_id, message_thread_id: rec.thread_id }); } } async function handleAdminReply(msg, env, ctx) { const threadId = msg.message_thread_id; const text = (msg.text || "").trim(); // 反查 UserId let userId = null; const list = await env.TOPIC_MAP.list({ prefix: "user:" }); for (const { name } of list.keys) { const rec = await env.TOPIC_MAP.get(name, { type: "json" }); if (rec && Number(rec.thread_id) === Number(threadId)) { userId = Number(name.slice(5)); break; } } // 如果找不到用户,说明可能是在普通话题,或者数据丢失,直接返回 if (!userId) return; // --- 指令区域 --- if (text === "/close") { const key = `user:${userId}`; let rec = await env.TOPIC_MAP.get(key, { type: "json" }); if (rec) { rec.closed = true; await env.TOPIC_MAP.put(key, JSON.stringify(rec)); await tgCall(env, "closeForumTopic", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId }); await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: "🚫 **对话已强制关闭**", parse_mode: "Markdown" }); } return; } if (text === "/open") { const key = `user:${userId}`; let rec = await env.TOPIC_MAP.get(key, { type: "json" }); if (rec) { rec.closed = false; await env.TOPIC_MAP.put(key, JSON.stringify(rec)); await tgCall(env, "reopenForumTopic", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId }); await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: "✅ **对话已恢复**", parse_mode: "Markdown" }); } return; } if (text === "/reset") { await env.TOPIC_MAP.delete(`verified:${userId}`); await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: "🔄 **验证重置**", parse_mode: "Markdown" }); return; } if (text === "/trust") { await env.TOPIC_MAP.put(`verified:${userId}`, "trusted"); await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: "🌟 **已设置永久信任**", parse_mode: "Markdown" }); return; } if (text === "/ban") { await env.TOPIC_MAP.put(`banned:${userId}`, "1"); await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: "🚫 **用户已封禁**", parse_mode: "Markdown" }); return; } if (text === "/unban") { await env.TOPIC_MAP.delete(`banned:${userId}`); await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: "✅ **用户已解封**", parse_mode: "Markdown" }); return; } if (text === "/info") { const info = `👤 **用户信息**\nUID: \`${userId}\`\nTopic ID: \`${threadId}\`\nLink: [点击私聊](tg://user?id=${userId})`; await tgCall(env, "sendMessage", { chat_id: env.SUPERGROUP_ID, message_thread_id: threadId, text: info, parse_mode: "Markdown" }); return; } // 转发管理员消息给用户 if (msg.media_group_id) { await handleMediaGroup(msg, env, ctx, { direction: "t2p", targetChat: userId, threadId: null }); return; } await tgCall(env, "copyMessage", { chat_id: userId, from_chat_id: env.SUPERGROUP_ID, message_id: msg.message_id }); } // ---------------- 验证模块 (纯本地) ---------------- async function sendVerificationChallenge(userId, env, pendingMsgId) { // 直接从本地题库随机 const q = LOCAL_QUESTIONS[Math.floor(Math.random() * LOCAL_QUESTIONS.length)]; const challenge = { question: q.question, correct: q.correct_answer, options: shuffleArray([...q.incorrect_answers, q.correct_answer]) }; // 使用 8 位短 ID 防止按钮失效 const verifyId = Math.random().toString(36).substring(2, 10); const state = { ans: challenge.correct, pending: pendingMsgId }; await env.TOPIC_MAP.put(`chal:${verifyId}`, JSON.stringify(state), { expirationTtl: 300 }); const buttons = challenge.options.map(opt => { const safeOpt = opt.length > 20 ? opt.substring(0, 20) : opt; return { text: opt, callback_data: `verify:${verifyId}:${safeOpt}` }; }); const keyboard = []; for (let i = 0; i < buttons.length; i += 2) keyboard.push(buttons.slice(i, i + 2)); await tgCall(env, "sendMessage", { chat_id: userId, text: `🛡️ **人机验证**\n\n${challenge.question}\n\n请点击下方按钮回答 (回答正确后将自动发送您刚才的消息)。`, parse_mode: "Markdown", reply_markup: { inline_keyboard: keyboard } }); } async function handleCallbackQuery(query, env, ctx) { try { const data = query.data; if (!data.startsWith("verify:")) return; const parts = data.split(":"); if (parts.length < 3) return; const verifyId = parts[1]; const userAns = parts.slice(2).join(":"); const userId = query.from.id; const stateStr = await env.TOPIC_MAP.get(`chal:${verifyId}`); if (!stateStr) { await tgCall(env, "answerCallbackQuery", { callback_query_id: query.id, text: "❌ 验证已过期,请重发消息", show_alert: true }); return; } let state; try { state = JSON.parse(stateStr); } catch(e) { await tgCall(env, "answerCallbackQuery", { callback_query_id: query.id, text: "❌ 数据错误", show_alert: true }); return; } if (userAns === state.ans) { await tgCall(env, "answerCallbackQuery", { callback_query_id: query.id, text: "✅ 验证通过" }); // 30天有效期 await env.TOPIC_MAP.put(`verified:${userId}`, "1", { expirationTtl: 2592000 }); await env.TOPIC_MAP.delete(`chal:${verifyId}`); await tgCall(env, "editMessageText", { chat_id: userId, message_id: query.message.message_id, text: "✅ **验证成功**\n\n您现在可以自由对话了。", parse_mode: "Markdown" }); if (state.pending) { try { const fakeMsg = { message_id: state.pending, chat: { id: userId, type: "private" }, from: query.from, }; await forwardToTopic(fakeMsg, userId, `user:${userId}`, env, ctx); await tgCall(env, "sendMessage", { chat_id: userId, text: "📩 刚才的消息已帮您送达。", reply_to_message_id: state.pending }); } catch (e) { } } } else { await tgCall(env, "answerCallbackQuery", { callback_query_id: query.id, text: "❌ 答案错误", show_alert: true }); } } catch (e) { console.error("Callback Error", e); await tgCall(env, "answerCallbackQuery", { callback_query_id: query.id, text: `⚠️ 系统错误: ${e.message}`, show_alert: true }); } } // ---------------- 辅助函数 ---------------- async function createTopic(from, key, env) { const title = buildTopicTitle(from); if (!env.SUPERGROUP_ID.toString().startsWith("-100")) throw new Error("SUPERGROUP_ID必须以-100开头"); const res = await tgCall(env, "createForumTopic", { chat_id: env.SUPERGROUP_ID, name: title }); if (!res.ok) throw new Error(`创建话题失败: ${res.description}`); const rec = { thread_id: res.result.message_thread_id, title, closed: false }; await env.TOPIC_MAP.put(key, JSON.stringify(rec)); return rec; } function updateThreadStatus(threadId, isClosed, env) { return env.TOPIC_MAP.list({ prefix: "user:" }).then(list => { for (const { name } of list.keys) { env.TOPIC_MAP.get(name, { type: "json" }).then(rec => { if (rec && Number(rec.thread_id) === Number(threadId)) { rec.closed = isClosed; env.TOPIC_MAP.put(name, JSON.stringify(rec)); } }); } }); } function shuffleArray(arr) { return arr.sort(() => Math.random() - 0.5); } function buildTopicTitle(from) { const name = (from.first_name + " " + (from.last_name || "")).trim(); return (name || "User") + (from.username ? ` @${from.username}` : ""); } async function tgCall(env, method, body) { const base = env.API_BASE || "https://api.telegram.org"; const resp = await fetch(`${base}/bot${env.BOT_TOKEN}/${method}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); return await resp.json(); } async function handleMediaGroup(msg, env, ctx, { direction, targetChat, threadId }) { const groupId = msg.media_group_id; const key = `mg:${direction}:${groupId}`; const item = extractMedia(msg); if (!item) { await tgCall(env, "copyMessage", { chat_id: targetChat, from_chat_id: msg.chat.id, message_id: msg.message_id, message_thread_id: threadId }); return; } let rec = await env.TOPIC_MAP.get(key, { type: "json" }); if (!rec) rec = { direction, targetChat, threadId, items: [], last_ts: Date.now() }; rec.items.push({ ...item, msg_id: msg.message_id }); rec.last_ts = Date.now(); await env.TOPIC_MAP.put(key, JSON.stringify(rec), { expirationTtl: 60 }); ctx.waitUntil(delaySend(env, key, rec.last_ts)); } function extractMedia(msg) { if (msg.photo) return { type: "photo", id: msg.photo.pop().file_id, cap: msg.caption }; if (msg.video) return { type: "video", id: msg.video.file_id, cap: msg.caption }; if (msg.document) return { type: "document", id: msg.document.file_id, cap: msg.caption }; return null; } async function flushExpiredMediaGroups(env, now) {} async function delaySend(env, key, ts) { await new Promise(r => setTimeout(r, 2000)); const rec = await env.TOPIC_MAP.get(key, { type: "json" }); if (rec && rec.last_ts === ts) { const media = rec.items.map((it, i) => ({ type: it.type, media: it.id, caption: i===0?it.cap:"" })); if (media.length > 0) await tgCall(env, "sendMediaGroup", { chat_id: rec.targetChat, message_thread_id: rec.threadId, media }); await env.TOPIC_MAP.delete(key); } }