基于 Cloudflare Workers + Telegram Bot + D1 的双向匿名聊天系统完整实现

2565 字
13 分钟
基于 Cloudflare Workers + Telegram Bot + D1 的双向匿名聊天系统完整实现

在本篇文章中,我将分享一个基于 Cloudflare Workers + Telegram Bot + D1 数据库 的完全无服务器(Serverless)聊天系统。该机器人支持用户匿名与管理员双向通信,具备首访验证、自动诈骗检测、屏蔽管理、消息映射等功能,是一个非常适合作为客服、匿名反馈、小型 IM 系统的解决方案。


系统亮点#

  • 🚀 100% Serverless 无需服务器
  • 🔐 首访用户自动加减法验证(防骚扰)
  • 🔁 双向匿名对话(用户看不到管理员 ID)
  • 🛡 管理员可屏蔽 / 解封任何用户
  • 🧠 自动诈骗账号检测(基于 D1 fraud 表)
  • 💾 D1 数据库存储用户状态与消息映射
  • 📌 管理员收到用户消息时自动生成“用户资料卡”
  • 🔔 可控频率的通知提醒机制

🧩 系统架构#

[Telegram User]
↓ 发送消息
[Cloudflare Workers /endpoint]
[验证系统(加减法 1~10)]
↓ 通过后
[反诈检测(fraud)]
[自动转发消息 → 管理员]
管理员 ←→ 用户 (双向聊天)

📦 使用技术#

技术用途
Cloudflare Workers核心逻辑执行环境
Telegram Bot API双向聊天接口
Cloudflare D1用户资料、屏蔽状态、验证码、消息映射存储
Webhooks实时接收 Telegram 更新事件

🗄 数据库结构(D1)#

为保证机器人功能完整,需要创建以下表:

CREATE TABLE blocks (
user_id TEXT PRIMARY KEY,
is_blocked INTEGER
);
CREATE TABLE msg_map (
admin_msg_id TEXT PRIMARY KEY,
user_id TEXT
);
CREATE TABLE lastmsg (
user_id TEXT PRIMARY KEY,
ts INTEGER
);
CREATE TABLE fraud (
user_id TEXT PRIMARY KEY
);
CREATE TABLE verify (
user_id TEXT PRIMARY KEY,
question TEXT,
answer INTEGER,
passed INTEGER
);
CREATE TABLE user_first (
user_id TEXT PRIMARY KEY
);

🔢 首次验证系统(加减法 1~10)#

为了防止广告机器人、骚扰用户,系统会对第一次聊天的用户进行简单的数学验证:

✨ 验证示例#

为了防止骚扰,请先完成验证:
7 + 3 = ?
请直接回复数字答案。

答对后自动进入聊天流程;答错则重新生成题目。


🎯 核心功能说明#

1️⃣ 匿名聊天(Forward/Copy)#

用户发送消息 → 机器人自动转发给管理员
管理员「回复这条转发消息」→ 机器人将回复复制给用户

整个过程:

  • 用户不会看到管理员的 UID
  • 管理员同样看不到用户昵称(除资料卡)

实现完整“匿名双向通信”。


2️⃣ 用户资料卡(首次出现)#

管理员第一次收到用户消息时,自动生成用户信息卡:

管理员点按钮后 → 自动屏蔽


3️⃣ 屏蔽 / 解除屏蔽#

管理员可直接通过命令控制:

指令说明
/block屏蔽此用户
/unblock解除屏蔽
/checkblock查看用户是否被屏蔽

4️⃣ 诈骗检测(D1 fraud 表)#

只需点击按钮

或者

回复的是输入/block 也可以拉黑用户


5️⃣ 可控间隔的通知提醒系统#

为了避免管理员被刷屏,可设置提醒间隔(默认 1 小时):

作用:

  • 用户第一次联系 → 提醒
  • 用户长时间没聊天,再次联系 → 再提醒一次
  • 保证管理员不会被Spam

🛠 Worker 代码结构说明#

代码整体分为:

  • Webhook 接收器
  • Telegram API 封装
  • 核心消息处理
  • 加减法验证模块
  • 管理员控制命令
  • D1 数据访问模块
  • 首次资料卡发送模块(可扩展)

该架构清晰、可维护性高、支持未来扩展。


📚 使用教程#

1. 创建 Telegram Bot#

在 @BotFather 创建一个机器人,并获得:

  • Bot Token

2. 部署到 Cloudflare Workers#

// ================== 常量区 ==================
const START_MESSAGE = `
🤖 你好,欢迎使用 @guaguawa_bot 双向聊天机器人!
请通过此机器人发送您的消息与我联系,我会立即为您传达。
`;
const START_MESSAGE_ADMIN = `
🤖 <b>关于本机器人</b>
本机器人用于 <b>双向匿名聊天</b>,旨在提高用户的沟通体验,并防止骚扰。
📌 <b>功能介绍:</b>
- 匿名聊天:用户可以自由交流,管理员可管理消息流转。
- 防骚扰功能:管理员可使用 /block 指令屏蔽不良用户。
- 诈骗检测:自动检测是否属于诈骗用户,并提醒管理员。
📜 <b>可用命令:</b>
- /start 获取欢迎信息
- /block 屏蔽用户(管理员)
- /unblock 解除屏蔽(管理员)
- /checkblock 查询用户状态(管理员)
🔹 机器人由 <b>匿名聊天管理系统</b> 提供支持。
`;
// ================== Worker 入口(ESM 格式) ==================
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === "/endpoint") {
return handleWebhook(request, env, ctx);
}
if (url.pathname === "/registerWebhook") {
return registerWebhook(request, env);
}
if (url.pathname === "/unRegisterWebhook") {
return unRegisterWebhook(request, env);
}
return new Response("OK");
}
};
// ===================== Telegram API 封装 ========================
function apiUrl(methodName, params = null, env) {
let query = params ? "?" + new URLSearchParams(params).toString() : "";
return `https://api.telegram.org/bot${env.ENV_BOT_TOKEN}/${methodName}${query}`;
}
function requestTelegram(method, body, env, params = null) {
return fetch(apiUrl(method, params, env), body).then(r => r.json());
}
const makeReq = (body) => ({
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body)
});
const sendMessage = (msg, env) =>
requestTelegram("sendMessage", makeReq(msg), env);
const forwardMessage = (msg, env) =>
requestTelegram("forwardMessage", makeReq(msg), env);
const copyMessage = (msg, env) =>
requestTelegram("copyMessage", makeReq(msg), env);
// ===================== Webhook ========================
async function handleWebhook(request, env, ctx) {
if (request.headers.get("X-Telegram-Bot-Api-Secret-Token") !== env.ENV_BOT_SECRET) {
return new Response("Unauthorized", { status: 403 });
}
const update = await request.json();
ctx.waitUntil(onUpdate(update, env));
return new Response("OK");
}
// ===================== Update Routing ========================
async function onUpdate(update, env) {
if (update.message) {
await onMessage(update.message, env);
}
if (update.callback_query) {
await onCallback(update.callback_query, env);
}
}
// ===================== D1 数据库封装 ========================
// 拉黑
async function getBlock(env, userId) {
const row = await env.TG_BOT_DB.prepare("SELECT is_blocked FROM blocks WHERE user_id=?").bind(userId).first();
return row ? row.is_blocked === 1 : false;
}
async function setBlock(env, userId, val) {
await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO blocks (user_id, is_blocked) VALUES (?, ?)").bind(userId, val ? 1 : 0).run();
}
// 消息映射
async function getMsgMap(env, adminMsgId) {
const row = await env.TG_BOT_DB.prepare("SELECT user_id FROM msg_map WHERE admin_msg_id=?").bind(adminMsgId).first();
return row ? row.user_id : null;
}
async function setMsgMap(env, adminMsgId, userId) {
await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO msg_map (admin_msg_id, user_id) VALUES (?, ?)").bind(adminMsgId, userId).run();
}
// 通知节流
async function getLastMsg(env, userId) {
const row = await env.TG_BOT_DB.prepare("SELECT ts FROM lastmsg WHERE user_id=?").bind(userId).first();
return row ? row.ts : null;
}
async function setLastMsg(env, userId, ts) {
await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO lastmsg (user_id, ts) VALUES (?, ?)").bind(userId, ts).run();
}
// 诈骗
async function isFraud(env, userId) {
const row = await env.TG_BOT_DB.prepare("SELECT user_id FROM fraud WHERE user_id=?").bind(userId).first();
return !!row;
}
// ===================== 首次加减验证 ========================
function genVerifyQuestion() {
let a = Math.floor(Math.random() * 10) + 1;
let b = Math.floor(Math.random() * 10) + 1;
if (Math.random() < 0.5) {
return { question: `${a} + ${b} = ?`, answer: a + b };
} else {
if (a < b) [a, b] = [b, a];
return { question: `${a} - ${b} = ?`, answer: a - b };
}
}
async function getVerify(env, userId) {
return await env.TG_BOT_DB.prepare("SELECT * FROM verify WHERE user_id=?").bind(userId).first();
}
async function setVerify(env, userId, question, answer) {
await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO verify (user_id, question, answer, passed) VALUES (?, ?, ?, 0)").bind(userId, question, answer).run();
}
async function passVerify(env, userId) {
await env.TG_BOT_DB.prepare("UPDATE verify SET passed=1 WHERE user_id=?").bind(userId).run();
}
// ===================== 消息主处理 ========================
async function onMessage(message, env) {
const chatId = message.chat.id.toString();
const ADMIN_UID = env.ENV_ADMIN_UID;
// /start
if (message.text === "/start") {
return sendMessage({ chat_id: chatId, text: chatId === ADMIN_UID ? START_MESSAGE_ADMIN : START_MESSAGE, parse_mode: "HTML" }, env);
}
// -------- 管理员 --------
if (chatId === ADMIN_UID) {
return handleAdminMessage(message, env);
}
// -------- 用户 --------
return handleGuestMessage(message, env);
}
// ===================== 管理员逻辑 ========================
async function handleAdminMessage(message, env) {
const ADMIN_UID = env.ENV_ADMIN_UID;
if (!message.reply_to_message?.message_id) {
return sendMessage({ chat_id: ADMIN_UID, text: START_MESSAGE_ADMIN, parse_mode: "HTML" }, env);
}
const guestId = await getMsgMap(env, message.reply_to_message.message_id.toString());
if (!guestId) {
return sendMessage({ chat_id: ADMIN_UID, text: "无法找到用户映射" }, env);
}
// 管理员命令
if (message.text === "/block") return handleBlock(message, env);
if (message.text === "/unblock") return handleUnblock(message, env);
if (message.text === "/checkblock") return handleCheckBlock(message, env);
// 正常回复
return copyMessage({
chat_id: guestId,
from_chat_id: ADMIN_UID,
message_id: message.message_id
}, env);
}
// ===================== 用户逻辑(含验证 + 资料卡) ========================
async function handleGuestMessage(message, env) {
const chatId = message.chat.id.toString();
const ADMIN_UID = env.ENV_ADMIN_UID;
// --- 1. 验证 ---
const verify = await getVerify(env, chatId);
if (!verify) {
const q = genVerifyQuestion();
await setVerify(env, chatId, q.question, q.answer);
return sendMessage({
chat_id: chatId,
text: `为了防止骚扰,请先完成验证:\n\n<b>${q.question}</b>`,
parse_mode: "HTML"
}, env);
}
if (verify.passed !== 1) {
const ans = parseInt(message.text);
if (ans === verify.answer) {
await passVerify(env, chatId);
return sendMessage({
chat_id: chatId,
text: "验证通过!你现在可以给管理员发送消息了 🎉"
}, env);
}
const q = genVerifyQuestion();
await setVerify(env, chatId, q.question, q.answer);
return sendMessage({
chat_id: chatId,
text: `❌ 错误,请重新验证:\n\n<b>${q.question}</b>`,
parse_mode: "HTML"
}, env);
}
// --- 2. 是否被屏蔽 ---
if (await getBlock(env, chatId)) {
return sendMessage({ chat_id: chatId, text: "🚫 你被拉黑了" }, env);
}
// --- 3. 首次与管理员建立联系 → 发送资料卡 ---
const firstRow = await env.TG_BOT_DB.prepare("SELECT user_id FROM user_first WHERE user_id=?").bind(chatId).first();
if (!firstRow) {
await env.TG_BOT_DB.prepare("INSERT INTO user_first (user_id) VALUES (?)").bind(chatId).run();
const fullname = (message.from.first_name || "") + (message.from.last_name ? " " + message.from.last_name : "");
const username = message.from.username ? "@" + message.from.username : "无";
const now = new Date();
const timeDisplay = now.toLocaleString("zh-CN", { hour12: false });
const userCard = `
<b>用户资料卡</b>
---
• 昵称名称: <b>${fullname}</b>
• 用户名: <b>${username}</b>
• ID: <code>${chatId}</code>
• 首次连接时间: <code>${timeDisplay}</code>
`;
const blockKeyboard = {
inline_keyboard: [
[{ text: "🚫 屏蔽此人 (Block)", callback_data: `block:${chatId}` }]
]
};
await sendMessage({
chat_id: ADMIN_UID,
text: userCard,
parse_mode: "HTML",
reply_markup: blockKeyboard
}, env);
}
// --- 4. 转发消息 ---
const fw = await forwardMessage({
chat_id: ADMIN_UID,
from_chat_id: chatId,
message_id: message.message_id
}, env);
if (fw.ok) {
await setMsgMap(env, fw.result.message_id.toString(), chatId);
}
}
// ===================== 回调按钮(屏蔽) ========================
async function onCallback(query, env) {
const ADMIN_UID = env.ENV_ADMIN_UID;
if (query.from.id.toString() !== ADMIN_UID) {
return;
}
const data = query.data;
if (data.startsWith("block:")) {
const userId = data.split(":")[1];
await setBlock(env, userId, true);
await requestTelegram(
"answerCallbackQuery",
makeReq({ callback_query_id: query.id, text: "已拉黑该用户" }),
env
);
await requestTelegram(
"editMessageReplyMarkup",
makeReq({
chat_id: query.message.chat.id,
message_id: query.message.message_id,
reply_markup: {
inline_keyboard: [[{ text: "✔ 已拉黑", callback_data: "noop" }]]
}
}),
env
);
}
}
// ===================== 管理员命令 ========================
async function handleBlock(message, env) {
const guest = await getMsgMap(env, message.reply_to_message.message_id.toString());
await setBlock(env, guest, true);
return sendMessage({ chat_id: env.ENV_ADMIN_UID, text: `UID:${guest} 已屏蔽` }, env);
}
async function handleUnblock(message, env) {
const guest = await getMsgMap(env, message.reply_to_message.message_id.toString());
await setBlock(env, guest, false);
return sendMessage({ chat_id: env.ENV_ADMIN_UID, text: `UID:${guest} 已解除屏蔽` }, env);
}
async function handleCheckBlock(message, env) {
const guest = await getMsgMap(env, message.reply_to_message.message_id.toString());
const blocked = await getBlock(env, guest);
return sendMessage({
chat_id: env.ENV_ADMIN_UID,
text: `UID:${guest} → ${blocked ? "已屏蔽" : "未屏蔽"}`
}, env);
}
// ===================== Webhook 控制 ========================
async function registerWebhook(request, env) {
const url = new URL(request.url);
const webhookUrl = `${url.protocol}//${url.hostname}/endpoint`;
const res = await fetch(apiUrl("setWebhook", { url: webhookUrl, secret_token: env.ENV_BOT_SECRET }, env)).then(r => r.json());
return new Response(res.ok ? "OK" : JSON.stringify(res));
}
async function unRegisterWebhook(request, env) {
const res = await fetch(apiUrl("setWebhook", { url: "" }, env)).then(r => r.json());
return new Response(res.ok ? "OK" : JSON.stringify(res));
}

然后把上面的代码全部复制到里面去.

3. 绑定 D1 数据库#

D1的key为_BOT_DB

4. 设置环境变量#

变量说明
ENV_BOT_TOKENTelegram Bot Token
ENV_ADMIN_UID管理员 UID
ENV_BOT_SECRETWebhook secret(可随意生成)

  1. @BotFather获取token,并且可以发送/setjoingroups来禁止此Bot被添加到群组
  2. uuidgenerator获取一个随机uuid作为secret
  3. @username_to_id_bot获取你的用户id
  4. 配置worker的变量
    • 增加一个ENV_BOT_TOKEN变量,数值为从步骤1中获得的token
    • 增加一个ENV_BOT_SECRET变量,数值为从步骤2中获得的secret
    • 增加一个ENV_ADMIN_UID变量,数值为从步骤3中获得的用户id

5. 注册 Webhook#

访问:

https://你的worker域名/registerWebhook

6. 测试#

测试Bot: @guaguawa_bot

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

基于 Cloudflare Workers + Telegram Bot + D1 的双向匿名聊天系统完整实现
https://muxui.com/posts/基于-cloudflare-workers-telegram-bot-d1-的双向匿名聊天系统完整实现/
作者
Muxui
发布于
2025-11-16
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
从 WordPress 到 Astro迁移实战全记录
文章记录将 Muxui 从 WordPress(B2 Pro)完整迁移到 Astro 静态博客主题 Firefly 的全过程:导出 WXR、HTML 转 Markdown、图床改写、友链与导航定制,以及迁移中踩过的坑。
2
YOLO训练麻将(Mahjong)识别,导出ONNX
文章本文基于 Roboflow Universe 数据集(例如 $1 )与仓库内脚本,在 Windows 上完成训练,并导出 ONNX 供后端推理。下文配图均为本机实测截图,便于对照。快速体验 小程序 1\. 环境要求 \ Windows 10
3
自建 Headscale + DERP 全流程实战记录
文章记录一次完整、可上线、可长期运行的 Headscale + 自建 DERP 搭建过程。 本文不是“能跑就行”的教程,而是 生产可用、已多客户端验证 的配置方案。 简介 Tailscale(Headscale)就是组建一个大的局域网,可以将你
4
Mac中磁盘没有正常推出的解决方案
文章由于mac电脑关机或者没有实现正常推出硬盘导致的磁盘没有正常推出。 解决办法 : 1)打开终端,输入如下命令查看一下硬盘的信息,确定一下自己的磁盘是否可以显示: diskutil list 在终端的最后一行显示了我硬盘的名字,因此 /dev
5
白嫖 .com .net .jp .shop .xyz 等顶级域名
文章1.$1 日本IP 点开活动地址之后搜索你想要的域名 注意全程日本IP 身份生成:$1 2.付款选择 $1 输入你的卡信息 卡需要支持3ds 3.看到 您的申请已被接受。 来这里 $1 登录你的账户,账号是右上角的数字 。在邮箱里也可以看到
随机文章随机推荐

评论区

Profile Image of the Author
Muxui
Hello, I'm Muxui.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
24
分类
8
标签
19
总字数
53,213
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.14.5
文章许可
CC BY-NC-SA 4.0