news 2026/10/4 17:30:04

两周速成交付:Go + Vue3 + Cursor 复刻 B 站(cakecake),弹幕/私信/AI 助手全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
两周速成交付:Go + Vue3 + Cursor 复刻 B 站(cakecake),弹幕/私信/AI 助手全链路

1. 为什么我选择用 Go + Vue3 + Cursor 复刻 B 站 cakecake

如果你正在找一个能同时练到 Go 后端、Vue3 前端、WebSocket 实时通信和 AI 助手接入的全栈项目,cakecake 这个仿 B 站视频社区是个很合适的目标。它把视频投稿、实时弹幕、私信、全文搜索、AI 问答这些真实业务场景都串起来了,不是那种只做个 CRUD 的玩具项目。我实测下来,用 Cursor 辅助写代码,两周内跑通弹幕、私信、AI 助手三条链路是可行的,前提是配置和接口约定要提前定好。

cakecake 的技术栈是 Go 1.25 + Gin + GORM + Vue 3.5 + Vite 6 + TypeScript + MySQL + Redis + RabbitMQ + Elasticsearch + FFmpeg + WebSocket。听起来多,但核心链路其实就三块:弹幕走 WebSocket 长连接,私信走 WebSocket + 离线消息落库,AI 助手走 HTTP 流式接口对接大模型。你不需要一次性把所有中间件都装齐,先跑通 MySQL + Redis + WebSocket 就能看到弹幕飞起来。

适合谁跟做?有 Go 基础语法、Vue3 组合式 API 用过一点、想通过一个完整项目把前后端串起来的同学。如果你完全没写过 WebSocket,也没关系,我会把关键代码和配置都贴出来,你复制改改就能跑。Cursor 在这里的角色是帮你补全重复代码、生成类型定义、排查报错,而不是替你理解业务。我踩过的坑是:一开始让 Cursor 直接生成整个弹幕模块,结果它把消息广播写成了单播,调了半天才发现。所以规则文件要先约束好。

这篇会按「环境准备 → Cursor 规则配置 → Go 后端接口 → Vue3 前端组件 → 联调验证 → 报错排查」的顺序走,每一步都给可复制的代码和命令。TaoToken 在这里的作用是统一 AI 助手的 Key 和 API 通道,你不用在多个模型平台之间来回切换,一个 Key 就能调通对话接口。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,后面配置 AI 助手时会用到。

2. TaoToken 前置准备:统一 Key 与 API 通道接入 cakecake AI 助手

cakecake 的 AI 助手模块原本对接的是 DeepSeek,但实际开发中你可能会想换模型、做对比,或者团队里多人共用一套额度。这时候如果每个模型都单独申请 Key、单独改代码,维护成本很高。TaoToken 的思路是提供一个统一的 API 通道,你用同一个 Key 和 Base URL 就能调用不同模型,代码里只需要改 Model ID 这一个参数。对 cakecake 这种需要快速迭代 AI 助手的项目来说,省去了反复改配置的麻烦。

先说清楚它是什么:TaoToken 是一个大模型 API 聚合通道,兼容 OpenAI 风格的接口格式。你拿到 Key 之后,把 Base URL 指向 https://taotoken.net/api ,然后在请求体里指定模型名,就能发起对话。它不改变你原有的调用逻辑,只是把「找多个平台、管多个 Key」这件事收敛成一个入口。适合谁?适合正在做 AI 功能原型、需要频繁切换模型对比效果、或者不想在 Key 管理上花太多时间的开发者。

接入步骤很直接。第一步,打开 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 创建 API Key,复制保存好,后面 Go 后端的环境变量里要用。第二步,确认你要用的模型 ID,比如 deepseek-chat、gpt-4o-mini 这类,具体以文档为准,文档地址在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。第三步,在 cakecake 后端的 .env 里加两个变量:TAOTOKEN_API_KEY 和 TAOTOKEN_BASE_URL,Base URL 填 https://taotoken.net/api 。

这里有个细节要注意:TaoToken 的接口路径是 /v1/chat/completions,所以你在 Go 代码里拼接的时候,Base URL 后面要带上 /v1。我一开始只写了 https://taotoken.net/api ,结果请求 404,后来在文档里看到完整路径才改对。另外,如果你用的是流式输出(AI 助手打字机效果),请求体里要加 "stream": true,Go 侧用 bufio.Scanner 逐行读 SSE 数据。这部分代码在下一节会给。

还有一点,TaoToken 的 Key 不要硬编码在前端,一定要放在 Go 后端的服务端环境变量里,前端只调你自己的后端接口。这样既安全,也方便你做请求频率控制和日志记录。如果你后面要上 Coding Plan 做长期编码辅助,可以看 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,不过 cakecake 项目本身用按量计费的 API Key 就够了。

3. 可复制配置:Cursor 规则文件与 Go/Vue3 环境变量

这一节直接给可复制的配置文件,你照着建文件、填内容就行。先说 Cursor 的规则文件。Cursor 支持在项目根目录放 .cursorrules 文件,用来约束 AI 生成代码的风格和边界。cakecake 是前后端分离的,我建议在根目录放一份总规则,然后在 backend 和 frontend 目录各放一份细分规则。总规则内容如下,你直接复制到 .cursorrules :

# cakecake 项目规则 - 后端使用 Go 1.25 + Gin + GORM,目录结构:main.go 入口,internal/handler 放 HTTP 处理,internal/service 放业务逻辑,internal/model 放数据模型。 - 前端使用 Vue 3.5 + Vite 6 + TypeScript,组件放 src/components,页面放 src/views,API 请求统一走 src/api 目录下的封装。 - WebSocket 消息格式统一为 JSON:{"type":"danmaku|private_msg","payload":{...}},type 字段必填。 - 弹幕广播必须用房间维度,禁止全局广播。私信必须落库后再推送。 - AI 助手接口走服务端代理,前端不直接持有任何模型 API Key。 - 所有新增接口必须在 docs/api.md 里补一行说明。

这份规则的作用是让 Cursor 在补全代码时知道项目结构,不会把 handler 写到 main.go 里,也不会把弹幕写成全局广播。我实测下来,加了这份规则之后,Cursor 生成的 WebSocket 代码一次通过率明显提高。如果你用的是 Cursor 的 Composer 功能,可以在对话里 @ 这个文件,让它按规则生成。

接下来是后端环境变量。在 cakecake 后端根目录复制 .env.example 为 .env ,然后填入以下内容:

# 数据库 DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=root DB_PASSWORD=your_password DB_NAME=cakecake # Redis REDIS_ADDR=127.0.0.1:6379 REDIS_PASSWORD= # JWT JWT_SECRET=your_jwt_secret_change_me # TaoToken AI 助手 TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=deepseek-chat # 服务端口 SERVER_PORT=8080

前端环境变量在 cakecake-vue/bilibili-vue 目录下,复制 .env.example 为 .env :

VITE_API_BASE_URL=http://localhost:8080/api VITE_WS_URL=ws://localhost:8080/ws

这两个文件建好之后,后端 go mod tidy && go run main.go ,前端 npm install && npm run dev ,访问 http://localhost:8888 就能看到首页。如果数据库还没建,先执行建库语句:

CREATE DATABASE cakecake DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

然后 GORM 的 AutoMigrate 会自动建表。这里有个坑:MySQL 8.0 默认认证插件是 caching_sha2_password,Go 的驱动有时候连不上,你可以在 MySQL 配置里改成 mysql_native_password,或者升级驱动版本。我遇到的是连接超时,后来发现是 DB_HOST 写成了 localhost 但 MySQL 只监听 127.0.0.1,改成 127.0.0.1 就好了。

Cursor 规则和两份 .env 都配好之后,你的项目骨架就具备了。下一节开始写弹幕和私信的 Go 后端接口。

4. Go 后端接口:弹幕 WebSocket 与私信落库实现

弹幕是 cakecake 最核心的实时功能,用 WebSocket 实现。Go 侧我用的是 gorilla/websocket 库,先装依赖:

go get github.com/gorilla/websocket

然后在 internal/handler 下新建 danmaku.go ,核心逻辑是维护一个房间维度的连接池。每个视频房间对应一个 Hub,Hub 里存所有连接,收到弹幕消息就广播给同房间的所有连接。代码骨架如下:

package handler import ( "encoding/json" "log" "net/http" "sync" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) type DanmakuMsg struct { Type string `json:"type"` Payload struct { VideoID int64 `json:"video_id"` Content string `json:"content"` Color string `json:"color"` UserID int64 `json:"user_id"` } `json:"payload"` } type Hub struct { mu sync.RWMutex rooms map[int64]map[*websocket.Conn]bool } var hub = &Hub{rooms: make(map[int64]map[*websocket.Conn]bool)} var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } func DanmakuWS(c *gin.Context) { conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Println("upgrade err:", err) return } defer conn.Close() var videoID int64 for { _, data, err := conn.ReadMessage() if err != nil { hub.remove(videoID, conn) return } var msg DanmakuMsg if err := json.Unmarshal(data, &msg); err != nil { continue } if msg.Type == "danmaku" { videoID = msg.Payload.VideoID hub.add(videoID, conn) hub.broadcast(videoID, data) } } } func (h *Hub) add(videoID int64, conn *websocket.Conn) { h.mu.Lock() defer h.mu.Unlock() if h.rooms[videoID] == nil { h.rooms[videoID] = make(map[*websocket.Conn]bool) } h.rooms[videoID][conn] = true } func (h *Hub) remove(videoID int64, conn *websocket.Conn) { h.mu.Lock() defer h.mu.Unlock() if h.rooms[videoID] != nil { delete(h.rooms[videoID], conn) } } func (h *Hub) broadcast(videoID int64, data []byte) { h.mu.RLock() defer h.mu.RUnlock() for conn := range h.rooms[videoID] { conn.WriteMessage(websocket.TextMessage, data) } }

这段代码的关键点是 videoID 从第一条消息里取,之后这个连接就绑定到该房间。广播时只发给同房间的连接,不会串台。你可以在 main.go 里注册路由:

r.GET("/ws/danmaku", handler.DanmakuWS)

私信模块比弹幕多一步落库。用户 A 给用户 B 发消息,先写 MySQL,再查 B 是否在线,在线就通过 WebSocket 推送,不在线就等 B 上线后拉取离线消息。私信的消息结构体里要有 from_user_id、to_user_id、content、created_at。落库用 GORM:

type PrivateMessage struct { ID int64 `gorm:"primaryKey"` FromUserID int64 `gorm:"index"` ToUserID int64 `gorm:"index"` Content string `gorm:"type:text"` IsRead bool `gorm:"default:false"` CreatedAt int64 `gorm:"autoCreateTime"` }

发送接口用 POST /api/message/send ,收到请求后先 db.Create(&msg) ,然后查 Redis 里有没有 to_user_id 的连接标识,有就推。这里我用 Redis 存了 user_id 到 conn_id 的映射,因为 WebSocket 连接可能分布在多个实例上,单机的话直接用内存 map 也行。AI 助手的接口单独走 HTTP,在 internal/handler/ai.go 里用 http.Client 调 TaoToken:

func AIAssistant(c *gin.Context) { var req struct { Question string `json:"question"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "invalid request"}) return } body := map[string]interface{}{ "model": os.Getenv("TAOTOKEN_MODEL"), "messages": []map[string]string{ {"role": "user", "content": req.Question}, }, "stream": false, } jsonBody, _ := json.Marshal(body) httpReq, _ := http.NewRequest("POST", os.Getenv("TAOTOKEN_BASE_URL")+"/v1/chat/completions", bytes.NewBuffer(jsonBody)) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+os.Getenv("TAOTOKEN_API_KEY")) resp, err := http.DefaultClient.Do(httpReq) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) c.JSON(200, result) }

注意 Base URL 后面拼的是 /v1/chat/completions ,这是 TaoToken 兼容 OpenAI 格式的标准路径。如果你要流式输出,把 stream 改成 true,然后用 c.Stream 逐块转发。后端接口写完后,用 curl 测一下 AI 助手:

curl -X POST http://localhost:8080/api/ai/ask \ -H "Content-Type: application/json" \ -d '{"question":"cakecake 的弹幕是怎么实现的"}'

返回里有 choices 字段就说明通了。下一节写 Vue3 前端组件。

5. Vue3 前端组件:弹幕渲染、私信面板与 AI 助手对接

前端我用 Vue 3.5 的组合式 API 写,弹幕组件放在 src/components/Danmaku.vue 。核心逻辑是建立 WebSocket 连接,收到消息后创建弹幕元素,用 CSS 动画让它从右往左飘。先看 WebSocket 封装,放在 src/api/ws.ts :

export function createDanmakuWS(videoId: number, onMessage: (msg: any) => void) { const ws = new WebSocket(`${import.meta.env.VITE_WS_URL}/danmaku`) ws.onopen = () => { ws.send(JSON.stringify({ type: 'danmaku', payload: { video_id: videoId, content: '', color: '#fff', user_id: 0 } })) } ws.onmessage = (e) => { const msg = JSON.parse(e.data) if (msg.type === 'danmaku') onMessage(msg.payload) } return ws }

这里有个细节:连接建立后先发一条空弹幕,目的是让后端把这条连接绑定到 videoId 房间。之后收到的消息就都是这个房间的了。Danmaku.vue 里维护一个数组,每条弹幕有 id、content、color、top 偏移量,用 v-for 渲染:

<template> <div class="danmaku-layer"> <div v-for="item in danmakuList" :key="item.id" class="danmaku-item" :style="{ top: item.top + 'px', color: item.color }" > {{ item.content }} </div> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' import { createDanmakuWS } from '@/api/ws' const props = defineProps<{ videoId: number }>() const danmakuList = ref<any[]>([]) let ws: WebSocket | null = null onMounted(() => { ws = createDanmakuWS(props.videoId, (payload) => { const id = Date.now() + Math.random() danmakuList.value.push({ id, content: payload.content, color: payload.color || '#fff', top: Math.floor(Math.random() * 300) }) setTimeout(() => { danmakuList.value = danmakuList.value.filter(d => d.id !== id) }, 8000) }) }) onUnmounted(() => ws?.close()) </script> <style scoped> .danmaku-layer { position: absolute; inset: 0; pointer-events: none; overflow: hidden; } .danmaku-item { position: absolute; right: -200px; white-space: nowrap; animation: move-left 8s linear forwards; font-size: 18px; } @keyframes move-left { to { transform: translateX(-120vw); } } </style>

私信面板放在 src/components/MessagePanel.vue ,逻辑是拉取历史消息 + 监听 WebSocket 推送。历史消息走 GET /api/message/list?to_user_id=xxx ,新消息走 WebSocket 的 private_msg 类型。AI 助手组件放在 src/components/AIAssistant.vue ,调后端 /api/ai/ask 接口:

async function askAI() { const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/ai/ask`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: input.value }) }) const data = await res.json() answer.value = data.choices?.[0]?.message?.content || '无回复' }

注意前端不直接调 TaoToken,所有 AI 请求都经过 Go 后端代理。这样 Key 不会暴露在浏览器里。如果你要打字机效果,后端改成流式,前端用 ReadableStream 读。三个组件写完后,在视频播放页引入 Danmaku,在消息中心引入 MessagePanel,在侧边栏引入 AIAssistant。启动前端 npm run dev ,打开两个浏览器窗口,一个发弹幕一个看,能同步飘出来就说明 WebSocket 通了。

6. 联调验证与常见报错排查:401、proxy failed、choices 为空

联调阶段最容易卡在几个固定报错上,我按实际遇到的顺序列出来,你对照排查。

第一个是 401 Unauthorized。如果你在调 AI 助手时看到这个,先检查 Go 后端 .env 里的 TAOTOKEN_API_KEY 有没有填对,注意不要有多余空格。然后确认请求头是 Authorization: Bearer sk-xxx ,Bearer 后面有一个空格。如果 Key 是对的还报 401,可能是 Key 被禁用或额度用完,去 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 看一下状态。

第二个是 local proxy failed 或 connection refused。这个通常出现在前端连 WebSocket 的时候,VITE_WS_URL 写成了 ws://localhost:8080/ws ,但后端注册的路由是 /ws/danmaku ,路径不匹配。检查 main.go 里的 r.GET 路径和前端拼接的路径是否一致。另外如果你用了 Vite 的 proxy 配置,确认 proxy 里 ws: true 开了。

第三个是 reading choices 报错或 choices 为空。这说明请求发出去了,但返回体里没有 choices 字段。常见原因是模型 ID 写错了,比如把 deepseek-chat 写成了 deepseek ,或者 Base URL 少了 /v1 。你可以先用 curl 直接测 TaoToken 接口,确认返回结构:

curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"hi"}]}'

如果 curl 通了但 Go 里不通,检查 Go 的 http.NewRequest 里 URL 拼接有没有多斜杠或漏斜杠。我遇到过一次是 Base URL 结尾带了 / ,代码里又拼了 /v1 ,结果变成 //v1 ,服务器返回 404。

第四个是 OAuth 相关报错。如果你在 Cursor 里配置了 Claude Code 或 Codex 的认证,可能会看到 OAuth token expired。这个跟 cakecake 项目本身无关,是 Cursor 插件的认证问题。你可以在 Cursor 设置里重新登录,或者检查 ~/.cursor/ 下的配置文件。如果你用的是 CC Switch 或 Cline MCP,记得把 Base URL、Key、Model ID 三件套都填全,缺一个都会报认证失败。

第五个是弹幕不显示但 WebSocket 显示已连接。这种情况多半是消息格式对不上。后端广播的是 {"type":"danmaku","payload":{...}} ,前端解析时如果直接取 msg.content 就会 undefined。检查前端 onMessage 里有没有先取 msg.payload 。另外弹幕的 CSS 动画如果被父容器 overflow: hidden 裁掉了,也会看不到,确认 danmaku-layer 的定位是 absolute 且父容器 relative。

排查顺序建议:先 curl 测后端接口,再用浏览器 Network 面板看 WebSocket 帧,最后看前端控制台报错。三层都确认一遍,基本能定位到问题。如果 AI 助手一直超时,检查服务器能不能访问 https://taotoken.net/api ,有些内网环境需要配代理,但注意不要用违规的网络工具,直接用服务器本身的出网能力即可。

7. 从跑通到交付:把 cakecake 链路收尾的实用建议

两周速成交付的关键不是把每个功能做到完美,而是先把三条链路跑通,再补细节。我的建议是:第一周搞定环境、数据库、弹幕 WebSocket 和视频播放页,第二周搞私信落库和 AI 助手接入,最后两天做联调和部署。Cursor 在这个过程中主要帮你写重复的 CRUD 和类型定义,业务逻辑还是得自己理清楚。

部署的时候,后端用 go build 打包成二进制,前端 npm run build 出静态文件,用 Nginx 托管。WebSocket 需要在 Nginx 里加 Upgrade 和 Connection 头转发。AI 助手的 Key 放在服务器环境变量里,不要提交到 Git。如果你要长期做编码辅助,可以看看 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它适合需要频繁调用模型的场景。

最后说一个实际经验:弹幕的敏感词过滤不要放在前端做,前端过滤容易被绕过,放在 Go 后端广播之前用 Trie 树过滤一遍,成本很低但效果好。私信的已读未读状态用 Redis 存,比每次查 MySQL 快很多。AI 助手的回答如果要做结构化展示,让模型返回 JSON 格式,后端解析后再给前端,这样前端渲染工具结果会方便很多。这些细节做完,cakecake 的完整链路就真正交付了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 17:27:30

AI编程-MCP介绍:用TaoToken统一Key打通MCP工具链的配置实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 17:25:41

WorkBuddy 六大行业落地案例:从电商到教育的自动化实践指南

1. 从六个真实场景看 WorkBuddy 的落地逻辑第一次听到 WorkBuddy 这个名字&#xff0c;很多人会下意识把它归类成“又一个 AI 聊天工具”。但真正把它接进日常工作流之后你会发现&#xff0c;它更像是一个能调用外部工具、能读写业务数据、能按流程自动执行任务的协作中枢。这个…

作者头像 李华
网站建设 2026/10/4 17:20:12

大模型网关选型与落地实践:从模型路由到自动化编程

1. 先搞清楚&#xff1a;到底什么叫“企业大模型网关”大模型网关这个词&#xff0c;很多人一听就觉得抽象。我换个说法你就明白了&#xff1a;它就是企业内部的“模型路由器”。每个业务团队都可能要用大模型&#xff0c;有的是GPT&#xff0c;有的是国产的开源模型&#xff0…

作者头像 李华