fix(core): 🐛 优化数据库结构并添加用户状态标记

在 Telegram Bot 核心逻辑中,为消息表增加了 topic_message_id 字段以支持
话题模式。重构了 Telegram API 请求封装逻辑,增强了错误处理能力。

同时在文档中增加了关于“用户屏蔽 Bot”的常见问题说明。系统现在可以自动
检测用户屏蔽状态,并在管理界面展示屏蔽标记,当用户重新互动时会自动清
除该标记。
This commit is contained in:
2026-05-08 23:48:14 +08:00
parent 02b6cb1735
commit c4b6d5ec0f
2 changed files with 180 additions and 25 deletions

View File

@@ -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<BOT_TOKEN>/setWebhook" \
- **引用块**:以 `>``》``&gt;` 开头的文本会被渲染为 HTML 引用块
- **媒体欢迎语**:管理员可以在面板中上传图片/视频/GIF 作为欢迎语
### ⚠️ 常见问题:用户屏蔽 Bot
如果管理员回复时收到错误提示:**"⚠️ 用户已屏蔽 Bot"**,说明该用户已在 Telegram 中屏蔽了机器人。
**症状:**
- 管理员可以正常接收用户消息
- 管理员回复时显示 "用户已屏蔽 Bot" 错误
- 用户收不到管理员的回复
**原因:**
用户在 Telegram 中点击了"屏蔽机器人"Block Bot导致 Bot 无法再主动发消息给用户。即使用户之前发送过消息,屏蔽后 Bot 也无法回复。
**自动标记功能:**
系统会自动检测并标记被屏蔽的用户:
- 当检测到用户屏蔽 Bot 时,会自动在用户卡片上显示屏蔽状态和时间
- 用户卡片会显示 `⛔ 用户屏蔽Bot: 是 (时间)`
- 当用户重新发送消息或管理员解封时,自动清除屏蔽标记
**解决方案:**
需要通过其他方式联系该用户,让其按以下步骤解除屏蔽:
1. 打开与机器人的聊天窗口
2. 点击右上角菜单(三个点或机器人名称)
3. 选择"解除屏蔽"或"Unblock bot"
4. 重新发送 `/start` 命令激活机器人
**预防措施:**
- 在欢迎语中提醒用户不要屏蔽机器人
- 定期检查被屏蔽的用户列表(查看用户卡片上的屏蔽状态)
- 对于重要用户,建议通过其他渠道保持联系
## 安全建议
### 🔐 认证与授权

View File

@@ -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: `⚠️ <b>用户已屏蔽 Bot</b>\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📝 <b>附加备注:</b> ${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🚫 <b>管理员屏蔽:</b> 是`;
}
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⛔ <b>用户屏蔽Bot:</b> 是 (${blockTime})`;
}
return {
userId: id,
name,
username: tgUser.username,
topicName: name.substring(0, 128),
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}\n🕒: <code>${timeStr}</code>`
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}${blockStatus}\n🕒: <code>${timeStr}</code>`
};
};
@@ -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 };
};
};