【实战教程】从零开发Chrome扩展:自动采集小红书评论并接入DeepSeek AI
本文将手把手教你开发一个Chrome扩展自动读取小红书、抖音、B站评论接入DeepSeek大模型生成回复并一键填入页面输入框。包含完整代码和踩坑记录。前言作为一名开发者身边的朋友经常抱怨做小红书带货视频爆了以后评论区999翻到手酸也找不全问怎么买的人。这些高意向评论散落在海量互动中人工筛选效率极低。于是我决定开发一个工具来自动解决这个问题。本文将完整记录开发过程从环境搭建到上线希望能帮助有同样需求的同学。一、技术选型与架构设计1.1 为什么选择Chrome扩展有三种技术方案可选方案优点缺点官方API稳定、规范小红书等平台未开放评论读取接口爬虫灵活反爬严格需要维护登录态合规风险Chrome扩展读取用户可见内容合规直接操作页面DOM平台改版时需要更新选择器最终选择Chrome扩展 Go后端的架构┌─────────────────────────────────────────┐ │ Chrome Extension │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Content Script│ │ Sidepanel │ │ │ │ (小红书/抖音/ │◄───│ (React UI) │ │ │ │ B站DOM采集) │ └─────────────┘ │ │ └──────┬──────┘ │ │ │ 发送评论数据 │ └─────────┼───────────────────────────────┘ │ ▼ HTTPS ┌─────────────────────────────────────────┐ │ Go Backend (Gin) │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ JWT │ │DeepSeek │ │ 积分 │ │ │ │ 鉴权 │ │ API调用 │ │ 系统 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────────┘1.2 技术栈前端扩展: Plasmo React TypeScript后端: Go Gin GORM数据库: PostgreSQLAI: DeepSeek-V3 API二、环境搭建详细步骤2.1 安装依赖# 1. 克隆项目gitclone https://github.com/mustcanbedo/comment_copilot.gitcdcomment_copilot# 2. 安装Node依赖npminstall# 3. 安装Go依赖进入backend目录cdbackend go mod tidy2.2 配置数据库安装PostgreSQL创建数据库CREATEDATABASEcomment_copilot;2.3 配置后端复制配置文件模板cpbackend/config.yaml.example backend/config.yaml编辑backend/config.yaml# 服务端口号port:3000# PostgreSQL连接字符串database_url:postgresql://用户名:密码localhost:5432/comment_copilot?sslmodedisable# DeepSeek API密钥必填deepseek_api_key:sk-你的DeepSeek密钥# JWT签名密钥随便填一个随机字符串auth_secret:your-random-secret-key# 自动执行数据库迁移auto_migrate:true获取DeepSeek密钥访问 https://platform.deepseek.com/ 注册并创建API Key。2.4 配置扩展开发环境扩展的环境变量配置在apps/extension/.env.development# 本地开发默认连接本地后端PLASMO_PUBLIC_API_URLhttp://localhost:3000/api这个文件已配置好无需修改。三、核心功能实现详解3.1 内容脚本Content Script内容脚本是扩展注入到网页中的代码负责读取评论数据。3.1.1 小红书内容脚本创建apps/extension/contents/xiaohongshu.tsimporttype{PlasmoCSConfig}fromplasmo// 配置只在匹配的小红书页面注入exportconstconfig:PlasmoCSConfig{matches:[https://www.xiaohongshu.com/*],run_at:document_idle,// 页面加载完成后执行all_frames:false,}// 选择器配置后续可从服务端热更新letSELECTORS{commentList:.comment-item,authorName:a.name,content:.comment-inner-container span:not([class]),timestamp:span:not([class]) span:not([class]),}// 已采集评论ID集合去重用constseenIdsnewSetstring()constSEEN_IDS_MAX3000// 防止内存无限增长关键问题1Vue3数据的解包小红书使用Vue3window.__INITIAL_STATE__中的数据被包装成ref直接读取会得到undefined。解决方案递归解包Vue ref/** * 解包Vue3的ref对象 * Vue3使用__v_isRef标记ref对象实际数据在_value或value中 */functionunwrapVueRef(value:unknown):unknown{if(valuenull||valueundefined)returnvalueif(typeofvalue!object)returnvalueconstrvalueasRecordstring,unknown// 判断是否为Vue refif(r.__v_isReftrue){// Vue3 ref的数据可能在_rawValue或value中constinnerr._rawValue!undefined?r._rawValue:r.value// 递归解包可能嵌套多层refreturnunwrapVueRef(inner)}returnvalue}// 使用示例获取当前登录用户昵称constuserunwrapVueRef(window.__INITIAL_STATE__?.user)constnicknameuser?.nickname// 现在能正确读取了关键问题2过滤自己的评论避免在侧栏显示自己回复自己的尴尬情况/** * 判断是否为当前登录用户的评论 * 考虑昵称可能被截断的情况如张三显示为张... */functionisSelfComment(authorName:string,selfNick:string):boolean{// 标准化去空格、转小写constnormalize(s:string)s.replace(/\s/g, ).trim().toLowerCase()constanormalize(authorName)constbnormalize(selfNick)// 完全匹配if(ab)returntrue// 前缀匹配处理截断情况如张...匹配张三if(a.length5b.startsWith(a))returntrueif(b.length5a.startsWith(b))returntruereturnfalse}3.1.2 抖音内容脚本抖音的坑更多Shadow DOM 零宽字符。坑1Shadow DOM穿透抖音评论区使用了Shadow DOM需要递归穿透constDEEP_QUERY_MAX_ROOTS48_000// 节点预算防止卡死/** * 穿透Shadow DOM查询元素 * 从document开始递归进入所有shadowRoot查找匹配元素 */functionquerySelectorAllDeep(selector:string):Element[]{constresults:Element[][]constqueue:(Document|ShadowRoot)[][document]constseennewSetDocument|ShadowRoot()letcount0while(queue.lengthcountDEEP_QUERY_MAX_ROOTS){constrootqueue.shift()!if(seen.has(root))continueseen.add(root)count// 在当前root中查找root.querySelectorAll(selector).forEach(elresults.push(el))// 查找所有带shadowRoot的元素加入队列root.querySelectorAll(*).forEach(el{if(el.shadowRoot)queue.push(el.shadowRoot)})}returnresults}坑2零宽字符抖音昵称里可能插入零宽字符肉眼看不见导致正则匹配失败/** * 去除零宽字符并标准化空格 * 零宽字符\u200b-\u200d, \uFEFF */functioncleanText(s:string):string{returns.replace(/[\u200b-\u200d\uFEFF]/g,)// 删除零宽字符.replace(/\s/g, )// 多个空格合并.trim()}// 使用比较昵称前先cleancleanText(张三)cleanText(张\u200b三)// true3.1.3 节流扫描优化评论列表随滚动动态加载不能每次DOM变动都全量扫描会卡死页面/** * 创建节流函数 * param fn 要执行的函数 * param intervalMs 最小执行间隔毫秒 */functioncreateThrottledScan(fn:()void,intervalMs:number){letlastTime0returnfunction(){constnowDate.now()// 只有超过间隔时间才执行if(now-lastTimeintervalMs){lastTimenowfn()}}}// 使用每500ms最多执行一次扫描constthrottledScancreateThrottledScan(scanComments,500)// 监听DOM变化但使用节流版本document.addEventListener(scroll,throttledScan)3.2 后端AI接入3.2.1 DeepSeek API调用packagehandlerimport(bytesencoding/jsonnet/httptimegithub.com/gin-gonic/gin)// 设置超时时间DeepSeek可能较慢vardeepSeekClienthttp.Client{Timeout:90*time.Second,}constdeepSeekURLhttps://api.deepseek.com/v1/chat/completionsfunc(h*AIHandler)Reply(c*gin.Context){// 1. 获取用户信息userID:c.GetString(userId)ifuserID{c.JSON(401,gin.H{ok:false,error:未登录})return}// 2. 扣费前置防止刷接口pointsPerCall:1iferr:h.userRepo.DeductPoints(userID,pointsPerCall);err!nil{c.JSON(403,gin.H{ok:false,error:积分不足})return}// 3. 构建Promptprompt:buildPrompt(req.CommentContent,req.Persona)// 4. 调用DeepSeekbody,_:json.Marshal(map[string]interface{}{model:deepseek-chat,messages:[]map[string]string{{role:system,content:你是一个专业的小红书运营助手...},{role:user,content:prompt},},temperature:0.7,})req2,_:http.NewRequest(POST,deepSeekURL,bytes.NewReader(body))req2.Header.Set(Authorization,Bearer h.deepSeekAPIKey)req2.Header.Set(Content-Type,application/json)resp,err:deepSeekClient.Do(req2)// ...处理响应}关键点超时设置90秒DeepSeek有时响应慢先扣费再调用防止恶意刷接口非流式返回前端处理简单一次性拿到完整回复3.3 填入输入框的技术细节这是整个项目最难的部分。不同平台使用不同的编辑器平台编辑器类型填入方法小红书普通textareaelement.value text抖音Draft.js (contenteditable)模拟输入事件B站自定义组件模拟键盘事件3.3.1 Draft.js编辑器填入抖音使用Draft.js直接改innerHTML无效必须模拟真实用户输入/** * 向Draft.js编辑器填入文本 * Draft.js依赖React的受控组件状态必须模拟真实输入事件 */functionfillDraftEditor(element:HTMLElement,text:string):boolean{try{// 1. 聚焦编辑器element.focus()// 2. 创建选区全选现有内容constselectionwindow.getSelection()constrangedocument.createRange()range.selectNodeContents(element)selection?.removeAllRanges()selection?.addRange(range)// 3. 模拟输入事件这是关键constresultdocument.execCommand(insertText,false,text)// 4. 触发input事件让React更新状态element.dispatchEvent(newInputEvent(input,{bubbles:true,cancelable:true,}))returnresult}catch(e){console.error(填入失败:,e)returnfalse}}坑点说明必须用execCommand(insertText)直接改textContentDraft.js感知不到必须触发input事件否则React状态不同步点发送时内容消失3.3.2 通用填入函数封装一个兼容各平台的填入函数/** * 向任意输入框填入文本 * 自动判断编辑器类型选择最佳填入策略 */exportfunctionfillInput(element:HTMLElement,text:string):boolean{// 1. 普通input/textareaif(elementinstanceofHTMLInputElement||elementinstanceofHTMLTextAreaElement){element.valuetext element.dispatchEvent(newEvent(input,{bubbles:true}))returntrue}// 2. contenteditableDraft.js等富文本编辑器if(element.isContentEditable){returnfillDraftEditor(element,text)}// 3. 兜底直接修改textContent成功率低仅作备用element.textContenttextreturntrue}四、完整启动流程4.1 启动后端服务# 终端1启动后端cdbackend go run ./cmd/server# 看到以下输出表示成功# Go backend running at http://localhost:3000/api4.2 启动扩展开发模式# 终端2启动扩展cdapps/extensionnpmrun dev# 看到以下输出表示成功# Plasmo v0.86.0# http://localhost:18154.3 Chrome加载扩展打开 Chrome访问chrome://extensions/右上角打开「开发者模式」点击「加载已解压的扩展程序」选择目录apps/extension/.plasmo/chrome-mv3-dev看到扩展图标即成功4.4 测试流程打开小红书任意笔记页面打开Chrome侧边栏点击扩展图标或按快捷键侧边栏应显示当前页面评论列表点击任意评论的「生成回复」按钮应看到AI生成的回复建议点击「填入」回复应自动填入小红书回复框五、常见问题排查5.1 评论不显示可能原因选择器失效平台改版页面未完全加载排查步骤打开浏览器开发者工具F12检查Console是否有报错检查评论元素的选择器是否匹配5.2 填入失败可能原因平台改版编辑器结构变化填入时机不对页面未就绪解决方案检查输入框是否可见且可交互尝试手动聚焦输入框后再填入查看background.js日志5.3 AI回复为空可能原因DeepSeek API密钥错误积分不足网络超时排查检查backend/config.yaml中的deepseek_api_key查看后端日志错误信息六、生产部署6.1 构建生产包cdapps/extension# 1. 创建生产配置cp.env.production.example .env.production# 2. 编辑.env.production填入生产API地址# PLASMO_PUBLIC_API_URLhttps://your-api.com/api# 3. 构建商店包自动注入host_permissionsnpmrun package:store# 产物build/chrome-mv3-prod.zip6.2 后端部署推荐使用Docker部署FROM golang:1.21-alpine WORKDIR /app COPY . . RUN go build -o server ./cmd/server EXPOSE 3000 CMD [./server]七、开源地址项目已开源欢迎Star和ForkGitHub: https://github.com/mustcanbedo/comment_copilot包含完整代码、详细文档、部署指南。有问题欢迎提Issue。如果本文对你有帮助欢迎点赞收藏有任何问题评论区见