projects.py
首先来看cairn\src\cairn\server\routers\projects.py
首先开头注册了一个POST /projects 接口。创建成功时返回201。@router.post("/projects", response_model=ProjectDetail, status_code=201)
接着传入请求体body: CreateProjectRequest
函数中,首先进行数据库连接,并进入上下文管理器。with get_conn() as conn:
接着向 projects 表插入项目基本信息。新项目的状态固定为 active:
1 | conn.execute( |
每个项目都会自动创建两条基础事实:
1 | conn.execute( |
所有数据写入完成后,接口返回一个 ProjectDetail 对象。
整体流程:
1 | 接收并验证请求 |
app.py 和 Router
接下来查看 cairn\src\cairn\server\app.py。
app.py 是 Cairn 服务端的总入口,主要负责创建 FastAPI 应用,并把分散在各个 router 文件中的接口组装起来。创建项目的具体业务逻辑并不在 app.py 中,而是在 routers/projects.py 中。
app.py 首先导入各个接口模块:
1 | from cairn.server.routers import export, hints, intents, projects, settings |
然后通过 include_router() 将这些模块中的接口注册到 FastAPI 主应用:
1 | app.include_router(settings.router) |
以 projects.py 为例,文件中的接口由 projects.router 收集:
1 |
|
但仅仅定义接口还不够。执行下面这行代码后,项目接口才会被加入 FastAPI 应用的路由表:
1 | app.include_router(projects.router) |
因此,projects.py 能被外部访问,需要经过两个步骤:
1 | projects.py 定义接口 |
include_router() 的作用是注册接口,而不是执行业务。服务器启动时,它只负责建立路由关系;只有客户端真正发送 POST /projects 请求时,FastAPI 才会执行 create_project()。
Cairn 当前注册了五个 router:
| Router | 文件 | 主要职责 |
|---|---|---|
settings.router |
settings.py |
配置相关接口 |
projects.router |
projects.py |
项目相关接口 |
hints.router |
hints.py |
提示相关接口 |
intents.router |
intents.py |
意图相关接口 |
export.router |
export.py |
数据导出相关接口 |
除此之外,app.py 还直接定义了首页接口,并通过 app.mount() 提供静态文件。这些内容不属于上面的 router 模块。
serve 和 dispatch
接下来查看 cairn\src\cairn\cli.py 中的两个命令:serve 和 dispatch。
这两个命令会启动不同的程序,承担的职责也不同:
1 | cairn serve → 启动 API server,保存状态并提供接口 |
serve:启动 API server
serve 命令先配置 SQLite 数据库路径,然后导入 FastAPI 应用,最后交给 Uvicorn 启动:
1 | def serve(host: str, port: int, db_path: str, log_level: str, access_log: bool): |
因此,cairn serve 主要负责:
- 启动 HTTP 服务;
- 保存和读取项目状态;
- 提供创建项目、查询项目、更新 intent 等 API;
- 接收网页、用户或 dispatcher 发来的请求。
它本身不会启动调度循环,也不会主动选择 AI worker。
dispatch:启动调度器
dispatch 命令要求提供 dispatcher 配置文件,然后创建并运行 DispatcherLoop:
1 | def dispatch(config_path: Path, once: bool, startup_healthcheck_only: bool, log_level: str): |
DispatcherLoop 会读取配置中的 server 地址,并创建一个 CairnClient:
1 | self.config = DispatchConfig.load(config_path) |
CairnClient 通过 HTTP 请求访问 server,例如查询项目和读取设置:
1 | response = self._session().get(self._url("/projects"), timeout=self._timeout) |
这说明 dispatcher 不会直接读写 server 的 SQLite 数据库,而是通过 API 与 server 协作。
dispatcher 的大致工作流程如下:
1 | 通过 API 查询项目状态 |
只启动 server,AI 会自动工作吗?
不会。
只启动 server 后,API 和数据库可以正常工作,用户可以创建或查询项目,但没有 dispatcher 主动扫描项目、选择任务和调用 worker,所以 AI 不会自动推进项目。
可以把 server 理解为保存状态的工作台,而 dispatcher 才是不断检查工作台并分配任务的人。
只启动 dispatcher,没有 server 会怎样?
dispatcher 会尝试访问配置中的 server 地址。没有 server 时,它无法读取设置、获取项目或写回任务结果,因此不能推进任何项目。
在普通循环模式下,请求失败会被记录,dispatcher 等待一个调度间隔后继续重试:
1 | except requests.RequestException as exc: |
如果使用 --once 只运行一轮,请求异常会继续抛出,命令通常会直接失败退出。
因此,正常运行时应该先保证 server 可访问,再启动 dispatcher。
为什么 server 不直接调用 AI?
server 的核心职责是维护项目状态和提供稳定的 API。如果把 AI 调用也放进 server,请求处理就会和耗时较长、资源占用较高的 worker 执行耦合在一起。
将它们拆开后,可以获得几个好处:
- API server 不会因为 AI 任务运行时间较长而被阻塞;
- worker 的模型配置、密钥和运行环境可以由 dispatcher 单独管理;
- 调度失败不会直接导致 API server 停止服务;
- server 可以专注保证状态读写和接口行为的一致性。
为什么 dispatcher 要作为单独进程?
dispatcher 是一个长期运行的调度循环。它会定期查询项目、维护任务状态、使用线程池执行任务,并管理 worker 或容器。这类工作与处理 HTTP 请求的 server 有不同的运行方式和资源需求。
拆成独立进程后,server 和 dispatcher 可以分别启动、停止、重启和配置。即使 dispatcher 或某个 worker 出现故障,server 中已经保存的项目状态仍然可以继续访问。
二者的关系可以总结为:
1 | 用户或网页 |
最重要的一句话是:server 管状态,dispatcher 管推进。