一、 前言

  在上一篇文章 中,我们已经成功在 Hexo (Butterfly 主题) 框架下部署了 Cubism 3.0+ 标准的 Live2D (.moc3) 模型。但交互性却略显不足。在这个大模型时代,为什么不给博客看板娘装上一个真正的大脑呢?
  本文将作为上一篇教程的进阶,将介绍 Live2D AI 聊天功能的前端架构设计与后端部署教程。为了相关信息完整性,请务必先浏览上一篇文章内容。

本项目的 AI 聊天功能是基于 Deepseek + FastAPI 后端服务实现的,在开始后端部署前请确保你已经具备一个可用的云服务器以及一个可用的 deepseek API 密钥

二、 前端架构说明

  为了不破坏原有 Live2D 渲染框架,由 waifu-chat.js 负责对话逻辑,waifu-tips.js 仅作为 AI 对话功能的触发入口。下面展示了前端从触发唤醒到完成一次 AI 对话的交互流程:

前端架构

  接下来是简要的功能实现说明:

2.1 侧边栏工具

  在waifu-tips.js的工具栏配置对象 U 中,新增一个 chat 工具,并为其绑定回调函数。这里通过全局变量 window.live2dChatInstance 来进行跨文件通信:

// waifu-tips.js
var U = {
chat: {
icon: '<svg>...</svg>',
callback: () => {
if (window.live2dChatInstance) {
window.live2dChatInstance.toggle();
} else {
window.waifuShowMessage("聊天模块未加载", 3000, 10);
}
}
},
// 其他原生工具 (hitokoto, photo, etc.)
}

2.2 聊天引擎

  waifu-chat.js定义整个 AI 交互的逻辑,并通过 Live2DChat 类进行封装:

  1. 前端 RAG 机制:调用 getCurrentPageContext() 抓取 hexo 博客容器#article-container 里的纯文本,并通过读取 Hexo 的 /search.xml 建立 blogIndex 进行全局匹配。让 AI 能够精准回答 “这篇文章讲了什么” 或 “你的博客里有没有关于 XX 的内容”。
  2. 打字机与 Markdown 渲染:拦截后端的完整返回,并添加 “打字” 动画效果和 “等待” 动画效果。同时引入 marked.js 对 AI 返回的 Markdown 进行解析,正则处理可能导致的 DOM 解析错误问题。
  3. 聊天边界管理:聊天框弹出时,利用 getBoundingClientRect 与视窗大小计算,确保不管看板娘被拖拽到页面的哪个角落,聊天窗口永远不会超出屏幕边界被遮挡。

2.3 自定义配置

  将 System Prompt、欢迎语、快捷选项和 RAG 截断策略写入waifu-chat.json。这样以后修改 AI 人设或欢迎语时,无需动前端核心代码,直接修改配置文件即可。

三、后端配置

3.1 后端程序设置

  后端程序主要负责接收前端请求,调用 DeepSeek API 获取 AI 回答,并返回给前端。使用 FastAPI 框架来实现此功能。
  在云服务器上创建自定义的工作目录:

mkdir /home/git/live2dChat
cd /home/git/live2dChat
touch main.py
vi main.py

  向 main.py 中写入以下内容:

import os
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
import httpx

app = FastAPI()

ALLOWED_ORIGINS = [
"https://你的博客域名", # 请务必在这里替换为你的博客域名
"http://localhost:4000" # 本地测试地址
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["POST", "OPTIONS"],
allow_headers=["*"],
)

# 环境变量读取
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
CLIENT_UUID_KEY = os.getenv("CLIENT_UUID_KEY")

# 依赖注入:校验前端请求 Header 中的 Authorization Token
async def verify_client_token(request: Request):
auth_header = request.headers.get("Authorization")
if not auth_header or auth_header != f"Bearer {CLIENT_UUID_KEY}":
raise HTTPException(status_code=403, detail="Forbidden: Invalid Client Token")
return True
# 这里的接口为 /api/chat
@app.post("/api/chat", dependencies=[Depends(verify_client_token)])
async def chat_proxy(request: Request):
try:
data = await request.json()
messages = data.get("messages", [])

payload = {
"model": "deepseek-chat",
"messages": messages,
"stream": False
}

headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json"
}

# 异步调用模型 API
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.deepseek.com/chat/completions",
json=payload,
headers=headers,
timeout=30.0
)
response.raise_for_status()
return response.json()

except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"Upstream Error: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail="Internal Server Error")

  安装 python 依赖:

pip install fastapi httpx uvicorn

3.2 配置 Systemd 守护进程

  建立环境变量文件并写入 DeepSeek API Key:

echo 'DEEPSEEK_API_KEY="这里为 DeepSeek API密钥"' > /home/git/live2dChat/.env
chmod 600 /home/git/live2dChat/.env

  创建 /etc/systemd/system/live2dchat.service 文件,并写入以下内容:

TOML
[Unit]
Description=Live2D Chat FastAPI Proxy Service
After=network.target

[Service]
User=root
WorkingDirectory=/home/git/live2dChat
ExecStartPre=/bin/bash -c 'grep -q "CLIENT_UUID_KEY" /home/git/live2dChat/.env || echo "CLIENT_UUID_KEY=\$(uuidgen)" >> /home/git/live2dChat/.env'
EnvironmentFile=/home/git/live2dChat/.env
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8555
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
{% endcode %}

  程序的服务端口为 8555,当然你也可以根据需要修改为其他端口,但请确保后续 Nginx 反向代理配置中的端口一致。然后启动并验证服务是否正在运行:

systemctl daemon-reload
systemctl enable live2dchat.service
systemctl status live2dchat.service

  检查 /home/git/live2dChat/.env 文件中是否正确生成了 CLIENT_UUID_KEY

DEEPSEEK_API_KEY="DeepSeek API密钥"
CLIENT_UUID_KEY="生成的随机 UUID 值"

  如果没有请手动添加一个随机 UUID:

uuidgen  # 生成一个新的 UUID
echo 'CLIENT_UUID_KEY="生成的 UUID 值"' >> /home/git/live2dChat/.env
# 或者直接编辑 .env 文件并添加 UUID
vi /home/git/live2dChat/.env

  然后重启服务:

systemctl restart live2dchat.service

3.3 Nginx 反向代理

  在 /etc/nginx/nginx.conf 中配置 IP 请求限制:

http {
# 其他配置...
limit_req_zone $binary_remote_addr zone=live2d_api_limit:10m rate=10r/m; # 每分钟限制 10 次请求
}

  然后在 /etc/nginx/sites-available/default 或你的 Nginx 配置文件中添加以下 location 块,将 /api/chat 的请求反向代理到 FastAPI 服务:

server {
# 其他 server 配置...
location /api/chat {
limit_req zone=live2d_api_limit burst=10 nodelay;
proxy_pass http://127.0.0.1:8555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

  重载 Nginx 配置:

sudo nginx -t
sudo systemctl reload nginx

  到这里后端服务就部署完成了。

四、前端部署

  前端代码的开源地址为:LuoTian001/live2d-widget-AIChat。如果觉得教程对你有帮助,烦请 star 支持一下!(・̀ ω・́)✧

4.1 文件说明

  项目采用模块化设计,分离基础渲染层与 AI 逻辑层。主要目录结构与各文件功能说明如下:

live2d/
├── Core/
│ └── live2dcubismcore.js # Live2D Cubism 官方核心 Web 库 (底层骨骼运算,请勿修改)
├── model/ # moc3 模型资源存放目录
│ └── ... # 模型文件路径
├── model_list.json # 模型列表
├── live2d-sdk.js # Live2D 渲染与控制 SDK (基于 WebGL)
├── waifu.css # 看板娘本体基础 UI 样式表
├── waifu-tips.json # 基础交互语料库 (基于时间段、页面点击、元素悬浮的固定提示语)
├── waifu-tips.js # 看板娘主控制逻辑 (负责组件初始化、挂载侧边工具栏事件)
├── waifu-chat.css # 🆕 AI 聊天窗口样式表 (含毛玻璃与暗黑模式适配)
├── waifu-chat.json # 🆕 AI 对话外置配置 (预设 System Prompt、欢迎语、快捷指令等)
└── waifu-chat.js # 🆕 AI 聊天核心引擎 (负责 RAG 文本抓取、API 请求封装、Markdown 解析与打字机渲染)

4.2 前端部署

cd 你的博客目录/source/
git clone git@github.com:LuoTian001/live2d-widget-AIChat.git live2d

  在_config.yml博客配置文件下添加以下代码,排除 hexo 对 live2d 目录的渲染:

skip_render: 
- 'live2d/**'

  在 _config.butterfly.yml 文件中,找到 inject.bottom 节点,加入以下代码:

inject:
bottom:
- |
<script>
if (typeof window.live2d_initialized === 'undefined') {
window.live2d_initialized = true;
const cdnPath = "https://cdn.jsdelivr.net/gh/LuoTian001/live2d-widget-AIChat@main/"; // CDN 加速路径,指向本项目的 jsDelivr 镜像仓库
const localPath = "/live2d/"; // 本地资源路径,指向 Hexo 博客的 public 目录下的 live2d 文件夹
const config = {
path: {
homePath: "/", // 博客首页路径,默认 "/"
modelPath: localPath, // Live2D 模型资源路径,指向本地的 live2d/model/ 目录
cssPath: localPath + "waifu.css", // 看板娘基础样式表路径
tipsJsonPath: localPath + "waifu-tips.json", // 看板娘提示语料库路径
tipsJsPath: localPath + "waifu-tips.js", // 看板娘主控制逻辑脚本路径
chatJsPath: localPath + "waifu-chat.js", // AI 聊天核心引擎脚本路径
chatCssPath: localPath + "waifu-chat.css", // AI 聊天样式表路径
chatJsonPath: localPath + "waifu-chat.json", // AI 聊天配置文件路径
live2dCorePath: cdnPath + "Core/live2dcubismcore.js", // Live2D 核心库路径
live2dSdkPath: cdnPath + "live2d-sdk.js" // Live2D SDK 路径
},
tools: ["chat", "hitokoto", "express", "info", "quit"], // 侧边工具栏按钮配置
drag: { enable: false, direction: ["x", "y"] }, // 拖拽配置
switchType: "order" // 模型/材质切换方式
};
if (screen.width >= 768) {
window.addEventListener('load', () => {
const initTask = () => {
Promise.all([
loadExternalResource(config.path.cssPath, "css"),
loadExternalResource(config.path.live2dCorePath, "js"),
loadExternalResource(config.path.live2dSdkPath, "js"),
loadExternalResource(config.path.tipsJsPath, "js"),
loadExternalResource(config.path.chatJsPath, "js"),
loadExternalResource(config.path.chatCssPath, "css"),
loadExternalResource("https://cdn.jsdelivr.net/npm/marked/marked.min.js", "js") // 加载 Marked.js 库
]).then(() => {
if (typeof initWidget !== "undefined") {
initWidget({
waifuPath: config.path.tipsJsonPath,
cdnPath: config.path.modelPath,
tools: config.tools,
dragEnable: config.drag.enable,
dragDirection: config.drag.direction,
switchType: config.switchType
});
if (typeof Live2DChat !== "undefined") {
window.live2dChatInstance = new Live2DChat({
apiUrl: 'https://你的域名/api/chat', // 后端 AI 对话接口地址,需与后端部署的 FastAPI 服务地址一致
clientUuid: '你的鉴权UUID', // 简易鉴权 UUID,需与后端 FastAPI 服务中设置的 UUID 一致
configUrl: config.path.chatJsonPath
});
}
}
}).catch(err => {
console.error("Live2D 资源加载失败:", err);
});
};
if (window.requestIdleCallback) {
requestIdleCallback(initTask);
} else {
setTimeout(initTask, 500);
}
});
}
function loadExternalResource(url, type) {
return new Promise((resolve, reject) => {
let tag;
if (type === "css") {
tag = document.createElement("link");
tag.rel = "stylesheet";
tag.href = url;
} else if (type === "js") {
tag = document.createElement("script");
tag.src = url;
tag.async = false;
}
if (tag) {
tag.onload = () => resolve(url);
tag.onerror = () => reject(url);
document.head.appendChild(tag);
}
});
}
}
</script>

  同时确保你已经安装 hexo-generator-search 插件,并在 _config.butterfly.yml 中正确配置了 search.path。这是 AI RAG 全局检索功能的依赖:

cd 你的博客目录/
npm install hexo-generator-search --save
search:
use: local_search
path: search.xml
field: post
content: false
format: striptags
limit: 1000

local_search:
enable: true

  重新部署hexo clean && hexo g && hexo d,访问博客后你应该能够看到看板娘已经成功加载,并且侧边工具栏中出现了新的 “Chat” 图标。点击它就可以打开 AI 聊天窗口了。

4.3 参数配置

  AI 的行为逻辑、身份设定、UI 文本以及上下文处理策略由 waifu-chat.json 配置,可进行自定义:

参数 说明
api.url 后端 AI 对话接口的请求路由。此配置优先级低于前端 JS 实例化的传入参数。
api.uuid 客户端鉴权标识或 Token,将随请求头 Authorization: Bearer <uuid> 发送供后端校验。
ui.title 聊天窗口顶部栏显示的标题文本。
ui.placeholder 底部消息输入框的占位引导提示文本。
ui.errorMsg 后端接口响应异常或网络阻断时,看板娘弹出的原生错误提示语。
ui.typingSpeed 模拟打字机动画的单字符输出延迟时间(单位:毫秒)。
chat.storageKey 浏览器 LocalStorage 持久化存储对话历史记录的键名。
chat.maxHistory 存储在本地的最大历史对话消息对象数量(防止 Token 溢出与存储爆满)。
chat.pageContextSelector RAG 核心:当前页面阅读器抓取的 DOM 目标选择器,前端将提取其内部纯文本。
chat.pageContextMaxLength RAG 核心:抓取页面正文的最大字符截断长度,超过部分将被截断并追加系统提示。
chat.searchXmlPath Hexo 全站索引文件路径(需配合 hexo-generator-search),用于全局知识库匹配。
chat.welcomeMsg 访客首次打开聊天框时,AI 主动发送的第一条破冰消息。
chat.welcomeOptions 预设快捷按钮数组。display为按钮文本,send为实际发送指令(使用||分割来进行随机发送)。
chat.systemPrompt 系统人设与输出约束提示词。提示词每行以数组形式给出。
chat.contextTemplate RAG 隐式上下文拼装模板对象。用于规范化包含 “页面正文” 与 “检索结果” 的拼接格式。

4.4 注意事项

  • 基础功能配置简化说明

  本项目在原版底层框架上对 Live2D 功能进行了一定程度的默认简化。如果你需要进一步自定义模型,例如自定义 .exp3.json 触发专属表情、为 .motion3.json 动作绑定口型与音频、或调整模型在 Canvas 画布中的 scale 缩放与 translate 坐标偏移量等,请务必参阅原项目的详细文档 👉 live2d-widget-v3 使用说明

  • AI 模块的低耦合性说明

  AI 对话引擎 (waifu-chat.* 文件) 具有完全独立的生命周期。如果你在某些页面不想开启 AI 功能,只需在前端脚本注入时不加载这三个文件,看板娘依然可以作为普通的 Live2D 挂件正常运行。

  • RAG 容器匹配说明

  waifu-chat.js 中的本地阅读器默认通过 #article-container 选择器来提取当前页面的正文文本。如果你的 Hexo 博客未采用 Butterfly 主题,或者你在主题魔改中更改了文章主容器的 ID / Class,请务必在 waifu-chat.json 中同步修改 pageContextSelector 字段。同时需检查你的站点根目录是否存在 search.xml 文件(由 hexo-generator-search 生成),并将其路径正确配置到 searchXmlPath 字段。