一个下午省出来的时间,够我多写三个组件——Vue3 对话框接统一网关的完整流程
为什么要用统一网关
上个月接手一个智能客服的小项目,产品经理的原话是:“先接一个模型试试水,后面可能要换,也可能要多来几家做对比。”
如果按传统做法,我需要为每家模型供应商分别读文档、注册账号、适配各自的请求格式。上次干这事,光是消化三家不同的 SDK 和错误码体系,就花了我整整两天——还不算后续维护多套鉴权逻辑的成本。
这次换了思路:直接对接统一网关 ThisToken.AI。它把多家模型收敛到一套 OpenAI 兼容的接口后面,我的前端组件只需要写一次,切换模型时改一个字符串参数。实际计时结果:
- 旧方式(逐家适配):注册 + 读文档 + 写适配层,约 12~14 小时
- 这次(统一网关):注册 + 拿 Key + 跑通组件,不到 2 小时
省下的十几个小时,我用来打磨了流式输出的打字机效果和错误重试——这些才是用户真正能感知到的东西。对独立开发者来说,时间就是最贵的资源;把接入层的体力活压缩掉,才有余力做产品本身。
第一步:注册并获取 API Key
打开 ThisToken.AI 的注册页,邮箱验证后进入控制台。在「API Keys」页面点击创建,复制生成的 Key 妥善保存——只显示一次,丢了只能重建。
价格方面我不写具体数字,以官网价格页为准。统一网关的一个隐性好处是:多家模型的消耗汇总在一张账单里,月底对账时不用在五六个后台之间来回切换,我自己的月度账单核对时间从大约 40 分钟缩到 5 分钟。
第二步:Vue3 对话组件封装
核心思路:封装一个 useChat 组合式函数 + 一个展示组件,通过环境变量配置网关地址,以后换模型只改 VITE_MODEL 一个值。
项目结构:
src/
├── composables/useChat.js
├── components/AiChat.vue
└── .env.local.env.local:
VITE_API_BASE=https://api.thistoken.ai/v1
VITE_API_KEY=你的Key
VITE_MODEL=gpt-4o-miniuseChat.js(完整可复制):
import { ref } from 'vue'
export function useChat() {
const messages = ref([
{ role: 'system', content: '你是一个乐于助人的中文助手。' }
])
const input = ref('')
const loading = ref(false)
const error = ref(null)
async function send() {
const text = input.value.trim()
if (!text || loading.value) return
input.value = ''
messages.value.push({ role: 'user', content: text })
loading.value = true
error.value = null
// 占位一条 assistant 消息,流式填充
messages.value.push({ role: 'assistant', content: '' })
const replyIndex = messages.value.length - 1
try {
const res = await fetch(
`${import.meta.env.VITE_API_BASE}/chat/completions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${import.meta.env.VITE_API_KEY}`
},
body: JSON.stringify({
model: import.meta.env.VITE_MODEL,
messages: messages.value.slice(0, -1), // 不带空的占位消息
stream: true
})
}
)
if (!res.ok) throw new Error(`网关返回 ${res.status}`)
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() // 保留不完整的行
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed.startsWith('data:')) continue
const data = trimmed.slice(5).trim()
if (data === '[DONE]') continue
try {
const json = JSON.parse(data)
const delta = json.choices?.[0]?.delta?.content
if (delta) messages.value[replyIndex].content += delta
} catch { /* 忽略无法解析的片段 */ }
}
}
} catch (e) {
error.value = `请求失败:${e.message},请稍后重试`
if (!messages.value[replyIndex].content) {
messages.value.splice(replyIndex, 1)
}
} finally {
loading.value = false
}
}
return { messages, input, loading, error, send }
}AiChat.vue:
<script setup>
import { useChat } from '../composables/useChat'
const { messages, input, loading, error, send } = useChat()
</script>
<template>
<div class="chat">
<div class="msgs">
<div v-for="(m, i) in messages.slice(1)" :key="i" :class="m.role">
{{ m.content }}<span v-if="loading && i === messages.length - 2" class="cursor">▍</span>
</div>
<p v-if="error" class="err">{{ error }}</p>
</div>
<div class="bar">
<input v-model="input" @keyup.enter="send" placeholder="输入消息,回车发送" :disabled="loading" />
<button @click="send" :disabled="loading">{{ loading ? '生成中' : '发送' }}</button>
</div>
</div>
</template>几个封装时值得留意的点:
- Key 不要提交到仓库。
.env.local加进.gitignore;正式部署时更稳妥的做法是把请求转发放在一层轻量后端(或 Serverless 函数)里,前端只和自己的转发层通信。 - 流式解析要处理粘包。上面代码里
buffer的存留逻辑就是为了应对 chunk 在行中间被切断的情况。 - 换模型只改一个环境变量。这就是统一网关的核心收益:产品要求换成别的模型时,我改了一行配置、重启 dev server,前后 90 秒。放在以前,这意味着重新注册账号、重读一份新文档、改鉴权代码,至少半天。
时间账算下来
这轮接入的总开销:
| 事项 | 耗时 |
|---|---|
| 注册 + 获取 API Key | 10 分钟 |
| 封装组件 + 跑通流式输出 | 约 70 分钟 |
| 错误处理与重试打磨 | 约 30 分钟 |
而对独立开发者更有价值的是长期维护成本:一套接口格式、一张账单、一处鉴权逻辑。供应商侧有任何变动,我的前端代码一行不用动。
先把第一段代码跑起来,再谈架构。如果你还没注册,可以直接从这里开始:https://api.thistoken.ai/register ——注册拿到 Key 之后,把上面三个文件放进任何 Vue3 项目,npm run dev,今天下午你就能看到自己的 AI 对话框动起来。
---
想直接跑通示例?访问 https://api.thistoken.ai/register 注册 ThisToken.AI,获取 API Key 后即可开始。