跳转至

04 · 服务启动与请求生命周期

Composition RootExpress 5Startup ReconciliationGraceful Shutdown

一句话结论: server/src/index.ts 负责把数据库、插件、调度器、WebSocket 和进程生命周期接起来,app.ts 负责构造 HTTP 应用;启动不是“listen 即完成”,而是先迁移、装配、对账、恢复,再开放稳定服务。

1. 两个组合根

Paperclip 把 Server 入口分成两层:

文件 主要职责
server/src/index.ts 读取配置、启动数据库、迁移、服务组合、scheduler、HTTP server、WebSocket、shutdown/hot restart
server/src/app.ts 创建 Express app、中间件、auth、路由、插件宿主、MCP、UI 静态服务、错误处理

这种拆分使 createApp() 在测试里可复用,而端口、信号、embedded DB 等进程级副作用留在 index.ts。

2. 启动全景

sequenceDiagram
  participant OS as Process / OS
  participant IDX as index.ts
  participant DB as PostgreSQL
  participant APP as createApp()
  participant PLG as Plugin Runtime
  participant SCH as Scheduler / Recovery
  participant HTTP as HTTP + WS Server

  OS->>IDX: start
  IDX->>IDX: load config + logging + telemetry
  IDX->>DB: external connect or start embedded PostgreSQL
  IDX->>DB: inspect / reconcile / apply migrations
  IDX->>PLG: build registries, worker manager, lifecycle
  IDX->>APP: create Express application
  APP-->>IDX: app + plugin services
  IDX->>DB: startup reconciliation
  IDX->>SCH: adopt/reap/resume/recover
  IDX->>HTTP: listen
  IDX->>HTTP: attach live-event and terminal WebSocket upgrade handlers
  SCH->>SCH: periodic ticks

顺序很重要:如果迁移尚未完成就启动 scheduler,旧 schema 上的恢复任务可能先执行;如果插件 registry 尚未建立就恢复 adapter run,外部 adapter 可能无法解析。

3. 配置解析不是简单读 .env

Server 配置来自共享 Zod schema、配置文件和环境变量覆盖,包含:

  • deployment mode 与 exposure;
  • bind mode、host、allowed hostnames;
  • Better Auth base URL;
  • embedded/external PostgreSQL;
  • backup;
  • local disk/S3 storage;
  • local encrypted 或外部 secrets provider;
  • scheduler、telemetry、company deletion 等开关。

local_trusted、authenticated/private、authenticated/public 的合法组合在 schema 中做交叉校验。例如 public exposure 要求 explicit public URL,local trusted 只能是 private。

配置文件默认值
  → 环境变量覆盖
  → 路径展开 / bind 推导
  → 跨字段验证
  → Runtime Config

这使 CLI doctor 和 Server 能共享同一套约束语言。

4. 数据库启动的两条路径

首次本地运行时,Paperclip 在实例数据目录启动受管理 PostgreSQL。CLI 负责路径、端口、健康检查和修复;Server 最终仍通过标准 PostgreSQL driver 连接。

部署使用 connection string,可另设 migration URL。连接池、prepared statement、idle/connect timeout 等由 DB 包配置。

源码与旧文档漂移

一处旧架构页仍写“PGlite”,但当前 config type 是 embedded-postgres | postgres,实现使用 embedded Postgres 进程和标准 PostgreSQL。进行部署决策时应以代码和 docs/deploy/database.md 为准。

5. 迁移:先检查再执行

数据库包维护 Drizzle schema 和 SQL migration journal。启动过程会:

  1. 读取已应用迁移;
  2. 对比打包迁移清单与 journal;
  3. 检查异常/危险状态;
  4. 依据配置应用缺失迁移;
  5. 在失败时阻止应用进入半可用状态。

迁移不仅为“建表”。大量条件索引、历史 backfill 和 contract expansion 只能在 SQL migration 中表达,不能假设 schema TS 自动同步生产库。

6. createApp() 的中间件顺序

请求大致经过:

flowchart LR
  REQ["Request"] --> PROXY["trust proxy"]
  PROXY --> BODY["JSON / upload limits"]
  BODY --> COMP["compression policy"]
  COMP --> LOG["request logger + redaction"]
  LOG --> HOST["private hostname guard"]
  HOST --> ACTOR["actor middleware"]
  ACTOR --> AUTH["auth routes / Better Auth"]
  AUTH --> GUARD["board mutation guard"]
  GUARD --> ROUTES["domain routes"]
  ROUTES --> ERR["error handler"]

顺序背后的意图:

  • proxy 信任影响 IP/secure cookie,必须最先稳定;
  • body 限制在业务路由前阻断过大请求;
  • logger 的 redaction 在任何敏感错误输出前生效;
  • actor 在授权路由前建立身份;
  • mutation guard 可以统一保护 Board 写操作;
  • error handler 最后把 Zod、HTTP 和未知异常转成一致响应。

7. Actor middleware 建立“谁在请求”

HTTP 层不会让每个 route 自己解析 token。Actor middleware 处理:

  • local trusted 下的隐式 local board;
  • authenticated 下的 Better Auth session;
  • cloud tenant headers(受部署配置约束);
  • agent run JWT;
  • agent API key;
  • board API key。

它把统一 actor context 放入 request,后续 route/service 再做资源级授权。认证回答“你是谁”,不是“你能对这个 Issue 做什么”。

8. 路由装配为什么有顺序

app.ts 挂载几十组资源路由:company、agent、project、issue、goal、approval、secret、cost、activity、workspace、routine、pipeline、tool access 等。

几个特别的顺序约束:

  • auth endpoint 与 Better Auth handler 需要在通用 API 行为内正确排序;
  • tool gateway/approval 依赖被建立后,Issue 路由才能把工具审批交给它;
  • plugin UI static、plugin API route 和普通 UI fallback 要避免路径吞噬;
  • MCP protocol 路由直接挂到 app,不等同于普通 CRUD router;
  • 静态 UI 或 Vite dev middleware 必须在 API 之后,避免 SPA fallback 把 API 404 变成 HTML。

9. 服务不是逐个 new 出来就完事

App 组合时建立一组协作对象:

adapter registry
plugin registry / loader / lifecycle
worker manager
plugin event + stream bus
job scheduler / dispatcher
tool registry / tool gateway
environment drivers
storage / secrets services
domain services

其中插件与 adapter 有双向影响:插件可贡献外部 adapter 或 environment driver,而 heartbeat 执行前又要从 registry 解析它们。组合根要先固定 registry,再把依赖注入 heartbeat 和 routes。

10. HTTP 与 WebSocket 共用 server,但鉴权独立

Paperclip 使用 server.on("upgrade") 处理至少两类 WebSocket:

  • company live events;
  • environment custom image terminal。

WebSocket upgrade 不经过常规 Express request pipeline,因此实现必须重新:

  • 解析 path/company/session/token;
  • 检查 company membership 或 API key 归属;
  • 为不同 upgrade handler 标记“已处理”,避免多个 handler 争抢 socket;
  • 设置 ping/pong 和连接 cleanup。

这是许多 Web 应用容易漏掉的权限缝隙。

11. 启动对账:把“数据库状态”与“进程现实”重新对齐

进程重启后,数据库可能仍有 running run、持有中的 lease、queued wakeup 或 execution lock。Paperclip 不直接相信这些旧状态,而会运行多组 reconciliation:

  • 读取 hot-restart intent,尝试接管仍存活进程;
  • 识别服务器停机期间已自然结束的执行;
  • reap orphan run/process;
  • 提升到期 scheduled retry;
  • 恢复 queued run;
  • 扫描 stranded issue、stale lock 和 liveness 缺口;
  • 恢复 periodic scheduler。
flowchart TD
  DBRUN["DB says running"] --> PROC{"process still attributable?"}
  PROC -->|yes| ADOPT["adopt / continue tracking"]
  PROC -->|finished while down| FINAL["finalize from evidence"]
  PROC -->|missing / unsafe| LOST["mark lost / recover visibly"]
  LOST --> REQUEUE["retry / recovery issue / release lock"]

12. 定时工作并非一个“大 cron”

Server 启动多个不同责任的 tick,例如:

  • heartbeat timer scheduler;
  • scheduled retry promotion;
  • orphan/stale lock sweep;
  • stranded issue recovery;
  • issue graph liveness;
  • task watchdog;
  • silent active-run scanner;
  • productivity review;
  • plugin jobs、log retention、connection health;
  • database backup。

拆开频率与职责可以避免一项慢扫描拖住全部恢复,但也要求 shutdown 能统一停止 timer 和 worker。

13. Shutdown 与 hot restart

普通 shutdown 需要按依赖反序清理:

停止接收新工作
→ 停 scheduler / job dispatcher
→ 记录 active run snapshot
→ 终止或交接运行
→ 停 plugin workers / runtime services
→ 关闭 WebSocket / HTTP server
→ 关闭 DB / embedded PostgreSQL

Hot restart 额外写入原子 JSON intent:旧 server PID/identity、版本、active runs、是否需要 drain。新进程生成 report,把每条 run 分类为 adopted、finalized while down、lost 或 skipped。

这个显式交接协议优于“新进程起来后猜数据库”,因为它保留旧进程看到的最后一刻事实。

14. 失败发生在哪一层

阶段 典型失败 正确结果
配置 public URL 缺失、bind 非法 启动前失败,给出诊断
DB 无法连接、迁移异常 不开放半可用 API
Plugin manifest/worker 失败 记录 plugin health;核心服务按策略继续或阻断
Startup recovery 孤儿 run/lease 分类、释放或创建恢复路径
HTTP request Zod/授权/业务冲突 4xx + 安全错误信息
Runtime adapter/process/provider 失败 run 终态 + liveness/retry/recovery
Shutdown 超时/活动执行 snapshot + 有界 drain + 可对账状态

15. 可复用经验

  1. 将 app 构造与 process lifecycle 分开,测试和运维都会更清晰。
  2. WebSocket upgrade 要独立做认证,不能假设经过 HTTP middleware。
  3. 监听端口前先完成迁移与关键对账,避免提供“看似健康”的错误服务。
  4. 任何持久化队列系统都应把 startup reconciliation 设计成正式阶段。
  5. Hot restart 用显式 intent/report 协议保存交接证据,不依赖隐式进程探测。

源码入口

  • server/src/index.ts:进程组合、启动对账、WebSocket、shutdown
  • server/src/app.ts:Express app 与路由组合
  • server/src/config.ts:运行时配置解析
  • packages/shared/src/config-schema.ts:共享配置约束
  • server/src/middleware/auth.ts:actor 建立
  • server/src/services/hot-restart.ts:intent/report 协议
  • server/src/realtime/live-events-ws.ts:WebSocket upgrade 鉴权

下一章关注跨层一致性:05 · 共享契约与 API 设计。