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 条不可协商的硬规则
- 代码目录固定
deployPath(登记backup.appDir,未登记时按<OPS_DEPLOY_ROOT>/<id>兜底,安装默认/opt/apps/{id}),数据目录独立(默认<deployPath>/data),数据不得位于releases/或current之内。 - PM2 进程名 =
pm2Name,cwd=deployPath,两者与后台登记逐字一致。 - 端口只从环境变量读取,服务只监听
127.0.0.1,禁止0.0.0.0。 autorestart: true,并实现SIGTERM优雅退出。- 健康检查默认路径
/healthz(登记probePath),只有 2xx/3xx 通过,>=400判失败。 - 日志只写 stdout/stderr,由 PM2 收集;应用不自行写轮转文件。
- 启动参数按性质拆两处:非密钥运行参数(
PORT/HOST/NODE_ENV/WEB_DIST与数据路径键)写入 ecosystemenv,作为启动唯一真源;密钥(JWT_SECRET、第三方 API Key 等)只放.env(权限600,不入 git、不进日志、不进备份包)。数据目录不强制叫DATA_DIR:也可写应用真正消费的DATABASE_FILE、UPLOAD_ROOT等键,解析后必须落在登记dataDir内;无持久化状态在 projects.json 写stateless: true。禁止为了凑字段写一个应用从不读取的假键。 - 备份范围为整个
data/目录;SQLite 必须用sqlite3 ... ".backup",禁止直接复制.db文件。 - 运维脚本固定放
/opt/ops-scripts/{id}/,不放项目 git 目录(会被git pull覆盖)。 - runbook 固定放
/opt/runbooks/{id}.md,8 章齐全,≤200KB;不得写死具体 release 或版本号(会天天过期),一律用readlink -f <deployPath>/current或读current/package.json动态取。 - 发布产物 =
{id}-{version}.tar.gz+ 同名.sha256,由项目内固定 build 脚本一条命令产出(含禁项扫描、软链检查、解包后随机端口 + 临时数据目录的实际启动自检),落到releases/{ts}并用current软链切换,不覆盖旧版本。 - 不改动服务器上其他站点、不重启 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 注入」只适用于密钥;非密钥参数必须由 ecosystemenv注入,否则/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必须显式写入 ecosystemenv,值等于登记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;备份可关闭 |
判定顺序(与实现一致,命中即停):
projects.json的stateless === true→ 视为无持久化状态,通过;- ecosystem
env中DATA_DIR非空 → 采用该键,并按与具体数据键相同的规则解析、校验落在dataDir内; - ecosystem
env中符合_FILE/_DIR/_PATH/_ROOT/_DB后缀的数据类键(排除NODE_PATH、WEB_DIST、HEALTH_PATH、PATH)→ 逐个解析并校验落在dataDir内; - 以上都没有时,读取项目根
.env(deployPath/.env)仅补充发现数据类键,再做同样的路径校验; - 仍没有 → 判 fail:
ecosystem.env 缺少 DATA_DIR,也未声明具体数据键(*_FILE/_DIR/_PATH/_ROOT/_DB)或 stateless=true:无法确认持久化数据目录。
.env只是补充发现来源:从.env读到的键只参与「发现 + 路径校验」,不参与/proc/<pid>/environ运行时比对——.env由应用在进程内注入,不进入execve初始环境,看不到这些键。只有 ecosystemenv声明的键才纳入运行时校验。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,与 ecosystemenv逐键比对PORT、WEB_DIST(如有)与 ecosystemenv中声明的数据路径键(仅在.env中发现、未写入 ecosystemenv的键不参与运行时比对);不一致即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→ 重启进程 → 验证;不要通过修改 PM2env下发密钥(会被进程快照固化,改.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 非零退出时平台当前只告警、不中断打包,因此导出成功与否必须在脚本内部自行保证并落盘可验证标记。
- 导出责任:项目自己的
/opt/ops-scripts/{id}/backup.sh负责导出数据库;平台只打包dataDir,不直连数据库。 - 导出落点:导出文件必须写入
dataDir下的专用子目录(建议$DATA_DIR/db-dump/),先写.tmp再原子mv,避免平台 tar 到半个文件。 - 一致性参数:
- MySQL:
mysqldump --single-transaction --quick --routines --triggers(InnoDB);需要强一致快照时从只读副本导出。 - PostgreSQL:pg_dump --format=custom --no-owner;全实例级备份(pg_basebackup)单独由运维评估。 - 凭据:数据库口令只从
deployPath/.env读取,不得出现在命令行参数、脚本明文或日志中(避免ps泄露)。 - 校验:导出后检查退出码、文件非空、格式可识别(如
pg_restore --list);任何一项失败即exit 1,并在dataDir写入失败标记供监控识别。 - 保留:
db-dump/只保留最新一份工作文件,历史由平台备份包承载;本地与远端目标仍按 8.3 各自保留 7 份,避免data/无限增长。 - 恢复:runbook 第 4 章必须写明取哪份包、解出 dump、恢复到哪个实例、恢复后如何校验关键表行数;每季度至少演练一次。
- 未覆盖即视为无备份:未按本节导出的 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。脚本至少做三件事:- 禁项扫描:确认包内无
.env、data/、backups/、logs/、.git/、releases/、current,命中即失败退出(ecosystem.config.*可选,不作禁项)。 - 软链检查:拒绝指向包外的符号链接与绝对路径条目,避免解包逃逸。
- 解包启动自检:在临时目录解包,用随机空闲端口 + 临时数据目录真实启动,轮询
/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" |
入口探测优先级(按顺序,命中即停):
projects.json的entry(相对产物根;显式覆盖)package.json→scripts.start(仅接受可直接交给 node 的命令:node [--flags] <file>或纯文件路径;npm/pnpm/pm2/next等包装命令不猜)package.json→main- 常见入口:
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 - 静态站点:
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、数据路径键与 ecosystemenv一致,且数据键与DATA_DIR解析后落在登记dataDir内(DATA_DIR完整校验依赖后端 ≥2.1.0)。项目方确认返回的env.*结果与预期一致;/proc/<pid>/environ比对需服务器权限,仅供管理员核对 - [建议检]
requestTimeout不早于反代read_timeout/write_timeout,且未把requestTimeout当作响应流超时 - [必检]
.env只放密钥(权限600),PM2env中无任何密钥 - [必检] 端口读自
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声明与 ecosysteminterpreter指向的 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 → 部署 → 复检。任何一步失败都停在该步,不要靠跳过换取“部署成功”。
- 自检:按第 13 章逐条自检;体检以
ops_check_conformance的实时结果为准,不需要生成静态文件。 - 找管理员建项目并签 Key:由管理员用全量视图完成
id/pm2Name/ 端口 / 域名冲突预检,登记projects.json,并签发deploy或readonly项目 Key。项目 Key 下跑ops_check_conflict仍返回limitedView:true(不能证明全局无冲突),但用于已登记项目时自身命中会标self:true且不计入ok,不会再把本项目报成冲突。 - 投放产物:两种通道,任选其一(都返回同一个
artifactPath): - HTTP(推荐,不依赖 SSH):POST https://mcp.soyooo.top/artifacts- stdio 本地:ops_upload_artifact传sourcePath(MCP 读取本机文件后转发) - 已上传确认:ops_upload_artifact传fileName(+sha256),只做存在性与校验和确认,不重复传输 - 校验:把响应里的
sha256与本地sha256sum app.tar.gz比对;服务端已强制.tar.gz/.tgz与体积上限,文件名会被清洗,artifactPath一定落在本项目目录。 - 干跑:先
ops_plan_deployment确认planType与路径口径(deployPath = <OPS_DEPLOY_ROOT>/{id},更新计划取已登记backup.appDir),再ops_deploy_project传artifactPath+dryRun=true。 - 收敛 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。 - 部署:
dryRun=false+confirm=true,Key 需具备project:deploy。执行器自动解析启动配置:部署目录已有 → 产物自带 → 按登记探测渲染(见 11.2);dryRun返回ecosystem预览,不必先手工准备ecosystem.config.*。 - 复检:首次部署成功后立即复检上述四项;执行器把结果写入
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'时,必须使用数据库级咨询锁(PostgreSQLpg_advisory_lock、MySQLGET_LOCK、SQLiteBEGIN 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 处理):
- 文件 → 记录:release 内每个迁移文件在表中至多一条记录,且
version唯一。 - 记录 → 文件:表中存在记录、但当前 release 找不到对应文件 → fail(说明库比代码新,或发布包缺文件)。
- 校验和:已应用文件的
checksum必须与当前文件内容一致;不一致 → fail(已发布迁移被改写,禁止继续)。 - 连续性:若存在序号更大的记录,则所有更小序号的迁移都必须有记录;出现「跳号未应用」→ fail。
- 未应用:文件中没有记录的,按顺序执行;执行成功写一条记录,失败不写。
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/DELETEschema_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:
- 本次发布只包含向后兼容迁移;
- 发布说明明确标注「可回滚」,且回滚目标 release 仍在
releases/中(保留 3 个); - 回滚后按 19.4 验证旧代码能读写新 schema。
回滚仍只切 current、不回写数据;数据库保持前进状态。
19.3 必须前进修复(forward fix)的情况
出现以下任一情况,禁止回滚到旧代码,只能前进修复:
- 本次包含破坏性迁移(已删列/改类型/重命名/收紧约束);
- 迁移已写入数据(如回填、拆分表),旧代码无法解释新数据;
- 记录表显示数据库版本高于目标 release 的迁移集合;
- 回滚后旧代码启动即报 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 两条原则
- 标准声明的是「已验证的大版本与工具链」,不是「最新版本」,也不是补丁号。 具体补丁(Node 22.x 的哪个小版本、Caddy 的哪个 patch)由平台维护,会随升级变化;标准不把这类数字写成项目义务。不要为了追新而升级运行时,也不要因为平台补丁前进了就认为标准要求也变了。
- 升级节奏由平台方决定。 项目方不得在发布窗口内自行升级 Node / PM2 / Caddy / glibc / OS 大版本,也不得改
package.json的engines与依赖区间来「顺便升级」。需要升级时由平台统一安排,并与执行器、备份、迁移一起回归。 - 新机器安装取当时的稳定版本。 默认 Node 22 LTS、Caddy 稳定版,不把补丁号写死;需要复现某次发布时,用
manifest.json的minServerVersion、develop-site/requirements.txt的项目 lockfile 锁定基线,而不是回退到旧补丁包。新机器只保证「大版本与工具链满足」,补丁由平台在安装后按稳定版推进。 - 升级边界:运行时不做热升级,应用依赖可升但必须重启。 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 层动作:由平台决定,升级后必须重新基线化站点哈希并重新发布,不得在内容发布中夹带。