新增品牌维护页和404页面并补齐网关路由 将临时维护公告迁到release外运行态覆盖并增加构建门禁 同步Nginx、Pingora、部署脚本与生产运维文档 隐藏移动端顶部SEO介绍并保留桌面端与H1语义 增加维护页、404页和移动端回归测试
156 KiB
本地开发验证与生产运维
更新时间:2026-06-12
标准开发流程
同步代码 -> 读 AGENTS.md / docs/project-memory 共享记忆 -> 查当前 docs -> 小步实现 -> 本地验证 -> 更新 docs / project-memory -> 提交
如果当前文档不足以指导编码,先补文档再落地工程修改。
本地启动
安装依赖:
npm install
完整联调:
npm run dev
该命令启动:
- SpacetimeDB standalone。
- Rust
api-server。 - 主站 Vite。
- 后台 Vite。
npm run dev 和单模块 npm run dev:web、npm run dev:api-server、npm run dev:spacetime、npm run dev:admin-web 启动后都会更新根目录 .app/dev-stack.json。该文件记录本次命令、数据库、更新时间,以及 spacetime、api-server、web、admin-web 的 pid、监听 host / port、可访问 URL、启动状态和当前命令。.app/ 是本地运行态目录,不提交 Git;端口漂移、服务重启或子进程退出后以该文件里的实际状态为准。
单独启动主站前端:
npm run dev:web
单独启动 Rust API server:
npm run dev:api-server
Linux 本机多用户并发开发时,npm run dev 和 npm run dev:* 单模块命令会先在系统级端口段注册表里给当前用户分配一个端口段,再把该段映射为 web = start、api = start + 1、spacetime = start + 2、admin-web = start + 3。默认注册表目录是 /var/tmp/genarrative-dev-port-ranges/,其中 registry.json 记录各用户的活跃段,registry.lock 负责串行化分配;可以用 GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR 覆盖目录。系统自动分配时从 10000-10099 开始,每次占用 100 个端口块,后续块按 10100-10199、10200-10299 递增;GENARRATIVE_DEV_PORT_RANGE 或 --port-range 只在 Linux 上生效,Windows 仍按原来的 3000 / 8082 / 3101 / 3102 端口探测与漂移逻辑运行,不读这个系统级注册表。
后端日志默认写入 logs/api-server/。后端 API smoke 使用 npm run dev:api-server 并检查 /healthz;需要确认实例可接生产流量时检查 /readyz。不要使用旧 api-server:maincloud 或任何 GENARRATIVE_SPACETIME_MAINCLOUD_* 口径。
Windows 本地 npm run dev / npm run dev:api-server 会用空的 RUSTC_WRAPPER / CARGO_BUILD_RUSTC_WRAPPER 覆盖 server-rs/.cargo/config.toml 里的 sccache,从而直连真实 rustc。不要把 wrapper 绕过值写成 rustc;Cargo 会按 wrapper 协议调用 rustc <真实rustc路径> - ...,最终报 multiple input filenames provided 并导致 api-server 无法启动。排查本地启动失败时,先看 dev 日志是否出现该错误,再确认脚本注入的 wrapper 为空。
Windows 本地如果已在 %LOCALAPPDATA%\Genarrative\ffmpeg\bin 安装 FFmpeg,npm run dev / npm run dev:api-server 会自动把该目录加入本次 api-server 子进程 Path,并注入 CHARACTER_ANIMATION_FFMPEG_PATH / CHARACTER_ANIMATION_FFPROBE_PATH 的绝对路径。这样即使外层终端或长期运行的 dev 进程是在安装 FFmpeg 之前启动,角色动画抽帧也不会继续因为 ffmpeg: program not found 失败;若手动配置了上述环境变量或 GENARRATIVE_CHARACTER_ANIMATION_* 前缀变量,显式配置优先。
开发态 npm run dev 与 npm run dev:api-server 会默认注入 GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true 和 GENARRATIVE_PROCESS_ROLE=all,因此密码登录在本地开发环境可直接注册未知手机号账号,且本地 api-server 会同时监听 HTTP 并消费外部生成队列;显式设置 GENARRATIVE_PROCESS_ROLE 时保留显式值。Linux 本地默认 all 角色启动前,dev 脚本会停止当前仓库、同一个 SpacetimeDB server / database 下遗留的 GENARRATIVE_PROCESS_ROLE=external-generation-worker 进程,避免旧 worker 二进制继续抢同一条队列并在业务写回时制造 procedure 超时;显式拆分 api / external-generation-worker 做生产式验证时不会触发这项清理。生产环境仍按 api-server 配置默认关闭密码自动注册,并由独立 worker 进程消费队列。
本地排查外部内容生成 worker 队列时,默认同一 Rust 进程同时监听 HTTP 并消费 external_generation_job 队列;更接近生产的验证应分别启动 api、external-generation-worker 和 external-generation-controller。生产默认 GENARRATIVE_PROCESS_ROLE=api,外部生成任务由独立 GENARRATIVE_PROCESS_ROLE=external-generation-worker 进程消费;生产与容器扩缩容验证保持 queue。当前进入持久队列的外部生成动作包括:拼图 compile_puzzle_draft / generate_puzzle_images / generate_puzzle_ui_background,跳一跳 compile-draft / regenerate-tiles,拼消消 compile-draft / regenerate-atlas,敲木鱼 compile-draft / regenerate-hit-object,以及图片画布 editor_image_generation / editor_image_edit / editor_background_removal / editor_icon_spritesheet_generation / editor_ui_design_asset_extraction / editor_character_animation_generation / editor_video_generation / editor_sound_effect_generation / editor_background_music_generation。非外部 provider 生成动作继续 inline,不进入队列。显式把本地进程角色设为 api 且没有 worker 时,HTTP 只返回 queued/running,不会兜底执行外部 provider。
生产拆分角色时,external-generation-worker 和 external-generation-controller 的专属 env 示例会把 GENARRATIVE_SPACETIME_POOL_SIZE 覆盖为 1;非 HTTP 角色只保留 external_generation_job 队列窄订阅作为响应式唤醒信号,实际抢占和扩缩容判断仍走 SpacetimeDB procedure,且不再订阅 API 读模型连接池。worker / controller 不执行模型定价 seed,启动时先调用受 runtime writer 鉴权的 queue-stats procedure 做只读预检,身份不匹配时 fail-fast;当前正式 systemd unit 通过共同加载 /etc/genarrative/api-server.env 继承同一 GENARRATIVE_SPACETIME_TOKEN,专属角色 env 示例不重复配置该 token。GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS 与 controller poll interval 只作为订阅失效、漏事件和 lease 过期这类时间条件的兜底,不作为正常领取任务的主路径。
生产 worker 默认 GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS=600,只覆盖 worker 心跳抖动和短暂断连窗口,不再把 lease 当成完整任务时长;默认 GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS=900,角色动画 / 视频类长任务使用 GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800。worker 在单次尝试超过执行预算后会停止当前尝试、写入失败 / 重试状态并释放 worker 槽位;如果 SpacetimeDB 当时不可写,当前租约最多再保留到 lease 过期,之后任务重新变为可领取。生产部署和 provision 脚本会给 /etc/genarrative/api-server.env 与 /etc/genarrative/external-generation-worker.env 补齐这些变量;已有自定义值不覆盖,只会把历史旧默认 3600 迁移为 600。
lease 过期后不代表任务一定再次执行:claim transaction 只有在 attempt < max_attempts 时才会递增 attempt 并返回 worker;如果过期的是最终 attempt,则直接把 job 收口为 failed、清理 lease,并按入队冻结价格为当前 attempt 原子退款或写 cancellation intent。该终态任务不会再次进入 provider executor,迟到 consume 会被 settlement intent 拒绝。
图片画布角色图、图标素材和 UI 素材提取在绿色 / 蓝色幕布去背景时优先调用 BgFilter;默认 GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS=180000,连续失败达到 GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3 后熔断 GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300 秒。BgFilter 调用失败和熔断期均先走阿里云通用抠图,只有阿里云失败才走本地幕布色去背景兜底。阿里云这层默认 GENARRATIVE_ALIYUN_MATTING_ENABLED=true,但必须在 api-server.env 填入 GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID / GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET(或标准 SDK 命名 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET)才会真正启用;AccessKey 缺失时启动日志会打印「阿里云抠图 AccessKey 未配置,跳过抠图客户端初始化」,抠图直接塌成 BgFilter→本地两级,npm run check:api-server-env 也会给出对应告警。修改这些变量后需要重启对应 api-server / worker 进程;排查时先从 worker 启动日志确认 lease 和 job timeout,再看 editor_bgfilter_request_start、editor_bgfilter_fallback_to_aliyun_matting、editor_bgfilter_circuit_open_fallback_to_aliyun_matting,以及阿里云失败后的 editor_aliyun_matting_fallback_to_local_screen_background_removal 日志。
我的 页签或排障面板展示队列等待时,只读取 BFF 队列接口:GET /api/runtime/external-generation/queue-overview 查看当前用户可见队列概览,GET /api/runtime/external-generation/jobs/{jobId} 查看单 job 状态。生成页 / 进度页不承接队列概览,只展示当前玩法业务进度;队列接口只提供等待 / 运行 / 失败 / 完成状态补充,最终草稿、作品和结果页仍要轮询对应玩法 session/detail 接口收敛到 ready 或 failed;不要直接查询 external_generation_job private table,也不要把 worker 内部 payload 暴露到前端。
外部生成任务摘要投影与历史 payload 维护使用 npm run spacetime:external-generation:maintain -- ...,且只能由已授权 migration operator 的 SpacetimeDB CLI 登录态执行。脚本默认 dry-run、每次只处理一批,绝不自动循环全表;--apply 才写入。先发布包含 external_generation_job_summary 与 cursor 索引的 SpacetimeDB 模块,在维护模式内对事故时间以前的编辑器终态任务执行小批 dry-run,例如 npm run spacetime:external-generation:maintain -- --database <database> --server-url <url> --limit 5 --completed-before-micros <micros>;核对 matched_count、before_bytes、after_bytes 和 inline_media_count 后,保持本批输入 cursor 不变并追加 --apply 重跑同一批,即使最后一批 has_more = false,只要 dry-run 仍有 matched_count / selected_count 也必须 apply;只有 apply 成功后才使用它返回的 next_cursor_job_id 继续。B-tree cursor 的选择阶段最多反序列化 limit + 1 行,apply 会再按主键逐条读取选中行但不会同时保留整批 payload;如怀疑存在单行异常巨型历史 JSON,先用 --limit 1。payload 压缩硬限制 source_module = editor-canvas;终态压缩完成后,用 --backfill-summaries 先 dry-run、再 --apply 分批补齐仍缺失的活动任务或无内联媒体历史任务摘要,直到 has_more = false,最后再切换使用 summary procedure 的 api-server。Stdb 构建 artifact 和完整 release 包都必须包含 scripts/spacetime-maintain-external-generation-jobs.mjs 与 scripts/spacetime-migration-common.mjs。首次上线不得让 Full Build 从 Stdb 自动直落 API:STDB_API_ROLLOUT_MODE 默认 fail-closed 为 pause-after-stdb,必须填写受限的 STDB_API_ROLLOUT_APPROVERS;Stdb Publish 通过 KEEP_MAINTENANCE_MODE 保持维护文件并停止旧 API/controller/worker,暂停点最多等待 4 小时,完成上述维护并确认无后续批次后才由指定审批人放行 API。定时构建缺少审批人时必须在发布前失败,不能静默退回 normal;也可分开运行 Stdb publish、维护、API deploy 三个受控 Job。任一批次都不得处理 pending / running payload;不要用 runtime writer、bootstrap secret 或匿名 identity 代替 migration operator,也不要在未核对 dry-run 时直接 apply。
自 2026-07-11 起,Genarrative-Full-Build-And-Deploy 的每日 04:00 timer 默认以 DEPLOY_TARGET=development、STDB_API_ROLLOUT_MODE=normal 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate。三个下游 Build 都由 Full Job 显式传 PUBLISH_AFTER_BUILD=false,不得依赖下游 Job 默认值或提前各自发布;统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 pause-after-stdb,且必须配置 STDB_API_ROLLOUT_APPROVERS。上文“定时构建缺少审批人时失败”的旧口径不再作为当前 dev 定时发布行为。
Full Job 通过 EXIT_MAINTENANCE_MODE_AFTER_COMPLETION 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 KEEP_MAINTENANCE_MODE=true,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 Exit Maintenance 阶段。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。Genarrative-Api-Deploy 也单独暴露 KEEP_MAINTENANCE_MODE 参数,并转换为随发布包脚本的 --keep-maintenance-mode;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。
需要验证“更新 API 不停 worker”和“worker 是否持续消费队列”时,优先使用隔离容器 smoke:npm run container:worker-smoke -- smoke。该脚本生成 gitignored 的 deploy/container/worker-smoke/api-server.env,启动独立 compose project 与独立 SpacetimeDB,发布当前 spacetime-module 后写入 worker_smoke_unsupported 测试 job;预期 worker claim 后执行 unsupported 失败分支,再执行 API-only recreate 并确认 worker 容器 ID 不变,最后再次入队验证 API 更新后队列仍可消费。external_generation_job 是 private table,脚本通过 worker 日志确认 job_id 被消费,不用 CLI SQL 查询私表。该 smoke 不读取 .env.local,也不依赖真实 VectorEngine / OSS 密钥;真实生图链路联调再在本地私有 env 中补齐 provider 配置。worker-smoke 默认把本机 spacetime CLI 打成轻量 SpacetimeDB 镜像,避免本机首次 smoke 依赖官方大镜像下载。若容器内 Cargo 拉取 crates.io 依赖不稳定,可用 npm run container:worker-smoke -- smoke --local-binary 让容器内 Cargo 复用本机 Cargo 缓存构建当前二进制,再打入 Debian bookworm smoke runtime 临时镜像;可用 GENARRATIVE_WORKER_SMOKE_LOCAL_BASE_IMAGE 覆盖运行时基础镜像;若隔离端口或库数据需要重建,追加 --force。完成 queue 链路验证时,还要用队列概览 BFF 和单 job 状态接口确认 job 从 queued/running 收敛,并用对应玩法 session/detail 接口确认业务状态同步完成。
本地只做账号/UI smoke 且需要短信登录时,SMS_AUTH_PROVIDER 应显式设为 mock,并把 SMS_AUTH_MOCK_VERIFY_CODE 设为固定值(当前常用 123456),再重启 npm run dev 或 npm run dev:api-server。如果 .env.local 还保留 SMS_AUTH_PROVIDER=aliyun,POST /api/auth/phone/login 用 mock 验证码会稳定报“验证码错误”,不是前端表单问题。真实短信联调再切回 aliyun 并重启。
微信小程序虚拟支付使用 WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID、WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY、WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY 和 WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV 配置。小程序充值统一走 wechat_mp_virtual / wx.requestVirtualPayment:泥点属于代币(coin),buyQuantity 按当前充值商品快照里的 points_amount 传;会员和后台新增道具类商品走 short_series_goods,productId 对应微信后台道具 ID。旧登录快照若缺 session_key,需要用户在小程序内重新登录后再支付;客户端成功回调不是最终到账,仍以后端通知或查询确认订单为准。详细口径见 docs/【技术方案】微信虚拟支付接入-2026-05-26.md。
普通微信充值订单本地有效期为 5 分钟。SpacetimeDB 原生 profile_recharge_order_expiration_timer 到点后只把仍为 pending 的订单改为 expired;HTTP api-server 只订阅活跃 timer 表的删除事件,按事件中的 order_id 重新读取订单并仅对 expired 执行微信查单补偿,不订阅完整充值订单历史表。支付或主动关闭也会删除 timer,但读取到非 expired 后直接忽略;监听断线窗口由未检查过期订单 catch-up 补齐。external-generation-worker 和 external-generation-controller 不运行充值过期逻辑,也不应因为扩容外部生成 worker 放大微信查单或关单流量。查账时本地未支付终态保持 expired,不再改写为 closed;expiration_checked_at、expiration_provider_state、expiration_last_error 用于判断 HTTP 监听器是否已经完成补偿。
微信小程序订阅消息生成结果通知使用 WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED、WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID 和 WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE 配置。当前模板为 AI创作生成结果通知;H5 在生成动作发起前先进入生成进度态并立即继续生成动作,同时非阻塞跳转到小程序原生订阅授权页尝试请求授权,用户接受、拒绝或返回都不能阻塞生成,且原生页不改写上一页 webViewUrl,避免返回后丢失 H5 当前进度页状态。后端只在玩法草稿生成成功或失败终态后用微信登录保存的 openid 调用 subscribeMessage.send,发送失败只打 warning,不影响生成主链路。模板 thing1 字段发送玩法模板名,例如 拼图、敲木鱼、抓大鹅;number6 字段发送本次生成结算后的实际泥点扣除,失败退款后固定为 0。模板 time4 字段固定发送北京时间 YYYY-MM-DD HH:mm,不要使用内部微秒时间戳、秒级时间戳或带时区后缀的 RFC3339 字符串,否则微信会返回 argument invalid! data.time4.value invalid。当前已接入拼图、敲木鱼、抓大鹅、跳一跳、方洞、视觉小说的草稿生成终态;分槽素材生成或发布动作不得直接复用生成结果通知,避免一次作品生成产生多条订阅消息。
如果本地 GET /api/creation-entry/config 返回 No such procedure,或 api-server 日志出现 no such table: puzzle_gallery_card_view / no such table: wooden_fish_gallery_card_view 这类公开 view 缺失,通常是 .env.local 指向的 SpacetimeDB 库还没有发布当前 spacetime-module,或当前 CLI 身份无权发布该库。debug 构建的 api-server 会临时使用后端默认入口配置兜底,避免创作作品架整块消失;正式修复仍应切换到拥有目标库权限的 SpacetimeDB 身份后重新运行 npm run dev 完成发布,或用 gitignored 的 spacetime.local.json 指向可发布的本地库。
本地排查 schema 漂移时,先用当前 dev server 显式查询目标库,例如:
spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --server http://127.0.0.1:3101
如果旧 .env.local 仍指向缺少当前 view 的库,例如 xushi-p4wfr,而当前可发布库已经包含这些 view,可在 gitignored 的 spacetime.local.json 写入 {"database":"genarrative-dev-codex"} 作为本机覆盖;写入时不要带 UTF-8 BOM,否则 scripts/dev.mjs 会忽略该文件。修改后重启 api-server,再检查 /healthz 和 /api/runtime/puzzle/gallery。
本地 npm run dev:spacetime 发布模块时必须显式忽略仓库根目录的 spacetime.json,由脚本固定追加 --no-config 并使用命令参数里传入的数据库名和 --server http://127.0.0.1:3101。否则 CLI 可能把发布目标改写到配置文件里的其他数据库,导致 dev:spacetime 启动后又因发布失败自动退出,浏览器随后会在 ws://127.0.0.1:3101/v1/database/.../subscribe 看到连接拒绝。
本地 spacetime CLI / standalone 版本必须和 server-rs/Cargo.toml 里锁定的 spacetimedb 版本一致;当前统一版本为 2.6.0。若版本错配,procedure 返回值可能在宿主侧触发 Failed to BSATN deserialize procedure return value,api-server 最终表现为敲木鱼等创作动作的 SpacetimeDB procedure 调用超时。排障时先运行 spacetime --version,再对照 server-rs/Cargo.toml 的 spacetimedb = "...";遇到版本不匹配时不要继续深挖业务超时,直接执行 spacetime version install <version> && spacetime version use <version>,或在目标就是最新版本时执行 spacetime version upgrade,升级后重启 npm run dev:spacetime 再重试。当前 scripts/dev.mjs 会在启动和复用本地 SpacetimeDB 前写入并校验 dev-spacetime-tool-version,避免把旧 standalone 继续带进新一轮创作。
本地 .env、.env.local 或 .env.secrets.local 修改后必须重启 api-server 才会生效;若已经通过 npm run dev 启动完整联调,可在该终端输入 rs api-server。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 VECTOR_ENGINE_BASE_URL、VECTOR_ENGINE_API_KEY 和 VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine gpt-image-2 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 server-rs/crates/platform-image;api-server 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS 下限收口到 1000000,旧进程仍可能沿用重启前的短超时。若 VectorEngine 在 send() 阶段失败且日志显示 SendRequest,先看同一 request_id 的 provider 日志字段 source、source_chain、source_chain_depth,再查 external_api_call_failure.metadata_json.errorSource;当前 multipart /v1/images/edits 单独强制 HTTP/1.1。拼图关卡资产按 level_scene -> ui_spritesheet -> level_background 顺序生成,日志会带 slot、asset_kind 和 elapsed_ms。
VectorEngine 图片生成 / 编辑在 request_send 阶段出现 timeout、connect、libcurl 35 SSL connect reset、libcurl 56 receive error / unexpected eof while reading、recv failure 等临时传输错误,或在 upstream_status 阶段收到 408 / 429 / 5xx(例如 Nginx HTML 502 Bad Gateway)时,platform-image 会对同一请求最多发送 5 次;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。日志中 VectorEngine 图片请求发送失败,准备重试 或 VectorEngine 图片上游状态可重试,准备重试 表示本次失败已进入下一次尝试;最终仍失败时才会写入 external_api_call_failure 并返回 504 / 502。排查生产失败时应同时统计 retry 前的尝试日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。
拼图入口直创的 compile_puzzle_draft 是长耗时链路:后端会先快速编译草稿并返回 image_refining / generating 快照,然后在 api-server 后台任务中完成首图、UI 资产、OSS 持久化、作品投影、计费退款和失败态回写。生产排查小程序 Failed to fetch 时,若 Nginx access log 里 action POST 是 499、upstream_status=-,说明客户端或 WebView 先断开;此时不应再把长 POST 是否返回作为生成成败依据,而应继续按实际 session_id 查后台任务日志、VectorEngine provider 日志、external_api_call_failure 和后续 GET 轮询结果。同一用户可能先轮询旧的 puzzle-session-*,随后 POST 新建实际生成 session;必须用 action POST 的 request_id 和 /api/runtime/puzzle/agent/sessions/<session_id>/actions 路径对齐真实失败请求,避免被前端显示的“来源草稿”误导。
查看本地 Rust / SpacetimeDB 日志:
npm run dev:rust:logs
后台前端:
npm run admin-web:dev
npm run admin-web:build
npm run admin-web:typecheck
常用检查
npm run check:encoding
npm run check:spacetime-schema
npm run check:production-ops
npm run check:server-rs-ddd
npm run lint:eslint
npm run typecheck
npm run test
npm run build
npm run check:content
一期创作流程统一化新增 quality-gates/ 提交前门禁。涉及拼图、抓大鹅、敲木鱼统一创作页、统一生成页或 dev 栈启动脚本时,先执行 quality-gates/README.md 列出的脚本,再按对应门禁文档完成体验检查。
综合检查:
npm run lint
npm run check
npm run build 由 scripts/build-gate.mjs 串行构建主站和后台;该门禁会把 Vite warning 当成失败处理。若看到 Build gate failed because warnings were emitted,先看 warning 原文,例如 chunk 体积超过 vite.config.ts / apps/admin-web/vite.config.ts 的 chunkSizeWarningLimit,不要先按 Rust 编译失败排查。
视觉小说负向扫描与验收门禁:
npm run check:visual-novel-vn11
npm run check:visual-novel-vn12
SpacetimeDB bindings:
npm run spacetime:generate
CodeGraph 本地代码索引
项目已安装 @colbymchenry/codegraph 作为开发期依赖,用于在本地生成语义代码索引,辅助 AI / IDE 做符号搜索、调用关系和影响范围分析。索引目录为 .codegraph/,其中 config.json 可提交,数据库、缓存和日志由 .codegraph/.gitignore 保持本机私有。
项目文档 RAG 索引使用 scripts/rag/ 下的脚本和本地 .rag/ 运行时目录,主要供 Agent 检索项目上下文,不作为人工阅读入口。默认不安装 RAG 相关依赖,不把 LanceDB、Transformers.js 或本地 embedding 模型写入根 package.json;需要启用时,Agent 必须先询问用户是否安装,并在用户确认后只安装到 gitignored 的 .rag/runtime/。索引范围默认包含 AGENTS.md、CONTEXT.md、docs/project-memory/ 和 docs/,不把 .hermes/ 工具目录作为项目知识库索引源。
首次拉取或需要重建索引时:
npm install
npm run codegraph:init
日常使用:
npm run codegraph:status
npm run codegraph:sync
npm run codegraph:index
Codex 项目级 hook 已放在 .codex/config.toml 与 .codex/hooks/:
PreToolUsehook 会在 Codex 准备执行git commit前运行node .codex/hooks/pre-submit-compile-check.mjs,依次执行npm run typecheck、npm run admin-web:typecheck、cargo check -p api-server --manifest-path server-rs/Cargo.toml,发现编译错误会阻止本次提交。PostToolUsehook 会在 Codex 工具修改文件后运行node .codex/hooks/post-edit-codegraph-sync.mjs,执行npm run codegraph:sync刷新本地语义索引。- 如果某个 Codex 客户端版本尚未自动加载项目级 hook,可先手动运行
node .codex/hooks/pre-submit-compile-check.mjs与node .codex/hooks/post-edit-codegraph-sync.mjs;个人模型、token、MCP server 仍放在个人~/.codex/config.toml,不要提交。
若要把 CodeGraph 接到 Codex CLI / Cursor / Claude Code 等 MCP 客户端,按本机 agent 配置执行 codegraph install 或参考 codegraph install --print-config codex 输出;不要把个人全局 agent 配置、token 或本机绝对路径提交到仓库。
后端改动验收
后端代码修改后,按变更范围选择:
cargo test -p <crate> --manifest-path server-rs/Cargo.tomlcargo test -p platform-image --manifest-path server-rs/Cargo.tomlcargo check -p api-server --manifest-path server-rs/Cargo.tomlcargo check -p spacetime-client --manifest-path server-rs/Cargo.tomlcargo check -p spacetime-module --manifest-path server-rs/Cargo.tomlnpm run check:server-rs-dddnpm run dev:api-server后请求/healthz
其中推荐页匿名游玩与 work_play_start 相关改动,至少要补跑:
cargo check -p api-server --manifest-path server-rs/Cargo.toml
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend tab enters runtime without login modal|logged out desktop recommend page renders runtime directly|logged out desktop recommend rail enters runtime without login modal"
涉及 SpacetimeDB schema 时必须补:
npm run spacetime:generate
npm run check:spacetime-schema
前端改动验收
前端修改后,根据范围选择:
npm run check:encodingnpm run lint:eslintnpm run typechecknpm run test -- <具体测试文件>- 移动端视口人工检查或截图检查
UI 相关修改要重点验证:
- 390px 左右移动端宽度不横向溢出。
- 输入法弹出时平台画布不被压缩。
- 弹窗、抽屉和独立面板没有实现成当前面板下方展开。
- UI 不包含默认规则说明长文。
- 私有图片和音频不裸请求
/generated-*。
SpacetimeDB 操作规则
- 不在人工命令、本地联调或文档示例中使用
spacetime --root-dir;CI/CD 脚本内部为隔离运行用户登录态的受控用法例外,但不得写成手工排障命令。 - 本地开发使用项目脚本维护数据目录;需要清空本地数据时先确认可丢弃,再停止服务并处理本地数据目录。
- 发布目标必须显式
--server/--server-url。 - 身份问题先查
spacetime login show、spacetime server list和目标库权限,不通过切回旧 Node / PostgreSQL 绕过。 - 旧库迁移或 private 表数据保留走
migration.rs的 JSON 导入导出和分片导入思路。 - Jenkins 数据库导入 / 导出流水线会先加载
scripts/jenkins-prepare-toolchain-env.sh,显式补齐 Jenkins 用户的 Node、Cargo、SpacetimeDB 工具链目录;如果目标机器安装路径不同,用GENARRATIVE_JENKINS_TOOL_PATHS传入额外bin目录。 - 本地
npm run dev/npm run dev:api-server若没有显式GENARRATIVE_SPACETIME_TOKEN,会在 SpacetimeDB 就绪后调用/v1/identity创建专用 Web API identity token,并按 server 写入 gitignored 的<spacetimeDataDir>/dev-api-identities/<serverSha256>.json;运行时 bootstrap secret 固定为 64 位十六进制,按 server + database 写入<spacetimeDataDir>/dev-runtime-service-bootstrap-secrets/<scopeSha256>.json。两个文件都必须是普通文件且权限为0600,后续启动持久复用,使 API token、WASM 构建摘要和首次 writer 授权保持同一身份链路;它们只注入api-server,不写回.env.local,也不传给 Web / Vite。启动日志只打印 identity 前缀或是否复用,禁止打印 token / secret 明文;若仍出现subscribe ... 401 Unauthorized,先确认是否绕过了项目 dev 脚本、记录文件是否因权限或作用域不匹配被重建,以及是否连接到非本次启动的 SpacetimeDB server。 - 独立运行
npm run dev:admin-web时,admin Vite 仅在 serve 模式把仓库根public/挂到/admin/base 下;后台源码引用共用 public 资产时,开发态必须基于import.meta.env.BASE_URL生成/admin/...地址,生产构建仍使用主站根/...地址,不能把整份 public 再复制进 admin build。
SpacetimeDB 数据目录 OSS 备份
数据库备份不放进 spacetime-module reducer / procedure:备份属于文件系统与 OSS 外部副作用,必须由运维脚本在 SpacetimeDB 宿主外执行。当前统一脚本为 scripts/database-backup-to-oss.mjs(npm 命令 npm run database:backup:oss);生产 provision 还会安装 genarrative-database-backup.timer,每天 03:20 左右自动执行一次 OSS 冷备份:
npm run database:backup:oss -- --data-dir /stdb --stop-service spacetimedb.service --restart-service-after genarrative-api.service --restart-service-after genarrative-external-generation-worker@1.service --restart-service-after genarrative-external-generation-controller.service
脚本会将数据目录打包成 tar.gz,上传到 oss://<bucket>/<prefix>/<database>/<database>-<UTC时间>.tar.gz。生产建议做冷备份:传入 --stop-service spacetimedb.service,脚本会在打包前停止服务、打包后恢复服务,再上传 OSS;因 genarrative-api.service、genarrative-external-generation-worker@*.service 和 genarrative-external-generation-controller.service 都依赖 spacetimedb.service,生产定时冷备份还必须传入对应的 --restart-service-after,确保备份后 API、保底 worker 和 controller 随数据库一起恢复。2026-06-10 release 故障就是现场 unit 漏掉 API 重启参数,03:20 冷备份停止 SpacetimeDB 后 API 被依赖关系一并停止,备份脚本只恢复了 SpacetimeDB,API 直到人工重启前都不可用;2026-06-24 release 又出现同类依赖停机后只恢复 API、未恢复外部生成 worker/controller,导致图片画布生成任务长期停留在队列中。后续现场变更、provision 模板和 Jenkins 归档都必须通过 npm run check:production-ops 防止回退。由于 OSS 上传可能受服务器带宽限制,Genarrative-Stdb-Module-Publish 默认使用 DATABASE_BACKUP_MODE=async:先在 publish 前用 --defer-upload 生成本地冷备份和 .manifest.json,随后继续执行 publish;发布脚本退出前会用后台 node -- ... --upload-archive <tar.gz> 上传同一份发布前备份,不等待上传完成。Genarrative-Full-Build-And-Deploy 必须显式暴露并透传同一个 DATABASE_BACKUP_MODE,不得静默使用下游 async;release 已有验真冷备且明确禁止再上传时,Full 必须选择 skip。发布脚本在校验 wasm 后、执行 spacetime publish 前会等待显式 SPACETIME_SERVER_URL 的 /v1/ping 就绪,默认最多等待 60 秒;如生产机器冷备份恢复 spacetimedb.service 较慢,可临时设置 GENARRATIVE_STDB_PUBLISH_READY_TIMEOUT_SECONDS 调整等待时间。需要强一致发布闸门时改用 DATABASE_BACKUP_MODE=sync(等价脚本参数 --backup-mode sync),备份会在 publish 前同步打包并上传,失败会阻断 publish;确认已有其他备份窗口时才使用 DATABASE_BACKUP_MODE=skip(兼容脚本参数 --skip-backup)。若业务不能接受停机窗口,应先规划 SpacetimeDB 原生快照或主备策略,不要直接在写入中的数据目录上做热拷贝并当作强一致备份。
生产环境变量模板在 deploy/env/api-server.env.example:
主站前端运行时配置由 api-server 下发;画板右侧 Agent 入口使用
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false 默认关闭,需要开启时只改生产
api-server 环境变量并重启 api-server,不再通过 VITE_* 构建期变量控制。若后台再配置
image-editor:agent-sidebar 灰度 gate,enabled=true 且 rolloutPercent=0 表示有意关闭该入口;
只有当前判定涉及的已启用 gate 配置了用户标签白名单时,api-server 才读取用户标签。
GENARRATIVE_DATABASE_BACKUP_DATA_DIR=/stdb
GENARRATIVE_DATABASE_BACKUP_WORK_DIR=/var/lib/genarrative/database-backups
GENARRATIVE_DATABASE_BACKUP_OSS_BUCKET=
GENARRATIVE_DATABASE_BACKUP_OSS_ENDPOINT=oss-cn-shanghai.aliyuncs.com
GENARRATIVE_DATABASE_BACKUP_OSS_PREFIX=database-backups
GENARRATIVE_DATABASE_BACKUP_KEEP_LOCAL=false
GENARRATIVE_DATABASE_BACKUP_MIN_FREE_BYTES=
GENARRATIVE_DATABASE_BACKUP_OSS_ACCESS_KEY_ID=
GENARRATIVE_DATABASE_BACKUP_OSS_ACCESS_KEY_SECRET=
GENARRATIVE_DATABASE_BACKUP_OSS_BUCKET 为空时会回退 ALIYUN_OSS_BUCKET;AccessKey 默认复用 ALIYUN_OSS_ACCESS_KEY_ID / ALIYUN_OSS_ACCESS_KEY_SECRET,也可用 GENARRATIVE_DATABASE_BACKUP_OSS_ACCESS_KEY_ID / GENARRATIVE_DATABASE_BACKUP_OSS_ACCESS_KEY_SECRET 为备份 bucket 单独配置最小权限账号。冷备脚本会在停止 SpacetimeDB 前检查 GENARRATIVE_DATABASE_BACKUP_WORK_DIR 所在文件系统剩余空间;未设置 GENARRATIVE_DATABASE_BACKUP_MIN_FREE_BYTES 时,按数据目录大小加安全余量估算,空间不足会在停库前失败,避免写满根分区。即使打包或上传前步骤失败,只要脚本已经停过 SpacetimeDB,也会先恢复 SpacetimeDB 并执行 --restart-service-after 指定的 API / worker / controller,再带着原始备份错误退出。Genarrative-Server-Provision 会创建 /var/lib/genarrative/database-backups 并归属 genarrative:genarrative,同时安装并启用 genarrative-database-backup.timer。手动检查定时器:systemctl list-timers genarrative-database-backup.timer;手动触发一次:systemctl start genarrative-database-backup.service。如果 timer 显示 enabled 但 inactive/dead 且 NEXT / Trigger 为空,先写入当前 stamp 避免 Persistent=true 在白天立刻补跑冷备份:touch /var/lib/systemd/timers/stamp-genarrative-database-backup.timer && systemctl daemon-reload && systemctl start genarrative-database-backup.timer,随后确认下一次触发时间约为次日 03:20。
冷备份后必须做一次只读验收,不要只看 genarrative-database-backup.service 是否成功退出:
systemctl is-active spacetimedb.service genarrative-api.service nginx.service
curl -fsS --max-time 5 http://127.0.0.1:3101/v1/ping
curl -fsS --max-time 5 http://127.0.0.1:8082/healthz
curl -fsS --max-time 5 http://127.0.0.1:8082/readyz
curl -fsS --max-time 5 http://127.0.0.1/api/creation-entry/config >/dev/null
curl -fsS --max-time 5 http://127.0.0.1/api/runtime/puzzle/gallery >/dev/null
生产运维
生产部署当前口径:
systemd 托管 SpacetimeDB 与 Rust api-server
Nginx 负责站点和反向代理
Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分
生产健康巡检
Genarrative-Server-Provision 会安装并启用 genarrative-health-patrol.timer,默认每 5 分钟运行一次 genarrative-health-patrol.service。巡检脚本随 API release 归档到 /opt/genarrative/current/scripts/ops/production-health-patrol.mjs,只读检查:
- 默认
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx,检查genarrative-api.service、genarrative-external-generation-controller.service、spacetimedb.service、nginx.service是否 active;Pingora 直连切换后改为pingora-direct,检查genarrative-api.service、genarrative-external-generation-controller.service、spacetimedb.service、genarrative-pingora-gateway.service,不再要求nginx.serviceactive。 - 至少一个
genarrative-external-generation-worker@*.service实例是否 active;如果 controller 存活但 worker 全部退出,巡检直接返回CRITICAL,避免外部生成队列长期无人消费。 - API 直连
/healthz、/readyz。 - SpacetimeDB 直连
/v1/ping。 - 默认通过本机公网网关入口检查
/api/creation-entry/config、/api/runtime/puzzle/gallery、/api/runtime/custom-world-gallery;如需走正式域名,在/etc/genarrative/health-patrol.env配置GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://<域名>。若在目标机本机打https://127.0.0.1或http://127.0.0.1,同时配置GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>,确保 public probe 命中正确 vhost / Host 语义。 - Pingora 影子网关只在同时配置
GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL与GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN时纳入巡检;脚本会访问GET /__genarrative_pingora/healthz并校验返回gateway=pingora-shadow,未配置时不影响现有生产巡检。 - 最近 15 分钟对应 gateway mode 下
genarrative-api.service、genarrative-external-generation-controller.service、genarrative-external-generation-worker@*.service、spacetimedb.service和nginx.service或genarrative-pingora-gateway.service的err..alert日志。
巡检脚本的显式 --timeout-ms、--slow-ms、GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS 和 GENARRATIVE_HEALTH_PATROL_SLOW_MS 必须是正整数,非法值会直接失败,不静默回退默认 5000ms / 3000ms;生产巡检、health patrol env 复核和 env 切换脚本读取的布尔 env 也必须是明确布尔值,非法值会直接失败。health patrol env 复核脚本的 --env-file,以及 env 切换脚本的 --env-file / --check-script 都必须是绝对路径且不能是文件系统根目录,也不能包含换行或 NUL;env 切换脚本写入的 public base URL / Host 同样不能包含换行或 NUL。env 切换 --apply 还必须直接指向真实普通 env 文件,不能传符号链接。切换窗口调整超时、慢请求阈值、巡检模式开关或 env 路径时,先确认 env 与命令行参数格式正确,再把失败当作配置错误处理。
Pingora 网关行为变更后应先在本机运行 npm run check:pingora-gateway-smoke。该脚本不依赖完整 dev stack,会用临时 mock 上游覆盖静态路由、HTML / 普通静态资源 no-cache、Vite 指纹静态资源 immutable 缓存、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、TLS 直连、HTTP 到 HTTPS 重定向、API 代理头、body limit、429 接流保护、上游断连 / 超时 JSON 错误、维护模式和 SpacetimeDB WebSocket Upgrade,并复用 check-pingora-direct-live.mjs 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。API 代理头必须保持 Nginx 口径:透传 Host,写入 X-Forwarded-Host、配置化的 X-Forwarded-Proto、TCP 对端 IP 作为 X-Real-IP,并把 TCP 对端 IP 追加到 X-Forwarded-For;TRUST_X_FORWARDED_FOR 只用于接流保护 client key。gzip 默认由 GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED=true 开启,GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL=5 与 GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=1024 对齐当前 Nginx gzip_comp_level 5 / gzip_min_length 1024;GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip 是当前唯一允许的压缩算法白名单。Pingora 静态缓存头默认由 GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL=no-cache、GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL=public, max-age=31536000, immutable 和 GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL=no-cache 分别控制入口 HTML、指纹资源和其它静态资源。Pingora 正式化口径固定为 gzip-only,Brotli 仍由 Nginx / 前置代理能力探测承担,直连 Pingora 不以 Brotli parity 作为切换门禁。Pingora 上游超时显式配置在 peer 上:连接默认 3000ms,无显式长超时代理路由默认读 60s,通用 /api、公开列表 / 详情和 SpacetimeDB subscribe 默认读 3600s,写上游默认 3600s;读 / 写 / 连接超时统一返回 JSON 504 GATEWAY_UPSTREAM_TIMEOUT。当前 Pingora 接流保护默认只覆盖单进程单实例;GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true 且 GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1 时,必须先落地共享限流 / 共享并发保护层并设置 GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,否则网关启动和目标机 direct preflight 都会失败。若关闭网关保护后横向多实例运行,全局接流保护必须由前置 Nginx / LB 承担。
直连 HTTPS 入口只在显式设置 GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN、证书链和私钥文件后启用,证书链和私钥必须允许 genarrative 用户读取;HTTP 重定向入口只在已配置 TLS 入口后启用,且 ACME challenge 仍从 ACME_ROOT 静态读取。默认 systemd service 仍以非 root genarrative 用户运行且不具备低端口绑定能力;Server-Provision 只把直连 drop-in 模板安装到 /etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf 作为参考和手动覆盖来源,正式切换默认使用 current release 内的 /opt/genarrative/current/deploy/systemd/genarrative-pingora-gateway-direct-entry.conf。需要绑定 80/443 时必须先释放 Nginx 或其它进程占用,再通过 /opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --preflight-check-cert-readable --preflight-check-service-env-file --preflight-check-service-user-cert-readable --preflight-check-service-binary-executable --preflight-check-ports-free --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log 安装到 /etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf、systemctl daemon-reload 并重启 Pingora;脚本会先拒绝带换行或 NUL 的 service、路径、URL、Host、probe token、access log、数据库名、tail 行数和 timeout 参数,并拒绝脚本、env、drop-in、release root、access log 等路径参数指向文件系统根目录,再执行 current release 自审,确认发布包自包含、pingora-gateway 可执行且 systemd ExecStart 指向随包网关,失败时不会安装 drop-in;随后确认当前执行用户可读 TLS 文件,确认 service 模板和 systemctl cat 最终配置读取的 EnvironmentFile= 都包含本次 preflight env,再按 genarrative-pingora-gateway.service 的 User= 验证真实服务用户可读证书链和私钥,并从 service ExecStart= 确认 current release 的 pingora-gateway 存在且可执行,重启后用 systemctl cat 核验低端口 capability drop-in 和 EnvironmentFile=/etc/genarrative/pingora-gateway.env 已进入 systemd 最终配置,用 systemctl show ... ExecStart 核验最终 service 仍指向随包主 service 模板中的 current release pingora-gateway,用 systemctl is-active 确认 Pingora active,并以 JSON 模式执行 direct live smoke,强制覆盖 HTTPS / HTTP redirect / ACME / WSS 101 和 Pingora access log request_id 落盘;启用脚本会解析 direct-access-log 结构化结果,要求 matchedCount == checked 且 missingCount=0、mismatchCount=0,缺少该结构化结果时即使 direct live 子进程退出 0 也会让启用失败。只有需要临时改用 /etc/genarrative/pingora/ 参考模板时,才显式传 --template-path 或设置 GENARRATIVE_PINGORA_DIRECT_TEMPLATE_PATH;切换窗口覆盖 --preflight-script、--direct-live-script、--current-release-audit-script、--template-path、--service-unit-path、--dropin-path 或 env 文件路径时必须使用绝对路径,且不能用 / 占位,--apply 会在安装 drop-in 前确认 current release 自审、direct preflight 和 direct live smoke 脚本都存在。
目标机验证直连入口时先运行 npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free,再运行 npm run check:pingora-direct-live;正式切换聚合门禁追加 --require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable;--require-direct 会强制要求 direct HTTPS base URL、direct HTTP base URL、正式域名 Host/SNI、redirect Location host、Pingora access log 文件、health patrol env 文件、direct preflight env 文件、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户证书可读检查、服务用户证书可读检查、service 二进制可执行检查和显式 SpacetimeDB 数据库名同时存在,并默认要求 WSS subscribe 返回 101,避免漏掉 HTTP 301 / ACME challenge、正式证书域名、HTTP redirect Location host、Pingora access log request_id 落盘、SpacetimeDB 长连接、低端口 capability、TLS 文件权限验证、systemd 实际 env 漂移、systemd 服务用户权限、current release 二进制可执行性或误用默认数据库名。--direct-pingora-access-log、--direct-health-patrol-env-file、--direct-preflight-env-file、direct live 单脚本的 --pingora-access-log 和 direct preflight 单脚本的 --env-file 都必须是绝对路径,且不能是文件系统根目录;direct preflight 的 --env-file、--systemd-service、服务用户和 env 中的 listen / cert / key 值还不能包含换行或 NUL,执行 systemctl cat 或 sudo -u <serviceUser> test -r <file> 前会复核子命令参数,避免污染参数进入目标机预检命令;direct live 的 URL、Host、probe token、额外 path、数据库名、access log 路径、timeout 和布尔 env 同样不能包含换行或 NUL,脚本会在发起 HTTPS / HTTP / WSS 请求前失败,避免污染请求头、URL、日志对账或 JSON 证据。若 env 中 GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true,preflight 会要求同时设置 GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true;当 TLS / HTTP redirect 监听公网地址时还会直接失败,因为公网直连 Pingora 不能信任客户端可伪造的 X-Forwarded-For。若 env 中 GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true 且 GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1,preflight 会要求同时设置 GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,避免把进程内保护误当成跨实例全局保护。生产证书必须可被系统信任,--direct-insecure-tls 只允许本机自签证书 smoke 使用;--direct-skip-wss 只允许单独 direct live 临时排障,release readiness --require-direct 会直接拒绝。生成 --dry-run-cutover 正式切换 runbook 时,--direct-redirect-host、--rollback-nginx-smoke-host 和 --direct-host 必须使用同一 hostname,只允许端口不同;同时必须提供 --rollback-health-patrol-public-base-url <切换前Nginx巡检入口>,若切换前 Nginx 巡检需要 Host 覆盖,再追加 --rollback-health-patrol-public-host <切换前Host>,避免回退计划把现场巡检入口覆盖成仓库默认值。runbook 的 Host 与回退巡检入口确认步骤会直接展示回退后要恢复的 public base URL / Host,并默认展示来自切换前真实 Nginx 入口的 rollback-nginx-smoke-expect-body,当班人员需要在启用前确认它们就是切换前记录值。多域名或 canonical redirect 切换需要单独设计,不混入第一版 runbook。runbook 可选追加 --rollback-pingora-shadow-probe-url / --rollback-pingora-shadow-probe-token,把回退后 Pingora shadow 高端口探针复核写入 rollback dry-run / apply,JSON 输出会隐藏 token 原文。
正式 runbook 的状态快照证据包必须显式传 --expected-pingora-env-mode:post-enable 使用 direct,证明 active /etc/genarrative/pingora-gateway.env 已是直连姿态;post-rollback 使用 shadow,证明回退后的 active env 已恢复 shadow 姿态。回退前的 pingora-gateway-env-shadow-switch.mjs --apply 必须同时清空 GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN、GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN、GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE 和 GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE,避免留下“有证书路径但无 TLS listen”的半直连 env。
直连验证失败时执行 /opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>',或在仓库工作区执行 npm run deploy:pingora-direct-rollback -- --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-expect-body '<!doctype html>';回退脚本会先拒绝带换行或 NUL 的 service、路径、Nginx smoke URL / Host / 响应片段、health patrol 复核参数、shadow probe URL / token 和二进制 override,并拒绝 service unit、drop-in、health patrol env、复核脚本、路径形式二进制 override 指向文件系统根目录,再拒绝符号链接形式的 drop-in 目录或目标文件,以及已存在但不是普通文件的 drop-in 目标,通过后才运行 nginx -t、移除 drop-in、重启 Pingora,并用 systemctl cat 核验低端口 capability 已从 systemd 最终配置中移除,用 systemctl show ... ExecStart 核验最终 service 仍指向随包主 service 模板中的 current release pingora-gateway,最后 reload Nginx、确认 Nginx service 仍为 active,并用 smoke URL 证明 Nginx 入口真实可访问;传入 --nginx-smoke-expect-body 时还会要求响应体包含该片段,避免 HTTP 200 命中错误入口;该 URL 与 body 必须来自切换前真实 Nginx 入口,不要继续用固定 http://127.0.0.1/healthz 与 "ok":true。--nginx-smoke-url 必须是 http(s) URL;本机打 127.0.0.1、localhost 或 ::1 时,--apply 还必须显式传 --nginx-smoke-host <域名>,且该值只能是 host 或 host:port,避免命中默认 vhost。覆盖 --nginx-binary 或 --curl-binary 时可以传裸命令名;如果值包含路径分隔符,则必须使用绝对路径,避免回退窗口受 cwd 影响。若 health patrol env 已在回退命令前切回 Nginx,可追加 --health-patrol-env-file /etc/genarrative/health-patrol.env,让回退脚本在 Nginx smoke 后复核 GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx 且 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 为空;若要证明回退后 Pingora 仍作为 shadow 高端口服务存活,可追加 --pingora-shadow-probe-url http://127.0.0.1:18081/__genarrative_pingora/healthz --pingora-shadow-probe-token <token>。否则回退后必须单独运行 node -- /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs --env-file /etc/genarrative/health-patrol.env --expected-gateway-mode nginx --require-empty-public-host。启用 / 回退脚本默认 dry-run,必须显式传 --apply 才修改 systemd;启用脚本 --apply 必须先通过 current release 自审,并同时传 --preflight-env-file、--preflight-check-cert-readable、--preflight-check-service-env-file、--preflight-check-service-user-cert-readable、--preflight-check-service-binary-executable、--preflight-check-ports-free、--direct-https-base-url、--direct-http-base-url、--direct-host、--direct-redirect-host、--direct-pingora-access-log 和 --direct-spacetime-database,回退脚本 --apply 必须同时传 --reload-nginx 和 --nginx-smoke-url;当 smoke URL 是本机地址时还必须传 --nginx-smoke-host。本机 npm run check:pingora-direct-enable 和 npm run check:pingora-direct-rollback 会用临时文件验证 dry-run 不修改 drop-in、current release 自审失败时启用脚本不会安装 drop-in、direct preflight / direct live smoke 脚本缺失时安装前失败、direct live 退出 0 但缺少 direct-access-log JSON 证据时启用失败、回退脚本在 drop-in 路径安全异常时不会继续执行 nginx -t 或删除真实目标、会打印对应命令、展示 release layout 默认读取随包 deploy/systemd direct-entry 模板、systemd 最终配置核验、ExecStart 指向核验、Pingora active 核验、direct live smoke、Nginx 配置语法检查、Nginx reload 后状态核验、Nginx smoke 及响应体片段核验、可选 health patrol env 复核、可选 Pingora shadow probe 复核,并拒绝相对路径、文件系统根目录路径、非法 smoke URL、路径形式的二进制 override 和控制字符参数。
npm run check:pingora-current-release-audit 会烟测 current release 自审脚本,npm run check:pingora-cutover-status-snapshot 会烟测随包状态快照脚本,npm run check:pingora-cutover-evidence-bundle 会烟测证据包归档脚本,npm run check:pingora-cutover-command-evidence 会烟测 apply 命令 stdout / stderr / 退出码归档脚本,npm run check:pingora-cutover-evidence-verify 会烟测证据 manifest 只读验真脚本,npm run check:pingora-cutover-evidence-audit 会烟测证据根目录三阶段总审计脚本;正式切换窗口先用 /opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show 只读确认发布包自包含、api-server.sha256 / pingora-gateway.sha256 与当前文件匹配、release-manifest.api-server.json 已登记 pingora-gateway、pingora-gateway 可执行且 systemd ExecStart 指向 current release,再使用 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-bundle.mjs 在 pre-cutover、post-enable、post-rollback 三个阶段生成时间戳证据目录,保存 manifest.json、snapshot.json、snapshot.stdout.txt、snapshot.stderr.txt 和 snapshot-command.json;证据包 manifest.files 会给已生成的 snapshot / stdout / stderr / command / parse-error / direct-live 文件统一记录 path、sizeBytes 和 sha256,便于窗口后复核归档文件未漂移。每个阶段证据目录生成、复制或归档后,都要用 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir <本阶段bundleDir> 做只读验真;该 verifier 把 manifest.files 视为闭集,除已登记文件和 manifest.json 外,目录中混入任何未登记普通文件、目录或符号链接都会默认失败,--allow-extra-files 只允许人工排障时显式放行,正式切换归档不使用。enable / rollback apply 命令证据生成后也要立即用同一个 verifier 做只读验真,把上一步 stdout 中的 bundleDir 分别填入 <enable-apply-bundle-dir> / <rollback-apply-bundle-dir>,确认 command.stdout.txt、command.stderr.txt、command-record.json 与 manifest 元数据一致后,再继续 health patrol 切换或回退后复核。三阶段证据、enable / rollback apply 命令证据和三条 env 变更命令证据都完成并即时验真后,再用 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-audit.mjs --evidence-root <证据根目录> --require-phase pre-cutover --require-phase post-enable --require-phase post-rollback --require-command enable-apply:pingora-direct-enable-apply --require-command post-enable:pingora-health-patrol-direct-env-switch --require-command rollback-prep:pingora-gateway-shadow-env-switch --require-command rollback-prep:pingora-health-patrol-nginx-env-switch --require-command rollback-apply:pingora-direct-rollback-apply --require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --require-command-executable post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --require-command-executable rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --require-command-executable rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --require-command-arg enable-apply:pingora-direct-enable-apply:--apply --require-command-arg post-enable:pingora-health-patrol-direct-env-switch:--apply --require-command-arg post-enable:pingora-health-patrol-direct-env-switch:pingora-direct --require-command-arg rollback-prep:pingora-gateway-shadow-env-switch:--apply --require-command-arg rollback-prep:pingora-health-patrol-nginx-env-switch:--apply --require-command-arg rollback-prep:pingora-health-patrol-nginx-env-switch:nginx --require-command-arg rollback-apply:pingora-direct-rollback-apply:--apply --require-cutover-run-id <本次cutoverRunId> --timeline-max-span-ms 86400000 自动定位每个阶段最新证据目录和五条真实切换命令证据目录,并复用 verifier 的 --require-summary-ok 严格模式输出总审计 JSON。证据根目录总审计默认只接受带 manifest.json 的证据目录,根目录夹带普通文件、无 manifest 子目录或符号链接都会失败;--allow-extra-root-entries 只允许人工排障显式放行,正式切换归档不使用。阶段证据必须是不带 manifest.commandName 的状态快照证据包,命令证据不能冒充同名 phase 的阶段证据。验真与总审计脚本只读取 manifest 和证据文件,拒绝路径逃逸、未登记额外条目、根目录额外条目、缺文件、大小漂移、sha256 漂移、符号链接证据目录、非目录证据路径、缺阶段证据、缺命令证据、阶段 manifest.summary.status 非 OK、命令 manifest.summary.status 非 OK、命令 manifest.summary.exitCode 非 0,以及标准八段证据的 manifest.generatedAt 顺序不满足 pre-cutover -> enable-apply -> post-enable:pingora-health-patrol-direct-env-switch -> post-enable -> rollback-prep:pingora-gateway-shadow-env-switch -> rollback-prep:pingora-health-patrol-nginx-env-switch -> rollback-apply -> post-rollback 或八段跨度超过默认 24 小时、要求 --require-cutover-run-id 时任一证据缺少同一 manifest.cutoverRunId,不修改 /etc、systemd、Nginx、Pingora 或证据文件;runbook 会生成或接受 --cutover-run-id <id>,并把同一 manifest.cutoverRunId 写入三阶段证据包、五条真实切换命令证据和最终总审计;确需跨更长维护窗口时,生成 runbook 时显式传 --cutover-evidence-timeline-max-span-ms <ms>,让总审计 JSON 记录本次 timeline.maxSpanMs 和 timeline.spanMs。runbook 的 direct enable apply / rollback apply 还会通过 /opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs 包装真实脚本执行,额外保存 command.stdout.txt、command.stderr.txt、command-record.json 和 manifest.json,失败时同样保留证据并返回真实退出码,命令证据 manifest.files 也会记录这三份命令证据文件的 path、sizeBytes 和 sha256 供归档后复核。若状态快照 stdout 无法解析为 JSON,证据包会改写 snapshot-parse-error.txt 并在 manifest.files.snapshotParseError 与最终 stdout 中给出路径;启用后 direct live stdout 无法解析时,同理写入 direct-live-parse-error.txt 并在 manifest.files.directLiveParseError 与最终 stdout 中给出路径,避免解析失败原因只散落在终端输出里。底层状态快照收录 summary、healthPatrolEnv、pingoraEnv、releaseArtifacts、systemd 和 checks;其中 systemd.pingoraUnit.environmentFileMatchesPingoraEnvFile 必须为 true,证明 systemctl cat genarrative-pingora-gateway.service 的 EnvironmentFile= 精确包含本次 --pingora-env-file,避免证据包读取一份 env 而真实服务读取另一份 env。直连 runbook 的三个证据包阶段都会向状态快照透传 --require-pingora-gateway,因此 checks.current-release-audit.details 会同步保存 Pingora 二进制、sha256、release manifest 和 systemd ExecStart 自审结果。自审脚本和快照脚本只读采集状态,证据包脚本只写 --output-root 下的新目录;命令证据脚本只负责执行 -- 后面的真实命令并归档输出,不自行修改 /etc、systemd、Nginx 或 Pingora;自审、快照和证据包传入的 --release-root 都必须是绝对路径且不能是文件系统根目录,状态快照的 --health-patrol-env-file / --pingora-env-file 以及证据包所有显式路径参数也不能是文件系统根目录,--output-root 及其已存在上级路径不能是符号链接,且已存在的 --output-root 必须是目录;路径异常时会在执行状态快照前失败,不写入软链真实目标、不覆盖已有文件、不写 /etc、不 reload systemd,也不修改 Nginx 或 Pingora;正式 runbook 传 --run-health-patrol --fail-on-critical,任何 CRITICAL 都应阻断继续切换或回退确认。
证据包还会把 Pingora env 的 listen、tlsListen、httpRedirectListen、tlsCertFile、tlsKeyFile、mode 和 shadowReady 提升到 manifest.summary.pingoraEnvShadow;正式总审计必须追加 --require-phase-direct-live-access-log post-enable --require-phase-direct-live-static-headers post-enable --require-phase-pingora-env-shadow post-rollback,其中 post-rollback 必须证明 listen=127.0.0.1:18081、mode=shadow、shadowReady=true,且低端口 TLS / HTTP redirect 监听和证书路径均为空。
证据 verifier / 总审计的 --manifest、--bundle-dir、--evidence-root、--verify-script 和 manifest 登记文件名都不能包含换行或 NUL 字符;遇到此类失败应修正 runbook 参数或重新生成证据,不要把被污染的路径留给下游 JSON 归档兜底。
current release 自审的 --release-root 和 --systemd-service 不能包含换行或 NUL 字符;启用 --systemd-show 时,脚本还会在执行 systemctl show 前复核子命令可执行文件和所有参数不含换行或 NUL,避免只读自审命令被污染参数带偏。
证据根目录总审计的“最新证据”只以 manifest.generatedAt 为准;任何阶段或命令候选缺少合法 manifest.generatedAt 都会作为根目录 CRITICAL 失败,不能用目录 mtime 兜底。同一阶段或同一命令如果出现多个候选共享最新 manifest.generatedAt,总审计会以 AMBIGUOUS_LATEST 失败并列出重复目录,不能按目录名排序打平;应重新归档该阶段 / 命令证据,或把旧证据移出正式证据根目录后再审计。所有证据 manifest 必须是 schemaVersion=1,命令证据中的 manifest.command 与独立 command-record.json 也必须是 schemaVersion=1 且字段一致;manifest.generatedAt、命令 startedAt 和 finishedAt 必须使用 new Date().toISOString() 产出的 UTC 毫秒格式 YYYY-MM-DDTHH:mm:ss.sssZ,不接受省略毫秒、本地时区或其它可被 Date.parse 宽松解析的字符串;命令证据的顶层 manifest.commandName 和内嵌 manifest.command.name 只要存在就必须各自是安全非空命令名,且两者同时存在时必须一致,否则按坏 manifest 处理;旧格式或手工 JSON 即使 hash 正确,也不能进入正式证据链。标准八段时间线在阶段和命令都被要求时,还会显式检查八段条目的审计状态都是 OK,任一阶段或命令为 MANIFEST_FAILED / VERIFY_FAILED 等非 OK 状态都会写入 timeline 诊断。这样证据目录被复制、归档或恢复后,选择最新证据和标准八段时间线证明仍只依赖 manifest 中可验真的事实。
标准八段时间线只要任一阶段或命令证据声明了 manifest.cutoverRunId,八段就必须全部声明同一个值;缺字段或混入其它批次都会让总审计失败。任何证据 manifest 只要显式写入 cutoverRunId 字段,就必须是安全非空 ID,不能用空字符串伪装成缺省字段。正式 runbook 仍必须显式传 --require-cutover-run-id <本次cutoverRunId>,避免人工临时审计遗漏参数。
总审计 JSON 的 timeline.failedCount 会按标准八段时间线的具体失败项累计,timeline.failureBreakdown 会区分 nonOkItems、missingGeneratedAt、cutoverRunIdMismatch、outOfOrder 和 spanExceeded。切换窗口排障时不要只看顶层 ok=false,应先按 breakdown 定位是证据状态未达标、时间字段缺失、批次混入、顺序倒挂还是维护窗口跨度超限。
正式 runbook 的每个即时 verifier 步骤,以及三条 env 变更命令证据即时验真步骤,都必须追加 --require-summary-ok:三阶段状态证据、enable / rollback apply 命令证据和三条 env 变更命令证据在刚生成后就要同时通过 manifest.files 完整性校验和 manifest.summary.status=OK 校验;缺少 summary、状态为 CRITICAL 或文件 hash 漂移都必须立即停住。最终证据根目录总审计仍负责跨目录聚合、命令身份、--apply 参数、时间线和 cutoverRunId 的二次确认,不能用来替代即时验真;总审计内部再次复用 verifier 时也必须启用 --require-summary-ok。
正式 cutover runbook 不允许携带 --warn-only、--allow-extra-files 或 --allow-extra-root-entries。这些参数只用于 runbook 之外的人工排障命令;一旦需要它们,说明正式证据链还不干净,应先修现场状态、归档方式或重新生成证据,再回到无放行参数的 runbook。
最终证据根目录总审计必须在 --require-command 之外追加五条 --require-command-executable 和七条 --require-command-arg 要求,覆盖 enable apply、health patrol direct env switch、Pingora gateway shadow env switch、health patrol nginx env switch 和 rollback apply;其中 include enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh、post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs、rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh,并要求 --apply、pingora-direct 与 nginx 等关键参数,复核命令证据里的 manifest.expectedExecutable、manifest.command.executable 与独立 command-record.json 的 executable 都是 current release 随包脚本,并要求 manifest.command.args 与独立 command-record.json.args 都包含 --apply,且每个 args 字符串都不含换行或 NUL 字符。--require-command-executable 的 executable 段必须是安全绝对路径,不能是文件系统根目录,也不能包含换行或 NUL 字符;总审计 JSON 会记录 requiredCommandExecutables,便于复盘本次绑定的真实 current release 随包脚本。总审计还会要求 manifest.commandName 与 manifest.command.name 一致,并要求 manifest.command 与 command-record.json 的 schemaVersion / phase / commandName / cutoverRunId / exitCode / signal / startedAt / finishedAt / durationMs / stdoutPath / stderrPath / args / command / cwd / error 等关键字段一致;stdoutPath / stderrPath 还必须分别与 manifest.files.stdout.path / manifest.files.stderr.path 指向同一份归档文件,args 与 command 用于证明真实 apply 参数未被替换成 dry-run 或其它动作。命令记录时间必须满足 finishedAt >= startedAt、durationMs == finishedAt - startedAt,且 manifest.generatedAt 不能早于命令 finishedAt。这些参数会隐式要求对应命令证据存在;旧证据缺少 schemaVersion 或 expectedExecutable、缺少必需 --apply 参数、人工同名证据的 executable 漂移、manifest 与 command-record 语义漂移、命令 stdout / stderr 引用漂移、命令参数漂移、命令参数控制字符污染、命令时间线漂移或同一命令绑定多个不同脚本路径都会失败。
命令证据的 manifest.expectedExecutable 和 manifest.command.expectedExecutable 只要出现,就必须是安全绝对路径;空字符串、相对路径、文件系统根目录或包含换行 / NUL 的值都会让总审计失败,避免坏字段被当作缺省值跳过。生成命令证据时,pingora-cutover-command-evidence.mjs --expected-executable 也会在执行真实命令前拒绝相对路径、文件系统根目录和带换行 / NUL 的路径。
只要命令证据声明了 expectedExecutable,manifest.command.executable 和独立 command-record.json.executable 就必须同时是同一个绝对路径;即使未显式传 --require-command-executable,真实 executable 与 expectedExecutable 漂移也必须失败。
正式命令证据不能只依赖 command 字符串复盘真实命令;manifest.command.executable 和 command-record.json.executable 都是必填安全绝对路径,缺失任一字段都会让总审计失败。
生成命令证据时,pingora-cutover-command-evidence.mjs 的 --output-root 必须是安全绝对路径,不能是文件系统根目录、符号链接或包含换行 / NUL 字符;-- <command> 也必须使用绝对路径,不能依赖 PATH 裸命令名;真实命令不能是文件系统根目录,真实命令和每个真实命令参数都不能包含换行或 NUL 字符。这保证生成端写出的结构化 executable / args[] 天然符合最终总审计的必填安全证据要求。
current release 自审、状态快照、证据包、direct preflight、生产巡检、health patrol env 复核和 env 切换脚本读取的布尔 env 只接受 true/false、1/0、yes/no、on/off 或空值,拼写错误会直接失败,避免 REQUIRE_GATEWAY、RUN_HEALTH_PATROL、REQUIRE_PINGORA_GATEWAY、FAIL_ON_CRITICAL、TRUST_X_FORWARDED_FOR 或巡检切换开关被悄悄当成 false。
正式直连 runbook 的启用前 release readiness 基础门禁和启用后 --require-direct 复核,都必须调用 current release 随包的 /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs。生产 API release、Jenkins API Build 归档、Jenkins API Deploy 复制清单和 production-api-deploy.sh 都必须携带该聚合门禁脚本;缺失时部署应在 current 切换前 fail-fast、清理 staging 并退出本次打开的维护模式,切换窗口不能回退到源码 checkout 或 Jenkins workspace 的相对路径脚本。
current release 随包执行的 release readiness 必须追加 --release-runtime-only,只运行包内可自包含的 current release 自审、live canary、真实 access log 对账、direct preflight、health patrol env 复核和 direct live smoke;不要在 /opt/genarrative/current 上运行默认源码全量门禁。默认不带 --release-runtime-only 的聚合门禁仍属于本机 / CI / 构建环境使用,负责覆盖 Cargo、npm、Docker、Nginx 静态 / 真机校验和发布包构建烟测。
Pingora direct 切换门禁按阶段拆分:启用前 preflight、--dry-run-cutover 和 pingora-direct-enable.sh --apply 必须带 --direct-preflight-check-ports-free / --preflight-check-ports-free,证明 Nginx、Gitea 等所有占用 80/443 的进程已释放;启用后 --require-direct --release-runtime-only 复核不再要求端口空闲,因为此时 80/443 应由 Pingora 占用。回退 runbook 必须显式提供来自切换前真实 Nginx 响应的 --rollback-nginx-smoke-url 与 --rollback-nginx-smoke-expect-body,例如 dev 首页 https://dev.genarrative.world/ 和 <!doctype html>;不要继续用固定 http://127.0.0.1/healthz 与 "ok":true。如果切换窗口把 /etc/genarrative/pingora-gateway.env 提升为 direct 低端口配置,rollback apply 前必须用 current release 随包 node -- /opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --apply --env-file /etc/genarrative/pingora-gateway.env 预置 Pingora shadow env,确认 GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081,并清空 GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN / GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN,再重启 Pingora。
涉及 Nginx 模板、Pingora 路由、限流分组或路由文档时,必须同步更新 deploy/pingora/nginx-route-parity.matrix.json,并运行 npm run check:pingora-route-parity;Rust 侧 cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix 会读取同一份矩阵验证 classify_path 的路由结果、body limit 和接流保护分组。
Pingora canary 入口默认由 Server-Provision 安装但不接入主站配置。deploy/nginx/snippets/genarrative-pingora-canary.conf 是前缀 canary,人工 include 后用 /__genarrative_pingora_canary/ rewrite 到 Pingora shadow;deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf 是前缀 canary 之后、direct 直连之前的真实路径 canary,只能在 Nginx http 上下文 include,默认监听 127.0.0.1:18083 并写独立 /var/log/nginx/genarrative-pingora-realpath-canary.access.log,不要 include 到生产 443 server 内覆盖正式 location。启用前必须运行 npm run check:nginx-pingora-canary;目标 agent 有 Nginx 时运行 node scripts/check-nginx-pingora-canary.mjs --require-nginx,强制把前缀 location 片段和真实路径 server 片段一起执行 nginx -t。本机或 CI 可用 npm run check:pingora-canary-docker 启动 Docker Nginx、真实 Pingora 和 mock 上游复现两条 Nginx -> Pingora handoff,并复用 scripts/check-pingora-canary-access-log-parity.mjs 按 request_id 对账。需要证明 Docker Nginx -> Pingora -> 真实本地服务时,先启动真实 api-server 与 SpacetimeDB,再运行 node scripts/check-pingora-canary-docker.mjs --require-docker --real-upstreams --api-upstream 127.0.0.1:<api-port> --spacetime-upstream 127.0.0.1:<spacetime-port> --web-root dist;该模式不会启动内置 mock 上游,会先检查真实 /healthz 和 /v1/ping,随后复用同一组 live smoke 与 access log 对账。真实 SpacetimeDB 对 GET /v1/identity 返回 405 Method Not Allowed 属于可接受语义,canary live 只把它作为路径路由代表,不要求该 GET 创建 identity。前缀 canary reload 后运行 GENARRATIVE_PINGORA_CANARY_BASE_URL=http://127.0.0.1 GENARRATIVE_PINGORA_CANARY_HOST=<域名> npm run check:pingora-canary-live;真实路径 canary 启停统一使用 current release 随包脚本:/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083 写入 /etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf 并执行 nginx -t、reload 和 realpath live smoke,/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply 删除该配置并复核 Nginx;两者失败都会恢复操作前状态。真实路径 canary reload 后运行 node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>,随后用 node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/creation-entry/config --path /v1/identity --path /assets/app.js 对账;真实路径模式除 healthz 探针外要求 Nginx path 与 Pingora path 完全一致。canary live 的 base URL、prefix、Host、额外 path、--timeout-ms / GENARRATIVE_PINGORA_CANARY_TIMEOUT_MS 不能包含换行或 NUL,脚本会在发起请求前失败;canary access log 对账脚本的日志路径、prefix、必需路径和 --since-lines / GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES 也不能包含换行或 NUL,日志行里解析出的 URI / path 含控制字符时必须失败;timeout 和 access log 对账的 --since-lines / 对应 env 必须是正整数,非法值直接失败,不静默回默认值。
下方聚合门禁示例以前缀 canary 已启用为前提;如果现场只启用了真实路径 canary,源码 checkout / CI 与目标机 --release-runtime-only 都不要传 --require-live,改用 --require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log。只有前缀 canary 和真实路径 canary 都启用时,才同时传两组 live 参数。
Pingora 正式切换前必须额外运行聚合门禁:node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log。该门禁串起 Rust 单测、mock smoke、路由 parity、Nginx snippet 校验、Docker Nginx handoff、canary access log 对账烟测、直连入口静态预检、直连启用 / 回退 dry-run 行为检查、health patrol env 切换脚本烟测、current release 自审烟测、release readiness 计划自检、生产运维护栏、API release build 烟测、Pingora production release 真实构建烟测、API deploy release 烟测、目标机 live canary 和目标机真实 access log 对账;其中 --require-live 只能在目标 Nginx 已人工 include canary snippet 并 reload 后执行,并会强制要求 --live-host,避免只访问 127.0.0.1 命中 Nginx 默认 vhost。live smoke 成功后会立即读取 Nginx 与 Pingora access log 尾部记录,按 request_id 验证 /__genarrative_pingora_canary/healthz 和 /__genarrative_pingora_canary/api/creation-entry/config 已在两边落盘且 method/status/path 没有漂移。启用真实路径 canary 时追加 --require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,它会用独立 Nginx access log 对账真实 /api、/v1 与 /assets 路径。live canary、direct live 和 access log 对账的显式毫秒超时或尾部行数参数都必须是正整数,非法值应先修参数再重跑,不能把默认值兜底后的结果当成切换证据;direct live 与 release readiness 读取的直连布尔 env 只接受 true/false、1/0、yes/no、on/off 或空值,拼写错误会直接失败,避免 REQUIRE_WSS_UPGRADE、preflight 开关或 SKIP_WSS 被悄悄当成 false。若验证 Pingora 直连公网入口,还必须追加 direct preflight 参数检查目标机 env、systemd drop-in、service EnvironmentFile 一致性、当前用户证书权限、服务用户证书权限和 service 二进制可执行性:--require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable;该模式缺少 --direct-http-base-url、--direct-host、--direct-redirect-host、--direct-pingora-access-log、--direct-preflight-systemd、--direct-preflight-check-cert-readable、--direct-preflight-check-service-env-file、--direct-preflight-check-service-user-cert-readable、--direct-preflight-check-service-binary-executable、缺少 --direct-health-patrol-env-file 或缺少 --direct-spacetime-database 会直接失败,且会拒绝 --direct-skip-wss,并默认要求 WSS subscribe 返回 101,证明 HTTP redirect / ACME、正式域名 Host/SNI、redirect Location host、Pingora access log request_id + path/status 对账、health patrol direct 模式、systemd drop-in、service EnvironmentFile 一致性、当前用户证书权限、服务用户证书权限、service 二进制可执行性与目标 SpacetimeDB 长连接都能从直连入口透传。check-pingora-release-readiness.mjs --help 中的正式直连和只生成 runbook 示例也必须显式带 --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,避免值班人员复制示例后被 --require-direct 自身拦住。普通本机提交前可先跑 npm run check:pingora-release-readiness,但该默认模式不能替代目标机切换窗口的强制门禁。直连切换前还应先运行 npm run plan:pingora-direct-cutover -- --require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable --rollback-nginx-smoke-url https://<域名>/ --rollback-nginx-smoke-expect-body '<!doctype html>' --rollback-health-patrol-public-base-url <切换前Nginx巡检入口> 生成只读 JSON runbook;若切换参数提供 --direct-probe-token,启用后复核会继续透传 direct probe token 检查内部探针,runbook JSON 只显示 <redacted>。runbook 会列出 Host 与回退巡检入口确认、current release 自包含自审、current release preflight、启用前基础门禁、direct enable dry-run、direct enable apply、用 /opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --apply 切到 pingora-direct、启用后 health patrol env 直连复核、启用后 --require-direct 复核、rollback dry-run、回退前用同一脚本预置回 nginx 并恢复切换前 public base URL / Host、rollback apply、回退后 health patrol env Nginx 模式复核,供当班人员逐条审阅。direct enable / rollback --apply 都会拒绝符号链接形式的 systemd drop-in 目录或目标文件,并拒绝已存在但不是普通文件的目标,避免把低端口 capability 写入非预期位置,或在回退窗口误删 / 误判非预期 systemd 位置;health patrol env 切换脚本 --apply 会保留原文件权限和 owner/group,此时 --env-file 必须直接指向真实普通文件,不能是符号链接。如果现场 env 是链接,先确认真实目标路径后再传给脚本,避免替换链接本身或写入非预期目标。
目标机在 /opt/genarrative/current 上执行上述启用前基础门禁或启用后 --require-direct 复核时,命令必须显式使用 --release-runtime-only。该模式会拒绝 --require-docker、--pull-docker 和 --require-nginx,这些源码 / 构建环境门禁应在 CI 或构建机的默认聚合门禁中完成。
上述聚合门禁默认还会执行 npm run check:pingora-cutover-status-snapshot、npm run check:pingora-cutover-evidence-bundle、npm run check:pingora-cutover-command-evidence、npm run check:pingora-cutover-evidence-verify 和 npm run check:pingora-cutover-evidence-audit。正式直连 runbook 会额外列出 切换前状态快照证据包、启用后状态快照证据包 和 回退后状态快照证据包,每个证据包阶段后都会列出对应的 证据 manifest 只读验真 步骤;Pingora direct enable apply 与 Pingora direct rollback apply 后也会分别列出 启用命令证据 manifest 只读验真 和 回退命令证据 manifest 只读验真,先用随包 verifier 复核命令证据 manifest,再进入 health patrol 切换或回退后 env 复核;最后在三阶段与 enable / rollback apply 命令证据和三条 env 变更命令证据都完成并即时验真后列出 切换证据根目录三阶段总审计。这些步骤都从 /opt/genarrative/current 读取随包脚本和支撑文件,分别校验 Nginx 基线、Pingora direct 接流状态、回退后的 Nginx 状态、真实切换命令 stdout / stderr / 退出码证据和证据根目录阶段完整性,并通过 --require-pingora-gateway --fail-on-critical 防止把发布物 checksum / manifest 漂移、env 漂移、release 缺文件、systemd capability 残留、systemd 实际读取的 Pingora env 与快照 env 不一致或生产巡检失败当作可忽略信息。启用后证据包还会通过 --run-direct-live 归档 direct-live.json、stdout / stderr、命令记录和 Pingora access log request_id 反查结果,确保 direct live 不是只停留在终端输出;direct-live.json 中的 direct-access-log 检查必须保留扫描行数、匹配数量、缺失明细以及 path/status 漂移明细,便于事后不用翻 stderr 就能定位具体请求。证据包还会把 directLiveAccessLog 摘要写入 manifest.summary,缺少 direct-access-log 结构化结果会直接标记 CRITICAL。若 snapshot 或 direct live stdout 解析失败,证据包必须同时归档对应 *-parse-error.txt 并在 manifest 中索引。证据链脚本的 timeout 必须是正整数,布尔 env 必须是明确布尔值,非法值应先修配置再重跑,不能把静默 false 当成切换证据;状态快照脚本单独执行时会拒绝带换行或 NUL 字符的 release/env 路径,并在执行 systemctl、current release 自审、health patrol env 复核或生产巡检子命令前复核子命令参数;证据包脚本在执行状态快照或 direct live 子命令前必须先拒绝任一带换行或 NUL 字符的子命令参数,且 --run-direct-live 的 direct URL、Host、probe token、数据库名、Pingora access log 路径和 tail 行数会在配置层先拒绝换行或 NUL,--direct-pingora-access-log 还必须是绝对路径且不能是文件系统根目录,避免污染后的结构化 args[] 进入证据链;命令证据脚本的 --phase / --command-name 只允许 ASCII 安全字符,--output-root 必须是绝对路径且不能是符号链接或文件系统根目录,-- 后的真实命令也必须是绝对路径、不能是文件系统根目录、不能包含换行或 NUL 字符;证据根目录总审计的 --require-command <phase>:<commandName> 同样只接受 ASCII 安全阶段名和命令名,缺失 enable-apply:pingora-direct-enable-apply 或 rollback-apply:pingora-direct-rollback-apply 任一命令证据都必须失败。
命令证据脚本还支持 --expected-executable <绝对路径> 和 --require-arg <参数>,正式 runbook 的 enable / rollback apply 会分别绑定到 /opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh、/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh,并在执行前要求真实命令参数包含 --apply。如果 -- 后真实命令与预期脚本不一致、缺少必需 --apply 参数,或任一真实命令参数包含换行 / NUL 字符,脚本会在创建正式命令证据前失败,避免只靠 commandName 把错误命令伪装成正式切换证据。
Pingora shadow 的 access log 由 GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE=/var/log/genarrative/pingora-gateway.access.log 控制,Server-Provision 会创建 /var/log/genarrative、安装 /etc/logrotate.d/genarrative-pingora-gateway,并让 genarrative-pingora-gateway.service 的 systemd 沙箱允许写该目录。做 canary 对照时同时看 Nginx access log、Pingora access log 和 Pingora tracing 日志;scripts/check-pingora-canary-access-log-parity.mjs 会按 request_id 对照两边 access log,Nginx canary exact /healthz 映射到 Pingora shadow /__genarrative_pingora/healthz,其余前缀路径按 rewrite 后路径比对。canary 对账的 --nginx-log-file / --pingora-log-file 以及 release readiness live 模式的 --live-nginx-access-log / --live-pingora-access-log 必须是绝对路径且不能是文件系统根目录,避免把日志扫描误指向 /。
Pingora 启动会拒绝明显不安全或不完整的配置:probe token 不能使用占位值或短 token,TLS 入口必须同时给出可读取的证书链和私钥文件,HTTP redirect 入口必须依附已配置的 TLS 入口且不能复用同一监听地址,redirect scheme 当前只允许 https,GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL 必须在 0..=9,GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES 必须大于 0,GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS 当前只允许 gzip,所有 GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_*TIMEOUT* 必须大于 0,GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true 时必须同时设置 GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true,并确认前置 Nginx / LB 已清洗 X-Forwarded-For;接流保护配置中 BURST>0 时对应 RATE_PER_SECOND 不能为 0;开启网关保护时 GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1 必须同时设置 GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,否则多实例会把进程内限流额度按实例数放大。
巡检输出总状态 OK / WARNING / CRITICAL;只有 CRITICAL 默认让 systemd service 失败,WARNING 只写日志和状态文件,避免历史日志噪声把 timer 长期打成失败。最近一次结果写入 /var/lib/genarrative/health-patrol/status.json。手动执行:
systemctl start genarrative-health-patrol.service
systemctl status genarrative-health-patrol.service --no-pager
journalctl -u genarrative-health-patrol.service -n 80 --no-pager
cat /var/lib/genarrative/health-patrol/status.json
如需接外部告警,可在 /etc/genarrative/health-patrol.env 配置 GENARRATIVE_HEALTH_PATROL_WEBHOOK_URL;脚本只会在 WARNING 或 CRITICAL 时向该 webhook 发送 JSON。未配置 webhook 时,告警来源是 systemd 失败状态、journal 和状态文件。
Genarrative-Web-Build 的主站构建失败若出现 Rollup 报错 "xxx" is not exported by "src/services/publicWorkCode.ts",优先按前端公开作品号工具缺失处理,而不是排查 Jenkins 节点环境。修复时要让 publicWorkCode.ts 的 build<Play>PublicWorkCode 与 isSame<Play>PublicWorkCode 成对导出,并补 src/services/publicWorkCode.test.ts 覆盖对应玩法前缀;随后用 npm run build:production-release -- --component web --name <临时名> 复现 Jenkins web 构建路径。
Genarrative-Web-Build 会把 build/<version>/web.tar.gz、web.tar.gz.sha256、release-manifest.json 和 scripts/deploy/production-web-deploy.sh 直接归档为 Jenkins 构建产物;Genarrative-Web-Deploy 只通过 copyArtifacts 从指定上游构建复制这些产物和部署脚本,不再在目标机器 checkout Git,再执行随构建归档的 scripts/deploy/production-web-deploy.sh。Web 发布不再读取构建机本地缓存目录,也不再通过 release agent rsync 回构建机拉取大包;如果 deploy 找不到 web.tar.gz,应先检查上游 Web Build 是否按同一 BUILD_VERSION 成功归档产物。
Genarrative-Api-Build 的 Jenkins 归档产物必须包含 build/<version>/api-server、api-server.sha256、release-manifest.json、build/<version>/scripts/deploy/production-api-deploy.sh、build/<version>/scripts/deploy/maintenance-on.sh、build/<version>/scripts/deploy/maintenance-off.sh、scripts/database-backup-to-oss.mjs、scripts/ops/production-health-patrol.mjs、scripts/ops/pingora-current-release-audit.mjs、scripts/ops/pingora-cutover-status-snapshot.mjs、scripts/ops/pingora-cutover-evidence-bundle.mjs、scripts/ops/pingora-cutover-command-evidence.mjs、scripts/ops/pingora-cutover-evidence-verify.mjs、scripts/ops/pingora-cutover-evidence-audit.mjs、scripts/check-pingora-direct-preflight.mjs、scripts/check-pingora-direct-live.mjs、scripts/check-pingora-canary-access-log-parity.mjs、scripts/check-production-health-patrol-env.mjs、scripts/deploy/pingora-direct-enable.sh、scripts/deploy/pingora-direct-rollback.sh、deploy/systemd/**、deploy/env/** 和 deploy/pingora/**。deploy/systemd/genarrative-database-backup.service 从 /opt/genarrative/current/scripts/database-backup-to-oss.mjs 执行冷备份,deploy/systemd/genarrative-health-patrol.service 从 /opt/genarrative/current/scripts/ops/production-health-patrol.mjs 执行巡检;Genarrative-Api-Deploy 会从上游 API 构建产物复制并执行 build/<version>/scripts/deploy/production-api-deploy.sh,同目录的 maintenance-on.sh / maintenance-off.sh 也必须来自同一 build 产物;部署脚本会先写入 ${RELEASE_ROOT}/.${VERSION}.staging.$$,把 release-manifest.json 校验后复制为 current release 的 release-manifest.api-server.json,并把备份脚本、巡检脚本、Pingora 直连启用 / 回退 / 预检 / live smoke / canary access log 对账 / health patrol env 复核 / current release 自审 / 状态快照 / 证据包 / 命令证据 / 证据验真 / 证据根目录审计脚本,以及 deploy/systemd、deploy/env、deploy/pingora 支撑配置全部复制完成后,才用非合并语义提升为 ${RELEASE_ROOT}/${VERSION} 并用固定替换语义切换 current 符号链接,不再在目标机器 checkout Git,也不再执行部署工作区根部脚本。Pingora 直连启用脚本必须能从 /opt/genarrative/current 独立执行 preflight 和 direct live smoke,并默认读取 current release 随包 deploy/systemd/genarrative-pingora-gateway-direct-entry.conf,不依赖 Jenkins 工作区、源码 checkout 或 /etc 参考模板;plan:pingora-direct-cutover 必须能用同一组参数生成 current release 切换 / 回退 runbook。production-api-deploy.sh 对 release manifest、备份脚本、巡检脚本、env 示例目录和 Pingora 直连依赖都执行 fail-fast,且 --release-root、--current-link、--api-env-file 必须是绝对路径,--version 必须以数字或字母开头并只能包含数字、字母、点、下划线和短横线,禁止 . / .. 点目录;发布产物缺少 manifest、manifest 未登记 api-server、缺少脚本 / 配置目录、同版本 release 目录已存在、current 路径不是符号链接、提升前 release 目录竞态出现或 staging 构建中失败时会在 current 切换前停止部署,清理 staging 并退出本次打开的维护模式;current 切换后的 Pingora 重启、worker 重启、controller 启动或 readiness 失败仍保留维护模式,避免暴露半发布版本;失败不会留下正式 release 目录,不再从部署机工作区兜底补文件,也不把旧同名 release 目录和新文件混合。如果 API 发布后 current release 中缺少这些脚本或目录,应先检查 Genarrative-Api-Build 的 archiveArtifacts 和 Genarrative-Api-Deploy 的 copyArtifacts 过滤器是否仍包含 build/<version>/release-manifest.json、build/<version>/scripts/deploy/production-api-deploy.sh、build/<version>/scripts/deploy/maintenance-on.sh、build/<version>/scripts/deploy/maintenance-off.sh、build/<version>/scripts/database-backup-to-oss.mjs、build/<version>/scripts/ops/production-health-patrol.mjs、build/<version>/scripts/ops/pingora-current-release-audit.mjs、build/<version>/scripts/ops/pingora-cutover-status-snapshot.mjs、build/<version>/scripts/ops/pingora-cutover-evidence-bundle.mjs、build/<version>/scripts/ops/pingora-cutover-command-evidence.mjs、build/<version>/scripts/ops/pingora-cutover-evidence-verify.mjs、build/<version>/scripts/ops/pingora-cutover-evidence-audit.mjs、build/<version>/scripts/check-pingora-direct-preflight.mjs、build/<version>/scripts/check-pingora-direct-live.mjs、build/<version>/scripts/check-pingora-canary-access-log-parity.mjs、build/<version>/scripts/check-production-health-patrol-env.mjs、build/<version>/scripts/deploy/pingora-direct-enable.sh、build/<version>/scripts/deploy/pingora-direct-rollback.sh、build/<version>/deploy/systemd/**、build/<version>/deploy/env/** 与 build/<version>/deploy/pingora/**,不要只在部署机工作区手工补文件。本机用 npm run check:production-api-release 通过临时 CARGO_TARGET_DIR 和假 api-server / pingora-gateway release binary 验证 build-production-release.sh --component api-server --skip-api-build 会把这些文件打进 API release,并验证显式 --include-pingora-gateway --skip-pingora-gateway-build 时发布包包含 pingora-gateway、pingora-gateway.sha256 和 manifest 登记;npm run check:pingora-production-release-build 则用假 api-server 和真实 cargo build -p pingora-gateway --release --target x86_64-unknown-linux-gnu 验证显式 include 路径能构出可执行网关二进制、checksum 和 manifest 登记;再用 npm run check:production-api-deploy 通过临时 release、fake systemctl / curl 验证从发布产物内执行 production-api-deploy.sh 会把这些文件复制到 current release,并验证缺少 release manifest、manifest 未登记 api-server、Pingora manifest / 二进制漂移、--require-pingora-gateway 缺少 Pingora、缺少数据库备份脚本、健康巡检脚本、健康巡检 env 复核脚本、current release 自审脚本、状态快照脚本、证据包脚本、证据验真脚本、证据根目录审计脚本、canary access log 对账脚本、env 示例目录或 direct live smoke 脚本时都会在 current 切换前失败并退出本次打开的维护模式,还会验证相对 release root / current link / api env file、点目录或点开头 version 被拒绝、失败时不留下 staging / 正式 release 目录、同版本 release 目录已存在、current 路径不是符号链接、提升前 release 目录竞态出现时拒绝覆盖 / 合并,以及 current 切换后的 readiness 失败会保留维护模式。Pingora 影子网关在 Genarrative-Api-Build、Genarrative-Api-Deploy 和 Genarrative-Full-Build-And-Deploy 中默认随 release 构建、归档、复制并用 --require-pingora-gateway 硬校验;只有显式取消 INCLUDE_PINGORA_GATEWAY 时才允许 API release 不带 pingora-gateway / pingora-gateway.sha256。本地 CLI 仍保留显式 npm run build:production-release -- --component api-server --include-pingora-gateway,用于在需要 Pingora 的手工发布包里登记 manifest 和 checksum;Jenkins 会先检查 cmake、C 编译器和 C++ 编译器,避免进入 Cargo 后才因 libz-ng-sys 构建依赖缺失失败。发布包包含 Pingora 时,deploy 会在提升 release 前读取 systemctl cat genarrative-pingora-gateway.service 和其 EnvironmentFile,拒绝 direct-entry CAP_NET_BIND_SERVICE、拒绝非 127.0.0.1:18081 的 shadow listen、拒绝 TLS_LISTEN / HTTP_REDIRECT_LISTEN,确认仍是本机 shadow 高端口后才切换 current;切换后执行 systemctl restart genarrative-pingora-gateway.service 并复核 active,让 shadow / canary 机器加载新网关二进制。该自动拉起不会启用公网 80/443 直连入口;已经进入 direct-entry 状态的机器应走正式直连 runbook 或先回退到 shadow。
Pingora shadow 部署前检查必须按首个权威错误 fail-fast:读取 systemd 最终配置失败或发现 CAP_NET_BIND_SERVICE 后,不得继续把空的 EnvironmentFile 解析结果传给 shadow listen 校验,也不得再输出“缺少 EnvironmentFile”或“LISTEN 为空”的连带误报。npm run check:production-api-deploy 的 direct-entry fixture 会锁定这一错误顺序,避免值班人员被多条互相矛盾的诊断引向错误配置。
Pingora current release 自审脚本 scripts/ops/pingora-current-release-audit.mjs、直连切换状态快照脚本 scripts/ops/pingora-cutover-status-snapshot.mjs、证据包脚本 scripts/ops/pingora-cutover-evidence-bundle.mjs、命令证据脚本 scripts/ops/pingora-cutover-command-evidence.mjs、证据验真脚本 scripts/ops/pingora-cutover-evidence-verify.mjs、证据根目录审计脚本 scripts/ops/pingora-cutover-evidence-audit.mjs 和 canary access log 对账脚本 scripts/check-pingora-canary-access-log-parity.mjs 都属于 API release 的强制随包依赖;缺少任一脚本时 check:production-api-release、check:production-api-deploy 和生产运维护栏都必须失败,避免切换窗口只能靠 Jenkins 工作区或源码 checkout 临时补自审、证据或日志对账脚本。
同一 API release 随包依赖还必须包含 scripts/check-pingora-release-readiness.mjs 与 scripts/check-pingora-canary-live.mjs。前者在 current release 上以 --release-runtime-only 汇总运行时复核,后者支撑目标 Nginx canary live smoke;缺少任一脚本时不能进入直连切换窗口。
Genarrative-Stdb-Module-Build 的 Jenkins 归档产物必须包含 build/<version>/spacetime_module.wasm、spacetime_module.wasm.sha256、release-manifest.json、scripts/deploy/production-stdb-publish.sh、scripts/deploy/production-runtime-writer-identity-rotate.mjs、scripts/deploy/maintenance-on.sh、scripts/deploy/maintenance-off.sh、scripts/spacetime-migration-common.mjs 和 scripts/database-backup-to-oss.mjs,不得包含 migration-bootstrap-secret.txt 或任何原始 bootstrap secret。Genarrative-Stdb-Module-Build 只接受 MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb release-manifest.json 以 migration_bootstrap_secret_sha256 记录该非敏感摘要。Genarrative-Stdb-Module-Publish 只通过 copyArtifacts 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret File;publish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 migration_bootstrap_secret_sha256 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secret;ID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
三个 SCM Jenkinsfile 将 MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 默认固定为 genarrative-spacetime-bootstrap-secret-dev-file。Secret File 的原文只存在于 Jenkins Credentials;credential ID、参数默认值和定时 / 发布行为以仓库 Jenkinsfile 为事实源,不能只改 Job UI,因为 Declarative Pipeline 下一次载入会重写参数定义。旧 Secret Text genarrative-spacetime-bootstrap-secret-dev 继续保留给 Database Import / Export,不得原地改类型或删除。
生产 Stdb publish 固定传 --delete-data=never --yes=migrate,break-clients,普通 Stdb Jenkins Job 不提供 CLEAR_DATABASE;任何需要删除数据的迁移都必须失败并重新核对 schema 与 artifact,不能在发布路径内切换清库继续。
生产运行时不把 bootstrap secret 明文写进 /etc/genarrative/*.env。api-server.env 和 worker env 只登记固定 FILE 路径 GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE=/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt;若检测到明文 GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET 或其他 FILE 路径,Server-Provision / API deploy 必须失败。Full Build 先执行 Stdb publish、后执行 API deploy,因此两段必须透传同一 API_ENV_FILE / WORKER_ENV_FILE;Stdb Publish 把 Secret File 路径作为 --migration-bootstrap-secret-file 传给随包 production-stdb-publish.sh。脚本先进入维护模式、按所选模式完成发布前冷备份、校验 checksum 并发布 module;成功后拒绝符号链接目标,把 secret 安装成 root:genarrative 0440、目录收紧为 root:genarrative 0750,原子补齐 API / worker env 的固定 FILE 配置,再快照并重启发布前为 active 的 API、controller 和 worker。systemctl is-active 只有明确返回合法的非 active 状态时才允许跳过,查询错误或 active worker 的 list-units 失败都必须保留维护模式并阻断;所有原 active 服务重启后必须重新确认为 active。如果 API 原本 active,还必须在 maintenance-off 前通过本机 http://127.0.0.1:8082/healthz readiness;可用 --api-health-url / GENARRATIVE_STDB_PUBLISH_API_HEALTH_URL 调整本机 URL,并用 --api-readiness-timeout-seconds / GENARRATIVE_STDB_PUBLISH_API_READINESS_TIMEOUT_SECONDS 调整超时。这样旧服务器首次 rollout 也不会等到后续 API deploy 才拿到 FILE;只覆盖 secret 文件或只补 env 而不重启都不生效,因为 AppConfig 在进程启动时读取 secret。人工执行 npm run build:production-release -- --component spacetime-module --name <version> 且未显式提供 secret / SHA-256 时,原始随机 secret 只写入 gitignored 的 server-rs/.spacetimedb/build-secrets/<version>.txt,目录权限 0700、文件权限 0600,发布包和 release-manifest.json 都不收录它;必须把该受保护文件另行交给 publish 阶段。旧 npm run deploy:rust:remote Ubuntu 直传入口也使用同一 sidecar 目录,发布包内不含原文;上传模式通过独立 SSH 标准输入把 secret 原子安装到远端发布目录并收紧为 0600,--skip-upload 时必须单独受保护交付。 本地 dev 的原始值也只能进入 api-server,不得扩散给 Web / Vite,任何控制台、Jenkins 日志、归档或生成 README 都不得输出明文。相关变更至少运行 bash -n scripts/deploy/production-stdb-publish.sh scripts/deploy/production-api-deploy.sh scripts/deploy-rust-remote.sh scripts/jenkins-server-provision.sh、node --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjs、npm run check:production-ops、npm run check:encoding 和 git diff --check。
生产 runtime writer 不能通过替换 bootstrap secret 或重启服务隐式轮换。migration operator 与 runtime writer 必须身份互斥:operator 不能成为 writer,当前 writer 不能授权为 operator;已有任一 operator 后,bootstrap secret 不得新增或接管 operator。先准备新 api-server identity,并使用当前已授权 migration operator 的 CLI 登录态执行 node scripts/deploy/production-runtime-writer-identity-rotate.mjs --database <database> --server-url <url> --operator-identity <operatorIdentity> --operator-user-id <userId> --next-writer-identity <nextIdentity> --confirm-next-writer-identity <nextIdentity> --note <reason>。CLI 会校验当前登录 identity、双录新 identity 和审计原因;模块 procedure 还会拒绝把 writer 设为任一已登记 migration operator。成功后必须核对 editor_generation_runtime_identity_rotation 的旧 writer、新 writer、operator identity、操作人、原因和服务端时间,再切换 API token;轮换只改 writer,不改模型价格。pause-after-stdb 人工维护不得把 api-server.env 的 GENARRATIVE_SPACETIME_TOKEN identity 授权为 migration operator;首次初始化前数据库还没有 writer 记录,模块无法提前识别这枚 identity 的未来用途,误授权会让 API 启动持续报“数据库迁移操作员 identity 不能初始化为模型生成运行时服务 identity”。若已误授权,必须在维护完成后由该 identity 自撤 migration operator 权限,再确认 rollout gate。API deploy 的本机 readiness 探测每次请求固定 --max-time 2,即使端口已建立但 API 尚未响应也会回到有限重试,不得使用无超时 curl。
Genarrative-Web-Build 打包 web.tar.gz 前、Genarrative-Web-Deploy 解包后都会把 Web 静态目录规范为目录 755、文件 644。如果前端页面能打开但 public 图片、字体或音频返回 403 Forbidden,优先检查当前 /srv/genarrative/web 指向的 release 中对应文件权限是否被异常归档为 600,临时恢复可对该 release 的 web 目录执行目录 755、文件 644 的权限修正。
维护模式只拦截公网流量
Nginx 与 Pingora 在维护 marker 存在时对内网来源绕过整站维护闸,主站页面与静态资源、普通 API、后台页面与 /admin/api/**、SpacetimeDB 路由均按非维护状态继续处理;公网应用主站、普通 API、后台和 SpacetimeDB 路由继续返回维护响应。内网范围为 IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local。Nginx 只按 TCP $remote_addr 判定;Pingora 只按 TCP peer 判定,peer 为 loopback 的同机 Nginx 时才读取 Nginx 强制覆盖的 X-Real-IP,绝不能把客户端可伪造的 X-Forwarded-For 用作维护放行依据。应用本身的登录、管理员鉴权和其它业务鉴权不变。
该规则只绕过网关维护响应,不会自动拉起 api-server、SpacetimeDB 或其它已停止的服务。人工执行 maintenance-on.sh 且后端仍运行时,可以从内网继续访问整站和修改后台数据;pause-after-stdb 会停止旧 API/controller/worker,在 API 被停期间静态页面可能仍可加载,但普通 API 与 /admin/api/** 仍不可用。验证使用 npm run check:nginx-spa-routes、npm run check:pingora-route-parity、cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml、npm run check:pingora-gateway-smoke 和 npm run check:production-ops,不要在 live 机器上为测试临时创建维护 marker。
版本化默认维护页固定为 public/maintenance.html,使用 public/branding/taonier-maintenance-page.png 作为品牌视觉,只允许保存无日期、无具体时段的通用文案;正常 Web 构建由 Vite 复制到发布包根目录的 web/maintenance.html,并由 check-maintenance-page.mjs 在打包前拒绝“今天 / 今晚”、具体日期或 HH:MM 等临时公告。计划内停服的临时公告必须放在 release 外文件中,通过 /opt/genarrative/current/scripts/deploy/maintenance-on.sh --page-file <公告HTML> <维护原因> 原子安装到 /var/lib/genarrative/maintenance/page.html。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 maintenance-on.sh 调用保留已安装公告,maintenance-off.sh 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 genarrative.world 与 www.genarrative.world 的真实 HTTPS 响应校验 503 和公告正文。
生产 Jenkins 的 Pipeline script from SCM 由 Jenkins controller 读取 Jenkinsfile。Genarrative-Server-Provision 是服务器初始化流水线,Job 配置里的 SCM URL 必须使用 controller 本机可访问的仓库路径或内网 Gitea 地址,不能使用 https://git.genarrative.world/...;否则日志一开始的 Checking out git ... to read jenkins/Jenkinsfile.production-server-provision 就会先从公网拉 Jenkinsfile。构建类流水线和 Genarrative-Server-Provision 的 Jenkinsfile 内部源码准备阶段统一使用 ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git,并显式传入 Jenkins SSH 凭据 genarrative-local-gitea-ssh;不再配置 https://git.genarrative.world/... 公网 fallback,也不再默认使用 https://git.genarrative.world/git/GenarrativeAI/Genarrative.git。所有 GitSCM checkout 都必须保留单分支 refspec、shallow=true、depth=1、noTags=true 与 honorRefspec=true。API / Web / Stdb 发布类流水线不在目标机器 checkout Git,统一执行上游构建归档里的部署脚本,避免产物 commit 与部署脚本 commit 漂移;Server-Provision 也不在目标 dev / release agent checkout Git,而是由 Jenkins 构建节点先准备 provision 脚本与配置并上传给目标 agent。
当前 Jenkins / 本机内网 Git 入口固定为 ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git,用于 controller、构建节点和本机 Agent 直接拉取仓库,避免绕公网 git.genarrative.world。验证时在具备对应 SSH key 和 known_hosts 的环境执行 git ls-remote ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git HEAD,应能返回 HEAD。若机器仍保留旧的 https://git.genarrative.world/git/GenarrativeAI/Genarrative.git 或 http://10.2.0.10/GenarrativeAI/Genarrative.git 内网入口,只作为历史兼容和排障参考,新流水线不再默认使用。
scripts/jenkins-checkout-source.sh 是生产 Jenkinsfile 内部二次确认源码的统一入口。构建流水线由 Jenkins GitSCM checkout 先用凭据完成浅克隆,再以 GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT=true 调用脚本复用当前 checkout;只有显式 COMMIT_HASH 不在这次浅克隆里时,脚本才通过传入的 SSH 远端继续 fetch 并逐步加深。构建流水线和服务器初始化流水线传入 COMMIT_HASH 时,脚本必须先保持 depth=1 浅拉,若上游 commit 已在浅历史内则直接校验并 checkout;只有浅历史无法证明 commit 属于目标分支时,才按 GENARRATIVE_JENKINS_CHECKOUT_DEEPEN_STEPS(默认 50 200 1000 5000)逐步加深,最后才尝试展开完整历史。Genarrative-Api-Deploy、Genarrative-Web-Deploy 和 Genarrative-Stdb-Module-Publish 仍保留上游构建传入的 COMMIT_HASH 作为通知和追溯字段,但不再用它在目标机器重新 checkout 部署脚本。
Genarrative-Stdb-Module-Publish 在 Pipeline script from SCM 阶段如果一开始就报 No such DSL method 'pipeline',优先检查 jenkins/Jenkinsfile.production-stdb-module-publish 是否带 UTF-8 BOM。Jenkins Declarative Pipeline 的首个 token 必须是纯 pipeline;仓库中的 Jenkinsfile 应保存为 UTF-8 without BOM,只有临时写给 Windows PowerShell 5.1 -File 执行的 .ps1 才需要按对应 helper 转成带 BOM。验证时可检查文件前三字节不再是 EF BB BF,并运行 validateDeclarativePipeline 或重放该流水线。
Genarrative-Stdb-Module-Build 或 SpacetimeDB module 构建失败若出现 Rust E0425 cannot find function migrate_*,优先排查 server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs 等同文件内默认种子迁移 helper 是否在分支合并时只保留了调用、漏掉了函数定义。Genarrative-Stdb-Module-Build 现在运行在 linux && genarrative-build 节点上,Checkout 与 Build 都走 bash + cargo + sccache,不再依赖 Windows PowerShell 或 Git Bash;Stdb module 的 CARGO_HOME、CARGO_TARGET_DIR 和 SCCACHE_DIR 默认落在稳定缓存根 ~/caches/genarrative-jenkins/stdb-module 下,可用 GENARRATIVE_STDB_CACHE_ROOT 覆盖,避免 WORKSPACE@tmp 被清理后无改动也触发近似冷构建。修复时不要直接删除迁移调用;应恢复只纠偏历史默认种子且不覆盖后台手动配置的 helper,并用 cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml 复现 Jenkins module 编译路径。
Genarrative-Server-Provision 只做服务器初始化,不再承担构建职责。流水线先在 linux && genarrative-build 节点读取指定 SOURCE_BRANCH / COMMIT_HASH 的仓库内容,只把 scripts/jenkins-server-provision.sh、scripts/prepare-server-provision-tools.sh、scripts/deploy/**、deploy/** 和 .jenkins-source-commit 打包上传给目标 agent;目标 agent 不再接收 SOURCE_GIT_REMOTE_URL,也不再 checkout Git。真正会读取目标机现状和写系统配置的阶段仍只在目标服务器 agent 执行:DEPLOY_TARGET=development 使用 linux && genarrative-dev-deploy,DEPLOY_TARGET=release 使用 linux && genarrative-release-deploy;Prepare Provision Tools 与 Provision Server 在同一个目标 agent 工作区内顺序执行,前者准备 SpacetimeDB 与 otelcol-contrib 交付件,后者写入 /etc / systemd / Nginx、创建系统用户并修改服务。目标 dev / release agent 非 dry-run 时必须具备 root 权限。
生产环境变量模板:deploy/env/api-server.env.example。真实密钥只放服务器,不提交 Git,不写入文档示例。
api-server 进程角色由 GENARRATIVE_PROCESS_ROLE 控制:api 只监听 HTTP,external-generation-worker 只消费外部生成队列,external-generation-controller 只管理 worker systemd 实例,all 仅用于本地或临时 smoke,不隐式启动 controller。外部生成策略由 GENARRATIVE_EXTERNAL_GENERATION_MODE 控制;生产和容器压测默认保持 queue,本地 npm run dev / npm run dev:api-server 默认由 dev 脚本注入 GENARRATIVE_PROCESS_ROLE=all,如果外部生成策略为 queue 会由同一进程消费队列。inline 只用于本地或低并发同步排查,HTTP handler 会直接复用 worker executor,完成后返回 completed,但不会落 external_generation_job,也不能通过增加 worker 进程扩吞吐。外部生成 worker 使用同一发布包和同一套 SpacetimeDB 配置,按实例数和 GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY 动态扩缩;生产默认由 genarrative-external-generation-controller.service 读取 get_external_generation_queue_stats_and_return,按 claimable_pending + running_active + expired_running 计算目标 worker 数,并对 genarrative-external-generation-worker@N.service 精确执行 systemctl start/stop。controller 参数模板是 deploy/env/external-generation-controller.env.example:默认保底 MIN_WORKERS=1、上限 MAX_WORKERS=8、每 worker 目标 TARGET_JOBS_PER_WORKER=2、POLL_INTERVAL_MS=10000、连续 SCALE_DOWN_IDLE_ROUNDS=6 轮完全空闲才缩容;缩容每轮只停止最高编号的一个实例,且不主动停止 @1。worker 收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd TimeoutStopSec,未完成任务才会在 lease 过期后由其它 worker 重领。每个 worker 实例应设置唯一 GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID,默认会用主机名和 pid 兜底;systemd 生产模板 deploy/systemd/genarrative-external-generation-worker@.service 会用 %H-%i 生成实例 ID,并把 tracking outbox 隔离到 /var/lib/genarrative/tracking-outbox/%H-%i。Genarrative-Server-Provision 会安装 worker 模板、controller unit 和两份专属 env 模板,默认 enable 首个 genarrative-external-generation-worker@1.service 与 genarrative-external-generation-controller.service;首次 API deploy 会在默认 worker pattern 下自动 enable --now genarrative-external-generation-worker@1.service 并等待 worker active,同时重启并验活 controller。手动兜底扩容仍可用 systemctl start genarrative-external-generation-worker@2.service / @3.service,缩容用 systemctl stop genarrative-external-generation-worker@N.service;controller 下轮会按队列压力修正到目标实例数。worker 专属参数模板是 deploy/env/external-generation-worker.env.example,密钥与 SpacetimeDB 连接仍复用 /etc/genarrative/api-server.env。API 发布脚本默认会重启并验活 genarrative-external-generation-worker@*.service 和 genarrative-external-generation-controller.service;若本次只发 HTTP 且不希望滚动 worker,可传 --no-worker-services,若不希望重启 controller 可传 --no-worker-controller。GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS 控制空队列轮询间隔,GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS 控制单次 lease,worker 会约每三分之一 lease、最长 30 秒续租;该值应覆盖一次心跳网络抖动窗口,不需要大于完整外部生成链路耗时。SpacetimeDB 使用自身事务时间计算 claim/renew/complete/fail,完成和失败回写还会校验 lease_token 与未过期 lease,避免同一 job 被过期 worker 覆盖。首版 worker 粒度是单动作单 job,不拆阶段 job;当前外部生成动作覆盖拼图、跳一跳、拼消消、敲木鱼和图片画布编辑器生成 / 抠图入口,纯元信息保存、发布、试玩启动、运行态动作和公开读取继续 inline。图片画布 worker 成功后由后端保存 editor_project_resource / editor_asset / editor_canvas.layers_json,前端只轮询 job 并重新读取项目快照。当前生成业务失败只做用户重新触发,不做自动业务重试,避免 worker 退款和重试成功之间产生钱包账本漂移。
worker 被硬杀或断电后,lease 过期任务只有尚未耗尽 max_attempts 才由其它 worker 重领;最终 attempt 的过期任务由 claim transaction 直接失败并结算,不会继续执行外部 provider。当前业务入队点默认 max_attempts=1,因此一次已领取任务若以 lease 过期结束,后续 poll 负责终态与退款收口,而不是发起第二次 provider 请求。
生产 genarrative-api.service 通过 systemd Wants=genarrative-external-generation-controller.service 弱依赖拉起 controller;这不是 Rust HTTP 进程内执行 systemctl,因此不会把 controller 管理 worker 的权限放进 genarrative 用户运行的 API 进程。该弱依赖只保证启动 API 时尝试同时启动 controller,controller 异常不应反向拖停 API。
Genarrative-Server-Provision 会安装 systemd 模板和 Nginx 站点模板,不再安装 clang / lld / pkg-config / OpenSSL headers / sccache 等通用构建链依赖。因 VectorEngine 图片上游 POST 已改用 libcurl,当前 Linux release 构建出的 api-server 运行时需要 OPENSSL_3.2.0 符号;Ubuntu 24.04 apt 默认只提供 OpenSSL 3.0.x,不能直接满足该符号版本。Provision 会把 OpenSSL 3.2.0 独立安装到 /opt/genarrative/openssl-3.2.0,校验官方 tarball SHA256,并只通过 genarrative-api.service 的 LD_LIBRARY_PATH=/opt/genarrative/openssl-3.2.0/lib64:/opt/genarrative/openssl-3.2.0/lib 让 api-server 使用,避免替换系统 OpenSSL 或影响 ssh / nginx / apt。Ubuntu / apt 目标机为完成这一步会安装 build-essential、ca-certificates、curl、perl、tar 等 OpenSSL 运行时自举工具;这只服务于独立 OpenSSL 运行时安装,不代表 provision 重新承担 api-server 构建职责。Ubuntu / apt 目标机会额外安装 libnginx-mod-http-brotli-filter 与 libnginx-mod-http-brotli-static,随后由 scripts/jenkins-server-provision.sh 通过临时 nginx -t 配置探测 Brotli 指令是否可用;该临时配置必须先 include /etc/nginx/modules-enabled/*.conf,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 nginx -V 编译参数里。探测成功才在渲染后的 deploy/nginx/genarrative.conf / genarrative-dev-http.conf 中启用 Brotli,避免未安装模块的机器直接写入无效配置。Provision 写入 Genarrative Nginx 站点时会把 /etc/nginx/sites-enabled/default* 移到 /etc/nginx/sites-disabled/,避免 Debian / Certbot 默认站点继续占用 genarrative.world / www.genarrative.world 并在 nginx -T 中出现 conflicting server name ... ignored。如果 nginx -t 失败,脚本会恢复写入前的 Genarrative 配置和被移动的默认站点。
50 HTTP req/s 首版压测优化口径:
api-server生产模板默认GENARRATIVE_API_LISTEN_BACKLOG=1024、GENARRATIVE_API_WORKER_THREADS=4;本地未设置 worker threads 时继续使用 Tokio 默认值。GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512开启应用内 HTTP 并发背压;GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320、GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64、GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回429 Too Many Requests和Retry-After: 1,/healthz与/readyz不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连api-server的极高 RPS 压测若出现connection refused,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。api-server正常运行时/healthz只返回进程存活状态,/readyz会同时检查进程是否仍接收新流量和 SpacetimeDB 连接租约是否健康;收到SIGINT/SIGTERM后会先把 readiness 标记为不可用,再让 Axum 停止接新连接并等待已有 HTTP 请求排空。systemd 仍以KillSignal=SIGINT停服务,TimeoutStopSec=90作为长请求排空上限。- SpacetimeDB 健康检查默认使用
GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS=2的短等待窗口,和业务 procedure 的GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS分开。/readyz失败时details.spacetime.stage会标出当前卡住阶段:pool_acquire、connect_build、connect_handshake、read_model_subscribe、procedure_result、reducer_result或read_cache;elapsedMs/timeoutMs用于确认是否命中健康检查窗口。业务请求日志也会写入operation_kind、operation_name、spacetime_stage和elapsed_ms,后续 45 秒超时不再只靠 Nginxrequest_time=45s推断。 genarrative-api.service设置LimitNOFILE=65535、TasksMax=2048;上线后用systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax -p TimeoutStopUSec和cat /proc/$(pidof api-server)/limits核对。- Server provision 不再通过 Windows helper 下载,也不再通过 Linux build 节点中转 SpacetimeDB / otelcol 工具包;Linux build 节点只负责从内网 Git 源准备 provision 脚本和配置并上传给目标 agent。
Prepare Provision Tools在目标 dev / release agent 工作区内先检查/usr/local/bin/otelcol-contrib与${SPACETIME_ROOT}/bin/current:版本已满足时直接复用目标机现有文件生成provision-tools/,只有缺失或版本不匹配时才使用PROVISION_DOWNLOADS_DIR里的本地包或从配置的下载源准备 SpacetimeDB2.6.0/otelcol-contrib 0.151.0;如果目标服务器下载需要代理,在PROVISION_DOWNLOAD_PROXY配置目标机可访问的 HTTP 代理。 - 除
Genarrative-Server-Provision外,Genarrative-Stdb-Module-Build、Genarrative-Web-Build、Genarrative-Api-Build、Genarrative-*Deploy、Genarrative-Database-Import/Export、Genarrative-Full-Build-And-Deploy和Genarrative-Notify-Email的生产流水线现都以 Linux agent 为主,仍按各自 Jenkinsfile 的 checkout 口径执行。Server provision 不使用公网备用 Git 源,目标部署 agent 也不再需要访问源码 Git remote。 otelcol-contrib.service作为可选系统服务加入 provision,默认监听127.0.0.1:4317/4318并使用deploy/otelcol/genarrative-debug.yaml。api-server 是否发送 OTLP 仍由GENARRATIVE_OTEL_ENABLED控制,服务 unit 见deploy/systemd/otelcol-contrib.service。该服务必须存在系统用户 / 组otelcol,并且/etc/otelcol/genarrative-debug.yaml已安装到目标机;若看到status=217/USER或Failed to determine user credentials,优先检查getent passwd otelcol,再补齐/etc/otelcol配置目录并重启服务。- Nginx
/api/与/admin/api/通过genarrative_apiupstream 代理到127.0.0.1:8082,upstream keepalive 为 64;limit_conn负责连接 / 并发保护,limit_req负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到genarrative_gallery_rps,默认rate=5000r/s、burst=4096、limit_conn=320;公开详情和普通 API 放到genarrative_api_rps,后台 API 放到genarrative_admin_rps。通用/apilocation 设置client_max_body_size 64m是反代兜底,防止拼图入口页 / 新增关卡本地参考图 Data URL 或旧兼容请求在到达api-server前被默认 1 MiB 上限拦截;拼图本地参考图前后端统一限制 6MB,历史图片仍提交referenceImageAssetObjectId(s)。若线上出现413 Request Entity Too Large且 access log 中request_time=0.000、upstream_status=-,说明请求在 Nginx 层被拦截,先用nginx -T | grep client_max_body_size检查 release 模板是否已渲染并 reload,同时检查前端是否超出 6MB 或错误提交了未压缩大图。limit_conn_status 429和limit_req_status 429必须在 HTTP 与 HTTPS server 中同时生效;若线上压测看到limiting connections by zone "genarrative_api_conn"却返回 503,优先检查nginx -T里 HTTPS server 是否缺少这些状态码,以及/api/runtime/puzzle/gallery是否误落到通用location ~ ^/api的limit_conn=64。压测时看/var/log/nginx/genarrative.access.log中的request_time、upstream_connect_time、upstream_header_time、upstream_response_time、upstream_status、request_id。 - 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用
SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works。 - 作品列表短期继续由
api-server/ BFF 订阅 SpacetimeDB 公开 read model 后读本地 cache,不让浏览器前端直接订阅完整列表;未来如新增public_work_gallery_entry等专用公开作品列表 read model,前端只可订阅稳定、低基数、公开的专用投影,禁止订阅puzzle_work_profile、custom_world_profile等玩法源表后自行 join、聚合或判断权限。前端直订阅落地前必须先补齐权限、字段契约、排序 / 分页、埋点和 BFF 回退策略。 - 50 HTTP req/s 验收目标为
http_req_failed < 1%、p95 < 2s、dropped_iterations = 0,同时压测窗口内 Nginx 无新增 502。2026-05-19 容器 2C / 2G 连续 10 轮不重启 SpacetimeDB 压测:PEAK_RPS=2500等价约 5000 HTTP req/s,平均实际吞吐约4219 HTTP req/s,10 轮总计1,897,357个 200、212,542个 429、0个 5xx,200 请求平均p95=123ms、p99=234ms;该档会把 SpacetimeDB 容器内存从约366MiB推到约885MiB / 896MiB,因此当前不要继续抬公开 gallery 入口并发,应优先处理 SpacetimeDB 侧连接 / 订阅 / tracking 写入后的内存高水位。
容器化压测与隔离部署方案单独放在 deploy/container/,用于本机或预发模拟 Linux release + Nginx + OTLP Collector 拓扑,不替换当前生产 systemd + Nginx + Jenkins 发布路径。当前容器模拟参数按 genarrative-release 采样值收口为 2 vCPU / 2 GiB RAM / nofile=4096 / worker_connections=768,并在 compose 里落实到 spacetimedb cpus=1.0 mem_limit=896m、api-server cpus=2.0 mem_limit=1g、external-generation-worker cpus=2.0 mem_limit=1g、nginx cpus=0.5 mem_limit=128m、otelcol cpus=0.25 mem_limit=128m、k6 cpus=1.0 mem_limit=512m。容器 api-server 默认 GENARRATIVE_API_WORKER_THREADS=4,只增加 Tokio worker 调度并发,不突破 api-server cpus=2.0 的 CPU 配额;容器默认 GENARRATIVE_EXTERNAL_GENERATION_MODE=queue,可用 npm run container:up -- --scale external-generation-worker=N external-generation-worker 验证外部生成 worker 动态扩缩容,inline 模式不参与该验证:
npm run container:init
npm run container:config
npm run container:build
npm run container:up
npm run container:k6
npm run container:down
容器方案默认暴露 http://127.0.0.1:18080,api-server 在容器内监听 0.0.0.0:8082,Nginx 通过 api-server:8082 upstream 反代 /api/ 和 /admin/api/。SpacetimeDB 也纳入 compose,容器内由 spacetimedb:3101 提供服务,宿主机通过 http://127.0.0.1:13101 进行模块发布;Collector 镜像使用 otel/opentelemetry-collector-contrib:0.151.0。生产 provision 侧现在由目标 dev / release agent 自己准备 provision-tools/otelcol-contrib,并安装本机 otelcol-contrib.service,真实库名、token 和外部服务密钥只写本地 deploy/container/api-server.env,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 deploy/container/README.md。
npm run container:config 默认只做 quiet 校验,避免把本地 env 中的 token 展开到终端;确需排查完整 compose 时再传 -- --print。
隔离验证 worker 队列和 API-only 更新时使用 npm run container:worker-smoke -- smoke。该命令不复用 deploy/container/api-server.env,会在 deploy/container/worker-smoke/ 生成本机专用 env 与端口 state,并使用 unsupported job 验证 worker claim / fail 回写,不需要真实外部生成密钥;本机 crates.io 网络不稳时使用 --local-binary,由容器内 Cargo 复用本机 Cargo 缓存构建,并把产物放进 Debian bookworm smoke runtime。
OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日志与 Nginx 文件日志仍保留:
- 生产与容器
api-serverenv 模板默认GENARRATIVE_OTEL_ENABLED=true;压测、排障或短期要关闭 OTLP 时,必须显式设置GENARRATIVE_OTEL_ENABLED=false。 - Collector 使用官方
otelcol-contrib,安装与启用仍由ENABLE_OTELCOL/ provision 控制,只监听127.0.0.1:4317/4318;本地用npm run otel:debug启动 debug exporter,用npm run otel:rider转发到 Rider,再接 Jaeger、Tempo、Prometheus、Grafana 或托管平台。 - api-server 发送 OTLP HTTP 时,生产模板使用
OTEL_SERVICE_NAME=genarrative-api、OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318,容器模板使用OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318。 OTEL_EXPORTER_OTLP_ENDPOINT必须指向 Collector 的 HTTP base endpoint;不要填 gRPC4317,也不要直接填 Rider 端口,Rider 由 Collector 通过RIDER_OTLP_GRPC_ENDPOINT转发。- 应用日志仍通过
journalctl -u genarrative-api.service查看,Nginx 日志仍写文件;日志等级继续用GENARRATIVE_API_LOG/RUST_LOG控制,例如info,tower_http=info,spacetime_client=info。 - debug exporter / Rider 转发都会同时接收 traces、metrics 和 logs。
- api-server 会随 metrics 发送进程级指标:
process.memory.usage、process.memory.virtual、process.cpu.time、genarrative.process.cpu.usage_percent、process.thread.count、genarrative.process.memory.private;Windows 额外发送process.windows.handle.count,Linux 额外发送process.unix.file_descriptor.count。这些指标只描述当前进程,不携带请求、用户或作品 label。 - HTTP 运行态补充发送
genarrative.http.server.response_bodies.in_flight与genarrative.http.server.request_permits.available,后者带低基数pool=default|gallery|detail|adminlabel,用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送genarrative.puzzle_gallery.cache.*指标,记录 fresh hit、stale hit、未命中、后台刷新开始 / 失败、重建耗时和预序列化 data JSON 字节数。 - 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine
gpt-image-2图片生成 / 编辑失败由platform-imageprovider 输出结构化日志字段,字段包括 provider、endpoint、failure_stage、status、source、source_chain、source_chain_depth、timeout、retryable、latency_ms、prompt_chars、reference_image_count、image_model、request_params 和 raw_excerpt;图片编辑请求参数日志还会带 reference_image_bytes_total,并在 request_params.referenceImages 中记录每个 multipartimagepart 的 fileName、mimeType 和 bytes,不记录 API key 或原始图片 bytes;api-server再记录指标genarrative.external_api.failures{provider,failure_stage,status_class,retryable},并写入tracking_event,event_key = external_api_call_failure、module_key = external-api、scope_kind = module、scope_id = provider。调用方能拿到身份上下文时,失败事件还会在行级user_id/owner_user_id/profile_id和metadata_json.userId/metadata_json.profileId/metadata_json.requestId/metadata_json.errorSource中记录触发者、草稿 / 作品作用域、请求标识和传输错误链。排障时先按 provider / failureStage 聚合,再下钻 userId / profileId,最后结合 request 日志、errorSource 和上游响应 excerpt 判断是限流、超时、解析失败还是未返回图片。 - OSS 平台适配器也输出结构化日志,覆盖
sign_post_object、sign_get_object_url、head_object和put_object。排查资产签名、上传或确认失败时,先按provider=aliyun-oss与operation过滤,再看object_key/key_prefix、status、status_class、error_kind、content_length、content_type和elapsed_ms;日志不得包含 AccessKey、policy、signature、Authorization header 或完整 signed URL。排查 generated 图片重复下载时,先确认前端输入是否为/generated-*legacy path 或可归一化的https://*.oss-*.aliyuncs.com/generated-*;正确链路应先调/api/assets/read-url,再由浏览器请求 signed URL,且同一路径、同一refreshKey版本和未临近过期的 signed URL 应复用。新上传 generated 私有对象应带Cache-Control: public, max-age=31536000, immutable;旧对象若只有ETag/Last-Modified,浏览器会走 304 协商缓存而不是长期强缓存,可通过刷新 OSS 元数据或 CDN 配置补齐。 - SpacetimeDB 观测分为两类:procedure / reducer 调用继续用
genarrative.spacetime.procedure.*,订阅本地 cache 读使用genarrative.spacetime.read.*。read=list_puzzle_gallery表示拼图广场当前从puzzle_gallery_card_view本地 cache 读取,不再每个 HTTP 请求调用list_puzzle_galleryprocedure。 - 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高
PREALLOCATED_VUS可能让 300 个本地 Established 连接把api-serverprivate memory 瞬时推到 GB 级,且/healthz小响应也能复现;若压测结束后回落、response_bodies.in_flight和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。 - Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes;请求完成日志会直接带
request_id、http.request.method、http.route、url.scheme、url.path、http.response.status_code、status_class、latency_ms和slow_request,完整链路继续到 Traces 面板按 trace/span 查看。 - 指标 label 只允许低基数字段:HTTP 使用
method、route、status_class,SpacetimeDB 调用使用procedure、status_class;request_id只进入 trace/log attribute,不进入 metric label。
常见外部服务变量:
GENARRATIVE_SPACETIME_SERVER_URLGENARRATIVE_SPACETIME_DATABASEGENARRATIVE_SPACETIME_TOKENGENARRATIVE_DATABASE_BACKUP_*GENARRATIVE_LLM_*VECTOR_ENGINE_*(已弃用,LLM 文本调用统一迁移到 VectorEngine)APIMART_*APIMART_*(历史残留,创意 Agent LLM 已迁移到 VectorEngine)HYPER3D_*VOLCENGINE_SPEECH_*DASHSCOPE_*ALIYUN_SMS_*WECHAT_*ALIYUN_OSS_*
结构化创作 / RPG 的 Responses JSON 链路默认不打开 web_search;本地和生产如需联网增强,必须显式配置 GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=true 或 GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=true。如果上游未开通工具,Responses 可能先吐自然语言再返回 ToolNotOpen,这类报错应按工具不可用排查,不要先当成 JSON 解析 bug。
创意 Agent gpt-5.4-mini 文本链路已从 APIMart 切到 VectorEngine:api-server 读取 VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY 构造 OpenAI-compatible LLM client,并自动补齐 /v1 前缀后请求 /chat/completions。通用 /api/llm/chat/completions 代理使用 GENARRATIVE_LLM_PROVIDER=openai-compatible、GENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1、GENARRATIVE_LLM_MODEL=gpt-5.4-mini,未单独配置 GENARRATIVE_LLM_API_KEY 时可复用 VECTOR_ENGINE_API_KEY。排查或切换密钥后,可在本地运行:
node scripts/test-ve-llm.mjs
该脚本读取仓库根目录 .env.secrets.local 中的 VECTOR_ENGINE_BASE_URL 和 VECTOR_ENGINE_API_KEY,依次探测 /v1/models、/v1/chat/completions、/v1/responses、gpt-5.4-mini Chat Completions 和基础 JSON 输出能力;脚本只输出 HTTP 状态、耗时、模型和截断摘要,不应打印密钥。若 .env.secrets.local 不存在,先补本地 secrets 文件再运行,不要把 secrets 提交进仓库。
手机验证码短信
手机验证码发送走阿里云普通短信 SendSms,验证码由 module-auth 在当前 api-server 进程内生成、哈希存储和校验,不再调用阿里云托管验证码的 SendSmsVerifyCode / CheckSmsVerifyCode。因此 api-server 重启后,已发送但未校验的验证码会失效。
生产默认短信配置:
ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com
ALIYUN_SMS_SIGN_NAME=北京亓盒网络科技
ALIYUN_SMS_TEMPLATE_CODE=SMS_506245486
ALIYUN_SMS_TEMPLATE_PARAM_KEY=code
阿里云模板参数固定发送为 {"code":"<验证码>"}。旧托管验证码相关变量如 ALIYUN_SMS_CODE_LENGTH、ALIYUN_SMS_CODE_TYPE、ALIYUN_SMS_RETURN_VERIFY_CODE、ALIYUN_SMS_CASE_AUTH_POLICY、ALIYUN_SMS_SCHEME_NAME 不再影响真实阿里云校验;验证码长度、有效期、冷却和失败次数由后端本地逻辑控制。真实短信联调仍需 SMS_AUTH_PROVIDER=aliyun、SMS_AUTH_ENABLED=true 和有效 ALIYUN_SMS_ACCESS_KEY_*。修改 .env.local 后必须重启 api-server,再用 /api/auth/login-options 确认返回包含 phone;如果通过 shell 临时覆盖,PowerShell 使用 $env:SMS_AUTH_ENABLED="true",cmd 使用 set SMS_AUTH_ENABLED=true,不要把引号作为环境变量值的一部分传给进程。
如需在本地确认平台层确实调用阿里云 SendSms,可手动运行默认忽略的真实短信测试。该测试会向 ALIYUN_SMS_REAL_TEST_PHONE_NUMBER 发送验证码短信,普通 cargo test 不会执行:
$env:ALIYUN_SMS_ACCESS_KEY_ID="..."
$env:ALIYUN_SMS_ACCESS_KEY_SECRET="..."
$env:ALIYUN_SMS_REAL_TEST_PHONE_NUMBER="13800138000"
cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms_real_provider_sends_verify_code -- --ignored --nocapture
埋点与运营查询
用户行为埋点原始事实写入 tracking_event,聚合投影写入 tracking_daily_stat。高频 HTTP route tracking 不直接阻塞请求链路:api-server 将普通 route tracking 先写入本机 tracking outbox,再由后台 worker 按数量或时间阈值批量写入 SpacetimeDB;daily_login、作品游玩 work_play_start、付费、任务领奖和钱包相关关键事件继续同步直写数据库,避免用户任务进度、游玩统计或支付状态出现可感知延迟。任务配置、进度、领奖、钱包流水分别写入:
profile_task_configprofile_task_progressprofile_task_reward_claimprofile_wallet_ledgerprofile_wallet_config
个人任务首版 scope 仅支持 user。每日登录任务按北京时间自然日 0 点重置;用户已登录并停留在“我的”页跨日时,前端需要先非阻断调用 refresh session 以写入新业务日 daily_login,再请求 /api/profile/tasks 刷新任务中心。认证成功后的 daily_login 必须通过 SpacetimeClient::record_daily_login_tracking_event(...) 调用 SpacetimeDB 专用 record_daily_login_tracking_event_and_return procedure,由数据库事务时间生成当日幂等事件并推进任务进度;不要改回普通 record_tracking_event_after_success、tracking outbox 或旧 profile.login.daily 事件键。后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 等特定链路按 tracking 中间件排除规则处理;作品游玩统一使用 work_play_start。
外部 API 失败审计复用 tracking_event,不新增表。失败事件优先写入本机 tracking outbox,再由后台 worker 批量落库;如果 outbox 因权限、磁盘或保护阈值不可写,会回退同步直写 SpacetimeDB。metadata_json 包含 endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、errorSource、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt、userId、profileId 和 requestId;其中 userId 是触发生成的用户,profileId 是调用方传入的草稿 / 作品 / 场景作用域,requestId 用于回查同一次 HTTP 请求日志,入口拿不到上下文时允许为空。常用查询:
SELECT event_id, scope_id AS provider, metadata_json, occurred_at
FROM tracking_event
WHERE event_key = 'external_api_call_failure'
ORDER BY occurred_at DESC
LIMIT 50;
按失败阶段、触发者和作品作用域聚合时:
SELECT
json_extract(metadata_json, '$.failureStage') AS failure_stage,
user_id,
profile_id,
COUNT(*) AS failures,
MIN(occurred_at) AS first_seen,
MAX(occurred_at) AS last_seen
FROM tracking_event
WHERE event_key = 'external_api_call_failure'
GROUP BY failure_stage, user_id, profile_id
ORDER BY failures DESC, last_seen DESC
LIMIT 100;
VectorEngine request_send 且 timeout = true 的记录表示 reqwest::Error::is_timeout() 判定为超时,常见于连接、发送请求体、等待上游首包或上游长时间无响应;errorSource 会保存 reqwest 底层错误链,若只看到 client error (SendRequest),表示 Hyper 只暴露到发送请求阶段,仍不等于最终根因。若 statusCode 为空,应优先查同一 requestId 的 api-server request 日志、provider 日志 source_chain、request_params、reference_image_bytes_total、Nginx / 出口网络、VectorEngine 可用性和请求体大小;若已有 502、429 moderation_blocked 等状态码,则按上游网关或内容审核失败单独处理,不要和传输超时混为一类。
tracking outbox 默认配置:
GENARRATIVE_TRACKING_OUTBOX_ENABLED=true
GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox
GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE=500
GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS=1000
GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES=268435456
GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS=5000
outbox 采用 NDJSON 文件保存原始事件。达到 BATCH_SIZE 时会立刻把当前 active 文件原子封存为 sealed 文件,并马上切到新的 active 继续写入;后台 worker 异步 flush sealed 文件,HTTP 请求线程不等待 SpacetimeDB。FLUSH_INTERVAL_MS 只负责兜底封存长时间未满批的 active 文件。SpacetimeDB 批量 procedure 返回成功后删除 sealed 文件,失败则保留文件并重试。MAX_BYTES 是磁盘保护阈值,不是 flush 阈值;超过后低价值 route tracking 可以被丢弃并记录日志 / 指标,关键同步事件不进入该丢弃路径。sealed 文件若出现无法解析的坏行,会重命名为 corrupt-* 隔离并记录 genarrative.tracking_outbox.files.corrupt 指标,避免一个坏文件阻塞后续批量入库。api-server 收到退出信号后会在 GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS 窗口内封存 active 文件并尽力 flush sealed 文件,超时或 SpacetimeDB 暂不可用时保留本地文件给下次启动继续投递。该机制提供至少一次投递语义,依赖 tracking_event.event_id 幂等跳过重复事件。
release 机器如果日志每秒刷 tracking outbox ... Permission denied (os error 13),先检查 /etc/genarrative/api-server.env 是否缺少 GENARRATIVE_TRACKING_OUTBOX_DIR。缺少时 api-server 会回退到本地开发默认相对路径 server-rs/.data/tracking-outbox,而 systemd 的工作目录是只读发布目录 /opt/genarrative/releases/<version>,genarrative 用户无法在其中创建 server-rs。修复顺序:
install -d -o genarrative -g genarrative -m 0750 /var/lib/genarrative/tracking-outbox
grep -n '^GENARRATIVE_TRACKING_OUTBOX' /etc/genarrative/api-server.env
systemctl restart genarrative-api.service
journalctl -u genarrative-api.service --since '30 seconds ago' --no-pager | grep -E 'tracking outbox|Permission denied|os error 13'
Genarrative-Server-Provision 和 Genarrative-Api-Deploy 会在保留旧 /etc/genarrative/api-server.env 的前提下补齐缺失的 tracking outbox 运行态路径,并确保 /var/lib/genarrative/tracking-outbox 归属 genarrative:genarrative。用户认证真相源只允许在 SpacetimeDB 正式认证表(user_account / auth_identity / refresh_session)恢复;不要再配置或依赖 GENARRATIVE_AUTH_STORE_PATH / auth-store.json,module-auth 也不再维护本地文件持久化;auth_store_snapshot 不再作为备查或运行期恢复源,只在正式认证表为空时一次性转移最新旧快照并清空,且旧 get_auth_store_snapshot / upsert_auth_store_snapshot / import_auth_store_snapshot 入口已经删除。如果 api-server 启动时连不上 SpacetimeDB,会持续重试启动恢复,直到认证工作集从 SpacetimeDB 正式表恢复成功后才开始监听 HTTP,以避免用空本地状态或旧快照覆盖认证表。
前端登录态恢复只把 /api/auth/refresh 的 401 / 403 当成权威失效信号;服务器重启窗口里的 502 / 503 / 504、浏览器 Failed to fetch 或 refresh 响应契约异常都必须保留已有本地 access token,不触发全局 auth 变化。refresh 成功响应以共享契约 RefreshSessionResponse { token } 为准,前端不要额外要求业务 ok 字段。排查“重启后用户都掉线”时,先区分前端是否被暂时不可用清掉本地 token,再检查 SpacetimeDB 正式认证表是否缺 user_account / refresh_session 数据。
常用检查思路:
-- 最近埋点
SELECT * FROM tracking_event ORDER BY created_at DESC LIMIT 50;
-- 用户每日聚合
SELECT * FROM tracking_daily_stat WHERE user_id = '<user_id>' ORDER BY stat_date DESC;
-- 任务进度
SELECT * FROM profile_task_progress WHERE user_id = '<user_id>';
-- 钱包流水
SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>' ORDER BY created_at DESC;
-- 充值订单
SELECT * FROM profile_recharge_order ORDER BY created_at DESC LIMIT 50;
-- 未完成过期补偿的充值订单
SELECT * FROM profile_recharge_order WHERE status = 'expired' AND expiration_checked_at IS NULL ORDER BY expired_at ASC;
-- 充值商品配置
SELECT * FROM profile_recharge_product_config ORDER BY sort_order ASC;
后台通用表查询已经处理 SpacetimeDB 无载荷枚举的 SATS 形态。新增后台表展示时,枚举列优先按表名和列名做业务映射,再落回通用解码。
后台通用表查询的“每页条数”不是筛选前的 SQL 截断量。API Server 通过单次 SELECT * ... LIMIT 50001 读取哨兵行,最多保留前 50,000 条候选;关键词 / JSON 过滤、所选列的完整候选集稳定排序和 1-based page 分页都基于这一次 SQL 结果,totalMatched 不再依赖另一份 COUNT(*) 快照。请求页码超过实际总页数时钳制到末页,零结果固定返回第 1 页。存在第 50,001 条哨兵行时响应必须返回 scanLimitReached=true,后台固定分页栏上方明确提示匹配总数和分页结果可能不完整,不得把扫描范围外的数据误报为不存在。候选 SQL 响应体仍受 32 MiB 和 20 秒硬限制;宽表即使每页条数很小也可能整次拒绝,不会返回部分结果。实时写入仍可能改变相邻请求的候选快照,精确审计应使用对应业务表的专用查询而不是通用浏览页。
Issue 与交接
- Issue tracker:自托管 Gitea。
- 可用工具:Gitea UI/API 或
teaCLI。 - 禁止默认使用:GitHub
gh、GitLabglab。 - Canonical triage labels:
needs-triage、needs-info、ready-for-agent、ready-for-human、wontfix。
交接时写清:
- 背景和目标。
- 已改文件。
- 未完成风险。
- 已运行命令和结果。
- 下一步建议。
文档维护
当前 docs/ 只保留少量融合文档。新增稳定知识时优先更新现有文档;只有现有文档无法容纳时才新增带 【标签名】 的 Markdown。阶段性流水账、一次性修复记录和已关闭实验不要再新增成长期文档。
微信登录与孤儿作品归属处理
- 微信新用户的
username统一拼为名字_openid;若昵称或 openid 为空,则分别回退到微信旅人和openid。 - 微信新用户的内部
user_id改为不可复用的user_前缀 UUID 风格,避免清库后旧作品被后来的顺序号账号顶替。 - 当作品作者的
owner_user_id找不到真实账号时,作品统一显示为占位作者:失效作者,公开陶泥号固定为SY-00000000,占位账号 ID 为wx-openid-placeholder。 - 该占位账号只用于作品作者域,不扩展到全站其它身份域。
- 如需把历史孤儿作品批量回填到占位作者,使用
scripts/rebind-orphan-work-owners.mjs先基于当前 auth 快照识别有效用户,再把缺失作者对应的作品表写回为占位 ID;脚本输入输出都基于 SpacetimeDB 迁移 JSON。
回填脚本用法
node scripts/rebind-orphan-work-owners.mjs --in <exported-migration.json> --out <rebound-migration.json>
node scripts/rebind-orphan-work-owners.mjs --in <exported-migration.json> --dry-run
node scripts/rebind-orphan-work-owners.mjs --in <exported-migration.json> --out <rebound-migration.json> --placeholder-user-id wx-openid-placeholder
--in:SpacetimeDB 导出的迁移 JSON。--out:写回后的迁移 JSON 输出路径。--dry-run:只统计回填行数,不写文件。--placeholder-user-id:需要时可覆盖默认占位账号 ID。