Runbooks

「发版」其实是三件不同的事

三条链路,动的东西完全不同,风险也完全不同。 把它们当成一件事,是这个岗位最容易出的事故。

基线:2026-08-12 的 gameboxclient 工作区快照。 本页每条断言都在源码里当场核对过,含两个用 Node 实测验证过的行为。

⛔ 读之前先明确

在拿到「哪些环境可以安全操作」的明确答复之前,这三条 runbook 都只用来读,不要照着做。

三条里有两条会动线上:Runbook A 提交到 SVN 仓库, Runbook C 会把所有在线玩家踢下线并重启服务器。 它们都没有「撤销」按钮。

三条链路一览

A · 资源发布 B · 客户端打包 C · 服务器更新
入口 /expert/release 命令行 npm run build:electron:* Dashboard 首页的「更新服务器」卡片
动了什么 本地隐藏工作副本 + 远端 SVN 仓库 只动你自己的机器(产出安装包) 线上游戏服务器
影响谁 拉取该资源的人 没人,直到你把包发出去 所有在线玩家(会被踢下线)
风险 ⚠️ 中 ✅ 低 🔴 高

Runbook A · 资源发布

入口 专家模式 → 发布(/expert/release)
实际动了什么 不是本代码仓库。是 <盘符根>/.hidden_resource/<渠道> 下的 SVN 工作副本
需要的凭据 SVN 账号密码 —— .env 里为空, 只能靠登录下发
不可逆动作 svnCommit()。提交前的自动 add / delete 也已经改了工作副本(见下)
成功证据 ⚠️ 不能只看界面提示 —— 见下方 100 个文件的坑
怎么停 提交前:关掉弹窗即可。提交后:只能 svn revert 或反向提交,需要有人有仓库权限
找谁 SVN 仓库管理员(待确认是谁)

A-1 · 先搞清楚它动的是哪个目录

这是最容易搞错的一点。workspace() (src/api/workspace/index.ts:96-102)返回的是:

getHiddenResourcePath() + '/' + gameEnv.VITE_API_CHANNEL_SVN

而 getHiddenResourcePath()(:75-86)最后一行是:

path.join(path.parse(baseurl).root, '.hidden_resource')

path.parse(x).root 取的是盘符根目录 —— Windows 上是 C:\,macOS 上是 /。 所以工作副本在 C:\.hidden_resource\<渠道>, 不在应用安装目录下,也不在你的代码仓库里。

⚠️ 两套工作区,别混

你的源码仓库(gameboxclient,当前有 9401 条 未提交改动)和运行时的 SVN 工作副本 (C:\.hidden_resource\…)是完全无关的两个东西。

git status 显示的那 9401 条跟发布内容毫无关系, 绝不能拿它推断「有什么要发」。要看发布内容,只能看发布页列出的文件列表。

A-2 · ⚠️ 自动的 add / delete 发生在你确认之前

点发布卡片后,complieproject_svn() (src/utils/extend/svn.ts:161-176)做的第一件事是:

if (flag) {
  await svnAddUnversionedFiles(localUrl, option)   // 把未纳入版本的文件 svn add
  await svnRemoveMissingFiles(localUrl, option)    // 把本地已删的文件 svn delete
}
const target = await svnStatus(localUrl)           // 然后才列状态给你看

也就是说,等弹窗出现、你看到那份文件列表时, 「加入版本控制」和「标记删除」这两件事已经做完了。 弹窗只决定要不要 commit。

❌ 别学

确认框之前就修改了状态。用户以为「我还没确认,所以什么都没发生」, 实际上工作副本已经被改了 —— 包括把一些你可能根本不想加进版本控制的临时文件 svn add 了进去。

规范做法是:先只读地算出「将要做什么」, 展示给用户确认,确认后再一次性执行 add / delete / commit。

实操影响:关掉弹窗不等于回到原状。 如果你只是想「看一眼有什么改动」,看完请检查工作副本状态, 必要时手动 svn revert。

A-3 · ⚠️ 一次最多只提交 100 个文件,多的静默丢弃

svnCommit()(svn.ts:268-284):

const commitList = localFiles.map(...)
SVN.commands.commit(commitList.splice(0, 100), mergeOption, function (error) {
  resolve(!error)      // ← 只要这 100 个没报错,就返回成功
})

Array.splice(0, 100) 返回前 100 个元素, 剩下的留在 commitList 里 —— 而 commitList 之后再没被用过。我实测验证过:

// 250 个文件的情况
传给 commit 的: 100 个
剩在 commitList 里没人管的: 150 个

❌ 真 bug

改动超过 100 个文件时,只有前 100 个被提交,其余静默丢弃, 而且界面照样显示成功(resolve(!error) 只反映那 100 个的结果)。

后果:「我明明发布成功了,但资源没更新 / 只更新了一部分」—— 这是本项目最难查的一类事故,因为界面给的是成功。

动手前必做:看发布弹窗里列出的文件数量。 超过 100 就不要直接提交,先跟人确认怎么分批。

规范做法是分批循环提交直到列表为空,并把每批结果汇总; 或者至少在超过上限时明确报错而不是假装成功。

A-4 · 全局闸门(但它比看上去弱)

只要 Jenkins 上有任何构建在跑,发布就走不下去 —— 包括纯本地的 SVN 提交。看到「当前有打包任务在进行中」是设计如此,不是 bug。 范围确实是「任何」:getActiveBuilds() 遍历 JOB_TYPES 里配置的全部 job (jenkins/monitor.ts:73、jenkins/config.ts:8)。

⚠️ 两个细节,别理解成「点不动」

① 卡片是能点的。模板里唯一的 :disabled 绑定是 userInfo?.user_type == 3(权限), 跟 Jenkins 状态无关 (views/Option/index.vue:250,278,304)。 闸门在 compileProject() 函数体内 (:90-110)—— 你点下去,它先去问一次 Jenkins, 然后才弹警告。所以是「点了才拦」,不是「点不动」。

② Jenkins 查不通时,闸门会静默打开。 getActiveBuilds() 整个循环包在 try 里, catch 只 console.error 就 return activeBuilds (jenkins/monitor.ts:99-103)—— 网络断了或 Jenkins 挂了,返回空数组, 调用方会当成「没有构建在跑」而放行。

规范做法:查询失败 ≠ 检查通过, 这两种情况必须能区分开 —— 现在它们都表现为一个空数组。 具体怎么改见 写操作前必查 · 提案 6。

Runbook B · 客户端打包与自动更新

入口 命令行,没有界面入口
实际动了什么 只动你自己的机器,产出 release/ 下的安装包
需要的凭据 无(本地构建)
不可逆动作 构建本身没有。但把包传到更新服务器之后, 所有客户端都会自动拉到
成功证据 release/ 目录下有对应平台的安装包
怎么停 传出去之前随时可以停;传出去之后要撤只能发新版覆盖
找谁 管更新服务器的人(待确认 —— 见下方地址不一致问题)

B-1 · build:pro 不是打包

命令 实际做什么 产出
npm run build:pro vite build --mode pro 只是前端资源(dist/), 不是安装包
npm run build:electron:pro rm -rf release/ && vite build --mode pro && electron-builder 这才是安装包
npm run build:electron vue-tsc --noEmit && vite build && electron-builder 带类型检查,但 vite build 没带 --mode —— 走的是默认环境,和 :pro 不是同一份配置
npm run build:electron:winpro 同 :pro 但不清 release/ Windows 包

在 macOS 上跑 build:electron:* 产出的是 macOS 包, 不是 Windows 安装包。要出 Windows 正式包必须在 Windows 环境。

B-2 · ⚠️ 打包的发布地址和客户端的更新地址对不上

配置在哪 指向
打包时的 publish electron-builder.json5:49-53 http://192.168.31.90:9009/latest.yml(内网 IP、HTTP)
客户端拉更新 electron/main/tools/updater.ts:66-69 https://39hezi-518qn-…cos.ap-shanghai.myqcloud.com/Gamebox_Develop/future(COS、HTTPS)

⚠️ 必须问清楚才能发版

打出来的包该传到哪、客户端到底从哪拉,这两处配置各说各话。 可能的解释:内网那个是给内部测试用的,COS 才是正式渠道; 也可能是某一处已经过期没人改。

在问清楚之前不要发客户端版本 —— 传错地方等于没发,传对地方但配置过期则可能推给所有用户一个错的包。

而且没有缓冲:客户端会立刻下载。 updater.ts:65 那句 autoUpdater.autoDownload = false 在 30 行后被抵消了 —— update-available 处理器里直接调了 autoUpdater.downloadUpdate()(:95), 中间没有任何用户确认:

// updater.ts:89-97
autoUpdater.on('update-available', (info) => {
  Logger.info('检查到有更新,开始下载新版本')
  autoUpdater.downloadUpdate()          // ← 无条件,没人问过用户
  sendUpdateMessage(message.updateAva)  // ← 这只是通知渲染层,下载已经开始了
})

autoDownload = false 关掉的是 electron-updater 自己在检查后自动下载的行为,代码随后又手动触发了一次。 渲染层收到 updateAva 时下载已在进行, 它是个通知,不是一道闸门 (全仓库搜不到第二处调 downloadUpdate() 的地方)。

所以误发的后果是全量的:包一旦传到客户端真正拉取的那个地址, 所有在线客户端下次启动检查时就会把它拉下来。 更要在发版前把这两个地址问清楚。

Runbook C · 服务器更新 🔴

🔴 这是全站风险最高的一个操作

它会把所有在线玩家踢下线,然后重启游戏服务器。 入口就在登录后的首页上。

📝 2026-08-14 订正

本节初版把两个不同入口混成了一个,并断言「没有二次确认」—— 三处都是错的。实际上两个入口都有确认框。 订正内容见下,出错经过记在 learning-records/0007。

入口 有两个,走的是不同组件、不同按钮 —— 见下方 C-1
实际动了什么 线上游戏服务器
不可逆动作 踢出所有在线玩家;重启服务进程。 且对你勾选的每一个区服都执行一遍
成功证据 任务面板四步全部变为已完成
怎么停 点下去就停不了了。四步是顺序 await, 中间失败会抛错中断,但已经踢掉的人不会自动回来
找谁 动之前找运营确认可以踢人的时间窗口

C-1 · 两个入口,别搞混

Dashboard 首页上有两张卡片都能通向「重启线上服务器」, 但走的是完全不同的组件。初版文档只写了一个,还写错了组件名。

入口一:「更新服务器」卡片 → 打包构建

①「更新服务器」卡片 (Dashboard/index.vue:1174-1180)→ showServerControlVisible = true

② 打开 ElDialog 标题「服务器管理」 (:1682-1685),内容是 ServeChooseView (:1696)—— 不是 serverControl.vue

③ 里面三个按钮:「表回滚」「一键开服」 「打包构建」 (serveChooseView.vue:36-38)

④「打包构建」→ startChangeServer(2) → ElMessageBox.confirm (:125-135)→ Emits('serverClose', arr, 2)

⑤ → serverBuild(data, 2) → method.ts:564 的 for (const item of data) → 对每个服调 startFun(item)(:582,没有 await)

入口二:「服务器管理」卡片 → 更新服务程序

①「游戏管理」区的「服务器管理」卡片 (Dashboard/index.vue:953-956)→ serverConrtolFunction()(method.ts:542)

② → serverControlRef.value.showCurrentVisible() → 打开 serverControl.vue

③ 四个按钮 openDia(1..4):修改服务器状态 / 下线所有玩家 / 打包服务程序 / 更新服务程序

④ 都只是转发给 EditStatusDialog.openModifyDiaglog(type) (serverControl.vue:133-135)—— 在那个弹窗里勾选区服后, 才真正提交

C-2 · 四步流程

入口一最终跑的是 startFun()(src/views/GameEditor/Dashboard/method.ts:593) 顺序执行,每步失败就 throw 中断:

① 服务器状态 —— changeServerStatus()

② 踢出所有人 —— tickAllPeople() → /1000y/sj/serverKickAll (method.ts:622-635)

③ 打包服务器 —— packServer() → /1000y/sj/packServer,成功判据是返回里含 Finished: SUCCESS(method.ts:637-644)

④ 重启服务器

C-3 · 确认框是有的 —— 但它不告诉你要动哪些服

两个入口都有 ElMessageBox.confirm:

入口 位置 文案
「打包构建」 serveChooseView.vue:125-135 打包构建会将选择的服修改为维护状态,并踢人下线,服务器打包构建。
「下线所有玩家」 EditStatusDialog.vue:42 此操作会强制服务器内的所有玩家下线,请确认操作
「打包服务程序」 EditStatusDialog.vue:118 此操作会编译打包新的游戏服务器程序,请确认操作
「更新服务程序」 EditStatusDialog.vue:186 此操作将更新部署并启动新的服务器程序,请确认操作

文案统一定义在 src/views/Database/const.ts:59-61 的 WarningInfo。 serverControl.vue:55-62 那个 ElTooltip 是悬停提示,确认框在后面的弹窗里 —— 初版文档看到 tooltip 就下了「没有确认框」的结论,是只看了一层。

⚠️ 真正的风险在这里

确认框的文案里没有区服名字,也没有在线人数。 它只说「选择的服」,而你要动的是你刚才勾选的全部区服:

// method.ts:564-584
for (const item of data) {
  if (index == 1) { await changeServerStatus(item.sId, 1) }   // 一键开服
  else if (index == 3) { excelBackByCopy(...) }               // 表回滚
  else { startFun(item) }        // ← 注意:没有 await
}

勾错一个服,确认框不会帮你发现 —— 它长得跟只选一个服时一模一样。

另外注意 startFun(item)(:582) 前面没有 await,而 startFun 是 async 的。 所以勾了 N 个服,是 N 条「踢人 → 打包 → 重启」流程同时开跑, 不是排队一个一个来。出问题时它们的日志会交错在一起。

规范做法:不可逆且影响线上用户的操作, 确认框里要把目标对象名字逐个打出来 (「即将重启 XX 服、YY 服,当前在线 N 人,确认?」), 让用户有机会发现自己选错了。

在改代码之前,你能做的: 点确认之前,回头看一眼勾选列表, 而不是只看确认框那句话。

站点版本 dcc04aa · 2026-08-14 内容基线 gameboxclient @ 2026-08-12