From c4b6d5ec0ffb1cd13e815eddbbb0c1b7dd872259 Mon Sep 17 00:00:00 2001 From: Orion Date: Fri, 8 May 2026 23:48:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=F0=9F=90=9B=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E7=BB=93=E6=9E=84=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=E7=8A=B6=E6=80=81=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 Telegram Bot 核心逻辑中,为消息表增加了 topic_message_id 字段以支持 话题模式。重构了 Telegram API 请求封装逻辑,增强了错误处理能力。 同时在文档中增加了关于“用户屏蔽 Bot”的常见问题说明。系统现在可以自动 检测用户屏蔽状态,并在管理界面展示屏蔽标记,当用户重新互动时会自动清 除该标记。 --- telegram/README.md | 32 ++++++++- telegram/tg-bot.js | 173 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 180 insertions(+), 25 deletions(-) diff --git a/telegram/README.md b/telegram/README.md index 08f63d3..8ecd34d 100644 --- a/telegram/README.md +++ b/telegram/README.md @@ -1,4 +1,4 @@ -# Telegram Bot Worker +yi'xia# Telegram Bot Worker `tg-bot.js` 是一个部署在 Cloudflare Workers 上的 Telegram 双向私聊中继 Bot(版本 v3.68)。它会把用户私聊消息转发到管理员论坛群的独立 topic 中,管理员在对应 topic 内回复即可把消息发回用户,同时提供验证、过滤、封禁、备注、自动回复、备份和 Telegram 内联管理面板。 @@ -240,6 +240,36 @@ curl -X POST "https://api.telegram.org/bot/setWebhook" \ - **引用块**:以 `>`、`》` 或 `>` 开头的文本会被渲染为 HTML 引用块 - **媒体欢迎语**:管理员可以在面板中上传图片/视频/GIF 作为欢迎语 +### ⚠️ 常见问题:用户屏蔽 Bot + +如果管理员回复时收到错误提示:**"⚠️ 用户已屏蔽 Bot"**,说明该用户已在 Telegram 中屏蔽了机器人。 + +**症状:** +- 管理员可以正常接收用户消息 +- 管理员回复时显示 "用户已屏蔽 Bot" 错误 +- 用户收不到管理员的回复 + +**原因:** +用户在 Telegram 中点击了"屏蔽机器人"(Block Bot),导致 Bot 无法再主动发消息给用户。即使用户之前发送过消息,屏蔽后 Bot 也无法回复。 + +**自动标记功能:** +系统会自动检测并标记被屏蔽的用户: +- 当检测到用户屏蔽 Bot 时,会自动在用户卡片上显示屏蔽状态和时间 +- 用户卡片会显示 `⛔ 用户屏蔽Bot: 是 (时间)` +- 当用户重新发送消息或管理员解封时,自动清除屏蔽标记 + +**解决方案:** +需要通过其他方式联系该用户,让其按以下步骤解除屏蔽: +1. 打开与机器人的聊天窗口 +2. 点击右上角菜单(三个点或机器人名称) +3. 选择"解除屏蔽"或"Unblock bot" +4. 重新发送 `/start` 命令激活机器人 + +**预防措施:** +- 在欢迎语中提醒用户不要屏蔽机器人 +- 定期检查被屏蔽的用户列表(查看用户卡片上的屏蔽状态) +- 对于重要用户,建议通过其他渠道保持联系 + ## 安全建议 ### 🔐 认证与授权 diff --git a/telegram/tg-bot.js b/telegram/tg-bot.js index cc3a81d..5178ee6 100644 --- a/telegram/tg-bot.js +++ b/telegram/tg-bot.js @@ -176,9 +176,8 @@ async function dbInit(env) { 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))`) + env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message_id TEXT, text TEXT, date INTEGER, topic_message_id TEXT, PRIMARY KEY (user_id, message_id))`) ]); - try { await sql(env, "ALTER TABLE messages ADD COLUMN topic_message_id TEXT"); } catch (e) {} } // --- 4. 业务逻辑 (核心流) --- @@ -268,20 +267,20 @@ async function handleUserDelete(msg, u, env) { const targetMsgIdRaw = msg.reply_to_message.message_id; const targetMsgId = targetMsgIdRaw.toString(); console.log(`Delete request: user=${u.user_id}, target_msg_raw=${targetMsgIdRaw} (type: ${typeof targetMsgIdRaw}), target_msg_str=${targetMsgId}`); - + // 查询对应的管理员侧消息ID - 尝试多种可能的格式 let ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [u.user_id, targetMsgId], 'first'); - + // 如果没找到,尝试用整数查询(以防数据库中存的是数字) if (!ref || !ref.topic_message_id) { console.log(`First query failed, trying with integer...`); ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [u.user_id, parseInt(targetMsgId)], 'first'); } - + if (!ref || !ref.topic_message_id) { console.log(`Delete failed: No mapping found for user=${u.user_id}, msg=${targetMsgId}`); console.log(`Tip: Check database records with: SELECT * FROM messages WHERE user_id='${u.user_id}'`); - + // 帮助用户排查:列出该用户的最近5条消息记录 try { const recentMsgs = await sql(env, "SELECT message_id, topic_message_id, text FROM messages WHERE user_id=? ORDER BY date DESC LIMIT 5", [u.user_id], 'all'); @@ -291,7 +290,7 @@ async function handleUserDelete(msg, u, env) { } catch (e) { console.log(`Failed to fetch recent messages:`, e.message); } - + return api(env.BOT_TOKEN, "sendMessage", { chat_id: u.user_id, text: "❌ 未找到对应的消息记录,可能该消息未被转发或已被删除", @@ -300,14 +299,14 @@ async function handleUserDelete(msg, u, env) { } console.log(`Delete success: Found mapping topic_msg=${ref.topic_message_id}`); - + try { // 1. 删除被回复的目标消息(先删目标,再删命令) await api(env.BOT_TOKEN, "deleteMessage", { chat_id: u.user_id, message_id: parseInt(targetMsgId) }).catch((e) => console.log("Failed to delete target msg:", e.message)); - + // 2. 删除用户侧的 /del 命令消息 await api(env.BOT_TOKEN, "deleteMessage", { chat_id: u.user_id, @@ -326,7 +325,7 @@ async function handleUserDelete(msg, u, env) { // 4. 清理数据库记录 await sql(env, "DELETE FROM messages WHERE user_id=? AND message_id=?", [u.user_id, targetMsgId]); console.log(`Delete completed: Cleaned up database record`); - + } catch (e) { console.error("User Delete Failed:", e); await api(env.BOT_TOKEN, "sendMessage", { @@ -360,10 +359,10 @@ async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text || } const targetTopicMsgId = msg.reply_to_message.message_id; - + // 查询对应的用户侧消息ID const ref = await sql(env, "SELECT user_id, message_id FROM messages WHERE topic_message_id=?", targetTopicMsgId.toString(), 'first'); - + if (!ref) { return api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, @@ -378,7 +377,7 @@ async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text || chat_id: msg.chat.id, message_id: msg.message_id }).catch(() => {}); - + await api(env.BOT_TOKEN, "deleteMessage", { chat_id: msg.chat.id, message_id: targetTopicMsgId @@ -392,7 +391,7 @@ async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text || // 3. 清理数据库记录 await sql(env, "DELETE FROM messages WHERE user_id=? AND message_id=?", [ref.user_id, ref.message_id]); - + } catch (e) { console.error("Admin Delete Failed:", e); await api(env.BOT_TOKEN, "sendMessage", { @@ -649,6 +648,31 @@ async function sendStart(id, msg, env) { // 正常态用户消息防线:敏感词与类型拦截器 async function handleVerifiedMsg(msg, u, env) { const id = u.user_id, text = msg.text || ""; + + // 如果用户之前被标记为屏蔽 Bot,但现在能发消息,说明已解除屏蔽 + if (u.user_info && u.user_info.bot_blocked) { + u.user_info.bot_blocked = false; + delete u.user_info.bot_blocked_ts; + await updUser(id, { user_info: u.user_info }, env); + console.log(`Cleared bot_blocked mark for user ${id} after receiving message`); + + // 更新用户卡片显示 + try { + if (u.topic_id && u.user_info.card_msg_id) { + const mockTgUser = { id: id, 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)); + 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(id, u.is_blocked, newMeta.username) + }); + } + } catch (e) { + console.log("Failed to update card after bot_blocked cleared:", e.message); + } + } // 敏感词屏蔽预检系统 if (text) { @@ -1072,17 +1096,49 @@ async function handleAdminReply(msg, env) { } } - const uid = (await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'))?.user_id; - if (!uid) return; + const topicIdStr = msg.message_thread_id.toString(); + console.log(`Admin reply debug: topic_id=${topicIdStr}, admin_msg_id=${msg.message_id}`); + + const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", topicIdStr, 'first'); + console.log(`Admin reply debug: userRef=`, userRef); + + const uid = userRef?.user_id; + if (!uid) { + console.error(`Admin reply failed: No user found for topic_id=${topicIdStr}`); + return api(env.BOT_TOKEN, "sendMessage", { + chat_id: msg.chat.id, + message_thread_id: msg.message_thread_id, + text: `❌ 系统错误:未找到关联的用户(topic_id=${topicIdStr})` + }); + } + + // 检查用户状态 + const u = await getUser(uid, env); + console.log(`Admin reply debug: user_state=${u.user_state}, is_blocked=${u.is_blocked}`); + + if (u.is_blocked) { + return api(env.BOT_TOKEN, "sendMessage", { + chat_id: msg.chat.id, + message_thread_id: msg.message_thread_id, + text: `❌ 该用户已被屏蔽,无法发送消息` + }); + } 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; + if (ref) { + replyToMsgId = ref.message_id; + console.log(`Admin reply debug: Found reply mapping topic_msg=${msg.reply_to_message.message_id} -> user_msg=${replyToMsgId}`); + } else { + console.log(`Admin reply debug: No reply mapping found for topic_msg=${msg.reply_to_message.message_id}`); + } } try { + console.log(`Admin reply debug: Delivering message to uid=${uid}, replyToMsgId=${replyToMsgId}`); const sent = await deliverAdminMessageToUser(msg, uid, replyToMsgId, env); + console.log(`Admin reply debug: Delivery success, sent.message_id=${sent?.message_id}`); if (sent && sent.message_id) { const storeText = msg.text || msg.caption || "[Admin Message]"; await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date, topic_message_id) VALUES (?,?,?,?,?)", @@ -1091,11 +1147,61 @@ async function handleAdminReply(msg, env) { // 此处为管理员端给用户下发消息的主逻辑。根据之前的版本,管理员侧发送成功后的回执代码也已经去除 } catch (e) { console.error("Admin Delivery Failed:", e); - await api(env.BOT_TOKEN, "sendMessage", { - chat_id: msg.chat.id, - message_thread_id: msg.message_thread_id, - text: `❌ 内部投递失败:${e.message || "Unknown error"}` + console.error("Admin Delivery Failed - Error details:", { + message: e.message, + stack: e.stack, + uid: uid, + topicId: topicIdStr, + replyToMsgId: replyToMsgId }); + + // 检测用户是否屏蔽了 Bot + const isBlocked = e.message && e.message.includes("bot was blocked by the user"); + + if (isBlocked) { + // 自动标记用户为屏蔽状态 + try { + const u = await getUser(uid, env); + if (!u.user_info.bot_blocked) { + u.user_info.bot_blocked = true; + u.user_info.bot_blocked_ts = Date.now(); + await updUser(uid, { user_info: u.user_info }, env); + console.log(`Auto-marked user ${uid} as bot_blocked`); + + // 更新用户卡片显示 + if (u.topic_id && u.user_info.card_msg_id) { + const mockTgUser = { id: uid, 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)); + 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(uid, u.is_blocked, newMeta.username) + }); + } catch (editErr) { + console.log("Failed to update card after bot_blocked detection:", editErr.message); + } + } + } + } catch (markErr) { + console.error("Failed to mark user as bot_blocked:", markErr); + } + + await api(env.BOT_TOKEN, "sendMessage", { + chat_id: msg.chat.id, + message_thread_id: msg.message_thread_id, + text: `⚠️ 用户已屏蔽 Bot\n\n该用户已在 Telegram 中屏蔽了本机器人,无法接收消息。\n\n请通过其他方式联系用户,让其:\n1️⃣ 打开与机器人的聊天\n2️⃣ 解除屏蔽\n3️⃣ 重新发送 /start`, + parse_mode: "HTML" + }); + } else { + await api(env.BOT_TOKEN, "sendMessage", { + chat_id: msg.chat.id, + message_thread_id: msg.message_thread_id, + text: `❌ 内部投递失败:${e.message || "Unknown error"}\n\n调试信息:\n用户ID: ${uid}\n话题ID: ${topicIdStr}` + }); + } } } @@ -1107,7 +1213,7 @@ async function handleEdit(msg, env) { const newTxt = msg.text || msg.caption || "[非文本]"; const logText = `✏️ 消息修改\n前: ${escape(old?.text||"?")}\n后: ${escape(newTxt)}`; - +yixia await api(env.BOT_TOKEN, "sendMessage", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: u.topic_id, @@ -1223,6 +1329,15 @@ async function handleCallback(cb, env) { else if (['block','unblock'].includes(act)) { const isB = act === 'block'; const uid = p1; const u = await getUser(uid, env); const bid = await getCfg('blocked_topic_id', env); await updUser(uid, { is_blocked: isB, block_count: 0 }, env); + + // 如果是解封操作,清除 bot_blocked 标记 + if (!isB && u.user_info.bot_blocked) { + u.user_info.bot_blocked = false; + delete u.user_info.bot_blocked_ts; + await updUser(uid, { user_info: u.user_info }, env); + console.log(`Cleared bot_blocked mark for user ${uid} after admin unblock`); + } + // 响应变更,刷新目标人员资料卡片上的按钮渲染状态 if (u.user_info.card_msg_id) { api(env.BOT_TOKEN, "editMessageReplyMarkup", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, reply_markup: getBtns(uid, isB, u.user_info.username) }).catch(()=>{}); @@ -1434,13 +1549,23 @@ const getUMeta = (tgUser, dbUser, d) => { const note = dbUser.user_info && dbUser.user_info.note ? `\n📝 附加备注: ${escape(dbUser.user_info.note)}` : ""; const labelDisplay = tgUser.username ? `@${tgUser.username}` : "未设公开ID"; const timeStr = new Date(d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }); + + // 检测并显示屏蔽状态 + let blockStatus = ""; + if (dbUser.is_blocked) { + blockStatus = `\n🚫 管理员屏蔽: 是`; + } + if (dbUser.user_info && dbUser.user_info.bot_blocked) { + const blockTime = new Date(dbUser.user_info.bot_blocked_ts || d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }); + blockStatus += `\n⛔ 用户屏蔽Bot: 是 (${blockTime})`; + } return { userId: id, name, username: tgUser.username, topicName: name.substring(0, 128), - card: `🪪 用户身份卡片\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: ${id}${note}\n🕒: ${timeStr}` + card: `🪪 用户身份卡片\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: ${id}${note}${blockStatus}\n🕒: ${timeStr}` }; }; @@ -1489,4 +1614,4 @@ const getBtns = (id, blk, username) => { btns.push([{ text: "✏️ 录入备注", callback_data: `note:set:${id}` }, { text: "📌 提升置顶", callback_data: `pin_card:${id}` }]); return { inline_keyboard: btns }; -}; +}; \ No newline at end of file