# NovelWorld 部署指南 > 当前支持边界是管理员控制的私有单节点自托管预览。默认仅限 localhost;任何 > 非本机访问必须由管理员提供加密隧道或 TLS 边界。 > 默认栈没有通过公网托管所需的 TLS、CORS、滥用治理、内容政策、法务和持续运维 > 资格审查。完整边界见 [产品合同](./docs/PRODUCT_CONTRACT.md)。 Compose 默认发布 `127.0.0.1:80`。如需调整,在私有 `.env` 中显式设置 `NGINX_HTTP_BIND` 和 `NGINX_HTTP_PORT`。只有在管理员已配置加密私有网络或 TLS 入口时,才将绑定地址改为所需的私有接口;更改绑定地址或端口本身不会启用 TLS。 ## 系统要求 | 组件 | 最低配置 | 推荐配置 | |---|---|---| | CPU | 2 核 | 4 核+ | | 内存 | 4 GB | 8 GB+ | | 磁盘 | 20 GB SSD | 50 GB SSD | | 操作系统 | Windows 10/11、Ubuntu 22.04、Debian 12 | Windows 11、Ubuntu 24.04 | | Docker | 24.0+ | 最新稳定版 | | Docker Compose | 2.20+ | 最新稳定版 | --- ## 快速部署 服务端可选择下方的直接启动方式,或使用 [Jenkins 服务端部署](#jenkins-服务端部署可选)。 Jenkins 仅是可选的服务器部署入口;本地启动、桌面版和其他部署方式由用户自行选择。 ### 第 1 步:安装 Docker Windows 请安装并启动 [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)。 Linux 可运行: ```bash curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER newgrp docker ``` ### 第 2 步:克隆代码 ```bash git clone https://github.com/Wisdoverse/novelworld.git cd novelworld ``` ### 第 3 步:一条命令启动 ```bash # Linux ./start.sh ``` Windows 在命令提示符运行,或在资源管理器中双击: ```bat start.cmd ``` 首次交互启动会引导确认 L0 所需的 PostgreSQL 用户名和数据库名,自动生成数据库 密码,最后写入 `BOOTSTRAP_L0_COMPLETE=true`,并在任何容器启动前自动重启 启动器一次。有效的旧版或预配置 `.env` 会无提示迁移;未完成配置的非交互启动 会明确失败并要求预置 `POSTGRES_USER`、`POSTGRES_DB` 和强密码。 重启后脚本只自动生成 JWT、配置加密和服务间鉴权所需的 L1 启动根。 新安装默认持久化 `CACHE_MODE=postgres`,不生成 Redis 密码,也不启动 Redis。 脚本使用 Compose `--wait`,只在整个选定 profile 达到 readiness 后才打开 `http://localhost`。先创建唯一的首位管理员;DeepSeek/OpenAI 可稍后在设置页 配置,API Key 不会保存在浏览器中。 首次构建约需 5-15 分钟(Rust 编译较慢)。 ### 第 4 步:验证部署 ```bash # 检查基础 profile 状态 docker compose ps # 通过公开 Nginx 入口检查聚合健康 curl --fail http://localhost/health curl --fail http://localhost/ready # 查看日志 docker compose logs -f gateway # 按 .env 中的 CACHE_MODE 检查;Redis 仅在 redis 模式中是必需健康项 ./infra/ops/health-checks.sh ``` 默认仅在本机通过 `http://localhost` 访问;非本机访问必须先增加加密传输边界。 首次访问只需创建唯一的首位管理员。未配置 LLM 时基础服务仍可 ready, 国内模型 API、Coding/Token Plan 的中国版与国际版在 Settings 中分开选择; 各版本的端点、模型和 Key 说明见 [LLM 服务商配置](./docs/LLM_PROVIDERS.md)。 切换服务商、地区或套餐须重新填写 Key,套餐耗尽不会自动转到普通 API。 AI 操作返回明确的 `503 llm_not_configured`。LLM 密钥由 User Service 加密保存 或从环境变量读取,浏览器不会保存密钥。 ### 显式启用 Redis 投影 在 `.env` 中同时设置 `CACHE_MODE=redis` 和至少 16 位、包含至少 8 种字符的 URL-safe `REDIS_PASSWORD`(`A-Z a-z 0-9 . _ ~ -`),再重新运行 `start.sh` 或 `start.cmd`。脚本会由该唯一 mode 同时派生 Redis profile 和 `REDIS_URL`; 缺密码、占位值、未知 mode 或只选中一半都会拒绝启动。旧版启动器已 生成 Redis 密码但没有 `CACHE_MODE` 的安装,首次运行新脚本会一次性持久化 `redis`,避免升级后静默切换适配器。 Redis 为 Agent Service 提供可重建的消息投影,不承接小说解析队列。上传接收后 持久化的 PostgreSQL 导入任务通过 claim、lease 和恢复扫描等待解析槽位;解析 繁忙不阻止有接收容量的新书入队。批量上传最多选择 50 本,浏览器按每批最多 5 本、合计 40 MiB 顺序发送。共享书库可直接添加已有 Ready 小说,避免重新 上传和解析;尚未提供自动文件哈希去重。 ### 显式启用 RustFS 原文件存储 RustFS 使用已有 S3 适配器,由管理员独立部署;仓库 Compose 不会自动创建 RustFS 容器或 bucket。为其固定镜像 digest、配置独立持久化数据卷,并让 Novel Service 通过共同的私有 Docker 网络访问。无需向宿主机发布 S3 或 控制台端口;应用凭据与管理员 root 凭据分开保管。 例如容器网络别名为 `novel-rustfs`、S3 端口为 `9000` 时,在私有 `.env` 设置: ```dotenv S3_ENABLED=true S3_ENDPOINT=http://novel-rustfs:9000 S3_BUCKET=novel-world-uploads S3_REGION=us-east-1 S3_FORCE_PATH_STYLE=true ``` 先创建 bucket,再从本地秘密配置填写配套的 `S3_ACCESS_KEY` 与 `S3_SECRET_KEY`;长期应用账户的 `S3_SESSION_TOKEN` 留空。此 HTTP 示例仅用于 同主机私有容器网络;跨主机连接须提供受信任 TLS 边界。不要在容器里使用 `127.0.0.1` 指向另一容器,也不要提交 `.env` 或在命令输出中展示凭据。 应用账户仅需 bucket 上的 `s3:ListBucket`,以及该 bucket 的 `source-files/*` 对象上的 `s3:GetObject`、`s3:PutObject`、`s3:DeleteObject`。`ListBucket` 用于 `HeadBucket` readiness,不能加上只适用于列举对象的前缀条件而阻断 HEAD。 RustFS 服务账户继承父账户权限:给父 IAM 用户附加上述最小权限策略,或在创建 派生服务账户时附带限制性的 session policy;不要把服务账户当作独立 IAM 用户 直接绑定策略。须检查最终生效权限,见 [官方 IAM 说明](https://docs.rustfs.com/en/security-compliance/iam)。 按受管部署流程重建 Novel Service 后,验证其 `/ready`、用应用账户执行 HEAD 和一次私有测试对象的 PUT/GET/DELETE,并确认前缀外对象与其他 bucket 访问被拒绝。不得用真实小说解析来代替存储探针。启用只保留之后上传的原文件, 不会为历史上传补齐源对象;原文件保留不等于自动去重。已保留源对象的恢复、 删除仍需要此 bucket,停用前需明确这些任务的处理方式。PostgreSQL 备份不含 RustFS 数据卷,管理员须单独安排对象存储备份与恢复,见 [备份边界](./docs/BACKUP_RESTORE.md#scope)。 --- ## 同一世界剧情来源迁移 0036 0036 新增 Narrative 所有的来源操作记录和可空的回合来源坐标,不重写旧世界 JSON。首次明确进入下一幕后,session 使用 schema v2 和独立 active source context; 原始 entry、角色、规则和旧回合保留。使用现有 managed release 流程停止旧 Narrative,再发布客户端,停止并 drain Novel/Agent,执行 migration 后启动匹配版本。 新 UI 只能配合支持来源扩展的 Novel/Narrative 使用。 release 的五个 barriers 必须齐全。即使 0036 SQL 是 additive,也不能在世界扩展后 恢复旧 Narrative reader;rollback/marked restore 拒绝跨过 0036。保留数据库与操作 key,通过兼容版本向前恢复,不能删除 source_context、重置角色或手写 JSON 回退。 读取进度后退时只隐藏后续派生内容,不能删除已接入来源。 参见 [ADR 0013](docs/adr/0013-same-world-source-progression.md)。 --- ## Jenkins 服务端部署(可选) 仓库根目录的 [`Jenkinsfile`](./Jenkinsfile) 在 Linux 私有单节点服务器上构建并自动 部署六个应用镜像。代码、架构、测试和秘密扫描检查由现有 GitHub Actions CI 负责;触发 Jenkins 前,应确认选用的受信任分支或提交已通过 CI。每次成功构建都会运行现有 `start.sh`,随后执行 `infra/ops/health-checks.sh`,确认部署 readiness 和部署后健康状态。 部署依赖 Job 默认 workspace 中预先配置且跨构建保留的 `.env`;缺少时,镜像构建前 即失败关闭。Compose 使用该 `.env` 构建和启动,不使用示例配置。 若服务器需要本机网络或挂载配置,在同一 workspace 根目录保留私有的 `docker-compose.override.yml`。构建、启动和镜像核验都会由 Compose 自动读取该文件。 该文件与 `.env` 一样不进入 Git,也不进入根目录的 Docker 构建上下文;不要把它加入 构建归档。源码检查仍拒绝其他未忽略文件和已修改的受跟踪文件。 每次成功部署应在 Job 的默认 workspace 中归档 `.local/jenkins-deployment/deployment.tsv`:记录干净检出的完整 Git SHA,以及 `gateway`、`user-service`、`novel-service`、`agent-service`、`narrative-service` 和 `frontend` 各自的构建镜像 ID 与部署容器 ID。首次成功部署也按此方式记录;每次运行都应 先移除 workspace 中旧的未归档记录,并在 `start.sh`、readiness 和健康检查全部成功后 生成记录。健康检查后,应确认每个部署容器仍使用对应的构建镜像 ID,再归档完整记录; 失败时不得留下可被误认为成功的部分记录。若部署已成功但记录或归档失败,先检查当前 服务状态和镜像身份,再决定是否重试;此流程不会自动回滚或重置安装。 该记录只提供受信任单写入者部署流程的观察证据,不是隔离构建、可复现构建、签名或 正式来源证明;不得据此追认旧构建。记录中的 SHA 只证明已检出提交及其受跟踪文件干净, 不证明默认 workspace 内被忽略的本地文件、环境或 Docker 主机状态。 其中也包括 `docker-compose.override.yml`;运维者须单独审查和保留该配置。 1. 在目标 Linux 部署服务器安装 Git、Bash、OpenSSL、curl,以及带 BuildKit 的 Docker 和 Compose v2。Jenkins agent 用户必须能访问该服务器的 Docker daemon。 Jenkins 主机无需安装 Rust、Node.js、pnpm 或 Python;镜像所需的 Rust、Node.js 与 pnpm 工具链由 Dockerfiles 在容器内提供。 2. 创建 **Pipeline script from SCM** 作业,选择本仓库的受信任分支,Script Path 为 `Jenkinsfile`;需要 Jenkins Pipeline、Git 和 Timestamper 插件。此作业必须专用于 受信任分支,不运行外部 PR。由于流水线使用 `agent any`,所有符合条件的 executor 都必须位于同一 Linux 部署服务器并使用同一 Docker daemon。Pipeline 使用 Jenkins 分配给该 Job 的默认 workspace;确保它在该服务器上跨构建保持不变。 部署前应确认所选受信任分支或提交已通过 GitHub Actions CI;Jenkins 只负责镜像 构建和部署。 3. 在首次构建前,于 Job 的默认 workspace 内私下配置 `.env`:可复制 `.env.example`,填写有效的 `POSTGRES_USER`、`POSTGRES_DB` 和满足启动器要求的强 `POSTGRES_PASSWORD`, 设置 `BOOTSTRAP_L0_COMPLETE=true`,权限设为 `600`。启动器会生成缺少的 L1 根; 之后应保留这些值,LLM 可稍后在设置页配置。Redis 仍由 `CACHE_MODE` 决定。 不要启用 SCM 清理未跟踪文件,也不要清理 Job 的默认 workspace;`.env` 中的数据库 密码和启动根必须跨构建保留。不要把 `.env` 放入 Git、Jenkins 参数、日志或构建归档。 4. 确认已通过 GitHub Actions CI,准备维护窗口和已验证的数据库备份后,运行 Job。 Jenkins 调用 `start.sh` 先构建镜像;构建失败时旧容器仍保持运行。构建成功后才停止 旧 writer,再以不重复构建的方式启动完整栈并重放迁移。Compose 等待 readiness 后再 通过入口与容器健康检查。每次部署会造成停机;配置不完整、构建失败或健康检查失败 都会使作业失败。 已有源码安装接入前,先在私有 `.env` 中设置与该安装完全相同的 `COMPOSE_PROJECT_NAME`(当前安装为 `novel-world`),并保留原有的 `POSTGRES_USER`、`POSTGRES_DB`、`POSTGRES_PASSWORD`、`BOOTSTRAP_L0_COMPLETE`、 `JWT_SECRET`、`RUNTIME_CONFIG_KEY` 和 `INTERNAL_SERVICE_TOKEN`。这样 PostgreSQL 数据卷 仍由原 Compose project 管理,启动根也保持不变;不要执行数据卷迁移或重新初始化。 新建安装可不设置 `COMPOSE_PROJECT_NAME`,Compose 会沿用 Job 默认 workspace 的名称, 该 workspace 必须跨构建稳定。若 Jenkins agent 在容器中,确保 Docker daemon 可访问 到相同 workspace 路径,使构建上下文和 Compose bind mounts 指向正确位置。 健康检查默认访问 `http://127.0.0.1:80`;若该地址不能从 Jenkins agent 到达,可在私有 `.env` 配置未加引号的 `NGINX_URL=http://...`,指向该 agent 可访问的部署入口。该 URL 只用于 Jenkins 发起的部署后探测。每个服务器仍只配置一个作业,流水线禁止并发运行。 服务端仍默认仅在 localhost 访问,远程访问使用既有加密边界。 此入口沿用直接启动器的源码安装流程,没有自动回滚。遇到失败先排查迁移和健康 状态,不自动重跑或删除数据卷。存在 `.release` 的受管安装在检出前会被拒绝,仍按 下方的不可变 `release.env` 升级、确认与恢复流程处理;不可用源码启动绕过该契约。 --- ## D20 基础规则迁移 0030 0030 改变 Novel Service 的模板写入契约,旧 Novel writer 与新 schema 不兼容。 升级时先关闭入口写入并停止、排空 Novel 和 Narrative 两个服务;不能只停 Narrative。候选 release 必须包含完整的 0021、0024、0025、0030、0036 五个 required schema barriers;应用完整候选 release 后再启动兼容版本,避免旧 writer 在迁移后写入。现有 v1 profiles 和 sessions 保留原绑定,继续按 v1 读取; 不要重写为 v2。数据库只前向迁移,旧版 rollback 不受支持,故障恢复使用兼容 release 前向修复。此要求是 rollout 契约,不代表该迁移已在生产执行。 每次受管部署都会重放标准迁移目录。导入可在章节可用前原子创建默认 `reading_progress`;`current_chapter = 1` 只是元数据,不证明该章节存在。 迁移 0002 对 `pending`、`parsing`、`error` 且在公布章节数内没有有效章节的小说 保留进度及其偏好;`ready` 或状态缺失/未知而没有有效章节时必须失败关闭,且只可 按现有有效章节规范化。升级前先完成并验证 PostgreSQL 备份,不要手动删除或改写 进度记录来绕过迁移错误。当前一键 `start.sh` 会先构建所选 profile 的镜像;若构建失败, 旧容器仍保持运行。构建成功后才执行 `docker compose down`,再用 `up --no-build` 启动完整 栈并重放迁移;它不是只重开入口的命令。 迁移 0019 只在首次引入 `user_novels` 关系时回填上传者书架:它在创建关系之前记录该表 是否已存在,并在事务内锁定 `novels`。若表原已存在(即使为空),重放保留现有书架, 不会把已移出的书重新挂回上传者。此检查用于区分首次采用与重放,不会自动修复任意 历史部分迁移。`tests/integration/tests/legacy_migration.rs` 中的全迁移重放回归覆盖显式移除 后的保留行为;Issue #424 记录最终 CI 与 live 部署证据。 若受管停机后只需重开已经存在的 Nginx 容器,本机 Compose `start --help` 不提供 `--no-deps`;确认只返回一个 Nginx 容器 ID 后再使用: ```bash nginx_id=$(docker compose ps --all -q nginx) test -n "$nginx_id" && test "$(printf '%s\n' "$nginx_id" | wc -l)" -eq 1 && docker start "$nginx_id" ``` ## 生产升级与回滚 Rust 原生镜像的源码新鲜度属于构建阶段契约,不是部署脚本可补救的事项。 构建必须在现有锁定的 BuildKit Cargo target cache 内只清理 workspace release 产物(`cargo clean --locked --release --workspace`),保留第三方已编译依赖及 registry/layer cache,不清理宿主机共享 Cargo target。历史 git archive 的源码 mtime 可能让 Cargo 复用其他 revision 编译的 workspace 二进制;Docker `RUN` 成功本身不能证明二进制对应当前源码。发布证据必须证明 Cargo 实际编译了变更源码。 对注册候选 release pair,B 的真实 runtime 变更至少须使一个受影响应用镜像的 image ID 和至少一个 filesystem layer 均与基线不同;仅文档/缓存变化或未改变的 runtime 输入不要求每个镜像或二进制都变化。当前 #432 与 #429 的交付顺序是 A→C→B; 只有实际合并的 C 基线和其严格后继 B 候选、且两端都含该保护,才可参与 后续 artifact pair 资格验证。旧 A 基线和失败预览产物须保留为失败证据,且不合格。 `v*` Tag workflow 会先运行完整 CI,只发布以 Git SHA 标记的应用镜像。 全部镜像和 Windows/Linux/macOS 客户端构建成功后,同一 GitHub Release 会附加 `release.env`、SBOM、客户端压缩包、`desktop-SHA256SUMS` 和 `release-attestation.json`。发布文件的逐文件 provenance 验证流程见 [`SECURITY.md#release-file-provenance`](SECURITY.md#release-file-provenance)。 `release.env` 只允许版本、代码 SHA、六个应用镜像和三个经源码审批的 基础镜像 digest;不要部署单个镜像或使用 `latest`。 消费者必须使用独立复核取得的 source/signer SHA,并独立取得 trusted roots;不能 只信 `release.env` 或随 release 附带的 root。当前 `release.sh` 仍不会自动执行 provenance 或 deploy-time SBOM admission,相关 H2 证据仍待完成。 PostgreSQL、Redis、Nginx 的 digest 固定在 `docker-compose.yml`。普通应用发布 要求候选与当前 release 的三个 digest 完全一致,总是检查 PostgreSQL, 只在 `CACHE_MODE=redis` 时检查 Redis, 不会重建或降级它们。基础镜像变更必须作为独立基础设施变更,先完成数据库备份、 格式兼容与恢复演练;本脚本会拒绝把它混入应用发布。 Redis 7 到 8 是一次冷切换,不是普通 `release.sh upgrade`。本次切换只适用于 尚无用户的默认 Unix 预览环境。`CACHE_MODE=redis` 的受管安装使用以下一次性 流程;不要 `source` manifest: ```bash set -euo pipefail target=/absolute/path/to/redis8-release.env repo_root=$(git rev-parse --show-toplevel) cd "$repo_root" archive_root="$(dirname "$repo_root")/novelworld-release-archive" install -d -m 700 "$archive_root" ./infra/docker/release.sh validate "$target" docker compose --env-file .env --env-file .release/current.env --profile redis \ stop nginx gateway agent-service redis redis_volume=$(docker inspect --format \ '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}' novel-redis) redis_project=$(docker inspect --format \ '{{ index .Config.Labels "com.docker.compose.project" }}' novel-redis) test -n "$redis_volume" && test -n "$redis_project" test "$(docker volume inspect --format \ '{{ index .Labels "com.docker.compose.volume" }}' "$redis_volume")" = redis_data test "$(docker volume inspect --format \ '{{ index .Labels "com.docker.compose.project" }}' "$redis_volume")" = "$redis_project" docker compose --env-file .env --env-file .release/current.env --profile redis rm -f redis archive="$archive_root/redis7.$(date -u +%Y%m%dT%H%M%SZ)" test ! -e "$archive" mv .release "$archive" sync docker volume rm "$redis_volume" unset REDIS_IMAGE COMPOSE_PROFILES docker compose --env-file .env --env-file "$target" --profile redis pull redis docker compose --env-file .env --env-file "$target" --profile redis up -d --wait redis ./infra/docker/release.sh adopt "$target" ``` 仓库外的 `.release` 归档是删除卷前的单向提交点;之后不得恢复其中的 Redis 7 manifest。 `adopt` 会拒绝运行中 Redis 与目标 exact image 不一致的情况。失败时保持入口关闭 并只向 Redis 8 重试。不得使用 `down -v`,PostgreSQL 卷必须保留。 `CACHE_MODE=postgres` 没有运行中的 Redis,只需停入口和应用、归档旧 release 状态并重新 `adopt`。新安装直接采用 Redis 8。任何已有用户、Windows 原地升级 或需保留 Redis 投影的环境都不在本次批准范围内,必须另行设计并验证迁移。 自动回滚只适用于新格式 release。如果当前环境已经使用九镜像契约,首次采用前先 验证一份与实际代码和镜像完全一致的基线: ```bash ./infra/docker/release.sh validate /path/to/current-release.env install -d -m 700 .release install -m 600 /path/to/current-release.env .release/current.env ``` 旧版 all-in-one 环境无法建立上述基线。此时必须先保留并验证旧源码 bundle、精确 镜像和数据库备份的人工恢复路径,再执行一次性采用;按提示输入 `ADOPT-`。采用成功只建立 current,不伪造 previous,因此自动 rollback 要到下一次成功 upgrade 后才可用: ```bash ./infra/docker/release.sh adopt /path/to/artifact/release.env ``` 升级时把 artifact 解压到临时目录。脚本使用 `set -euo pipefail`,严格拒绝额外、 重复或缺失字段,从不执行或 `source` manifest;Compose 同时读取生产 `.env` 和 已验证的候选 manifest。候选只有在全部 readiness 成功后才原子提升为 `.release/current.env`,旧 current 才成为 previous: ```bash ./infra/docker/release.sh upgrade /path/to/artifact/release.env ``` 脚本先更新兼容前端并暂停。确认章节切换成功返回 `PUT /api/progress/:novelId`,设置状态在旧后端上明确显示暂不可用,且聊天请求携带 UUID v4 `Idempotency-Key` 后,输入脚本 显示的代码 SHA;随后才会执行迁移和后端更新。旧标签页会收到 `426 client_upgrade_required`,不会以请求体中的过期章节启动 LLM。新 Agent 只在 同一事务写入一对消息并完成 turn;`done` 是数据库提交确认。 升级中途失败时 current/previous 不会被覆盖。恢复最后成功的 current: ```bash ./infra/docker/release.sh restore ``` 成功上线后若需回滚,数据库 migration 保持前向兼容且不执行 down migration;脚本 部署 previous,通过 readiness 后再交换 current/previous: ```bash ./infra/docker/release.sh rollback ``` 回滚到本最小启动决策之前的 release 会在替换任何服务前拒绝,除非: `CACHE_MODE=redis`、Redis 密码能认证健康容器;并且有有效的 LLM 环境覆盖, 或仍运行且未使用环境覆盖的当前 User Service 能以内部身份访问既有运行时 LLM 端点。守卫只检查 HTTP 状态并丢弃响应体,不读取或输出密钥/密文。 --- ## 服务端口说明 | 服务 | 内部端口 | 对外暴露 | 说明 | |---|---|---|---| | nginx | 80 | ✅ 80 | 反向代理入口 | | gateway | 8080 | ❌ 默认关闭(可选 8080) | API 网关 | | user-service | 8001 | ❌ 内部 | 用户认证 | | novel-service | 8002 | ❌ 内部 | 小说解析 | | agent-service | 8003 | ❌ 内部 | 角色对话 | | narrative-service | 8004 | ❌ 内部 | 分支叙事 | | postgres | 5432 | ❌ 默认关闭(可选 5432) | 数据库 | | redis | 6379 | ❌ 默认不启动(`redis` profile) | 可选重建投影 | **生产环境建议**:关闭 postgres 和 redis 的对外端口映射,仅通过内部网络访问。 --- ## API 契约 部署文档不重复维护接口清单。当前规范见 [SPEC §10](./SPEC.md#10-api-contract), 当前支持范围见 [PRODUCT_CONTRACT.md](./docs/PRODUCT_CONTRACT.md)。 --- ## 可选的告警通知 已有监控使用 `docker-compose.monitoring.yml`。需要通知时,再叠加 `docker-compose.alerting.yml`。默认启动方式不要求配置或部署 Alertmanager。 把 `infra/monitoring/alertmanager.example.yml` 复制到仓库外的私有目录, 将示例 URL 换成已授权、支持 Alertmanager webhook 格式的 HTTPS 接收端。 目录权限保持 `0700`,文件须能被容器的 `nobody` 用户读取;私有目录内 的 `0644` 文件可通过只读单文件挂载读取。把绝对路径写入 `.env` 的 `ALERTMANAGER_CONFIG_FILE`。接收端密钥留在这个私有文件中。 先用 `amtool check-config` 验证,再启动通知。命令、超时与重试规则、 接收端去重责任和停用方法见 [告警通知操作说明](./docs/OPERATIONS.md#optional-operator-notifications)。 保留 `alertmanager_data` 卷才能保留通知记录和静默规则。不要用删除数据卷 来停用通知。Alertmanager 只与 Prometheus 共用专用桥接网络,不接入应用网络, 也不发布主机端口。该组件不代表真实值班接收或 SLO 已验收。 ## LLM 指标与发布预算 `user-service`、`novel-service`、`agent-service` 和 `narrative-service` 在各自的内部端口暴露 `/metrics`。公网 Nginx 对 `/metrics` 返回 404;由 内部 Prometheus 网络直接抓取服务端口。 指标按受控的 `service/provider/model/operation/mode/status` 标签记录逻辑 请求、实际 provider 尝试、重试、延迟、首 token、usage 缺失、输入/输出/ 缓存命中 token,以及 cached-input/uncached-input/output 计费 token。指标不 包含 prompt、URL、错误正文、用户或小说标识。设置页按当前 Key 从 Prometheus 查询近 30 天增量,以随构建发布、带官网来源和核验日期的价格快照重估已报告 计费类别。中国区人民币、国际区美元分别计算,无需汇率;峰谷/上下文/思考 档位缺少逐次信息时显示区间。未知单价不视为免费;Coding/Token Plan 单列 套餐月费报价,不由累计 token 推算实付费用或剩余额度。 `LLM_PRICING_USD_PER_MILLION` 和 `LLM_PRICING_CNY_PER_MILLION` 可显式覆盖 普通 API 的精确 provider/model 单价,同键冲突或套餐 token 单价使启动失败。 `USD_CNY_RATE` 仅用于旧版点估算字段的可选换算。报价核验、更新流程、未统计 费用和回滚边界见 [LLM 计费估算](./docs/LLM_PRICING.md)。这些设置不改变冻结 Diagnostic 的价格、预算或证据。 H3 发布样本使用版本化策略校验: ```bash python3 tools/llm-budget/verify.py \ --policy tools/llm-budget/policy-v2.json \ --metrics release-sample.prom \ --commit "$(git rev-parse HEAD)" ``` 样本必须来自一个完成的、有边界的发布测试窗口;进程重启会重置计数器。 `h3-llm-budget-v2` 使用 3 个 30 分钟 summary 分桶,使任一样本至少保留 60 分钟 (最多 90 分钟),覆盖 45 分钟发布任务上限; 若已启动 operation 的 output-token-limit 窗口为空,校验器会失败而不是把零当作优秀结果。 校验器会拒绝缺服务、缺 operation、usage 缺失、未完成请求、未知/敏感标签、 超出重试/错误/延迟/首 token/计费 token 预算或 provider 实际 token 上限的样本。 --- ## 常见问题 **Q: Rust 编译太慢怎么办?** A: 首次编译需要 5-15 分钟,后续增量编译很快。可以预先拉取 Rust 镜像: ```bash docker pull rust:1.82-slim-bookworm ``` **Q: 如何更换 LLM 提供商?** A: 创建首位管理员后,可在受保护的设置页选择 DeepSeek 或 OpenAI。高级部署 可在启动前设置 `.env` 中的 `LLM_API_URL`、`LLM_API_KEY` 和 `LLM_MODEL`, 然后重新运行启动器;环境配置优先于数据库设置并在网页中只读显示。 **Q: 如何启用 Laya (Jev) 行动建议和 D20 裁定?** A: 两者共用可选的 Laya (Jev) decision 服务,但用途不同:行动建议只分类 玩家显式请求的动作类型;配置成对的 `LAYA_API_URL` 与 `LAYA_API_KEY` 后, 高级 D20 回合还会发送受限的行动/角色上下文进行可行性与难度分类。该上下文 不含小说全文、历史、骰子或结果,仍可能包含玩家背景、能力、库存和目标显示名; 服务商的数据保留由其策略决定。裁定错误、低置信度或上下文超限会回退到模板 DC,服务器校验和骰子仍是权威,语义质量尚未认证。它不替代 DeepSeek 故事生成。 优先复用现有且可从两个容器访问的私有/共享 Laya;正常源码启动和 Jenkins 部署 不会提供或启动 Laya。只有明确需要独立实例时,才叠加 `docker-compose.laya.yml` 并运行下方的 `--profile laya` 命令。 让 Laya 服务与 `novel-service`、`narrative-service` 处于同一条私有 Docker 网络,在 `.env` 中填写 `LAYA_API_URL=http://:8000` 和 `LAYA_API_KEY`,然后重建并重启 Novel 与 Narrative,以启用 D20 裁定和系列匹配。地址必须分别从 Novel 和 Narrative 容器内可达;宿主机的 `127.0.0.1:18881` 在容器内指向 Narrative 自己。只使用私有网络, 不要公开 Novel、Narrative 或 Laya 端口。未同时配置 URL 和密钥时,行动 建议入口隐藏,系列识别返回未配置并保留手动关联。系列推荐需用户确认; Laya 失效时仍可手动选择系列或行动,高级检定回退到模板 DC。系列绑定需要 匹配的 Novel、Narrative 和前端版本;0031 和 0033 经正常受管停机迁移执行,且不新增 发布屏障。0033 允许先创建仅共享背景的系列;D20 规则由用户以后显式生成, 来源书的就绪规则只会固定一次。来源规则未就绪时,系列成员不能用各自的规则 代替。绑定状态产生后,旧版本回滚不受支持。 **Q: 如何显式运行或升级 NovelWorld 独立 Laya?** A: 仅在需要独立实例时,显式叠加 `docker-compose.laya.yml`,使用包含 `laya==0.3.20` 和已审查的 multilingual checkpoint 的不可变镜像。在根 `.env` 设置 `LAYA_IMAGE` 为镜像 digest(本机测试也可用精确 image ID)、`LAYA_MODEL_PATH` 为镜像内固定 revision 的本地 checkpoint 路径、`LAYA_API_URL=http://laya:8000`,保持既有密钥。先验证 镜像包版本、checkpoint 内容和权限;不要使用浮动 `latest`。该 endpoint 不下载 模型,不发布宿主机端口,只加载 multilingual,限制 CPU/内存/线程,并对会截断 state 的请求返回 422;问题头部和选项仍使用上游 token 预算。升级不会自动提升 识别准确率或改变 0.8 弃权阈值。 ```bash docker compose -f docker-compose.yml -f docker-compose.laya.yml --profile laya up -d laya ``` 验证健康、鉴权以及系列/行动/D20 协议兼容后,按正常部署流程更新 Novel 与 Narrative 的 URL;保留原共享服务和其他消费者。回滚只恢复这两个消费者原来的 URL 并重建,随后可停止本项目独立 Laya。不要删除共享模型缓存或其他项目容器。 系列识别只调用已配置的 Laya 服务。用户可主动点击 DeepSeek 补判,使用设置页当前 配置的 DeepSeek API 与现有计费路径;其他 provider 或 Diagnostic 绑定会被拒绝。 相同证据/候选/策略/模型复用 PostgreSQL 结果。每条 claim 最多一次实际请求,输出上限为 512 tokens(启用思考时包含思考 tokens), 不做 HTTP 重试、JSON fallback 或修复重发;未知结果也不自动重试。“查询结果”使用只读的 `check_only=true` 模式;证据或 模型配置改变时,也不会把查询变成新的付费请求。0032 缓存迁移 必须先于新版 Novel 启动,缓存不会改已有系列或 D20 绑定。本功能测试和部署不 构成执行新的付费 Diagnostic 的授权。 **Q: pgvector 扩展安装失败?** A: pgvector 是当前 schema 的必需扩展。不要修改 `init.sql` 绕过它;修复镜像或扩展 安装后重新执行迁移。项目目前不承诺无 pgvector 的生产降级模式。 **Q: 如何备份数据库?** ```bash docker exec novel-postgres pg_dump -U novel novel_world > backup_$(date +%Y%m%d).sql ``` 以上示例未加密、未做完整性校验,也未经恢复演练验证。已批准的恢复目标 (RPO/RTO)、加密与完整性要求、保留上限和擦除重放契约见 [`docs/BACKUP_RESTORE.md`](docs/BACKUP_RESTORE.md);配套的脚本化备份/恢复 工具随该政策的实现变更交付。 --- ## 私有部署安全基线 1. **网络**:保持在 localhost;非本机访问使用管理员维护的加密隧道或 TLS,且不要 把默认 Nginx 直接暴露到公网。 2. **端口**:只开放私网入口,关闭 5432/6379/8080 的外部映射。 3. **密钥**:使用启动脚本生成的随机值并限制 `.env` 文件权限。 4. **备份**:制定并演练 PostgreSQL、S3 和操作员日志的恢复与删除策略;示例 `pg_dump` 命令不是已验证的 RPO/RTO。 5. **监控**:从内部网络抓取各服务 `/metrics`;Nginx 继续对公网路径返回 404。 公网托管需要 H2 的独立安全、隐私、内容、滥用、供应链、TLS/CORS 和恢复审查; 上述基线不能替代该资格门槛。 ### Community series suggestions (migration 0035) Apply additive migration 0035 before starting the matching Novel/frontend pair; normal managed migrations and desktop startup include it. Existing series do not contribute until their owner opts in. No provider credential or additional configuration is needed. Rolling back the frontend/API keeps the additive table and all prior private backgrounds, rules and frozen sessions intact; no down migration is required. See [ADR 0012](docs/adr/0012-opt-in-community-series.md).