干掉复杂工作流!我用 Next.js + GitHub + Gemini 打造了一个全自动个人 AI 报告终端
前言:为什么要干掉之前的 Go + n8n + Hugo 方案?
之前为了分析个人智能日记和个人资产,我曾折腾过一套看似完美的方案(参见前作 《从零构建全自动 Go+n8n+Hugo 个人智能日记分析系统》)。那套方案在本地跑得挺欢,但时间一长,痛点也暴露无遗:
- 服务器开销与维护:VPS 需要持续续费,n8n 工作流在 Docker 容器里偶尔会因为内存占用过高而挂掉。
- 链路太长:Go 抓数据、n8n 编排、Hugo 渲染静态页面,最后再发布。任何一个环节的网络波动都会导致当天的任务中断。
- 数据隐私问题:第三方工作流平台托管敏感数据始终让人觉得不够安全。
为了实现零服务器成本、免维护、数据绝对安全且完全掌握在自己手里的目标,我用 Next.js + GitHub API + Gemini 构建了这套全新的个人 AI 灵魂与资产分析终端:MarsMind-Zone。
整个系统完全部署在 Vercel 上(薅免费额度),数据全部通过 API 读写自己的私有 GitHub 仓库,真正做到了“无服务器自运转”。
一、 系统架构设计:让 Serverless 保持纯粹的“无状态”
MarsMind-Zone 的核心设计理念是**“数据流编排(Orchestration)与状态外部化(Externalization)”**。
为了不花一分钱买数据库,我直接把私有 GitHub 仓库当作了系统的“外部状态机”。系统的核心拓扑如下:
┌──────────────────────────┐
│ MarsMind Web UI │
└────────────┬─────────────┘
│ HTTP API
▼
┌──────────────────────────┐
│ Next.js Route Handler │◄────── Cron Trigger (外部触发)
└────────────┬─────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Postgres DB │ │ Target HTTP │ │ GitHub API │
│ (日记数据) │ │ (资产/账单) │ │ (脚本/配置) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└─────────────────────────┼───────────────────────────┘
│ 提取数据 (Stitched)
▼
┌─────────────────────┐
│ Gemini AI API │
└──────────┬──────────┘
│ 生成 HTML 报告
▼
┌───────────────────────┐
│ 备份与归档 (GitHub) │
│ 邮件送达 (SMTP Server)│
└───────────────────────┘
1. 状态存储与配置中心化
系统运行所需的核心参数(如 API 密钥、数据库连接串、邮件授权码、自定义步骤配置等)都存储在我的私有仓库 yyszone/backup_server 下的 website/config.json 中。
- 零本地状态:部署在 Vercel 上的 Next.js 服务保持完全的 Stateless,哪怕 Vercel 实例频繁冷启动也毫无影响。
- 私密性保障:敏感数据不会硬编码在前端,仅在用户输入正确的安全密码并校验后,才在服务端通过客户端传入的 Header(
x-access-password等)进行获取或拉取。
2. 多源异构数据抓取引擎
系统支持三类常见的数据源读取,允许用户在控制面板任意组合、开关(Enabled/Disabled)或添加步骤:
- GitHub File:通过 API 抓取指定仓库(如自动化测试脚本仓库)下的源码或配置文件,交给 AI 进行分析。
- HTTP Request (网页爬取):直接请求特定的网页(如理财记账页),系统支持提取整个页面净化后的纯文本,也支持通过特定的 Script ID 选择器精准截取内嵌的 JSON 数据块。
- Postgres Query:允许执行原生 SQL,支持
{{todayDate}}动态变量替换(自动映射为北京时间的今日日期),适用于拉取每日增量数据(如个人生活日记)。
二、 关键技术实现:那些在开发中踩过的坑
1. 极其严密的“多重并发与防重放锁”
在自动任务(Cron)和手动触发场景下,由于外部调度器偶尔会重试,或者用户不小心在前端“双击”,极易导致 AI API 被重复调用,从而造成 Token 账单暴涨或重复生成两份一模一样的报告。
为此,我在 lib/workflow.ts 中设计了**“双锁防线”**:
- 双层金库防重放锁:
- 自动调度任务 (Cron):实行自然天锁。今天只要成功运行过一次(通过判断
LAST_SUCCESS_TIME的日期前缀),哪怕外部 Cron 触发再多次也一律瞬间拦截。 - 手动任务 (Manual):实行 30分钟冷却锁。防止双击、浏览器超时自动重发,但也保留了手动调优后重跑的弹性。
- 自动调度任务 (Cron):实行自然天锁。今天只要成功运行过一次(通过判断
- 瞬时并行防刷锁:
- 只要有进程启动,系统会立即更新
LAST_RUN_START_TIME为当前北京时间并写回 GitHub。如果 3 分钟内有其他请求进来,直接拦截,防止并发拥堵。
- 只要有进程启动,系统会立即更新
2. 北京时区时间对齐:解决 Serverless 平台的“时差痛点”
在 Vercel 等 Serverless 平台上部署时,系统时区默认是 UTC 0。如果直接使用 new Date().getDate(),在北京时间晚上 8 点前,系统抓到的“今天”其实都是“昨天”。
为了让定时器准确地按照北京时间(UTC+8)运行,在 /api/cron/route.ts 中,我采用了一种高兼容性的时间处理逻辑:
const bjNow = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
const year = bjNow.getFullYear();
const month = String(bjNow.getMonth() + 1).padStart(2, '0');
const day = String(bjNow.getDate()).padStart(2, '0');
const currentHourNum = bjNow.getHours();
const currentMinuteNum = bjNow.getMinutes();
通过将当前时间转换为上海时区,再计算出当日起始分钟绝对值与控制面板中配置的 SCHEDULED_HOURS(如 08:00,12:30)进行比对。配合外部免费的 cron-job 平台,每隔 15 分钟触发一次 /api/cron,就能实现误差极低的主动补跑和定时运行。
3. Vercel 10秒超时限制:SSH 命令分发的避坑实践
为了操控内网穿透的本地远程服务器(如控制下载机挂机),系统在 app/tools 页面集成了一个轻量级 SSH2 终端。
这里遇到了 Vercel Free 平台最大的限制:HTTP 响应必须在 10 秒内结束。
- 非阻塞追加设计:点击运行后,控制台不等待后台,直接追加当前正在执行的命令回显,避免页面卡死。
- 超时优雅清洗:由于执行下载脚本(如 Python 爬虫)属于长耗时任务,如果在 SSH 中同步等待必定超时报错。系统做了特化提示,引导用户在命令末尾加上
&和nohup,使其脱离当前 Shell 转为后台守护进程运行。
4. 重点升级:AI 繁忙(503/High Demand)自动降级与模型链切换
在实际运行中,使用 Gemini 免费 Key 经常会遇到以下这种令人崩溃的报错信息:
[失败] Gemini 响应异常: This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.
在自动化 Cron 运行时,如果遇到 API 限制而直接失败,会使整个自动化链路中断。为此,系统在 lib/workflow.ts 中引入了全新的 “多模型高可用切换链路”:
- 引入全新 Gemini 3.5 / 3.1 家族:加入了最新的旗舰模型
gemini-3.5-flash和极速轻量模型gemini-3.1-flash-lite。 - 容灾降级模型链(Model Chain):如果用户首选的模型(例如
gemini-3.5-flash)繁忙,系统会自动捕获 503 异常,并依次向下尝试gemini-3.1-flash-lite、gemini-2.5-flash甚至是极稳的经典款gemini-1.5-flash。 - 针对 Serverless 优化等待时间:为了防止 Vercel 10s 超时,我们在遇到高负载报错时,采用**“短延时(3秒)+ 快速切换备选”**的策略,完美避开了平台的执行时限。
核心重试逻辑代码如下:
const selectedModel = config.SELECTED_MODEL || 'gemini-3.5-flash';
// 💡 定义备选模型降级链路(从新 3.5 依次向下兼容)
const modelChain = [
selectedModel,
'gemini-3.5-flash',
'gemini-3.1-flash-lite',
'gemini-2.5-flash',
'gemini-1.5-flash'
].filter((value, index, self) => self.indexOf(value) === index); // 数组去重
let generatedHtml = '';
let apiSuccess = false;
let lastError = '';
// 💡 循环轮询备选链路
for (let i = 0; i < modelChain.length; i++) {
const currentModel = modelChain[i];
await saveCronLog(`[分析] 尝试呼叫 AI 模型 (${currentModel})...`, token);
try {
const geminiRes = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${currentModel}:generateContent?key=${config.GEMINI_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
signal: AbortSignal.timeout ? AbortSignal.timeout(25000) : undefined // 防止挂起超时
}
);
const geminiData = await geminiRes.json();
if (geminiRes.status === 200) {
generatedHtml = geminiData.candidates?.[0]?.content?.parts?.[0]?.text || '';
if (generatedHtml) {
apiSuccess = true;
await saveCronLog(`[分析] AI 响应成功!当前使用模型: ${currentModel}`, token);
break; // 成功获取,跳出重试链路
}
}
const errText = geminiData.error?.message || '未知错误';
lastError = `[HTTP ${geminiRes.status}] ${errText}`;
// 如果是高负载繁忙错误 (503 / high demand)
if (geminiRes.status === 503 || errText.includes('high demand') || errText.includes('temporary')) {
const waitSeconds = 3; // 托管在 Vercel 免费版,建议设为 3s 快速重试;自建 VPS 可设为 30s
await saveCronLog(`[繁忙] 模型 ${currentModel} 负载过高。等待 ${waitSeconds} 秒后尝试下一备选模型...`, token);
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
} else {
await saveCronLog(`[失败] 模型 ${currentModel} 响应异常: ${errText}。正在无缝尝试下一备选模型...`, token);
}
} catch (err: any) {
lastError = err.message || '网络请求超时';
await saveCronLog(`[网络异常] 连接 ${currentModel} 失败: ${lastError}。尝试下一备用模型...`, token);
}
}
if (!apiSuccess) {
return { success: false, error: `所有备选 AI 模型均调用失败。最后一次错误: ${lastError}` };
}
三、 数据大一统与 AI 整合
当各节点的数据(日记数据、理财 API 的 JSON、测试脚本代码等)读取完毕后,系统通过 PromptBlueprint 组件和 lib/workflow.ts 将数据流和该步骤专属的 AI 分析指令组装成完整的 Master Prompt。
最终,Gemini 将处理好的异构内容融合成一个可以直接用浏览器打开的精美 HTML 页面:
- GitHub 归档:建立 GitHub PUT 请求,将 HTML 页面保存归档至
website/template/YYYY-MM-DD.html。 - 邮件自动送达:通过
nodemailer安全地投递至用户的目标邮箱。每天清晨醒来,邮箱里已经躺着一份排版精美的“个人昨日数据深度交叉分析周报”了。
四、 附录:如何优雅地管理与修改本站 Git 版本号?
在 MarsMind-Zone 主页底部的悬浮卡片中,会展示当前系统的版本号。根据 /app/api/git-info/route.ts 里的逻辑,版本号采用的是**“Git Tag 优先”**的动态拉取设计。
如果您在后续开发中想要修改这个版本号,有以下三种方案:
方案 A:通过发布新的 Git Tag 来修改版本号(强烈推荐)
这是最优雅的标准做法,无需修改任何一行项目代码,直接通过 Git 命令行打标即可。
在您的本地 marsmind 项目根目录下,执行以下命令:
# 1. 确保您的代码是最新的,并已推送到主分支
git checkout main
git pull origin main
# 2. 创建一个带附注的本地 Tag(例如升级为 v1.2.0)
git tag -a v1.2.0 -m "Release version 1.2.0: Integrated Gemini 3.5 & Model Fallback Chain"
# 3. 将新的 Tag 推送到 GitHub 远程仓库
git push origin v1.2.0
推送成功后,刷新网页,API 接口便会动态显示最新版本:Version v1.2.0。
方案 B:修改代码中的降级默认版本号 (Fallback Version)
如果在未联网或者本地离线测试时,想要修改默认底噪版本号,可以打开 /home/yys/marsmind/app/api/git-info/route.ts,将第 15 行修改为您期望的值:
// 原代码:
let version = '0.1.0'; // 默认底噪版本
// 修改后:
let version = '1.2.0'; // 默认底噪版本
方案 C:修改标准的 npm 包版本号
为了符合标准的软件工程规范,您还可以更新项目配置文件 package.json 中的版本:
打开 /home/yys/marsmind/package.json,修改 version 属性:
{
"name": "marsmind",
"version": "1.2.0",
...
}
修改完成后,进行正常的 git commit 并推送即可。
五、 项目总结
MarsMind-Zone 展示了如何巧妙地将无状态的前端服务、外部 GitHub 配置管理与 Gemini 自动化重试模型链融为一体。它不仅帮我省去了 VPS 维护服务器的烦恼,而且在接口健壮度上,通过多重并发锁和高可用切换策略,极大地降低了调用失败率,让个人专属的 AI 数据中枢也能拥有工业级运行质感。

更多推荐

所有评论(0)