乐于分享
好东西不私藏

Hexo 博客集成 AI 问答助手:DeepSeek + Vercel + 悬浮球聊天面板

Hexo 博客集成 AI 问答助手:DeepSeek + Vercel + 悬浮球聊天面板

前言

ByteFisher 博客已发布 95+ 篇文章,一个痛点越来越明显:静态博客缺少即时互动能力

读者经常遇到的问题场景:

  • "有没有 Unity 相关的入门教程?"

  • "C# 委托的用法在哪篇文章讲过?"

  • "你们用的这个图床是什么?"

在之前,这些问题的唯一回答渠道是文章底部的评论区——读者留言,博主看到后回复,周期可能是几小时甚至几天。如果读者没有留下联系方式,即使回复了对方也看不到。

为了解决这个问题,我决定在博客右下角加一个 AI 问答助手悬浮球:读者点击即可提问,基于 DeepSeek 大模型实时获得回复。

本文记录完整的实现过程:后端代理、前端交互、Hexo 集成、Vercel 部署。


一、整体架构

AI 问答助手的架构分为三层:

┌─────────────────────────────────────────────────────────┐
│ 浏览器 (Frontend)                                       │
│  ┌───────────────────┐   ┌───────────────────────────┐  │
│  │  source/js/        │   │  body-end.swig            │  │
│  │  ai-assistant.js   │ ← │  <script data-pjax>      │  │
│  │                    │   │                           │  │
│  │  createBtn()       │   │  styles.styl              │  │
│  │  createPanel()     │   │  main.css 编译注入         │  │
│  │  sendMessage()     │   │                           │  │
│  └────────┬──────────┘   └───────────────────────────┘  │
└───────────┼─────────────────────────────────────────────┘
            │ POST https://bytefisher-ai.vercel.app/api/chat
            │ JSON { messages: [...] }
            ▼
┌─────────────────────────────────────────────────────────┐
│ Vercel Serverless (Proxy Layer)                         │
│  ┌──────────────────────────────────────────────────┐   │
│  │  api/chat.js                                     │   │
│  │  ├── setCorsHeaders()   ← 动态 origin 检测      │   │
│  │  ├── 验证请求格式       ← messages 存在性检查    │   │
│  │  ├── 读取环境变量       ← DEEPSEEK_API_KEY      │   │
│  │  ├── 转发到 DeepSeek    ← POST /v1/chat/...     │   │
│  │  └── 返回响应           ← JSON + CORS Header    │   │
│  └──────────────────────────────────────────────────┘   │
└───────────────────────┬─────────────────────────────────┘
                        │ POST https://api.deepseek.com/v1/chat/completions
                        │ Authorization: Bearer sk-xxx
                        ▼
┌─────────────────────────────────────────────────────────┐
│ DeepSeek API                                            │
│  ┌──────────────────────────────────────────────────┐   │
│  │  model: deepseek-chat                            │   │
│  │  messages: [system, user]                        │   │
│  │  temperature: 0.7                                │   │
│  │  max_tokens: 2000                                │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

技术选型对比

方案优点缺点选择理由
DeepSeek API中文友好,¥0.14/百万tokens成本极低,博客用得起
OpenAI API生态完善¥2.5/百万tokens,贵 18 倍
本地 Ollama免费需要服务器资源
Vercel免费额度,自动 HTTPS冷启动 1-3s已有 Vercel 账号(Waline)
Cloudflare Workers免费额度更高需额外注册

二、后端:Vercel 代理 API

2.1 为什么需要代理层

直接在前端调用 DeepSeek API 有两个问题:

  1. API Key 泄露 — 前端代码所有人可见,Key 会被盗用

  2. 无法干预 — 请求频率、日志、错误处理都不可控

解决方案:在 Vercel 上部署一个 Serverless Function 作为代理。API Key 存储在 Vercel 环境变量中,前端只与代理交互。代理层还可以处理 CORS、格式校验、错误标准化。

2.2 api/chat.js 完整代码

// api/chat.js — Vercel Serverless Function

// CORS 预处理:动态检测来源域名
function setCorsHeaders(req, res) {
  var origin = req.headers.origin || '';
  if (
    origin === 'https://www.bytefisher.top' ||
    origin.indexOf('localhost') !== -1 ||
    origin.indexOf('127.0.0.1') !== -1
  ) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  } else {
    res.setHeader('Access-Control-Allow-Origin', 'https://www.bytefisher.top');
  }
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}

module.exports = async (req, res) => {
  setCorsHeaders(req, res);

  // OPTIONS 预检请求
  if (req.method === 'OPTIONS') {
    return res.status(204).end();
  }

  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  var { messages } = req.body;
  if (!messages || !messages.length) {
    return res.status(400).json({ error: 'Messages required' });
  }

  var apiKey = process.env.DEEPSEEK_API_KEY;
  if (!apiKey) {
    return res.status(500).json({ error: 'API key not configured' });
  }

  try {
    var response = await fetch('https://api.deepseek.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + apiKey
      },
      body: JSON.stringify({
        model: 'deepseek-chat',
        messages: messages,
        temperature: 0.7,
        max_tokens: 2000
      })
    });

    var data = await response.json();

    if (response.ok) {
      res.status(200).json(data);
    } else {
      res.status(response.status).json({ error: data.error || 'API error' });
    }
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

2.3 关键细节说明

CORS 动态 origin:本地开发时前端运行在 http://localhost:4000,如果 CORS 头写死为 https://www.bytefisher.top,浏览器会拦截请求。通过检测 req.headers.origin 可以实现本地开发和生产环境同时可用。

⚠️ 踩坑:module.exports 不是 export default:Vercel 默认按 CommonJS 解析 .js 文件。如果写成 export default async function handler(...),部署后会报语法错误,API 返回 500。

OPTIONS 预检:浏览器在跨域 POST 请求前会先发一个 OPTIONS 预检。如果不处理 OPTIONS,浏览器直接报 CORS 错误,真正的 POST 不会发出。

2.4 vercel.json

在项目根目录创建 vercel.json,控制 Serverless Function 的超时时间:

{
  "functions": {
    "api/*.js": {
      "maxDuration": 10
    }
  }
}

Vercel 免费 Hobby 计划的 Serverless Function 最长执行 10 秒,超过会超时断开。


三、前端:悬浮球按钮

3.1 设计目标

  • 右下角固定定位,不干扰主内容

  • 默认显示 🎣 emoji

  • 悬停展开显示文字 "AI 助手"

  • 品牌色渐变背景

  • 点击后面板弹出,按钮隐藏

3.2 创建按钮

function createBtn() {
  var btn = document.createElement('div');
  btn.id = 'ai-assistant-btn';
  btn.title = 'AI 问答助手';
  btn.innerHTML = '<span class="ai-btn-icon">🎣</span><span class="ai-btn-text">AI 助手</span>';
  btn.addEventListener('click', toggle);
  document.body.appendChild(btn);
}

3.3 悬停展开动画

#ai-assistant-btn
  display: flex
  align-items: center
  gap: 4px
  padding: 0 6px 0 14px
  height: 44px
  border-radius: 22px
  background: linear-gradient(135deg, #37c6c0, #32b2ad)
  color: #fff
  cursor: pointer
  z-index: 9998
  box-shadow: 0 4px 16px rgba(55,198,192,0.35)
  transition: padding 0.3s, opacity 0.3s, box-shadow 0.3s

  .ai-btn-icon
    font-size: 22px
    line-height: 1

  .ai-btn-text
    font-size: 14px
    white-space: nowrap
    overflow: hidden
    max-width: 0
    opacity: 0
    transition: max-width 0.3s, opacity 0.3s

  &:hover
    padding: 0 14px 0 14px
    box-shadow: 0 6px 24px rgba(55,198,192,0.45)

    .ai-btn-text
      max-width: 80px
      opacity: 1

  &.hidden
    opacity: 0
    pointer-events: none

核心技巧:用 max-width + opacity 的 transition 实现文字展开。默认 max-width: 0 隐藏文字,悬停时设为 80px 并淡入。同时配合 padding 动画,药丸形状从紧凑变为舒展。


四、前端:聊天面板

4.1 面板布局

面板固定定位在右下角,与按钮同位置:

function createPanel() {
  var panel = document.createElement('div');
  panel.id = 'ai-assistant-panel';
  panel.innerHTML =
    '<div class="ai-header">' +
      '<span>🎣 ' + CONFIG.botName + '</span>' +
      '<button class="ai-close">&times;</button>' +
    '</div>' +
    '<div class="ai-messages" id="ai-msgs"></div>' +
    '<div class="ai-input-area">' +
      '<textarea id="ai-input" rows="1" placeholder="' + CONFIG.placeholder + '"></textarea>' +
      '<button id="ai-send">发送</button>' +
    '</div>';
  document.body.appendChild(panel);
}

4.2 消息气泡样式

采用对话式 UI 的经典风格:用户消息右对齐,机器人消息左对齐。

.ai-message
  max-width: 85%
  padding: 10px 14px
  border-radius: 12px
  font-size: 14px
  line-height: 1.6

  &.ai-message-user
    background: #37c6c0
    color: #fff
    margin-left: auto
    border-bottom-right-radius: 4px

  &.ai-message-bot
    background: #f0f4f8
    color: #333
    margin-right: auto
    border-bottom-left-radius: 4px

    code
      background: rgba(0,0,0,0.06)
      padding: 2px 6px
      border-radius: 4px

    pre
      background: #1e1e1e
      color: #d4d4d4
      padding: 12px
      border-radius: 8px
      overflow-x: auto

4.3 Markdown 渲染引擎

不引入任何第三方库,纯前端正则实现:

function render(text) {
  // 第一步:XSS 防护 — 转义 HTML 特殊字符
  text = text.replace(/&/g, '&amp;')
             .replace(/</g, '&lt;')
             .replace(/>/g, '&gt;');
  // 第二步:代码块
  text = text.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
  // 第三步:行内代码
  text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
  // 第四步:加粗
  text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  // 第五步:换行
  text = text.replace(/\n/g, '<br>');
  return text;
}

转义顺序很重要:先转义 HTML,再渲染 Markdown。否则用户输入 <script> 会绕过转义。

4.4 打字机加载动画

三点弹跳,错峰延迟:

@keyframes ai-bounce
  0%, 60%, 100%
    transform: translateY(0)
  30%
    transform: translateY(-6px)

.ai-message-typing
  background: #f0f4f8
  display: flex
  gap: 4px
  align-items: center

  span
    width: 6px
    height: 6px
    border-radius: 50%
    background: #ccc
    animation: ai-bounce 1.4s infinite

    &:nth-child(2)
      animation-delay: 0.2s

    &:nth-child(3)
      animation-delay: 0.4s

4.5 键盘快捷键

// Enter 发送,Shift+Enter 换行
document.getElementById('ai-input').addEventListener('keydown', function(e) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    send();
  }
});

// Escape 关闭
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape' && isOpen) toggle();
});

五、System Prompt 工程

5.1 提示词设计原则

System Prompt 是 AI 助手的"人格设定",直接影响回答质量。设计时遵循三个原则:

原则说明实现
身份明确让模型知道自己是谁"你是一个博客助手"
上下文充分提供足够背景信息作者、内容领域、文章数
约束清晰限制回答风格和范围"简洁中文、不确定不编造"

5.2 完整 Prompt

var system = [
  '你是一个博客助手,帮助访客了解 ByteFisher 博客。',
  '',
  '## 博客基本信息',
  '作者:淡水鱼(Unity 游戏开发者 + 钓鱼爱好者)',
  '内容领域:Unity3D、C#、Lua、Python、钓鱼技巧、游戏开发教程',
  '文章总数:95+ 篇',
  '博客地址:https://www.bytefisher.top',
  '',
  '## 回答规则',
  '- 使用简洁的中文回复,可以适当使用 emoji',
  '- 不知道的内容不要编造',
  '- 如果用户查找文章,引导他们使用搜索功能',
  '- 回答控制在 200 字以内'
].join('\n');

5.3 边界情况处理

场景处理方式
无关问题(如"今天天气怎么样")友好表示能力有限,引导回博客主题
敏感话题礼貌拒绝
找不到相关信息"抱歉没找到相关内容,换个问法试试?"
连续追问当前设计为单轮问答(不保留历史),每次独立

六、Hexo / NexT 集成

6.1 样式注入

NexT 主题支持通过 source/_data/styles.styl 注入自定义样式。所有 AI 助手相关的 CSS 都追加在此文件末尾,会自动编译到 main.css 中。

6.2 脚本加载

source/_data/body-end.swig 末尾追加一行:

<!-- AI 博客助手 -->
<script src="/js/ai-assistant.js" data-pjax></script>

data-pjax 属性是关键:NexT 使用 PJAX 实现无刷新页面切换,加了此属性的脚本会在每次 PJAX 渲染时重新执行。

6.3 ⚠️ 踩坑:PJAX 重复创建

首次加载页面正常。但在 PJAX 导航到其他页面后,脚本重新执行,每次都创建一个新的悬浮球和面板,欢迎语跟着叠加。

  function init() {
+   if (document.getElementById('ai-assistant-btn')) return;
    createBtn();
    createPanel();
    addMsg('bot', CONFIG.welcomeMessage);
    autoResizeInput();
  }

加一行防重复检查即可:如果按钮已存在,跳过创建。

6.4 文件清单

文件操作行数说明
api/chat.js新建62Vercel Serverless Function
vercel.json新建6Vercel 配置
source/js/ai-assistant.js新建167前端全部逻辑
source/_data/styles.styl追加~120AI 助手样式
source/_data/body-end.swig追加1加载脚本标签

七、Vercel 部署指南

7.1 创建 Vercel 项目

1. 登录 https://vercel.com(与 Waline 同一个账号)
2. Dashboard → Add New → Project
3. 选择 BlogCode 仓库 → Import
4. Configure Project:
   ├── Framework Preset: Other
   ├── Root Directory: ./
   ├── Build Command: (留空)
   └── Output Directory: (留空)
5. 点击 Environment Variables
   ├── Name: DEEPSEEK_API_KEY
   └── Value: sk-xxxxxxxxxxxxxxxxx
6. 点击 Deploy,等待 1-2 分钟

7.2 配置前端 API 地址

部署完成后,Vercel 会分配一个域名,如 https://bytefisher-ai.vercel.app

source/js/ai-assistant.js 中的 API 地址更新为实际地址:

var CONFIG = {
  apiEndpoint: 'https://bytefisher-ai.vercel.app/api/chat',
  // ↑ 替换为你的 Vercel 项目 URL
};

7.3 验证 API

用 curl 测试 API 是否正常工作:

curl -X POST https://bytefisher-ai.vercel.app/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"你好"}]}'

期望返回格式:

{
  "choices": [
    {
      "message": {
        "content": "你好!欢迎来到 ByteFisher 博客...",
        "role": "assistant"
      }
    }
  ]
}

浏览器直接 GET 访问 API 应返回 {"error":"Method not allowed"},这是正常的(只接受 POST)。

7.4 触发重新部署

Vercel 默认自动连接 GitHub,推送代码后自动重新部署。如果初始项目创建时 api/ 目录还不存在,可能不会自动检测变化,需要手动 Redeploy:

Vercel Dashboard → 项目 → Deployments → 最新 commit → Redeploy

八、完整交互流程

用户操作链路:

打开博客首页
  → 右下角出现 🎣 悬浮球
  → 鼠标悬停 → 展开显示 "AI 助手"
  → 点击悬浮球 → 面板弹出,按钮隐藏
  → 显示欢迎语:
      "🎣 欢迎来到 ByteFisher 博客!

       我是 ByteBot,可以帮你:
       📖 推荐文章
       💡 解答技术问题
       🎯 了解博客内容

       有什么想了解的?"
  → 输入 "你们博客有哪些 Unity 文章?"
  → 按 Enter → 三点跳动加载
  → AI 回复(支持代码块、加粗等 Markdown 渲染)
  → 按 Escape 或点击 × 关闭面板
  → 按钮恢复显示

数据流转时序:

Frontend                   Vercel Proxy              DeepSeek API
   │                          │                         │
   ├── POST /api/chat ────────┤                         │
   │   { messages: [...] }    │                         │
   │                          ├── POST /v1/chat/completions ──┤
   │                          │   Authorization: Bearer    │
   │                          │   Body: { messages, ... }  │
   │                          │                         │
   │                          │   ←── 200 JSON ──────────┤
   │  ←── 200 JSON ──────────┤   { choices: [...] }     │
   │  { choices: [...] }     │                         │
   │                          │                         │
   │  渲染消息到面板           │                         │

错误处理路径:

故障场景表现用户看到
网络断开fetch 超时"网络开小差了,请稍后重试 🐟"
API Key 无效DeepSeek 返回 401"抱歉没理解,换个问法试试?"
请求超时Vercel 10s 超时"网络开小差了,请稍后重试 🐟"
参数错误前端校验不发送请求,提示用户输入内容

九、费用与性能评估

费用估算

项目计算公式月费
DeepSeek 输入¥0.14/百万tokens × 4.5万tokens/月≈ ¥0.0063
DeepSeek 输出¥0.28/百万tokens × 0.5万tokens/月≈ ¥0.0014
Vercel 托管Hobby 计划免费额度¥0
总计日均 30 次问答≈ ¥0.01/月

按日均 30 次问答,每次约 1500 tokens(含 system prompt)计算。DeepSeek 的价格极低,几乎可以忽略不计。

性能指标

阶段耗时说明
Vercel 冷启动0.5-2s闲置 15 分钟后首次请求
DeepSeek 推理0.8-2s模型响应时间
网络传输0.2-0.5s客户端 → Vercel → DeepSeek
总耗时1.5-4.5s冷启动时更慢,后续请求更快

对于个人博客的流量,冷启动不可避免。Vercel Hobby 计划在 15 分钟无请求后会回收实例。但 1-3s 的等待对问答场景来说可以接受。


十、总结

改造前后对比

维度之前之后
互动方式评论区留言,等待回复AI 即时问答
覆盖范围仅文章底部全站右下角悬浮球
回答问题博主自己回复DeepSeek 大模型
技术栈DeepSeek + Vercel + 原生 JS
月运营成本≈ ¥0.01

技术收获

整个实现过程中的几个关键经验:

  1. Vercel Serverless 函数要用 CommonJSmodule.exports 而非 export default

  2. CORS 要动态检测 origin:本地开发(localhost)和生产环境(bytefisher.top)需要不同允许来源

  3. PJAX 兼容要防重复创建data-pjax 脚本每次导航都执行,需要在 init() 中加守卫检查

  4. 纯前端 Markdown 渲染:不引入第三方库也能满足基本需求,HTML 转义顺序至关重要

可以做的下一步扩展

  • 对话历史:用 localStorage 持久化对话记录,刷新不丢失

  • RAG 检索增强:结合博客的 search.json 做文章检索,AI 可以基于博客内容回答,不依赖模型训练数据

  • 多人格切换:钓鱼助手、编程助手、闲聊助手三种模式

  • 语音输入:集成 Web Speech API,支持语音提问


本文所有代码托管在 [GitHub](https://github.com/grj1981/BlogCode),欢迎 Star 和交流。

原文链接:https://www.bytefisher.top/2026/05/09/Hexo 博客集成 AI 问答助手:DeepSeek API + Vercel Serverless + 悬浮球聊天面板/
分类:Hexo博客搭建
标签:series:Hexo博客搭建、Hexo、Next主题、AI、DeepSeek、博客优化
ByteFisher
分享编程技术 · 记录钓鱼乐趣
▸ 扫码关注 ◂