04 · 服务启动与请求生命周期¶
一句话结论:
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。
这使 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。启动过程会:
- 读取已应用迁移;
- 对比打包迁移清单与 journal;
- 检查异常/危险状态;
- 依据配置应用缺失迁移;
- 在失败时阻止应用进入半可用状态。
迁移不仅为“建表”。大量条件索引、历史 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. 可复用经验¶
- 将 app 构造与 process lifecycle 分开,测试和运维都会更清晰。
- WebSocket upgrade 要独立做认证,不能假设经过 HTTP middleware。
- 监听端口前先完成迁移与关键对账,避免提供“看似健康”的错误服务。
- 任何持久化队列系统都应把 startup reconciliation 设计成正式阶段。
- Hot restart 用显式 intent/report 协议保存交接证据,不依赖隐式进程探测。
源码入口¶
server/src/index.ts:进程组合、启动对账、WebSocket、shutdownserver/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 设计。