MiroFish SaaS 实例部署逻辑梳理

基于当前 my_mirofish 最新代码整理。本文只记录部署逻辑、代码入口、状态流转和运维边界; 涉及密钥的地方只写环境变量名,不记录任何真实值。

代码提交fca5b4a
分支main / origin/main
文档日期2026-05-20
范围新 SaaS 实例创建与发布链路

一眼结论

MiroFish 的 SaaS 实例部署被拆成两条链路:用户购买后的“实例创建链路”和工程发布时的“平台更新链路”。 用户购买后,Cloudflare Pages Functions 负责创建订单、确认 Creem 支付、写 D1 状态,并调用 206 Runtime Server; Runtime Server 负责真正落地一个隔离实例,包括解压模板包、写实例环境变量、打补丁、分配端口、启动前后端、生成 nginx 路由、返回控制台 URL。

1. 选套餐前端调用 /api/launch-checkout 创建 Creem checkout 和 D1 订单。
2. 支付完成成功页或 webhook 调 confirm/provision,把订单标为 paid。
3. 建实例记录D1 写入 mf_instances,状态先置为 creating
4. 调 RuntimePages Function 用 MIROFISH_RUNTIME_API_TOKEN 请求 Runtime POST /instances
5. 落地实例Runtime 解模板、写 .env、补丁、启动、健康检查、写 nginx。
6. 回写可访问地址Runtime 返回 console_url、端口、路径,D1 状态变为 running 或 failed。

系统边界

Cloudflare Pages 层

承载官网、checkout、dashboard、console 页面和 functions/api/*。它不直接创建系统进程,只负责订单、支付确认、D1 状态和调用 Runtime。

206 Runtime 层

runtime-server/server.mjs 是实例编排器。它持有平台 LLM / Zep / Graphiti 配置,给实例注入代理 token,并在机器上创建真实工作目录和进程。

实例模板层

模板来自 MiroFish-Core,生产机器上维护 mirofish-template-current.tar.gz。新实例永远从当前模板解压,旧实例不会自动获得模板更新。

关键分工:Pages Functions 管“业务状态”,Runtime Server 管“真实资源”。这能避免把 SSH、系统进程、nginx 写入等高权限动作放进 Cloudflare Worker。

购买到创建实例的完整链路

  1. 前端发起 checkout。 functions/api/launch-checkout.js 解析套餐,当前套餐有 starterproenterprise; 也支持 analysis_pack_5 这种追加分析次数包。未登录用户会生成 guest token 并写 cookie。
  2. 创建 Creem 商品和 checkout。 如果环境里没有配置固定的 Creem product id,函数会通过 Creem API 临时创建 product,再创建 checkout。 checkout 的 request_id 和 metadata 都会写订单 ID,用于回调核对。
  3. 写入订单。 D1 表 mf_orders 写入 pending 订单,保存 plan_id、金额、customer email、guest/user 归属、creem_checkout_idtest_mode 等。
  4. 支付确认。 成功页会调 functions/api/checkout/creem-confirm.js;异步回调会进 functions/api/webhooks/creem.js。 两者最终都进入 provisionPaidOrder(),并先通过 Creem checkout 状态或 webhook 签名确认支付可信。
  5. 创建或复用实例记录。 provisionPaidOrder() 把订单标记为 paid。如果已有 running / creating / queued 实例,会幂等返回;否则生成 instance id,插入或更新 mf_instances.status = creating
  6. 调用 Runtime 创建实例。 callRuntimeCreateInstance() 请求 POST /instances,body 包含 instanceIdorderIdplanIdbillingStartedAtenv=prodtestModeforce
  7. 回写结果。 Runtime 成功后返回 metadata,updateInstanceFromRuntime() 回写 console_urlhost、端口、service 名、workspace 路径、template 版本等。失败则把实例置为 failed 并记录截断后的错误。

Runtime 创建实例的内部动作

Runtime 的入口在 runtime-server/server.mjs,授权后 POST /instances 进入 deployInstance()。 它的核心动作如下:

阶段 函数/逻辑 具体动作 失败行为
配置读取 getRuntimeConfig() 读取 base dir、模板路径、实例域名、nginx 配置目录、端口范围、LLM/Zep/Graphiti 配置、部署超时。 缺 token 时受保护接口不可用;缺平台 key 时实例 env 渲染会失败。
幂等检查 deployInstance() 若实例 metadata 已是 running,直接返回 idempotent: true;若已有失败实例且 force,先停止并删除旧工作目录。 已有非 running 实例且未 force 会拒绝覆盖。
端口分配 allocatePorts() 扫描现有 metadata,避开已用 backend/frontend 端口,再检查端口可监听。 端口段耗尽会抛错,实例保持 failed。
工作区准备 prepareWorkspace() 创建 mf-<id> 工作目录、data/uploads、memory、tasks、logs、config,解压当前模板到 app 目标目录已存在会拒绝,避免覆盖用户实例。
环境注入 renderInstanceEnv() 生成实例专属 INSTANCE_TOKEN,写入 LLM/Zep/Graphiti 配置、模型名、graph scope、端口、Secret key 和 OASIS 轮次。 平台 LLM key、Zep key 或 Graphiti service URL 缺失时按当前 provider 要求抛错。
模板补丁 patchInstanceTemplate() 补前端 API base URL、额度路由刷新、模拟时间线顺序、Zep/Graphiti provider、模拟生命周期、Graph API 兼容和额度计数。 补丁尽量按锚点替换;上游结构变动时部分补丁会跳过,需用测试发现。
启动与验证 startInstance() 执行模板内 scripts/start.sh,再跑 healthcheck.shsmoke-test.sh 任一检查失败会 stop 实例、写 failed metadata、抛错给 Pages Function。
路由接入 writeNginxConfig() mf-<id>.<instanceDomain> 写 nginx server block,代理到实例 frontend port,并 reload nginx。 nginx reload 失败被忽略,但路由可能不可用,需要发布验证捕捉。

每个实例被写入什么

metadata.json

Runtime 会先写 status=creating,启动通过后改成 running。关键字段包括:

  • idorder_idplan_id
  • monthly_analysis_limit:Starter 5、Pro 15、Enterprise 20;testMode 为 2
  • backend_portfrontend_portservice_name
  • workspace_pathconsole_urlgraph_provider
  • secrets 里只保存脱敏后的平台 key 状态

.env

实例 .env 是隔离的。真实平台 key 不直接写入实例;在 proxy mode 下,实例拿到的是 INSTANCE_TOKEN,再通过 Runtime 转发。

  • LLM_BASE_URL=http://127.0.0.1:<runtime-port>/v1
  • ZEP_BASE_URL=http://127.0.0.1:<runtime-port>/zep,仅 Zep provider 需要
  • ZEP_GRAPH_PREFIX=mf_<instance>,用于图谱隔离
  • GRAPH_PROVIDERGRAPHITI_MODEGRAPHITI_SERVICE_URL
  • FLASK_PORTOASIS_DEFAULT_MAX_ROUNDS
当前架构里,实例之间主要靠独立目录、独立端口、独立 token、graph prefix 和 nginx 子域名隔离。模板更新只影响新实例,已经创建的实例需要单独补丁、重启或重建。

数据表和状态流

职责 部署相关字段
mf_orders 订单、支付和履约状态。 payment_statuscreem_checkout_idpaid_attest_modeproduct_typetarget_instance_idcredit_amountfulfillment_status
mf_instances 实例业务视图和 Runtime 回写结果。 statusconsole_urlbackend_portfrontend_portservice_nameworkspace_pathruntime_instance_idtemplate_version
mf_instance_claims 用于把订单/实例领取到账号,支持 guest 购买后登录绑定。 order_idinstance_idclaim_token_hashexpires_at
mf_users / mf_sessions 账号和会话。 dashboard、console 和 analysis pack 购买时用来判断实例归属。

状态流

正常状态为 pending order -> paid order -> instance creating -> instance running。 Runtime 创建失败时进入 failed,错误写入 mf_instances.error_message;删除接口会先把 D1 状态标为 stopped,再尽力调用 Runtime 删除真实工作区。

额度和次数包逻辑

每个实例有月度分析额度。额度在 Runtime 的实例目录里以 usage.json 记录,而不是存在 Cloudflare D1。 Dashboard 会通过 Pages Function 转调 Runtime 的 GET /instances/:id/quota 获取实时额度。

动作 触发点 实现
月度额度判断 Graph 构建或 Zep 写入路径 checkAndRecordAnalysis() 按 billing anchor 计算周期,使用 graph id 去重,额度不足时返回 429。
Graphiti provider 计数 实例后端 /api/graph/build Runtime 在模板补丁里给 graph.py 注入 record_analysis_usage(project_id),避免绕过 Zep proxy 后不计数。
追加次数包 analysis_pack_5 支付成功 Pages Function 调 Runtime POST /instances/:id/quota/credits,Runtime 对 orderId 做幂等并增加 extra_analysis_credits

Graphiti / Zep / LLM 代理逻辑

Runtime 同时承担平台能力代理和实例隔离职责。实例里不直接持有平台 LLM key;它用 INSTANCE_TOKEN 请求 Runtime 的 /v1/*,Runtime 校验 token 后再带平台 key 请求上游 OpenAI-compatible LLM。

如果使用 Zep provider,实例通过 Runtime 的 /zep/* 代理访问上游 Zep。代理会拦截 body 和 URL path 中的 graph_id, 要求它必须以该实例自己的 mf_<instance>_ scope 开头,避免跨实例访问图谱。

如果使用 Graphiti provider,Runtime 给实例写入 GRAPH_PROVIDER=graphitiGRAPHITI_SERVICE_URL。 独立的 graphiti-service 负责 Graphiti API 兼容、ontology 正规化、Neo4j/FalkorDB 后端和 LLM/Embedding 调用。

当前最新提交集中在 Graphiti ontology payload 兼容:Graphiti service 支持 legacy payload、PUT ontology 更新,以及 OpenAI-compatible JSON schema 结构化调用适配。

发布链路:更新平台本身

工程发布分三类,不等于创建一个用户实例。

发布目标 命令 作用 注意
预检查 npm run release:verify 检查 my_mirofish 和 sibling MiroFish-Core 的变更,判断需要发布 Pages、Runtime 还是模板,并跑 Runtime 测试。 只给建议,不部署。
Cloudflare Pages npm run release:pages git archive HEAD 发布已提交代码到 Pages,并检查自定义域 dashboard / analysis pack checkout。 未提交改动不会被发布,这是有意设计,防止把临时文件带上生产。
206 Runtime npm run release:206 -- --runtime 上传 runtime-server/server.mjs,远端 node --check,备份旧文件,重启 mirofish-runtime,检查 health。 影响新创建、重启、删除和额度等 Runtime 接口;不自动改旧实例代码。
206 模板 npm run release:206 -- --template MiroFish-Core 打补丁包,远端基于当前 Linux 模板解压叠加、py_compile、重打 mirofish-template-current.tar.gz symlink。 只影响以后新建的实例。旧实例不自动更新。
生产 smoke test npm run release:prod:verify 检查 Pages deployment、自定义域页面、analysis pack 入口、Runtime health、模板 symlink 和模板关键文件。 验证的是发布结果,不会修复失败。

关键保护机制

已经做了的保护

  • 幂等已运行实例重复 provision 会直接返回,不重复创建。
  • 不覆盖工作区已存在时默认拒绝,只有 failed 实例且 force 才会清理重建。
  • token 代理实例只拿 INSTANCE_TOKEN,平台 key 留在 Runtime。
  • graph scopeZep 路由检查 graph id 前缀,防止跨实例图谱访问。
  • 路径保护ensureInside() 防止工作区路径逃逸。
  • 发布隔离Pages 发布只用 committed HEAD,模板更新基于远端 Linux 模板叠加。

仍需人工注意

  • 旧实例模板更新不会自动补旧实例。
  • nginx写配置后 reload 失败被吞掉,需要外部验证子域名访问。
  • 资源每个实例复制完整后端 venv,磁盘占用增长快。
  • 进程手工 kill 父进程可能连带影响多个实例,需要先看进程树。
  • PagesGit 自动部署可能覆盖手工 wrangler 部署,因此生产代码必须先提交推送。

代码入口对照

文件 入口 职责
functions/api/launch-checkout.js onRequest()createCreemCheckout() 解析套餐、创建 Creem checkout、写 mf_orders pending 订单。
functions/api/checkout/creem-confirm.js onRequest() 成功页主动确认支付,触发实例 provision。
functions/api/webhooks/creem.js onRequest() 验证 Creem webhook 签名,异步触发实例 provision。
functions/_shared/mirofish.js ensureSchema()provisionPaidOrder()callRuntimeCreateInstance() D1 schema、订单支付状态、实例记录、Runtime 调用、错误回写。
functions/api/console-data.js reconcileOrderPayment() Console 查询订单/实例时会顺手 reconcile 已完成但未履约的支付。
functions/api/dashboard-data.js fetchRuntimeQuota() 列出用户订单和实例,并拉 Runtime quota 给 dashboard。
runtime-server/server.mjs deployInstance()prepareWorkspace()renderInstanceEnv() 真实实例编排、模板解压、环境注入、补丁、启动、nginx、quota、LLM/Zep proxy。
graphiti-service/graphiti_service/main.py /graphs/* FastAPI 路由 Graphiti provider 的内部图谱服务,处理 ontology、episode、node/edge/search。
scripts/deploy-pages-prod.sh npm run release:pages 发布 Cloudflare Pages 并做自定义域 smoke test。
scripts/deploy-206-template-runtime.sh npm run release:206 -- --template/--runtime 发布 206 Runtime 或实例模板。
scripts/verify-prod.sh npm run release:prod:verify 发布后检查 Pages、custom domain、Runtime health 和模板包关键文件。

建议的排查顺序

  1. 先查 D1:订单是否 paid,实例是否存在,statuscreatingrunning 还是 failed
  2. 再查 Runtime:/health 是否正常,GET /instances/:id 能否读到 metadata。
  3. 看实例目录:metadata.json.envusage.json、logs 是否符合预期。
  4. 看进程和端口:backend/frontend port 是否监听,是否被旧进程占用。
  5. 看 nginx:对应 mf-<id> 配置是否存在,Host 路由是否打到正确 frontend port。
  6. 最后看前端:dashboard 是否显示 console URL,analysis quota 是否能从 Runtime 拉到。
不建议为了修复单个实例直接全局清理进程、删除实例目录或改模板 symlink。先用 instance id 精确定位,再决定是否 restart、补丁或重建。

一句话架构判断

当前 MiroFish SaaS 的部署模型是“Cloudflare 负责支付与控制台,206 负责实例编排和进程生命周期,MiroFish-Core 模板负责实际产品运行时”。 这个模型的优点是支付/账号逻辑和高权限运维隔离清楚;主要成本是每个实例复制完整运行环境,旧实例不会随模板自动升级,生产修复需要同时判断 Pages、Runtime、模板、历史实例四个层面。