最近把一个紫微斗数排盘项目fork下来部署到了Cloudflare Pages上。这个项目排盘、真太阳时、流年、合盘这些功能都比较齐全,唯一的问题是AI解读需要访客自己去模型平台注册充值,然后填写API Key才能使用。对只想算一卦的普通用户来说,这个门槛太高了。
所以我打算配置一个默认的DeepSeek API Key,让访客不填Key也能直接使用AI解读。但项目是纯前端应用,AI请求由浏览器直接发往api.deepseek.com,这意味着Key要么打包进JS要么存在localStorage,只要Key在浏览器里,就没有任何保密可言。这篇文章记录一下我的处理方案。
为什么不能把Key放在环境变量里
首先踩过的坑是VITE_前缀的环境变量。在Cloudflare Pages中,以VITE_开头的变量会在构建时被Vite内联进前端JS包。部署后任何访客都可以在浏览器DevTools的Sources里搜索打包产物,直接提取到完整的Key:
1 | grep -o 'sk-[A-Za-z0-9]\{6,\}' dist/assets/*.js |
⚠️ 不要用VITE_前缀的变量存放任何Key,它本质上是一个公开变量。
更深一层的问题是架构:项目原本是浏览器直连api.deepseek.com。只要保持直连,Key就必须存在于浏览器里,保密就是伪命题。所以要让默认Key不暴露,只能让浏览器不直接调用DeepSeek,改走自己的服务端。
方案:用Cloudflare Pages Functions做代理
Cloudflare Pages自带服务端运行时Pages Functions。在仓库的app目录下放一个functions/api/chat.ts,部署后会自动变成POST /api/chat接口,和静态站在同一个域名。
函数做的事情很简单:接收前端传来的messages,读取服务端环境变量DEEPSEEK_API_KEY,加上Authorization头转发给api.deepseek.com,再把响应返回给浏览器。Key只出现在服务端,浏览器全程看不到:
1 | export async function onRequestPost(context) { |
在Cloudflare Dashboard的Pages项目 → Settings → Variables and Secrets里添加DEEPSEEK_API_KEY,类型选择Secret。部署完成后检查构建产物,应该找不到任何Key相关的字符串:
1 | grep -rl 'DEEPSEEK_API_KEY' dist/ |
前端路由逻辑
前端在llm.ts里加了一个判断函数,只有同时满足”选择DeepSeek厂商”和”没有填写自己的API Key”两个条件才走共享代理:
1 | export function isSharedDefaultActive(provider, apiKey) { |
具体规则:
- 没填Key的DeepSeek用户:请求发往/api/chat,使用站点提供的默认Key;
- 填了自己Key的用户:仍然直连DeepSeek,Key只在用户浏览器和DeepSeek之间传输,不经过我的服务器;
- 其他厂商(Kimi、Gemini、Claude):保持原有逻辑不变。
另外我把默认厂商从kimi改成了deepseek。这样新访客打开网页,排完盘直接就能用AI解读,不需要任何配置。老访客的localStorage里保存的还是原来的kimi配置,不会被覆盖。
如果部署方不想开放共享额度,可以用构建变量VITE_SHARED_DEEPSEEK=false关闭这个功能。
防止共享Key被恶意刷
共享Key放在公共接口后面,任何人都能调用,恶意脚本可以消耗DeepSeek的余额。所以函数里加了一些限制:
| 限制 | 说明 |
|---|---|
| 请求格式 | 只接受POST + application/json |
| messages数量 | 最多10条 |
| 总字符数 | 不超过20k |
| max_tokens | 普通模式封顶4096,思考模式封顶8000 |
| 来源白名单 | 可选,配置ALLOWED_ORIGINS后只允许指定来源调用 |
| 按IP限流 | 可选,绑定KV命名空间RATE_LIMIT_KV后每IP每窗口最多10次 |
max_tokens封顶用于限制单次请求的消耗,按IP限流用于限制同一个IP的调用频率。另外建议在DeepSeek控制台开启余额预警,一旦余额异常可以及时发现。
这些措施能降低被刷的风险,但无法完全杜绝。如果对安全要求更高,可以在Cloudflare控制台配置WAF规则或更严格的限流策略。