前言:为什么要干掉之前的 Go + n8n + Hugo 方案?

之前为了分析个人智能日记和个人资产,我曾折腾过一套看似完美的方案(参见前作 《从零构建全自动 Go+n8n+Hugo 个人智能日记分析系统》)。那套方案在本地跑得挺欢,但时间一长,痛点也暴露无遗:

  1. 服务器开销与维护:VPS 需要持续续费,n8n 工作流在 Docker 容器里偶尔会因为内存占用过高而挂掉。
  2. 链路太长:Go 抓数据、n8n 编排、Hugo 渲染静态页面,最后再发布。任何一个环节的网络波动都会导致当天的任务中断。
  3. 数据隐私问题:第三方工作流平台托管敏感数据始终让人觉得不够安全。

为了实现零服务器成本、免维护、数据绝对安全且完全掌握在自己手里的目标,我用 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分钟冷却锁。防止双击、浏览器超时自动重发,但也保留了手动调优后重跑的弹性。
  • 瞬时并行防刷锁
    • 只要有进程启动,系统会立即更新 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 中引入了全新的 “多模型高可用切换链路”

  1. 引入全新 Gemini 3.5 / 3.1 家族:加入了最新的旗舰模型 gemini-3.5-flash 和极速轻量模型 gemini-3.1-flash-lite
  2. 容灾降级模型链(Model Chain):如果用户首选的模型(例如 gemini-3.5-flash)繁忙,系统会自动捕获 503 异常,并依次向下尝试 gemini-3.1-flash-litegemini-2.5-flash 甚至是极稳的经典款 gemini-1.5-flash
  3. 针对 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 页面:

  1. GitHub 归档:建立 GitHub PUT 请求,将 HTML 页面保存归档至 website/template/YYYY-MM-DD.html
  2. 邮件自动送达:通过 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 数据中枢也能拥有工业级运行质感。

首页

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐