Files
writer-work-flow/DEPLOYMENT.md

449 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 部署指南DEPLOYMENT
> 本文是把「AI 辅助中文网文写作工作流」部署到服务器的实操指南。
> 所有命令/端口/服务名均以仓库当前 `docker-compose.yml`、两份 `Dockerfile`、
> `.env.example`、`packages/config/ww_config/settings.py`、`apps/api/ww_api/main.py`
> 为准。**遇到与你手上 compose 不一致处,一律以实际 `docker-compose.yml` 为准。**
>
> ⚠️ **重要前提:本产品当前是「单用户原型」,没有任何认证 / 多租户**`users`/`owner_id`
> 是桩)。这直接决定了安全部署的红线——见 [§6 安全红线](#6-安全红线单用户原型的硬约束)。
> **不要在没有网络层保护的情况下把它裸暴露到公网。**
---
## 1. 架构与前置条件
### 1.1 三个组件
部署单元由三个容器组成(服务名即 `docker-compose.yml` 中的 key
| 服务名 | 角色 | 镜像来源 | 容器端口 | compose 发布端口 | 健康检查 |
|---|---|---|---|---|---|
| `pg` | Postgres 16 数据库(唯一真源) | `postgres:16-alpine`(拉取) | 5432 | `5432:5432` | ✅ `pg_isready -U writer`interval 5s / timeout 3s / retries 10 |
| `api` | FastAPI 后端async + SSE + LangGraph 编排) | 由 `apps/api/Dockerfile` 构建,构建上下文 = **仓库根 `.`** | 8000 | `8000:8000` | ❌ 无(见 §1.5 缺口) |
| `web` | Next.js 前端standalone 产物) | 由 `apps/web/Dockerfile` 构建,构建上下文 = **`./apps/web`** | 3000 | `3000:3000` | ❌ 无(见 §1.5 缺口) |
依赖关系compose `depends_on`
- `api` 依赖 `pg`,且**等 pg 健康**后才起(`condition: service_healthy`)。
- `web` 依赖 `api`,但**只是启动顺序**(无 `condition`——web 可能先于 api 就绪,属可容忍的运行期竞态。
数据持久化:命名卷 `pgdata` 挂到 `pg``/var/lib/postgresql/data`。**备份/迁移数据 = 备份这个卷**(见 §7
### 1.2 镜像怎么来
两个应用镜像都从源码 `Dockerfile` **本地构建**(仓库未发布预构建镜像):
- **`apps/api/Dockerfile`**`python:3.12-slim` + 从 `ghcr.io/astral-sh/uv` 拷入 `uv``COPY pyproject.toml uv.lock* / packages / apps/api``uv sync --no-dev --frozen`(失败回退非 frozen`CMD uv run uvicorn ww_api.main:app --host 0.0.0.0 --port 8000`
- **`apps/web/Dockerfile`**多阶段。build 阶段 `node:22-slim` + corepack(pnpm)`pnpm install --frozen-lockfile``pnpm build`Next `output: "standalone"`,见 `apps/web/next.config.mjs`run 阶段只拷 `.next/standalone` + `.next/static` + `public``CMD node server.js`
> web 镜像在构建期消费**已提交的** OpenAPI TS 客户端 `apps/web/lib/api/schema.d.ts`(该文件在 git 里,仅 `openapi.json` 被 gitignore。因此 web 构建**不需要**联网跑 `pnpm gen:api`。**但**:后端 schema 改动后,必须先 `cd apps/web && pnpm gen:api` 重生成并提交 `schema.d.ts`,再构建 web 镜像,否则前后端契约漂移。
### 1.3 需要的环境变量(逐项)
变量来源 = `packages/config/ww_config/settings.py`(后端)+ `apps/web/lib/api/config.ts`(前端)。
**后端api 容器):**
| 变量 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|
| `DATABASE_URL` | 建议显式设 | `postgresql+asyncpg://writer:writer@localhost:5432/writer` | **异步**连接串asyncpg。运行时 + `alembic upgrade head` 都读它(`packages/db/migrations/env.py` 用它建 async engine 跑迁移)。容器内主机名用 compose 服务名 `pg`。 |
| `DATABASE_URL_SYNC` | 建议显式设 | `postgresql+psycopg://writer:writer@localhost:5432/writer` | **同步**连接串psycopg`.env.example` 与 compose 均定义;供同步工具链使用。 |
| `APP_ENV` | 否 | `dev` | 环境名。生产设 `prod`。 |
| `LOG_JSON` | 否 | `false` | 结构化日志是否输出 JSON。**生产设 `true`**structlog JSON便于采集。 |
| `CREDENTIAL_ENC_KEY` | **✅ 必填** | 空(`SecretStr("")` | provider 凭据对称加密密钥Fernet, urlsafe-base64 32B。**api 启动即校验**`main._lifespan``_fernet(...)`):缺失/非法 → **进程直接启动失败**,不带病接流量。生产**必须**用新密钥,见 §2.3。 |
| `CORS_ORIGINS` | 生产强烈建议 | `["http://localhost:3000","http://127.0.0.1:3000"]` | 浏览器跨源白名单,逗号分隔覆盖。**启动断言不含通配 `*`**(与 `allow_credentials=True` 不兼容,`main.create_app``assert "*" not in cors_origins`)。生产填前端真实来源,如 `https://writer.example.com`。 |
**前端web 容器):**
| 变量 | 是否必填 | 默认值(`config.ts` 回退) | 说明 |
|---|---|---|---|
| `NEXT_PUBLIC_API_BASE` | 生产建议 | `http://localhost:8000` | **浏览器端**访问 API 的基址。⚠️ Next.js 的 `NEXT_PUBLIC_*` 在**构建期内联**进客户端 bundle——只在运行期(compose environment)设它**不会改**已构建产物(见 §1.5 缺口)。生产需在**构建期**注入前端可达的公网 API 地址(通常是反代后的 `https://writer.example.com/api` 之类)。 |
| `API_BASE_INTERNAL` | 容器编排建议 | 回退到 `NEXT_PUBLIC_API_BASE` | **服务端组件(RSC)**取数用的内网基址。容器内应设为 `http://api:8000`compose 服务名)。**当前 compose 未设它**(见 §1.5 缺口RSC 会回退到 `localhost:8000` 而打不到 api 容器。 |
### 1.4 LLM provider key 不进环境变量
**不要**把任何 LLM 供应商密钥DeepSeek/Kimi/OpenAI/Anthropic/Qwen/GLM/Gemini 等)写进 env 或 compose。
本产品的 provider 凭据是**部署后在应用「设置」页录入**,由后端用 `CREDENTIAL_ENC_KEY`Fernet
**加密后存进 Postgres**`provider_credentials` 表 / `settings_providers` 路由)。
Kimi Code 走 OAuth 订阅授权(`kimi_oauth` 路由),同样落库。
推论:`CREDENTIAL_ENC_KEY` 是解开所有已存凭据的唯一钥匙——**丢了它 = 库里所有 provider 凭据不可解密**,只能重新录入。务必备份保管(见 §2.3 / §6
### 1.5 已发现的部署面缺口(部署前需知)
以下是审阅 Dockerfile/compose 时发现的、影响「照抄即部署」的点,本文各节给出规避办法:
1. **api 镜像未拷 `alembic.ini`**`apps/api/Dockerfile``COPY pyproject.toml / packages / apps/api`**没有** `COPY alembic.ini`(它在仓库根)。因此 `docker compose run --rm api uv run alembic upgrade head` 会因找不到 `alembic.ini` 而失败。规避:从**宿主机 checkout** 跑迁移§2.4 方式一,当前即可用),或给 Dockerfile 补一行 `COPY alembic.ini ./`§2.4 方式二)。
2. **compose 的 `api` 服务未提供 `CREDENTIAL_ENC_KEY`**(也没 `env_file`base compose 里 `api.environment` 只有 `DATABASE_URL/DATABASE_URL_SYNC/APP_ENV=dev`。由于该密钥启动即校验,**base compose 直接 `up` 会让 api 启动失败**。生产必须补齐§2.2 覆盖文件)。
3. **compose 的 `web` 服务未设 `API_BASE_INTERNAL`**RSC 服务端取数会回退到 `localhost:8000`,在容器里打不到 api。需在覆盖文件设 `API_BASE_INTERNAL=http://api:8000`§2.2)。
4. **`NEXT_PUBLIC_API_BASE` 是构建期内联**web Dockerfile 未声明该 build ARG浏览器 bundle 里的 API 基址会固定为 `http://localhost:8000`。生产要改,需在**构建阶段**注入§3 反代方案里说明取舍)。
5. **api/web 无 HEALTHCHECK**:两个 `Dockerfile` 和 compose 的 `api`/`web` 都没有健康检查(只有 `pg` 有)。编排/反代的存活探测需自行加health 端点已就绪,见 §7.1)。
> 以上缺口不阻断部署,但决定了「不能照抄 base compose 一把梭」。生产用 §2.2 的覆盖文件补齐即可。
---
## 2. 方案 A单机 docker compose推荐起步
适合:单台服务器 / VPS起步验证。前提服务器已装 Docker含 Docker Compose v2 插件)。
### 2.1 装 Docker + 拉代码
```bash
# 服务器上(以 Ubuntu 为例)安装 Docker Engine + compose 插件
curl -fsSL https://get.docker.com | sh
docker --version && docker compose version # 确认 compose v2 可用
# 拉代码
git clone <你的仓库地址> writer-work-flow
cd writer-work-flow
git checkout main # 或你的发布分支
```
### 2.2 准备生产配置(覆盖 base compose 的 dev 缺省)
base `docker-compose.yml` 是**开发导向**api 用 `APP_ENV=dev`、缺 `CREDENTIAL_ENC_KEY`
web 缺 `API_BASE_INTERNAL`、pg 端口对外发布)。生产用一个**覆盖文件** `docker-compose.prod.yml`
补齐,不改 base 文件:
```yaml
# docker-compose.prod.yml —— 与 base 叠加docker compose -f docker-compose.yml -f docker-compose.prod.yml ...
services:
pg:
# 不把 5432 暴露到公网:仅绑本机回环(供宿主机跑迁移用),或整段删掉只走 compose 内网
ports:
- "127.0.0.1:5432:5432"
api:
environment:
DATABASE_URL: postgresql+asyncpg://writer:${PG_PASSWORD}@pg:5432/writer
DATABASE_URL_SYNC: postgresql+psycopg://writer:${PG_PASSWORD}@pg:5432/writer
APP_ENV: prod
LOG_JSON: "true"
CREDENTIAL_ENC_KEY: ${CREDENTIAL_ENC_KEY} # 见 §2.3 生成
CORS_ORIGINS: "https://writer.example.com" # 前端真实来源,逗号分隔;不得含 '*'
# 可选:补健康检查(镜像里已装 uv/python可用 python 探 /health
# 若加了反代做存活探测,这里可省
web:
environment:
NEXT_PUBLIC_API_BASE: https://writer.example.com # 浏览器可达的公网 API经反代
API_BASE_INTERNAL: http://api:8000 # RSC 服务端内网直连 api 容器
```
同时准备 pg 的强口令与密钥(**不要提交进仓库**),可放在同目录 `.env`compose 会自动读取用于 `${...}` 插值):
```bash
# .envcompose 变量插值用chmod 600勿入库
PG_PASSWORD=<强随机口令>
CREDENTIAL_ENC_KEY=<§2.3 生成的 Fernet key>
```
> ⚠️ base compose 里 pg 的 `POSTGRES_PASSWORD: writer` 是硬编码弱口令。生产请在覆盖文件里
> 用 `POSTGRES_PASSWORD: ${PG_PASSWORD}` 覆盖 pg并让 `DATABASE_URL*` 用同一口令
> (上例已用 `${PG_PASSWORD}`)。若你保持 base 的 `writer` 口令,**务必**确保 pg 不可从公网访问§6
### 2.3 生成并妥善保管 CREDENTIAL_ENC_KEY
⚠️ **这是最关键的一步**。用 Fernet 生成一把新密钥(`.env.example` 里那把是开发占位,**生产禁用**
```bash
# 任一装了 cryptography 的环境(也可用仓库 uv 环境)
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# 或纯 pythonpython -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```
把输出的一行密钥填进 §2.2 的 `.env``CREDENTIAL_ENC_KEY`,并**离线备份**(密码管理器/密钥库)。
> **丢失 = 灾难**:所有已录入的 provider 凭据(存在 Postgres 里、用它加密)将**永久不可解密**
> 只能清空后重新录入。轮换密钥需先用旧钥解密全部凭据、再用新钥重加密(当前无自动轮换脚本,
> 见 §6。**切勿**复用 `.env.example` 里的占位 key那等于凭据未加密。
### 2.4 构建镜像 + 跑迁移 + 起服务
```bash
# 在仓库根,带上覆盖文件
export COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
# 1) 构建两个应用镜像
$COMPOSE build
# 2) 先起数据库
$COMPOSE up -d pg
# 等 pg 健康compose 已配 healthcheck
$COMPOSE ps # 看 pg 是否 healthy
# 3) 跑数据库迁移(含 LangGraph checkpointer 建表)—— 见下方两种方式
```
**迁移的关键事实**`alembic upgrade head` 一条命令会做**两件事**——
(a) 建/升级 16 张业务表;(b) 迁移 `d3e4f5a6b7c8_langgraph_checkpoint_setup` 会调用 LangGraph 的
`checkpointer.setup()``checkpoints/checkpoint_blobs/checkpoint_writes/checkpoint_migrations`
四张检查点表。**checkpointer 的建表只在迁移里发生,绝不在应用运行时**(符合 CLAUDE.md「LangGraph」纪律
所以**部署时必须先把迁移升到 head应用才能正常写章/审稿/续跑**。
因 api 镜像未随包 `alembic.ini`§1.5 缺口 1有两种可用方式
**方式一(当前即可用,无需改 Dockerfile——从宿主机 checkout 跑迁移:**
```bash
# 宿主机需装 uvcurl -LsSf https://astral.sh/uv/install.sh | sh; export PATH="$HOME/.local/bin:$PATH"
cd writer-work-flow
uv sync
# 指向 compose 里 pg 暴露到 127.0.0.1:5432 的端口§2.2 覆盖文件已绑本机)
DATABASE_URL="postgresql+asyncpg://writer:${PG_PASSWORD}@127.0.0.1:5432/writer" \
DATABASE_URL_SYNC="postgresql+psycopg://writer:${PG_PASSWORD}@127.0.0.1:5432/writer" \
CREDENTIAL_ENC_KEY="${CREDENTIAL_ENC_KEY}" \
uv run alembic upgrade head
uv run alembic check # 可选:确认无漂移
```
**方式二(更干净,需给 Dockerfile 补一行)——在 api 容器里跑迁移:**
先在 `apps/api/Dockerfile``COPY alembic.ini ./`(放在 `COPY apps/api ./apps/api` 之后),
`$COMPOSE build api` 重建后:
```bash
$COMPOSE run --rm api uv run alembic upgrade head
```
> 这一步需要 parent/维护者决定是否接受改 Dockerfile。改前方式二不可用。
**4) 起全部服务:**
```bash
$COMPOSE up -d
$COMPOSE ps # api/web 应为 running注意它们无 healthcheck看不到 healthy
```
### 2.5 健康检查
```bash
# API根 + health 端点(均返回 200 JSON
curl -f http://127.0.0.1:8000/ # {"service":"ww-api","status":"ok"}
curl -f http://127.0.0.1:8000/health # {"status":"ok"}
# Web首页
curl -fI http://127.0.0.1:3000/
# 看日志api 启动会校验 CREDENTIAL_ENC_KEY、seed 单用户桩、reap 僵尸 job
$COMPOSE logs -f api
```
若 api 启动即退出,最常见原因是 `CREDENTIAL_ENC_KEY` 缺失/非法§1.5 缺口 2 / §2.3)——
日志会显示 CredentialKeyError 相关堆栈。
部署完成后,**打开前端「设置」页录入 LLM provider 凭据**§1.4),产品才能真正调用模型。
---
## 3. 反向代理 + HTTPS
生产应在应用前放一层反代做 TLS 终止、单一入口、并(配合 §6做访问控制。
拓扑:浏览器 → 反代(443) → `web:3000`(页面/RSC`api:8000``/api` 或独立子域)。
> 注意 `NEXT_PUBLIC_API_BASE` 的构建期内联§1.5 缺口 4浏览器直接打 API 的地址在**构建 web 镜像时**就定死了。
> 若走「同源 `/api` 前缀反代」,需让前端构建时 `NEXT_PUBLIC_API_BASE` 指向该前缀(当前 web Dockerfile 未暴露该 build ARG需按缺口 4 处理);
> 若走「api 独立子域」(如 `api.example.com`),同理需在构建期注入。
### 3.1 Caddy推荐自动证书
```caddy
# Caddyfile —— 自动申请/续签 Let's Encrypt 证书
writer.example.com {
# 后端 API同源 /api/* 反代到 api 容器(去掉 /api 前缀按你的前端约定调整)
handle_path /api/* {
reverse_proxy api:8000
}
# 其余流量到 Next 前端
reverse_proxy web:3000
# SSE 长连接:关掉缓冲,避免审稿/写章流被缓存卡住
reverse_proxy api:8000 {
flush_interval -1
}
}
```
将 Caddy 也放进 compose与 api/web 同网络,用服务名访问),暴露 80/443。
### 3.2 Nginx手动 / certbot 证书)
```nginx
server {
listen 443 ssl;
server_name writer.example.com;
ssl_certificate /etc/letsencrypt/live/writer.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/writer.example.com/privkey.pem;
# 前端
location / {
proxy_pass http://web:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 后端 API含 SSE写章/四审流)
location /api/ {
proxy_pass http://api:8000/;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection ""; # SSE保持长连接
proxy_buffering off; # SSE关缓冲事件即时下发
proxy_read_timeout 3600s; # 长任务/流式容忍
}
}
```
> 无论 Caddy/Nginx**SSE 必须关代理缓冲**(写章、四审都是事件流),否则前端进度条会卡住。
---
## 4. 方案 BPaaS / 托管平台
容器化已就绪(两个 Dockerfile + standalone 前端),可托管到 PaaS。共性**用托管 Postgres**
在平台面板设 env尤其 `CREDENTIAL_ENC_KEY``CORS_ORIGINS``DATABASE_URL*`
部署后照 §2.4 跑一次 `alembic upgrade head`。provider key 仍在应用设置页录入(不进平台 env
### 4.1 Fly.io
- 两个 app`fly launch` 分别为 api`apps/api/Dockerfile`,构建上下文=仓库根)与 web`apps/web/Dockerfile`,上下文=`apps/web`)。
- Postgres 用 `fly postgres create`(或外部托管 PG`DATABASE_URL`/`DATABASE_URL_SYNC` 设成其连接串(注意驱动前缀 `+asyncpg`/`+psycopg`Fly 给的是原生 `postgres://`,需自行加前缀)。
- `fly secrets set CREDENTIAL_ENC_KEY=... CORS_ORIGINS=... PG_PASSWORD=...`
- 迁移:`fly ssh console` 进 api 机器跑 `alembic upgrade head`(同样受 §1.5 缺口 1 影响——需镜像含 `alembic.ini`,或用 `fly ssh` + release_command。可用 Fly `[deploy] release_command` 在每次发布前自动跑迁移(需 alembic.ini 在镜像里)。
- web 的 `NEXT_PUBLIC_API_BASE` 在**构建期**注入Fly build args / Dockerfile ARG缺口 4
- api 内部端口 8000、web 内部端口 3000`fly.toml``internal_port` 对应。
### 4.2 Render
- 两个 Web ServiceDocker 运行时),分别指向两个 Dockerfileweb 的 Docker build context 设 `apps/web`api 设仓库根。
- Postgres 用 Render Managed Postgres把内部连接串填进 api 的 `DATABASE_URL*`(加 `+asyncpg`/`+psycopg` 前缀)。
- 在 api service 的 Environment 填 `CREDENTIAL_ENC_KEY`/`CORS_ORIGINS`/`APP_ENV=prod`/`LOG_JSON=true`
- 迁移:用 Render 的 **Pre-Deploy Command**`uv run alembic upgrade head`(需镜像含 `alembic.ini`,缺口 1
- web 与 api 的互通用 Render 的内部地址;`API_BASE_INTERNAL` 指向 api 内部 URL`NEXT_PUBLIC_API_BASE`(构建期)指向 api 公网 URL。
> 两个平台的共同注意点:**全部继承 §1.5 的缺口**(尤其 alembic.ini 未随镜像、NEXT_PUBLIC 构建期内联)。
> 上线前按缺口处理,或接受「迁移只能从含源码的环境跑」。
---
## 5. 迁移与升级流程
### 5.1 常规升级
```bash
export COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
git pull # 拉新代码(含新迁移 / schema.d.ts
$COMPOSE build # 重建 api/web 镜像
# 先升迁移到 head含新增业务表 + 可能的 checkpointer 变更),再滚动重启
# —— 用 §2.4 的方式一或方式二
uv run alembic upgrade head # 方式一(宿主机 checkout指向 pg
$COMPOSE up -d # 用新镜像重启 api/webpg 不动)
# 冒烟
curl -f http://127.0.0.1:8000/health && curl -fI http://127.0.0.1:3000/
```
顺序要点:**先迁移、后换应用镜像**——新代码通常假设 schema 已到 head。若后端 schema 有变,
记得升级前已在开发侧 `pnpm gen:api` 重生成并提交了 `apps/web/lib/api/schema.d.ts`§1.2
否则 web 构建/类型会漂移。
### 5.2 回滚
```bash
# 应用回滚:切回上一个 tag/commit 重建重启
git checkout <上一个可用 tag>
$COMPOSE build && $COMPOSE up -d
# 数据库回滚(谨慎):按需降级一版
uv run alembic downgrade -1
```
> ⚠️ DB 降级可能丢列/丢表数据,且 `d3e4f5a6b7c8` 的 `downgrade()` 会 `DROP` 四张 checkpoint 表
> (在跑的写章/链任务会丢控制流状态)。**回滚前先备份**§7.3)。业务表回滚同理不可逆,优先「向前修复」。
---
## 6. 安全红线(单用户原型的硬约束)
⚠️ **本产品没有认证、没有多租户、没有登录**`users`/`owner_id` 是桩,见 CLAUDE.md「Prototype scope」
任何拿到 URL 的人都能读写全部小说数据、录入/触发用花你钱的 LLM 凭据。因此:
1. **绝不裸暴露到公网。** 至少满足其一:
- 只在 **VPN / 内网 / SSH 隧道**内访问;
- 反代前加 **IP 白名单****HTTP Basic Auth**Caddy `basic_auth` / Nginx `auth_basic`
- 或**先补一层认证**再上公网(当前无,属产品级改造)。
2. **Postgres 不对公网开放。** base compose 把 `pg` 发布到 `5432:5432`(全网卡)。生产**必须**改成
仅回环 `127.0.0.1:5432:5432`§2.2)或删掉端口映射只走 compose 内网。**改掉 base 里的弱口令 `writer/writer`**。
3. **CORS 显式白名单,禁通配。** `CORS_ORIGINS` 必须是精确来源;后端已在启动断言不含 `*`
`allow_credentials=True``*` 会被浏览器拒且语义危险)。别为图省事放宽。
4. **`CREDENTIAL_ENC_KEY` 的密钥管理与轮换:**
- 用 §2.3 生成的**新** Fernet key绝不用 `.env.example` 占位值;
- 存进密钥库 / 平台 secrets`.env` `chmod 600` 且**不入库**
- **备份它**(丢了 = 所有 provider 凭据不可解密);
- 轮换需「旧钥解密全部凭据 → 新钥重加密」(当前无自动脚本,轮换前规划停机 + 备份)。
5. **provider key 只走应用设置页**(加密落库),永不写进 env/compose/日志§1.4)。
6. **日志脱敏已内建**structlog 结构化日志按 CLAUDE.md「Logging」纪律**不记全 prompt/正文**(记长度/哈希)、
**不记 API key**。保持这一约束,别在自建反代/中间件里回退成明文全量日志。
7. **HTTPS 强制**§3别让凭据/正文走明文。
---
## 7. 可观测性与运维
### 7.1 健康端点
- `GET /``{"service":"ww-api","status":"ok"}`
- `GET /health``{"status":"ok"}`
两者均无鉴权、返回 200可作反代/编排的存活探测目标。
提醒api/web 的 Dockerfile 与 compose **未内置** HEALTHCHECK§1.5 缺口 5——存活探测靠反代/平台配。)
### 7.2 结构化日志
- 后端 structlog**生产设 `LOG_JSON=true`** 输出 JSON便于日志系统采集/检索。
- 每个请求带 `request_id`(贯穿 assemble→write→四审→accept 一条链),也进错误响应信封——
用户报错时可用它端到端 grep。
- 采集:`docker compose logs`,或把容器 stdout 接到你的日志栈Loki/ELK/云日志)。
### 7.3 备份 Postgres含凭据库
数据全在 `pgdata` 卷 + Postgres 里,**包含加密后的 provider 凭据**。定期逻辑备份:
```bash
export COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
# 逻辑备份(推荐)
$COMPOSE exec pg pg_dump -U writer -d writer -Fc > backup_$(date +%F).dump
# 恢复
$COMPOSE exec -T pg pg_restore -U writer -d writer --clean < backup_YYYY-MM-DD.dump
```
> **备份数据库还不够**`CREDENTIAL_ENC_KEY` 要**单独异地备份**——DB 里的凭据是密文,
> 没有这把钥匙备份也解不出§2.3 / §6。两者分开保管、都要备。
### 7.4 成本账本LLM 调用)
每次 LLM 调用都按 `ARCHITECTURE §4.8` 的字段集记结构化日志provider/model/tier/token 数/耗时/`request_id` 等),
喂成本账本。运维时用这些日志:
- 核对每章/每次生成的 token 与花费;
- 排查慢/失败调用(网关的路由/回退/熔断也在此可见);
-`request_id` 关联「一次写章」的整条链。
采集方式同 §7.2JSON 日志接入日志栈后按字段聚合)。
---
## 附:核心事实速查
- **服务/端口**`pg`(5432, 唯一健康检查 `pg_isready -U writer`) · `api`(8000) · `web`(3000)。
- **镜像**均本地构建api 上下文=仓库根web 上下文=`apps/web`standalone
- **必填 env**`CREDENTIAL_ENC_KEY`(启动即校验,缺失/非法则 api 起不来)。
- **迁移**`alembic upgrade head` 同时建业务表 + LangGraph checkpointer 表checkpointer 只在迁移建表,不在运行时。
- **provider key**:不进 env部署后在设置页录入、加密落库。
- **已知缺口**§1.5api 镜像缺 `alembic.ini`compose `api``CREDENTIAL_ENC_KEY``web``API_BASE_INTERNAL``NEXT_PUBLIC_API_BASE` 构建期内联api/web 无 HEALTHCHECK。
- **红线**:单用户无认证 → 不裸暴露公网pg 不对外CORS 禁 `*``CREDENTIAL_ENC_KEY` 备份+轮换。