免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

网页版群聊系统实战:用 WebSocket + Mongoose 搭建 TaoToken 配置骨架

网页版群聊系统实战:用 WebSocket + Mongoose 搭建 TaoToken 配置骨架 1. 网页版群聊系统为什么绕不开 WebSocket 和 Mongoose网页版群聊系统说白了就是让多个浏览器页面同时在线任何一个人发消息其他人都能立刻看到。这个需求听起来简单但真动手写服务端第一个卡点就是通信协议选型。HTTP 是请求-响应模型客户端不问服务器就不能主动说话。群聊恰恰需要服务器主动把某个人发的消息推给所有在线的人HTTP 做不到这件事所以必须换成 WebSocket。WebSocket 位于应用层它先借一次 HTTP 请求完成握手服务器返回 101 状态码之后这条连接就从 HTTP 升级成 WebSocket之后客户端和服务器都能主动收发数据是全双工的长连接。这个特性正好匹配群聊的广播需求一个人发消息服务器遍历所有连接把消息推给每一个人。另一个绕不开的是数据层。群聊消息要落库用户要注册登录这些都需要一个稳定的数据模型层。Mongoose 在这里承担两个角色一是作为 Node.js 侧的 MongoDB 对象建模工具用 Schema 定义用户和消息的结构让读写有约束二是它底层连接池和多路复用的机制让频繁的消息写入不会把数据库连接打满。很多人第一次写群聊消息直接塞内存数组重启就丢用 Mongoose 把消息落库之后历史记录才能查得回来。这篇文章面向的是想跑通最小群聊闭环的开发者。我会用 WebSocket 长连接加 Mongoose 数据模型作为主线给出可复制的 config.toml 和 settings.json 骨架把 TaoToken 统一 Key 的接入位置标清楚最后用连接建立、消息落库、断线重连三个动作验证整条链路。你跟着做能拿到一个能跑起来的服务端骨架而不是一堆散落的代码片段。需要提前说明的是TaoToken 在这里的角色是统一模型调用入口。群聊系统里如果有 AI 回复、消息摘要、敏感词判断这类需要调模型的能力走 TaoToken 的 API 可以统一管理 Key不用在每个模块里散落不同的凭证。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这两个地址后面配置里会用到。2. TaoToken 前置准备统一 Key 与配置文件骨架在写 WebSocket 和 Mongoose 之前先把配置层搭好。很多项目后期难维护就是因为 Key 和连接串散落在各个文件里。我习惯在项目根目录放两个配置文件config.toml 管服务端运行参数settings.json 管模型调用相关的凭证和模型 ID。这样换环境、换 Key 只改一处。先说 TaoToken 的 Key 怎么拿。进入控制台在 API Keys 页面创建一个新的 Key复制出来。这个 Key 就是后面 settings.json 里要填的值。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建的时候建议按项目命名比如 chat-server-dev方便后面区分。拿到 Key 之后先写 config.toml。这个文件管的是服务端本身的参数跟模型调用无关# config.toml [server] host 0.0.0.0 port 8080 ws_path /ws [database] uri mongodb://127.0.0.1:27017/chatroom pool_size 10 server_selection_timeout_ms 5000 [message] max_length 2000 history_page_size 50这里几个参数值得说清楚。ws_path 是 WebSocket 的挂载路径前端连接时要用 ws://host:port/ws。database.uri 是 Mongoose 的连接串chatroom 是库名。pool_size 控制连接池大小群聊消息写入频繁给 10 比较稳。server_selection_timeout_ms 是选主超时本地单机 MongoDB 给 5000 毫秒够用。再写 settings.json这个文件管模型调用{ taotoken: { base_url: https://taotoken.net/api, api_key: sk-你的TaoTokenKey, model_id: claude-sonnet-4-5, timeout_ms: 30000 }, features: { ai_reply: true, message_summary: false } }base_url 固定填 https://taotoken.net/api 注意这里不加任何查询参数。api_key 填你刚才在控制台创建的 Key。model_id 按你实际要用的模型填比如做 AI 回复可以用 claude-sonnet-4-5 这类。timeout_ms 是请求超时群聊场景下 AI 回复不能等太久30 秒是上限。这两个文件的分工要记牢config.toml 管基础设施settings.json 管模型凭证。后面代码里读取的时候服务启动读 config.toml需要调模型时读 settings.json。这样即使你把项目部署到不同环境也只需要替换这两个文件不用翻代码。有一点要提醒settings.json 里含 Key务必加入 .gitignore不要提交到仓库。生产环境建议用环境变量覆盖代码里读取时优先取 process.env取不到再读文件。这个习惯能避免很多凭证泄露的坑。3. 可复制配置WebSocket 服务与 Mongoose 模型落地配置骨架有了接下来把 WebSocket 服务和 Mongoose 模型接上。这一步的目标是让服务能启动、能建连、能落库。我按文件拆开讲每个文件都可以直接复制。先建 Mongoose 模型文件 models/Message.js 和 models/User.js// models/Message.js const mongoose require(mongoose); const messageSchema new mongoose.Schema({ roomId: { type: String, required: true, index: true }, userId: { type: String, required: true }, nickname: { type: String, default: 匿名 }, content: { type: String, required: true, maxlength: 2000 }, type: { type: String, enum: [text, system, ai], default: text }, createdAt: { type: Date, default: Date.now, index: true } }); messageSchema.index({ roomId: 1, createdAt: -1 }); module.exports mongoose.model(Message, messageSchema);// models/User.js const mongoose require(mongoose); const userSchema new mongoose.Schema({ username: { type: String, required: true, unique: true }, passwordHash: { type: String, required: true }, nickname: { type: String, default: }, online: { type: Boolean, default: false }, lastSeenAt: { type: Date, default: Date.now } }); module.exports mongoose.model(User, userSchema);Message 模型里 roomId 和 createdAt 都建了索引因为群聊查历史记录基本是按房间加时间倒序。这个复合索引 { roomId: 1, createdAt: -1 } 能让分页查询走索引不会全表扫。然后是数据库连接和配置读取// lib/db.js const mongoose require(mongoose); const fs require(fs); const toml require(iarna/toml); const config toml.parse(fs.readFileSync(./config.toml, utf-8)); async function connectDB() { await mongoose.connect(config.database.uri, { maxPoolSize: config.database.pool_size, serverSelectionTimeoutMS: config.database.server_selection_timeout_ms }); console.log([db] connected); } module.exports { connectDB, config };WebSocket 服务用 ws 库配合 HTTP server 一起起// server.js const http require(http); const WebSocket require(ws); const { connectDB, config } require(./lib/db); const Message require(./models/Message); const server http.createServer(); const wss new WebSocket.Server({ server, path: config.server.ws_path }); const clients new Map(); wss.on(connection, (ws, req) { const userId new URL(req.url, http://localhost).searchParams.get(userId) || guest; clients.set(ws, { userId }); ws.on(message, async (raw) { let payload; try { payload JSON.parse(raw.toString()); } catch (e) { return ws.send(JSON.stringify({ error: invalid json })); } const doc await Message.create({ roomId: payload.roomId || default, userId, nickname: payload.nickname || 匿名, content: payload.content, type: text }); const out JSON.stringify({ id: doc._id, userId, nickname: doc.nickname, content: doc.content, createdAt: doc.createdAt }); for (const [client] of clients) { if (client.readyState WebSocket.OPEN) client.send(out); } }); ws.on(close, () clients.delete(ws)); }); connectDB().then(() { server.listen(config.server.port, config.server.host, () { console.log([ws] listening on ${config.server.host}:${config.server.port}); }); });这段代码里连接建立时从 URL 参数取 userId消息进来先落库再广播。落库用 Message.create广播遍历 clients 这个 Map。注意广播前判断 readyState避免往已关闭的连接发数据报错。如果你要在群聊里加 AI 回复就在落库之后调 TaoToken// lib/ai.js const fs require(fs); const settings JSON.parse(fs.readFileSync(./settings.json, utf-8)); async function aiReply(content) { const res await fetch(${settings.taotoken.base_url}/v1/messages, { method: POST, headers: { Content-Type: application/json, x-api-key: settings.taotoken.api_key, anthropic-version: 2023-06-01 }, body: JSON.stringify({ model: settings.taotoken.model_id, max_tokens: 512, messages: [{ role: user, content }] }) }); const data await res.json(); return data.content?.[0]?.text || ; }这里 base_url 直接读 settings.json 里的 https://taotoken.net/api Key 走 x-api-key 头。模型 ID 也从配置读不写死在代码里。这样换模型只改 settings.json。4. 验证请求连接建立、消息落库、断线重连三步走配置写完必须验证。我按三步来每步都有明确的成功标志你照着做能确认链路是通的。第一步验证连接建立。启动 MongoDB再启动服务node server.js看到 [db] connected 和 [ws] listening on 0.0.0.0:8080 就说明服务起来了。然后用 wscat 或浏览器控制台建连npx wscat -c ws://127.0.0.1:8080/ws?userIdu001连上之后终端显示 Connected说明 WebSocket 握手成功。这一步如果失败先查端口是否被占再查 path 是否和 config.toml 里的 ws_path 一致。第二步验证消息落库。在 wscat 里发一条 JSON{roomId:default,nickname:测试用户,content:第一条群聊消息}发送后你会在同一个 wscat 里收到广播回来的消息带 id 和 createdAt。这时候去 MongoDB 里查mongosh chatroom --eval db.messages.find().sort({createdAt:-1}).limit(1)能看到刚才那条消息说明落库成功。如果广播收到了但库里没有检查 Message.create 是否抛错常见的是 content 超过 maxlength 2000 被拒。第三步验证断线重连。前端侧的重连逻辑这样写// client reconnect let ws; let retry 0; function connect() { ws new WebSocket(ws://127.0.0.1:8080/ws?userIdu001); ws.onopen () { retry 0; console.log(connected); }; ws.onclose () { const delay Math.min(1000 * Math.pow(2, retry), 30000); retry; console.log(reconnect in ${delay}ms); setTimeout(connect, delay); }; ws.onmessage (e) console.log(msg:, e.data); } connect();验证方法连上之后手动把服务停掉观察控制台是否按 1s、2s、4s 的间隔重试。再把服务起回来看是否自动恢复连接并能继续收发消息。指数退避上限设 30 秒避免无限快速重试打爆服务端。三步都通过最小群聊闭环就跑通了连接能建、消息能存、断了能重连。这时候你再去加房间切换、历史记录分页、AI 回复都是在稳定骨架上叠功能不会返工。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth配置和验证过程中有几类报错出现频率特别高。我把它们和真实原因对照着列出来你遇到时直接对号入座。401 Unauthorized。这个基本都出在调 TaoToken 的时候。原因通常是 settings.json 里的 api_key 填错或者请求头名字写错。Anthropic 风格的接口用 x-api-keyOpenAI 风格用 Authorization: Bearer。如果你混用了就会 401。排查方法把 Key 复制到模型对话页面手动发一条能通说明 Key 没问题问题在代码里的头字段。模型对话入口是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 。local proxy failed。这个报错通常出现在你本地配了代理但代理进程没起或者端口不对。群聊服务本身不需要代理如果你在 settings.json 或环境变量里设了 HTTP_PROXY而代理不可用fetch 就会报这个。解决办法是把代理环境变量清掉或者确认代理进程在跑。注意这里说的是本地开发环境的网络配置问题不是让你去搭什么通道只是排查环境变量。reading choices。这个报错是典型的响应结构不匹配。代码里按 OpenAI 格式读 data.choices[0].message.content但实际返回的是 Anthropic 格式 data.content[0].text读 choices 就是 undefined再取下标就报 reading choices。解决办法是确认你用的接口格式Anthropic 风格读 content 数组OpenAI 风格读 choices 数组。settings.json 里 model_id 和接口格式要配套。OAuth 相关报错。如果你用的是 Claude Code 这类工具它可能走 OAuth 流程而不是 API Key。OAuth 报错通常是 token 过期或回调地址不对。这种情况建议改用 API Key 方式接入在 settings.json 里配 base_url 和 api_key避免 OAuth 的复杂性。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL、Key、Model ID 三件套的完整填法。还有一个容易忽略的Mongoose 连接超时。报错是 serverSelectionTimeoutError原因是 MongoDB 没起或者 uri 写错。先确认 mongod 进程在跑再确认 config.toml 里的 uri 端口和库名对得上。本地默认是 127.0.0.1:27017如果你改了端口配置也要跟着改。排查的时候有个通用思路先确认基础设施MongoDB、端口再确认凭证Key、头字段最后确认数据结构响应格式。按这个顺序查大部分问题五分钟内能定位。6. 从最小闭环到长期编码把配置骨架用起来跑通最小闭环之后这个骨架能支撑你继续往下走。群聊系统往后加功能无非是几个方向房间管理、历史记录、在线状态、AI 辅助。这些都能在现有结构上扩展不用推倒重来。房间管理就是在 Message 模型里用 roomId 区分广播时按 roomId 过滤 clients。历史记录用 Message.find({ roomId }).sort({ createdAt: -1 }).limit(pageSize) 分页复合索引已经建好查询很快。在线状态用 User 模型的 online 字段连接建立时置 true断开时置 false。AI 辅助这块如果你要做长期编码或者 Agent 类的功能比如让 AI 自动回复群消息、做消息摘要、判断敏感词建议用 Coding Plan 来管理调用额度。入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它适合需要持续调用的场景比按次计费更划算。配置骨架的价值在于你把 base_url、api_key、model_id 这三件套固定在一个文件里后面不管加多少功能接入位置都不变。新来的同事看 settings.json 就知道模型怎么调看 config.toml 就知道服务怎么起。这种一致性在项目变大之后特别重要。最后说一个我踩过的坑settings.json 里的 timeout_ms 不要设太短。群聊里 AI 回复如果 5 秒超时用户会觉得卡。给 30 秒前端加个 loading 状态体验会好很多。另外重连的指数退避上限也别设太小30 秒比较合适既能快速恢复又不会在服务端故障时疯狂重试。骨架搭好剩下的就是往里填业务。先把三步验证跑通再动手加功能顺序别反。
返回列表