Skill 方案 S2 · 站点重启与隧道切换
一句话:把「重启考研学习导航门户、词汇站和 natapp 隧道,按以下顺序严格执行」这条被复述了几十遍的指令,变成一条不会被顺序坑到的命令;约 40 次完整重启链路消耗的 0.2 亿 token,可压到 0.03 亿。
一、为什么需要它
扫描里命中「按以下顺序严格执行」这一模板的会话有 50 个,累计 0.31 亿 token;其中包含完整重启链路(停三个服务 + 清端口 + 起隧道 + 验健康)的约 40 次,合计 0.2 亿 token。
| 指标 | 数值 | 说明 |
|---|---|---|
| 模板会话 | 50 个 | 用户原话高度固定,只换服务名 |
| 完整重启链路 | 约 40 次 | 其余是单服务重启 |
| 累计 token | 0.2 亿 | 平均每次 5 万 |
| 平均工具调用 | 9–14 次 | 停、查端口、起、再查、再看日志 |
| 一次成功比例 | 约六成 | 其余要二次返工 |
现状痛点不是"不会重启",而是顺序和副作用:
- 顺序错就白停一次。先停门户再停隧道,公网会在这几十秒里持续吐 502;先起隧道再起门户,Cloudflare 侧会把连接打到空端口上,日志里留下
connection refused,下一次巡检容易误判成隧道故障。 - 端口清理不彻底。
Stop-ScheduledTask只结束任务实例,launch.ps1拉起的 node 子进程有时留成孤儿,继续占 3000/3001/3102,下一次启动直接EADDRINUSE。 - Prisma 引擎 DLL 锁。笔记站的 query engine 落在
notes/generated/db3,服务一跑起来这个 DLL 就被独占,此时手工prisma generate必然EPERM。这是本机最反复踩的坑,schema 从generated/prisma一路换到db3就是被它逼的。 Start-Process起不来。本机同时存在HTTPS_PROXY与https_proxy,PowerShell 会抛「已添加项」,任何子进程都起不来;所以隧道只能由计划任务在-Command里直接& cloudflared.exe。
二、触发词与边界
| 触发词 | 不适用场景 |
|---|---|
| 「按以下顺序严格执行」「重启网站」「全量重启」 | 只是看看状态 → S1 巡检 |
| 「重启笔记站」「重启词汇站」「重启隧道」 | 刚改完代码要上线 → S3 构建发布 |
| 「端口被占了」「EADDRINUSE」 | 要改任务定义/触发器 → 单独确认后手改 |
| 「停一下所有服务」 | 要退公网入口 → rollback.ps1,不是重启 |
| 「重启后 502」 | 数据库迁移 → 不在本 skill 范围 |
边界:只操作已存在的计划任务与端口,不新建任务、不改 XML。任务缺失时停下来报告,附带 Get-ScheduledTask 的实际输出,不要凭记忆重建。
三、输入与前置检查
| 任务名 | 服务 | 端口 | 启动器 |
|---|---|---|---|
KaoyanPortal | portal/server.js | 3000 | launch.ps1 -Name portal |
KaoyanNextServer | 词汇站 Next | 3001 | launch.ps1 -Name vocab |
KaoyanNotes | 笔记站 Next | 3102 | launch.ps1 -Name notes |
KaoyanCLab | C 语言实验室 | 3103 | launch.ps1 -Name clab |
KaoyanTunnel | cloudflared | — | 任务内直接托管 |
前置三查:
Get-ScheduledTask -TaskName "Kaoyan*" | Select-Object TaskName, State
Get-NetTCPConnection -LocalPort 3000,3001,3102,3103 -State Listen | Select-Object LocalPort, OwningProcess
Get-Process node, cloudflared -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, StartTime
若 KaoyanTunnel 不存在或为 Disabled,先停下;用 ops\cf\install-task.ps1 重装是独立动作,需要用户确认。
四、执行步骤
步骤 1 · 停,严格按「外向内」的顺序。 每一步之间留 800 ms:
# 1) 先断公网入口,避免用户看到 502
Stop-ScheduledTask -TaskName "KaoyanTunnel"
Get-Process cloudflared -ErrorAction SilentlyContinue | Stop-Process -Force
# 2) 再停门户,不再接新请求
Stop-ScheduledTask -TaskName "KaoyanPortal"
# 3) 最后停下游
Stop-ScheduledTask -TaskName "KaoyanNextServer"
Stop-ScheduledTask -TaskName "KaoyanNotes"
Stop-ScheduledTask -TaskName "KaoyanCLab"
Stop-ScheduledTask 不会带走 cloudflared,必须像上面第二行那样显式强杀,否则会出现两个连接器抢 20241 指标端口。
步骤 2 · 清端口,按 PID 杀而不是按名字杀。
foreach ($p in 3000,3001,3102,3103) {
$c = Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
if ($c) { Stop-Process -Id $c.OwningProcess -Force -ErrorAction SilentlyContinue }
}
Start-Sleep -Milliseconds 800
杀完必须复查一遍,端口仍被占就再杀一次;残留的常驻 node 可能是 SYSTEM 权限,普通权限杀不掉,此时只能报错并提示需要提权,不要假装成功。
步骤 3 · Prisma generate 只能在这个窗口做。 端口已清、服务未起,是唯一能安全生成的时间点:
$N = "C:\Users\cag\.workbuddy\binaries\node\versions\22.22.2-6\node.exe"
& $N "C:\Users\cag\WorkBuddy\2026-10-11-06-00-16\notes\node_modules\prisma\build\index.js" generate `
--schema "C:\Users\cag\WorkBuddy\2026-10-11-06-00-16\notes\prisma\schema.prisma"
ops/launch.ps1 在 -Name notes 分支里已经内置了这一步(在杀端口之后、启动之前),所以日常重启不需要手工跑;这里列出来是为了排障——如果 prisma-generate.log 里有 EPERM,说明顺序被打乱了,服务当时还在跑。
步骤 4 · 起,严格按「内向外」反序。
Start-ScheduledTask -TaskName "KaoyanNextServer"
Start-ScheduledTask -TaskName "KaoyanNotes"
Start-ScheduledTask -TaskName "KaoyanCLab"
Start-Sleep -Seconds 12 # Next 冷启动约 8-12 秒,太早起门户会反代失败
Start-ScheduledTask -TaskName "KaoyanPortal"
Start-Sleep -Seconds 3
Start-ScheduledTask -TaskName "KaoyanTunnel"
0x800710E0 = 有卡死实例,先 Stop-ScheduledTask 再 Start,脚本里作为固定分支处理,不要重试同一条命令。
步骤 5 · 健康断言,逐层放行。
(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:3000/healthz -TimeoutSec 8).Content
(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:3001/vocab/ -TimeoutSec 8).StatusCode
(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:3102/notes/ -TimeoutSec 8).StatusCode
(Invoke-WebRequest -UseBasicParsing https://cag.quest/ -TimeoutSec 15).StatusCode
/healthz 返回 {"portal":"ok","vocab":"ok","notes":"ok","clab":"ok"} 才算门户层通过——它内部会分别探三个上游,比逐个 curl 更快也更准。
步骤 6 · 隧道切换(仅 natapp 模式需要)。 读 mode.txt:tunnel 走 Cloudflare 固定域名,无需换域名;非 tunnel 则读 natapp.log 尾部 256 KB 里最后一次 Tunnel established at,再与 domain-state.json 比对,变了才落 outbox/mail-*.json。日志已数 MB,整文件读取会拖慢整个流程。
五、输出与验收
{
"stopped": ["KaoyanTunnel", "KaoyanPortal", "KaoyanNextServer", "KaoyanNotes"],
"portsFreed": [3000, 3001, 3102, 3103],
"prismaGenerate": "ok",
"started": ["KaoyanNextServer", "KaoyanNotes", "KaoyanCLab", "KaoyanPortal", "KaoyanTunnel"],
"health": { "portal": "ok", "vocab": "ok", "notes": "ok", "clab": "ok", "public": 200 },
"elapsedSec": 38
}
三条硬断言:
- 四个端口必须全部出现在
portsFreed里,任一端口仍被占用即失败退出,不许带病启动。 /healthz的四个字段全为ok,且公网https://cag.quest/返回 200;本地全绿但公网非 200 视为隧道层失败。- 进程数守恒:重启前后
node进程数与站点数一致(门户 1 + 词汇站 1 + 笔记站 1 + 实验室 1),多出来就是孤儿进程。
六、失败处理与回滚
| 现象 | 根因 | 动作 |
|---|---|---|
EADDRINUSE | 旧进程没杀干净 | 回步骤 2,按 PID 强杀后重跑 |
prisma generate EPERM | 服务还在跑,DLL 被独占 | 停掉 3102 对应进程,再 generate |
Start-Process 报「已添加项」 | 同时存在 HTTPS_PROXY 与 https_proxy | 不要用 Start-Process,走计划任务 |
门户起但 /healthz 里 vocab=down | 上游没起够 12 秒 | 等 10 秒重探,不要立刻重启 |
| 公网 502 而本地全绿 | 隧道连到了旧端口/旧进程 | 强杀 cloudflared 后单独重启 KaoyanTunnel |
任务启动被拒 0x800710E0 | 卡死实例 | Stop-ScheduledTask 后再 Start |
回滚:重启本身是幂等的,重复执行不会叠加进程,所以"回滚"只有一种——停止本次改动。若刚才是全量重启且用户要求恢复原状,按同样顺序再重启一次即可;若涉及隧道模式变更,用 ops\cf\rollback.ps1 -Check 先试运行再执行。
七、收益测算
| 项目 | 现状 | 改造后 |
|---|---|---|
| 单次 token | 5 万(9–14 次工具往返 + 日志阅读) | 0.8 万(一条命令 + 读 JSON) |
| 次数 | 约 40 次 | 约 40 次 |
| 累计 | 0.2 亿 | 0.03 亿 |
| 单次耗时 | 3–8 分钟 | 40–60 秒 |
| 二次返工 | 约四成 | 接近零(顺序与 Prisma 窗口写死) |
省下的主要是返工:现状每次失败都要重新走一遍"停—查—起—查",一次返工就是 5 万 token 再来一遍。节省 0.17 亿,约 85%。