两周实战:用Plasmo+DeepSeek打造评论区AI助手,聊聊Vue3解包和Shadow DOM踩坑

3 阅读3分钟

从Plasmo到DeepSeek,从Vue3解包到Shadow DOM,记录一个多平台评论区AI助手的开发实录。

做小红书、抖音带货的朋友都知道,视频爆了之后评论区999+,翻到手酸也找不全问"怎么买"的人。这些评论不是字,是钱在跟你打招呼,你给错过了。

我决定用技术解决这个问题。两周时间,做了一个Chrome扩展+Go后端的组合拳,自动采集评论、AI识别购买意向、一键生成回复。

一、为什么要用 Plasmo?

一开始打算用原生 Manifest V3 写,但 Plasmo 的 Content Script 自动注入、热更新、类型支持太香了:

// 小红书内容脚本配置
export const config: PlasmoCSConfig = {
  matches: ["https://www.xiaohongshu.com/*"],
  run_at: "document_idle",
  all_frames: false,
}

一个文件对应一个平台,自动打包成独立的 content script,不用手动写 webpack。

二、小红书的坑:Vue3 的 ref 怎么解?

小红书页面用 Vue3,window.__INITIAL_STATE__ 里存了当前登录用户信息。但直接读 .nicknameundefined —— 因为 Vue3 把数据包成 ref 了。

必须递归解包:

function unwrapVueRef(value: unknown): unknown {
  if (value === null || value === undefined) return value
  if (typeof value !== "object") return value
  const r = value as Record<string, unknown>
  if (r.__v_isRef === true) {
    const inner = r._rawValue !== undefined ? r._rawValue : r.value
    return unwrapVueRef(inner)
  }
  return value
}

不解包就抓不到登录用户昵称,导致"自己回复自己"的尴尬场面。

三、抖音的坑:Shadow DOM + 零宽字符

抖音评论区用了 Shadow DOM,还得穿透多层 shadow root 找输入框。更恶心的是昵称里插零宽字符(\u200b),正则匹配直接失效:

function stripZeroWidthAndNormalizeSpaces(s: string): string {
  return s
    .replace(/[\u200b-\u200d\uFEFF]/g, "")  // 干掉零宽
    .replace(/\s+/g, " ")
    .trim()
}

输入框选择器也得兼容 Draft.js 的 contenteditable 和自定义组件,不是简单的 input

四、节流扫描:页面滚动时不卡死

评论列表随滚动动态加载,如果每次 DOM 变动都全量扫描,页面直接卡爆。实现了一个带预算的节流扫描:

const DEEP_QUERY_MAX_ROOTS = 48_000  // 节点预算上限

function createThrottledScan(fn: () => void, intervalMs: number) {
  let last = 0
  return () => {
    const now = Date.now()
    if (now - last >= intervalMs) {
      last = now
      fn()
    }
  }
}

配合 MutationObserver 监听评论列表变化,只有滚动停止后才触发扫描。

五、DeepSeek 对接:不是简单调 API

直接调 OpenAI 格式的 API 有坑:

  • 超时时间太短会挂死,得设 90 秒
  • 流式输出前端不好处理,改成非流式一次性返回
  • 人设 prompt 要精简,token 贵
var deepSeekHTTPClient = &http.Client{Timeout: 90 * time.Second}

// 扣费逻辑前置,防止刷接口
pointsPerCall := 1
user.DeductPoints(pointsPerCall)

六、填入输入框:平台兼容性地狱

小红书用普通 textarea,抖音用 Draft.js 的 contenteditable,B站又是另一种结构。最通用的方法是模拟用户输入事件:

// 填充 Draft.js 编辑器
function fillDraftJS(element: HTMLElement, text: string) {
  // 1. 聚焦
  element.focus()
  // 2. 创建选区
  const selection = window.getSelection()
  const range = document.createRange()
  range.selectNodeContents(element)
  selection?.removeAllRanges()
  selection?.addRange(range)
  // 3. 模拟输入事件
  document.execCommand("insertText", false, text)
}

不是简单的 element.value = text,否则 React/Draft.js 的状态不同步,点发送时内容消失。

七、架构图

Chrome Extension (Plasmo)
  ├── Content Script (小红书/抖音/B站)
  │   └── DOM 采集 → 节流 → 去重
  ├── Sidepanel (React)
  │   └── 展示 → AI生成 → 填入
  └── Background
      └── API 通信

Go Backend (Gin)
  ├── JWT 鉴权
  ├── 租户隔离 (x-tenant-id)
  ├── DeepSeek 调用
  └── 积分扣费

总结

做浏览器扩展最大的坑不是代码,是平台的 DOM 随时会变。抖音一个月改两次选择器结构,得靠服务端热更新 selector 配置,否则每次都要发新版扩展等 Chrome Web Store 审核(通常 2-7 天)。

代码开源了,欢迎参考和吐槽:

GitHub: github.com/mustcanbedo…


如果这篇对你有帮助,点个赞吧~有问题评论区见