prod-server-hotpatch

SKILL运维和基础设施
运维和基础设施 未标注权限 许可证不明确
安装这个技能

对线上生产服务器做代码热补丁的通用方法论(改文件→语法检查→滚动重启→健康验证→可回滚)。适用于只有 SSH、代码不在本地、服务正在对外提供服务的场景。当需要在远程生产机上改代码、接入新服务、修线上 bug,或遇到 SSH 被封禁 / 服务不是常规 systemd 单元 / 需要幂等可回滚补丁时使用。触发词:生产机改代码、线上热修、部署补丁、滚动重启、SSH 被封、服务器改文件后重启验证、幂等补丁脚本、改了配置没生效、重启到底有没有生效、env 文件改了要重启吗、uptime 反推启动时间、部署前置检查失效、安全脚本静默失败、脚本只在 /tmp 没进仓库、部署工具脚本版本不一致、检查脚本查错了数据源、部署脚本跑了却只打印 SKIP 看不到结果、幂等锁吞掉第一次输出、命令被执行两次、跨机传图片文件损坏、base64 传二进制、ssh 传文件字节不一致、投多个文件带新依赖、只投一个文件导致全站挂、模块加载失败全站崩、新 require 目标机没有该文件、投递前依赖检查、require.resolve 只解析不执行、部署后语法检查误报、Permission denied 被误判成语法错误、stderr 重定向到固定 tmp 文件、部署可以自动回滚、健康检查失败自动还原、回滚后要再验一次、新增文件回滚是删除、健康端点真名要先枚举、备份幂等要用固定目录加 ORIGINAL.md5、静态资源必须先于引用它的代码、模型权重文件部署、vendor 资源缺失、启动期自检硬校验、改了自检函数却没投资源、多变量耦合上线的归因分层、sudo cp 覆盖已存在文件属主不变。

一句话选型结论

按资料字段整理

prod-server-hotpatch是运维和基础设施类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类运维和基础设施 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_6af9f15b/prod-server-hotpatch
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

运维和基础设施
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_6af9f15b仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791622043
正文体量45.8 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
查服务与日志

按现象定位服务状态与日志线索。

写部署与巡检脚本

把重复的部署、巡检写成脚本。

故障排查

按步骤排查网络、依赖与配置。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在运维和基础设施场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
prod-server-hotpatch当前 未标注权限 未标注 未标注 未标注 有
Azure 部署 调用外部服务 GitHub 仓库 664.2 万 Codex、Claude、Cursor 有
Azure diagnostics 问题管理 调用外部服务 GitHub 仓库 657.6 万 Codex、Claude、Cursor 有
Azure aigateway 部署 调用外部服务 GitHub 仓库 655.3 万 Codex、Claude、Cursor 有
Azure AI 搜索 调用外部服务 GitHub 仓库 651.4 万 Codex、Claude、Cursor 有
Azure Cloud migrate 部署 调用外部服务 GitHub 仓库 650.7 万 Codex、Claude、Cursor 有

SKILL.md 全文

45.8 KB
模型实际读到的说明书原文,来自技能仓库

生产服务器热补丁流程(通用方法论)

目标:改得动、验得过、退得回。适用于只有 SSH、代码不在本地、服务正在对外提供服务的场景。

> 本 skill 是跨项目通用的。平台专属信息(某个项目跑在哪个目录、服务单元叫什么、要不要跳板、改完还要动哪些文件)查对应的平台 skill,例如幻镜漫剧平台见 manju-platform-patching。

一、先摸清真实运行方式(别上来就 restart)

很多服务并非名为 <app>.service,先确认:

ps -eo pid,ppid,user,etime,cmd | grep -E "node|python|java" | grep -v grep
systemctl list-units --all | grep -i <app>
ls -la /etc/systemd/system/ | grep -i <app>
command -v pm2 && pm2 list

典型陷阱:app.service 是指向 /dev/null 的 masked 符号链接,实际跑的是模板实例 app@3000/3001/3002。正确重启是 sudo systemctl restart app@$p,而不是 systemctl restart app。

同时确认文件属主(stat -c %U file),决定用不用 sudo;写入新文件后要 chown 回原属主,否则服务可能读不到。

还要确认数据目录(改配置类文件时是高危陷阱):

systemctl cat <unit> | grep -iE "Environment|EnvironmentFile"

仓库里常有 data/xxx.json 副本,但生产 Environment=DATA_DIR=... 才是真正生效的。改错地方 = 白忙一场且毫无报错。

权限不一致很常见:同一个 lib/ 目录下可能 a.js 是 ubuntu:ubuntu、b.js 是 root:root。补丁脚本里不要假定权限,统一 sudo node + 保属主写入(见 §3 writeKeepOwner),最省心。

二、SSH 被封禁时走跳板

症状:ping 通、业务端口(443)通、/api/health 200,但 22 端口 TCP 超时,长时间等待也不恢复。

不要用高频 TCP 探测(bash -c "exec 3<>/dev/tcp/host/22" 循环)—— 会加剧封禁。

解法:找一台能访问目标机 22 端口的同环境机器做跳板(同机房的开发机/跳板机通常不被封)。

# 1) 把私钥投到跳板机(/tmp,用完删)
cat /path/key.pem | ssh -i /path/key.pem ubuntu@<JUMP_IP> \
  "umask 077; cat > /tmp/<prod-key>.pem; chmod 600 /tmp/<prod-key>.pem; echo KEY_WRITTEN"

# 2) 跳板执行
ssh -i /path/key.pem ubuntu@<JUMP_IP> \
  "ssh -i /tmp/<prod-key>.pem -o StrictHostKeyChecking=no ubuntu@<PROD_IP> '命令'"

注意:
- ssh-agent forwarding 在 Windows Git Bash 下不可用(Error connecting to agent),别指望 ssh -A。
- 远程命令三层引号嵌套极易出错。改成脚本转发,稳定得多:
bash cat local-script.sh | ssh -i KEY ubuntu@<JUMP_IP> \ 'cat > /tmp/s.sh; ssh -i /tmp/<prod-key>.pem ubuntu@<PROD_IP> "bash -s" < /tmp/s.sh'
- Node 补丁脚本需 sudo 时,脚本落在跳板机上生产机读不到,要 stdin 二次转发:
bash cat patch.js | ssh -i KEY ubuntu@<JUMP_IP> \ 'cat > /tmp/p.js; ssh -i /tmp/<prod-key>.pem ubuntu@<PROD_IP> "cat > /tmp/p.js && sudo node /tmp/p.js" < /tmp/p.js'
- 清理顺序:先清生产机临时文件 → 再删跳板私钥。反了会连不上,得重投。
- 跳板机 /tmp 可能被自动清理,间隔稍久需重新投递。
- ⚠️ 内层 ssh 必须加 -n(2026-09-15 静默截断事故):
当整段脚本是用 cat x.sh | ssh 跳板 'bash -s' 喂进去时,stdin 就是脚本本身。
脚本里若再 ssh 一次跳到生产机,那个内层 ssh 默认会把 stdin 转发给远端命令,
于是把脚本剩余部分整段啃掉 —— 症状是「跑完第 1 段就没有下文」,不报任何错;
若靠 stdout 回传 tar,拿到的是 0 字节文件(tar: unexpected end of file)。
⚠️ 迷惑性极强:内层 ssh 认证失败时它快速返回、症状轻,脚本可能侥幸跑完;
一旦认证成功就必然截断 —— 所以「上次这么写没问题」不能当依据。
修法:内层 ssh 一律加 -n;而 bash -s 那一层绝不能加 -n(那会把脚本本身设成 /dev/null,一条都不执行)。
自检:脚本分段输出 + 末尾打一个 XXX_DONE 标记,没看到标记就是被截断了,别只看「跑过了」。
- ⚠️ 私钥投递必须与「使用它的那条命令」在同一条命令里(2026-09-15 实战):
先投私钥、再单独发一条用它的命令,第二步会报
Warning: Identity file /tmp/<prod-key>.pem not accessible + Permission denied
—— 投递那一跳结束时私钥已被清理(或两次连接落在不同实例)。
正确写法:一次登录内完成「投」和「用」,退出前自己清:
bash cat key.pem | ssh -i key.pem ubuntu@<JUMP_IP> \ "umask 077; cat > /tmp/<prod-key>.pem; bash /tmp/run.sh; rc=\$?; shred -u /tmp/<prod-key>.pem; exit \$rc"
(/tmp/run.sh 需先用同一跳板投递;脚本内不要再自己 shred 私钥,避免双删报错。)
- 💡 优先「先落文件、再 bash 文件」,少用 cat x.sh | ssh 'bash -s':
bash -s 把脚本塞进 stdin,任何内层 ssh / 交互式命令都会跟脚本抢 stdin(即上一条 -n 事故的根源)。
落文件后 bash /tmp/x.sh,stdin 空闲,坑少一半。脚本头部自带
[ ! -f "$PRODKEY" ] && exit 9 自检,比靠报错信息反推可靠。
- ⚠️ 一条 ssh 命令里只能有一个 cat > 目标文件(2026-09-15 实测):
stdin 是一次性的流,第一个 cat 读到底后,第二个 cat 只会拿到 EOF。
bash # ❌ 脚本写对了,私钥写成 0 字节 cat verify.sh | ssh dev 'cat > /tmp/verify.sh; cat > /tmp/<prod-key>.pem; bash /tmp/verify.sh'
症状:内层 ssh 报 Load key "/tmp/<prod-key>.pem": error in libcrypto
+ Permission denied (publickey,password) —— 极易误判成"密钥不对/没权限",实际是文件空的。
→ 投脚本与投私钥必须拆成两条命令,每条 stdin 只喂一件事。
- ⚠️ 管道转发必须比对字节数:cat x | ssh 'cat > y' 是"静默失败"重灾区
(-n 吞成 0 字节、认证失败静默跳过、网络中断只留半截),
转发脚本里加一句 [ "$local_sz" = "$remote_sz" ] || exit 1 才算真校验过。
- ⚠️ 别给外层管道加 < /dev/null(2026-09-15 实测):
bash # ❌ 文件传成 0 字节,exit 0,不报任何错 cat x.tgz | ssh -i KEY ubuntu@<JUMP> 'cat > /tmp/x.tgz' < /dev/null
症状极隐蔽:退出码 0、没有任何报错,远端文件 md5 = d41d8cd98f00b204e9800998ecf8427e(空文件)。
「内层 ssh 加 -n」与「外层管道加 < /dev/null」是两回事:前者必须加(见上一条),
后者禁止 —— cat x | 的输入端正是那条管道,< /dev/null 会把它掐掉。
自检:转发后立刻双端 md5sum / wc -c 比对,别只看 exit code。
- ⚠️ 本地 /tmp ≠ 远端 /tmp(2026-09-15 实测):
cat local.tgz | ssh 跳板 'tar xzf /tmp/local.tgz' 会在远端报
Cannot open: No such file or directory —— 那个路径在跳板机上并不存在。
管道传输必须在目标机显式落盘:
bash cat local.tgz | ssh 跳板 'cat > /tmp/x.tgz && tar xzf /tmp/x.tgz'
同理,node /tmp/probe.js 里的 /tmp 指的是执行那台机器的 /tmp,不是本机的。

⚠️ 混合属主 → 命令非 0,但文件已「部分成功」覆盖(2026-09-15 实战)

同一目录文件属主不统一(典型:engine.js 属 root、lib/*.js 属 ubuntu),
用 ubuntu 身份跑 cp a b c /opt/app/:

  • 到 root 属主那个文件时 Permission denied,命令退出码非 0;
  • 但排在它前面的文件已经覆盖成功 → 线上变成半新半旧。
    你只看到「失败了、没生效」,容易直接放弃或重跑,根本不会想到已经改了一半。

处置顺序(严格按此,别急着重启):

  1. 先核对现状与备份的 md5,确认「改了哪些、没改哪些」,别凭命令输出推断:
    bash md5sum /opt/app/engine.js /opt/app/lib/*.js md5sum /opt/app/.bak-<TS>/engine.js /opt/app/.bak-<TS>/lib/*.js # 回滚点是否完好
  2. 确认回滚点完好后,改 sudo cp 补齐剩余文件(保属主可用 §3 的 writeKeepOwner)。
  3. 再核对一遍 md5(应为「全部 == 本地」)。
  4. 最后才滚动重启 + 健康检查。

预防:投递脚本开头就 [ "$(id -u)" = "0" ] || { echo 需 root; exit 1; },或干脆一律 sudo;
并在脚本末尾逐个文件打印「期望 md5 vs 实际 md5」,让「部分成功」在输出里无处藏身 ——
「命令成功」不等于「文件都变了」,反之亦然。

三、写幂等补丁脚本(不要手写 sed)

用 Node 写补丁脚本,在服务器以 sudo node /tmp/patch.js 执行。核心模板:

const fs = require('fs');
const TS = new Date().toISOString().replace(/[-:T]/g,'').slice(0,14);

function backup(f){ const b=f+'.bak-'+TS; if(!fs.existsSync(b)) fs.copyFileSync(f,b); return b; }

function writeKeepOwner(f, content){          // 保持原属主/权限,root 写入也不破坏服务
  let uid,gid,mode;
  if (fs.existsSync(f)) { const st=fs.statSync(f); uid=st.uid; gid=st.gid; mode=st.mode; }
  else { const st=fs.statSync(require('path').dirname(f)); uid=st.uid; gid=st.gid; mode=0o100644; }
  fs.writeFileSync(f, content);
  try { fs.chownSync(f,uid,gid); fs.chmodSync(f,mode & 0o7777); } catch(e){}
}

// 锚点替换:缺失/不唯一都报错,绝不做半截改动
function rep(src, from, to, all){
  const n = src.split(from).length - 1;
  if (n === 0) throw new Error('ANCHOR_NOT_FOUND: '+JSON.stringify(from.slice(0,70)));
  if (!all && n > 1) throw new Error('ANCHOR_NOT_UNIQUE('+n+'): '+JSON.stringify(from.slice(0,70)));
  return all ? src.split(from).join(to) : src.replace(from, to);
}

幂等五原则(血泪教训):

  1. 先清理历史残留再写入。脚本可能被重复执行(工具重试、网络重传),第二轮的锚点已变。写一个 stripPrevious() 把上一轮插入的块删干净。
  2. 幂等检测要用特征串,绝不能用「逗号」这类通用字符。错误写法 src.indexOf(insert.trim().split('\n')[0]) 拿到的是 ,,永远命中 → 永远跳过或永远不跳过。要用结果里独有的字符串(如新条目的完整 key 名)。
  3. 插入对象字面量条目时,正则不要吞掉收尾结构。错误做法:正则匹配到 };\n /* 注释 */,插入点落到对象外面 → 语法错误。正确做法:用分组只匹配末项,替换时重组保留 };:
    js const re = /(` \})\n( {2}\};\n {2}\/\* 注释 \*\/)/; // 分两组 src = src.replace(re, '$1,\n' + NEW_ENTRIES + '\n };\n /* 注释 */');
  4. 写文件保属主:fs.writeFileSync 后跟 chown/chmod,否则 sudo 写入会让文件变成 root:root,服务可能读不到。
  5. ⚠️ 守卫条件里的特征串,写完必须 grep -c 验一次命中次数(2026-09-15 实测翻车):
    守卫写成 grep -q '角色特效' lib/llm-steps.js,但那个标签实际写在 engine.js 里
    → 命中 0 次 → 条件恒假 → 守卫形同虚设。
    现象极具迷惑性:第二遍执行打印了 BACKUP_SKIP(备份那半段的守卫是好的),
    却照样跑完了安装 + 滚动重启(完成标记那半段是废的)—— 看着像"幂等生效了",实际没有。
    bash # 写完守卫,先在本地把每个特征串数一遍(都应为 >0) grep -c 'charSheetPrompt' engine.js # 2 grep -c 'unitCountFor' lib/pipeline.js # 10 grep -cF 'vfx: vfx' lib/llm-steps.js # 1
    另:多个标记最好落在多个不同文件(防单个文件被回滚后守卫误判"已装");
    含 | ( 等字符的串用 grep -F 避免被当正则。

  6. ⚠️ 「完成标记」不要用 touch 一个标记文件,要用「结果本身」当标记(2026-09-15 实测):
    bash # ❌ 守卫生效了,但把证据盖掉了 [ -f /tmp/relaykit/.done ] && { echo "SKIP: 已执行过"; exit 0; } mkdir -p /tmp/relaykit; touch /tmp/relaykit/.done ... 真正干活 ...
    问题不在"守卫失效",而在守卫生效得太好:执行环境会把同一条命令串行跑两遍,
    第一遍真跑(拿到的才是真数据),第二遍命中标记打印 SKIP ——
    而这行 SKIP 把第一遍的真实输出整个盖掉了,你只看到 SKIP,一份数据都拿不到。
    正确写法:以「输出文件」为完成标记,第二遍直接 cat 复用:
    bash # ✅ 两遍看到的是同一份真实结果 OUT=/tmp/job.out [ -s "$OUT" ] && { cat "$OUT"; exit 0; } { ... 真正干活 ... } 2>&1 | tee "$OUT"
    这个模式已固化成通用执行器 scripts/run-probe.sh(跨机投递 + 双端 md5 校验 + 输出落盘),可直接复用。

大文件内容(如新增模块)可 base64 内嵌进脚本,实现「一次连接完成全部」。

> ⚠️ 脚本报 ALREADY 不等于成功。EACCES 写入失败也会走到跳过分支;工具重试导致脚本跑两次同样全报 ALREADY。
> 每次打完补丁必须上机 grep 每个改动点验真,不能信脚本输出。

四、验证与重启(顺序不能颠倒)

# 1) 改完立刻语法检查,错误必须在这里拦住
for f in server.js app.js lib/*.js; do node --check "$f" || echo "FAIL $f"; done

# 2) 滚动重启(多实例逐个,避免整体中断)
for p in 3000 3001 3002; do
  sudo systemctl restart "<unit>@$p"; sleep 7
  curl -s -m 6 "http://127.0.0.1:$p/api/health"          # 单实例健康
done

# 3) 外网 + 日志双验证
curl -s -m 8 https://<domain>/api/health
sudo journalctl -u '<unit>@*' -n 20 --no-pager --since '-2min' | grep -iE "error|fatal"

若改的是前端静态资源,额外验证外网取到的版本(证伪 CDN 缓存):
curl -s https://<domain>/app.js | grep -c "新加的标识串"

四·五、验证技巧:token 拿不到时,直接 require 派生

管理后台类接口常需要 admin token,而 token 未必在以为的位置(可能已从 env 文件移除、改为账号登录)。拿不到 token 时不必卡住 —— 直接在服务器上 require 业务模块,打印派生结果,这比走 HTTP 更硬:

node -e "
const gen = require('/opt/app/lib/generators.js');
Object.keys(gen.PROVIDERS).forEach(id => console.log(id, JSON.stringify(gen.PROVIDERS[id])));
"

能直接证明「UI 数据源里有没有这一项」。注意此时进程没有 systemd 注入的环境变量(DATA_DIR 等会走默认值),结论只对代码派生逻辑有效,不要用它判断数据文件内容。

> ⚠️ 调用前先打印 fn.toString() 确认真实签名(2026-09-15 实测,差点误报):
> 直接 require 业务函数取值时,签名猜错不会报错,会静默返回一个"看起来合理"的值。
> 真实案例:某计费函数的签名是 4 个位置参数 (provider, type, unitCount, u),
> 按"一个配置对象"的直觉写成 f({provider, type, unitCount}) →
> 内部 rates[provider] 变成 rates['[object Object]'] → 取不到费率 → 函数开头的
> if (!pr) return 0; 直接命中 → 所有样例金额整整齐齐输出 0。
> 这个"全 0"长得极像"计费坏了",差点被当成资损缺陷写进报告(真因只是探针调用姿势不对)。
> js > console.log(String(mod.someFn)); // ① 先看实现,确认真实签名 > console.log(mod.someFn(a, b, c, d)); // ② 再按真实签名调用 >
> 原则:「整齐的 0 / 整齐的 null」优先怀疑自己的调用姿势,而不是被调用方有 bug。

> 用 HTTP 调 admin 接口若鉴权方式已变,可能静默拿到登录页 HTML(302 重定向)而非报错 —— 极难发现。看到 HTML 响应先怀疑这个。

四·六、密钥类文件:用只读探针,别用肉眼判「损坏」(2026-09-14 血亏)

审计含私钥/证书的代码(payment.js、证书配置等)时,别把 grep 到的内容当真。

踩过的坑:某平台 payment.js 的 PEM 头 grep 出来是「5 个横杠 + 一串随机字母」,而且两台机器的串还不一样 → 判定「PEM 被脱敏流程改坏了」并写进了交付报告。结论是错的 —— 那是输出脱敏在渲染时把疑似密钥替换成了随机串,文件本身完好。

识别指纹:同一个文件的同一处内容,两次读取得到不同的串 ⇒ 是脱敏,不是损坏。 凡「乱码」值在不同次输出里不一致,一律先按脱敏处理,别急着"修"。

正确姿势:布尔 / 计数探针(只输出判定结果,不打印任何材料):

const s = require('fs').readFileSync(F, 'utf8');
const D = '-'.repeat(5);                        // 关键:命令文本里也别出现 PEM 字面量
console.log('PKCS8:', s.includes(D + 'BEGIN ' + 'PRIVATE KEY' + D));
console.log('PKCS1:', s.includes(D + 'BEGIN ' + 'RSA PRIVATE KEY' + D));
const body = s.split('\n').filter(l => /^[A-Za-z0-9+/]{40,}={0,2}$/.test(l.trim()));
console.log('base64 正文行数:', body.length);    // 0 = 只有格式串,没有真实密钥实体

四·七、证明「重启真的发生过」:用 /api/health 的 uptime 反推,不碰 22 端口

改完 EnvironmentFile 类的配置(/etc/xxx/env)后,必须重启才生效(systemd 只在启动时读入进程环境)。
但「重启过了」这件事,别只用被检查机器上的证据(systemctl show、journalctl 都在那台机器上,
且脚本自述更不可信)。拿一条外部、独立、无法伪造的证据:

# 外网直接读健康端点,看进程 uptime
curl -s https://<domain>/api/health
# → {"ok":true,"uptime":1081,"version":"1.9.7","port":"3000",...}

把 uptime 从当前时刻反推启动时刻,与 journal 记录的滚动重启时间比对:

观测 值
读取时刻 12:05:10
uptime 1081s
反推启动时刻 11:47:09
journal 记录的滚动重启 11:47:11 / 11:47:12 / 11:47:13
结论 吻合(±2s) → 那次重启真实发生,且此后没有二次重启

为什么这条比上机查更硬:
1. 不碰 22 端口 —— 绕开「短时多次建连即封禁」那个坑(见 §一 的封禁说明),零风险。
2. 不依赖服务器上任何脚本自述、也不依赖 journal(本机可被改写)。
3. 一次请求同时验证「服务活着 + 重启时刻对得上 + 配置注入的进程是同一个」三件事。

⚠️ 两个前提,别用错场景:
- uptime 是进程级的。若服务做了 warm reload / 优雅重启(不重建进程),uptime 不会归零,
此法失效 —— 只适用于「restart 真的换进程」的场景(systemd Type=simple 默认如此)。
- 多实例时健康端点通常只由其中一个实例应答 → 反推出的是那个实例的启动时刻。
滚动重启各实例间隔几秒,所以应当看「落在重启窗口内」,而不是要求与某一个实例秒级相等。

两个附加收益:

  • base64 正文行数 = 0 证明文件里没有嵌入真实私钥/证书 → 这是把含密钥的文件提交进仓库前的必查项。
  • 反之若正文行数 > 0,说明真有密钥实体,不要 commit,先改成读环境变量。

> 命令本身也会被拦:命令文本里出现 PEM 字面量会触发敏感审批;一旦超时,整条命令被丢弃且没有任何输出(看起来像命令没执行)。用 '-'.repeat(5) 拼接构造标记即可规避。

四·八、部署工具脚本本身也是代码:别让它只活在 /tmp(2026-09-15 事故)

滚动部署常配一个「前置安全检查」,例如"此刻是否还有任务在跑?有就中止部署"。
这类脚本有个极隐蔽的失效模式:它一直只活在跳板机/生产机的 /tmp 里,从未进仓库。

真实事故:某次 e2e 的前置检查输出为空,部署侧按「未知」继续 ——
于是"错开线上任务"这个保证根本没兑现。排查发现逻辑本身是好的
(逐项复现:能定位数据文件、能 require 依赖、查得到 0 条)。
真因是版本时序:

  • 该脚本此前修过一次(第一版查错了数据源:查 PostgreSQL,而目标表在 SQLite)
  • 但修正只存在于临时目录,部署时打包的是修正之前的版本
  • 于是生产上跑的仍是"查错库"的旧版 → 查询失败 → 静默输出空 → 显示"未知"

同一个 bug 因同一个原因,现形了两次。

两条硬规则

  1. 部署/运维脚本必须进仓库(放 scripts/ 等),与业务代码同等对待、一起提交。
    只活在 /tmp 的脚本 = 没有版本管理 —— 它会在你最需要它的时候悄悄退化成旧版,
    而且没有任何迹象提示你。

  2. "检查失败"的语义要显式设计:
    - ❌ 纯静默失败最危险 —— 调用方看到"未知"会当作良性状态继续,安全检查形同虚设;
    而一个会静默失败的检查脚本比没有检查更糟,因为它让人以为已经检查过了。
    - ✅ 折中做法:stdout 仍输出空/中性值(保持调用方兼容,不因工具故障阻断发布),
    但必须往 stderr 打醒目报错,并带上关键上下文(DATA_DIR、ROOT 等)。
    让操作者在部署输出里一眼看见「这次其实没检查成」。
    - 给"探测不到路径"这类前置失败也单独告警 —— 否则它会被后续
    path.join(undefined, ...) 的 TypeError 掩盖成一句没头没尾的报错,排查时看不出真根因。

验证这种修复:照抄调用方的取法

不要"我觉得它能工作",要用与调用方完全相同的形式跑一遍:

# 部署脚本里是这么取的:
R=$(node scripts/check_running.js "$ROOT" 2>/dev/null | tail -1)
# 那验证就必须照抄这一行,看它到底得到什么(而不是单独 node 跑一遍就算数)

判定标准:修复后应得到 '[0]'(不再是空);再故意传一个错误路径,
确认 stderr 确实大声报错。

> 顺带一个 shell 陷阱:$(cmd || echo 0) 在 cmd 已输出 0 且退出码非 0 时,
> || 会再补一个 0 → 命令替换结果是 0\n0,于是 echo "x = $(...)" 打出两行。
> 计数类写法定型为 $(cmd 2>/dev/null || true) 后再判空,或直接 cmd 2>/dev/null; true。

四·九、多步子脚本:每一个都要检查退出码,否则整条流水线会「绿着失败」(2026-09-15 事故)

把部署拆成多个子脚本(改代码 / 改配置 / 改 nginx / 各步验证)是个好结构,
但有一个会让整次部署结论完全失真的坑:

> 主脚本只检查了"最后一步"或"关键一步"的退出码,其余子步骤失败后继续往下跑,最后照样打印 ALL DONE。

真实事故:一次部署要打 4 个补丁,其中「nginx 路由收敛」那步的子脚本里
投送目录常量写错(/tmp/lp4 vs 实际 /tmp/p4in)→ node 报 Cannot find module →
但主脚本没检查这一步的退出码 → 于是它接着跑完后面全部验证、
最后打印 PROD_..._DONE —— 从输出看和成功一模一样。

后果:交付报告会写「4 项全部完成」,而线上实际只完成了 3 项。"看起来成功"比"明确失败"危害大得多。

硬规则

  1. 分布在各节里的每一个子脚本调用,都要接退出码并处理:
    bash bash "$IN/step_x.sh" RC=$? if [ $RC -ne 0 ]; then echo " 🔴 步骤 X 失败(退出码 $RC)—— 该子项未完成" # 关键:不中断(后续步骤可能仍要跑),但必须留下失败标记 STEP_X_FAILED=1 else STEP_X_FAILED=0 fi
  2. 最后一节必须汇总标记,而不是无条件报成功:
    bash if [ "${STEP_X_FAILED:-1}" = "0" ]; then echo " ✓ 全部完成"; else echo " ⚠️ 第 X 项未完成,需单独重跑"; fi
  3. 子脚本自己要保证"失败时不留半成品":例如打 nginx 补丁的子脚本,
    必须在 nginx -t 失败时自动从备份回滚且不 reload,让线上保持原样。
    这样即使主脚本漏检退出码,也不会造成实际故障。

补跑时:只跑缺的那一步,别整条重跑

发现某一步没生效后,不要图省事重跑整个部署脚本 —— 已经生效的步骤会被再做一遍
(配置改写、滚动重启),每多一次重启就多一次停机窗口和一次风险。
写一个只做缺项的最小执行体,并在注释里写清「为什么只做这一步」。


四·十、生产上的 git 仓库可能早已与远端分叉 —— 别盲目 pull(2026-09-15 发现)

部署文档常写「生产机 git pull + 重启」。先验证这一步是否真的在发生,再决定要不要动它。

真实情况:某平台文档写「开发机 push → 生产 pull + 重启」,实际查下来:

开发机 生产机
是否 git 仓库 ✓ ✓
HEAD 与远端一致 落后很多
工作区 干净 大片脏(M LICENSE、D *.preview.html、M app.js、M lib/*.js … 20+ 项)

也就是说:生产一直靠"直接投文件"部署,git pull 从来没在跑,仓库是历史遗留。
此时在生产执行 git pull 是高风险动作 —— 会撞上那堆没人管过的本地改动。

做法:
- 部署不依赖生产 git 状态:代码正确性用逐字节比对兜底
(md5sum 开发机 vs 生产,逐文件确认一致),这比"git 说它是最新的"更硬。
- 复核时主动查一次 git -C <dir> rev-parse HEAD 与 git status --short,把分叉事实写进报告。
- 想对齐就单独立项(先备份那 20+ 项本地改动),不要在部署过程中顺手做。

更强的形态:单个文件的内容就与本地不一致(2026-09-15 实测)

比"仓库分叉"更隐蔽的是:同一个文件,生产版含本地版没有的补丁。

真实案例:某平台 server.js 的生产版多了 8 行限流治理逻辑
(P0-CODE-PASSTHRU 标记、code / retryCount / realError 字段),
这些改动从未回流本地工作区。若照例"本地改完整体覆盖上去",限流治理会被静默抹掉 ——
而且功能测试大概率还是绿的(限流只在被触发时才现形)。

识别:动生产文件前,先 diff 一次「生产版 vs 本地版」,
看到本地根本没有的内容就要警觉,别默认把它当成"本地领先"。

处置:
1. 把生产版拉下来当新基线(存为 _work/xxx.prod-base.js);
2. 在这个基线上重新施加本轮改动,而不是把本地版推上去;
3. 部署脚本末尾强制校验那些"只在生产存在"的标记仍在:
bash grep -c 'P0-CODE-PASSTHRU' server.js # 必须 ≥1,否则视为构建失败

原则:任何「整体覆盖」之前,先证明「被覆盖的那份里没有你不知道的东西」。

> 通用原则:文档里写的流程,必须周期性用"实际状态"去反证。
> 一个从不被执行的流程步骤,比没有这个步骤更危险 —— 它会让人误以为有那道保护。


四·十一、幂等锁会吞掉第一次执行的输出 —— 校验段要放在 SKIP 之外(2026-09-15 实战)

症状:部署/探针脚本明明跑成功了,屏幕上却只有一句 SKIP 已跑过(锁存在);
第一次真跑出来的那一大段证据(md5、标记计数、健康状态、探针结果)一个字都看不到。

根因:本执行环境会把同一条命令串行跑两遍,而调用方只回显第二次的 stdout。
第二遍被幂等锁挡住 → 只打印 SKIP → 第一遍的证据永远丢失。

两层修法(叠加使用最好):

  1. 锁连带回放日志:执行时把输出落一份到 $LOG,SKIP 分支 cat 出来。
    bash LOCK=/tmp/.x.${TAG}.done ; LOG=/tmp/.x.${TAG}.log if [ -f "$LOCK" ]; then echo "SKIP 已跑过 → 回放上次输出:"; cat "$LOG" 2>/dev/null; exit 0 fi node probe.js "$@" > "$LOG" 2>&1 ; RC=$? ; cat "$LOG" [ "$RC" = 0 ] && touch "$LOCK" # ★ 只有成功才落锁,失败必须允许重试
    锁要按参数分文件(TAG),否则 dry 与 live 互相挡住;再给一个 --force 清锁重跑。
    ⚠️ 解析 flag 用 while case 循环而不是顺序 if —— 顺序敏感的实现会把
    --live 当成位置参数传下去(探针只会说"找不到项目",不报错)。

  2. 更好:把「校验段」整体放到 SKIP 判断之外,让脚本无论第几次执行都打印当前真实状态。
    这样它同时是部署脚本和巡检脚本,一次解决"证据被吞"与"事后核对"两件事。
    bash NEED=1 if grep -q '新标记' f.js; then NEED=0; echo "SKIP 已安装"; fi if [ "$NEED" = 1 ]; then ...解包/备份/替换/滚动重启... ; fi echo "=== 校验段(无条件执行)===" md5sum f.js ; grep -c 'P4标记' server.js ; curl -s -o /dev/null -w '%{http_code}\n' .../api/health
    ⚠️ 反例(当天踩的):SKIP 分支只 echo "已装过"; exit 0 →
    第二次执行只回一句 SKIP,第一次的部署证据全部丢失,只能再补跑一次独立校验。

同理:探针的"零成本 dry 模式"与"花钱 live 模式"要用不同 TAG,否则先跑的 dry 会把 live 锁在门外。

四·十二、跨机传二进制(图片 / 包)必须 base64(2026-09-15 实战)

Windows / Git Bash 环境下 ssh ... > local.bin 会在编码层破坏字节 —— 图片直接损坏,且全程不报错。

# 元信息(ls / md5sum)重定向到 stderr,stdout 只留纯净的 base64 流
remote_cmd='ls -l "$F" >&2; md5sum "$F" >&2; base64 -w0 "$F"'
cat key.pem | ssh jump 'cat > /tmp/k.pem; chmod 600 /tmp/k.pem; bash /tmp/fetch.sh /path/f.bin' > out.b64
base64 -d out.b64 > local.bin && md5sum local.bin      # ★ 与远端 md5 双边比对
  • 元信息必须走 stderr:混进 base64 流会让 base64 -d 直接失败或产出脏数据。
  • 落地后必须比对 md5 —— 只比字节数不够(损坏可能等长)。
  • 干净的 gzip/tgz 同理;走管道时在转发脚本里加 dev_sz == prod_sz 断言。

四·十三、一次要投多个文件且带新依赖时:投递前三查 + 自动回滚(2026-09-18 实战)

单文件热补丁的套路(§三)在「投一个带新 require 的新版本」时会直接把站点搞挂。
这次是上线一个新功能,最终同时投了 3 个文件(1 个覆盖 + 2 个新增),
过程中踩到一个把正常部署误判成失败的坑。以下三条前置检查缺一不可。

检查 1:新版本的 require 在目标机是否都存在(最容易全站挂)

Node 在模块加载期解析 require —— 缺文件不是"降级",是模块加载失败 = 全站挂(进程起来就崩)。

# ① 本地版新增了哪些 require?先 diff 出来
grep -nE "^const .*require\('\./" new/lib/pipeline.js
# ② 再逐个到目标机确认存在
for f in seed consistency; do
  ssh ... "test -f $APP/lib/$f.js && echo OK_$f || echo MISSING_$f"
done

不要只投"改动的那一个文件"。本例 dev 版 pipeline.js 顶部新增
require('./seed') + require('./consistency'),而生产两个文件都不存在 →
若只投 pipeline.js,平台当场全挂。

> 对照 §四·十(反向):那边讲"生产版含本地没有的补丁,小心覆盖抹掉";
> 这边讲"本地版含生产没有的依赖,小心加载即崩"。双向都要查。

检查 2:投递前用「只解析不执行」验证依赖完整性

直接 require('./lib/pipeline.js') 会执行模块顶层代码(建连接、起定时器)——有副作用。
用 Module.createRequire().resolve() 只走路径解析:

node <<'NODE'
const Module = require('module');
const req = Module.createRequire('/opt/app/lib/pipeline.js');
let bad = 0;
for (const m of ['./seed', './consistency', './frames', './store', './logger']) {
  try { req.resolve(m); console.log('  OK', m); }
  catch (e) { console.log('  FAIL', m, e.message.split('\n')[0]); bad++; }
}
process.exit(bad ? 1 : 0);
NODE

必须在重启之前跑:这一步失败就回滚,连重启都不用(零停机窗口)。

检查 3:确认健康端点的真名

不要凭直觉 curl /health。先枚举一次:

for ep in /api/health /health /api/healthz /api/ping /; do
  printf "%-16s -> %s\n" "$ep" "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000$ep)"
done

判据错会让"重启成功"被记成失败(或反过来)。⚠️ 前缀不同可能返回登录页 HTML 且 200(见 §四·五)。

分阶段投递:先 /tmp,再安装

阶段1  校验源 md5 → 建 bundle → 投目标机 /tmp/xxx-bundle/ → 双边 md5+wc 比对   ← 不触碰运行目录
阶段2  备份(固定目录 + ORIGINAL.md5)→ sudo cp → chown → node --check
阶段3  Require.resolve 依赖检查 → 滚动重启 → 逐实例健康 → 全局健康 → 写完成标记

好处:投递失败/中断不会留下半个文件;阶段 1 可反复重跑无副作用。

⚠️ stderr 重定向到固定 /tmp/<名> → 把正常部署误判成失败

真实事故:语法检查写成 node --check "$f" 2>/tmp/syn.err,三个文件全报"语法错误",
触发自动回滚。真因是:

bash: line 83: /tmp/syn.err: Permission denied

/tmp/syn.err 是上一轮部署以 root 身份留下的 644 root:root 文件;
本次以 ubuntu 运行无法覆盖它 → 重定向失败(退出码非 0)→ if node --check …; then 判为失败。

修法:禁用固定 /tmp/<名> 做 stderr 重定向,改命令替换:

if SYN_OUT=$(node --check "$APP/lib/$f" 2>&1); then
  echo "  OK $f"
else
  echo "  FAIL $f"; echo "$SYN_OUT" | sed 's/^/       /'; SYNFAIL=1
fi

> 一般化:任何"用固定临时文件名在两个进程/两种权限之间传递数据"的写法都是雷。
> 同族坑:共享 .state 文件被并发实例互相污染。
> 附带一条:这类误判发生在语法检查这一步,代价还算小(在重启前拦住了);
> 若发生在"重启后健康检查"那一步,就会白白回滚一次。所以检查步骤越靠前越好。

自动回滚:健康检查不过就自己退回,不留半坏状态

if all_healthy; then
  echo "$WANT_MD5" | sudo tee "$MARK" > /dev/null
else
  # 还原「被覆盖的文件」+ 删除「本次新增的文件」+ 重启回原状 + 再验一次
  sudo cp "$BAKDIR/pipeline.js" "$APP/lib/pipeline.js"; sudo chown ubuntu:ubuntu "$APP/lib/pipeline.js"
  sudo rm -f "$APP/lib/seed.js" "$APP/lib/consistency.js"   # ← 新增文件回滚 = 删除
  for p in 3000 3001 3002; do sudo systemctl restart "manju@$p"; sleep 5; done
  sleep 12; all_healthy && echo "回滚后恢复 200" || echo "回滚后仍异常 → 需人工介入"
fi

要点:
- 新增文件与覆盖文件的回滚动作不同(删 vs 还原),备份时就要分清哪类是新增。
- 回滚后必须再验一次:只回滚不验证 = 不知道回滚成功没有。
- 回滚后仍不健康要打印每个实例的 is-active + health + journal 尾部,别只报一句失败。
- ⚠️ 远端脚本 exit 1 的退出码必须被外层捕获,否则外层照样打印 DONE(见 §四·九)。
- 健康检查给一次二次机会(sleep 15 后重试),避免把"慢启动"误判成失败而白回滚一次。
- 备份目录用固定路径 + ORIGINAL.md5,不能用「时间戳名 + test -f」——
本环境命令会串行跑两遍,第二遍时源文件已是新版,备份里存的就是新版(回滚回去还是坏的)。

幂等 SKIP 判据要覆盖全部被投文件

不能只看主文件的 md5 —— 漏投一个依赖时,判据仍会认为"已装"。用
「完成标记 + 每个文件 md5 + 健康检查」三者同时满足才 SKIP(§四·十一 的校验段仍放 SKIP 之外)。

可复用脚本清单(本项目已入仓)

p11-recon-prod.sh(只读侦察)· p11-diff-prod.sh(dev↔prod 逐行 diff)·
p11-prod-lib-inventory.sh(依赖盘点)· p11-check-prod-env.sh(DATA_DIR/SITE_BASE 真值)·
p11-prod-precheck.sh(端点/用户/属主/磁盘)· p11-prod-deploy-stage1.sh(投 /tmp)·
p11-prod-deploy-stage23.sh(安装+重启+验证+自动回滚)· p11-prod-postmortem.sh(回滚后核实)·
p11-prod-verify-final.sh(幂等/外网/日志)· p11-probe-consistency-prod.sh(真机引擎探针)


四·十四、新增「运行时读取的静态资源」+ 代码换代:先资源、后代码(2026-09-20 实战)

§四·十三 讲的是「新 require 依赖」—— 缺了进程起不来,症状明显。
比它更隐蔽的是运行时才读取的静态资源(模型权重、wasm、字体、字典):缺了进程照常启动,
只有走到那条功能路径时才失败,失败形态往往只是一行日志,部署后极易被忽略。

顺序铁律

备份 → 静态资源 → 代码 → md5 判据 → 语法 → 滚动重启 → 健康验证

资源必须先于代码到位。原因:代码里常带出厂自检函数(checkVendorAssets() 之类),
它在启动/首次调用时对资源做存在性 + 体积下限硬校验。顺序反了 ⇒ vendor-missing: xxx
⇒ 该功能全线不可用(不是降级,是不可用),而健康检查仍是绿的。

与 require 缺失的差别(决定了要不要单独做一步)

缺 require 目标 缺运行时静态资源
进程能否启动 ❌ 启动即崩 ✅ 正常启动
首现位置 启动日志 / 进程状态 功能调用时
健康端点表现 全站 503 200(假绿)
检出手段 require.resolve(见 §四·十三) 只能靠资源清单 + 自检日志

⇒ 静态资源必须有独立校验步骤:既查「文件在不在」,也查体积 / md5
(自检函数本身就依赖体积下限,说明体积是被信任的判据)。

三件事耦合上线时,如何保留归因能力

「检测器换代 + 门控策略变更 + 阈值语义调整」这类多变量同时上线,用户常担心「出问题归因难」。
不能靠"分多次部署"解决(有时必须一次推完),要靠判据分层:

层 判据 失败时指向
资源层 自检日志无 xxx-missing + 资源 md5 全命中 资源没到位
代码层 启动日志打出新引擎 / 新版本的专属文案 代码没生效
行为层 关键常量计数(grep -c 新旧特征词,如 tier:'soft' 2 处 / 'hard' 0 处) 改动点漂移
服务层 逐实例 200 + 外网 200 + uptime 归零 重启 / 依赖问题

启动日志里的「版本 / 引擎切换」文案是最值钱的一条 —— 它是"新代码真的在跑"的直接证据,
比 md5 更硬(md5 只证明文件变了,不证明进程载入了它)。
⇒ 上线前就应确认代码里有这么一行日志;没有的话本步部署应顺手加上。

md5 判据用关联数组写,别逐条 echo 期望值

declare -A EXP=(
  ["lib/face-scorer.js"]=ac43ca6fb8600c1f11a37a9516868de9
  ["lib/vendor/face/yunet.onnx"]=4ae92eeb150c82ce15ac80738b3b8167
)
FAILN=0
for f in "${!EXP[@]}"; do
  got=$(md5sum "$f" 2>/dev/null | awk '{print $1}')
  [ "$got" = "${EXP[$f]}" ] || { echo "  FAIL $f got=$got want=${EXP[$f]}"; FAILN=$((FAILN+1)); }
done
echo ">>> MD5_FAILN=$FAILN"

逐条 echo "期望: xxx" 只是打印给人看,脚本自己并不比对 ⇒ 眼睛看漏就过去了。

sudo cp 覆盖已存在文件:属主不变

混合属主目录里,sudo cp -f 到已存在文件会沿用原 inode ⇒ 属主 / 权限保持原样,
不会因为 sudo 变成 root:root;只有新建的文件才以执行者身份落盘。
所以「一律 sudo cp」是安全的(§二 说的"部分成功"根源是权限不够,不是 sudo 造成的)。
落位后仍要 ls -l 确认一遍,别默认。


五、回滚

补丁脚本的 backup() 必须在任何写入之前调用,且每个文件保留一份改动前的原始备份。

注意:备份文件名若只用秒级时间戳,同一秒内的第二次运行会把原始备份覆盖成已改版本,导致回滚失效。要么加随机后缀,要么第一轮跑完立刻确认。

一轮改动涉及多个文件时,用统一时间戳后缀(如 .bak-featurex-<ts>),便于整套识别与整体回退。

回滚 = cp x.js.bak-xxx x.js + 滚动重启。整套一起退,不要只退一部分。

六、收尾

  • 清理中间备份,每个文件只留一份原始回滚点。
  • 删除跳板机上的临时私钥 /tmp/<prod-key>.pem(先清生产机,再删私钥)。
  • 在交付文档里写清:改了哪些文件/行、验证结果、剩余需人工操作的步骤、回滚命令。

验证清单

  • [ ] 摸清真实运行方式(ps / systemctl list-units / pm2 list),确认单元名与实例数
  • [ ] 确认 Environment=DATA_DIR 等数据目录,改的是真生效的那份
  • [ ] 补丁脚本幂等(先清理 → 特征串检测 → 写入)+ 保属主写入
  • [ ] 改动文件逐个 node --check
  • [ ] 上机 grep 验真每个改动点(脚本报 ALREADY 不代表成功)
  • [ ] 滚动重启后每个实例 systemctl is-active 全 active
  • [ ] 内网每个实例端口 + 外网域名双验证
  • [ ] 改前端资源:外网抓取确认新字符串存在(证伪 CDN 缓存)
  • [ ] 重启后 2 分钟 journalctl 无 error/fatal/EACCES
  • [ ] 每文件留一份原始备份作回滚点;清理中间备份
  • [ ] 清理生产机临时脚本 → 再删跳板私钥
  • [ ] 投静态资源时:资源先于代码,且自检日志无 xxx-missing
  • [ ] 多变量耦合上线:四层判据(资源 / 代码 / 行为 / 服务)逐层都有输出
  • [ ] 部署/运维脚本已进仓库(不是只活在 /tmp);安全检查失败时 stderr 有声,且用调用方同款取法验证过

脱敏说明(发布版)

本包在发布前做过脱敏:原文中的开发机 / 生产机 IP、生产域名、SSH 私钥文件名、
部署根目录已替换为占位符 —— <dev-host> / <prod-host> / <prod-domain> /
<dev-key>.pem / <prod-key>.pem / <dev-root> / <prod-root>。

示例命令里的占位符需要替换为你自己的环境取值后才可执行;127.0.0.1、ubuntu 等
非敏感标识未做替换。方法与判据部分完整保留,脱敏不影响任何结论。

常见问题

5 问
答案由本站依据条目字段整理
prod-server-hotpatch怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_6af9f15b/prod-server-hotpatch。安装方式为未标注。装完刷新技能列表或重开客户端。

prod-server-hotpatch支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

prod-server-hotpatch权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

prod-server-hotpatch收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

prod-server-hotpatch用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。