1. 从「写死祝福语」到「模型实时生成」:这个新年页面到底解决什么问题
新年祝福语生成网页,说白了就是一个能根据昵称、职业、字数,自动拼出一段拜年话的小工具。纯前端用 JavaScript 拼字符串也能做,但拼出来的东西翻来覆去就那几句,用户输入「程序员」和「老师」,出来的句子骨架几乎一样,只是换了个词。真正让这个页面「活」起来的,是后台接一个大模型,把昵称、职业、语气、字数这些参数交给模型,让它现场写一段。
问题也随之而来:你在 VS Code 里调试这个网页,前端要调模型接口,模型 Key 得放在某个地方。如果今天用 DeepSeek,明天想换另一个模型对比效果,Key 就散落在.env、settings.json、浏览器 localStorage 好几个位置,改一处忘一处,接口地址和鉴权头格式还不一样。我试过在三个文件里各存一份 Key,结果调试时请求一直 401,排查了半小时才发现是某个文件里的 Key 少复制了一位。
TaoToken 在这里的角色,是把「多个模型的 Key 和接口地址」收敛成一套统一的入口。你只需要在 VS Code 的settings.json里配一次,网页请求也走同一个地址,换模型时改的是请求体里的模型名,而不是到处翻 Key。这篇就按「VS Code 里从零搭页面 → 配统一 Key → 本地跑起来验证 → 排错」的顺序走一遍,目标是你跟着做完,能在浏览器里看到模型实时生成的祝福语。
适合谁看:会用 VS Code、写过一点 HTML/JS、想让自己的小工具接上大模型但被多 Key 管理烦到的人。不需要你懂后端框架,一个静态页面加一个本地服务就够。
2. TaoToken 前置准备:统一 Key 与接口地址怎么拿
在动手写页面之前,先把「钥匙」和「门牌号」准备好。TaoToken 的定位是统一模型接入层,你拿一个 Key,就能在同一个接口规范下调用包括 DeepSeek 在内的多个模型。对咱们这个新年祝福语页面来说,好处是:前端请求地址只写一次,以后想换模型只改一个字段。
第一步,打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册并登录。登录后进入控制台,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在左侧找到 API Keys 相关入口,新建一个 Key。这个 Key 就是后面settings.json和网页请求里要用的凭证,复制下来先存到安全的地方,页面关闭后通常不再完整显示。
第二步,确认接口地址。TaoToken 的 API 基址是 https://taotoken.net/api ,注意这个地址不带任何查询参数。后面网页里发请求时,路径一般拼成/v1/chat/completions这种 OpenAI 兼容格式,具体以接入文档为准。文档入口在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面会写清楚请求头、请求体字段和返回结构,建议开着对照。
第三步,想先确认模型能不能通,不用写代码,直接用模型对话页面发一句话试试: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。在里面选 DeepSeek 相关模型,输入「用一句话给程序员写个新年祝福」,能正常返回就说明 Key 和账户状态没问题。这一步能帮你把「Key 问题」和「代码问题」提前分开,省得后面页面报错时两头猜。
注意:Key 属于敏感信息,不要直接提交到 Git 仓库,也不要在截图里露出完整字符串。本地调试可以放在
.env或 VS Code 的用户级settings.json里,并在.gitignore中排除。
3. 可复制配置:VS Code settings.json 骨架与网页请求示例
这一节是核心,分两块:编辑器侧的配置骨架,和网页侧的请求代码。两块都用同一套 Key 和基址,做到「配一次,两边通用」。
3.1 VS Code settings.json 配置骨架
在 VS Code 里按Ctrl + Shift + P,输入Open User Settings (JSON),打开用户级settings.json。如果你只想对当前项目生效,就在项目根目录建.vscode/settings.json。加入下面这段骨架:
{ "taotoken.apiBase": "https://taotoken.net/api", "taotoken.apiKey": "把你的_TaoToken_Key_填在这里", "taotoken.defaultModel": "deepseek-chat", "taotoken.requestTimeout": 30000, "terminal.integrated.env.windows": { "TAOTOKEN_API_KEY": "${config:taotoken.apiKey}", "TAOTOKEN_API_BASE": "${config:taotoken.apiBase}" }, "terminal.integrated.env.linux": { "TAOTOKEN_API_KEY": "${config:taotoken.apiKey}", "TAOTOKEN_API_BASE": "${config:taotoken.apiBase}" }, "terminal.integrated.env.osx": { "TAOTOKEN_API_KEY": "${config:taotoken.apiKey}", "TAOTOKEN_API_BASE": "${config:taotoken.apiBase}" } }这里几个字段的作用:apiBase固定为 TaoToken 的 API 地址,apiKey填你刚才复制的 Key,defaultModel先写deepseek-chat,后面换模型改这一处。terminal.integrated.env.*是把这两个值注入到 VS Code 集成终端的环境变量里,这样你在终端里跑本地服务时,脚本能直接读process.env.TAOTOKEN_API_KEY,不用在代码里硬编码。
如果你用的是 Cursor,它兼容 VS Code 的设置体系,同样可以在设置 JSON 里加这段,效果一致。区别只是 Cursor 自带的模型面板和这里的配置是两套东西,咱们这个页面走的是自己配的 TaoToken 通道,不依赖编辑器内置模型。
3.2 网页请求示例:三文件结构
按前面 excerpt 里提到的目录结构,把页面拆成三个文件,方便维护:
new-year-wish/ ├── index.html ├── style.css └── app.jsindex.html负责结构,输入昵称、职业、字数,一个生成按钮,一个展示区:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>新年祝福语生成器</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div class="container"> <h1 class="title">新年祝福语生成器</h1> <div class="input-group"> <input id="nickname" type="text" placeholder="被祝福人的昵称" /> </div> <div class="input-group"> <input id="job" type="text" placeholder="被祝福人的职业,如程序员" /> </div> <div class="input-group"> <select id="length"> <option value="50">约 50 字</option> <option value="100" selected>约 100 字</option> <option value="150">约 150 字</option> </select> </div> <button id="generate">生成祝福语</button> <div class="output" id="output"></div> <div class="copy-success" id="copyTip"></div> </div> <script src="app.js"></script> </body> </html>style.css按暖色调来,背景图你可以换成自己的新年图:
body { margin: 0; background-color: #ffefd5; background-image: url('./your-imgs.jpg'); background-size: cover; background-position: center; height: 100vh; font-family: 'Arial', sans-serif; color: #333; display: flex; align-items: center; justify-content: center; } .container { max-width: 600px; width: 90%; background-color: rgba(255, 255, 255, 0.8); padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } .title { font-size: 2.5em; color: #d2691e; text-align: center; font-family: 'Comic Sans MS', cursive, sans-serif; } .input-group { width: 80%; margin: 15px auto; } input, select { width: 100%; padding: 10px; font-size: 1em; border-radius: 5px; border: 1px solid #ccc; box-sizing: border-box; } button { display: block; margin: 0 auto; background-color: #d2691e; color: #fff; padding: 10px 20px; font-size: 1.2em; border: none; border-radius: 5px; cursor: pointer; } .output { margin: 20px auto; font-size: 1.2em; color: #d2691e; width: 80%; min-height: 60px; white-space: pre-wrap; } .copy-success { color: green; margin: 10px auto; font-size: 1em; width: 80%; text-align: center; }app.js是重点,它负责收集输入、发请求、渲染结果。注意这里请求的是本地服务/api/wish,而不是直接在前端暴露 Key:
const generateBtn = document.getElementById('generate'); const outputEl = document.getElementById('output'); const copyTip = document.getElementById('copyTip'); generateBtn.addEventListener('click', async () => { const nickname = document.getElementById('nickname').value.trim(); const job = document.getElementById('job').value.trim(); const length = document.getElementById('length').value; if (!nickname || !job) { outputEl.textContent = '昵称和职业都填一下,模型才知道给谁写。'; return; } outputEl.textContent = '正在生成,请稍候...'; copyTip.textContent = ''; try { const resp = await fetch('/api/wish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname, job, length }) }); if (!resp.ok) { const errText = await resp.text(); throw new Error(`服务返回 ${resp.status}: ${errText}`); } const data = await resp.json(); outputEl.textContent = data.wish || '模型没有返回内容。'; } catch (err) { outputEl.textContent = '生成失败:' + err.message; } }); outputEl.addEventListener('click', async () => { const text = outputEl.textContent; if (!text || text.startsWith('正在生成') || text.startsWith('生成失败')) return; try { await navigator.clipboard.writeText(text); copyTip.textContent = '复制成功'; setTimeout(() => (copyTip.textContent = ''), 1500); } catch (e) { copyTip.textContent = '复制失败,请手动选中复制。'; } });3.3 本地服务:把 Key 留在服务端
前端不直接拿 Key,所以需要一个很小的本地服务来转发请求。用 Node.js 写一个server.js,放在项目根目录:
const http = require('http'); const fs = require('fs'); const path = require('path'); const API_BASE = process.env.TAOTOKEN_API_BASE || 'https://taotoken.net/api'; const API_KEY = process.env.TAOTOKEN_API_KEY; const MODEL = process.env.TAOTOKEN_MODEL || 'deepseek-chat'; if (!API_KEY) { console.error('缺少 TAOTOKEN_API_KEY,请检查 VS Code settings.json 或环境变量。'); process.exit(1); } const server = http.createServer(async (req, res) => { if (req.method === 'POST' && req.url === '/api/wish') { let body = ''; req.on('data', chunk => (body += chunk)); req.on('end', async () => { try { const { nickname, job, length } = JSON.parse(body); const prompt = `请写一段新年祝福语,送给昵称叫「${nickname}」、职业是「${job}」的人。` + `要求:语气温暖真诚,约 ${length} 字,结尾用感叹号,不要出现引号包裹的模板感。`; const apiResp = await fetch(`${API_BASE}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` }, body: JSON.stringify({ model: MODEL, messages: [ { role: 'system', content: '你是一个擅长写新年祝福语的中文助手。' }, { role: 'user', content: prompt } ], temperature: 0.9 }) }); if (!apiResp.ok) { const t = await apiResp.text(); res.writeHead(apiResp.status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: t })); return; } const data = await apiResp.json(); const wish = data.choices?.[0]?.message?.content?.trim() || ''; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ wish })); } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); } }); return; } // 静态文件 const filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url); fs.readFile(filePath, (err, content) => { if (err) { res.writeHead(404); res.end('Not Found'); return; } const ext = path.extname(filePath); const type = ext === '.css' ? 'text/css' : ext === '.js' ? 'application/javascript' : 'text/html'; res.writeHead(200, { 'Content-Type': type }); res.end(content); }); }); server.listen(3000, () => { console.log('本地服务已启动:http://localhost:3000'); console.log('使用模型:', MODEL); });这段代码里,API_BASE和API_KEY都从环境变量读,而环境变量正是第 3.1 节在settings.json里注入的。这样 Key 只存在于编辑器配置和进程环境里,不会出现在浏览器网络面板中。
4. 本地运行验证:从终端启动到看到模型返回
配置写完,接下来跑起来验证。整个过程分三步:装依赖、起服务、点按钮。
第一步,确认 Node.js 版本。在 VS Code 集成终端里执行:
node -v建议 18 以上,因为代码里用了全局fetch。如果版本太低,升级一下 Node。
第二步,在项目根目录启动服务。因为settings.json已经把环境变量注入集成终端,直接运行即可:
node server.js正常会看到:
本地服务已启动:http://localhost:3000 使用模型: deepseek-chat如果这里报「缺少 TAOTOKEN_API_KEY」,说明环境变量没注入成功。先检查settings.json里taotoken.apiKey是否填了真实 Key,再确认你是从 VS Code 集成终端启动的,而不是系统自带终端。系统终端不会读 VS Code 的设置。
第三步,浏览器打开http://localhost:3000,填入昵称「小林」、职业「程序员」、字数选 100,点「生成祝福语」。几秒后输出区应该出现一段完整祝福语,类似:
小林,新年好!过去一年你在代码世界里披荆斩棘,愿新的一年里,bug 绕着你走,需求一次通过,升职加薪都有你的份,身体健康,万事顺遂!
点一下输出文字,下方出现「复制成功」,说明剪贴板逻辑也通了。到这一步,整条链路——页面输入 → 本地服务 → TaoToken → DeepSeek → 返回渲染——就全部验证完毕。
想换模型对比效果,只改settings.json里的taotoken.defaultModel,或者在启动时临时指定:
TAOTOKEN_MODEL=另一个模型名 node server.js接口地址和 Key 都不用动,这就是统一 Key 配置省事的地方。
5. 本篇常见错排查:401、404、CORS 与超时
调试过程中最容易撞上的几类问题,我按现象、原因、处理列一下,方便你对照。
现象一:页面提示「生成失败:服务返回 401」。这是鉴权没过。先确认settings.json里的 Key 是完整的、没有多余空格;再确认请求头是Authorization: Bearer <Key>格式,Bearer 后面有一个空格。如果 Key 是在控制台新建后没复制全,重新生成一个再试。控制台入口: https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
现象二:服务返回 404。多半是接口路径拼错了。TaoToken 的基址是https://taotoken.net/api,聊天补全路径是/v1/chat/completions,拼起来是https://taotoken.net/api/v1/chat/completions。如果你在基址后面又重复加了/v1,就会 404。对照接入文档确认路径: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
现象三:浏览器控制台报 CORS 错误。如果你图省事,把请求直接写在前端app.js里发往 TaoToken,就会遇到跨域。正确做法就是本篇的结构:前端只请求同源的/api/wish,由本地 Node 服务去转发。这样既避开 CORS,又不暴露 Key。
现象四:请求长时间不返回,最后超时。先看settings.json里的requestTimeout是不是设得太短,模型生成 100 字通常几秒内完成,30 秒足够。如果持续超时,去模型对话页面单独发一句测试,确认账户和模型状态正常: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果那边正常,问题就在你的网络或本地服务,检查终端有没有报错。
现象五:返回内容为空,但状态码 200。检查解析路径data.choices[0].message.content是否和实际返回结构一致。不同模型返回字段可能有细微差别,打印一下完整data看看结构,再调整取值。
现象六:改了settings.json但环境变量没变。VS Code 的集成终端在启动时读取设置,改完设置要新开一个终端窗口,旧终端不会自动刷新。
6. 接下来怎么走:把调试通道固定下来
页面能跑通之后,你可能会想加更多功能,比如让用户选语气(正式/幽默)、生成多条备选、把历史记录存到 localStorage。这些都属于前端交互,不影响 Key 配置。真正值得花时间的是把「调试通道」固定成习惯:所有模型请求都走 TaoToken 的统一入口,Key 只配在settings.json一处,换模型只改模型名。
如果你后面要长期在这个项目上做编码和 Agent 类的实验,可以了解一下 Coding Plan,它面向的是持续性的编码场景,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。日常只是想快速验证某个模型对祝福语的效果,用模型对话页面就够了。需要管理多个 Key 或查看用量,回控制台。接入细节有疑问,翻接入文档。
最后留一个实用习惯:把server.js里的MODEL做成命令行参数,配合settings.json里的默认值,你就能在不改代码的情况下,一条命令切换模型跑对比。这个页面本身不复杂,但它把「前端调试 + 统一 Key + 模型调用」这条链路跑通了,以后做类似的小工具,直接复用这套骨架就行。