1. Vue 项目里塞一只 Live2D 看板娘,到底难在哪
先说清楚我们要做的东西是什么。Live2D 看板娘,就是网页右下角或左下角那个会眨眼、会跟着鼠标转头、点一下还会说话或做动作的二次元角色。它本质上是把一张被切分并绑定骨骼的 2D 立绘,通过 Live2D 的渲染库实时驱动,再叠加一层交互逻辑(鼠标跟随、点击反馈、语音播放)。在 Vue 项目里集成它,适合的人群很明确:想给个人博客、后台管理首页、产品落地页加一点拟人化交互的前端开发者,尤其是已经用 Vue2 或 Vue3 搭好项目、不想大改架构的人。
真正上手你会发现,难点不在“把模型显示出来”,而在三件事上:第一,模型资源、渲染库、配置文件三者的路径关系很容易错位,本地能跑打包后白屏是常态;第二,Vue 的组件生命周期和 Live2D 的全局初始化时机对不上,created里直接 init 经常拿不到 DOM;第三,看板娘一旦要“会聊天”,就得接大模型 API,而每个模型厂商的 Key、Base URL、模型 ID 都不一样,散落在前端代码里既难管又不安全。
这篇教程就按“模型加载 → 交互事件 → API 调用分层”的顺序拆。模型加载部分给你可直接复制的组件配置,API 调用部分用 TaoToken 的统一 Key 来收口——一个 Key 走多家模型,前端只认一个 Base URL 和一个模型 ID,换模型不用改调用代码。本地启动验证步骤我会写全,包括怎么确认看板娘真的加载成功、怎么确认对话请求真的通了。踩过的坑我也会标出来,比如L2Dwidget is not defined、reading 'choices'这类报错怎么定位。
你不需要有 Live2D 建模基础,也不需要懂骨骼绑定,只要会 Vue 的基本组件写法、能在终端跑npm run dev,就能跟着做完。下面从项目准备开始。
2. 前置准备:模型资源、依赖与 TaoToken 统一 Key 的接入位置
2.1 模型资源与目录约定
Live2D 看板娘需要三类文件:渲染库(L2Dwidget.min.js及其依赖)、模型资源(每个模型一个live2d-widget-model-xxx文件夹,里面有.model.json、贴图、动作、语音)、以及一份初始化配置。社区里流传的live2dw资源包结构大致是这样:
public/ live2dw/ lib/ L2Dwidget.min.js L2Dwidget.0.min.js live2d-widget-model-haru_2/ assets/ haru02.model.json haru02.2048/ texture_00.png ...关键点:lib和live2d-widget-model-xxx必须是同级目录,因为L2Dwidget.init里的pluginRootPath会以它为根去拼pluginJsPath和pluginModelPath。很多人白屏就是因为把模型文件夹放到了lib里面,路径拼出来 404。
Vue2 项目放static或public,Vue3(Vite)项目统一放public,因为 Vite 不会处理public之外的静态资源引用。放好后,index.html里引入渲染库:
<!-- public/index.html 或项目根 index.html --> <script src="/live2dw/lib/L2Dwidget.min.js"></script>注意这里用绝对路径/live2dw/...,不要用./static/...这种相对路径,否则在嵌套路由下会 404。
2.2 TaoToken 统一 Key 的定位
看板娘要“会聊天”,就得调大模型。传统做法是前端直接写某家厂商的 Key 和 URL,问题是:换模型要改代码,多个模型要维护多套 Key,前端暴露 Key 也不安全。TaoToken 的作用是把这层收口——它提供统一的 API 入口,你用同一个 Key、同一个 Base URL,通过改model字段就能切换不同模型。
接入位置建议放在一个独立的src/api/ai.js里,而不是塞进看板娘组件。这样看板娘组件只负责“什么时候发请求、拿到回复怎么显示”,API 层负责“怎么发、发给谁”。分层之后,以后换模型只动ai.js一行。
TaoToken 的 API 地址是https://taotoken.net/api,兼容 OpenAI 的/v1/chat/completions格式,所以你可以直接用fetch或axios发请求,不需要额外 SDK。Key 的获取在控制台里创建,后面第 3 节会给完整配置。
2.3 依赖清单
除了 Live2D 的静态资源,前端还需要一个 HTTP 客户端。用axios或原生fetch都行,我这里用axios方便统一拦截器:
npm install axios如果你用 Vue3 + Vite,确认public目录存在;Vue2 + Webpack 确认static或public目录存在。环境变量方面,把 Key 放到.env.local里,不要硬编码进组件:
VITE_TAOTOKEN_KEY=你的Key VITE_TAOTOKEN_BASE=https://taotoken.net/apiVue2 项目把VITE_换成VUE_APP_。这样打包时 Key 不会进 git,也方便不同环境切换。
3. 可复制配置:Live2D 组件 + TaoToken 请求层完整代码
3.1 Live2D 看板娘组件(Vue3 写法)
先给一个能直接跑的组件。核心是把L2Dwidget.init放到onMounted里,并用nextTick确保 DOM 就绪。Vue2 把onMounted换成mounted、ref换成this.$refs即可。
<!-- src/components/Live2DWidget.vue --> <template> <div ref="live2dContainer" class="live2d-container"></div> </template> <script setup> import { onMounted, ref, nextTick } from 'vue' import { chatWithAI } from '@/api/ai' const live2dContainer = ref(null) onMounted(async () => { await nextTick() if (!window.L2Dwidget) { console.error('L2Dwidget 未加载,检查 index.html 引入路径') return } window.L2Dwidget.init({ pluginRootPath: '/live2dw/', pluginJsPath: 'lib/', pluginModelPath: 'live2d-widget-model-haru_2/assets/', tagMode: false, debug: false, model: { jsonPath: '/live2dw/live2d-widget-model-haru_2/assets/haru02.model.json' }, display: { position: 'left', width: 300, height: 600, hOffset: 0, vOffset: -20 }, mobile: { show: true, scale: 0.5 }, react: { opacityDefault: 0.9, opacityOnHover: 0.6 }, log: false }) // 监听看板娘点击事件,触发对话 window.addEventListener('live2d-tap', handleTap) }) async function handleTap() { const reply = await chatWithAI('你好,介绍一下你自己') console.log('看板娘回复:', reply) } </script> <style scoped> .live2d-container { position: fixed; left: 0; bottom: 0; z-index: 999; pointer-events: none; } .live2d-container canvas { pointer-events: auto; } </style>几个参数说明:pluginRootPath是资源根目录,pluginModelPath是模型相对路径,jsonPath是模型描述文件的完整路径。display.position控制左右,hOffset/vOffset微调位置。react.opacityOnHover让鼠标悬浮时半透明,避免挡住页面内容。
3.2 TaoToken 请求层(统一 Key)
这是分层的关键。所有模型调用都走这一个文件,换模型只改MODEL_ID:
// src/api/ai.js import axios from 'axios' const BASE_URL = import.meta.env.VITE_TAOTOKEN_BASE || 'https://taotoken.net/api' const API_KEY = import.meta.env.VITE_TAOTOKEN_KEY const MODEL_ID = 'claude-3-5-sonnet' // 换模型只改这一行 const client = axios.create({ baseURL: BASE_URL, timeout: 30000, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}` } }) export async function chatWithAI(userMessage) { try { const res = await client.post('/v1/chat/completions', { model: MODEL_ID, messages: [ { role: 'system', content: '你是网页看板娘,回复简短、口语化,不超过50字。' }, { role: 'user', content: userMessage } ], temperature: 0.8, max_tokens: 200 }) return res.data.choices[0].message.content } catch (err) { console.error('AI 请求失败:', err.response?.status, err.response?.data) return '呜……我现在有点连不上大脑,稍后再试吧' } }注意BASE_URL用https://taotoken.net/api,请求路径拼/v1/chat/completions,最终是https://taotoken.net/api/v1/chat/completions。这是 OpenAI 兼容格式,messages数组、model字段、choices返回结构都是标准的。
3.3 环境变量与启动
.env.local:
VITE_TAOTOKEN_KEY=sk-你的实际Key VITE_TAOTOKEN_BASE=https://taotoken.net/api启动:
npm run dev打开页面,左下角应该出现看板娘。打开浏览器控制台,手动调一下chatWithAI验证请求层:
// 控制台里执行 import('/src/api/ai.js').then(m => m.chatWithAI('测试一下'))如果返回一段文字,说明 Key 和 Base URL 都通了。这一步先别急着接点击事件,把请求层单独验证通过,后面排障会省很多事。
4. 验证请求与成功结果:从模型加载到对话联动
4.1 确认模型真的加载成功
看板娘显示出来不等于加载成功。打开 DevTools 的 Network 面板,筛选model.json,应该看到haru02.model.json返回 200。再筛texture,贴图 PNG 也应该是 200。如果model.json404,说明jsonPath写错了;如果贴图 404,说明pluginModelPath和jsonPath的目录层级对不上。
Console 里如果出现L2Dwidget is not defined,是index.html的 script 没加载或路径错。如果出现Failed to load resource: 404,对着 Network 面板的请求 URL 和你的实际文件路径逐段比对。
4.2 验证对话请求
在handleTap里加日志,或者直接在控制台调chatWithAI。成功的返回长这样:
{ "id": "chatcmpl-xxx", "object": "chat.completion", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "你好呀,我是看板娘~" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 30, "completion_tokens": 15, "total_tokens": 45 } }你能拿到choices[0].message.content,就说明整条链路通了。这时候再把回复显示到页面上——可以做一个简单的气泡,或者直接console.log先看效果。
4.3 把回复接到看板娘身上
最简做法:在组件里加一个ref存回复,模板里渲染一个气泡:
<template> <div ref="live2dContainer" class="live2d-container"></div> <div v-if="bubbleText" class="live2d-bubble">{{ bubbleText }}</div> </template> <script setup> import { ref } from 'vue' const bubbleText = ref('') async function handleTap() { bubbleText.value = '让我想想……' const reply = await chatWithAI('你好') bubbleText.value = reply setTimeout(() => { bubbleText.value = '' }, 5000) } </script> <style scoped> .live2d-bubble { position: fixed; left: 320px; bottom: 400px; max-width: 200px; padding: 10px 14px; background: #fff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 14px; z-index: 1000; } </style>这样点击看板娘 → 发请求 → 气泡显示回复,动作和对话就联动了。如果你想让看板娘在回复时做动作,可以在handleTap里调window.L2Dwidget暴露的动作接口,或者直接触发模型自带的 tap 动作。
4.4 换模型验证
把MODEL_ID从claude-3-5-sonnet改成另一个模型 ID,重启 dev server,再点一次看板娘。如果回复正常,说明统一 Key 的切换生效了。这一步是 TaoToken 方案的核心价值——你不用改ai.js里的 URL 和 Key,只改一个字符串。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
5.1 401 Unauthorized
最常见。原因有三种:Key 没读到、Key 写错、请求头格式错。先确认.env.local里的VITE_TAOTOKEN_KEY有没有被 Vite 读到——在组件里console.log(import.meta.env.VITE_TAOTOKEN_KEY),如果打印undefined,说明变量名前缀不对(Vue2 用VUE_APP_)或者没重启 dev server。环境变量改动必须重启。
如果 Key 读到了还是 401,检查Authorization头是不是Bearer sk-xxx格式,中间有空格,Bearer首字母大写。用 curl 单独验证:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":"hi"}]}'curl 通了说明 Key 没问题,问题在前端代码;curl 也 401 说明 Key 本身要重新生成。
5.2 local proxy failed
这个报错通常出现在你本地配了开发代理,但代理目标写错或没启动。Vue CLI 的vue.config.js或 Vite 的vite.config.js里如果配了proxy,检查target是不是https://taotoken.net,changeOrigin是不是true。如果你没配代理,那这个报错可能来自浏览器插件或系统代理,关掉再试。
还有一种情况:你把BASE_URL写成了相对路径/api,然后指望 dev server 代理转发,但代理没配。最稳的做法是BASE_URL直接写完整地址https://taotoken.net/api,不走代理。
5.3 reading 'choices' / Cannot read properties of undefined
这个报错说明res.data.choices是undefined,也就是返回结构和你预期的不一样。原因通常是:请求根本没成功,返回的是错误对象,但你的代码直接取了choices。在ai.js里加一层判断:
if (!res.data || !res.data.choices || !res.data.choices.length) { console.error('返回结构异常:', res.data) return '回复格式不对' }然后看console.error打印出来的实际结构。常见的是返回了{ error: { message: '...' } },这时候error.message会告诉你具体原因,比如模型 ID 不存在、余额不足、参数不合法。
5.4 OAuth / 认证相关报错
如果你用的是某些需要 OAuth 的客户端工具(比如 Claude Code、Codex 这类),报错可能提示 OAuth 失败或 token 过期。这类工具通常有自己的认证流程,和纯 API Key 调用不是一回事。如果你只是在前端fetch调 TaoToken,不会遇到 OAuth 问题;遇到的多半是你把某个 CLI 工具的配置和前端混了。前端场景统一用Authorization: Bearer头,不走 OAuth。
5.5 看板娘白屏但无报错
检查display.position和hOffset/vOffset,可能模型被移出可视区域了。把width/height调大,position改成right试试。另外确认.live2d-container的z-index没被其他元素盖住,pointer-events设置正确。
6. 把看板娘接进真实项目:分层、换模型与后续扩展
走到这里,你已经有了一个能显示、能点击、能对话的看板娘。接下来聊怎么把它用得更好。
分层是这套方案最值得保留的部分。Live2DWidget.vue只管渲染和交互触发,ai.js只管请求,环境变量只管配置。三层解耦之后,你想换模型,改MODEL_ID;想换 Key,改.env.local;想换看板娘形象,改pluginModelPath和jsonPath。互不影响。
换模型的实际操作:TaoToken 支持多个模型 ID,你在控制台能看到可用列表。把MODEL_ID从claude-3-5-sonnet换成别的,重启,点看板娘,回复风格会变。如果你想让看板娘根据场景用不同模型——比如闲聊用轻量模型、代码问题用强模型——可以在chatWithAI里加一个参数:
export async function chatWithAI(userMessage, modelId = MODEL_ID) { const res = await client.post('/v1/chat/completions', { model: modelId, messages: [...] }) return res.data.choices[0].message.content }调用时传不同modelId即可。这样看板娘可以做成“点一下闲聊、点两下问代码”的双模式。
语音和动作方面,Live2D 模型自带的motions和sounds可以在model.json里配置。你可以在handleTap里根据回复内容触发不同动作,比如回复带“开心”就播happy动作。具体动作名看模型文件夹里的motions定义。语音播放用new Audio('/live2dw/.../sound.mp3').play(),注意浏览器自动播放策略——首次播放需要用户交互触发,点击看板娘正好满足。
性能上,看板娘会持续渲染 canvas,低端设备可能掉帧。可以在页面不可见时(document.hidden)暂停渲染,或者把display.width/height调小。移动端用mobile.scale缩小。
最后给一个实用建议:把看板娘的对话历史存到sessionStorage,这样刷新页面后上下文还在,体验更连贯。但注意别存太多,max_tokens和上下文长度都有上限,超出会报错。每次请求只带最近几轮对话即可。
这套东西跑通之后,你可以继续加:根据路由切换看板娘台词、接入语音识别做语音对话、用看板娘做新手引导。核心链路已经通了,剩下的都是在这个骨架上加肉。