MiroFish SaaS 实例部署逻辑梳理
基于当前 my_mirofish 最新代码整理。本文只记录部署逻辑、代码入口、状态流转和运维边界;
涉及密钥的地方只写环境变量名,不记录任何真实值。
一眼结论
MiroFish 的 SaaS 实例部署被拆成两条链路:用户购买后的“实例创建链路”和工程发布时的“平台更新链路”。 用户购买后,Cloudflare Pages Functions 负责创建订单、确认 Creem 支付、写 D1 状态,并调用 206 Runtime Server; Runtime Server 负责真正落地一个隔离实例,包括解压模板包、写实例环境变量、打补丁、分配端口、启动前后端、生成 nginx 路由、返回控制台 URL。
/api/launch-checkout 创建 Creem checkout 和 D1 订单。confirm/provision,把订单标为 paid。mf_instances,状态先置为 creating。MIROFISH_RUNTIME_API_TOKEN 请求 Runtime POST /instances。.env、补丁、启动、健康检查、写 nginx。console_url、端口、路径,D1 状态变为 running 或 failed。系统边界
承载官网、checkout、dashboard、console 页面和 functions/api/*。它不直接创建系统进程,只负责订单、支付确认、D1 状态和调用 Runtime。
runtime-server/server.mjs 是实例编排器。它持有平台 LLM / Zep / Graphiti 配置,给实例注入代理 token,并在机器上创建真实工作目录和进程。
模板来自 MiroFish-Core,生产机器上维护 mirofish-template-current.tar.gz。新实例永远从当前模板解压,旧实例不会自动获得模板更新。
购买到创建实例的完整链路
-
前端发起 checkout。
functions/api/launch-checkout.js解析套餐,当前套餐有starter、pro、enterprise; 也支持analysis_pack_5这种追加分析次数包。未登录用户会生成 guest token 并写 cookie。 -
创建 Creem 商品和 checkout。
如果环境里没有配置固定的 Creem product id,函数会通过 Creem API 临时创建 product,再创建 checkout。
checkout 的
request_id和 metadata 都会写订单 ID,用于回调核对。 -
写入订单。
D1 表
mf_orders写入pending订单,保存plan_id、金额、customer email、guest/user 归属、creem_checkout_id、test_mode等。 -
支付确认。
成功页会调
functions/api/checkout/creem-confirm.js;异步回调会进functions/api/webhooks/creem.js。 两者最终都进入provisionPaidOrder(),并先通过 Creem checkout 状态或 webhook 签名确认支付可信。 -
创建或复用实例记录。
provisionPaidOrder()把订单标记为paid。如果已有running/creating/queued实例,会幂等返回;否则生成 instance id,插入或更新mf_instances.status = creating。 -
调用 Runtime 创建实例。
callRuntimeCreateInstance()请求POST /instances,body 包含instanceId、orderId、planId、billingStartedAt、env=prod、testMode、force。 -
回写结果。
Runtime 成功后返回 metadata,
updateInstanceFromRuntime()回写console_url、host、端口、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.sh 和 smoke-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。关键字段包括:
id、order_id、plan_idmonthly_analysis_limit:Starter 5、Pro 15、Enterprise 20;testMode 为 2backend_port、frontend_port、service_nameworkspace_path、console_url、graph_providersecrets里只保存脱敏后的平台 key 状态
.env
实例 .env 是隔离的。真实平台 key 不直接写入实例;在 proxy mode 下,实例拿到的是 INSTANCE_TOKEN,再通过 Runtime 转发。
LLM_BASE_URL=http://127.0.0.1:<runtime-port>/v1ZEP_BASE_URL=http://127.0.0.1:<runtime-port>/zep,仅 Zep provider 需要ZEP_GRAPH_PREFIX=mf_<instance>,用于图谱隔离GRAPH_PROVIDER、GRAPHITI_MODE、GRAPHITI_SERVICE_URLFLASK_PORT、OASIS_DEFAULT_MAX_ROUNDS
数据表和状态流
| 表 | 职责 | 部署相关字段 |
|---|---|---|
mf_orders |
订单、支付和履约状态。 | payment_status、creem_checkout_id、paid_at、test_mode、product_type、target_instance_id、credit_amount、fulfillment_status。 |
mf_instances |
实例业务视图和 Runtime 回写结果。 | status、console_url、backend_port、frontend_port、service_name、workspace_path、runtime_instance_id、template_version。 |
mf_instance_claims |
用于把订单/实例领取到账号,支持 guest 购买后登录绑定。 | order_id、instance_id、claim_token_hash、expires_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=graphiti 和 GRAPHITI_SERVICE_URL。
独立的 graphiti-service 负责 Graphiti API 兼容、ontology 正规化、Neo4j/FalkorDB 后端和 LLM/Embedding 调用。
发布链路:更新平台本身
工程发布分三类,不等于创建一个用户实例。
| 发布目标 | 命令 | 作用 | 注意 |
|---|---|---|---|
| 预检查 | 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 和模板包关键文件。 |
建议的排查顺序
- 先查 D1:订单是否
paid,实例是否存在,status是creating、running还是failed。 - 再查 Runtime:
/health是否正常,GET /instances/:id能否读到 metadata。 - 看实例目录:
metadata.json、.env、usage.json、logs 是否符合预期。 - 看进程和端口:backend/frontend port 是否监听,是否被旧进程占用。
- 看 nginx:对应
mf-<id>配置是否存在,Host 路由是否打到正确 frontend port。 - 最后看前端:dashboard 是否显示 console URL,analysis quota 是否能从 Runtime 拉到。
一句话架构判断
当前 MiroFish SaaS 的部署模型是“Cloudflare 负责支付与控制台,206 负责实例编排和进程生命周期,MiroFish-Core 模板负责实际产品运行时”。 这个模型的优点是支付/账号逻辑和高权限运维隔离清楚;主要成本是每个实例复制完整运行环境,旧实例不会随模板自动升级,生产修复需要同时判断 Pages、Runtime、模板、历史实例四个层面。