🌐 在线访问:https://stock-ai-r7qr.onrender.com
部署平台 Render Free Tier;15 分钟无访问会进入休眠,首次唤醒约 30 秒。
一个用 Next.js + DeepSeek + Supabase 搭的全栈小应用:
- 输入股票代码(A股 6 位 / 港股 5 位 / 美股字母),从多路行情源拉取实时行情
- 调用 DeepSeek 让 LLM 返回严格 JSON 结构的分析(
summary/sentiment/risk_level/key_factors) - 结果落入 Supabase Postgres,主页展示历史记录
| 层 | 选型 |
|---|---|
| 前端 | Next.js (App Router) + TypeScript + Tailwind CSS |
| 后端 | Next.js API Routes (Node runtime) |
| 行情 | 腾讯财经 / 百度股市通 / 东方财富 / Twelve Data / Yahoo Finance |
| LLM | DeepSeek(deepseek-chat,兼容 OpenAI SDK) |
| 数据库 | Supabase Postgres |
| 部署 | Render Web Service |
.
├── app/
│ ├── api/
│ │ ├── analyze/route.ts # POST 触发分析 + 落库
│ │ ├── quote/route.ts # GET 查询行情
│ │ ├── trend/route.ts # GET 查询 K 线均线趋势
│ │ └── history/route.ts # GET 历史记录
│ ├── layout.tsx
│ ├── page.tsx # 主界面
│ └── globals.css
├── lib/
│ ├── eastmoney.ts # 行情主入口:A 股优先腾讯财经,多源降级
│ ├── baidu.ts # 百度股市通 K 线 + MA5/10/20
│ ├── deepseek.ts # DeepSeek 调用 + JSON 校验
│ └── supabase.ts # Supabase 客户端 + 读写
├── supabase/schema.sql # 建表 SQL
├── .env.example
└── package.json
# 1. 安装依赖
npm install
# 2. 配置环境变量
cp .env.example .env.local
# 然后编辑 .env.local 填入真实凭据(见下文「申请凭据」)
# 3. 启动开发服务器
npm run dev
# 访问 http://localhost:3000- 打开 https://platform.deepseek.com/ ,微信或手机号注册
- 顶部「充值」充 ¥10(按当前价格用很久)
- 左侧菜单「API Keys」→「Create new API key」→ 复制
sk-...到.env.local的DEEPSEEK_API_KEY
- https://supabase.com/ → GitHub 登录 → New Project
- Region 建议选 Singapore 或 Tokyo
- 数据库密码自行设定并记好
- 项目创建完成后,左侧菜单
SQL Editor→ 新建 query → 粘贴supabase/schema.sql的内容 → Run - 左侧菜单
Settings → API:- 复制
Project URL填到SUPABASE_URL - 复制
anon public key填到SUPABASE_ANON_KEY
- 复制
注意:演示 SQL 里启用了 RLS 但允许 anon 直接读写。生产场景请改用 service role key 或更严格的策略。
git init
git add .
git commit -m "feat: 初始化 AI 股票分析面板"
git branch -M main
git remote add origin https://github.com/<你的用户名>/stock-ai.git
git push -u origin main- 登录 https://render.com → 「New +」→「Web Service」
- 连接 GitHub,选你刚推的仓库
- 配置如下:
- Environment:
Node - Build Command:
npm install && npm run build - Start Command:
npm run start - Plan: Free 即可
- Environment:
- Environment Variables 里添加(来自
.env.local):DEEPSEEK_API_KEYSUPABASE_URLSUPABASE_ANON_KEY
- 点 Create Web Service。第一次构建约 3-5 分钟,完成后会得到一个
https://stock-ai-xxxx.onrender.com的地址
Render 免费实例 15 分钟无访问会休眠,第一次请求约需 30 秒冷启动。
| 路径 | 方法 | 说明 |
|---|---|---|
/api/quote?symbol=600519 |
GET | 返回行情 JSON |
/api/trend?symbol=600519 |
GET | 返回百度 K 线 + MA5/10/20 趋势 |
/api/analyze |
POST { symbol } |
返回 { quote, analysis, saved } |
/api/history |
GET | 返回最近 20 条历史记录 |
LLM 默认非常喜欢「在 JSON 前后写一堆解释」「把代码块用 ```json 包起来」「字段名拼写不一致」,这些都会让前端解析炸掉。本项目用三层防御保证最终拿到的一定是合法且字段合规的 JSON。
文件位置:lib/deepseek.ts
const SYSTEM_PROMPT = `你是一名资深股票分析师。基于用户给出的实时行情快照,输出客观的短评。
返回必须是严格的 JSON 对象,字段如下:
- summary: string(中文,120-200 字,总结当前价格表现、成交活跃度、相对昨收的位置,避免任何投资建议)
- sentiment: "Bullish" | "Neutral" | "Bearish"(基于技术面信号判断当下情绪)
- risk_level: "Low" | "Medium" | "High"(结合波幅、换手率、估值给出风险)
- key_factors: string[](3-5 条最关键的支撑/风险点,每条不超过 30 字)
只返回 JSON,不要任何解释性前后缀,不要使用 markdown 代码块包裹。`;关键点:
- 逐字段列约束:每个字段都标出类型 + 取值范围 + 字数限制,比一句「输出 JSON」有效得多
- 枚举值用字面量写出:
"Bullish" | "Neutral" | "Bearish",模型会照抄 - 明确禁止 markdown 包裹:很多模型会输出
```json ... ```,导致JSON.parse失败
文件位置:lib/deepseek.ts
const completion = await client.chat.completions.create({
model: "deepseek-chat",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: buildUserPrompt(quote) },
],
response_format: { type: "json_object" }, // ← 关键
temperature: 0.4, // ← 降低发散度
max_tokens: 800,
});response_format: { type: 'json_object' }:DeepSeek(兼容 OpenAI 协议)会在解码层强制约束输出为合法 JSON 对象,从根本上杜绝非 JSON 输出temperature: 0.4:股票分析不需要创造性,低温度让字段取值更稳定
文件位置:lib/deepseek.ts
function validate(data: unknown): Analysis {
if (!data || typeof data !== "object") throw new Error("LLM 输出格式错误");
const obj = data as Record<string, unknown>;
const summary = typeof obj.summary === "string" ? obj.summary : "";
const sentiment = obj.sentiment;
const risk_level = obj.risk_level ?? obj.riskLevel; // 兼容 camelCase
const key_factors = Array.isArray(obj.key_factors)
? obj.key_factors.map(String).slice(0, 6)
: Array.isArray(obj.keyFactors)
? (obj.keyFactors as unknown[]).map(String).slice(0, 6)
: [];
if (!summary) throw new Error("summary 字段缺失");
if (
sentiment !== "Bullish" &&
sentiment !== "Neutral" &&
sentiment !== "Bearish"
) {
throw new Error(`sentiment 取值非法: ${String(sentiment)}`);
}
if (
risk_level !== "Low" &&
risk_level !== "Medium" &&
risk_level !== "High"
) {
throw new Error(`risk_level 取值非法: ${String(risk_level)}`);
}
return { summary, sentiment, risk_level, key_factors };
}- 即使前两层都失守,这里也会拦住任何「字段缺失 / 枚举值乱写 / 类型错误」的输出
- 兼容
riskLevel/risk_level两种命名(模型偶尔会用驼峰),但不接受Up/Down、高/中/低这类擅自翻译
输入:{ "symbol": "000001" }(平安银行)
{
"summary": "平安银行今日低开低走,现价10.7元,较昨收下跌0.65%,成交额约11.94亿元,换手率0.57%显示交投清淡。股价运行于10.69-10.8区间,低于昨日收盘价,整体偏弱。PE仅3.57倍,PB 0.45倍,估值处于历史低位,但短期技术面承压。",
"sentiment": "Bearish",
"risk_level": "Low",
"key_factors": [
"股价低于昨收,日内高点未能突破开盘价",
"成交量及换手率偏低,市场参与度低",
"PE/PB 处于历史估值低位",
"短期技术面承压但波动有限"
]
}部署初次试跑时,前端能拉到行情、AI 能正常返回分析,但响应里始终是 saved: false,Supabase 写入失败。
POST /api/analyze 200
[save] 落库失败: Error: Supabase 写入失败: Invalid path specified in request URL
at saveAnalysis (lib\supabase.ts:48:20)
错误信息很短(PostgREST 错误码 PGRST125),没有堆栈细节,光看日志无法定位问题。
我把 Invalid path specified in request URL 这条错误丢给 Claude,AI 给出的可能原因列表:
SUPABASE_URL末尾带了/或路径- 表不在
publicschema Data API没启用- 客户端版本与 publishable key 不兼容
由于无法直接读 .env.local(包含真实凭据),AI 帮我写了一段脱敏诊断脚本 scripts/check-supabase.mjs:
// 关键脱敏打印:只显示前后几位
console.log('URL :', url);
console.log('KEY :', key.slice(0, 12) + '...' + key.slice(-6));
// 逐层诊断:根路径 → 读表 → 写表
const root = await fetch(`${url}/rest/v1/`, { headers: { apikey: key, ... } });
const read = await fetch(`${url}/rest/v1/analyses?select=*&limit=1`, ...);
const write = await fetch(`${url}/rest/v1/analyses`, { method: 'POST', ... });跑一次 node --env-file=.env.local scripts/check-supabase.mjs,输出:
URL : https://rvjgpyxorigsjxzkrbhu.supabase.co/rest/v1/ ← 问题暴露!
KEY : sb_publishab...7ZzQXI (共 46 字符)
⚠️ URL 末尾不应有 /
⚠️ URL 格式可能异常,标准形如 https://abc123.supabase.co
SUPABASE_URL 末尾多了 /rest/v1/ —— 用户在 Supabase Dashboard 复制时,从 "API Docs" 页拿了完整 endpoint,而不是从 "Project URL" 字段拿。
@supabase/supabase-js 内部会自己拼 /rest/v1/<table> 路径,所以变量被覆盖成:
https://rvjgpyxorigsjxzkrbhu.supabase.co/rest/v1/ + /rest/v1/analyses
= https://.../rest/v1/rest/v1/analyses ← 路径不存在
PostgREST 返回 404 PGRST125 Invalid path specified in request URL 是正确的。
.env.local 改一行:
- SUPABASE_URL=https://rvjgpyxorigsjxzkrbhu.supabase.co/rest/v1/
+ SUPABASE_URL=https://rvjgpyxorigsjxzkrbhu.supabase.co重启 dev server 后立即跑通:
=== 3. 测试写 analyses 表 ===
POST analyses → 201 [{"id":"28ee06cd-...","symbol":"TEST",...}]
- 配置类错误用诊断脚本而不是改代码:写一段独立的连通性测试,比反复改 SDK 调用更快定位
- 脱敏打印是必须的:让 AI 看真实凭据值是错的,但「URL 长这样、Key 长这样」这种结构信息可以放心暴露
- 错误码搜文档:
PGRST125直接 grep PostgREST 源码就能确认是路径解析失败
这段排查脚本被保留在仓库的
scripts/check-supabase.mjs,以后换环境复测可以直接跑。
本项目仅作技术演示与学习用途,AI 分析结果不构成任何投资建议。