opsstd · Ops 接入标准

Ops 服务接入规范

把本页或 llms.txt 交给 AI,按同一套契约生成服务端;目标是让新服务一次通过 Ops 纳管校验。

标准版本 1.2.6 major v1 生成日期 2026-11-14 最低服务端 2.1.0(不强制校验) 公网入口:Token + 项目 Key 无 Key 只能读规范
权限说明:本页是开发规范,站点本身只读。真正的项目访问与部署走公网 MCP 入口,需要「客户端 Token + 项目 Key」两层凭据;无 Key 只能读标准文档资源,项目 Key 由管理员签发。
站点附加说明 · 非标准条文

给同事:怎么用这一页

  1. 先找管理员:用全量视图做冲突检测、登记项目,并按用途签发 deploy 或 readonly 项目 Key。项目 Key 下跑 ops_check_conflict 会漏报,不能代替这一步。
  2. 把地址交给 AI:附一句「请先完整读取 llms.txt,再按规范生成服务端;目标是能通过 ops 纳管校验」。不要把本页 HTML 当作唯一输入。
  3. 把 MCP 接到项目端的 AI 环境:按下文片段配置远程 MCP,确认能看到 ops_* 工具(详见规范第 17 章)。
  4. 先对齐契约再写代码:让 AI 先复述 6 个必填字段、R1–R7 运行时契约、健康检查与备份要求,确认后再产出目录、代码、脚本与 runbook。
  5. 交付前自检:按第 13 节逐条勾选;接入后跑一次 ops_check_conformance,确认无 fail、unknown 已人工核对。

项目端接入 MCP(外部配置,项目目录不加任何实现)

服务地址 https://mcp.soyooo.top/mcp,传输方式为 Streamable HTTP;两个请求头缺一不可,都不要写进仓库,用本机环境变量。

{
  "mcpServers": {
    "ops": {
      "type": "http",
      "url": "https://mcp.soyooo.top/mcp",
      "headers": {
        "Authorization": "Bearer ${OPS_CLIENT_TOKEN}",
        "X-Ops-Project-Key": "${OPS_PROJECT_KEY}"
      }
    }
  }
}
  • 自检顺序:ops_get_project_context → ops_list_projects(只应返回本项目)→ ops_check_conformance → ops_get_health。
  • 没有客户端 Token → 401;有 Token 没 Key → PROJECT_KEY_REQUIRED;Key 格式不对 → 400;Key 绑定了别的项目 → 403。
  • 写工具(部署/回滚)需要 deploy 类型 Key、dryRun=false + confirm=true,且 conformance 无 fail。

发布产物

只有 {id}-{version}.tar.gz 与同名 .sha256。conformance.json 不是交付物:体检由 ops_check_conformance 实时计算,静态快照会过期,缺失它不影响部署。

本轮重点:数据目录、打包与超时

  • 数据目录不强制叫 DATA_DIR:写应用真正读取的键(如 DATABASE_FILE、UPLOAD_ROOT),解析后落在登记 dataDir 内即可;无持久化状态在 projects.json 写 stateless: true。为了凑字段写一个没人读的假键,比缺字段更糟。
  • 打包用固定 build 脚本:一条命令产出 tar.gz + .sha256,并自带禁项扫描、软链检查、解包后随机端口 + 临时数据目录启动自检。不要靠人记 tar 参数。
  • 超时:requestTimeout 不早于反代 read_timeout/write_timeout,且它只约束接收请求体,不约束响应流;长任务按业务最长耗时单独设计。
  • runbook 不写死版本:用 readlink -f <deployPath>/current 或读 current/package.json,否则每次部署都要改文档。

两种入口不要混用

  • 公网入口 https://mcp.soyooo.top/mcp:客户端 Token 识别调用方与限流,X-Ops-Project-Key 决定项目与能力;无 Key 只能读标准文档资源,所有项目工具返回 PROJECT_KEY_REQUIRED。
  • 本地 stdio:管理员在本机排查用,可读全部项目,行为与旧版一致。不要把这里的可见范围套到公网入口。

项目 Key 的错误语义

  • Key 头格式非法、重复数组、超过 128 字节 → 400 INVALID_PROJECT_KEY。
  • 伪造、过期、已吊销、一次性已使用、绑定项目不存在 → 401。
  • 访问非绑定项目 → 403,提示不区分项目是否存在。
  • Key 等于生产写权限:不要发到群聊、提交进仓库、写进日志或截图;泄露立即吊销。
验收里的 unknown 表示什么:当前没有只读接口可以判定(crontab 引用、ss 监听、.env 权限等)。它不等于通过,也不等于失败,必须人工补证并在报告中列出。

1.1.0 新增:迁移与回滚

  • 迁移文件放 current/migrations/NNNN_name.sql,随 release 发布,启动前执行;失败必须退出,不得带着半套 schema 提供服务。
  • schema_migrations 必须与文件一一对齐;迁移语句要可重复执行,禁止人工补记录行。
  • 回滚只切 current;破坏性迁移不得自动回滚,失败只能前进修复。执行器当前无条件 switch-back,且完全不感知迁移。

1.2.0 新增:运行时与依赖基线

  • 项目必须保证的只有两项:Node.js 大版本 ≥22(推荐 22.x LTS,以平台现网为准)、含原生模块时的工具链(python3 + make + g++、glibc ≥2.28)。
  • PM2、Caddy、SQLite CLI、glibc 运行时、OS/systemd、磁盘、内存属于平台保证项:由平台负责,项目无需处理,也未提供降级路径;文中保留失败表现,仅作排障线索。
  • 标准声明的是已验证的大版本与工具链,不写补丁号;发布窗口内不要自行升级运行时。

四个最常见的踩坑

  • 监听 0.0.0.0:端口直接暴露公网,绕过 Caddy 的 TLS 与限流。
  • 把 data/ 放进 releases/ 或 current/:切换版本后数据丢失。
  • 健康检查返回 404 却当成存活:现在只有 2xx/3xx 才算通过,>=400 判失败。
  • 用项目 Key 跑冲突检测:只能看到本项目,漏报后接入就撞端口/域名;必须由管理员做全量预检。

Ops 服务接入规范 · AI 开发单页(opsstd v1)

  • 标准:opsstd
  • 标准版本:1.2.6(唯一事实源:opsstd://v1/manifest.json 的 version;version.json 为镜像元数据,两者不一致时启动即报错)
  • 最低服务端版本:2.1.0(1.1.0 的 DATA_DIR 完整校验与 integrity 失败即备份失败依赖 2.1.0;当前不强制校验:没有接口暴露服务端版本,仅作规划与排障参考)。1.2.2 的备份目标语义(provider 覆盖、本地目标、远端 fail-closed)只在后端包含该改动的版本生效,发布前旧后端仍按「默认上传 OSS」行为运行
  • 生成日期:2026-11-14
  • 适用对象:部署在 47.110.36.244 上、由 PM2 托管并由 Caddy 反向代理的 Node.js / Python / Go / 静态服务
  • 使用方式:将本文件整份作为起点,生成代码、目录、脚本、runbook 与项目登记;文中规则全部可离线自检,只有标注「平台校验」的条目需要 Ops 平台在线判定

鉴权先读(v1.0.0 起):公网入口 https://mcp.soyooo.top/mcp 需要两层凭据——客户端 Token(识别调用方与限流)+ 项目 Key(X-Ops-Project-Key,决定项目与能力)。不带项目 Key 时只能读取标准文档资源,所有项目数据工具返回 PROJECT_KEY_REQUIRED;带 Key 时只能访问 Key 绑定的项目。本地 stdio 模式行为不变。详见 1.1、1.2;项目端客户端配置与排查见第 17 章。

服务端返回的 standardVersion / standardDigest 若与本文件不一致,以服务端为准,并拒绝任何写操作(STANDARD_VERSION_MISMATCH,不做静默降级)。


0. 速览:12 条不可协商的硬规则

  1. 代码目录固定 deployPath(登记 backup.appDir,未登记时按 <OPS_DEPLOY_ROOT>/<id> 兜底,安装默认 /opt/apps/{id}),数据目录独立(默认 <deployPath>/data),数据不得位于 releases/ 或 current 之内。
  2. PM2 进程名 = pm2Name,cwd = deployPath,两者与后台登记逐字一致。
  3. 端口只从环境变量读取,服务只监听 127.0.0.1,禁止 0.0.0.0。
  4. autorestart: true,并实现 SIGTERM 优雅退出。
  5. 健康检查默认路径 /healthz(登记 probePath),只有 2xx/3xx 通过,>=400 判失败。
  6. 日志只写 stdout/stderr,由 PM2 收集;应用不自行写轮转文件。
  7. 启动参数按性质拆两处:非密钥运行参数(PORT/HOST/NODE_ENV/WEB_DIST 与数据路径键)写入 ecosystem env,作为启动唯一真源;密钥(JWT_SECRET、第三方 API Key 等)只放 .env(权限 600,不入 git、不进日志、不进备份包)。数据目录不强制叫 DATA_DIR:也可写应用真正消费的 DATABASE_FILE、UPLOAD_ROOT 等键,解析后必须落在登记 dataDir 内;无持久化状态在 projects.json 写 stateless: true。禁止为了凑字段写一个应用从不读取的假键。
  8. 备份范围为整个 data/ 目录;SQLite 必须用 sqlite3 ... ".backup",禁止直接复制 .db 文件。
  9. 运维脚本固定放 /opt/ops-scripts/{id}/,不放项目 git 目录(会被 git pull 覆盖)。
  10. runbook 固定放 /opt/runbooks/{id}.md,8 章齐全,≤200KB;不得写死具体 release 或版本号(会天天过期),一律用 readlink -f <deployPath>/current 或读 current/package.json 动态取。
  11. 发布产物 = {id}-{version}.tar.gz + 同名 .sha256,由项目内固定 build 脚本一条命令产出(含禁项扫描、软链检查、解包后随机端口 + 临时数据目录的实际启动自检),落到 releases/{ts} 并用 current 软链切换,不覆盖旧版本。
  12. 不改动服务器上其他站点、不重启 Caddy、不使用 pm2 delete/kill/save、不对 ops 项目做任何操作。

1. 平台关系与边界

事项 约定
公网入口 Caddy 终止 TLS 后 reverse_proxy 127.0.0.1:{port};应用不直接暴露公网端口
证书 静态证书 /etc/caddy/certs/fullchain.pem 与 privkey.pem;新域名必须已被证书 SAN 覆盖,否则拒绝接入(执行方:平台 / 运维;项目方只需确认域名可正常 HTTPS 访问)
平台角色 登记 projects.json、Caddy 站点块、备份目标(本地/OSS/COS)、健康探测、审计
项目角色 应用代码、.env、data/、本地备份触发、runbook 内容
预留资源 端口 80/443/2019(Caddy)、3100(ops-backend)、3200(ops-mcp)、8080(jingyu-server)、25884(scmusic);项目端口建议 3000–3999
保留标识 projectId 不得为 ops;pm2Name 不得为 ops-backend、caddy、pm2-logrotate

1.1 两种使用方式(不要混为一谈)

方式 入口 凭据 可见范围
公网入口 https://mcp.soyooo.top/mcp 客户端 Token + X-Ops-Project-Key 仅 Key 绑定项目;无 Key 只能读标准文档资源
本地 stdio 本机 ops-mcp 进程(管理员) 本地配置;OPS_PROJECT_KEY 可选 本地管理员可读全部项目,行为与旧版一致
  • 公网入口面向开发者与 CI,必须按 1.2 携带两层凭据;不要把本地 stdio 的「可读全部项目」当成公网行为。项目端如何配置客户端、自检与排错见第 17 章。
  • stdio 模式适合管理员在本机排查;它不经过公网入口的 Key 校验,但同样不能绕过 ops-backend 的鉴权与硬约束。

1.2 公网入口凭据与错误语义

第一层:客户端 Token。放在 Authorization: Bearer <token>(兼容 X-Ops-Token),用于识别调用方与限流(默认 120 次/分钟,超出返回 429)。它不授予任何项目数据权限。

第二层:项目 Key。放在 X-Ops-Project-Key,格式 ops_pt_<projectId>_<random>,绑定唯一项目,决定可见范围与能力。Key 由 superadmin 通过 POST /api/keys 签发,服务端只存哈希,明文只在签发时展示一次。

两层凭据发放入口不同、不可互相替代:

  • 项目 Key:在运维后台「项目 Key」签发,随「复制信息」一并下发(含 X-Ops-Project-Key 明文或掩码)。
  • 客户端 Token:在运维后台「MCP 客户端」按调用方单独签发(ops_ct_<clientId>_<random>),不属于项目接入材料;后台「复制信息」里只有 Authorization: Bearer <客户端 Token> 占位,必须由管理员另行签发后填入。
  • 只有项目 Key、没有客户端 Token 时,第一个 initialize 就会返回 401 CLIENT_TOKEN_REQUIRED(reason=missing),不会进入 MCP 层,也拿不到任何 ops_* 工具;反之只有客户端 Token 时工具可见,但项目数据类工具一律 PROJECT_KEY_REQUIRED。

  • 不带项目 Key:只可读取标准文档类资源(opsstd://*、manifest.json、规范正文、版本与 digest);opsproj://current 只返回 bound:false。所有项目数据类工具一律返回 PROJECT_KEY_REQUIRED,不会触达 ops-backend。

  • 带项目 Key:只能看到绑定项目——列表类接口只返回该项目,其余工具传入的项目必须与 Key 绑定项目一致;跨项目请求返回 403,错误信息不区分项目是否存在。
  • 项目 Key 可读自己项目的只读信息:项目列表、部署状态、inventory(仅本项目域名/端口/进程)、备份列表、runbook、日志、调度状态、覆盖本项目域名的证书。跨项目访问与全部写操作仍返回 403;全局管理接口(/api/keys、/api/caddy/* 写接口等)一律拒绝。
  • 冲突检测与新项目登记只能在管理员侧完成:ops_check_conflict 依赖全量项目视图。项目 Key 下调用不会给出“无冲突”:返回 ok:false、limitedView:true、requiresAdminReview:true,并提示由管理员用全量视图复核。正确顺序是「先找管理员建项目并签 Key,再用 Key 部署」。
现象 返回
未携带 Authorization 401,CLIENT_TOKEN_REQUIRED,reason=missing
客户端 Token 错误、被吊销、已过期 401,CLIENT_TOKEN_REQUIRED,reason=invalid / revoked / expired(code 相同,按 reason 区分,不要只看 code)
未带项目 Key 调用项目工具 工具错误 PROJECT_KEY_REQUIRED(不触达后端)
Key 头格式非法、重复数组、超过 128 字节 400,INVALID_PROJECT_KEY
Key 伪造、过期、已吊销、一次性已使用、绑定项目不存在 401
访问非绑定项目 403,提示不区分项目是否存在
使用 ops 项目的 Key 403

Key 类型(均由 superadmin 签发):

类型 scope 用途 默认有效期
deploy project:read、project:deploy、project:rollback、project:backup:run、project:caddy:write、project:env:write 本项目发布、回滚、备份与改环境变量等写操作 24 小时(可设 1 小时–7 天)
readonly project:read、project:logs、project:backup:read 本项目只读、日志与备份查询 90 天(可设 1 小时–90 天)
  • deploy 类 Key 可设为一次性,成功使用一次后立即失效,再次使用返回 401。
  • Key 等于生产写权限:不要发到群聊、提交进仓库、写进日志或截图;只放本机环境变量或密钥管理,泄露立即吊销。

安全前提:本模型只约束 MCP / 平台通道。如果开发者同时持有服务器 SSH/sudo,可直接绕过全部约束——「只发 Key、不发 shell」是合规成立的前提。


2. 当前能力边界(务必先读,避免误判)

2.1 已实现

  • MCP 工具 14 个:11 个只读工具 ops_list_projects、ops_get_health、ops_get_logs、ops_get_runbook、ops_list_backups、ops_get_scheduler_status、ops_get_inventory、ops_check_conflict、ops_plan_deployment、ops_check_conformance、ops_get_project_context;3 个写工具 ops_upload_artifact、ops_deploy_project、ops_rollback_project(部署/回滚默认 dryRun=true,真正写入需 dryRun=false + confirm=true)。
  • 公网入口两层凭据:客户端 Token 识别调用方与限流;项目 Key 绑定项目并决定能力。无 Key 仅标准文档资源可用,有 Key 仅绑定项目可见。
  • 项目 Key 的签发/列表/吊销(/api/keys,仅 superadmin)、/api/project/context、/api/project/deploy、/api/project/rollback、/api/project/env(GET/PUT)。
  • 受限执行器:部署/回滚/staging/current 切换/健康检查/失败回滚、.env 掩码读写,均在 ops-backend 内完成。
  • 回滚语义:ops_rollback_project 支持可选 targetRelease——必须是该项目 releases/ 中已存在的目录名,允许向前或向后;不传时保持“退一格”。目标等于当前会被拒绝;返回 direction(forward/backward)与 targetSource(explicit/default)。目标切换后重启、环境或健康校验失败时,统一切回原 release 并恢复 env 快照;切回失败会保留现场并写 error 级事件。oplog 成功事件的 reason 明确为 null,失败事件必须带 reason(health/env/pm2/error)。
  • 线上 conformance(ops_check_conformance):实时拉登记、查进程、探端口、读 Caddy 后现场比对,不读取发布包内任何文件;覆盖登记字段、runbook 路径与章节、备份配置与生效的备份目标、PM2 进程存在与 online、端口探测、Caddy 站点块与反代目标、OSS 资源前缀。
  • 部署工具内置 conformance 门槛:存在 fail 时拒绝部署(CONFORMANCE_FAILED),项目 Key 无 override 权限。首次部署(项目从未有 releases/current)自动把 process.ecosystem_loadable、network.port_probe、caddy.site_block、caddy.proxy_target 标为 deferred(不计 fail),部署成功后执行器补建站点块并立即复检、写入审计,MCP 在响应里返回 postDeployConformance;已部署项目仍按 fail 拦截。
  • 备份调度、按生效目标(本地 / OSS / COS)落盘与按来源保留 7 份、审计日志、Dashboard、Caddy 配置备份与 reload 管理(人工在后台操作)。
  • 规范站静态分发:develop.soyooo.top 提供 /、/llms.txt、/index.md,内容与 standards/llms.txt 同哈希(/v1/ 版本化路径与 digest 在线校验尚未实现)。

2.2 部分实现(不得当作完整能力)

  • 项目 Key 的只读范围已放宽到本项目:GET /api/projects、/api/projects/deploy-status、/api/caddy/inventory(按项目过滤)、/api/scheduler/status(按项目过滤)、/api/certs(按域名过滤)、/api/projects/:id/{runbook,logs,backups,ecosystem-check};跨项目与写接口仍 403。project:caddy:write / project:backup:run 对应的项目级写接口尚未开放。
  • conformance 门槛目前由 MCP 工具在部署前校验,后端未二次强制。
  • pathPrefixes 对所有角色实际为 ['**'],前缀隔离未生效;新账号需显式收窄,但不能把它当作唯一安全边界。
  • automation 角色不存在(规划中),不得在代码或脚本中依赖。
  • MCP 层 plan:read / conformance:read 只读工具已可用,但权限分层未在 MCP 侧强制。

2.3 尚未实现(不要调用)

  • ops init 脚手架、ops check --offline 离线自检、CI 门槛。
  • 项目级备份执行接口 /api/project/backup/run、项目级 Caddy 写入接口 /api/project/caddy。
  • ops_register_service、ops_preflight_onboard、ops_update_project、ops_publish_runbook、ops_run_backup、ops_adopt_service、ops_query_audit。
  • backup.sh / monitor.sh 的受控投递、runbook 自动发布、多版本兼容窗口。

现状补充:项目登记仍由管理员通过后台/API 写入 projects.json,MCP 侧没有 ops_register_service。启动配置(ecosystem.config.cjs)从 2.11.2 起已由部署执行器自动解析/渲染,不再要求人工预先生成;产物自带配置也可被采用。

2.4 平台自动判定 / 只能人工

判定方式 条目
平台可自动判定 registry.*(字段格式与必填)、runbook.path_standard / runbook.readable / runbook.sections、backup.app_dir / backup.schedule / backup.oss_prefix(按生效目标可列出)、process.exists / process.online、network.port_registered / network.port_probe、caddy.site_block / caddy.proxy_target / caddy.domain_unique、oss.asset_prefix
当前一律返回 unknown(需人工补证) backup.script_referenced、monitor.script_referenced(crontab)、network.listen_loopback(ss)、process.start_params、caddy.hardening、security.env_permissions、security.public_bind

unknown 不等于 fail,也不代表通过;出现 unknown 必须人工核对并在报告中列出。标注「平台自动判定」的条目在只读数据源不可用时(OSS 列取失败、runbook 读取异常、pm2 不可达)同样会返回 unknown。

结论:当前接入必须人工执行。conformance 只校验「登记与可观测项」,不校验代码质量;本文件的运行时契约(R1–R7,定义见 2.5)目前依赖开发者自检与后续离线校验,平台尚未逐条校验。

2.5 运行时契约 R1–R7

本节是 R1–R7 的唯一定义;其他章节引用编号时一律指回本节,不再重复展开。

# 契约 规范要求 判定方式 当前强度
R1 进程标识与工作目录 PM2 name 必须等于登记 pm2Name;cwd 必须等于 deployPath(取登记 backup.appDir;未登记时 <OPS_DEPLOY_ROOT>/<id>,安装默认 /opt/apps/{id}) 预检解析 ecosystem 的 name/cwd;pm2 describe {pm2Name} 的 pm_cwd 复核 fail(已实现)
R2 监听地址 端口只来自 PORT;只监听 127.0.0.1,禁止 0.0.0.0 预检 PORT 键;健康探测命中 127.0.0.1:{localPort};项目方确认代码绑定 127.0.0.1 且与登记 port 一致(ss -tlnp 需服务器权限,属管理员核对手段) 预检部分 fail;0.0.0.0 检测恒 unknown(无 ss 只读数据源)
R3 健康检查 路径以登记 probePath 为准,默认 /healthz;仅 2xx/3xx 通过,>=400 判失败 部署后轮询 probePath;conformance network.port_probe fail(健康失败触发回滚)
R4 日志 只写 stdout/stderr;不得自建文件轮转 人工检查 createWriteStream/日志框架 file transport;pm2 logs 可读 warn(无自动判定)
R5 优雅退出 处理 SIGTERM:停止接收新请求 → 等在途请求 → 释放 DB/定时器/队列 → exit 0 人工 kill -TERM 验证;kill_timeout 留足时间 warn(无自动判定)
R6 环境分层 非密钥运行参数(PORT/HOST/NODE_ENV/WEB_DIST/数据路径键)必须写入 ecosystem env;密钥只放 deployPath/.env(600),由应用自行加载 预检 ecosystem.env 缺键即 fail;/proc/<pid>/environ 比对 PORT、WEB_DIST、ecosystem 声明的数据键;密钥是否误入 env 仅人工检查 缺键 fail;密钥误入 env warn(无自动扫描)
R7 数据目录独立 dataDir 及其解析后的数据键不得位于 releases/ 或 current 内;切换版本后数据必须仍在 数据键路径校验(fail);dataDir 本身的 realpath 比对依赖后端 2.1.0 数据键 fail;DATA_DIR/dataDir 自身校验在 2.1.0 前不生效

判定约定:

  • 表中「当前强度」描述的是 1.1.0 已实现的行为;与设计文档早期版本不一致的,以本节为准。
  • R6 已按现行环境模型修正:旧表述「.env 由应用自身加载、不依赖 PM2 注入」只适用于密钥;非密钥参数必须由 ecosystem env 注入,否则 /proc 不可校验。
  • R2/R4/R5 属于「规范硬性、平台只能人工或 unknown」的条目,不得因为 conformance 返回 unknown 就视为通过。

3. 目录约定

<OPS_DEPLOY_ROOT>/{id}/         deployPath,PM2 的 cwd(安装参数 --deploy-root,默认 /opt/apps/{id})
├── releases/{YYYYMMDD-HHMMSS}/ 不可变发布目录,每次部署新建
├── current -> releases/{ts}    当前版本软链,回滚即重指
├── data/                       dataDir,持久数据,禁止放进 release
├── .env                        运行时配置,权限 600,不入库
├── .env.example                键名 + 说明 + 安全默认值,可入库
├── src/                        应用代码(随 release 发布)
└── package.json                依赖与启动脚本

/opt/ops-scripts/{id}/backup.sh   备份脚本(运维归属,不进项目 git)
/opt/ops-scripts/{id}/monitor.sh  监控脚本(同上)
/opt/runbooks/{id}.md             运维手册,≤200KB

图中 <OPS_DEPLOY_ROOT>/{id} 是 deployPath 的标准写法:实际值优先取项目登记的 backup.appDir;未登记时取部署根(--deploy-root,默认 /opt/apps,即 /opt/apps/{id})。deployPath 不是 projects.json 的独立字段,不要自行改写或写入登记条目;任何 /opt/{id} 都是旧口径残留。

硬要求:

  • dataDir 的 realpath 不得以 releases/ 或 current 为前缀——切换版本后数据必须仍在。
  • current 是软链,不要把它当真实目录写入;应用不应依赖自身位于哪个 release。
  • 运维脚本与 runbook 都不要放项目仓库:历史上 git pull 覆盖脚本导致备份静默失败。
  • 目录与文件建议属主为非 root 运行用户;.env 权限恒为 600。
  • 无端口服务:localPort=0,不参与端口探测、不生成 Caddy 站点块,其余契约不变。

4. PM2 运行契约

项 规范
进程名 name 必须等于登记 pm2Name,全局唯一
工作目录 cwd 必须等于 deployPath(取登记 backup.appDir;未登记时 <OPS_DEPLOY_ROOT>/<id>,安装默认 /opt/apps/{id})
入口 script 指向 current/ 下的入口文件,切换版本后重启即生效
实例 默认 instances: 1、exec_mode: 'fork';需要 cluster 时必须先与平台确认共享状态
重启 autorestart: true、max_restarts 有限值、max_memory_restart 明确上限
退出 kill_timeout 建议 10000ms,配合应用侧 SIGTERM 处理
环境 env 必须显式写全非密钥运行参数:PORT(= localPort;无端口服务除外)、数据路径键(DATA_DIR 或应用真正消费的 DATABASE_FILE/UPLOAD_ROOT 等,见第 7 节)、HOST、NODE_ENV,有前端产物时加 WEB_DIST;密钥只走 .env,禁止写进 env
超时 应用层 requestTimeout 不得早于反代 read_timeout/write_timeout(否则反代还在等、应用先断);它只约束接收请求体,不约束响应流,不能当流式响应超时用
禁止 pm2 delete、pm2 kill、pm2 save、改监听端口、启动 ops-backend 替身进程

ecosystem.config.js 参考骨架(平台从 2.11.2 起会在首次部署时按登记自动渲染;本地自检可参照同一结构):

module.exports = {
  apps: [{
    name: 'demo-api',              // 必须等于 projects.json 的 pm2Name
    cwd: '/opt/apps/demo-api',     // 必须等于 deployPath(<OPS_DEPLOY_ROOT>/{id} 或登记 backup.appDir)
    script: 'current/server.js',   // 通过 current 软链指向当前 release
    instances: 1,
    exec_mode: 'fork',
    autorestart: true,
    max_restarts: 10,
    max_memory_restart: '300M',
    kill_timeout: 10000,
    env: {                          // 非密钥运行参数:启动唯一真源,与登记值逐字一致
      NODE_ENV: 'production',
      PORT: '3000',                 // 必须等于登记 localPort
      HOST: '127.0.0.1',
      DATA_DIR: '/opt/apps/demo-api/data', // 数据路径键之一;应用实际读 DATABASE_FILE/UPLOAD_ROOT 时改写对应键
      WEB_DIST: 'current/dist'      // 仅当产物含前端静态资源时必填
    }                               // 密钥(JWT_SECRET、第三方 API Key)只放 .env,不进 env
  }]
};

SIGTERM 语义(R5):收到信号后停止接受新连接 → 等待在途请求结束 → 关闭 DB / 定时器 / 队列 → 以 0 退出;超过 kill_timeout 允许被强杀,但不得留下锁文件或半写数据。SIGINT 同样处理。

超时口径(实测教训):

  • 应用层 requestTimeout 必须不早于反代侧 read_timeout / write_timeout,否则会出现「反代还在等、应用先断开」的假 502/504。
  • requestTimeout 只约束接收请求体的时长,不约束响应流;把它当成流式响应超时会导致长任务被误判。
  • 真正的超时要按业务最长耗时单独设计(下游 API 调用、批处理、导出等),并在 runbook 写明依据。first 的经历是:瓶颈来自上游 API 的 120s 中止,而不是本服务或反代,调小本服务超时只会更快失败。

5. 健康检查契约

  • 默认路径 /healthz;实际探测路径以登记 probePath 为准(默认值 /healthz,必填)。
  • 成功:HTTP 200,Content-Type: application/json,响应体 {"status":"ok"}。
  • 失败:依赖不可用时返回 503(不要用 200 掩盖 DB/依赖故障)。
  • 判定:只有 2xx/3xx 视为通过;4xx/5xx 一律判失败,404 不再视为存活。
  • 要求:健康检查不得触发写操作或长耗时查询;接口必须在进程启动后尽快可用。
  • 无端口服务:localPort=0,不做端口探测。

6. 日志约定

  • 应用把结构化日志写到 stdout / stderr,由 PM2 收集与 pm2-logrotate 轮转。
  • 应用不得自行实现文件轮转(createWriteStream + 自建切割、日志框架 file transport 均属不合规,记为 warn 级)。
  • 单行 JSON 优先:{"t":"ISO8601","level":"info","msg":"..."},便于检索。
  • 禁止打印:.env 值、AccessKey/Secret、Token、完整请求头、用户隐私字段。
  • 日志量需可控:健康检查等高频路径不逐次打 info;错误日志必须包含可定位的上下文。

7. 环境变量约定

启动环境按性质拆成两个来源,职责不重叠:

类别 内容 位置 作用
非密钥运行参数 PORT、HOST、NODE_ENV、WEB_DIST,以及数据路径键(DATA_DIR 或应用真正消费的键) ecosystem.config.js 的 env 启动唯一真源;平台预检与启动后校验都以它为准
密钥与业务凭据 JWT_SECRET、第三方 API Key、数据库口令等 deployPath/.env(权限 600) 只在进程内使用;不入库、不入日志、不进发布产物

7.1 端口

  • PORT 必须显式写入 ecosystem env,值等于登记 localPort。
  • 无端口服务(localPort=0)不声明 PORT,其余键照常。

7.2 数据目录(三种表达,任选其一)

方式 写法 校验
A. 通用键 DATA_DIR=<deployPath>/data 解析后必须落在登记 dataDir 内(含相等与子目录);相对路径以部署根解析
B. 应用真实消费的键 如 DATABASE_FILE=<deployPath>/data/app.db、UPLOAD_ROOT=<deployPath>/data/uploads 解析后必须落在登记 dataDir 内(含相等与子目录)
C. 无持久化状态 projects.json 写 stateless: true 不要求任何数据键,也不校验 dataDir;备份可关闭

判定顺序(与实现一致,命中即停):

  1. projects.json 的 stateless === true → 视为无持久化状态,通过;
  2. ecosystem env 中 DATA_DIR 非空 → 采用该键,并按与具体数据键相同的规则解析、校验落在 dataDir 内;
  3. ecosystem env 中符合 _FILE / _DIR / _PATH / _ROOT / _DB 后缀的数据类键(排除 NODE_PATH、WEB_DIST、HEALTH_PATH、PATH)→ 逐个解析并校验落在 dataDir 内;
  4. 以上都没有时,读取项目根 .env(deployPath/.env)仅补充发现数据类键,再做同样的路径校验;
  5. 仍没有 → 判 fail:ecosystem.env 缺少 DATA_DIR,也未声明具体数据键(*_FILE/_DIR/_PATH/_ROOT/_DB)或 stateless=true:无法确认持久化数据目录。
  • .env 只是补充发现来源:从 .env 读到的键只参与「发现 + 路径校验」,不参与 /proc/<pid>/environ 运行时比对——.env 由应用在进程内注入,不进入 execve 初始环境,看不到这些键。只有 ecosystem env 声明的键才纳入运行时校验。
  • DATA_DIR 校验的生效版本:DATA_DIR 的完整路径校验自后端 2.1.0 起生效;2.0.0 及更早版本的实际行为是「DATA_DIR 存在即通过,不做路径比对」。在低于 2.1.0 的后端上,接入方仍应主动把 DATA_DIR 指向登记 dataDir,不要依赖宽松行为;平台在该版本前不会因此判 fail。
  • 相对路径以部署根为基准:相对路径按 PM2 cwd(即 deployPath)解析,绝对路径原样使用。校验失败的错误信息会写明基准目录与登记目录,例如 DATABASE_FILE=data/app.db(相对 cwd=/opt/apps/demo-api 解析 → /opt/apps/demo-api/data/app.db)不在登记 dataDir=/opt/apps/demo-api/data 内。
  • 为什么允许 B:jingyu 这类应用读的是 DATABASE_FILE 与 UPLOAD_ROOT,从不读 DATA_DIR。硬填 DATA_DIR 等于在登记表里造一条永远无人读取的假数据——这比缺字段更糟,因为它会让预检「通过」却什么都没验证。用真实键才能既表达清楚,又能被实际校验。
  • 一个项目可以同时有多个数据键,但解析后都必须落在同一个 dataDir 内;需要多个独立目录时,先与管理员确认再调整登记。
  • stateless 与数据键/dataDir 互斥使用:声明 stateless: true 后,不应再指望平台校验数据目录;若之后产生持久化数据,必须去掉该字段并补数据键。

7.3 其余运行参数

  • 非密钥参数必须显式写入 ecosystem env:PORT 等于登记 localPort,数据路径键按 7.2,产物含前端静态资源时还必须写 WEB_DIST。缺失即预检失败(ecosystem.env 判 fail),不依赖应用代码或 .env 兜底。
  • 进程启动后,平台读取 /proc/<pid>/environ,与 ecosystem env 逐键比对 PORT、WEB_DIST(如有)与 ecosystem env 中声明的数据路径键(仅在 .env 中发现、未写入 ecosystem env 的键不参与运行时比对);不一致即 ENV_MISMATCH 并回滚。这是「配置的值」与「进程实际值」之间的可校验通道。
  • 为什么不能把运行参数只放 .env:node --env-file=.env(以及 dotenv)是在进程内部注入的,不进入进程的 execve 初始环境,/proc/<pid>/environ 里看不到这些键;而 PM2 会把调用方父进程的环境合并进应用,且 --env-file 不覆盖已存在的变量。结果是「配置里写的值」与「进程实际拿到的值」可能不一致,却没有可观测通道能发现。写进 env 后,参数在 PM2 spawn 时即确定、/proc 可验证,预检与运行期校验才能闭环。
  • 运行参数变更流程:改 ecosystem env → pm2 restart <ecosystem> --only <pm2Name> --update-env(若 /proc 仍未生效则 pm2 delete + pm2 start)→ 用 /proc/<pid>/environ 确认 PORT 与数据路径键。
  • 密钥变更流程:改 .env → 重启进程 → 验证;不要通过修改 PM2 env 下发密钥(会被进程快照固化,改 .env 不生效)。
  • .env 放 deployPath/.env,权限 600,不提交 git,不打进发布产物,不写入 runbook 明文。
  • .env.example 入库,只写键名、说明与安全默认值。
  • 端口键固定为 PORT;数据路径键优先用应用真实消费的键(DATA_DIR、DATABASE_FILE、UPLOAD_ROOT 等),并在 runbook 第 7 章登记键名与解析后的绝对路径(值脱敏)。
  • 密钥不入库、不入日志、不作为命令行参数、不返回给调用方。

8. 数据与备份契约

8.1 数据

  • dataDir 顶层定义,默认 <deployPath>/data;backup.dataDir 仅作历史兼容,缺省取顶层值。
  • 默认 SQLite,库文件位于 dataDir,可开 WAL。

8.2 备份范围与一致性

备份整个 data/ 目录,app.db 只是其中一部分:

# 1) 先复制 data/ 全量,跳过 SQLite 主库与 WAL/SHM
tar -C "$DATA_DIR" --exclude='*.db' --exclude='*.db-wal' --exclude='*.db-shm' \
  -cf - . | tar -C "$stage" -xf -

# 2) 每个 SQLite 库用在线备份生成一致性副本,并自检
sqlite3 "$DATA_DIR/app.db" ".backup '$stage/app.db'"
sqlite3 "$stage/app.db" "PRAGMA integrity_check;"   # 必须返回 ok

# 3) 打包并原子落盘
tar -C "$stage" -czf "$BACKUP_DIR/{id}-$ts.tar.gz.tmp" .
mv -f "$BACKUP_DIR/{id}-$ts.tar.gz.tmp" "$BACKUP_DIR/{id}-$ts.tar.gz"

禁止:直接 cp / tar 正在写入的 .db(WAL 模式下会得到损坏或落后的副本)。

完整性失败的语义:PRAGMA integrity_check 不为 ok 时,本次备份必须判为失败,不得上传或标记成功。模板 backup.sh 已按 exit 1 实现;平台 doBackup 自后端 2.1.0 起也按 fail 处理(2.0.0 及更早版本的实际行为是仅打印 warn 并继续上传,运维必须人工核对告警日志)。

8.2.1 非 SQLite 数据库(MySQL / PostgreSQL)

8.2 的 data/ 打包只覆盖文件型数据。使用 MySQL/Postgres 的项目,数据库不在 dataDir 内,平台备份包不包含业务数据,必须按本节补导出。

显式前置条件:本节依赖「平台在 tar 打包 dataDir 之前先执行 backup.triggerCmd」这一现有执行顺序(见 §10 与备份调度器实现)。如果后端改为并行执行、跳过 triggerCmd,或在 triggerCmd 失败后仍继续打包上传,本节失效,必须重新评审。triggerCmd 非零退出时平台当前只告警、不中断打包,因此导出成功与否必须在脚本内部自行保证并落盘可验证标记。

  1. 导出责任:项目自己的 /opt/ops-scripts/{id}/backup.sh 负责导出数据库;平台只打包 dataDir,不直连数据库。
  2. 导出落点:导出文件必须写入 dataDir 下的专用子目录(建议 $DATA_DIR/db-dump/),先写 .tmp 再原子 mv,避免平台 tar 到半个文件。
  3. 一致性参数: - MySQL:mysqldump --single-transaction --quick --routines --triggers(InnoDB);需要强一致快照时从只读副本导出。 - PostgreSQL:pg_dump --format=custom --no-owner;全实例级备份(pg_basebackup)单独由运维评估。
  4. 凭据:数据库口令只从 deployPath/.env 读取,不得出现在命令行参数、脚本明文或日志中(避免 ps 泄露)。
  5. 校验:导出后检查退出码、文件非空、格式可识别(如 pg_restore --list);任何一项失败即 exit 1,并在 dataDir 写入失败标记供监控识别。
  6. 保留:db-dump/ 只保留最新一份工作文件,历史由平台备份包承载;本地与远端目标仍按 8.3 各自保留 7 份,避免 data/ 无限增长。
  7. 恢复:runbook 第 4 章必须写明取哪份包、解出 dump、恢复到哪个实例、恢复后如何校验关键表行数;每季度至少演练一次。
  8. 未覆盖即视为无备份:未按本节导出的 MySQL/Postgres 项目,不具备可恢复备份;登记时不得把「备份已开启」当作数据保护已完成的依据。

8.3 备份目标与保留

备份目标是三层解析:项目级 backup.provider → 全局 OPS_BACKUP_PROVIDER → 本地,默认 本地。

  • 本地目标:落 OPS_BACKUP_LOCAL_DIR(建议指向独立数据盘,如 /data/ops-backups,避免与系统盘/发布目录争空间);未配置时由平台按自身数据目录落盘。保留最近 7 份,按时间倒序清理。
  • 打包时只排除目录级 logs/,不要全局排除 *.log——否则 data/ 内的日志类数据文件会被漏掉。
  • 远端目标(oss / cos):必须先在后台「存储设置」完成配置并通过连接测试才可用。未配置、测试失败或凭据失效时,备份请求 fail-closed:返回 409 BACKUP_TARGET_UNAVAILABLE,不会静默退回本地。升级到新备份模型后必须点一次连接测试,否则远端目标一律视为不可用。
  • 项目级覆盖:backup.provider 取 ""(跟随全局)/ local / oss / cos;只有显式填远端时才强制走远端,填 local 可让单个项目绕开全局远端。
  • 按来源保留:本地与各远端目标各自计数、各自清理(前缀 backups/{id}/),切换目标不会删除另一来源的历史包;每个来源保留最新 7 份。
  • 按来源查看/下载/恢复:GET /api/projects/{id}/backups?provider=<local|oss|cos> 列出该来源的包,GET /api/oss/download/{key}?provider=... 下载,POST /api/projects/{id}/backups/restore 指定来源与对象恢复;远端下载优先使用短期预签名 URL,不经 ops 公网带宽。
  • 项目侧只产出本地一致性快照,不要自建第二条上传链路(双跑会产生重复备份与保留错乱)。
  • 静态资源前缀 assets/{id}/,可选。

8.4 定时任务

  • 备份与健康检查的 crontab 由平台/运维登记,项目侧不重复注册同类任务。
  • monitor.sh 建议 */5 * * * *;异常只写日志并 exit 1,不做任何重启动作。

9. runbook 必备章节(8 章,缺一章即 warn)

# ${SERVICE_NAME} 运维手册
## 1. 服务概览(PM2 名、端口、目录、运行用户、依赖)
## 2. 域名与反代(域名、Caddy 站点块、证书 SAN)
## 3. 启停与部署(命令、部署脚本、回滚步骤)
## 4. 数据与备份(DB 路径、backup.sh 位置、备份目标与保留策略)
## 5. 健康检查与监控(${PROBE_PATH}、monitor.sh、crontab)
## 6. 日志位置与排查(pm2 logs、应用日志、审计)
## 7. 关键环境变量(脱敏)
## 8. 已知风险与注意事项
  • 路径 /opt/runbooks/{id}.md,UTF-8,≤200KB。
  • 第 7 章只列键名与用途,值一律脱敏。
  • 第 3 章必须能按步骤复现「部署 + 回滚」,不能只写「见 CI」。
  • 第 1、3、4 章禁止写死具体 release 或版本号(如 releases/20261109-030000、v1.4.2),它们会随每次部署过期。统一用 readlink -f <deployPath>/current 或读 current/package.json 的 version 动态获取;示意写法:readlink -f /opt/apps/{id}/current。
  • 占位符统一为 ${VAR} 一种写法(如 ${SERVICE_NAME}、${PM2_NAME}、${PORT}、${PROBE_PATH}),不再使用 {service} 等变体。

10. projects.json 字段

必填:id、name、pm2Name、localPort、runbookPath、probePath。schema 已设 additionalProperties=false,未列出的字段(含拼写错误)一律报错。probePath 必填是为了强制显式确认,登记表单与迁移脚本自动填入默认值 /healthz。

字段 必填 默认 含义与校验
id 是 — ^[a-z][a-z0-9-]{1,31}$,全局唯一,不得为 ops
name 是 — 展示名,1–40 字符
pm2Name 是 — 必须存在于 pm2 jlist,全局唯一,不得为保留名
localPort 是 — 0–65535,0 表示无端口服务;其余值不得与预留端口冲突。3000–3999 只是建议段:登记不因超出该区间失败,部署规划仅产生一条 warning。真正参与硬校验的是登记范围 0–65535 与规划接口要求的 1024–65535;与登记值不一致时才报错
runbookPath 是 — 绝对路径,标准位置 <OPS_RUNBOOKS_DIR>/{id}.md(默认 /opt/runbooks/{id}.md),存在且 ≤200KB。项目 Key 可读自己项目的 runbook;文件不存在返回 404 RUNBOOK_NOT_FOUND,路径越界返回 403 RUNBOOK_PATH_NOT_ALLOWED
probePath 是 /healthz 探测路径,必须返回 2xx/3xx
dataDir 否 <deployPath>/data 登记的数据目录(顶层唯一定义)。运行时由 DATA_DIR 或应用真实消费的键(DATABASE_FILE、UPLOAD_ROOT 等)表达;两者解析后都必须落在该目录内(DATA_DIR 的完整校验自后端 2.1.0 起生效)。不得位于 releases/、current 中;stateless: true 的项目不要求该目录存在
stateless 否 false true = 显式声明无持久化状态:不要求数据键、不校验 dataDir,判定优先级最高。声明后平台会忽略数据键,不会因「互斥」报错;若项目仍有本地持久化文件,必须去掉该字段并补真实数据键
standardVersion 否 1.1.0 项目登记的标准版本,只由显式登记/adopt 变更;与平台当前版本不是同一个概念。ops_list_projects 额外返回 registeredStandardVersion(登记值)与 platformStandardVersion(平台值),旧字段保留为登记值以兼容
healthCheck.enabled 否 true 是否纳入 healthy 判定
environment 否 production 环境标识,避免测试服务混入健康判定
owner 否 "" 负责人,用于审计与告警
managed 否 true false 时只展示,禁止任何控制动作
downloadSync 否 false 是否把上传同步到 /var/www/apk/;仅安装包类项目置 true
deployPath(派生,非 schema 字段) — 取 backup.appDir 不写入 projects.json,additionalProperties=false 下写入会被判非法字段。实际值取登记的 backup.appDir;未登记时由 ops_get_project_context 与规划器统一返回 <OPS_DEPLOY_ROOT>/<id>(默认 /opt/apps/{id},deployPathSource:"default"),不写回登记。与 PM2 cwd 必须逐字一致;规划器与 conformance 使用同一口径,不再用 /opt/{id} 对照
backup.appDir 开启备份时必填 — 历史兼容别名,实际对应 deployPath,不作为备份内容根(备份范围以顶层 dataDir 为准)。绝对路径且存在,不得为 /、/opt、/etc、/var/www;新项目填 <OPS_DEPLOY_ROOT>/<id>(默认 /opt/apps/{id})。计划在下一个 minor 版本改为可选并废弃
backup.dataDir 否 取顶层 dataDir 历史兼容覆盖字段
backup.provider 否 "" 项目级备份目标覆盖:"" 跟随全局 OPS_BACKUP_PROVIDER,或 local / oss / cos。远端目标必须已配置且连接测试通过,否则备份 fail-closed(409 BACKUP_TARGET_UNAVAILABLE),不退回本地
backup.triggerCmd 否 — 绝对路径可执行文件,不含 ; | & $ \ ` > < 等 shell 元字符;标准值 /opt/ops-scripts/{id}/backup.sh
backup.schedule.enabled 否 false 是否启用定时备份
backup.schedule.cron 启用时必填 0 3 * * * 5 段标准 cron,需通过 cron.validate
backup.schedule.timezone 否 Asia/Shanghai 时区
oss.assetPrefix 否 assets/{id}/ 该服务的 OSS 资源前缀
webUrl / adminUrl 否 — 对外访问入口;有域名站点块时 webUrl 必填
description / icon 否 — 展示用
onboardedAt / onboardedBy 否 — 接入审计元数据

存量迁移提醒:probePath 已成为必填,上线 schema 收紧前必须由 ensureProjectDefaults() 为老条目补齐,否则旧数据直接判 fail。


11. 发布产物形态(v1 定稿)

dist/
├── demo-api-1.0.0.tar.gz          应用与依赖(不含 data/、.env、backups/)
└── demo-api-1.0.0.tar.gz.sha256   内容:<64位小写sha256>  demo-api-1.0.0.tar.gz
项 规范
包名 {id}-{version}.tar.gz;version 匹配 ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$
校验文件 同名 {id}-{version}.tar.gz.sha256,一行,<64位小写hex> <文件名>(两空格,兼容 sha256sum -c)
包内结构 解包后根目录即应用根:package.json、入口文件(server.js 或 dist/)、node_modules/ 或依赖锁文件 + 离线缓存
包内禁止 .env、data/、backups/、logs/、.git/、releases/、current
启动配置 推荐不打包,由平台按登记渲染。可选自带 ecosystem.config.{js,cjs,mjs,json}:部署目录没有配置时平台会复制并采用;部署目录已有配置时以部署目录为准
落盘 解包到 {deployPath}/releases/{YYYYMMDD-HHMMSS}/,current 软链指向它,保留最近 3 个 release
  • 发布产物只有两个文件:压缩包与同名 .sha256。除此之外不要求、也不读取任何附加文件。
  • 打包必须用项目内固定 build 脚本,一条命令产出 {id}-{version}.tar.gz 与 .sha256。脚本至少做三件事:
    1. 禁项扫描:确认包内无 .env、data/、backups/、logs/、.git/、releases/、current,命中即失败退出(ecosystem.config.* 可选,不作禁项)。
    2. 软链检查:拒绝指向包外的符号链接与绝对路径条目,避免解包逃逸。
    3. 解包启动自检:在临时目录解包,用随机空闲端口 + 临时数据目录真实启动,轮询 /healthz(或登记 probePath)返回 2xx/3xx 后再清理退出;启动失败即不产出产物。
  • 反例:靠人记 tar 参数打包。手工 tar czf 容易漏排除项、把 .env 或 data/ 打进去、忘记生成 .sha256,且打包前从不验证能否启动——问题全部推迟到生产部署时才暴露。build 脚本应入库,并把上述检查作为脚本的一部分,而不是写在文档里靠人执行。
  • conformance 是实时计算,不是交付物:ops_check_conformance 每次都重新拉项目登记、查进程、探端口、读 Caddy 现场比对;conformance.json 不是发布产物,也不是部署前置条件——缺失它不会阻止部署。静态快照会立即过期,反而误导判断,因此不再要求,也不再归档。需要体检结论时直接调用该工具。
  • 默认不联网:优先内置 node_modules,或在服务器保留离线缓存后执行 npm ci --omit=dev --offline。
  • 部署流程(2.11.2 起已实现):上传 → 校验 sha256 与大小 → staging 解包(拒绝绝对路径、..、符号链接) → releases/{ts} → 入口探测 / 启动配置解析或渲染 → 切换 current → 启动/重启 PM2 → 轮询 probePath → 失败自动回滚上一 release。
  • 保留数量:releases/ 仅保留最近 3 个(OPS_DEPLOY_KEEP_RELEASES,默认 3);更早的 release 由平台清理,显式回滚只能指向仍存在的目录。清理由平台执行,项目方只需在回滚前确认目标 release 仍在。
  • 首次部署失败(keep-current):当项目只有一个 release、没有可切回的上一版本时,部署失败不会删除入口:current 仍指向新 release,平台标记本次失败并记录 strategy=keep-current。此时没有「旧版本」可回退,必须人工处置:修复后重新发布,或确认可接受后保留现状。该策略只保证入口不消失,不代表新版本可用。
  • 带迁移的发布:部署前必须按第 19 章判定迁移兼容性;current 的自动切换不等于数据库可回退,回滚语义见第 19 章。
  • 部署前必须 conformance fail === 0;warn 允许但写审计。

11.2 启动配置渲染与入口探测(1.2.6 / 服务端 2.11.2)

平台按以下优先级决定本次部署使用哪份启动配置,结论写入审计事件 project.ecosystem.render,并在部署响应/dryRun 的 ecosystem 字段回显:

优先级 来源 行为
1 部署目录已有 ecosystem.config.{js,cjs,mjs,json} 直接使用;projects.json 的 ecosystemMode="render" 才强制重渲染,覆盖前备份为 *.bak-<YYYYMMDDHHMMSS>
2 产物根目录自带上述配置 部署目录没有配置时复制并采用(部署目录已有则仍用部署目录的)
3 按登记渲染 写入 {deployPath}/ecosystem.config.cjs,name=pm2Name、cwd=deployPath、script=current/<入口>、env 含 PORT(= localPort,0 则省略)与 DATA_DIR(stateless=true 时省略),并 autorestart:true / instances:1 / exec_mode:"fork" / max_memory_restart:"256M"

入口探测优先级(按顺序,命中即停):

  1. projects.json 的 entry(相对产物根;显式覆盖)
  2. package.json → scripts.start(仅接受可直接交给 node 的命令:node [--flags] <file> 或纯文件路径;npm/pnpm/pm2/next 等包装命令不猜)
  3. package.json → main
  4. 常见入口:server.js、index.js、app.js、main.js、dist/server.js、dist/index.js、src/server.js、src/index.js、build/server.js、server/index.js、app/server.js
  5. 静态站点:dist/index.html、build/index.html、public/index.html、out/index.html、web/dist/index.html,以及根目录 index.html(平台会写入 current/.ops/static-server.cjs 并提供 /healthz)

探测不到直接失败(ENTRY_DETECT_FAILED),返回 detail 含已尝试的候选路径与原因,suggestion 给出修复方式;不会猜一个「可能能跑」的入口。项目侧可补: - projects.json 显式 entry(如 server.js、dist/server.js); - 或 package.json 的 scripts.start / main 指向真实存在的文件; - 或提供上述静态站点目录之一。

dryRun 只在临时解包目录生成 .ops/ecosystem.preview.cjs 预览,不写部署目录;返回 ecosystem.contentPreview 可直接查看渲染结果。


12. 最小 Node.js 骨架(30 行内,含健康检查、env 端口、SIGTERM)

'use strict';
const http = require('node:http');
const path = require('node:path');
const PORT = Number(process.env.PORT || 3000);                    // 端口只来自环境变量
const HOST = '127.0.0.1';                                         // 禁止 0.0.0.0
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data'); // cwd=deployPath

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/healthz') {           // 路径需与 probePath 一致
    res.writeHead(200, { 'content-type': 'application/json' });
    return res.end(JSON.stringify({ status: 'ok' }));
  }
  res.writeHead(404, { 'content-type': 'application/json' });
  res.end(JSON.stringify({ error: 'not_found' }));
});

server.listen(PORT, HOST, () => {
  console.log(JSON.stringify({ t: new Date().toISOString(), msg: 'listening', host: HOST, port: PORT, dataDir: DATA_DIR }));
});

let closing = false;
const shutdown = (signal) => {
  if (closing) return;
  closing = true;
  console.log(JSON.stringify({ t: new Date().toISOString(), msg: 'shutdown', signal }));
  server.close(() => process.exit(0));            // 停止新请求,等待在途请求结束
  setTimeout(() => process.exit(0), 10000).unref(); // 兜底强制退出
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

配套:PORT/HOST 与数据路径键由 ecosystem env 注入,应用只读 process.env,不得硬编码;.env 只放密钥,入口仍可用 node --env-file=.env 或首行 require('dotenv').config() 加载(--env-file 不覆盖 env 已注入的变量)。示例用的是 DATA_DIR;若应用实际读 DATABASE_FILE / UPLOAD_ROOT,改成对应键,只要解析后落在登记 dataDir 内(见 7.2)。数据文件一律写在 dataDir 内。


13. 验收清单(逐条自检)

清单分两组:13.1 由项目方自己勾(项目仓库内、项目进程内能完成的事);13.2 由平台执行,项目方只做确认(不要试图自己配 DNS、改证书或写 Caddy)。技术口径与编号引用保持不变。

优先级:每条开头标注档位。必检 = 不满足会导致部署失败、被平台拒绝或线上故障;建议检 = 不满足不阻断部署,但会削弱可观测性、可维护性或排障效率。13.1 共 31 条:必检 23 条,建议检 8 条;时间紧时至少把「必检」全部走一遍。

权限前提:项目方通常只持有项目 Key,不能执行 pm2、ss、ls releases/、读 /proc 等需要服务器 shell 的操作。这类核对一律改为「确认平台接口返回值」;文中出现的命令保留给管理员核对用。

13.1 项目方必须完成

代码与运行

  • [必检] ecosystem env 显式写全 PORT(无端口服务除外)、HOST、数据路径键(DATA_DIR 或应用真实消费的 DATABASE_FILE/UPLOAD_ROOT 等;无持久化状态在 projects.json 写 stateless: true),有前端产物含 WEB_DIST;与登记值一致
  • [必检] 由 ops_check_conformance 判定 env.*:PORT、数据路径键与 ecosystem env 一致,且数据键与 DATA_DIR 解析后落在登记 dataDir 内(DATA_DIR 完整校验依赖后端 ≥ 2.1.0)。项目方确认返回的 env.* 结果与预期一致;/proc/<pid>/environ 比对需服务器权限,仅供管理员核对
  • [建议检] requestTimeout 不早于反代 read_timeout/write_timeout,且未把 requestTimeout 当作响应流超时
  • [必检] .env 只放密钥(权限 600),PM2 env 中无任何密钥
  • [必检] 端口读自 PORT,host 固定 127.0.0.1,代码中无 0.0.0.0
  • [必检] curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:{port}{probePath} 返回 2xx/3xx(本地或部署后由平台探测结果确认)
  • [建议检] 健康检查在依赖故障时返回 503,不以 200 掩盖
  • [必检] 实现 SIGTERM:收到信号后正常退出、无锁文件残留、退出码 0
  • [建议检] 日志只写 stdout/stderr,无自建轮转文件
  • [必检] 若自带 ecosystem.config.*:name = pm2Name、cwd = deployPath、autorestart: true;未自带则由平台渲染,部署响应 ecosystem 字段会回显来源与入口
  • [建议检] 项目方确认登记字段与平台 conformance 返回一致(pm2 状态、pm_cwd)。pm2 describe {pm2Name} 需服务器权限,保留给管理员核对
  • [必检] engines 声明与 ecosystem interpreter 指向的 Node 大版本一致(见 20.3)

数据与备份

  • [必检] 数据路径不在 releases/ 或 current/ 下:确认 ops_check_conformance 的 config.* 无 fail。realpath 比对需服务器权限,供管理员核对
  • [必检] 备份包覆盖整个 data/,SQLite 副本 PRAGMA integrity_check 返回 ok;integrity 失败即视为备份失败(平台自后端 2.1.0 起按 fail 处理),MySQL/Postgres 包内包含数据库导出与校验结果(见 8.2.1)
  • [建议检] runbook 第 4 章写明迁移方式、迁移失败处置与数据库恢复步骤
  • [必检] 发布说明标注本次迁移为「向后兼容」或「破坏性」;破坏性迁移不依赖自动回滚(见第 19 章)
  • [必检] backup.sh、monitor.sh 的内容由项目方提供,并保证脚本语法正确、具备可执行权限(本地 bash -n 与 chmod +x 检查);落位到 /opt/ops-scripts/{id}/ 由平台执行,项目方确认平台接口可见
  • [必检] .env 权限 600,未出现在 git 与发布包中

登记与文档

  • [必检] id 合规且非 ops,pm2Name 唯一且非保留名
  • [必检] 项目 Key 未进群聊、仓库、日志或截图;只存于本机安全位置
  • [必检] probePath 与实际健康检查路径一致
  • [建议检] runbookPath 指向 /opt/runbooks/{id}.md,8 章齐全、≤200KB
  • [建议检] runbook 未写死 release 名或版本号,均用 readlink -f <deployPath>/current 或 current/package.json 动态获取
  • [必检] dataDir 为绝对路径且存在(stateless: true 的项目除外);backup.appDir 填 <OPS_DEPLOY_ROOT>/{id}(默认 /opt/apps/{id}),按 deployPath 别名理解
  • [必检] 登记的 standardVersion 与平台当前声明一致(以 manifest.json 或接口返回为准,不要写死具体版本号)

交付与接入

  • [必检] 公网入口同时携带客户端 Token 与 X-Ops-Project-Key;无 Key 时只读标准资源
  • [建议检] 项目 Key 下 ops_list_projects 只返回绑定项目,跨项目调用返回 403
  • [必检] ops_check_conformance 无 fail;warn 已记录;unknown 已人工核对
  • [必检] 发布产物为 {id}-{version}.tar.gz,同名 .sha256 可用 sha256sum -c 校验
  • [必检] 产物由项目内固定 build 脚本一条命令产出;该脚本含禁项扫描、软链检查、解包后随机端口 + 临时数据目录启动自检
  • [必检] 包内无 .env / data/ / backups/ / logs/ / .git/ / releases/ / current(ecosystem.config.* 可选)

13.2 平台方完成,项目方仅需确认

以下条目由平台执行,项目方不要自己动手;确认方式写的是“你如何验证它已生效”,不是“你如何去配置”。

事项 执行方 技术口径(不变) 项目方确认方式
DNS 解析 平台 / 运维 域名 DNS 指向 47.110.36.244 dig +short <domain> 返回该 IP
证书 平台 / 运维 静态证书 /etc/caddy/certs/fullchain.pem 的 SAN 覆盖该域名 curl -fsS https://<domain>{probePath} 无证书告警;openssl s_client -connect <domain>:443 可验证 SAN
Caddy 站点块 平台 / 运维 仅包含本项目域名,reverse_proxy 127.0.0.1:{port} 从公网访问 https://<domain>{probePath} 返回 2xx/3xx,且未串到其他站点
PM2 托管 平台 进程以 pm2Name 常驻,autorestart: true 确认 ops_check_conformance 返回的进程状态为 online;pm2 describe {pm2Name} 为管理员核对手段
项目登记与 Key 签发 平台 / superadmin 管理员全量冲突预检后登记 projects.json,通过 POST /api/keys 签发 deploy/readonly Key ops_get_project_context 返回本项目;ops_list_projects 仅返回本项目
端口与域名冲突预检 平台 / 管理员 用全量视图检查端口、域名、pm2Name、项目 id 部署前由管理员给出无冲突结论(项目 Key 下 ops_check_conflict 会漏报)
备份落盘与保留 平台 按生效目标(项目 backup.provider → 全局 OPS_BACKUP_PROVIDER → local)落盘,每个来源保留最新 7 份;远端目标需连接测试通过,否则 409 BACKUP_TARGET_UNAVAILABLE ops_list_backups 能看到当前来源的新对象;数量为 7
备份 cron 登记 平台 schedule.enabled / cron / timezone(Asia/Shanghai) 后台或 ops_get_project_context 确认 schedule 已启用且时区正确
release 清理 平台 releases/ 仅保留最近 3 个(OPS_DEPLOY_KEEP_RELEASES,默认 3) 连续部署后确认平台报告的保留数量为 3,且回滚目标仍在
审计日志 平台 记录登记、备份、部署(或接入)动作 后台审计视图能查到对应记录

14. 常见失败原因与反例

现象 根因 正确做法
端口从公网可直连,绕过 TLS 与限流 监听 0.0.0.0 固定 127.0.0.1;对外只经 Caddy
部署后数据丢失 data/ 放在 releases/ 或 current 内 数据目录独立于发布目录(R7,见 2.5,fail 级)
Caddy 502,但进程 online 端口硬编码与登记 localPort 不一致 端口只读 PORT,登记值与实际一致
健康检查 404 却显示健康 探测路径错误或旧版把 404 当存活 probePath 必须返回 2xx/3xx,否则判 fail
改 .env 不生效 密钥通过 PM2 env 注入并被进程快照固化 密钥只放 .env,改后重启进程
进程实际端口/数据目录与配置不一致 运行参数只写在 .env,被 PM2 合并的父进程环境(如 PORT=3100)覆盖,且 /proc 看不到、无法校验 PORT/HOST 与应用实际消费的数据路径键写入 ecosystem env 作为唯一真源,再用 /proc/<pid>/environ 复核
预检显示数据目录已配置,实际却没人读 为了满足「必须有 DATA_DIR」硬填一个应用从不读的键 按 7.2 写应用真实消费的键(DATABASE_FILE、UPLOAD_ROOT 等),并校验解析后落在 dataDir 内
无持久化服务被要求提供数据目录 未声明无状态,或误以为必须填 DATA_DIR projects.json 写 stateless: true,并移除数据键与 dataDir
在 .env 里写数据键,却发现运行时校验看不到 .env 是进程内注入,不进 execve 环境,/proc 不可见 数据键写进 ecosystem env;.env 仅作补充发现
502/504,或长任务被提前切断 requestTimeout 短于反代 read_timeout/write_timeout;或把 requestTimeout 当作响应流超时 应用超时不早于反代;流式/长任务按业务最长耗时单独设计
每次部署都要手工改 runbook runbook 写死了 current 指向的 release 或版本号 用 readlink -f <deployPath>/current 或读 current/package.json
产物夹带 .env/data/,或打包后才发现在生产起不来 靠人记 tar 参数手工打包,无禁项扫描与启动自检 用项目内固定 build 脚本一条命令出包,内置扫描与解包启动验证
重启时请求失败、SQLite 锁残留 未处理 SIGTERM 实现优雅退出,kill_timeout 留足时间
备份损坏或丢数据 直接 cp/tar 正在写的 .db 使用 sqlite3 ".backup"
备份包缺少 data/ 内非 DB 文件 只备份了 app.db 备份整个 data/,DB 单独用 .backup 覆盖
备份包缺少 data/ 内日志 打包时全局排除 *.log 只排除目录级 logs/
备份突然停止且无告警 backup.sh 放在项目 git 目录被覆盖 脚本放 /opt/ops-scripts/{id}/
磁盘被日志写满 应用自建轮转或无限追加文件 只写 stdout,交给 PM2 日志轮转
部署后进程起不来 产物不含依赖,且默认不联网 内置 node_modules 或使用离线缓存
产物被拒收 缺少 .sha256 或文件名不合规 按 {id}-{version}.tar.gz + 同名校验文件出包
修改 Caddy 后全站异常 直接改整份 Caddyfile 且未 validate 只增改本项目站点块,validate → 备份 → reload,失败回滚
登记被拒 id 使用 ops,或 pm2Name 使用保留名 更换合规标识
字段拼写错误却未报错 旧版 schema 允许额外字段 当前 additionalProperties=false,按字段表登记
引入密钥泄露 密钥进 git / 日志 / runbook / 命令行参数 只放 600 的 .env,日志与响应脱敏
公网入口所有项目工具都报 PROJECT_KEY_REQUIRED 只带了客户端 Token,没带 X-Ops-Project-Key 先找管理员签发项目 Key,再放入请求头
项目 Key 调用直接 400 INVALID_PROJECT_KEY 头格式错误、重复数组或超过 128 字节 只放单个 ops_pt_<projectId>_<random> 值
项目 Key 突然 401 过期、被吊销、一次性 Key 已用过,或绑定项目已删除 重新向管理员申请,不要复用旧 Key
冲突预检显示无冲突,接入后却撞端口/域名 用项目 Key 跑 ops_check_conflict,只看到本项目 冲突检测必须由管理员用全量视图完成
启动报 duplicate column / 迁移重复执行 迁移无记录表或记录表与文件不对齐,失败后盲目重跑 按第 18 章建立 schema_migrations,迁移语句可重复执行,失败即中止启动
回滚到旧代码后接口 500 / 字段不存在 数据库已前进,旧代码不认识新 schema,仍自动切回上一 release 破坏性迁移禁止自动回滚,只能前进修复;见第 19 章
迁移失败但服务仍在跑 迁移在 listen() 之后执行,或失败被吞掉 启动前执行,失败 exit 非零,绝不带着半套 schema 提供服务

15. 版本与变更

  • 当前标准版本 1.2.5;资源索引、minServerVersion(2.1.0)与 digest 覆盖范围见 standards/manifest.json 与 15.1。
  • 1.0.0(2026-11-09)权限模型变更:公网入口改为「客户端 Token + 项目 Key」两层凭据;无 Key 只能读标准文档资源,所有项目工具返回 PROJECT_KEY_REQUIRED;冲突检测与新项目登记只能由管理员完成。旧版权限描述作废,以 1.1、1.2 为准。
  • 1.0.0(2026-11-09)产物要求收敛:conformance.json 不再是发布产物,也不参与门禁判定;ops_check_conformance 实时计算,静态快照会过期。发布产物只有压缩包与同名 .sha256。
  • 1.0.0(2026-11-11)数据目录表达放宽:数据路径不再强制 DATA_DIR;可用应用真实消费的键(DATABASE_FILE、UPLOAD_ROOT 等),解析后落在登记 dataDir 内即可;校验跟着实际消费的键走。禁止为满足字段硬填无人读取的假键。
  • 1.0.0(2026-11-11)实战经验补充:新增超时口径(requestTimeout 与反代 read_timeout/write_timeout 的关系、不约束响应流)、项目内固定 build 脚本要求(禁项扫描、软链检查、解包启动自检)、runbook 禁止写死 release/版本号。
  • 1.0.0(2026-11-11)新增 stateless 字段:projects.json 与 ops_plan_deployment 输入均支持可选 stateless: boolean(默认 false,不进 required)。数据目录判定顺序为 stateless → DATA_DIR → ecosystem 中 _FILE/_DIR/_PATH/_ROOT/_DB 类键 → 项目根 .env 补充发现 → 都没有则拒绝;.env 来源的键不参与 /proc 比对。
  • 1.1.0(2026-11-12)运行时契约补全:新增 2.5,正式定义 R1–R7;修正 2.4、14 的悬空引用;R6 按现行环境模型改为「非密钥参数走 ecosystem env,密钥走 .env」。
  • 1.1.0(2026-11-12)数据目录校验收紧:DATA_DIR 与具体数据键一样,解析后必须落在登记 dataDir 内;自后端 2.1.0 起生效(2.0.0 为「存在即通过」)。该收紧会使部分存量项目由通过变失败,故按 minor 发布。
  • 1.1.0(2026-11-12)迁移规则:新增第 18、19 章,覆盖迁移文件位置与命名、记录表、幂等、失败中止、禁止人工补记录、带迁移回滚与 forward fix。
  • 1.1.0(2026-11-12)一致性修正:integrity_check 统一 fail 语义(后端 ≥ 2.1.0,2.0.0 为 warn 后继续上传);backup.appDir 明确为 deployPath 历史别名;stateless 不再表述为「强制互斥」;13 章与 7.2 C 对齐。
  • 1.1.0(2026-11-12)回滚风险明示:第 19 章明确执行器当前无条件 switch-back、完全不感知迁移;破坏性迁移必须人工闸门,迁移元数据自动判定仅作为未来建议,不构成现行要求。
  • 1.2.0(2026-11-12)新增运行时与依赖基线:新增第 20 章,按「平台自身依赖 / 项目必须满足 / 平台保证项 / 建议项」四类划分约束力。B 层只保留项目真正能决定的两项——Node.js 大版本、原生模块工具链;磁盘、内存、Caddy、PM2、SQLite CLI、glibc 运行时、OS/systemd 归入平台保证项,保留失败表现仅作排障线索。不写死补丁号,Node 推荐值表述为「22.x LTS,以平台现网为准」。补充项目侧 engines 声明、构建脚本 Node 大版本自检与原生模块重编前置检查。纯新增说明,不改变既有判定行为,minServerVersion 保持 2.1.0。
  • 1.2.0(2026-11-12)规范站构建可复现:develop-site/requirements.txt 固定 markdown==3.9;build.py --check 校验运行版本与钉版本一致;发布记录需同时包含构建依赖版本与四个文件的 sha256。
  • 1.2.0(2026-11-12)验收清单归属与优先级修正:第 13 章拆为「13.1 项目方必须完成」与「13.2 平台方完成,项目方仅需确认」。DNS、证书 SAN、Caddy 站点块、PM2 托管、项目登记与 Key 签发、端口冲突预检、备份上传与保留、备份 cron、release 清理、审计日志移入平台组,并给出「项目方确认方式」;/proc 比对、pm2 describe、脚本落位、realpath、ss、ls releases/ 等需服务器权限的核对统一改为「确认平台返回值」,命令保留给管理员。13.1 共 31 条按「必检 23 / 建议检 8」标注档位(判据:不满足是否导致部署失败或线上故障)。技术口径与编号引用不变;standardVersion 验收项改为「与平台当前声明一致(以 manifest.json 或接口返回为准)」,不再写死版本号。第 1 节证书行与第 11 章 release 保留同步补执行方说明。
  • 1.2.1(2026-11-12)digest 校验盲区修正:此前 digest 只覆盖 manifest.resources 的 14 个文件,规范正文 llms.txt、version.json、manifest.json 都不在集合内,导致 1.1.0 与 1.2.0 正文已变而 digest 仍是 sha256:6b4b4350…。现把 llms.txt 与 version.json 纳入 resources,正文或版本元数据的任何改动都会改变 digest;manifest.json 因自引用仍不参与,其版本字段由 version.json 镜像并在启动时校验一致。minServerVersion 保持 2.1.0,判定行为不变。
  • 1.2.2(2026-11-14)备份目标模型:备份目标改为「项目级 backup.provider → 全局 OPS_BACKUP_PROVIDER → 本地」,默认从 oss 改为 local;新增本地目标实现(OPS_BACKUP_LOCAL_DIR);远端(oss/cos)必须已配置且连接测试通过才可用,否则 fail-closed(409 BACKUP_TARGET_UNAVAILABLE,不退回本地);projects.schema.json 的 backup 新增可选 provider(""/local/oss/cos);保留份数按来源各自计数;新增按来源列出、下载、恢复接口。minServerVersion 保持 2.1.0(不强制校验),但新语义仅在后端包含本次改动后生效,升级后需在「存储设置」点一次连接测试。
  • 1.2.6(2026-11-14)首次部署自动启动配置:部署执行器新增入口探测与 ecosystem.config.cjs 渲染(部署目录已有 > 产物自带 > 按登记渲染,覆盖前备份),dryRun 返回 ecosystem.contentPreview;探测优先级为登记 entry → package.json scripts.start → main → 常见入口 → 静态站点,探测不到即 ENTRY_DETECT_FAILED(附候选与建议);渲染/探测写审计 project.ecosystem.render。projects.json 新增可选 entry / ecosystemMode(auto|render);产物可自带 ecosystem.config.*(不再列为禁项)。规范 §11.2 与 §2.3 已同步。需服务端 2.11.2;minServerVersion 保持 2.1.0(不强制校验)。
  • 1.2.5(2026-11-14)冷启动口径修正:① 规范站正文头部版本/生成日期改由构建脚本从 manifest.json 渲染,发布前断言首页、llms.txt、version.json、manifest.json 四份版本一致,不一致即构建失败;② 正文点名的 /standards/version.json、/standards/manifest.json、/v1/manifest.json 现在真实可访问,opsstd://v1/... 给出到 HTTPS 路径的映射;③ ops_check_conflict 对已登记项目自身的命中标 self:true 并从 ok 判定中排除,更新部署可直接放行;④ ops_get_project_context 内嵌 conformance 复用 ops_check_conformance 同一实现(含首次纳管 deferred),并返回 snapshotAt / rulesVersion;⑤ caddy.site_block 的 actual 区分「无站点块 / 有块无路由 / 有路由但目标不符」,caddy.domain_unique 与它同源;⑥ 首次部署把 caddy.site_block、caddy.proxy_target 一并 deferred,平台在部署成功后写入站点块并立即复检,已部署项目仍按 fail 拦截;⑦ 项目 Key 可读自己项目的 runbook,runbook 目录由 OPS_RUNBOOKS_DIR 配置(默认 /opt/runbooks),文件不存在返回 404 而非 403;⑧ ops_list_projects 与 context 增加 registeredStandardVersion / platformStandardVersion 显式字段(旧 standardVersion 保留兼容);⑨ ops_plan_deployment 对 stateless 或显式空密钥的项目把 .env 步骤标 applicable:false。需服务端 2.11.1;minServerVersion 保持 2.1.0(不强制校验)。
  • 1.2.4(2026-11-14)产物上传与部署闭环:新增项目产物上传通道 POST /artifacts(客户端 Token + X-Ops-Project-Key 两层鉴权)转发到后端 POST /api/project/artifacts,按项目落盘到 <OPS_ARTIFACTS_ROOT>/<projectId>/,文件名白名单清洗、仅接受 .tar.gz/.tgz、默认 200 MiB、每项目保留最近 10 份,返回 artifactPath/size/sha256/uploadedAt;新增 ops_upload_artifact。首次部署(从未有 releases/current)自动把 process.ecosystem_loadable 与 network.port_probe 标为 deferred,部署后立即复检;已部署项目仍按 fail 拦截。项目 Key 只读范围放开到本项目(项目列表、部署状态、inventory、调度、证书、runbook、日志、备份),跨项目与写接口仍 403。路径口径统一为 <OPS_DEPLOY_ROOT>/<id>,规划器/conformance/context 清理 /opt/{id} 残留。ops_plan_deployment 返回 planType=create|update,仅真实冲突 blocked。需服务端 2.11.0;minServerVersion 保持 2.1.0(不强制校验)。
  • 1.2.3(2026-11-14)鉴权与路径口径修订:1.2 补充两层凭据发放入口差异与 401 的 reason 语义(missing/invalid/revoked/expired,code 相同);17.1–17.3 明确客户端 Token 需在「MCP 客户端」单独签发、不属于项目接入材料,新增不依赖客户端的 curl initialize 自检,并说明客户端不展开 ${VAR} 时表现为 reason=invalid;ops_check_conflict 在项目 Key 下的描述改为与实现一致(ok:false + limitedView:true + requiresAdminReview:true);10 补 deployPath 为派生字段(取 backup.appDir,不写入 projects.json),并明确 3000–3999 只是建议段、硬校验为登记 0–65535 与规划 1024–65535;默认部署目录统一为 <OPS_DEPLOY_ROOT>/<id>(默认 /opt/apps/{id},与 install.sh、executor 实现一致);ops_get_project_context 未登记 backup.appDir 时返回空 deployPath,不再猜 /opt/{id}。无后端行为变更,minServerVersion 保持 2.1.0。
  • 版本语义:patch 文案与提示;minor 新增可选字段/模板或行为收紧,向后兼容;major 新增必填项或路径变更,升 v2 且 v1 保留至少两个发行周期。
  • 项目侧只通过显式登记/adopt 变更 standardVersion,不随平台升级自动追随。
  • 任何写入工具在 standardVersion / standardDigest 不一致时必须拒绝(STANDARD_VERSION_MISMATCH),不允许静默降级,也不解析 latest 别名。
  • minServerVersion 当前不强制校验;manifest.json 中 minServerVersionEnforced=false 已注明。
  • 标准内容变更流程:修改 standards/ → CI 校验 schema/模板占位符/示例 → 生成版本与 digest → 随发行包发布;禁止手工改写版本与 digest。发布记录必须包含构建依赖版本(develop-site/requirements.txt)与站点四个文件的 sha256。

15.1 digest 的覆盖范围与用途

算法:对 manifest.resources 中每个文件取内容 sha256,按 uri 字典序拼成 uri + \0 + sha256 + \n,再整体取 sha256,形如 sha256:<hex>。仓库内 manifest.json 的 digest 字段恒为 null,运行时按同一算法重算,禁止手工填写。

覆盖范围:

是否参与 对象 说明
参与 resources 列出的全部文件 含规范正文 llms.txt、version.json、两份 schema、模板、清单等;任一内容变化都会改变 digest
不参与 manifest.json 自身 digest 写在自身内部,自引用无法收敛;其 version / major 由 version.json 镜像,StandardRegistry 启动时校验两者一致,不一致直接抛 VALIDATION_ERROR
不参与 resources[].title / description / mime 只改元数据不会改变 digest,因此改元数据也必须升版本号
不参与 standards/ 下未登记为资源的文件 新增受控文件必须同时登记进 resources,否则等于没有校验

用途:证明「你读到的正文与平台登记的版本是同一份」,用于工具响应里的 standardDigest、写入前的版本比对与审计留痕。它不是发布包签名,也不是防篡改证明——同一路径下的文件被替换后再重算 digest,摘要同样会变,但摘要本身无法证明来源可信。

使用方式:写入类工具在不一致时返回 STANDARD_VERSION_MISMATCH;人工核对时以 opsstd://v1/manifest.json 返回的 digest 为准,不要缓存旧值。

路径映射(opsstd:// → HTTPS):opsstd://v1/manifest.json ↔ https://develop.soyooo.top/v1/manifest.json(与 /manifest.json、/standards/manifest.json 同内容);opsstd://v1/version.json ↔ /v1/version.json(与 /version.json、/standards/version.json 同内容);其余 opsstd://v1/<path> ↔ https://develop.soyooo.top/<path>。正文提到的元数据路径均已真实可访问,不需要按虚拟标识去猜目录。


16. 产物投放与部署闭环(顺序不要颠倒)

标准闭环是 投放 → 校验 → 干跑 → 收敛 conformance → 部署 → 复检。任何一步失败都停在该步,不要靠跳过换取“部署成功”。

  1. 自检:按第 13 章逐条自检;体检以 ops_check_conformance 的实时结果为准,不需要生成静态文件。
  2. 找管理员建项目并签 Key:由管理员用全量视图完成 id / pm2Name / 端口 / 域名冲突预检,登记 projects.json,并签发 deploy 或 readonly 项目 Key。项目 Key 下跑 ops_check_conflict 仍返回 limitedView:true(不能证明全局无冲突),但用于已登记项目时自身命中会标 self:true 且不计入 ok,不会再把本项目报成冲突。
  3. 投放产物:两种通道,任选其一(都返回同一个 artifactPath): - HTTP(推荐,不依赖 SSH):POST https://mcp.soyooo.top/artifacts - stdio 本地:ops_upload_artifact 传 sourcePath(MCP 读取本机文件后转发) - 已上传确认:ops_upload_artifact 传 fileName(+ sha256),只做存在性与校验和确认,不重复传输
  4. 校验:把响应里的 sha256 与本地 sha256sum app.tar.gz 比对;服务端已强制 .tar.gz/.tgz 与体积上限,文件名会被清洗,artifactPath 一定落在本项目目录。
  5. 干跑:先 ops_plan_deployment 确认 planType 与路径口径(deployPath = <OPS_DEPLOY_ROOT>/{id},更新计划取已登记 backup.appDir),再 ops_deploy_project 传 artifactPath + dryRun=true。
  6. 收敛 conformance:ops_check_conformance 直到 fail=0。首次部署(项目从未有 releases/current)会自动把 process.ecosystem_loadable、network.port_probe、caddy.site_block、caddy.proxy_target 标为 deferred,不需要在部署前“修好”它们;已部署项目仍是 fail,必须修完。Caddy 站点块由平台在执行器切换 current 前创建(caddyEnsureSite),项目侧不需要也不应该手工改 Caddyfile。
  7. 部署:dryRun=false + confirm=true,Key 需具备 project:deploy。执行器自动解析启动配置:部署目录已有 → 产物自带 → 按登记探测渲染(见 11.2);dryRun 返回 ecosystem 预览,不必先手工准备 ecosystem.config.*。
  8. 复检:首次部署成功后立即复检上述四项;执行器把结果写入 deploy.conformance 审计/oplog(含 caddyOk / caddyAction),MCP 在响应里返回 postDeployConformance。随后再跑一次 ops_check_conformance,确认无 fail、unknown 已人工核对。

16.1 产物上传契约(HTTP)

项 约定
路由 公网 POST https://mcp.soyooo.top/artifacts;后端实现 POST /api/project/artifacts
鉴权 两层:Authorization: Bearer <客户端 Token> + X-Ops-Project-Key: ops_pt_<projectId>_<random>;缺任一层都失败
请求体 multipart/form-data;文件字段名固定 artifact;可选字段 sha256(64 位小写十六进制,用于服务端提前比对)
文件名 原文件名只取 basename 并清洗为 [A-Za-z0-9._-],必须以 .tar.gz 或 .tgz 结尾;落盘名 <YYYYMMDD-HHMMSS>-<sha256前8位>-<name>
落盘位置 <OPS_ARTIFACTS_ROOT>/<projectId>/,默认 /opt/ops/shared/artifacts/<projectId>/;项目 Key 只能写自己项目目录
体积上限 200 MiB(OPS_ARTIFACT_MAX_BYTES / OPS_DEPLOY_MAX_ARTIFACT_BYTES)
保留策略 每项目保留最近 10 份(OPS_ARTIFACT_KEEP),按 mtime 清理旧文件
成功响应 { ok, projectId, artifactPath, fileName, size, sha256, uploadedAt, retention:{keep,pruned} };artifactPath 可直接传给 ops_deploy_project
查询/确认 GET /api/project/artifacts?name=<fileName> 返回 file:{artifactPath,size,sha256,uploadedAt};不带 name 时列本项目产物
错误码 401 客户端 Token/项目 Key 无效;403 跨项目或 scope 不足;400 ARTIFACT_REQUIRED/INVALID_FILENAME/CHECKSUM_MISMATCH;413 ARTIFACT_TOO_LARGE;415 UNSUPPORTED_ARTIFACT_TYPE;500 ARTIFACT_WRITE_FAILED

MCP 在转发前会做文件系统边界校验:来自暂存区的 artifactPath 必须属于本项目目录,跨项目产物直接 403 ARTIFACT_PROJECT_MISMATCH(这是路径隔离,不替代后端的 Key 绑定与 scope 校验)。

# 投放
curl -sS -X POST https://mcp.soyooo.top/artifacts \
  -H "Authorization: Bearer $OPS_CLIENT_TOKEN" \
  -H "X-Ops-Project-Key: $OPS_PROJECT_KEY" \
  -F "artifact=@demo-api-1.0.0.tar.gz" \
  -F "sha256=$(sha256sum demo-api-1.0.0.tar.gz | awk '{print $1}')"
# → { "artifactPath": "/opt/ops/shared/artifacts/demo-api/20261114-101500-ab12cd34-demo-api-1.0.0.tar.gz", ... }


### 16.2 冷启动读数与字段口径(1.2.5)

| 场景 | 约定 |
|------|------|
| 冲突检测 | 已登记项目自身的域名/端口/PM2 名/ID 命中标 `self:true`;`ok` 只由非 self 冲突决定,`selfConflicts` 给出数量,`effectiveConflicts` 只含真实冲突 |
| context 与 conformance | `ops_get_project_context.conformance` 复用 `ops_check_conformance` 同一实现(含首次纳管 deferred);两者都返回 `snapshotAt` 与 `rulesVersion`(`opsstd@<version>+<digest8>`),同一次调用的读数应一致 |
| Caddy 判定 | `caddy.site_block.actual` ∈ `无站点块` / `有站点块但无路由` / `存在(N 条路由)`;`caddy.proxy_target.actual` 区分无块、有块无路由、目标不符;`caddy.domain_unique` 与 `site_block` 同源(`inventory.sites`),不再出现「缺失」与「1 个站点块」并存 |
| 首次部署 | 四项 `deferred`:`process.ecosystem_loadable`、`network.port_probe`、`caddy.site_block`、`caddy.proxy_target`;执行器在切换 current 前创建站点块,部署后复检并写审计(`deploy.conformance` 含 `caddyOk` / `caddyAction`) |
| 版本字段 | `registeredStandardVersion`(项目登记值)与 `platformStandardVersion`(平台当前值)显式区分;旧 `standardVersion` 在 context 中保持平台值、在 `ops_list_projects` 中保持登记值 |
| .env 步骤 | `ops_plan_deployment.manualSteps` 每项带 `applicable`;`stateless:true` 或显式 `envKeys: []` 时 `.env` 步骤为 `applicable:false` 并给出 `reason`,无需人工判断 |

---

## 17. 项目端如何接入 MCP(客户端配置、自检与排查)

项目侧的 AI 环境默认没有任何 `ops_*` 工具:既查不到平台,也验证不了连通性。接入是**外部平台配置**,项目目录里不需要、也不应该添加任何实现(不加 SDK、不写代理、不放凭据文件)。

### 17.1 客户端配置

MCP 服务地址固定为 `https://mcp.soyooo.top/mcp`,传输方式为 **Streamable HTTP**(远程 MCP,非 stdio)。两个请求头缺一不可:

| 请求头 | 值 | 谁发放 | 能否写进仓库 |
|--------|----|--------|--------------|
| `Authorization` | `Bearer <客户端 Token>` | 平台管理员在运维后台「MCP 客户端」按调用方签发,识别调用方与限流;**不随项目接入材料下发** | **不能**。只放本机环境变量或客户端密钥管理 |
| `X-Ops-Project-Key` | `ops_pt_<projectId>_<random>` | 管理员通过 `POST /api/keys` 为你的项目签发 | **不能**。等同生产写权限,泄露立即吊销 |

配置片段(占位符替换为本机环境变量,不要把明文写进仓库):

```json
{
  "mcpServers": {
    "ops": {
      "type": "http",
      "url": "https://mcp.soyooo.top/mcp",
      "headers": {
        "Authorization": "Bearer ${OPS_CLIENT_TOKEN}",
        "X-Ops-Project-Key": "${OPS_PROJECT_KEY}"
      }
    }
  }
}

不同客户端字段名可能是 transport / type / streamableHttp,语义相同:URL + 两个 Header。codex / claude / cursor 等只要支持远程 HTTP MCP 均可按上表配置。

${OPS_CLIENT_TOKEN} 这类写法只在客户端自身支持环境变量展开时生效。若客户端不展开(把字面量原样发出),请求会带上无效 Token,返回 401 CLIENT_TOKEN_REQUIRED(reason=invalid)——这与“没配 Token”的 reason=missing 是两种不同问题。不支持展开时,改用客户端自带的密钥管理,或在仅本机可读的配置中直接填明文;任何情况下都不要把明文提交进仓库。客户端配置改动后需要重启客户端才会重新连接并刷新工具列表。

17.2 连通性自检

不依赖客户端,先用一条 curl 确认两层凭据都生效(initialize 不需要项目 Key 也能判定第一层):

curl -sS -X POST https://mcp.soyooo.top/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $OPS_CLIENT_TOKEN" \
  -H "X-Ops-Project-Key: $OPS_PROJECT_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"selfcheck","version":"0.0.0"}}}'

判定:200 且返回 serverInfo.name = ops-mcp 表示两层凭据都成立;401 CLIENT_TOKEN_REQUIRED 是第一层失败(看 reason);400 INVALID_PROJECT_KEY 是项目 Key 头格式问题。只带项目 Key、不带客户端 Token 时,initialize 一定 401,这是预期行为,不是服务故障。

配置生效后,工具列表应能看到 ops_* 前缀的工具:

  • 无项目 Key:只应看到标准资源类读取能力(opsstd://*、manifest、规范正文、版本与 digest),看不到项目数据。
  • 有项目 Key:只读工具应返回且仅返回绑定项目,例如 ops_get_project_context 返回 ok:true,且 data.projectId 等于你的 projectId;ops_list_projects 只有这一条。deployPath 取登记 backup.appDir;未登记时返回 <OPS_DEPLOY_ROOT>/{id} 并标记 deployPathSource:"default",不要把默认值写回登记。
  • 推荐自检顺序:ops_get_project_context → ops_list_projects → ops_check_conformance → ops_get_health。

只读工具(无需写权限):ops_list_projects、ops_get_health、ops_get_logs、ops_get_runbook、ops_list_backups、ops_get_scheduler_status、ops_get_inventory、ops_check_conflict、ops_plan_deployment、ops_check_conformance、ops_get_project_context。

写工具(ops_upload_artifact、ops_deploy_project、ops_rollback_project)需要三件事同时成立:deploy 类型项目 Key(ops_upload_artifact 与部署需要 project:deploy scope)、dryRun=false 且 confirm=true(上传不需要)、conformance 无 fail(首次部署的 deferred 不计)。只读 Key 调用会被拒绝。

17.3 无工具或报错时的排查顺序

按顺序排除,不要跳步:

顺序 检查 症状 处理
1 客户端是否配置了 MCP 服务、是否重启生效 工具列表里没有 ops_* 检查 URL 与传输方式;重启客户端
2 是否带客户端 Token 401 CLIENT_TOKEN_REQUIRED,reason=missing 向平台管理员申请客户端 Token(后台「MCP 客户端」),放进 Authorization;它不属于项目接入材料,不会随项目 Key 一起下发
2.1 客户端是否把 ${...} 原样发出 401 CLIENT_TOKEN_REQUIRED,reason=invalid 客户端不支持环境变量展开;改用明文或客户端密钥管理,然后重启客户端
3 是否带项目 Key 所有项目工具返回 PROJECT_KEY_REQUIRED 找管理员为你的项目签发 Key,放进 X-Ops-Project-Key
4 Key 头格式是否正确 400 INVALID_PROJECT_KEY 只放单个 ops_pt_<projectId>_<random>,不要数组、不要多余空格、不超过 128 字节
5 Key 是否绑定当前项目 403,提示不区分项目是否存在 Key 绑定了别的项目;本项目需单独签发,不要复用他人 Key
6 Key 是否过期/吊销/一次性已用 401 重新申请,不要复用旧 Key
7 权限是否够 写工具 403 或 scope 不足 换 deploy 类型 Key;readonly 只能读
  • 排查只看错误层级,不需要猜项目是否存在:401 是凭据问题(按 reason 再分 missing / invalid / revoked / expired),400 是格式问题,403 是项目/权限问题,PROJECT_KEY_REQUIRED 是漏了第二层凭据。
  • 所有项目数据都经由平台返回,不要在项目仓库里读取 projects.json 或直连 ops-backend;也不要把 OPS_PROJECT_KEY 写进 .env 后随备份包上传。

17.4 边界

  • 冲突检测与新项目登记必须在管理员侧完成。项目 Key 下 ops_check_conflict 只看到本项目,仍返回 limitedView:true + requiresAdminReview:true;但已登记项目的自身命中会标 self:true 并排除在 ok 之外(ok 可直接用于更新部署放行),不会再出现「自己撞自己」的 4 条冲突。
  • 本地 stdio 模式是管理员在本机排查用的,与公网入口凭据模型不同,不要把两者混用。
  • 项目目录只需按本规范写出应用;MCP 接入是外部配置,不产生任何代码或文件交付物。

18. 数据迁移规则

迁移失败的典型事故是 duplicate column:没有记录表、失败后盲目重跑,DDL 已生效却再次执行。本章给出可执行的统一规则。

18.1 迁移文件的位置

  • 目录固定为 release 根下的 migrations/,即运行时路径 current/migrations/。
  • 迁移文件是代码的一部分,随 {id}-{version}.tar.gz 发布,落在 releases/{ts}/migrations/;不得放在 data/,也不得在启动时从网络下载。
  • migrations/ 不在第 11 章禁项列表内,但必须出现在 build 脚本的包清单中;发布后该目录只读,不再修改。

18.2 命名与排序

  • 文件名格式:NNNN_snake_case.sql,NNNN 为至少 4 位、左补零的十进制序号(如 0001_create_user.sql、0002_add_user_email.sql)。
  • 排序:按序号数值升序;序号相同时按文件名字典序,但禁止序号重复。
  • 序号只增不减,不得复用已发布过的序号;允许留空号(如 0003 缺失),不允许插入到已有序号之前。
  • 不要求提供 down/回滚脚本;数据库回退不作为标准能力(见第 19 章)。

18.3 随 release 发布与执行时机

  • 迁移在进程启动阶段、listen() 之前执行;执行完成前不得接受任何请求。
  • 执行入口必须是确定性的:启动命令 → 加载配置 → 获取迁移锁 → 校验记录表 → 按序执行未应用迁移 → 释放锁 → 启动 HTTP 服务。
  • 多实例或 exec_mode: 'cluster' 时,必须使用数据库级咨询锁(PostgreSQL pg_advisory_lock、MySQL GET_LOCK、SQLite BEGIN IMMEDIATE)保证同一时刻只有一个执行者;未实现锁的项目不得开多实例。
  • 迁移执行必须有超时与日志:记录开始/结束、文件名、耗时、失败原因;日志不得输出数据库口令。

18.4 记录表结构与对齐要求

固定表名 schema_migrations(如项目已有同名表,按本节字段改造,不得另起别名)。记录表由迁移运行器在启动时以 CREATE TABLE IF NOT EXISTS 创建,不占用迁移序号,也不得依赖某条业务迁移来建表:

列 类型 约束 含义
version VARCHAR(32) PRIMARY KEY 迁移序号(与文件名前缀一致,如 0002)
name VARCHAR(255) NOT NULL 文件名(不含目录)
checksum CHAR(64) NOT NULL 迁移文件内容的 SHA-256(小写 hex)
applied_at VARCHAR(32) NOT NULL ISO8601 UTC 时间
duration_ms INTEGER NULL 执行耗时
release VARCHAR(64) NULL 产生该记录的 release 目录名,便于追溯

对齐规则(启动时校验,不满足即按 18.5 处理):

  1. 文件 → 记录:release 内每个迁移文件在表中至多一条记录,且 version 唯一。
  2. 记录 → 文件:表中存在记录、但当前 release 找不到对应文件 → fail(说明库比代码新,或发布包缺文件)。
  3. 校验和:已应用文件的 checksum 必须与当前文件内容一致;不一致 → fail(已发布迁移被改写,禁止继续)。
  4. 连续性:若存在序号更大的记录,则所有更小序号的迁移都必须有记录;出现「跳号未应用」→ fail。
  5. 未应用:文件中没有记录的,按顺序执行;执行成功写一条记录,失败不写。

18.5 幂等与失败处理

  • 运行器幂等:重复启动时,已记录且校验和一致的迁移必须跳过;重复启动不得重复执行。
  • 语句幂等:迁移本身应尽量可重复执行(如 CREATE TABLE IF NOT EXISTS、先查 information_schema 再 ALTER)。原因:MySQL 等引擎的 DDL 无法回滚,可能出现「DDL 已生效、记录尚未写入」的窗口;若语句不可重复执行,下一次启动就会报 duplicate column。
  • 失败必须中止启动:任何迁移失败 → 记录已成功的前序迁移 → 打印失败文件与原因 → 以非零码退出;禁止 listen()、禁止降级为「跳过失败继续跑」、禁止只写日志不退出。
  • PM2 会按 autorestart 重试;连续失败达到 max_restarts 后进入 errored,由监控告警。修复方式只能是:修正迁移文件(未发布时)、补一个向前修复的迁移(已发布时),或从备份恢复;不允许靠重试碰运气。
  • 破坏性迁移(删列、改类型、加非空约束等)在执行前必须先完成一次可验证备份,并在 runbook/发布说明中标注;平台目前没有「部署前自动备份」钩子,因此这是人工闸门。

18.6 禁止人工补记录行(事故处置 ≠ 标准做法)

  • 严禁在正常发布流程中手工 INSERT/UPDATE/DELETE schema_migrations,也严禁手工修改已应用迁移文件来「对齐」记录。
  • jingyu 的 duplicate column 事故中,曾通过人工补 tracking 行让服务继续启动。这属于事故处置,不是标准做法:它掩盖了「记录表与文件不一致」的真实原因,下一次发布仍会复发。
  • 只有以下情况允许人工介入,且必须同时满足:已停止写入、已做完整备份、由管理员执行、写入审计、事后补一条修正迁移或在演练环境复现验证: 1. 记录表损坏且无法从备份恢复; 2. DDL 已生效但记录缺失,且该迁移语句可证明可重复执行; 3. 数据库比代码新,需临时回退到匹配的 release。
  • 人工介入后必须在 runbook 第 8 章记录时间、原因、执行人、SQL、影响范围与后续修复计划。

19. 带迁移发布的回滚语义

回滚只切 current、不回写数据;但代码回退后数据库 schema 可能已经前进。本章规定什么情况下允许回滚、什么情况下必须前进修复。

19.1 两类迁移

类型 定义 示例
向后兼容(expand) 旧代码在新 schema 上仍可正常读写 新增可空列、新增表、新增索引、扩大字段长度
破坏性(contract) 旧代码在新 schema 上会报错或写坏数据 删列/删表、重命名、改类型、加非空且无默认值、收紧约束

19.2 允许回滚的条件

同时满足才允许把 current 切回上一 release:

  1. 本次发布只包含向后兼容迁移;
  2. 发布说明明确标注「可回滚」,且回滚目标 release 仍在 releases/ 中(保留 3 个);
  3. 回滚后按 19.4 验证旧代码能读写新 schema。

回滚仍只切 current、不回写数据;数据库保持前进状态。

19.3 必须前进修复(forward fix)的情况

出现以下任一情况,禁止回滚到旧代码,只能前进修复:

  1. 本次包含破坏性迁移(已删列/改类型/重命名/收紧约束);
  2. 迁移已写入数据(如回填、拆分表),旧代码无法解释新数据;
  3. 记录表显示数据库版本高于目标 release 的迁移集合;
  4. 回滚后旧代码启动即报 schema 相关错误。

处置顺序:停止继续发布 → 保留现场与日志 → 评估能否用一个向前迁移修复 → 修复并发布新 release;无法修复时才从备份恢复(会丢失备份点之后的数据,必须走事故流程)。

19.4 向后兼容的最低要求

  • 新 schema 必须至少兼容上一个 release(N-1)的代码;跨两个版本的破坏性变更必须拆成「扩展 → 双写/回填 → 收缩」多次发布。
  • 重命名列/表:先新增、双写、回填、切读、最后在后续 release 删除旧列;禁止在同一次发布内改名并删除。
  • 新增非空列必须带默认值或先允许为空,回填后再收紧。
  • 回滚验证至少覆盖:健康检查、登录/鉴权、核心写路径、迁移记录表读取。

19.5 现状风险:自动回滚不感知迁移(强制人工闸门)

风险结论:当前执行器在部署失败(健康检查、环境校验、PM2 启动等)时会无条件切回上一 release(switch-back)。它不读取迁移文件、不读取 schema_migrations,也没有任何「本次发布是否包含破坏性迁移」的判断。因此,一次包含破坏性迁移的发布如果失败,自动回滚会把旧代码指向已经前进的数据库,可能出现启动即报错、字段不存在、写坏数据等二次故障,且回滚动作本身会掩盖首次失败的原因。

  • 标准要求(现状下必须执行):包含破坏性迁移的发布不得使用自动部署工具直接发布,必须: 1. 走人工发布流程,管理员在场;发布说明显式标注「破坏性 / 禁止回滚」,并写入发布记录; 2. 发布前完成一次可验证备份,并确认恢复步骤可用; 3. 发布失败时不执行自动回滚,按 19.3 前进修复;若必须回到旧代码,先确认数据库已回退到与旧代码匹配的版本(只能从备份恢复,属事故流程)。
  • 使用自动部署工具发布向后兼容迁移时,仍可能触发自动回滚;这类回滚理论上安全,但必须满足 19.4 的 N-1 兼容要求,并在回滚后验证健康检查、登录/鉴权与核心写路径。
  • 首次部署(只有一个 release)失败时保留 current(keep-current),不会切回旧代码;但新代码/新 schema 可能仍不可用,同样需要人工处置。

未来能力(建议,不是现行标准要求,不得依赖):执行器在回滚前读取 release 内的迁移元数据(例如发布说明中的 rollback: allowed|forbidden、迁移清单哈希或迁移文件是否存在),发现破坏性迁移时拒绝自动回滚并返回明确错误。该能力目前完全不存在;在实现并发布之前,填写任何标记都不会生效,也不能作为通过依据或免责理由。

19.6 发布前判定清单

  • 列出本次新增迁移文件与序号,确认无重复、无改写已发布文件
  • 逐条标注「向后兼容/破坏性」,并写入发布说明
  • 破坏性迁移:已做可验证备份,已确认走人工发布且不会被自动回滚
  • 发布说明已标注「破坏性 / 禁止回滚」,并知会执行发布的管理员
  • 回滚目标 release 由平台保证仍存在:确认平台报告的回滚目标版本可用,且其迁移集合不高于当前数据库版本;不要用 ls releases/ 自行判断
  • runbook 第 3、4 章已更新迁移与恢复步骤

20. 运行时与依赖基线

本章回答一个问题:新建项目要能被 ops 部署,哪些运行时条件需要项目自己保证、哪些由平台保证。事实来源是 2026-11-12 的只读盘点(现网实测 + 官方发布记录);标准只把项目能够选择的项写成义务,宿主条件一律归平台。

20.1 四类约束力(先分清哪层管你)

层级 内容 对项目方的约束力
A. 平台自身依赖 ops-backend、ops-mcp、规范站构建所用的包及其版本 无约束。仅作说明与排障参考,项目方不需要关心,也不得据此要求平台升级
B. 项目必须满足 Node.js 大版本、原生模块工具链(python3 / make / g++ / glibc) 规范。这些是项目自己的代码与依赖能否装上、能否跑起来的前提,不满足会在安装或启动阶段直接失败
平台保证项 PM2、Caddy、SQLite CLI、glibc 运行时、OS / systemd、磁盘、内存 由平台负责,项目无需处理,也未提供降级路径。列出仅为排障定位:出现对应症状时找管理员,不要在项目内绕过
C. 建议项 满足更顺,不满足不阻断部署 建议。只影响可维护性与一致性

四类不得混用:不要把 A 层或平台保证项的版本写成对项目的要求,也不要把 C 层的建议写成 fail 条件。宿主条件(磁盘、内存、Caddy、PM2、SQLite CLI、OS)不是项目能选择的,写进项目义务只会把平台自身的运维责任转嫁出去。

20.2 A 层:平台自身依赖(仅说明)

平台组件(项目方无需关心,仅供参考):

组件 现网版本 说明
Express 4.22.3 ops-backend 的 HTTP 框架
@modelcontextprotocol/sdk 1.32.2 MCP 工具与传输
zod 3.25.76 MCP 工具入参校验,必须与 SDK 支持的 zod 主版本一致
better-sqlite3 11.10.0 first / jingyu 的数据库驱动,按各自 Node ABI 编译
node-cron / ali-oss / jsonwebtoken / bcryptjs / dotenv / multer 4.6.0 / 6.23.0 / 9.0.3 / 2.4.3 / 16.6.1 / 1.4.5-lts.2 调度、对象存储、鉴权、密码、配置、上传
PM2 7.0.x 进程托管,执行器读取 pm2 jlist
Caddy 2.11.x 反向代理,执行器调用 validate --config / reload
markdown 3.9 规范站构建,见 20.8

上表是平台事实,不构成对项目的要求,项目侧下限只在 20.3 声明。

20.3 B 层:项目必须满足(仅 2 项)

项 最低可用 推荐 为什么需要 不满足时的具体表现
Node.js 大版本 ≥ 22 22.x LTS,以平台现网为准(Node 24 需平台白名单) 平台侧 engines 基线、--env-file、现代 ESM 行为;执行器用 node --check 校验入口 Node 20 及以下:npm install 报 EBADENGINE(engine-strict 时直接失败);应用或脚本使用 --env-file 时 node 报 bad option,进程起不来;ESM/CJS 互操作行为差异,表现为 ERR_REQUIRE_ESM 或导入失败
原生模块工具链(仅当含原生模块) python3 + make + g++(gcc ≥ 10)且 glibc ≥ 2.28 与平台现网一致,以管理员告知为准 better-sqlite3 等原生模块必须按目标机 Node ABI 重编;跨机部署不是「装一次到处用」 npm install 或 npm rebuild 报 gyp ERR! stack Error: not found: make / python3 / g++,构建中断;只拷贝 node_modules 不自检时,进程启动报 NODE_MODULE_VERSION 不匹配;glibc 过低时直接执行 node 报 GLIBC_2.28 not found

Node 大版本怎么选(22 与 24 的差异)

  • 默认使用 22:系统 /usr/local/bin/node,也是 PM2 daemon 与多数量级项目的运行时。
  • 24 仅限平台白名单(如 first)。使用方法是在 ecosystem 中显式指定解释器:

js module.exports = [{ name: '${PM2_NAME}', cwd: '${DEPLOY_PATH}', script: '${ENTRY}', interpreter: '/opt/node24/bin/node', }];

不写 interpreter 时使用系统 Node(22)。不得在项目内自行安装或切换 Node 版本。 - engines 必须与 interpreter 实际指向的大版本一致:用 22 写 ">=22 <23",获准用 24 写 ">=24 <25"。两者不一致时,本地安装与线上运行会表现出不同的模块解析行为。 - 22 与 24 的 ABI 不同:同一项目的原生模块必须与它实际运行的 Node 版本一起编译。first 用 Node 24、jingyu 用 Node 22,两者的 better-sqlite3 必须分别重编。

补充约束:

  • 跨机迁移不是升级:新机按平台现网版本复制,只重编 native、重装平台相关包(如 tsx/esbuild 的 linux-x64 包)。
  • 无端口服务(localPort=0)同样要满足 Node 与工具链下限;端口不是本层的判断依据。

20.4 平台保证项(由平台负责,项目无需处理)

以下条件由平台保证。项目方不需要安装、升级或校验,平台也未提供降级路径(例如不能在项目内自建 Caddy 或替换 SQLite CLI)。「不满足时的表现」是排障线索:若线上出现这些症状,说明平台侧条件异常,应直接找管理员,不要在项目里绕过。

项 平台保证 为什么需要 不满足时的表现(排障线索)
PM2 版本支持 pm2 jlist 的 pm_cwd、pm_exec_path、restart_time,以及 --only、kill_timeout、treekill 执行器据此判断进程工作目录与重启行为 jlist 缺字段,R1(cwd 必须等于 deployPath)判定拿不到实际值而失败;启动参数含 --only 时 pm2 start 报未知选项;treekill 不生效导致重载残留旧进程
Caddy 支持 caddy validate --config 与 caddy reload,站点块按 v2 语法 接入校验与 TLS 终止 不支持 validate 的参数形式,接入校验停在 validate 一步,配置不落盘;reload 行为差异导致新站点块未生效但返回成功
SQLite CLI 存在,且支持 .backup 与 PRAGMA integrity_check 备份脚本的核心命令 command not found,或旧版不支持 .backup:backup.sh 非零退出,首次备份失败;后端 2.1.0 起未通过 integrity 的备份会被判为失败并拒绝上传
glibc 运行时 ≥ 2.28 Node 22/24 官方二进制与 native 模块的运行前提 直接执行 node 报 GLIBC_2.28 not found,PM2 拉起后立即 errored 并反复重启
OS / 架构 Linux x86_64 + systemd PM2 与 Caddy 需要服务化 pm2 startup 报无 systemd / 不支持的 init,开机不自启;非 x86_64 时 native 预编译包缺失,回退到源码编译
磁盘 满足至少 3 个 release、本地备份(保留 7 份)与依赖共存 部署与备份的落盘空间 ENOSPC:npm install 失败、tar czf 中途失败、备份写 .tmp 时失败,表现为部署在校验或解包阶段中断
内存 满足 native 构建与多个 PM2 进程并行 构建与运行稳定性 构建被 OOM kill(Killed,无明确错误)、PM2 反复 restart、健康检查超时被判定部署失败

平台当前实装(记录时现网值,可能已变化,仅作排障参照):

项 记录时版本
Node(系统) 22.12.0(Node 22 LTS)
Node(/opt/node24) 24.22.2(白名单项目使用)
npm 10.9.0(Node 22)/ 11.19.1(Node 24)
SQLite CLI 3.42.0
gcc / make / python3 12.3.0 / 4.4.1 / 3.11.6
glibc 2.38
内核 / 架构 6.6.102 / x86_64(Alibaba Cloud Linux 4)

这些数字会随平台升级变化,标准不对项目方声明补丁号;需要精确值时向管理员确认。

20.5 C 层:建议项(不阻断)

建议 原因 不满足的后果
package.json 声明 engines 且与 interpreter 一致 本地与 CI 能提前暴露版本不符 只在部署时才失败,排查成本更高
本地开发环境与平台 Node 大版本对齐 避免本地能跑、线上装不上 native 模块与 ESM 行为在本地与线上不一致
npm ≥ 10 Node 22 自带,无需额外动作 旧 npm 的 lockfile 行为差异,可能装出不同依赖树
默认 Node 22,Node 24 仅白名单 与平台基线一致 混用会放大 ABI 问题
项目内固定构建依赖版本 构建可复现,产物哈希稳定 产物哈希漂移,无法判断是否真的改了代码

20.6 两条原则

  1. 标准声明的是「已验证的大版本与工具链」,不是「最新版本」,也不是补丁号。 具体补丁(Node 22.x 的哪个小版本、Caddy 的哪个 patch)由平台维护,会随升级变化;标准不把这类数字写成项目义务。不要为了追新而升级运行时,也不要因为平台补丁前进了就认为标准要求也变了。
  2. 升级节奏由平台方决定。 项目方不得在发布窗口内自行升级 Node / PM2 / Caddy / glibc / OS 大版本,也不得改 package.json 的 engines 与依赖区间来「顺便升级」。需要升级时由平台统一安排,并与执行器、备份、迁移一起回归。
  3. 新机器安装取当时的稳定版本。 默认 Node 22 LTS、Caddy 稳定版,不把补丁号写死;需要复现某次发布时,用 manifest.json 的 minServerVersion、develop-site/requirements.txt 的项目 lockfile 锁定基线,而不是回退到旧补丁包。新机器只保证「大版本与工具链满足」,补丁由平台在安装后按稳定版推进。
  4. 升级边界:运行时不做热升级,应用依赖可升但必须重启。 Node / Caddy / PM2 的升级要停机切换并跑部署、回滚、证书与健康检查回归,不能在业务进程运行中替换;项目自身的 npm 依赖可以在发布窗口内升级,但必须重新安装、重启 PM2 进程并完成回归。标准版本号变化只表示正文口径变化,不代表平台运行时已经升级。

20.7 项目侧自检(可直接复制)

(1) package.json 声明运行时下限

{
  "engines": {
    "node": ">=22 <23",
    "npm": ">=10"
  }
}

注意:engines 默认只是告警,只有开启 engine-strict=true 或在 CI 中显式检查才会阻断;PM2 完全不读 engines。因此还必须做 (2)。

(2) 构建脚本里的 Node 大版本硬校验(放在 build 脚本最前面,先于打包)

#!/usr/bin/env bash
set -euo pipefail

# 与本项目 ecosystem 的 interpreter 保持一致:系统 Node 写 22,白名单 Node 24 写 24
need_node_major=22
node_major="$(node -p 'process.versions.node.split(".")[0]')"
if [ "$node_major" -ne "$need_node_major" ]; then
  echo "FAIL runtime: Node $(node -v) 与目标大版本 ${need_node_major}.x 不一致" >&2
  exit 1
fi

echo "runtime ok: node=$(node -v)"

PM2、Caddy、SQLite CLI 属于平台保证项,项目侧不必自检;若在本地开发时临时用到 sqlite3,自行安装即可,不要把它写进部署前置条件。

(3) 原生模块重编前置检查(只有含原生模块时需要)

if grep -q '"better-sqlite3"' package.json 2>/dev/null; then
  for bin in python3 make g++; do
    command -v "$bin" >/dev/null || {
      echo "FAIL: 缺少 $bin,原生模块无法编译(gyp ERR! not found)" >&2; exit 1; }
  done
  node -e "require('better-sqlite3')" 2>/dev/null || {
    echo "FAIL: better-sqlite3 与当前 Node ABI 不匹配,执行 npm rebuild better-sqlite3" >&2
    exit 1; }
fi

(4) 自检结论写进交付说明:Node 主版本(是否与 interpreter 一致)、是否含原生模块及是否已按目标 Node 重编。这两项是部署失败时最先被问到的信息。

20.8 规范站(本文件)的构建可复现

规范站由 ops-mcp/standards/llms.txt 生成,构建依赖只有一个 Python 包,但历史上未钉版本(本机 3.9,最新 3.11.3),导致 index.html 的哈希对无关升级敏感。

  • develop-site/requirements.txt 固定 markdown==3.9;
  • 构建前执行 pip install -r requirements.txt,不要用全局已装的任意版本;
  • build.py --check 会比对运行版本与 requirements.txt 的钉版本,不一致直接失败;
  • 标准发布清单增加一条:发布记录必须包含构建依赖版本(requirements.txt 内容)与四个文件(llms.txt、index.md、index.html 及标准源副本)的 sha256,便于判断哈希变化是内容变化还是构建环境变化。

升级 markdown 属于 A 层动作:由平台决定,升级后必须重新基线化站点哈希并重新发布,不得在内容发布中夹带。