Files
Genarrative/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md
T
kdletters 1f904d28e9
Project CI / Frontend tests (push) Successful in 4m2s
Project CI / Repository checks (push) Successful in 2m16s
Project CI / Backend tests (push) Successful in 10m14s
Project CI / Native shell tests (push) Failing after 19m54s
AGC 客户端 MCP 能力暴露 (#274)
## 目标
保留现有客户端对话与 Codex app-server 链路,把客户端自身受控业务能力通过 MCP 暴露给 Codex。

## 范围
- 客户端会话、项目文件、资源、画布、生成、预览等稳定能力
- 审核 Skill 的索引与按需指导资源
- 复用现有账号、项目路径、权限、计费、幂等、锁和恢复边界

## 明确不做
- 不替换客户端对话入口或 Codex app-server
- 不让客户端替 Codex 判断高层意图、完成状态或规划
- 不暴露任意 Tauri command、shell、凭据、内部 URL、数据库和管理能力

当前 PR 先建立独立分支与审查边界,后续提交实现与定向验证。

Reviewed-on: #274
2026-09-08 22:01:29 +08:00

229 KiB
Raw Blame History

本地开发验证与生产运维

更新时间:2026-08-05

标准开发流程

同步代码 -> 读 AGENTS.md / docs/project-memory 共享记忆 -> 查当前 docs -> 小步实现 -> 本地验证 -> 更新 docs / project-memory -> 提交

如果当前文档不足以指导编码,先补文档再落地工程修改。

本地启动

AGC backend 模式与 all / api-server 一样,必须同时探测 API 和 BgFilter worker 端口,漂移后的 worker 地址同时传给 API、worker 和 readiness 检查。不能因为旧 worker 的 /readyz 可访问,就把新启动失败的同端口 worker 视为就绪;AGC 前端会等待完整配套后端,worker 失败可能最终表现为 Tauri 等待前端 180 秒超时。

npm run agc 外层启动器先执行 agc:serve 并等待前端与配套后端就绪,再启动 Tauri,同时清空本次 CLI 的 beforeDevCommand,避免重复拉起服务和把数据库发布时间计入 Tauri 的 180 秒前端等待。准备阶段最多等待 660 秒(后端门禁仍为 600 秒),退出时清理本次启动的服务树,不停止复用的服务。AGC 自动发布显式使用 --preserve-database,schema 冲突须人工确认迁移,不自动清空数据。

安装依赖:

npm install

完整联调:

npm run dev

该命令启动:

  • SpacetimeDB standalone。
  • 独立 bgfilter-worker
  • Rust api-server
  • 主站 Vite。
  • 后台 Vite。

npm run dev 和单模块 npm run dev:webnpm run dev:api-servernpm run dev:bgfilter-workernpm run dev:spacetimenpm run dev:admin-web 启动后都会更新根目录 .app/dev-stack.json。该文件记录本次命令、数据库、更新时间,以及 spacetimeapi-serverbgfilter-workerwebadmin-webpid、监听 host / port、可访问 URL、启动状态和当前命令。.app/ 是本地运行态目录,不提交 Git;端口漂移、服务重启或子进程退出后以该文件里的实际状态为准。

通过 nohup 在仓库根目录启动 dev 栈且未显式重定向 stdout / stderr 时,默认 nohup.out 会持续收集 SpacetimeDB、api-server、bgfilter-worker、主站 Vite 和后台 Vite 的整套 dev 栈输出;该文件已被主站 Vite watcher 和 Git 忽略,避免日志追加触发页面刷新循环,重启主站 Vite 后生效。若把输出显式重定向到其它仓库内文件(例如 > dev.out),该自定义文件不会自动获得同样的 watcher 保护,应改为写到 Vite root 之外,或同步配置精确的忽略规则。

单独启动主站前端:

npm run dev:web

单独启动 Rust API server

npm run dev:api-server

npm run dev:api-server 会由同一个启动器安全带起它依赖的独立 BgFilter worker,两者共享本次运行生成或显式配置的内部 Token;不要另外启动第二份 worker。只需单独运行内部 worker 时使用:

npm run dev:bgfilter-worker

Linux 本机多用户并发开发时,npm run devnpm run dev:* 单模块命令和 npm run agc 会先在系统级端口段注册表里给当前用户分配一个端口段,再把该段映射为 web = startapi = start + 1spacetime = start + 2admin-web = start + 3bgfilter-worker = start + 4agc-vite = start + 5。默认注册表目录是 /var/tmp/genarrative-dev-port-ranges/,其中 registry.json 记录各用户的活跃段,registry.lock 负责串行化分配;可以用 GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR 覆盖目录。系统自动分配时从 10000-10099 开始,每次占用 100 个端口块,后续块按 10100-1019910200-10299 递增;同用户已有 worktree 占用首选 AGC 槽位时,AGC 只在本用户段内继续漂移。GENARRATIVE_DEV_PORT_RANGE--port-range 只在 Linux 上生效,Windows 仍按原来的 3000 / 8082 / 3101 / 3102 / 8083 与 AGC 兼容优先端口 3080 统一探测并漂移,不读这个系统级注册表。父 API 与 worker 始终使用解析后的实际 GENARRATIVE_BGFILTER_WORKER_BASE_URL,不能写死 8083

后端日志默认写入 logs/api-server/,独立 BgFilter worker 日志默认写入 logs/bgfilter-worker/。后端 API smoke 使用 npm run dev:api-server,先检查 BgFilter worker /readyz,再检查 API /healthz;需要确认 API 实例可接生产流量时检查 API /readyz。不要使用旧 api-server:maincloud 或任何 GENARRATIVE_SPACETIME_MAINCLOUD_* 口径。

AI 游戏创作客户端使用 npm run agc。该入口由 apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs 解析 AGC Vite 实际端口:Linux 默认取当前用户端口段的 start + 5,占用时只在本用户段内漂移;Windows / macOS 保留 3080 为兼容首选并允许统一漂移。最终端口通过 GENARRATIVE_AGC_VITE_PORT 传给 beforeDevCommand 和配套后端端口解析器,通过 Tauri CLI 动态 build.devUrl 配置传给 WebView,并通过 Vite CLI --port 启动严格监听;Vite 继续使用 strictPort,任何一层都不得自行改到另一个端口。AGC 配套后端的 backend 模式启动 SpacetimeDB、独立 bgfilter-workerapi-server,并在复用现有后端前同时检查三者状态及 /v1/ping/readyz/healthz;worker 缺失时不得把不完整的 API/数据库组合误判为 ready。任一配套服务在启动阶段进入 failed 时,外层启动器必须立即报告具体服务和退出原因,不能继续等待前端地址超时。启动器在创建原生窗口前预检最终地址;若竞态中该地址被 AGC Vite、无响应监听器或其它服务占用,一律失败关闭,不复用、也不擅自终止无法证明归属的进程。

Tauri beforeDevCommand 默认与客户端构建并行,不能把上述检查只放在 beforeDevCommand 内:选定地址上若已有旧 Vite,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 SIGTERM、有界等待后升级 SIGKILLWindows 使用 taskkill /PID <pid> /T /F。Linux 容器中的孤儿后代退出后可能暂时保留为 zombie,kill(-PGID, 0) 仍会返回成功;启动器必须结合 /proc/<pid>/stat 判断同组是否还存在非 zombie 成员,不能把等待 PID 1 回收误报为清理失败。配套后端和 Vite 仍由 start-dev-stack.mjs 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对控制台输出的 AGC Vite 实际地址及其 marker、.app/dev-stack.json 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。

Windows 本地 npm run dev / npm run dev:api-server / npm run dev:bgfilter-worker 会用空的 RUSTC_WRAPPER / CARGO_BUILD_RUSTC_WRAPPER 覆盖 server-rs/.cargo/config.toml 里的 sccache,从而直连真实 rustc。完整栈和 dev:api-server 把 API 与 BgFilter worker 作为一个 Rust 重启单元:源码变化时先停两个进程,再先启动并验活 worker、最后启动并验活 API,避免两个 cargo run 并发链接同一个 Windows 可执行文件。不要把 wrapper 绕过值写成 rustcCargo 会按 wrapper 协议调用 rustc <真实rustc路径> - ...,最终报 multiple input filenames provided 并导致 api-server 无法启动。排查本地启动失败时,先看 dev 日志是否出现该错误,再确认脚本注入的 wrapper 为空。

本地 Rust 构建缓存与磁盘上限

server-rs/Cargo.tomlapps/ai-game-creator-shell/src-tauri/Cargo.toml 是两个独立 Cargo workspaceAGC 会以 path dependency 复用 agent-runtime-coreplatform-llmplatform-agentshared-contracts,但两边默认仍分别写入 server-rs/targetapps/ai-game-creator-shell/src-tauri/target。这个代码和锁文件边界继续保留,不为节省磁盘直接合并 workspace;生产构建脚本和 Tauri 发布还依赖当前 manifest / lock / target 身份。

一次性 cargo test 不应长期保留增量缓存。两个 workspace 的 [profile.test] 固定 incremental = falsedebug = 1AGC [profile.dev]server-rs 对齐 debug = 1codegen-units = 256 和非 LTO 的开发口径,保留交互式 cargo run 的增量编译。这样全量测试不再按每组 feature / crate hash 累积大量 debug/incremental 目录,同时不把每次开发启动都退化为冷编译。

仓库统一使用以下本地工具:

# 默认只读:统计两套 target 和 incremental 体积,超过默认阈值时警告
npm run audit:rust-build-cache

# 仍然只读:列出本次会清理的两个固定目录
npm run clean:rust-incremental

# 显式写入:仅删除两个 debug/incremental,不删 deps、release、源码或锁文件
npm run clean:rust-incremental -- --apply

审计结果为跨平台可复现的文件逻辑字节数,默认警告阈值为 120 GiB;Windows 稀疏文件、压缩或分配单元可使该数值与磁盘物理占用不完全一致。清理后同时使用操作系统剩余空间复核真实回收量,不把逻辑字节差值冒充物理空间。

清理工具只接受仓库内两个编译目标的固定 debug/incremental 路径,默认 dry-run;路径越界、根目录或目标是符号链接 / junction、仓库标记缺失,或存在活跃 cargo / rustc 进程时均失败关闭。清理后的首次测试或开发构建会变慢,属于可再生缓存预期行为。

共享 CARGO_TARGET_DIR 不作为默认第一阶段方案:npm run agc 会并发启动 server-rs 和 Tauri Cargo,两个 workspace 指向同一 target 会引入构建锁串行化。需要共享时必须先用冷 / 热启动基准证明依赖复用收益大于并发锁等待,并保持生产 CARGO_TARGET_DIR 显式覆盖和现有发布产物路径不变。

Windows 本地如果已在 %LOCALAPPDATA%\Genarrative\ffmpeg\bin 安装 FFmpegnpm 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 devnpm run dev:api-server 都会注入 GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true,因此密码登录在本地开发环境可直接注册未知手机号账号。完整 npm run dev 会强制父 API 使用 GENARRATIVE_PROCESS_ROLE=all,忽略外层显式角色,确保本地 api-server 同时监听 HTTP 并消费外部生成队列;只有单模块 npm run dev:api-server 会保留显式 GENARRATIVE_PROCESS_ROLE,未设置时默认为 allall 不内嵌 BgFilter worker;启动器总是先启动并验活独立 GENARRATIVE_PROCESS_ROLE=bgfilter-worker 进程,再启动父 API,并向两者注入同一个内部 base URL / Token。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 队列;更接近生产的验证应分别启动 apiexternal-generation-workerexternal-generation-controller。生产默认 GENARRATIVE_PROCESS_ROLE=api,外部生成任务由独立 GENARRATIVE_PROCESS_ROLE=external-generation-worker 进程消费;生产与容器扩缩容验证保持 queue。当前 worker 只领取 source_module = editor-canvas 的图片画布任务,包括 editor_image_generationeditor_image_editeditor_background_removaleditor_icon_spritesheet_generationeditor_ui_design_asset_extractioneditor_character_animation_generationeditor_video_generationeditor_sound_effect_generationeditor_background_music_generation。旧玩法历史任务即使仍为 pending / running 也不领取、不改状态;显式把本地进程角色设为 api 且没有 worker 时,现役编辑器生成请求只返回 queued/running,不会兜底执行外部 provider。

HTTP 角色的 GENARRATIVE_SPACETIME_POOL_SIZE 只表示 procedure / reducer 调用池大小;池连接不订阅 read model。HTTP 角色会额外创建 1 条共享缓存读连接,当前只保留可选的 user_account 读取;配置为 8 时基础连接拓扑是 8 条调用连接加 1 条缓存读连接。/readyz 同时检查调用池与缓存读连接,缓存连接未准备好时不能放量。

生产拆分角色时,external-generation-workerexternal-generation-controller 的专属 env 示例会把 GENARRATIVE_SPACETIME_POOL_SIZE 覆盖为 1;非 HTTP 角色不创建 API 缓存读连接,只保留 external_generation_job 队列窄订阅作为响应式唤醒信号,实际抢占和扩缩容判断仍走 SpacetimeDB procedure。worker / controller 不执行模型定价 seed,启动时先调用受 runtime writer 鉴权的 queue-stats procedure 做只读预检,身份不匹配时 fail-fast;当前正式 systemd unit 通过共同加载 API env 继承同一 GENARRATIVE_SPACETIME_TOKEN,默认路径为 /etc/genarrative/api-server.env,自定义部署由 provision 和 API deploy 按实际参数渲染,专属角色 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=900editor_image_generationeditor_image_editeditor_icon_spritesheet_generationeditor_ui_design_asset_extraction 四类 VectorEngine 图片任务与角色动画 / 视频类长任务使用 GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800,手动去背景、音效和背景音乐继续使用普通预算。worker 在单次尝试超过执行预算后会停止续租,但不会取消已启动的业务 future 或主动写入失败 / 重试状态;执行许可会一直绑定到 active 或 detached work 真正结束(或超过租约仲裁窗口被取消),避免超时任务脱管后立即补进新的高内存任务。在途执行由 lease fencing 仲裁,有效租约内写回仍可完成,租约过期后任务才可重新领取,attempt 耗尽时由认领事务标记失败并结算退款。生产部署和 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 素材提取和角色动作逐帧去背景使用 background_mode=flat;手动 POST /api/editor/images/background-removals 使用 background_mode=complexseg_model=birefnetcross_check=off。父流程不再直连 BgFilter,而是把已持久化的私有 OSS object key、模式参数、排队预算 maxQueueWaitMs 和调用预算 callBudgetMs 交给唯一的 loopback bgfilter-worker。子 worker 负责签发短期源 URL、全局 admission Q、provider 并发 N、最多两次顺序 attempt、结果校验和按 flat / complex 隔离的进程级熔断;成功时直接用内部 HTTP 二进制 body 把原始结果图片字节返回父流程,不写 raw OSS。当前冻结 N=16、单图估时 est=5000msQ 默认 2048 且仅作为连接风暴保险丝;角色动画仍可同时提交最多 48 个单帧逻辑调用,但健康 worker 中实际在飞的 BgFilter provider 请求不超过 N

BgFilter 不再配置独立的固定 attempt timeout。父子共同按 attempt = N × est × 2 派生单次真实 provider attempt 上限,并按 callBudgetMs = 2 × attempt + 1s 派生调用预算;当前 N=16 / est=5000ms 时分别为 160s / 321s。父侧仍管理父 job 总预算,按剩余绝对预算派生 maxQueueWaitMs;子 worker 从 admission 开始只用该字段等待 provider permit,取得 permit 后才启动 callBudgetMs,排队不侵蚀两次完整 attempt 窗口。flat 还由父侧预留阿里云 request timeout 和本地处理余量。flat 两次失败、熔断、overload 或内部 RPC 故障且父业务预算仍有效时,父流程才继续“阿里云通用抠图 → 本地键色”;阿里云 fallback 不属于 BgFilter worker。complex 的真实 provider 失败只累计自身熔断,任意失败或熔断仍直接使父流程失败,不接 flat fallback,也不影响 flat 状态。父侧不会重试已被 worker 接收的内部 HTTP(连接从未建立的失败按调度方案 §5.1 有界重连),避免子侧两次乘成四次 provider attempt。

阿里云通用抠图默认 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 未配置,跳过抠图客户端初始化」,flat 失败后直接进入本地键色。两种模式的 provider 配置统一使用 GENARRATIVE_EDITOR_BGFILTER_BASE_URLGENARRATIVE_EDITOR_BGFILTER_TOKEN,父子共同使用 GENARRATIVE_BGFILTER_WORKER_CONCURRENCYGENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS 派生预算;旧 GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN 只保留为 token 兼容别名。修改 provider、内部 worker 或 fallback 配置后,需要按角色重启对应进程。

阿里云通用抠图的非上海地域输入使用 AuthorizeFileUpload → Policy POST → SegmentCommonImage 正式链路,上传 Bucket / Endpoint / ObjectKey 由阿里云动态返回;不得恢复 GetOssStsToken、固定 viapi-customer-temp、临时 AK/SK 或 OSS V1 PUT。该切换不新增环境变量;真实链路冒烟可运行 cargo run -p platform-matting --example segment_smoke --manifest-path server-rs/Cargo.toml -- <图片路径>,预期日志中的输入 host 为授权响应返回的上海 OSS host,并完成结果下载。图片字节仍经过执行任务的 api-server / worker,排障时不要把 Advance 路径误判为阿里云直接抓取任意公网 URL。

BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去背景源图继续使用短期签名 URL,但签名只在 bgfilter-worker 内生成并传给 provider;父进程到子进程只传 object key 和参数,不传源图字节或签名 URL。子 worker 完整读取并校验 provider 成功 body 后,把同一图片内容作为受限原始字节响应返回;父流程继续负责 Alpha / 尺寸恢复、动画 finalizer、最终 OSS、画布写回、计费和终态。flat fallback 才由父流程按原路径下载源对象供阿里云或本地键色使用。排障日志只应出现 object key 与签名有效期,不得记录带 x-oss-* 查询参数的完整 URL、内部 Token 或图片字节。

图片编辑器任务侧栏与生成提交工作流只读取 BFF 队列接口:GET /api/runtime/external-generation/jobs 列出当前用户任务,GET /api/runtime/external-generation/jobs/{jobId} 查看单 job 状态,概览场景可使用 GET /api/runtime/external-generation/queue-overview。前端不直接查询 external_generation_job private table,也不展示 worker 内部 payload;完成态以编辑器项目和资源接口返回的正式数据为准。

外部生成任务摘要投影与历史 payload / history 维护使用 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_countbefore_bytesafter_bytesinline_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。历史清理使用 --prune-history,默认 source_module=editor-canvas、30 天保留期;候选必须是 completed / failed / cancelled 终态、主任务与摘要状态一致、摘要存在 notification_acknowledged_at 且终态时间不晚于 completed_before_micros,否则永不删除。先 dry-run,记下输出的 completed_before_micros,再保持相同 --cursor-job-id 与 cutoff 追加 --apply;apply 在一个事务中删除该 job 的所有 event、summary 和主任务,资产对象与钱包流水保留。需要清理其它 source module 时必须显式 --source-module 并先完成业务评估;这不是自动 systemd 任务,不得授予 runtime writer 清理权限。Stdb 构建 artifact 和完整 release 包都必须包含 scripts/spacetime-maintain-external-generation-jobs.mjsscripts/spacetime-migration-common.mjs。首次上线不得让 Full Build 从 Stdb 自动直落 APISTDB_API_ROLLOUT_MODE 默认 fail-closed 为 pause-after-stdb,必须填写受限的 STDB_API_ROLLOUT_APPROVERSStdb 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。

角色动作正式字段收口使用 node scripts/spacetime-normalize-editor-character-actions.mjs --database <database> --server-url <url>,且同样只能由已授权 migration operator 执行。必须先发布包含 normalization cursor 索引和 normalize_editor_character_animation_metadata_and_return 的 SpacetimeDB 模块,在 API / worker 仍处于维护模式时先运行默认全量 dry-run;脚本固定按 asset → project-resource → showcase → canvas 扫描,普通 scope 每批最多 25 行,canvas 每批最多 5 行。全量 dry-run 会在不写库的情况下把 asset 计划结果投影给同 owner / task / 首帧对象精确匹配的 project-resource,再把前置 scope 的计划结果投影给 canvas 检查;因此同 task 的误标预览 MP4 会先按权威视频对象排除,最终图片序列会逐帧核对并补齐精确 asset_object 身份。canvas 中仍引用误标 preview resource 的普通 video layer 会按 project-resource 计划态 video 跳过,只有 layout 明确声明动作却指向视频,或资源规划本身失败时才形成 blocker。apply 时仍要求前置 scope 已按顺序物理完成,不能跳过 asset 直接让 project-resource 借未落库结果。历史 canvas 复制的 sourceResourceId 不是迁移证据,不要因它仍指向原角色而手工改库,补建资源会采用最终账号素材的 DB 血缘。出现 blocker 时脚本会打印 ID、原因、owner、project、task、对象身份和来源资源;先据此区分最终候选为零 / 多个、正式与旧版冲突、帧对象不匹配或缺失资源,不得跳过 scope。确认 dry-run 后追加 --apply,脚本会对每批重新 dry-run、携带该批 SHA-256 apply,并在最后从头要求四个 scope 均为零匹配、零 blocker。只有该复核通过后才发布移除 action fallback 的 API / Web。Stdb build artifact 和完整 release 包必须同时包含 scripts/spacetime-normalize-editor-character-actions.mjsscripts/spacetime-migration-common.mjs。本地切换分支时若要避免 dev publish 因 schema 冲突使用 -c=on-conflict 清库,启动命令必须追加 --preserve-database,让冲突直接失败。动作视频抽帧临时目录固定使用 /var/lib/genarrative/character-animation-tmp,该路径已由生产 API / worker unit 放行;不要让动作抽帧重新依赖 PrivateTmp 下的 /tmp

普通图片错误素材类型清理使用 npm run spacetime:editor-image-asset-kind:clean -- --database <database> --server-url <url>,只能由已授权 migration operator 执行。先进入维护模式并发布包含 clean_editor_image_asset_kind_and_return 的 SpacetimeDB module,并保持旧版本 API / controller / worker 停止;随后运行默认全量 dry-run,核对 asset → project-resource → showcase → canvas 各 scope 的扫描数、命中行数、字段数和 blocker 均符合预期,再追加 --apply。脚本对每批重新 dry-run、绑定包含画布迁移摘要、结构化 layer 与 generation-dialog 权威 JSON 的 SHA-256,最后自动从头复核零命中;任一画布数据异常都会只输出哈希化 ID、scope 与原因并停止,不能跳过。清理只处理精确业务旧值,不修改 asset_object.asset_kind、MIME 或媒体类型;project-resource scope 在清行前验证同工程 migration 并将其状态纳入批次 hashlayout version 0 的 legacy 画布可以没有 migration,但 structured 画布缺 migration 必须立即形成 blocker,资源行不得先被清空;清行后能保持原 status 不变量时立即刷新摘要,否则只允许留给后续精确 canvas 字段清理收口。canvas scope 在任何布局写入前再次按 active / backfilled / rolled_back 状态验证原 migration 凭证和双份 legacy / structured 不变量,将 editor_canvas_generation_dialog.dialog_json 与 layer rows 一并扫描并在同一事务 patch;只允许本批资源清零及精确字段删除造成的差异,写入后从全部结构化权威行重建 layout、再次复核新状态才受控重签摘要,同时保持业务 revision、migration status 与全部时间戳不变。新版本 API、SpacetimeDB storage 创建入口、legacy 画布元数据提取和项目资源落表边界都会将 trim 后精确等于 imageassetKind 归一为 NULL,防止旧页面、滞留请求或 legacy 保存重新制造废弃值。完成零残留复核,并分别确认 cleaned backfilled 可激活、active 可继续保存、rolled_back 可重复复检后恢复应用版本,最后退出维护。Stdb build artifact 和完整 release 包必须同时包含 scripts/spacetime-clean-editor-image-asset-kind.mjsscripts/spacetime-migration-common.mjs

自 2026-07-11 起,Genarrative-Full-Build-And-Deploy 的每日 04:00 timer 默认以 DEPLOY_TARGET=developmentSTDB_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 阶段。该阶段只能通过 agent none 和显式 node(...) 分配目标机,直接执行 /opt/genarrative/current/scripts/deploy/maintenance-off.sh;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。Genarrative-Api-Deploy 也单独暴露 KEEP_MAINTENANCE_MODE 参数,并转换为随发布包脚本的 --keep-maintenance-mode;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 warning 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。

维护门禁启用时,Nginx 与 Pingora 只精确放行默认维护页依赖的 /branding/taonier-maintenance-page.png/branding/taonier-product-ip.png;不得放开整个 /branding//assets/ 或后台静态目录。公网验收除主站、后台和 API 继续返回维护响应外,还必须确认这两个品牌图片返回 200 image/png,避免维护页 HTML 正常但背景图请求被再次改写成 503

需要验证“更新 API 不停 worker”和“worker 是否持续消费队列”时,优先使用隔离容器 smoke:npm run container:worker-smoke -- smoke。该脚本生成 gitignored 的 deploy/container/worker-smoke/api-server.env,启动独立 compose project 与独立 SpacetimeDB,发布当前 spacetime-module 后写入 source_module = editor-canvasjob_kind = 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 状态接口和 worker 日志确认任务从 queued/running 收敛到预期失败态。

本地只做账号/UI smoke 且需要短信登录时,SMS_AUTH_PROVIDER 应显式设为 mock,并把 SMS_AUTH_MOCK_VERIFY_CODE 设为固定值(当前常用 123456),再重启 npm run devnpm run dev:api-server。如果 .env.local 还保留 SMS_AUTH_PROVIDER=aliyunPOST /api/auth/phone/login 用 mock 验证码会稳定报“验证码错误”,不是前端表单问题。真实短信联调再切回 aliyun 并重启。

微信小程序虚拟支付使用 WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_IDWECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEYWECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEYWECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV 配置。小程序充值统一走 wechat_mp_virtual / wx.requestVirtualPayment:泥点属于代币(coin),buyQuantity 按当前充值商品快照里的 points_amount 传;会员和后台新增道具类商品走 short_series_goodsproductId 对应微信后台道具 ID。旧登录快照若缺 session_key,需要用户在小程序内重新登录后再支付;客户端成功回调不是最终到账,仍以后端通知或查询确认订单为准。详细口径见 docs/【技术方案】微信虚拟支付接入-2026-05-26.md

普通微信充值订单本地有效期为 5 分钟。SpacetimeDB 原生 profile_recharge_order_expiration_timer 到点后只把仍为 pending 的订单改为 expiredHTTP api-server 只订阅活跃 timer 表的删除事件,按事件中的 order_id 重新读取订单并仅对 expired 执行微信查单补偿,不订阅完整充值订单历史表。支付或主动关闭也会删除 timer,但读取到非 expired 后直接忽略;监听断线窗口由未检查过期订单 catch-up 补齐。external-generation-workerexternal-generation-controller 不运行充值过期逻辑,也不应因为扩容外部生成 worker 放大微信查单或关单流量。查账时本地未支付终态保持 expired,不再改写为 closedexpiration_checked_atexpiration_provider_stateexpiration_last_error 用于判断 HTTP 监听器是否已经完成补偿。

普通微信支付 V3 退款联调与对账

普通微信支付 V3 的退款结果入口固定为 POST /api/profile/recharge/wechat/refund-notify。支付下单的 WECHAT_PAY_NOTIFY_URL 只接收支付结果,不能替代退款入口;通过 POST /v3/refund/domestic/refunds 发起退款时,必须在该次请求的 notify_url 中显式传入公网退款入口。回调不做用户登录态校验,但必须保留原始 body 和 Wechatpay-* 请求头供验签、解密;验签、契约校验和统一退款 observation 持久化成功后才返回 204。重复回调、主动查单、已验签退款申请响应和交易账单发现都进入 record_profile_recharge_refund_observation_and_return,依赖稳定 out_refund_no、微信退款单号和 observation 指纹幂等,禁止直接改充值订单或手写退款表。

主动查单和退款交易账单 worker 默认关闭,只由 api / all 这类 HTTP 角色运行。真实部署具备商户私钥、商户证书序列号、平台公钥及序列号、APIv3 密钥和 SpacetimeDB runtime service identity 后,才在服务私密环境中开启:

WECHAT_PAY_REFUND_RECONCILIATION_ENABLED=true

deploy/env/api-server.env.example 已把生产值固定为 trueproduction-api-deploy.sh 会为存量 env 缺失项补齐该值;若同时配置 WECHAT_PAY_ENABLED=trueWECHAT_PAY_PROVIDER=real 却显式关闭 reconciliation,部署在切换 current 前失败并保持 fail-closed。env 出现重复键时必须与 systemd EnvironmentFile 一致按最后一次赋值判断,不能让前面的 true 掩盖运行态最终生效的 false。发布后应在 API 启动日志确认 wechat pay refund reconciliation worker is enabled,不能只看 /readyz

开启后,processing / abnormal 退款按 1、5、10、20、30 分钟衰减主动查单;success 且权益尚未收口时只重试本地回收,不重复请求微信。成功退款早于支付通知时,order_missing / order_not_paid 会继续等待晚到支付事实;会员退款及金额、渠道、交易号冲突保持人工复核。候选列表按分钟轮转分页,超过单批 100 条也不会长期饿死,失败日志只输出哈希引用与静态分类。北京时间次日 10 点后,worker 按 30 个稳定分片轮转补扫微信 API 可查询的近 90 天 bill_type=REFUND 交易账单,每 30 分钟覆盖完整窗口;PLATFORM-ORIGINAL / PLATFORM-BALANCE 用于发现商户平台手工退款,但账单行不能直接决定本地终态,必须再按 out_refund_no 查单并验响应签名。单个坏行只让该日保持可重试,不阻塞其余行或日期,也不写完成 checkpoint;昨日返回 NO_STATEMENT_EXIST 时至少延迟到次日 10 点后再确认空账单。profile_recharge_refund_bill_checkpoint 已存在的日期不会重复处理;多实例或重启造成的重复 observation 仍由统一事务幂等收口。超过近 90 天 API 窗口的历史账单需从商户平台下载后受控核对。

公网联调可使用临时 HTTPS tunnel,但必须确认 tunnel 正在转发当前 api-server 实际监听端口,且公网 /healthz 与本地 /healthz 指向同一进程。notify_url 不能带查询参数;临时域名变化后,只影响之后新提交的退款请求,已经提交给微信的旧退款单仍绑定旧地址。无签名探测只能验证路由可达,不能伪造成功回调:

curl -i https://<公网域名>/healthz
curl -i -X POST 'https://<公网域名>/api/profile/recharge/wechat/refund-notify' \
  -H 'Content-Type: application/json' \
  --data '{}'

第二个请求在真实 provider 配置下应因缺少微信签名返回 4xx 与微信 FAIL envelope;若返回 404,优先检查 tunnel 目标端口和运行中的二进制是否已包含退款路由。不要把 ngrok inspect 页面、商户私钥、APIv3 密钥、平台公钥原文、签名、密文或解密 payload 写入文档、工单和日志。

真实联调时可开启支付 handler 与退款 reconciliation 的 debug 日志。日志应出现“收到微信支付 V3 退款结果通知,开始验签解密”“退款结果通知已持久化”或 wechat pay refund observation persisted,并通过稳定 HMAC / SHA256 引用关联重试;不得期待日志输出商户订单号、微信订单号、退款号或原始 payload:

npm run dev -- --log 'info,api_server::wechat::pay=debug,api_server::profile_recharge_refund_reconciliation=debug,tower_http=info'

联调后使用有权读取目标库私有表的 SpacetimeDB 身份做只读核对。以下查询中的占位符必须替换为目标环境值;不要用 SQL INSERT / UPDATE / DELETE 补退款,否则会绕过 observation 冲突校验、订单级累计退款和权益回收事务:

spacetime sql <database> "SELECT * FROM profile_recharge_order WHERE order_id = '<order_id>'" --server <server-url>
spacetime sql <database> "SELECT * FROM profile_recharge_refund WHERE order_id = '<order_id>'" --server <server-url>
spacetime sql <database> "SELECT * FROM profile_recharge_refund_observation WHERE out_refund_no = '<out_refund_no>'" --server <server-url>
spacetime sql <database> "SELECT * FROM profile_recharge_order_refund_settlement WHERE order_id = '<order_id>'" --server <server-url>
spacetime sql <database> "SELECT * FROM profile_recharge_refund_bill_checkpoint" --server <server-url>

核对规则:部分退款时原订单保持 paid;累计退款等于订单金额后才为 refunded,但 paid_at 继续保留,因此不会恢复首充资格。泥点退款只回收普通永久泥点;每日免费泥点和会员周期泥点不动。永久泥点不足时 recovery_status=shortfallunrecovered_points>0wallet_frozen=true,正式钱包消费在欠款清零前 fail-closed;会员订单统一为 manual_review,不得自动缩短有效期或扣周期泥点。

后台充值订单退款必须通过管理员鉴权接口执行,不得从数据库页面直接改表:列表 GET /admin/api/profile/recharge-orders、用户详情 GET /admin/api/profile/users/detail、历史花费手动对账 POST /admin/api/profile/users/reconcile-consumption、存量消费投影初始化 POST /admin/api/profile/users/initialize-consumption-projections、预检 POST /admin/api/profile/recharge-refunds/preview、执行 POST /admin/api/profile/recharge-refunds/execute、应急退款号登记 POST /admin/api/profile/recharge-refunds/register、人工冻结/解冻 POST /admin/api/profile/wallet-restriction。用户详情返回的 historicalConsumedPoints 来自 profile_wallet_consumption_total,表示退款不冲减的历史总消费;已有投影的正常消费只做按主键 O(1) 原子累加,缺行时按该用户钱包流水索引兜底重建一次,不得用只返回最近 50 条的钱包流水列表在 BFF 或前端重算。手动对账是独立操作权限:owner 始终拥有,member 必须在账号管理中单独勾选“手动对账用户历史花费”,任意 Tab 权限都不隐式授予;接口经二次确认后才扫描该用户全部权威流水、校准投影并记录管理员和时间。投影初始化接口只允许 owner,必须在首次上线的停写维护窗口执行并成功后再恢复业务流量;它扫描全部权威钱包流水并初始化或校准全部消费投影。维护遗漏的存量缺行仍由用户详情首次读取按用户索引兜底回填。预检返回微信支付状态、本地累计退款、剩余可退金额、预计追回泥点、钱包总额、可消费余额、活动占用和退款欠账;只有预检允许且二次确认后才提交退款。提交使用稳定 requestId,接口超时后重试必须复用同一值。若返回“退款处理中”,先查同一 out_refund_no,不要换号再次发起。商户平台应急退款完成后,在后台登记原 out_refund_no 触发验签查单;微信已退款但缺少退款号时等待 T+1 账单,不得凭截图或支付订单 REFUND 状态直接手写退款事实。

首次发布消费投影时,先停止业务写入并确认 api-server 与新 SpacetimeDB module 已就绪,再使用当前 owner 登录获得的短期 token 执行:

curl -fsS -X POST \
  -H "Authorization: Bearer ${ADMIN_BEARER_TOKEN}" \
  "https://<API 域名>/admin/api/profile/users/initialize-consumption-projections"

响应中的 scannedLedgerCount 是扫描流水数,projectedUserCount 是已存在钱包流水并完成投影的用户数;只有请求成功且返回 ok=true 后才恢复业务流量。该接口允许幂等重跑,但每次都会全表扫描,只能在维护窗口由 owner 执行,不得加入普通定时任务或页面自动请求。

正式落账上线前已经被旧 debug handler 返回成功的退款回调不会因部署新版本自动重放。已知 out_refund_no 的历史退款应由具备真实商户凭据的受控服务端操作先调用单笔退款查询,验签后写入同一 observation 事务;未知的商户平台退款等待次日交易账单发现。自动账单按分片补扫微信 API 可查询的近 90 天,超出窗口的历史退款需从商户平台导出核对后逐笔受控查单补录,不能直接把商户平台截图或 CSV 行当作退款终态,也不能开放匿名或普通用户补录 / 退款入口。

旧玩法生成结果订阅通知已随创作模板业务退役:小程序不再注册 pages/subscribe-messageapi-server 不再读取生成结果模板或订阅消息配置,platform-wechat 只保留现役登录、普通支付和虚拟支付所需能力。历史部署环境中遗留的同名变量可直接删除。

如果本地 GET /api/editor/showcase/resources 或鉴权后的 GET /api/runtime/settings 报告缺少现役 table / procedure,通常是 .env.local 指向的 SpacetimeDB 库还没有发布当前 spacetime-module,或当前 CLI 身份无权发布该库。切换到可发布的本地库后重新运行 npm run dev 完成发布;不要用恢复 /api/creation-entry/config、旧 gallery view 或模板默认配置兜底来修复。

本地排查 schema 漂移时,先用当前 dev server 显式查询目标库,例如:

spacetime sql <database> "SELECT * FROM runtime_setting LIMIT 1" --server http://127.0.0.1:3101

如果旧 .env.local 仍指向缺少现役 schema 的库,而当前可发布库已经包含这些 table / procedure,可在 gitignored 的 spacetime.local.json 写入 {"database":"genarrative-dev-codex"} 作为本机覆盖;写入时不要带 UTF-8 BOM,否则 scripts/dev.mjs 会忽略该文件。修改后重启 api-server,再检查 /healthz/api/editor/showcase/resources 和鉴权后的 /api/runtime/settings

本地 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.8.3CLI / standalone commit 固定核对为 8e410d2842147bd8e5a32a9589cc00c19f7478e2。若版本或 commit 错配,procedure 返回值可能在宿主侧触发 Failed to BSATN deserialize procedure return valueapi-server 最终表现为现役 settings、editor project 或 profile procedure 超时。排障时先运行 spacetime --version,再对照 server-rs/Cargo.tomlspacetimedb = "...";其它版本可执行 spacetime version install <version> && spacetime version use <version>,升级后重启 npm run dev:spacetime 再重试。当前 scripts/dev.mjs 会把 tool version 和 commit 一起写入 dev-spacetime-tool-version,启动新 standalone 与复用已有本地进程时都要求 2.8.3 + 8e410d28... 同时匹配;旧版本或旧单行版本记录会拒绝复用并要求重启。2.6.1 修复了 procedure context 中调用者 Identity / ConnectionId 始终为空的回归,依赖 ctx.sender 鉴权时必须同时确认宿主已升级。

本地 .env.env.local.env.secrets.local 修改后必须重启 api-server 才会生效;若已经通过 npm run dev 启动完整联调,可在该终端输入 rs api-server。排查图片编辑器 VectorEngine 生成链路时,确认 VECTOR_ENGINE_BASE_URLVECTOR_ENGINE_API_KEYVECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS 只在本地或服务器密钥文件中配置,不能写入 Git。VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS 是单次 attempt 的配置上限,默认 1000000;配置加载层允许显式值低于该默认值,不再在读取环境变量时强制抬高。业务模型和 VectorEngine provider 首选请求都使用 gpt-image-2,符合条件时才回退到兜底模型 gpt-image-2-c;图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 server-rs/crates/platform-imageapi-server 只做编辑器请求编排、OSS / asset 持久化、计费和失败审计落库。platform-image 会在 JSON 生成和 multipart 编辑请求发送前按同一 GPT-image-2 family 规则归一显式像素尺寸;若请求发送失败,先按同一 request_id 查看 provider 日志与 external_api_call_failure.metadata_json.errorSource,当前 multipart /v1/images/edits 单独强制 HTTP/1.1。

编辑器 ElevenLabs 音效生成只从服务端读取 ELEVENLABS_BASE_URLELEVENLABS_API_KEYELEVENLABS_REQUEST_TIMEOUT_MStimeout 默认 180000msbase URL 或 Key 缺失时失败关闭,不回退 Vidu。生产 API 与 external-generation worker 通过共享 API env 取得同一配置,模板见 deploy/env/api-server.env.exampleKey 不得进入 Web/Vite 环境、命令参数、日志、fixture 或仓库。普通测试只使用 loopback mock,禁止把真实付费请求作为 T3 自动验收。

SFX V2 发布必须使用维护窗:先关闭 SFX 入队,再对显式目标执行只读 spacetime sql <database> --server <server-url> --format json "SELECT job_id, status, request_payload_json FROM external_generation_job WHERE job_kind = 'editor_sound_effect_generation' AND (status = 'pending' OR status = 'running')";结果非零时保持旧 Worker drain,不得删除任务或让新 Worker 解析旧 Vidu payload。禁止依赖默认 server,禁止使用 --root-dir。清零后先部署共享 env 已对齐的 api-server / external-generation worker,检查 /healthz 和 Worker 启动,再部署 Web 并小流量开放 SFX。灰度对账 job 完成数、退款数、ElevenLabs POST 数、完成资源数和孤儿资源;翻译失败仍调用 provider、单 job provider POST 大于一次、成功退款或失败未退款均应立即停止放量。回滚先停止入队并收口 V2 pending / running job,不自动切回 Vidu,不执行 SpacetimeDB schema 或数据回滚。完整清单见 docs/【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md

VectorEngine 图片生成 / 编辑在 request_send 阶段出现 timeoutconnect、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。首个 provider attempt 使用 gpt-image-2;明确模型不可用、408 / 非拒绝类 429 / 5xx、响应解析失败或非拒绝类缺图时,下一 attempt 直接切兜底模型 gpt-image-2-c,之后只在剩余次数内重试兜底模型。发送 / 连接错误无法确认上游是否已受理,只重试同一首选模型,不切模型;认证、普通参数、安全拒绝、图片下载和 budget 错误同样不切。worker 从 job 开始的同一时钟起点计算绝对 deadline,常规保留最后 60 秒给审计、OSS 和终态写回;job 预算小于 120 秒时保留一半。VectorEngine 单次 attempt timeout 取配置值和剩余 provider 预算的较小值;退避或模型切换后已没有下一次 attempt 的预算时立即停止。该 deadline 覆盖参考图、provider 请求 / 响应和响应图片下载的整次 provider future,但只在 worker 进程内通过 RequestContext 传递;普通 HTTP / inline 没有该 deadline,继续保持原有 timeout 和重试行为。日志中 VectorEngine 首选图片模型失败,切换兼容模型 会携带 fallback_from_model / fallback_to_model;即使回退成功,首选模型错误仍写入 external_api_call_failure,成功运行摘要的 recoveredFailureCount 同时递增。排查生产失败时应同时统计 fallback / retry 日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。这项收口不修改 lease 续租 / fencing、迟到写回仲裁、attempt 耗尽与原子退款语义。

图片编辑器生成属于持久队列长任务:提交接口返回 job 后,前端通过 /api/runtime/external-generation/jobs/{jobId} 与编辑器项目资源状态收敛。生产排查小程序或 WebView Failed to fetch 时,若 Nginx access log 为 499upstream_status=-,先按提交请求的 request_id、job id、worker 日志和 external_api_call_failure 对齐真实任务,不把客户端断开直接判定为 provider 失败。

查看本地 Rust / SpacetimeDB 日志:

npm run dev:spacetime:logs

后台前端:

npm run dev:admin-web
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

修改 dev-stack 启动、端口探测、状态文件或退出清理时,先执行 quality-gates/README.md 列出的专项门禁;其它改动按本节和仓库入口规范选择现役定向测试,不再维护按历史玩法阶段冻结的平行门禁矩阵。

综合检查:

npm run lint
npm run check

npm run buildscripts/build-gate.mjs 串行构建主站和后台;该门禁会把 Vite warning 当成失败处理。若看到 Build gate failed because warnings were emitted,先看 warning 原文,例如 chunk 体积超过 vite.config.ts / apps/admin-web/vite.config.tschunkSizeWarningLimit,不要先按 Rust 编译失败排查。

Gitea Actions PR 门禁

仓库级 Gitea Actions 工作流固定为 .gitea/workflows/project-ci.yml,在向 master 推送、创建或更新 PR,以及手工触发时运行。工作流拆成四个必须通过的 job:

所有 CI job 和 Jenkins Web Build 在根 workspace 安装前都必须确认 npm --version10.9.7。Gitea job 使用预构建镜像内的固定版本;Jenkins Web Build 在每个独立 bash -lc 中 source scripts/jenkins-prepare-npm-env.sh,首次为 Jenkins 运行用户的版本隔离目录引导同版 npm,后续复用并把该 bin 放到 PATH 首位。旧固定镜像缺少版本元数据时只能报告 npm_version=partial 并由当前 job 的根 npm ci 继续校验 lock,不能把过渡状态当作工具链已闭合。

  • Repository checks:调用唯一入口 npm run check:repository-ci,执行 npm run lint、AI 游戏创作壳 AppSurface 定向测试、主站与后台生产构建和提交差异空白检查。本地 master pre-push 复用同一入口,禁止在 workflow 与 hook 中维护两份近似命令。
  • Frontend tests:按唯一根 workspace lockfile 执行一次干净的 npm ci,再独立执行根 npm run testnpm run bgfilter-worker:smoke-testnpm run check:production-health-patrolnpm run check:production-api-releasenpm run check:production-api-deploy,让 Vitest、Node test smoke harness 及不依赖真实服务的生产巡检 / 发布 / 部署行为 fixture 在 Gitea job 中持续执行;其中 .test.mjs 使用 Node test runner,不依赖 Vitest 的 scripts/**/*.test.ts 收集规则。
  • Backend tests:先对 server-rs/Cargo.lock 执行带 5 次整命令级有界重试的 cargo fetch --locked,再执行 npm run check:server-rs-dddcargo test --locked --workspace --exclude spacetime-module --no-fail-fastcargo test --locked -p spacetime-module --no-fail-fastapi-server --all-targets 编译和 cargo check --locked -p spacetime-module;普通 workspace host 测试排除 spacetime-module 以避免其 spacetime-types feature 统一污染领域 crate,模块自身的纯单元测试通过独立 package test 纳入门禁。spacetime-module 的 reducer / procedure 运行时行为仍必须通过真实 SpacetimeDB runtime/integration harness 验证,不能把 host 链接支持当作运行时替身。依赖准备必须位于会触发 Cargo build 的 DDD / 产物边界门禁之前,避免锁新增依赖未命中镜像缓存时绕过既有下载重试。runner 安装 ffmpeg,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 ignored,不能让普通 PR job访问现场环境。
  • Native shell tests:按唯一根 workspace lockfile 安装全部 App 依赖后执行 npm run check:native-shells,对所有触发方式一致覆盖微信壳、Expo 和 Tauri 的完整验收,并执行 npm run ai-game-creator-shell:check 与 AI 游戏创作壳 release build smoke;最后确认桌面壳与 AI 游戏创作壳的 Cargo.lock 都没有被构建过程改写。共享 Agent Runtime 后台锁 suite 固定 --test-threads=1,不能用并行偶发失败后的逐项通过替代整套稳定门禁。

四个 job 合起来覆盖根 npm run check,并补齐根检查没有包含的 BgFilter worker smoke harness、无密钥生产巡检 / 发布 / 部署行为 fixture、server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 check:* 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。

PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 SPACETIME_SCHEMA_BASE_REFcheck:spacetime-schema 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 master 直接推送时使用 push before SHA;手工触发先尝试 origin/master,若它与 HEAD 相同则改用 HEAD^,仍无法得到不同提交时失败关闭。

启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 1.26.4;不得在 1.26.2 上执行不受信任 PR 代码。runner 保留 ubuntu-latest 标签作为已有固定 digest Ubuntu 24.04 级环境,同时提供专用 genarrative-ci 标签,并将后者映射到已装入 runner 内层 Docker 的完整 Image ID;两者均不得映射到 host 执行器,job 不得获得 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 不再现场运行 apt、actions/setup-node 或 rustup 安装;Node 22、Rust 1.96.0、rustfmt、Chrome、bwraprgffmpegclang/lld 和 Tauri / 后端系统依赖都由预构建镜像提供。checkout 由镜像内 genarrative-gitea-checkout 直接从当前 Gitea 拉取事件 commit,并执行 5 次有界重试;不得恢复为运行时从 GitHub 克隆 action。当前锁命中时,npm 与 Linux 目标 Cargo 下载可完全使用镜像内预热缓存;锁文件新增依赖时才经受控 proxy 补齐。构建 CI 镜像仍需访问固定基础镜像、Ubuntu / Google Chrome 软件源、nodejs.org、npm registry 和 crates.io。本阶段不使用共享 Actions cache,避免不受信任 PR 污染跨 job 可写缓存。当前 Runner 2.0.0 已支持 job 级 timeout-minutes,但 runner 全局 3h 仍是所有任务的硬上限。

当前 genarrative-station 使用 Gitea 1.26.4 和基于 Gitea Runner 2.0.0-dind-rootless 的固定 digest 修补镜像。Runner 2.0.0 会先把 systempaths=unconfined 解析为空 MaskedPaths / ReadonlyPaths,再被 mergo.WithOverride 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 MaskedPaths=[]ReadonlyPaths=[]SecurityOpt=[seccomp=unconfined]Privileged=false、无 CapAdd 且 Binds=[]。外层 runner 以 rootless 用户运行,privileged=false、不增加 CAP_SYS_ADMIN,只映射 /dev/net/tun,内部 Docker 只监听私有 Unix socketrunner 配置保持 docker_host: "-"valid_volumes: []bind_workdir: falseforce_pull: false,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 gitea-actions internal networkgenarrative-station 由只转发 /git 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 seccomp/systempaths 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。

CI job 镜像由 deploy/container/gitea-ci-job.Dockerfile 定义:Ubuntu job base 固定为 sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614Rust stage 固定为 sha256:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9Node 22.23.1 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 150.0.7871.181-1。构建脚本以 NUL 分隔白名单 tar 流发送 Dockerfile、checkout 脚本、根 lock、全部 workspace manifests,以及 server-rs、桌面壳和 AI 游戏创作壳 Cargo manifests/lock。镜像按唯一根 npm workspace 锁、server-rs 锁、桌面壳锁和 AI 游戏创作壳 Cargo 锁预热四份下载缓存,不包含 node_modules 或 Cargo target;三个 cargo fetch --locked 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍分别以断网 cargo fetch --locked 关闭验证。CI 镜像定义或根 lock/workspace manifests 变化后必须重建并发布新的固定 Image ID;不得继续沿用旧镜像 digest。runner 标签保留 ubuntu-latest,并将 genarrative-ci 映射到当前已验证的完整 Image ID。内层 Docker 数据必须持久化;force_pull: false 表示只使用已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。

镜像更新命令:

bash scripts/gitea-ci-job-image.sh build
bash scripts/gitea-ci-job-image.sh verify
bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260807.1.tar.zst
bash scripts/gitea-ci-job-image.sh load-runner

执行账号只要有权访问宿主 Docker API 并管理 runner 容器即可,不强制使用 root;无该权限时由 runner 运维人员执行。更新顺序必须是 build/verify -> export 仓库外镜像归档与 SHA-256 sidecar -> load-runner -> 确认无活跃 job -> 备份当前 config -> 增加或替换 label -> docker restart --timeout 660 gitea-runner--timeout 660 只是停止宽限,不是 drain APIrootless DinD supervisor 可能同时停止内层 dockerd,因此重启前必须确认 Gitea 没有 in_progress run 且内层 docker ps 为空。config 和镜像归档只保存到仓库外受控位置,不在文档、仓库或日志中记录注册信息。重启后先重跑真实 PR 的四个 job,复核隔离边界并确认全部通过,再清理旧镜像。回滚时先把 workflow 的 runs-on 改回 ubuntu-latest,再恢复 config 备份并重启 runner。

四个 job 先运行镜像内 genarrative-gitea-checkout,再以 GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 执行 scripts/check-gitea-ci-job-image.sh,校验 Node 与 npm 固定版本、仓库 Rust toolchain、受信任 PATH、四份缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。运行时发现锁不匹配时必须输出对应 *_cache_lock=partial 和 Actions warning,提示可信分支落地后刷新镜像,不能把陈旧缓存误报为闭合。RUSTUP_AUTO_INSTALL=0,因此仓库 rust-toolchain.toml 变更必须先更新镜像,不能让 job 现场下载。每个 job 仍独立运行一次根 npm ci,以唯一 workspace lock 验证 PR 的全部 App 依赖;统一通过 scripts/ci-npm-ci-with-retry.sh 做最多 3 次整命令级有界重试,同时保留 NPM_CONFIG_PREFER_OFFLINE=true 和 npm 自身 10 次 fetch retry。命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 node_modules,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing,并设置 CARGO_NET_RETRY=10

站点 stack 仍由宿主受控目录管理,.env、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到与宿主相同的绝对路径再执行;挂载到不同路径会让相对 bind source 被 Docker daemon 解析到错误的宿主目录并启动空数据。升级或 runner 迁移前先停止 Gitea 写入,并把 Gitea 冷快照、数据库导出、compose/env 与 runner config/.runner 保存到仓库外受控备份位置。备份文件、绝对宿主配置和注册 token 不得提交 Git,也不在共享文档中记录具体路径或注册内容。

workflow 首次成功运行后,在 Gitea master 分支保护中把 Project CI / Repository checks (pull_request)Project CI / Frontend tests (pull_request)Project CI / Backend tests (pull_request)Project CI / Native shell tests (pull_request) 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 <workflow> / <job> (<event>)。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 genarrative-ci 标签的 runner,再检查精确 Image ID 是否已装入内层 Docker。

master 日常交付必须禁止直接 push,只允许经 PR 在当前 head 的四个 required context 全绿后合并;本地 pre-commit 的 staged ESLint/Prettier 和 master pre-push 的 Repository checks parity 只用于提前发现问题,可被 --no-verify 绕过,不能充当服务端权威门禁。紧急直推白名单如需保留,应按人员和时限最小化,并要求执行同一 npm run check:repository-ci <base> <head> 后回读 push CI。

SpacetimeDB bindings

npm run spacetime:generate

后台账号 procedure 的 identity、唯一索引和版本事务使用隔离 smoke 验证;脚本会在随机本机端口启动仓库锁定版本的临时 SpacetimeDB、发布当前 module,结束后自动关闭并清理临时数据:

npm run check:admin-account-procedures

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.mdCONTEXT.mddocs/project-memory/docs/,不把 .codex/ 工具目录作为项目知识库索引源。

首次拉取或需要重建索引时:

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/

  • PreToolUse hook 会在 Codex 准备执行 git commit 前运行 node .codex/hooks/pre-submit-compile-check.mjs,依次执行 npm run check:rustfmtnpm run typechecknpm run admin-web:typecheckcargo check -p api-server --manifest-path server-rs/Cargo.toml,发现格式或编译错误会阻止本次提交。
  • PostToolUse hook 会在 Codex 工具修改文件后运行 node .codex/hooks/post-edit-codegraph-sync.mjs,执行 npm run codegraph:sync 刷新本地语义索引。
  • 如果某个 Codex 客户端版本尚未自动加载项目级 hook,可先手动运行 node .codex/hooks/pre-submit-compile-check.mjsnode .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 或本机绝对路径提交到仓库。

后端改动验收

后端代码修改后,按变更范围选择:

  • npm run check:rustfmt
  • cargo test -p <crate> --manifest-path server-rs/Cargo.toml
  • cargo test -p platform-image --manifest-path server-rs/Cargo.toml
  • cargo check -p api-server --manifest-path server-rs/Cargo.toml
  • cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml
  • cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml
  • npm run check:server-rs-ddd
  • npm 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

仓库根目录的 rust-toolchain.toml 固定 Rust 1.96.0 并要求 rustfmt 组件, rustfmt.toml 固定 Edition 2024 的格式化口径。Rust 源码分属两个独立 Cargo workspace,必须分别执行以下格式化命令:

  • cargo fmt --all --manifest-path server-rs/Cargo.toml
  • cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml

统一只读入口 npm run check:rustfmt 会按同样顺序对两个 manifest 执行 -- --check。Codex 提交前门禁、API 生产构建和 SpacetimeDB module 生产构建都会执行同一检查,避免 不同开发机或构建节点反复产生格式差异。Web 生产构建还会执行 production-ops、 ESLint、主站与后台类型检查,以及排除已下线旧玩法后的当前 Vitest;API 生产构建 追加 production-ops、DDD/schema/runtime-access 和 api-server 全 target 编译检查; SpacetimeDB module 生产构建追加 production-ops、DDD/schema/runtime-access 和管理员 procedure smoke。上述门禁由 npm run check:production-ops 反查,不能只保留在本地 说明中。对需要跨格式保持稳定的脚本片段,门禁按去除空白后的源码片段匹配,避免仅 因换行或格式化差异误报。

前端改动验收

前端修改后,根据范围选择:

  • npm run check:encoding
  • npm run lint:eslint
  • npm run typecheck
  • npm run test -- <具体测试文件>
  • 移动端视口人工检查或截图检查

UI 相关修改要重点验证:

  1. 390px 左右移动端宽度不横向溢出。
  2. 输入法弹出时平台画布不被压缩。
  3. 弹窗、抽屉和独立面板没有实现成当前面板下方展开。
  4. UI 不包含默认规则说明长文。
  5. 私有图片和音频不裸请求 /generated-*

SpacetimeDB 操作规则

  1. 不在人工命令、本地联调或文档示例中使用 spacetime --root-dir;CI/CD 脚本内部为隔离运行用户登录态的受控用法例外,但不得写成手工排障命令。
  2. 本地开发使用项目脚本维护数据目录;需要清空本地数据时先确认可丢弃,再停止服务并处理本地数据目录。
  3. 发布目标必须显式 --server / --server-url
  4. 身份问题先查 spacetime login showspacetime server list 和目标库权限,不通过切回旧 Node / PostgreSQL 绕过。
  5. 旧库迁移或 private 表数据保留走 migration.rs 的 JSON 导入导出和分片导入思路。
  6. Jenkins 数据库导入 / 导出流水线会先加载 scripts/jenkins-prepare-toolchain-env.sh,显式补齐 Jenkins 用户的 Node、Cargo、SpacetimeDB 工具链目录;如果目标机器安装路径不同,用 GENARRATIVE_JENKINS_TOOL_PATHS 传入额外 bin 目录。
  7. 本地 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。
  8. 独立运行 npm run dev:admin-web 时,admin Vite 仅在 serve 模式把仓库根 public/ 挂到 /admin/ base 下;后台源码引用共用 public 资产时,开发态必须基于 import.meta.env.BASE_URL 生成 /admin/... 地址,生产构建仍使用主站根 /... 地址,不能把整份 public 再复制进 admin build。

SpacetimeDB 数据目录 OSS 备份

脚本停库前会在固定 work-dir 写入 .spacetimedb-stopped marker;正常 finally 恢复 SpacetimeDB 及 --restart-service-after 指定的 API / worker / controller 后才清理 marker。若 Node 因 MemoryMax / OOM 被强制终止,systemd ExecStopPost 会根据仍存在的 marker 兜底恢复这些服务;恢复未全部成功时保留 marker 供后续重试。

数据库备份不放进 spacetime-module reducer / procedure:备份属于文件系统与 OSS 外部副作用,必须由运维脚本在 SpacetimeDB 宿主外执行。当前统一脚本为 scripts/database-backup-to-oss.mjsnpm 命令 npm run database:backup:oss)。默认 --storage-format archive --mode full 保持原有全量压缩包冷备行为;--storage-format files 不生成 tar.gz,而是把目录树映射成逐文件 CAS 对象与 catalog,full 重跑只上传新增或内容变化的文件,history 只处理已被最新 snapshot 完全覆盖的历史 commitlog 与旧 snapshot。Genarrative-Server-ProvisionDATABASE_BACKUP_PROFILE 默认是 archive-full,继续安装每天 03:20 左右执行的全量冷备主 service;当前 release 只允许 archive-full,避免 files-history 在大目录上构造全量 catalog 导致 Node 内存峰值;development 才可以显式选择 files-history,且指定 work-dir 必须已经有与本机 database/bucket 匹配且已发布的 full baseline state

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.servicegenarrative-external-generation-worker@*.servicegenarrative-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;发布脚本退出前会用独立 systemd-run transient service 执行 --upload-deferred-dir <backup-dir>,串行补传该目录内同库的 deferred/pending 归档,不依赖 Jenkins 作业进程树存活。任一归档只有在 OSS archive、manifest 和 baseline state 全部上传并验真后,才按 keep-local 规则删除;失败归档保留原 manifest,由下次 publish 重试。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 灰度 gateenabled=truerolloutPercent=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_STORAGE_FORMAT=archive
GENARRATIVE_DATABASE_BACKUP_FILES_CONCURRENCY=16
GENARRATIVE_DATABASE_BACKUP_UPLOAD_MAX_BYTES_PER_SECOND=
GENARRATIVE_DATABASE_BACKUP_MIN_FREE_BYTES=
GENARRATIVE_DATABASE_BACKUP_BASELINE_STATE=/var/lib/genarrative/database-backups/genarrative-prod-history-state.json
# 仅 archive history 首次从一份 uploadStatus=uploaded 的全量 manifest 初始化 state 时设置或传 --baseline-manifest。
GENARRATIVE_DATABASE_BACKUP_BASELINE_MANIFEST=
GENARRATIVE_DATABASE_BACKUP_OSS_ACCESS_KEY_ID=
GENARRATIVE_DATABASE_BACKUP_OSS_ACCESS_KEY_SECRET=

GENARRATIVE_DATABASE_BACKUP_OSS_BUCKET 为空时会回退 ALIYUN_OSS_BUCKETAccessKey 默认复用 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 显示 enabledinactive/deadNEXT / 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

files-history 使用仓库模板 deploy/systemd/genarrative-database-backup-files-history.conf 覆盖主 service 的 ExecStart,从 /etc/genarrative/api-server.env 读取 data-dir、database、bucket、prefix 与 OSS 凭据,不在 unit 写死环境目标,也不传 --stop-service。Server-Provision 在 development 改动 drop-in 前,先用 current release 的同一脚本、同一 env 和 DATABASE_BACKUP_FILES_HISTORY_WORK_DIR 执行一次 history --dry-run;缺少已发布 full catalog 的 files state、current 脚本过旧或配置不匹配都会在安装 drop-in 和 daemon-reload 前失败。选择 archive-full 会主动删除仓库托管的 10-files-history.conf 与 dev 试点遗留的 10-dev-files.conf,防止 systemd 继续合并旧覆盖。genarrative-database-backup.service 还通过 NODE_OPTIONS=--max-old-space-size=768MemoryHigh=768MMemoryMax=1GOOMPolicy=stop 给备份进程设置独立护栏;release 若现场残留 files-history drop-in,必须先按 archive-full 重新 provision 并确认 drop-in 已删除,再恢复定时器。dev 可继续指定已有 /var/lib/genarrative/database-backups/dev-files;两台机器不得复用或互传本地 state 目录冒充本机基线。启用时通过 Server-Provision Job 选择目标、DATABASE_BACKUP_PROFILE=files-history 和对应 work-dir,先保持 DRY_RUN=true 核对,再以同参数正式 provision。不要直接在 /etc/systemd/system 手写第二份 drop-in。

files full 会递归扫描 data-dir,保留空目录、每个普通文件的相对路径,以及目标仍位于 data-dir 内部的相对符号链接;绝对链接或解析后越界的链接直接拒绝。文件按 SHA-256 上传到不可变对象 key,catalog 记录目录、路径、长度、SHA、对象 key 和相对链接目标,不写 staging 主机的绝对路径。相同 catalog 重跑不重复 PUT;新增或变化文件先 HEAD CAS 对象,存在且长度/SHA 元数据一致就复用,否则上传。16 MiB 及以下对象使用单次 PUT 后 HEAD 验真,大对象继续使用 multipart;对象操作默认以 16 路并行执行,可用 GENARRATIVE_DATABASE_BACKUP_FILES_CONCURRENCY=1..64 调整。需要给线上入口留带宽时设置 GENARRATIVE_DATABASE_BACKUP_UPLOAD_MAX_BYTES_PER_SECOND=<bytes/s>,该共享限速器只包裹备份上传流,空值或 0 表示不限速,不修改主机全局 qdisc。并发、限速和单次 PUT 都不改变“全部对象、catalog 与 latest pointer 成功后才推进 state/清理”的顺序。full 基线必须来自停库后的 data-dir 或已通过恢复验证的冻结副本;源文件上传前后 stat 虽会复核,但在线扫描不能保证大量文件属于同一跨文件一致时点。catalog 验真后,脚本把最新 full/history 引用发布到固定 <prefix>/<database>/latest.json,全新机器不需要本地 state 即可自动发现恢复入口。

history 的安全边界按每个 replica 独立计算。设最新完整且未锁定的 snapshot offset 为 S;数字更大但缺少同 offset .snapshot_bsatn、仍存在同名 .lock 的目录不能参与边界计算。脚本必须保留起始 offset 小于等于 S 的最后一个 commitlog segment,以及它之后的全部 segment;只处理更早的 .stdb.log / .stdb.ofs,snapshot 只处理最新目录之前的旧目录。files history 会递归展开候选目录,逐对象复用或上传,随后依次验真候选对象、history catalog 与 full baseline catalog,再重新扫描边界和 stat fingerprint,最后覆盖发布并验真 latest.json;任何一步失败都不推进 state 或删除源文件。脚本在 work-dir 使用 PID lock 拒绝同库并发上传,SSH 超时后必须先检查原进程,不能直接重跑。

files 本地续跑 state 使用 <database>-files-state.json.gz v2:只保存 full/history catalog 的 object key、长度、SHA 与验真时间,不再重复嵌入每份 catalog 的完整 files / symlinks 清单。旧 v1 .json 仍可读取,并且只在一次非 dry-run 备份的 OSS catalog、latest.json 与本地新 state 全部成功后原子迁移为 gzip v2,再删除旧 state;gzip 已存在但损坏时必须失败,不能回退到可能过期的旧 JSON。成功运行后,本地只压缩保留 latest full catalog 作为 full 增量复用缓存,已上传并验真的 history catalog、旧 full catalog、失败或 dry-run 遗留 catalog 自动清理;OSS catalog、CAS 对象与 latest.json 不删除、不改 schema。--result-file 只写 catalog 引用和计数,不再复制完整文件清单。metadata 压缩或清理失败时不得继续删除 /stdb history 源文件。

# 从停库目录或已验证冻结副本建立逐文件完整基线;相同 work-dir 重跑只传变化内容。
node -- scripts/database-backup-to-oss.mjs \
  --storage-format files \
  --mode full \
  --data-dir /path/to/frozen/stdb \
  --work-dir /var/lib/genarrative/database-backups/dev-files \
  --database genarrative-prod \
  --env-file /etc/genarrative/api-server.env

# 把完整 work-dir/state 放回 dev 后,先只读查看可清理历史候选。
node -- scripts/database-backup-to-oss.mjs \
  --storage-format files \
  --mode history \
  --data-dir /stdb \
  --work-dir /var/lib/genarrative/database-backups/dev-files \
  --database genarrative-prod \
  --env-file /etc/genarrative/api-server.env \
  --dry-run \
  --result-file /var/lib/genarrative/database-backups/history-dry-run.json

# 核对 dry-run 后执行真实归档与清理;history 在线处理不可变历史文件,不传 --stop-service。
node -- scripts/database-backup-to-oss.mjs \
  --storage-format files \
  --mode history \
  --data-dir /stdb \
  --work-dir /var/lib/genarrative/database-backups/dev-files \
  --database genarrative-prod \
  --env-file /etc/genarrative/api-server.env

dev 出口过慢时,可以把冻结基线经内网 rsync 到 release 独立 staging,再由 release 上传 dev bucket。staging 必须位于 /var/lib/genarrative/dev-database-backup-staging/ 一类隔离目录,命令显式传 staging --data-dir、独立 --work-dir、dev --bucket,且不得传 --stop-service;禁止指向或修改 release /stdb。中转 key 只为本次传输临时授权,结束后从 dev 私钥和 release authorized_keys 同时移除。上传完成后把整个 files work-dir/state 回传 devhistory 才能延续同一 baseline catalog。

完整恢复默认从 OSS 固定 latest.json 读取最新 full catalog:先创建 directories,再把每个 files[].objectKey 下载到 <restore-root>/<files[].path> 并逐项核对 sizeBytes / sha256history catalog 用于证明已清理历史仍有 OSS 对象,不需要把已被 full baseline 覆盖的旧文件叠回当前恢复目录。本地 state 仍可作为兼容入口,并同时支持旧 v1 JSON 与 v2 gzip,但不再是异机恢复的前置条件。随后用隔离 data-dir 启动同版本 standalone,验证 /v1/ping、日志中的 snapshot restore / commitlog replay / module launch、代表性 SQL 和 reducer。dev 已完成这轮 OSS-only 异机恢复与重启演练;release 使用 archive-full 时,现场最终 ExecStart、timer 状态与最近备份结果仍须在变更时重新核对。

node -- scripts/database-backup-to-oss.mjs \
  --env-file /etc/genarrative/api-server.env \
  --database genarrative-prod \
  --restore-files-latest \
  --restore-dir /var/lib/genarrative/database-backup-restore/stdb \
  --result-file /var/lib/genarrative/database-backup-restore/restore-result.json

冷备份后必须做一次只读验收,不要只看 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/ >/dev/null
curl -fsS --max-time 5 http://127.0.0.1/api/editor/showcase/resources >/dev/null

角色动画帧 OSS 排障

角色动画源帧 PUT、透明帧 PUT 和最终帧 HEAD 使用 AppState 内同一个 OSS HTTP Client/连接池,并受进程级 8 路 OSS permit 保护;BgFilter、阿里云抠图和本地处理不占用该 permit。每个 OSS attempt 最多 3 次(首次 + 2 次重试),退避为 250ms、500ms;只重试 timeout、无 HTTP 响应传输错误、OSS PutObject 的 400 + RequestTimeout、PUT 400 错误体读取失败(未解析出 Code,按 timeout/transport 归类)、408、429 和 500599。动作帧 PUT 收到 400 时只读取最多 16 KiB OSS 错误 XML,提取 CodeRequestIdoss_request_id 优先使用响应头 x-oss-request-id,XML 字段只作回退。错误体读取超时/断流不再按确定性 400 处理:已解析出的 Code 优先生效;未解析出 Code 时按读取失败原因置 timeout/transport 并重试,message 追加「错误响应体读取失败」。日志字段包括 frame_indexobject_keyoperation=source_put|final_put|final_headattemptmax_attemptsretryablewill_retryretry_delay_mspermit_wait_mstimeoutconnecttransportoss_codeoss_request_idstatuselapsed_ms请求 OSS 失败 时,timeout/connect/transport=true 表示传输类失败;status=400, oss_code=RequestTimeout, timeout=truestatus=429500599 表示暂时性失败,PUT 的 status=400oss_code 为空且 timeout=truetransport=true(message 含「错误响应体读取失败」)同样是暂时性失败。除 RequestTimeout 和该错误体读取失败两类例外外,其他 400、401/403/404、配置、URL 和签名错误是确定性失败,不会重试。最终帧 HEAD 失败只会重试 HEAD,不会重复 PUT;如果任一帧最终失败,确认整段动作已排空已启动 Future,并检查任务按现有契约退款且没有发布缺帧动画。

生产运维

生产部署当前口径:

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-bgfilter-worker.servicegenarrative-external-generation-controller.servicespacetimedb.servicenginx.service 是否 activePingora 直连切换后改为 pingora-direct,仍要求 BgFilter worker active,只把网关检查从 nginx.service 切到 genarrative-pingora-gateway.service
  • 至少一个 genarrative-external-generation-worker@*.service 实例是否 active;如果 controller 存活但 worker 全部退出,巡检直接返回 CRITICAL,避免外部生成队列长期无人消费。
  • API 直连 /healthz/readyz
  • BgFilter worker 直连 http://127.0.0.1:8083/readyz;可通过 GENARRATIVE_HEALTH_PATROL_BGFILTER_BASE_URL 覆盖探测地址。
  • SpacetimeDB 直连 /v1/ping
  • 默认通过本机公网网关入口检查 /;需要增加现役公开 API 时,用可重复的 --public-path 或对应环境配置显式追加 /api/editor/showcase/resources。如需走正式域名,在 /etc/genarrative/health-patrol.env 配置 GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://<域名>。若在目标机本机打 https://127.0.0.1http://127.0.0.1,同时配置 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=<域名>,确保 public probe 命中正确 vhost / Host 语义;不得把已退役模板 API 重新加入健康探针。
  • Pingora 影子网关只在同时配置 GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URLGENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN 时纳入巡检;脚本会访问 GET /__genarrative_pingora/healthz 并校验返回 gateway=pingora-shadow,未配置时不影响现有生产巡检。
  • 最近 15 分钟对应 gateway mode 下 genarrative-api.servicegenarrative-bgfilter-worker.servicegenarrative-external-generation-controller.servicegenarrative-external-generation-worker@*.servicespacetimedb.servicenginx.servicegenarrative-pingora-gateway.serviceerr..alert 日志。

巡检脚本的显式 --timeout-ms--slow-msGENARRATIVE_HEALTH_PATROL_TIMEOUT_MSGENARRATIVE_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-ForTRUST_X_FORWARDED_FOR 只用于接流保护 client key。gzip 默认由 GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED=true 开启,GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL=5GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=1024 对齐当前 Nginx gzip_comp_level 5 / gzip_min_length 1024GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip 是当前唯一允许的压缩算法白名单。Pingora 静态缓存头默认由 GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL=no-cacheGENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL=public, max-age=31536000, immutableGENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL=no-cache 分别控制入口 HTML、指纹资源和其它静态资源。Pingora 正式化口径固定为 gzip-onlyBrotli 仍由 Nginx / 前置代理能力探测承担,直连 Pingora 不以 Brotli parity 作为切换门禁。Pingora 上游超时显式配置在 peer 上:连接默认 3000ms,无显式长超时代理路由默认读 60s,通用 /api、公开列表 / 详情和 SpacetimeDB subscribe 默认读 3600s,写上游默认 3600s;读 / 写 / 连接超时统一返回 JSON 504 GATEWAY_UPSTREAM_TIMEOUT。当前 Pingora 接流保护默认只覆盖单进程单实例;GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=trueGENARRATIVE_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.confsystemctl 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.serviceUser= 验证真实服务用户可读证书链和私钥,并从 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 == checkedmissingCount=0mismatchCount=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 catsudo -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=truepreflight 会要求同时设置 GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true;当 TLS / HTTP redirect 监听公网地址时还会直接失败,因为公网直连 Pingora 不能信任客户端可伪造的 X-Forwarded-For。若 env 中 GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=trueGENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1preflight 会要求同时设置 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 / applyJSON 输出会隐藏 token 原文。

正式 runbook 的状态快照证据包必须显式传 --expected-pingora-env-modepost-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_LISTENGENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTENGENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILEGENARRATIVE_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.1localhost::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=nginxGENARRATIVE_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-enablenpm 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-gatewaypingora-gateway 可执行且 systemd ExecStart 指向 current release,再使用 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-bundle.mjspre-cutoverpost-enablepost-rollback 三个阶段生成时间戳证据目录,保存 manifest.jsonsnapshot.jsonsnapshot.stdout.txtsnapshot.stderr.txtsnapshot-command.json;证据包 manifest.files 会给已生成的 snapshot / stdout / stderr / command / parse-error / direct-live 文件统一记录 pathsizeBytessha256,便于窗口后复核归档文件未漂移。每个阶段证据目录生成、复制或归档后,都要用 /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.txtcommand.stderr.txtcommand-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.statusOK、命令 manifest.summary.statusOK、命令 manifest.summary.exitCode0,以及标准八段证据的 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.maxSpanMstimeline.spanMs。runbook 的 direct enable apply / rollback apply 还会通过 /opt/genarrative/current/scripts/ops/pingora-cutover-command-evidence.mjs 包装真实脚本执行,额外保存 command.stdout.txtcommand.stderr.txtcommand-record.jsonmanifest.json,失败时同样保留证据并返回真实退出码,命令证据 manifest.files 也会记录这三份命令证据文件的 pathsizeBytessha256 供归档后复核。若状态快照 stdout 无法解析为 JSON,证据包会改写 snapshot-parse-error.txt 并在 manifest.files.snapshotParseError 与最终 stdout 中给出路径;启用后 direct live stdout 无法解析时,同理写入 direct-live-parse-error.txt 并在 manifest.files.directLiveParseError 与最终 stdout 中给出路径,避免解析失败原因只散落在终端输出里。底层状态快照收录 summaryhealthPatrolEnvpingoraEnvreleaseArtifactssystemdchecks;其中 systemd.pingoraUnit.environmentFileMatchesPingoraEnvFile 必须为 true,证明 systemctl cat genarrative-pingora-gateway.serviceEnvironmentFile= 精确包含本次 --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 的 listentlsListenhttpRedirectListentlsCertFiletlsKeyFilemodeshadowReady 提升到 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:18081mode=shadowshadowReady=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、命令 startedAtfinishedAt 必须使用 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 会区分 nonOkItemsmissingGeneratedAtcutoverRunIdMismatchoutOfOrderspanExceeded。切换窗口排障时不要只看顶层 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.shpost-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjsrollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjsrollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjsrollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh,并要求 --applypingora-directnginx 等关键参数,复核命令证据里的 manifest.expectedExecutablemanifest.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.commandNamemanifest.command.name 一致,并要求 manifest.commandcommand-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 >= startedAtdurationMs == finishedAt - startedAt,且 manifest.generatedAt 不能早于命令 finishedAt。这些参数会隐式要求对应命令证据存在;旧证据缺少 schemaVersionexpectedExecutable、缺少必需 --apply 参数、人工同名证据的 executable 漂移、manifest 与 command-record 语义漂移、命令 stdout / stderr 引用漂移、命令参数漂移、命令参数控制字符污染、命令时间线漂移或同一命令绑定多个不同脚本路径都会失败。

命令证据的 manifest.expectedExecutablemanifest.command.expectedExecutable 只要出现,就必须是安全绝对路径;空字符串、相对路径、文件系统根目录或包含换行 / NUL 的值都会让总审计失败,避免坏字段被当作缺省值跳过。生成命令证据时,pingora-cutover-command-evidence.mjs --expected-executable 也会在执行真实命令前拒绝相对路径、文件系统根目录和带换行 / NUL 的路径。

只要命令证据声明了 expectedExecutablemanifest.command.executable 和独立 command-record.json.executable 就必须同时是同一个绝对路径;即使未显式传 --require-command-executable,真实 executableexpectedExecutable 漂移也必须失败。

正式命令证据不能只依赖 command 字符串复盘真实命令;manifest.command.executablecommand-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/false1/0yes/noon/off 或空值,拼写错误会直接失败,避免 REQUIRE_GATEWAYRUN_HEALTH_PATROLREQUIRE_PINGORA_GATEWAYFAIL_ON_CRITICALTRUST_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-cutoverpingora-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-parityRust 侧 cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix 会读取同一份矩阵验证 classify_path 的路由结果、body limit 和接流保护分组。

canary 与 access log 对账使用的代表性现役 API 统一为 /api/editor/showcase/resources。后续旧段落若仍记录 /api/creation-entry/config,只表示当时的历史验收,不得复制到当前命令或巡检配置。

Pingora canary 入口默认由 Server-Provision 安装但不接入主站配置。deploy/nginx/snippets/genarrative-pingora-canary.conf 是前缀 canary,人工 include 后用 /__genarrative_pingora_canary/ rewrite 到 Pingora shadowdeploy/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.mjsrequest_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/false1/0yes/noon/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-snapshotnpm run check:pingora-cutover-evidence-bundlenpm run check:pingora-cutover-command-evidencenpm run check:pingora-cutover-evidence-verifynpm run check:pingora-cutover-evidence-audit。正式直连 runbook 会额外列出 切换前状态快照证据包启用后状态快照证据包回退后状态快照证据包,每个证据包阶段后都会列出对应的 证据 manifest 只读验真 步骤;Pingora direct enable applyPingora 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-applyrollback-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 logNginx 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 当前只允许 httpsGENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL 必须在 0..=9GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES 必须大于 0GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS 当前只允许 gzip,所有 GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_*TIMEOUT* 必须大于 0GENARRATIVE_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;脚本只会在 WARNINGCRITICAL 时向该 webhook 发送 JSON。未配置 webhook 时,告警来源是 systemd 失败状态、journal 和状态文件。

Jenkins Copy Artifact 必须保持 Production 权限模式;产物生产者要在 Jenkinsfile 中用 copyArtifactPermission 精确授权消费者,不能依赖 Migration 模式或全局 Job/Read。固定映射为 Genarrative-Stdb-Module-BuildGenarrative-Stdb-Module-PublishGenarrative-Api-BuildGenarrative-Api-DeployGenarrative-Web-BuildGenarrative-Web-DeployGenarrative-Database-ExportGenarrative-Database-Import。如果 copyArtifactsUnable to find project for artifact copy,但来源 Job、指定构建号和归档都实际存在,先检查来源 Job 的 CopyArtifactPermissionProperty;修复 Jenkinsfile 后必须先运行一次产物生产者,让 Declarative Pipeline 把 Job property 写回 Jenkins,再重跑 Deploy / Publish / Import。npm run check:production-ops 会防止四条白名单再次丢失。

Genarrative-Web-Build 的主站构建失败若出现 Rollup 报错 "xxx" is not exported by "src/services/publicWorkCode.ts",优先按前端公开作品号工具缺失处理,而不是排查 Jenkins 节点环境。修复时要让 publicWorkCode.tsbuild<Play>PublicWorkCodeisSame<Play>PublicWorkCode 成对导出,并补 src/services/publicWorkCode.test.ts 覆盖对应玩法前缀;随后用 npm run build:production-release -- --component web --name <临时名> 复现 Jenkins web 构建路径。

Genarrative-Web-Build 先通过 scripts/jenkins-prepare-npm-env.sh 在 Jenkins 运行用户的持久版本目录准备 npm 10.9.7,显式提升该 bin 后校验真实版本,不依赖系统 /usr/bin/npmpackageManager 声明自动切版。在运行根 Vitest 前必须按唯一根 package-lock.json 执行一次干净的 npm ci。根 lock 聚合全部 workspace,因此会安装 AI 游戏创作壳合法声明的 @tauri-apps/api@tauri-apps/plugin-http 等 Tauri guest;这些依赖仍只归属 AGC workspace,不得加入根 H5 或 Desktop manifest。排查收集失败时先运行 npm run check:npm-workspaces 并核对根 lock 的 workspace entry,禁止恢复第二份 lock 或子目录安装。

Genarrative-Web-Build 会把 build/<version>/web.tar.gzweb.tar.gz.sha256release-manifest.jsonscripts/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-serverapi-server.sha256release-manifest.jsonbuild/<version>/scripts/deploy/production-api-deploy.shbuild/<version>/scripts/deploy/maintenance-on.shbuild/<version>/scripts/deploy/maintenance-off.shscripts/database-backup-to-oss.mjsscripts/ops/production-health-patrol.mjsscripts/ops/pingora-current-release-audit.mjsscripts/ops/pingora-cutover-status-snapshot.mjsscripts/ops/pingora-cutover-evidence-bundle.mjsscripts/ops/pingora-cutover-command-evidence.mjsscripts/ops/pingora-cutover-evidence-verify.mjsscripts/ops/pingora-cutover-evidence-audit.mjsscripts/check-pingora-direct-preflight.mjsscripts/check-pingora-direct-live.mjsscripts/check-pingora-canary-access-log-parity.mjsscripts/check-production-health-patrol-env.mjsscripts/deploy/pingora-direct-enable.shscripts/deploy/pingora-direct-rollback.shdeploy/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/systemddeploy/envdeploy/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-BuildarchiveArtifactsGenarrative-Api-DeploycopyArtifacts 过滤器是否仍包含 build/<version>/release-manifest.jsonbuild/<version>/scripts/deploy/production-api-deploy.shbuild/<version>/scripts/deploy/maintenance-on.shbuild/<version>/scripts/deploy/maintenance-off.shbuild/<version>/scripts/database-backup-to-oss.mjsbuild/<version>/scripts/ops/production-health-patrol.mjsbuild/<version>/scripts/ops/pingora-current-release-audit.mjsbuild/<version>/scripts/ops/pingora-cutover-status-snapshot.mjsbuild/<version>/scripts/ops/pingora-cutover-evidence-bundle.mjsbuild/<version>/scripts/ops/pingora-cutover-command-evidence.mjsbuild/<version>/scripts/ops/pingora-cutover-evidence-verify.mjsbuild/<version>/scripts/ops/pingora-cutover-evidence-audit.mjsbuild/<version>/scripts/check-pingora-direct-preflight.mjsbuild/<version>/scripts/check-pingora-direct-live.mjsbuild/<version>/scripts/check-pingora-canary-access-log-parity.mjsbuild/<version>/scripts/check-production-health-patrol-env.mjsbuild/<version>/scripts/deploy/pingora-direct-enable.shbuild/<version>/scripts/deploy/pingora-direct-rollback.shbuild/<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-gatewaypingora-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-BuildGenarrative-Api-DeployGenarrative-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 和 checksumJenkins 会先检查 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-releasecheck:production-api-deploy 和生产运维护栏都必须失败,避免切换窗口只能靠 Jenkins 工作区或源码 checkout 临时补自审、证据或日志对账脚本。

同一 API release 随包依赖还必须包含 scripts/check-pingora-release-readiness.mjsscripts/check-pingora-canary-live.mjs。前者在 current release 上以 --release-runtime-only 汇总运行时复核,后者支撑目标 Nginx canary live smoke;缺少任一脚本时不能进入直连切换窗口。

Genarrative-Stdb-Module-Build 的 Jenkins 归档产物必须包含 build/<version>/spacetime_module.wasmspacetime_module.wasm.sha256release-manifest.jsonscripts/deploy/production-stdb-publish.shscripts/deploy/production-runtime-writer-identity-rotate.mjsscripts/deploy/maintenance-on.shscripts/deploy/maintenance-off.shscripts/spacetime-migration-common.mjsscripts/spacetime-maintain-external-generation-jobs.mjsscripts/spacetime-clean-editor-image-asset-kind.mjsscripts/spacetime-normalize-editor-character-actions.mjsscripts/spacetime-migrate-editor-canvas-layout.mjsscripts/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.jsonmigration_bootstrap_secret_sha256 记录该非敏感摘要。Genarrative-Stdb-Module-Publish 只通过 copyArtifacts 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret Filepublish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 migration_bootstrap_secret_sha256 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secretID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。

三个 SCM Jenkinsfile 将 MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 默认固定为 genarrative-spacetime-bootstrap-secret-dev-file。Secret File 的原文只存在于 Jenkins Credentialscredential 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/*.envapi-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_FILEStdb 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.shnode --check scripts/dev.mjs scripts/check-production-ops-guardrails.mjsnpm run check:production-opsnpm run check:encodinggit 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.envGENARRATIVE_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-routesnpm run check:pingora-route-paritycargo test -p pingora-gateway --manifest-path server-rs/Cargo.tomlnpm run check:pingora-gateway-smokenpm 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;公告页和 marker 都使用同目录临时文件加 POSIX 兼容 mv -f 的原子替换,不得依赖 GNU mv -T,确保 Linux 生产机与 macOS/BSD 本地门禁语义一致。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 maintenance-on.sh 调用保留已安装公告,maintenance-off.sh 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 genarrative.worldwww.genarrative.world 的真实 HTTPS 响应校验 503 和公告正文。

生产 Jenkins 的 Pipeline script from SCM 由 Jenkins controller 读取 Jenkinsfile。所有生产 Job 的 SCM URL,以及 Jenkinsfile 内部在 Jenkins Built-In Node 执行的源码准备,统一使用 ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git,并显式传入 Jenkins SSH 凭据 genarrative-local-gitea-ssh;不再配置局域网 IP、https://git.genarrative.world/... 公网 fallback 或 https://git.genarrative.world/git/GenarrativeAI/Genarrative.git。所有 GitSCM checkout 都必须保留单分支 refspec、shallow=truedepth=1noTags=truehonorRefspec=true。API / Web / Stdb 发布类流水线不在目标机器 checkout Git,统一执行上游构建归档里的部署脚本;Server-Provision 和数据库导入导出也由带 linux && genarrative-build 标签的 Jenkins Built-In Node 先 checkout 并 stash 所需脚本,再交给目标 dev / release agent,避免目标机把 127.0.0.1 误解为远端 Gitea 或让产物 commit 与执行脚本漂移。

当前 Jenkins / 本机 Git 入口固定为 ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git。验证时在具备 Jenkins SSH key 和 [127.0.0.1]:2222 known_hosts 的环境执行 git ls-remote ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git HEAD,应能返回 HEAD;同时扫描 live Job config.xml 和仓库 Jenkinsfile,确认没有残留局域网 IP 或公网 Git 地址。旧的局域网、公网和 HTTP 内网入口只作为历史兼容与排障参考,新流水线不再默认使用。

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-DeployGenarrative-Web-DeployGenarrative-Stdb-Module-Publish 仍保留上游构建传入的 COMMIT_HASH 作为通知和追溯字段,但不再用它在目标机器重新 checkout 部署脚本。

Genarrative-Api-BuildGenarrative-Stdb-Module-Build 在浅克隆复用后必须取得待构建提交的第一父提交:本地对象缺失时只额外执行一次凭据保护的 git fetch --depth=2 <source_commit>,并把父提交 SHA 写入 SPACETIME_SCHEMA_BASE_REF 后再运行 check:server-rs-ddd。父提交不可取得、不可解析或与 HEAD 相同时必须失败关闭,禁止让 SpacetimeDB schema 门禁回退成 HEAD 自比。

Genarrative-Stdb-Module-PublishPipeline 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 BashStdb module 的 CARGO_HOMECARGO_TARGET_DIRSCCACHE_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.shscripts/prepare-server-provision-tools.shscripts/deploy/**deploy/**.jenkins-source-commit 打包上传给目标 agent;目标 agent 不再接收 SOURCE_GIT_REMOTE_URL,也不再 checkout Git。真正会读取目标机现状和写系统配置的阶段仍只在目标服务器 agent 执行:DEPLOY_TARGET=development 使用 linux && genarrative-dev-deployDEPLOY_TARGET=release 使用 linux && genarrative-release-deployPrepare Provision ToolsProvision Server 在同一个目标 agent 工作区内顺序执行,前者准备 SpacetimeDB 与 otelcol-contrib 交付件,后者写入 /etc / systemd / Nginx、创建系统用户并修改服务。目标 dev / release agent 非 dry-run 时必须具备 root 权限。

生产环境变量模板:deploy/env/api-server.env.example。真实密钥只放服务器,不提交 Git,不写入文档示例。

BgFilter 受限资源调度使用非模板单实例 genarrative-bgfilter-worker.service,固定以 GENARRATIVE_PROCESS_ROLE=bgfilter-worker 监听 127.0.0.1:8083,不挂 Nginx 或公共路由。api-server.env 是父侧与子 worker 的共享基础,集中保存 provider、OSS、冻结的 GENARRATIVE_BGFILTER_WORKER_CONCURRENCY=16GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS=5000、内部 worker 地址、Token 文件和连接超时;BgFilter unit 先加载它,再加载只含 HOST / PORT / MAX_REQUESTS、flat / complex 统一熔断阈值 / cooldown 和可选日志覆盖的 /etc/genarrative/bgfilter-worker.env,其中两种模式共享参数但状态独立,阈值默认 3、cooldown 默认 120sMAX_REQUESTS 默认 2048 且只作 admission 保险丝。N / est 是父侧派生 callBudgetMs、子侧派生 attempt 与队列估时的共同输入,专属 env 不得重复覆盖;同一 provider、OSS、Token 或预算基础配置也只保留一份。外部生成 worker unit 会在共享 API env 后加载 /etc/genarrative/external-generation-worker.env;该文件如重复定义内部 base URL、Token / Token 文件、connect timeout、N / est、OSS bucket 或 endpoint,最终有效值必须与共享 API env 完全一致,否则发布失败,避免父子预算或对象存储视图漂移。外部生成 worker 可以使用同一 bucket 下权限等价或更小的独立 AK,不要求凭据文本一致。Token 文件由 Provision 以 root:genarrative 0440 创建或保留,env 只引用路径,不保存内部 Token 明文;env 示例和仓库不得出现真实 provider / OSS secret。

生产发布必须按 共享配置 / endpoint / Token / N / est / Q 预检 → stop 旧 BgFilter worker → 等待 systemd 排空 → start 唯一实例 → 检查 worker readyz → 重启 API → 重启 external-generation worker / controller 的顺序执行。readyz URL 由部署脚本从已校验 env 的 HOST/PORT 派生(默认 http://127.0.0.1:8083/readyz):--bgfilter-worker-health-url 缺省即派生,Jenkins 流水线不传该参数;显式传入时必须与父进程 base URL 和子 worker listener 三方一致,否则预检失败。Token 预检要求 API env 指向非空、非符号链接的普通文件,权限固定为 root:genarrative 0440endpoint 预检要求父进程 GENARRATIVE_BGFILTER_WORKER_BASE_URL、子 worker HOST / PORT 和 readiness URL 指向同一个 127.0.0.1:<port>;预算预检要求共享 N / est 均为正整数、父子有效值一致,当前模板默认和压测前冻结值为 N=16 / est=5000ms,后续允许按真实压测校准 est,不由 deploy 脚本写死;Q 缺省为 2048,显式值不得小于 N,历史模板默认 128 会在 deploy / Provision 时定向迁移为 2048;熔断 cooldown 的历史模板默认 300s 同样会定向迁移为 120s;其它显式定制值均保留。任何预检失败都发生在 current 切换和停止现役 worker 之前。非模板 unit、固定 loopback 端口和显式 stop/start 共同避免新旧 BgFilter worker 重叠;第二实例会因固定端口绑定失败。内部请求分别携带 maxQueueWaitMs 与公式化 callBudgetMs;worker 停机时立即让尚未取得 provider permit 的排队请求失败,只排空已经取得 permit 的调用。默认 callBudgetMs=321sunit 使用 TimeoutStopSec=900 给最多两次 attempt、响应发送和进程收口留足余量,禁止沿用约 90s 的默认停止窗口。默认 API deploy 会安装、enable、启动并验活该 unit;只有明确回滚或应急排障时才使用 --no-bgfilter-worker 跳过,且不得让父进程偷偷恢复为直连 BgFilter。

api-server 进程角色由 GENARRATIVE_PROCESS_ROLE 控制:api 只监听 HTTPexternal-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=2POLL_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-%iGenarrative-Server-Provision 会安装 worker 模板、controller unit 和两份专属 env 模板,默认 enable 首个 genarrative-external-generation-worker@1.servicegenarrative-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.servicecontroller 下轮会按队列压力修正到目标实例数。worker 专属参数模板是 deploy/env/external-generation-worker.env.example,密钥与 SpacetimeDB 连接默认复用 /etc/genarrative/api-server.env。API 发布脚本默认会重启并验活 genarrative-external-generation-worker@*.servicegenarrative-external-generation-controller.service;安装随包 BgFilter、external-generation worker 和 controller unit 前,必须按本次 --current-link--api-env-file--worker-env-file--controller-env-file--bgfilter-worker-env-file 渲染路径,不能用原始模板覆盖 Server-Provision 已安装的自定义路径。Genarrative-Api-DeployGenarrative-Full-Build-And-Deploy 必须同时暴露并透传这三类角色 env 参数,避免独立发布脚本修复后又被流水线默认值截断。若本次只发 HTTP 且不希望滚动 worker,可传 --no-worker-services,若不希望重启 controller 可传 --no-worker-controllerGENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS 控制空队列轮询间隔,GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS 控制单次 leaseworker 会约每三分之一 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 时尝试同时启动 controllercontroller 异常不应反向拖停 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.serviceLD_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-essentialca-certificatescurlperltar 等 OpenSSL 运行时自举工具;这只服务于独立 OpenSSL 运行时安装,不代表 provision 重新承担 api-server 构建职责。Ubuntu / apt 目标机会额外安装 libnginx-mod-http-brotli-filterlibnginx-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=1024GENARRATIVE_API_WORKER_THREADS=4;本地未设置 worker threads 时继续使用 Tokio 默认值。
  • GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512 开启通用 HTTP 并发背压,GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16 为后台 API 提供独立热路径保护。超过许可时直接返回 429 Too Many RequestsRetry-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_acquireconnect_buildconnect_handshakeread_model_subscribeprocedure_resultreducer_resultread_cacheelapsedMs / timeoutMs 用于确认是否命中健康检查窗口。业务请求日志也会写入 operation_kindoperation_namespacetime_stageelapsed_ms,后续 45 秒超时不再只靠 Nginx request_time=45s 推断。
  • genarrative-api.service 设置 LimitNOFILE=65535TasksMax=2048;上线后用 systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax -p TimeoutStopUSeccat /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/currentSpacetimeDB 必须同时匹配运行版本 2.8.3 和 commit 8e410d28... 才能复用;只有缺失或版本 / commit 不匹配时才使用 PROVISION_DOWNLOADS_DIR 里的本地包或从配置的下载源准备官方 v2.8.3 资产。SPACETIME_EXPECTED_COMMIT 与下载根必须成对调整,安装结果也执行同一 commit 门禁。otelcol-contrib 当前锁定 0.151.0;如果目标服务器下载需要代理,在 PROVISION_DOWNLOAD_PROXY 配置目标机可访问的 HTTP 代理。
  • Genarrative-Server-Provision 外,Genarrative-Stdb-Module-BuildGenarrative-Web-BuildGenarrative-Api-BuildGenarrative-*DeployGenarrative-Database-Import/ExportGenarrative-Full-Build-And-DeployGenarrative-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/USERFailed to determine user credentials,优先检查 getent passwd otelcol,再补齐 /etc/otelcol 配置目录并重启服务。
  • Nginx /api//admin/api/ 通过 genarrative_api upstream 代理到 127.0.0.1:8082upstream keepalive 为 64;通用 API 使用 genarrative_api_rps,后台 API 使用 genarrative_admin_rps。通用 /api location 保留 client_max_body_size 64m 作为编辑器图片、视频和文档请求的反代兜底,真实大小仍由路由与业务校验负责。若线上出现 413 Request Entity Too Large 且 access log 中 request_time=0.000upstream_status=-,说明请求在 Nginx 层被拦截,先核对 release 模板与实际媒体大小。limit_conn_status 429limit_req_status 429 必须在 HTTP 与 HTTPS server 中同时生效。
  • 旧作品列表 K6 脚本、gallery 专属限流分组和对应容量结论已经退役;源码只作历史记录,不得作为当前发布门禁。新的容量验收必须针对现役编辑器、项目和素材 API 单独建立数据、负载与指标口径。

容器化隔离部署方案单独放在 deploy/container/,用于本机或预发模拟现有的 Linux release + Nginx + OTLP Collector 非 BgFilter 拓扑,不替换当前生产 systemd + Nginx + Jenkins 发布路径。当前 compose 没有 bgfilter-worker,不构成完整 BgFilter 预发拓扑,也不覆盖任何会触发 BgFilter 的现役任务;它只用于非 BgFilter 路径,或通过下述 unsupported job smoke 验证外部生成队列的 claim / fail 回写和 API-only 更新。当前容器模拟参数保留 genarrative-release 的 CPU、nofile=4096worker_connections=768 采样口径,并在 compose 里落实到 spacetimedb cpus=1.0 mem_limit=2gapi-server cpus=2.0 mem_limit=1gexternal-generation-worker cpus=2.0 mem_limit=1gnginx cpus=0.5 mem_limit=128motelcol cpus=0.25 mem_limit=128m。完整模块首次实例化会超过旧 896m cgroup 上限,因此 SpacetimeDB 必须使用 2g;这不改变生产服务资源合同。容器 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 验证不经过 BgFilter 的外部生成 worker 动态扩缩容,inline 模式不参与该验证:

npm run container:init
npm run container:config
npm run container:build
npm run container:up
npm run container:down

容器方案默认暴露 http://127.0.0.1:18080api-server 在容器内监听 0.0.0.0:8082Nginx 通过 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。旧 gallery K6 profile 已退役;当前容器拓扑(明确不含 BgFilter worker)、端口和 OTLP debug exporter 使用方法见 deploy/container/README.mdnpm run container:config 默认只做 quiet 校验,避免把本地 env 中的 token 展开到终端;确需排查完整 compose 时再传 -- --print

多人内网预览入口固定为 http://192.168.35.82/build/,不配置公网域名。该独立 Jenkins 容器预览部署控制面不让浏览器直接操作 Docker 或持有 Jenkins TokenSPA 通过同源代理触发固定 shared/Genarrative-Preview-Deployer Job。分支和 commit 输入框通过受认证的控制服务搜索固定内网 Git 仓库并展示下拉结果;提交构建前控制服务重新确认分支存在、可选 commit 存在且属于目标分支,失败时不触发 JenkinsJenkins checkout 仍保留最终复核。每个分支使用稳定的内部 deploymentId 和独立 Compose projectWeb 端口从 8400..8499 在文件锁内分配,同一分支换 commit 优先复用端口,卸载后释放;页面记录 ID 使用 Jenkins 构建编号。Jenkins 用 preview-result.json 向页面提供 resolved commit、发布结果和内网 Web URL,页面刷新时由控制服务实时复核 Web 健康;构建详情链接固定使用局域网 Jenkins 地址,不暴露 loopback 地址。失败/取消且不可卸载的记录保留 7 天,停止记录保留 30 天,仍可卸载的失败记录不会自动清理。安装资产为 deploy/systemd/genarrative-preview-deployer.servicedeploy/env/preview-deployer.env.exampledeploy/nginx/genarrative-preview-deployer-lan.conf;完整合同见 docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md。 隔离验证 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 回写,不覆盖 BgFilter 成功、失败或 fallback 链路,也不需要真实外部生成密钥;本机 crates.io 网络不稳时使用 --local-binary,由容器内 Cargo 复用本机 Cargo 缓存构建,并把产物放进 Debian bookworm smoke runtime。

独立 BgFilter worker 的本机全进程验证先运行 cargo build -p api-server --manifest-path server-rs/Cargo.toml,再依次运行 npm run bgfilter-worker:smoke-testnpm run bgfilter-worker:load-smokenpm run bgfilter-worker:fault-smoke。三条命令只使用动态 loopback 端口、假 OSS 签名配置和本地 mock provider;不会读取仓库 .env* 或请求真实 BgFilter / OSS。自定义或 WSL binary 通过 GENARRATIVE_BGFILTER_SMOKE_BINARY 指定。当前 fault 范围包含 overload、queue deadline、两类 HTTP 状态顺序重试结果,以及 provider 成功响应 body 中途 reset 后第二次 attempt 串行成功;慢读、大响应、父侧客户端断连与 SIGTERM 排空另行验证。

需要复核真实 OSS + BgFilter 契约时,先启动只监听 loopback 的 worker,并在 worker 与 smoke 的当前进程环境中以不回显方式注入同一个一次性 GENARRATIVE_BGFILTER_INTERNAL_TOKEN;token 不得写入命令参数、仓库 env 文件或日志。OSS / BgFilter 凭据继续只放本地私密环境。使用标准 worker 端口 8083 时,确认 http://127.0.0.1:8083/readyz 成功,再分别设置 GENARRATIVE_BGFILTER_SMOKE_MODE=flatcomplex,并显式传入 worker 地址:

cargo run -p platform-oss --example bgfilter_worker_live_smoke --manifest-path server-rs/Cargo.toml -- --worker-url http://127.0.0.1:8083

示例程序省略 --worker-url 时默认访问 http://127.0.0.1:18083;该默认值只适合把 worker 显式启动在自定义隔离端口的场景,不是标准 dev / 生产端口。该命令会访问真实服务并产生调用成本;成功标准是 PUT 前 HEAD=404、私有上传成功、无鉴权请求返回精确 401 JSON、带鉴权请求返回 200 image/png、DELETE 2xx 且最终 HEAD=404。对象只写入 generated-character-drafts/bgfilter-smoke/<requestId>/source.png;正常失败会继续清理,进程崩溃或被强杀时需按输出 object key 人工复核。此 smoke 不经过用户 job、计费或父 flat fallback;完整 mock worker 失败 → 父 flat → 真实阿里云 和动画写回链在 staging 验收。

OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日志与 Nginx 文件日志仍保留:

  • 生产与容器 api-server env 模板默认 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-apiOTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318,容器模板使用 OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318。生产 genarrative-bgfilter-worker.service 强制使用独立的 OTEL_SERVICE_NAME=genarrative-bgfilter-worker,并从 worker env 读取同一 Collector 的 endpoint,避免父 API 与受限资源 worker 的遥测混在同一 service identity 下。
  • OTEL_EXPORTER_OTLP_ENDPOINT 必须指向 Collector 的 HTTP base endpoint;不要填 gRPC 4317,也不要直接填 Rider 端口,Rider 由 Collector 通过 RIDER_OTLP_GRPC_ENDPOINT 转发。
  • 应用日志按进程查看:父 API 使用 journalctl -u genarrative-api.service,独立 BgFilter worker 使用 journalctl -u genarrative-bgfilter-worker.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.usageprocess.memory.virtualprocess.cpu.timegenarrative.process.cpu.usage_percentprocess.thread.countgenarrative.process.memory.privateWindows 额外发送 process.windows.handle.countLinux 额外发送 process.unix.file_descriptor.count。这些指标只描述当前进程,不携带请求、用户或作品 label。
  • HTTP 运行态补充发送 genarrative.http.server.response_bodies.in_flightgenarrative.http.server.request_permits.available,后者带低基数 pool=default|gallery|detail|admin label,用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送 genarrative.puzzle_gallery.cache.* 指标,记录 fresh hit、stale hit、未命中、后台刷新开始 / 失败、重建耗时和预序列化 data JSON 字节数。
  • 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine 图片生成 / 编辑失败由 platform-image provider 输出结构化日志字段,字段包括 provider、endpoint、failure_stage、status、source、source_chain、source_chain_depth、timeout、retryable、latency_ms、prompt_chars、reference_image_count、实际 provider image_model、request_params 和 raw_excerpt;发生模型回退时另带 fallback_from_model / fallback_to_model。图片编辑请求参数日志还会带 reference_image_bytes_total,并在 request_params.referenceImages 中记录每个 multipart image part 的 fileName、mimeType 和 bytes,不记录 API key 或原始图片 bytesapi-server 再记录指标 genarrative.external_api.failures{provider,failure_stage,status_class,retryable},并写入 tracking_eventevent_key = external_api_call_failuremodule_key = external-apiscope_kind = modulescope_id = provider。调用方能拿到身份上下文时,失败事件还会在行级 user_id / owner_user_id / profile_idmetadata_json.userId / metadata_json.profileId / metadata_json.requestId / metadata_json.errorSource 中记录触发者、草稿 / 作品作用域、请求标识和传输错误链。排障时先按 provider / failureStage / imageModel 聚合,再下钻 userId / profileId,最后结合 request 日志、errorSource 和上游响应 excerpt 判断是模型不可用、限流、超时、解析失败还是未返回图片。
  • OSS 平台适配器也输出结构化日志,覆盖 sign_post_objectsign_get_object_urlhead_objectput_object。排查资产签名、上传或确认失败时,先按 provider=aliyun-ossoperation 过滤,再看 object_key / key_prefixstatusstatus_classerror_kindcontent_lengthcontent_typeelapsed_ms;角色动画逐帧额外按 frame_indexoperation=source_put|final_put|final_headattempt/max_attemptswill_retryoss_codeoss_request_id 对齐同一对象的请求尝试。请求 OSS 失败 时,timeout/connect/transport=true 表示传输类失败,OSS PutObject 的 status=400, oss_code=RequestTimeout, timeout=truestatus=429500599 表示暂时性失败,PUT 的 status=400oss_code 为空且 timeout=truetransport=true(message 含「错误响应体读取失败」,即 400 错误体读取超时/断流)也会重试;除这两类例外外,其他 400、401/403/404、配置、URL 和签名错误是确定性失败,不会重试。最终帧 HEAD 失败只会重试 HEAD,不会重复 PUT。日志不得包含 AccessKey、policy、signature、Authorization header、完整 signed URL 或 OSS 错误响应体;oss_request_id 只用于关联 OSS 服务端排障。排查 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_gallery procedure。
  • 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高 PREALLOCATED_VUS 可能让 300 个本地 Established 连接把 api-server private memory 瞬时推到 GB 级,且 /healthz 小响应也能复现;若压测结束后回落、response_bodies.in_flight 和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。
  • Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes;请求完成日志会直接带 request_idhttp.request.methodhttp.routeurl.schemeurl.pathhttp.response.status_codestatus_classlatency_msslow_request,完整链路继续到 Traces 面板按 trace/span 查看。
  • 指标 label 只允许低基数字段:HTTP 使用 methodroutestatus_classSpacetimeDB 调用使用 procedurestatus_classrequest_id 只进入 trace/log attribute,不进入 metric label。

常见外部服务变量:

  • GENARRATIVE_SPACETIME_SERVER_URL
  • GENARRATIVE_SPACETIME_DATABASE
  • GENARRATIVE_SPACETIME_TOKEN
  • GENARRATIVE_DATABASE_BACKUP_*
  • GENARRATIVE_LLM_*
  • VECTOR_ENGINE_*
  • ELEVENLABS_*
  • APIMART_*(已弃用,LLM 文本调用统一迁移到 VectorEngine
  • APIMART_*(历史残留,创意 Agent LLM 已迁移到 VectorEngine
  • HYPER3D_*
  • VOLCENGINE_SPEECH_*
  • DASHSCOPE_*
  • ALIYUN_SMS_*
  • WECHAT_*
  • ALIYUN_OSS_*

旧结构化创作 / RPG 的 Responses web_search 开关已退出 api-server 配置;部署环境不再保留 GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLEDGENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED

platform-llm 请求默认使用 Responses 协议;需要接旧 OpenAI Chat Completions 兼容网关时,调用方必须显式选择 Chat Completions。三种协议(openai_chatopenai_responsesanthropic)都使用原生 function tools,并统一从最终 LlmRunResponse.tool_calls 读取工具调用;流式 on_delta 只发送文本,不能把工具参数当作文本增量转发。Anthropic 工具请求使用 input_schemaRequired 使用对象形态 { "type": "any" }Anthropic 当前不支持 web_search、图片内容和纯 system 消息。AI 游戏创作独立 App 是客户端,不读取 .env;发布 App 启动时会在 Tauri 应用配置目录生成 game-creator.config.json,主窗口“配置”面板读写该运行时文件,真实密钥和本机覆盖项写入该文件,仓库内 apps/ai-game-creator-shell/game-creator.config.json 只作为默认模板,开发 CLI 无 AppHandle 时才回退读取仓库旁边的 gitignored 覆盖文件。LLM 维度由 llm.apiKind 控制,默认 openai_responses,可设为 openai_chat 接旧 Chat Completions 兼容网关,或 anthropic 接 Anthropic Messages。

流式工具片段按协议 slot 聚合,Responses 允许从 response.completed / response.incompleteresponse.output[] 恢复只在整体终态事件中携带的工具调用与正文。response.incomplete 中的工具调用即使参数是完整 JSON 也返回 Deserialize,截断正文则保留为可用的降级结果。收尾时空参数默认 {},非空参数必须是完整 JSON;解析失败、流式工具缺少身份或参数截断属于 Deserialize。流式已声明工具调用但没有聚合出工具 slot 属于 StreamUnavailable,由调用方决定是否回退非流式;文本和工具调用均为空才是 EmptyResponse

验收证据分为三类:cargo test -p platform-llm 的确定性用例验证 checked-in SSE fixture 的 parser 行为;tests/live_stream_tool_calls.rs 中默认执行的本地解析测试验证 PLATFORM_LLM_LIVE_API_KIND 归一与失败关闭;同文件默认忽略的真实端点用例只做归一后的工具调用 smoke,检查最终工具名、id 和完整参数 JSON,文本增量字符数仅用于打印观测。该 smoke 不录制或逐事件比较原始 SSE,fixture 即使来源于真实抓包也不能据此宣称转录无偏差。

真实端点 smoke 必填 PLATFORM_LLM_LIVE_BASE_URLPLATFORM_LLM_LIVE_API_KEYPLATFORM_LLM_LIVE_MODELPLATFORM_LLM_LIVE_API_KIND 可选,取值为 anthropic / openai_chat / openai_responses,省略或仅含空白时默认 openai_responses,未知非空值直接失败,避免拼写错误静默测到另一种协议。仓库根目录没有 Cargo.toml,必须显式指定 workspace manifest

PLATFORM_LLM_LIVE_BASE_URL=https://api.example.com/anthropic \
PLATFORM_LLM_LIVE_API_KEY='<从密钥管理处取,勿写入仓库>' \
PLATFORM_LLM_LIVE_MODEL='<模型名>' \
PLATFORM_LLM_LIVE_API_KIND=anthropic \
cargo test -p platform-llm --manifest-path server-rs/Cargo.toml --test live_stream_tool_calls -- --ignored --nocapture

PowerShell 下按测试文件头部示例依次设置三个必填变量,并按需设置 $env:PLATFORM_LLM_LIVE_API_KIND,再执行同一条 cargo test。切换 PLATFORM_LLM_LIVE_API_KIND 逐个跑三种协议,才算覆盖完整;--nocapture 会打印解析出的工具名、id、参数和文本增量字符数,便于核对。

该用例只从进程环境变量读取凭据,不读 .env.secrets.local,也不会写入任何文件。真实 API Key 一律不得提交进仓库,也不要写进 docs/、脚本默认值或测试 fixture;临时密钥用完应在上游及时吊销。

创意 Agent gpt-5 文本链路已从 APIMart 切到 VectorEngineapi-server 读取 VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY 构造 OpenAI-compatible LLM client,并自动补齐 /v1 前缀用于 Responses 协议。排查或切换密钥后,可在本地运行: 创意 Agent gpt-5.4-mini 文本链路已从 APIMart 切到 VectorEngineapi-server 读取 VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY 构造 OpenAI-compatible LLM client,并自动补齐 /v1 前缀后请求 /chat/completions。通用 /api/llm/chat/completions 代理使用 GENARRATIVE_LLM_PROVIDER=openai-compatibleGENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1GENARRATIVE_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_URLVECTOR_ENGINE_API_KEY,依次探测 /v1/models/v1/chat/completions/v1/responsesgpt-5.4-mini Chat Completions 和基础 JSON 输出能力;脚本只输出 HTTP 状态、耗时、模型和截断摘要,不应打印密钥。若 .env.secrets.local 不存在,先补本地 secrets 文件再运行,不要把 secrets 提交进仓库。

手机验证码短信

手机验证码发送走阿里云普通短信 SendSms,验证码由 module-auth 在当前 api-server 进程内生成并哈希,短期验证码投影随 auth_store_projection_meta 同步到 SpacetimeDB 后由任一 API 节点恢复和校验;不再调用阿里云托管验证码的 SendSmsVerifyCode / CheckSmsVerifyCode。因此只要 SpacetimeDB 正常,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_LENGTHALIYUN_SMS_CODE_TYPEALIYUN_SMS_RETURN_VERIFY_CODEALIYUN_SMS_CASE_AUTH_POLICYALIYUN_SMS_SCHEME_NAME 不再影响真实阿里云校验;验证码长度、有效期、冷却和失败次数由后端本地逻辑控制。真实短信联调仍需 SMS_AUTH_PROVIDER=aliyunSMS_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 按数量或时间阈值批量写入 SpacetimeDBdaily_login、作品游玩 work_play_start、付费、任务领奖和钱包相关关键事件继续同步直写数据库,避免用户任务进度、游玩统计或支付状态出现可感知延迟。任务配置、进度、领奖、钱包流水分别写入:

  • profile_task_config
  • profile_task_progress
  • profile_task_reward_claim
  • profile_wallet_ledger
  • profile_wallet_config

后台“账号配置”通过 GET/POST /admin/api/profile/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,不新增表。普通 API / external-generation 调用的失败事件优先写入本机 tracking outbox,再由后台 worker 批量落库;如果 outbox 因权限、磁盘或保护阈值不可写,仍回退同步直写 SpacetimeDB。BgFilter worker 是受限资源例外:provider 失败审计在 spawn 前受进程级 1024 硬上限保护,获准任务写入 GENARRATIVE_TRACKING_OUTBOX_DIR/bgfilter-worker/ 独立目录;任务满载、outbox 缺失、达到保护阈值或写盘失败时直接丢弃并记录指标,不同步直写。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_sendtimeout = true 的记录表示 reqwest::Error::is_timeout() 判定为超时,常见于连接、发送请求体、等待上游首包或上游长时间无响应;errorSource 会保存 reqwest 底层错误链,若只看到 client error (SendRequest),表示 Hyper 只暴露到发送请求阶段,仍不等于最终根因。若 statusCode 为空,应优先查同一 requestIdapi-server request 日志、provider 日志 source_chain、request_params、reference_image_bytes_total、Nginx / 出口网络、VectorEngine 可用性和请求体大小;若已有 502429 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。worker 启动时会先封存并 flush 已存在的 active / sealed 文件,恢复窗口内 SpacetimeDB 暂不可用则保留文件并按后续周期重试;FLUSH_INTERVAL_MS 只负责兜底封存长时间未满批的 active 文件。SpacetimeDB 批量 procedure 返回成功后删除 sealed 文件,失败则保留文件并重试。MAX_BYTES 是每个 outbox 实例的磁盘保护阈值,不是 flush 阈值;超过后低价值 route tracking 和 BgFilter provider 失败审计可以被丢弃并记录日志 / 指标,关键同步事件不进入该丢弃路径。api-server 使用配置目录本身,BgFilter worker 固定使用其 bgfilter-worker/ 子目录,两个进程不得操作同一个 active 文件。sealed 文件若出现无法解析的坏行,会重命名为 corrupt-* 隔离并记录 genarrative.tracking_outbox.files.corrupt 指标,避免一个坏文件阻塞后续批量入库。进程收到退出信号后会在 GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS 窗口内封存各自 active 文件并尽力 flush sealed 文件,超时或 SpacetimeDB 暂不可用时保留本地文件给下次同角色启动继续投递。该机制对已 enqueue 记录提供至少一次投递语义,依赖 tracking_event.event_id 幂等跳过重复事件;BgFilter 尚未 enqueue 或因硬上限 / 保护阈值被丢弃的审计不在该保证内。

钱包退款正式 pending 队列在 SpacetimeDB 的 profile_wallet_refund_outbox 表中,由每个 API 节点的 worker 共同处理;worker 启动即扫描库内 pending 行,成功在同一事务内写钱包账本并删除 outbox 行,失败按库内 available_at / attempts 重试。只有 SpacetimeDB 完全不可达时才写本机 wallet-refund-outbox emergency spool;如果进程在“临时文件写完但尚未改名”阶段崩溃,启动恢复会校验 tmp-* 内容并原子提升为按 ledger id 命名的 pending 文件,损坏或冲突文件移入 corrupt-* 隔离目录。达到 MAX_BYTES 时不再静默丢弃退款,而是写入同一持久目录下的 refund-overflow-* 溢出文件并继续重放;溢出文件不计入普通容量阈值,但必须接入容量告警和人工补偿预案,底层磁盘写入失败仍按关键退款告警处理。worker 连接失败、库内 retry、emergency spool 写入 / 容量失败和 corrupt-* 出现都必须接入告警;人工补偿先按 refund ledger id 对账 profile_wallet_ledgerasset_operation_wallet_settlement 与两类 outbox,再通过受控退款 procedure 幂等重放,禁止直接手写钱包表。该目录不能替代库内 outbox;发布和主机替换必须保留 /var/lib/genarrative/wallet-refund-outbox 并纳入节点恢复 / 备份演练。容器 loadtest / 预览环境必须分别为 api-serverexternal-generation-worker 挂载各自的 tracking 与 wallet refund 命名卷,不能让节点重建清空本机恢复队列。

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-ProvisionGenarrative-Api-Deploy 会在保留旧 /etc/genarrative/api-server.env 的前提下补齐缺失的 tracking outbox 运行态路径,并确保 /var/lib/genarrative/tracking-outbox 归属 genarrative:genarrative。用户认证真相源只允许从 SpacetimeDB 正式认证表(user_account / auth_identity / refresh_session)和 auth_store_projection_meta 中的短期验证码 / 微信 state 投影恢复;所有会读取或变更本机认证工作集的认证主链路在领域操作前都会从正式投影做一次只读刷新,刷新失败即 fail closed,不依赖粘性会话。不要再配置或依赖 GENARRATIVE_AUTH_STORE_PATH / auth-store.jsonmodule-auth 也不再维护本地文件持久化;auth_store_snapshot 不再作为备查或运行期恢复源,只在正式认证表为空时一次性转移最新旧快照并清空,且旧 get_auth_store_snapshot / upsert_auth_store_snapshot / import_auth_store_snapshot 入口已经删除。所有 API 节点必须使用相同的 GENARRATIVE_JWT_SECRET,它也作为验证码哈希盐;轮换后尚未消费的验证码会失效。如果 api-server 启动时连不上 SpacetimeDB,会持续重试启动恢复,直到认证工作集从 SpacetimeDB 正式表和短期投影恢复成功后才开始监听 HTTP,以避免用空本地状态或旧快照覆盖认证表。

发短信运维门禁:handler 会先刷新正式认证投影,再通过 projection CAS 写入不可消费的占位验证码来占用跨节点冷却窗口;占用失败时不得调用短信 provider。微信 OAuth state 活动数量有上限,命中上限应返回服务错误并触发限流 / 入口告警;不要通过调大单个 auth_store_projection_meta JSON 字段来绕过该保护。

前端登录态恢复只把 /api/auth/refresh401 / 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 条候选;关键词 / 字段条件过滤、所选列的完整候选集稳定排序和 1-based page 分页都基于这一次 SQL 结果,totalMatched 不再依赖另一份 COUNT(*) 快照。filters 支持两种 JSON 形式:object(列名到等值,如 {"user_id":"u1"},兼容旧入口)与条件数组(如 [{"column":"points","op":"gt","value":"5"}],运算符覆盖 eqnegtgteltltecontainsnotContainsstartsWithendsWithinnotInisEmptyisNotEmpty,允许同列多条件,条件间为 AND);两种形式的用户输入都不进入 SQL,只在 API Server 内存中过滤。请求页码超过实际总页数时钳制到末页,零结果固定返回第 1 页。存在第 50,001 条哨兵行时响应必须返回 scanLimitReached=true,后台固定分页栏上方明确提示匹配总数和分页结果可能不完整,不得把扫描范围外的数据误报为不存在。候选 SQL 响应体仍受 32 MiB 和 20 秒硬限制;宽表即使每页条数很小也可能整次拒绝,不会返回部分结果。实时写入仍可能改变相邻请求的候选快照,精确审计应使用对应业务表的专用查询而不是通用浏览页。

后台表查询页的结构化筛选条件支持逐条勾选启用或停用;停用条件不会进入请求,但会保留当前字段和值。表单和标题区的查询入口共用同一完整性校验,未完成的启用条件不会静默丢弃。eq / ne 和比较、文本运算符一样必须提供标量 value;显式空值判断使用 isEmpty / isNotEmptyin / notIn 使用逐项值标签编辑,支持粘贴多行值,避免把字符串中的逗号误拆成多个条件。行内交互控件的键盘事件不会冒泡触发行详情,空字段详情复制会给出失败反馈。

Issue 与交接

  • Issue tracker:自托管 Gitea。
  • 可用工具:Gitea UI/API 或 tea CLI。
  • 禁止默认使用:GitHub gh、GitLab glab
  • Canonical triage labelsneeds-triageneeds-infoready-for-agentready-for-humanwontfix

交接时写清:

  1. 背景和目标。
  2. 已改文件。
  3. 未完成风险。
  4. 已运行命令和结果。
  5. 下一步建议。

文档维护

当前 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
  • --inSpacetimeDB 导出的迁移 JSON。
  • --out:写回后的迁移 JSON 输出路径。
  • --dry-run:只统计回填行数,不写文件。
  • --placeholder-user-id:需要时可覆盖默认占位账号 ID。

维护页目标文件安全边界(2026-08-05)

scripts/deploy/maintenance-on.sh 只允许把同目录临时普通文件原子替换到普通文件或尚不存在的 page.html / enabled 目标。目标只要是符号链接(包括指向目录的链接)或目录,脚本必须在替换前失败,不能跟随链接把临时文件移入链接目标,也不能打印“已进入维护模式”。page_tempmarker_temp 必须在 set -u 下安全初始化,清理 trap 必须在首次 mktemp 前生效;任一失败退出都不得在目标同级遗留 page.html.tmp.*enabled.tmp.*,成功替换后则清空临时路径并解除 trap,不能误删已安装目标。跨平台实现继续使用 POSIX mv -f,安全语义由替换函数的目标类型门禁保证;修改后运行 bash -n scripts/deploy/maintenance-on.shnpm run check:maintenance-page。 api-server 启动时会硬校验 AGC 官方 Router 配置:API/All 角色必须使用官方 HTTPS 地址和固定模型,并配置 provisioning secret、Router 管理员 Token,以及专用加密 secret 或有效 JWT secret;缺失或不匹配直接拒绝启动。test 环境仅允许 loopback fixtureworker-only 角色不执行 Router 配置校验。