OpenStock API 与架构解析:Inngest 事件驱动后台任务与多 Provider AI 降级机制
2026/9/14 12:03:43 网站建设 项目流程

OpenStock API 与架构解析:Inngest 事件驱动后台任务与多 Provider AI 降级机制

【免费下载链接】OpenStockOpenStock is an open-source alternative to expensive market platforms. Track real-time prices, set personalized alerts, and explore detailed company insights — built openly, for everyone, forever free.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenStock

OpenStock 的 API 层采用 Inngest 构建事件驱动架构,通过多 Provider AI 策略(Gemini 主用、Siray.ai 等备用)保障生成式功能的可用性。本文基于仓库文档 API_DOCS.md 展开,并深入源码逐层剖析:如何搭建事件/Cron 触发的后台任务流水线(个性化欢迎邮件、每周市场摘要、价格预警轮询、沉睡用户召回),以及 AI 路由、Kit 邮件广播、Finnhub 行情与 MongoDB 连接细节的具体实现,读完可完整理解并本地复现这套「行情 + AI + 自动化」的集成方案。

架构总览:事件驱动 + 智能模型路由

OpenStock 的后台能力由 Inngest 支撑,核心理念是不依赖单一故障点:AI 基础设施会在主 Provider 出错或限流时自动路由到备用 Provider。官方文档给出的调用链路如下:

这条链路在源码中对应三个环节:

  1. 触发层:用户行为(如注册)通过inngest.send()发出事件,或由 Inngest Cron 定时触发;
  2. 函数层:所有后台函数集中定义在 lib/inngest/functions.ts,并通过 app/api/inngest/route.ts 中的serve()暴露给 Inngest 平台;
  3. 执行层:函数内部调用 lib/ai-provider.ts 的callAIProviderWithFallback()完成 AI 推理,最终经 Nodemailer 或 Kit 投递邮件。

Inngest 客户端在 lib/inngest/client.ts 中初始化,idopenStock,并注入了INNGEST_SIGNING_KEY(Vercel 部署时必需)与 Gemini API Key:

import {Inngest} from "inngest" export const inngest = new Inngest({ id: "openStock", ai: {gemini: {apiKey: process.env.GEMINI_API_KEY}}, // Add signing key for Vercel deployment signingKey: process.env.INNGEST_SIGNING_KEY, })

路由层则将四个函数一次性注册进 Inngest 服务:

export const { GET, POST, PUT } = serve({ client: inngest, functions: [sendSignUpEmail, sendWeeklyNewsSummary, checkStockAlerts, checkInactiveUsers], })

AI Provider 抽象层:配置、路由与降级

API_DOCS 声明的「Primary: Google Gemini / Fallback: Siray.ai」策略,其完整实现位于 lib/ai-provider.ts。该模块通过AI_PROVIDER环境变量支持三种后端,均返回纯文本字符串:

Provider环境变量默认 Base URL默认模型
gemini(默认)GEMINI_API_KEYhttps://generativelanguage.googleapis.com/v1beta/modelsgemini-2.5-flash-lite(可经GEMINI_MODEL覆盖)
minimaxMINIMAX_API_KEYhttps://api.minimax.io/v1(可覆盖)MiniMax-M2.7(可覆盖)
siraySIRAY_API_KEYhttps://api.siray.ai/v1siray-1.0-ultra(固定)

配置解析集中在getProviderConfig()(lib/ai-provider.ts#L24-L60),优先级为「显式传参 >AI_PROVIDER环境变量 > 默认值gemini」。

降级策略getFallbackProviderName()(lib/ai-provider.ts#L66-L76)决定:从当前源码结构看,若主 Provider 是 Gemini,则优先选择 MiniMax(当其 Key 可用),其次 Siray;若主 Provider 非 Gemini,则回退到 Gemini。这与 API_DOCS 中「Gemini 失败即切 Siray.ai」的早期描述相比有所演进,以源码为准。

降级调用的入口是callAIProviderWithFallback()(lib/ai-provider.ts#L168-L184):先尝试主 Provider,抛出异常后打印告警并切换备用 Provider 重试一次。底层两种调用协议分别是:

  • callGemini():POST 到{baseUrl}/{model}:generateContent?key=...,从candidates[0].content.parts[0].text提取文本;
  • callOpenAICompatible():MiniMax 与 Siray 共用 OpenAI 兼容协议,POST{baseUrl}/chat/completions,Bearer 鉴权,temperature: 0.7,从choices[0].message.content提取文本。

任一 Provider 缺少 Key 或返回空响应都会抛错,从而触发上层的降级或兜底逻辑——这正是「Zero Downtime Guarantee」的实现基础:即使两级 Provider 全部失败,业务函数仍会用硬编码的兜底文案继续执行(见下文各函数分析)。相关行为亦有测试覆盖,可参考tests/ai-provider.test.ts 与tests/ai-provider.integration.test.ts。

Inngest 后台任务清单

四个后台函数的 ID、触发方式与用途如下(引自 API_DOCS.md,触发器细节已对照 lib/inngest/functions.ts 源码核实):

ID类型Schedule/TriggerPurpose
sign-up-emailEventapp/user.created个性化 Onboarding。基于用户注册问卷结果生成定制欢迎信息
weekly-news-summaryCron0 9 * * 1(每周一 9:00)市场情报。汇总最新财经新闻并通过 Kit 广播给全部用户
check-stock-alertsCron*/5 * * * *(每 5 分钟)实时监控。将用户价格目标与实时行情比对
check-inactive-usersCron0 10 * * *(每天 10:00)用户召回。识别沉睡用户(>30 天)并发送回归提醒

sign-up-email:注册事件的个性化欢迎邮件

触发链路始于认证动作:lib/actions/auth.actions.ts#L14-L17 中,注册成功后调用inngest.send()发出app/user.created事件,携带countryinvestmentGoalsriskTolerancepreferredIndustry四份问卷数据;事件发送失败不会导致注册失败(仅记录日志),实现了前端流程与后台异步解耦。

函数本体(lib/inngest/functions.ts#L10-L53)分两步执行:

  1. generate-welcome-intro:将问卷数据填入PERSONALIZED_WELCOME_EMAIL_PROMPT模板(定义于 lib/inngest/prompts.ts),经callAIProviderWithFallback()生成两句话、35–50 词的个性化 HTML 段落;若所有 Provider 均失败,回退为固定文案"Thanks for joining Openstock...",保证邮件必然发出;
  2. send-welcome-email:调用 lib/nodemailer/index.ts 的sendWelcomeEmail(),把 AI 文本填入{{intro}}占位符后经 Gmail SMTP 发送。

该模块的容错设计值得注意:Nodemailer 在未配置NODEMAILER_EMAIL/NODEMAILER_PASSWORD时不创建 transporter,发送函数直接返回{ status: 'skipped' }而非抛错,本地开发无需邮箱凭据也不会中断工作流。

weekly-news-summary:Cron 驱动的市场新闻广播

weekly-news-summary(lib/inngest/functions.ts#L56-L203)同时具备 Cron(0 9 * * 1)与手动事件(app/send.weekly.news)两类触发器。其流程为:

  1. 拉取新闻fetch-general-news步骤调用 Finnhub 的getNews()(lib/actions/finnhub.actions.ts)并截取前 10 条;无新闻则提前返回;
  2. 生成摘要:将新闻 JSON 注入NEWS_SUMMARY_EMAIL_PROMPT,并将提示词中的 "daily" 批量替换为 "weekly",再经 AI 降级链生成结构化 HTML(含h3分节、要点列表、Bottom Line 解读等约定格式,见 lib/inngest/prompts.ts#L50-L99);全部 Provider 失败时回退为"Market is moving. Log in to see more."
  3. Kit 广播send-kit-broadcast步骤先调用kit.listSubscribers()拉取订阅者并在 Inngest 日志中输出收件人清单(便于排障),随后以内联样式 HTML(黑色背景 +#20c997青色强调色)封装整封邮件,最后调用kit.sendBroadcast(subject, content)立即投递。

check-stock-alerts:5 分钟轮询的价格预警

该函数(lib/inngest/functions.ts#L205-L294)每 5 分钟执行一次,完整链路为:

  1. 取有效预警:从 MongoDB 查询Alert集合(模型见 database/models/alert.model.ts),条件为active: true && triggered: false && expiresAt > now
  2. 批量拉价:按 symbol 去重后逐个调用 Finnhub 的getQuote(),构造priceMap,单只标的失败不影响其余标的;
  3. 条件判定ABOVE条件在currentPrice >= targetPrice时触发,BELOWcurrentPrice <= targetPrice时触发;
  4. 落库标记:触发的预警被Alert.findByIdAndUpdate()更新为triggered: true, active: false,防止重复触发。

从源码结构看,当前触发后的通知处理以日志记录为主(ALERT FIRED),注释表明后续可接入 Kit 定向推送,即检测逻辑已完备、通知渠道仍可演进。

check-inactive-users:沉睡用户召回

check-inactive-users(lib/inngest/functions.ts#L296-L466)每天 10:00 运行,直接操作user集合。查询条件包含双重防打扰设计:

  • 活跃判定:lastActiveAt早于 30 天前,或无lastActiveAtcreatedAt早于 30 天前(该字段由登录时更新,见 lib/actions/auth.actions.ts#L44-L47);
  • 防重复:lastReengagementSentAt不存在或同样早于 30 天前才纳入名单,且每轮最多处理 50 人;
  • 发送后回写lastReengagementSentAt,形成闭环避免重复触达。

API 集成细节

Stock Data:Finnub / Finnhub

  • Base URLhttps://finnhub.io/api/v1(对应FINNHUB_BASE_URL
  • 核心能力:实时报价、技术指标、市场新闻,支撑股票搜索、公司资料与新闻流
  • 鉴权NEXT_PUBLIC_FINNHUB_API_KEY

在架构中的落点:check-stock-alerts依赖getQuote()取最新价(返回体字段c即现价),weekly-news-summary依赖getNews()获取新闻,实现均位于 lib/actions/finnhub.actions.ts。注意NEXT_PUBLIC_前缀意味着该 Key 会暴露给浏览器,且免费套餐可能存在报价延迟与速率限制(详见 MARKET_SUPPORT.md)。

Email & Marketing:Kit(ConvertKit)

  • 角色:高容量用户广播与标签管理
  • 鉴权KIT_API_KEY+KIT_API_SECRET,缺失时任一 Kit 调用都会抛错(见 lib/kit.ts#L8-L17)

API_DOCS 列出的关键端点为POST /v3/tags/{tag_id}/subscribe(用户迁移)与POST /v3/broadcasts(新闻简报)。对照当前源码 lib/kit.ts 的实际实现:

  • kit.addSubscriber()POST /v3/forms/{formId}/subscribe,表单 ID 取自KIT_WELCOME_FORM_ID环境变量,未配置时跳过并告警;
  • kit.sendBroadcast()POST /v3/broadcastspublic: true立即发送,send_at设定在 1 分钟后以确保处理;特别处理「发件地址未确认时广播被存为草稿」的 API 返回,将其降级为警告而非失败;
  • kit.listSubscribers()GET /v3/subscribers,仅用于验证与日志输出。

一个值得留意的工程取舍:Kit 的 Broadcast API 面向群发,做 1:1 事务邮件并不标准,因此召回邮件步骤中对单用户投递采用了受限的替代方案(见函数内注释说明),这属于该集成的已知边界。

Database:MongoDB Atlas

  • 连接:标准 URI。文档强调「DNS SRV bypassed for maximum reliability」——其实现位于 database/mongoose.ts#L5-L16:在建立连接前显式设置dns.setServers(['8.8.8.8'])dns.setDefaultResultOrder('ipv4first'),并在mongoose.connect()中指定family: 4bufferCommands: false,以规避 Node 17+ 下querySrv的 IPv6 连接被拒问题
  • 集合users(Better Auth 的user集合,含lastActiveAtlastReengagementSentAt等运营字段)、watchlists(模型见 database/models/watchlist.model.ts)、alerts(模型见 database/models/alert.model.ts)

连接采用global.mongooseCache缓存单例模式(database/mongoose.ts#L31-L52),在 Next.js 热重载场景下避免重复建连。本地可用pnpm test:db(对应 scripts/test-db.mjs)验证连通性。

本地运行与验证

要完整跑通上述 API 与自动化链路,前置条件与步骤如下(依据 README.md Quick Start):

  1. 配置.env:至少包含MONGODB_URIBETTER_AUTH_SECRETNEXT_PUBLIC_FINNHUB_API_KEYGEMINI_API_KEY;启用 Kit 广播需补充KIT_API_KEY/KIT_API_SECRET;启用 Inngest 云部署需INNGEST_SIGNING_KEY;启用个性化邮件需NODEMAILER_EMAIL/NODEMAILER_PASSWORDAI_PROVIDER默认gemini,可切换为minimaxsiray
  2. 启动应用:pnpm dev(Next.js + Turbopack);
  3. 本地启动 Inngest(承载事件、Cron 与 AI 流程):
npx inngest-cli@latest dev
  1. 验证数据库连通:pnpm test:db
  2. 验证路径:注册新用户可触发app/user.created→ 观察欢迎邮件流程;通过 Inngest 控制台手动触发app/send.weekly.news可提前演练每周摘要;Cron 类函数在本地 dev 环境下按各自周期自动运行。

小结

OpenStock 的 API 层以 lib/inngest/functions.ts 中的四个函数为骨架,把「用户行为 / 定时任务 → AI 生成 → 邮件投递」串成可重试、可观测的事件流水线;lib/ai-provider.ts 的多 Provider 降级与业务层的兜底文案共同构成其可用性保障;Finnhub 提供行情与新闻数据,Kit 承担大规模广播,MongoDB(含 DNS/SRV 处理)提供持久化。各组件的环境变量与端点均以当前仓库源码为准,可对照本文列出的文件路径逐层核查。

【免费下载链接】OpenStockOpenStock is an open-source alternative to expensive market platforms. Track real-time prices, set personalized alerts, and explore detailed company insights — built openly, for everyone, forever free.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenStock

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询