Project Map · Architecture

五张图看懂 GameBox 怎么转

每张图都能逐步播放。先看全景,再点「下一步」跟着数据走一遍。

基线:本页基于 2026-08-12 的 gameboxclient 工作区快照(未提交状态,git HEAD 仍是旧基线)。所有 file:line 引用当时逐条核对过;一旦有人提交或改动,行号会漂移, 文件名和函数名仍然有效。

先说人话:这个东西到底是什么

GameBox 是一个装在电脑上的桌面程序,策划和美术每天用它改游戏里的 NPC、物品、掉落、任务、地图,改完点发布,资源就上到测试服或正式服。

它不是网页,但它里面装着一个网页。Electron 干的就是这件事: 给你一个浏览器内核负责画界面,同时给你一个 Node.js 进程负责干浏览器干不了的活 —— 读写本地文件、调用 SVN 命令行、fork 子进程解压几个 G 的资源包。

所以看这个项目,第一件要分清的事永远是:我现在看的这段代码,跑在哪个进程里? 这决定了它能用什么 API、出错时去哪看日志、以及为什么有些代码在浏览器里根本没法调试。

FIG. 01进程与边界:代码到底跑在哪

渲染进程 · RENDERER 主进程 · MAIN preload 桥 从未被加载 vite.config.ts:58-72 整段注释 options.ts 里也没有 preload 键 Vue 3 页面 / Pinia src/views/** · src/store/** 直接 require('fs') src/utils/extend/svn.ts:1 @electron/remote 出口 src/utils/extend/electron.ts:1 ipcRenderer 直接 import from 'electron' 窗口管理 helpers/window.ts:25 自动更新 / 日志 tools/updater.ts · electron-log Remote.enable() helpers/window.ts:59 ipcMain 处理器 ×21 handler/channel.ts 1 send / invoke ↔ on / handle 2 渲染层等于拿到完整 Node 权限 fork 子进程 extend/worker/*.js 3 下载 · 解压 · 打包 渲染进程的 webPreferences:nodeIntegration: true · contextIsolation: false · webSecurity: false(electron/main/config/options.ts:6-8)

这张图最该记住的是那个虚线空框:本项目没有 preload。 标准 Electron 应用靠 preload 脚本在两个进程之间开一道窄门, 只把明确列出的方法暴露给页面。这里没有,取而代之的是把整个 Node 环境直接交给了渲染层。

为什么「没有 preload」是件大事

electron/preload/index.ts 这个文件是存在的,里面还老老实实写着 contextBridge.exposeInMainWorld。但它从来没被构建过 —— vite.config.ts:58-72 把整个 preload 构建块注释掉了, electron/main/config/options.tswebPreferences 里也没有 preload 这个键。

结果是:想在渲染层用 Node,唯一的路就是直接 import。 所以你会在 .vue 文件里看到 import { ipcRenderer } from 'electron'import Fs from 'fs'const SVN = require('node-svn-ultimate') 这些在正常 Web 项目里根本不该出现的东西。它们不是写错了,是这个项目的既定架构。

⚠️ 隔离

渲染层全开 Node 权限options.ts:6-8)。 这是架构级决定,几百个文件都建立在它之上,你接手第一周绝不该动它。 但要清楚代价:渲染层加载的任何第三方内容都拥有读写整台机器的能力, webSecurity: false 还关掉了同源策略。

规范做法是保持 contextIsolation: true, 用 preload + contextBridge 只暴露必要方法。见 Electron · Context IsolationElectron · Security

你现在该做的:新写的代码不要继续扩散 @electron/remote。要跟主进程说话就老实加一个 IPC 通道, 至少让边界是显式的、可数的。

它是怎么启动的

从双击图标到看见登录框,中间有八步。搞清楚这八步, 你才知道「界面白屏」这类问题该往哪一步查。

FIG. 02启动时序:从双击图标到看见首页

STEP 1 主进程入口 electron/main/index.ts ready() STEP 2 灌环境变量 handler/ready.ts:10 processHandler() STEP 3 建主窗口 helpers/window.ts:25 createMainWindow() STEP 4 加载页面 dev: VITE_DEV_SERVER_URL prod: dist/index.html ↓ 从这里开始,代码跑在渲染进程里 STEP 5 Vue 装配 src/main.ts:15 setupAll() 七步 STEP 6 环境进 Pinia src/App.vue:40 gameEnv.init(process.env) STEP 7 注册动态路由 src/App.vue:49 generateRoutes() 零过滤 STEP 8 守卫拦截 src/permission.ts:86 无 token → /login 登录成功后才跳 /game/dashboard(views/ReLogin/index.vue:165-169) STEP 7 只是 [...DynamicRouterMap] 加一条 404,没有按角色过滤任何路由 —— 菜单看不见 ≠ 进不去,手敲地址栏(hash 路由)照样能到

排障用法:白屏且没报错,先看是卡在 3–4 步(窗口起了但页面没加载) 还是 5–6 步(页面加载了但 Vue 没装配)。前者去主进程日志找, 后者按 F12 看渲染层控制台。

⚠️ 隔离

generateRoutes() 名不副实。 src/store/modules/permission.ts:37-56 里它只做了 [...DynamicRouterMap] 加一条 404 兜底,然后全部 router.addRoute()。函数名和文件名都写着「权限」, 但没有任何一行按用户角色过滤

这意味着:菜单里藏起来的页面(meta.hidden) 并不是「无权访问」,只是「不显示入口」。真正的授权只可能在服务端。 排障时如果有人说「他不该能进那个页面」—— 前端确实没做角色过滤,但守卫会查 token (src/permission.ts:129-137), 未登录仍会被重定向到 /login?redirect=…。 所以准确说法是:已登录用户之间没有权限区分

配置的三个来源:兜底、覆盖,还是永远为空

这是整个项目最容易搞错的一条链路 —— 我自己第一版就搞错了,而且错得很典型。

直觉是:.env 放开发配置,真实凭据靠登录下发。 但这个项目不是那样。.env 里已经有一整套能跑的完整配置:42 个键,含 GM 地址、pcode、渠道、SVN 仓库地址、Jenkins 地址凭据, 以及 11 个 Jenkins job 名。

登录做的是覆盖,不是填充initEnv() 改写 14 个键 —— 而这 14 个在 .env全都已经有条目

所以真正决定「能不能用」的不是「登录没登录」,而是那个键的兜底值是不是空的

FIG. 03兜底与覆盖:配置到底从哪来

配置来源 ① .env(42 个键) GM · pcode · 渠道 · SVN 地址 Jenkins 凭据 + 11 个 job 名 但 7 个键的值是空的 ② process.env ready.ts:11-13 逐个灌入 :14-19 再追加 DIST_EXTEND 等 ③ POST /user/info → userInfo.game api/login/index.ts:74 initEnv() 覆盖 14 个键 utils/extend/envOption.ts:8 SVN · Jenkins · GM · 渠道 userInfo.cos 传进来了,但函数体里没读它 GameEvn.ENV store/modules/GameEvn.ts App.vue:40 gameEnv.init() 运行时唯一的读取入口 覆盖 localStorage 键名 GameENV · 明文 GameEvn.ts:44-49 深度 watch 写入 优先读回 ⚠ 启动时只要 localStorage 有 GameENV, 就整个用它,根本不看 .env → 改了 .env 不生效多半是这个 ⚠ SVN 密码 / Jenkins token 明文落盘在这里 🟠 有兜底 axios / GM · Jenkins 不登录也能连,但连的是 .env 那套 🔴 无兜底 SVN 账号密码(.env 里为空) 必须靠登录下发 🔴 两边都没有 COS / OSS 密钥 .env 为空,initEnv 也不写 全项目 3 处读 · 0 处写 ⚠ 兜底 pcode 的陷阱 .env 三个 pcode 都是 boxTestLS 名字带 Test,但不含 _dev getBaseURL() service.ts:79 → 实际打到正式 GM .env* 定义 49 个 VITE_ 键,源码用到 50 个

排障用法:「点了发布没反应」「SVN 提交报没权限」这类问题, 第一步永远是敲 JSON.parse(localStorage.getItem('GameENV'))。 但「有值」不等于「对」 —— 还要看它是登录下发的, 还是 .env 的兜底值。完整分类见环境变量速查卡

❌ 别学

凭据既进 .env(且被 git 跟踪),又明文落 localStorage。 .envVITE_JENKINS_TOKEN 有真值, 而这个文件在版本控制里;同时 GameEvn.ts:44-49 的深度 watch 会把整个 ENV (含登录下发的 svn_password)写进 localStorage, 没有加密、没有过期。而渲染层又是 webSecurity: false 的全 Node 环境。

规范做法是凭据只留在主进程,渲染层通过 IPC 请求主进程代为执行(「帮我提交这些文件」), 而不是把密码交给渲染层自己拼命令行;主进程侧用 Electron safeStorage 走系统钥匙串加密。构建期配置则不应把生产凭据写进受版本控制的文件。

你现在该做的:先别改,但把它记进风险清单 —— .env 里那个 token 是你跟安全或运维对话时的第一个议题。

主业务:改一条 NPC 配置,数据是怎么走的

策划打开「NPC」页,搜一个 NPC,点进去改「刷新时间」,点保存。 这个动作在代码里要穿过两条并行的链路,最后在表单组件里汇合。

数据链负责「这个 NPC 现在的值是多少」,走网络。
描述链负责「这张表有哪些字段、每个字段该用什么控件、选项有哪些」, 走本地的 schema 文件。

分清这两条是理解本项目的关键:绝大多数「加个字段」「改个下拉选项」的需求,只动描述链,碰都不用碰页面组件。

FIG. 04配置表编辑:数据链 × 描述链

数据链 · 这个 NPC 现在是什么值 描述链 · 这张表有哪些字段 编辑页 Npc/NpcEdit.vue 调 api/game 的函数 请求拦截器 axios/service.ts:115 带上 Authorization 1000y 特判 service.ts:88 excelToken · pCode · 换 URL GM 服务 读: /1000y/engine/cmd 写: /1000y/sj/cmd GameEditor store store/modules/GameEditor.ts ViewData.NPC.currentData 响应回填 16 份 schema 文件 src/store/config/*.ts Item.ts 5647 行 · Npc.ts 1881 行 table store store/modules/table.ts:9 import.meta.glob → Map 取本表 schema getConfigByKey('Npc') table.ts:196 PropEditor src/components/PropEditor/index.vue 按 type / editor 挑控件:Select · Switch · Time … :row 数据 :config 描述 写回 · 发两个请求 ① cmd: 'excel_modify' ② cmd: 'excel_save_to_oss' Promise.all → 局部成功难判 table store 的 init() 在登录成功 1500ms 后才被调(views/ReLogin/index.vue:195-198)

最实用的一条:策划说「NPC 表里给我加个字段」, 你要改的是 src/store/config/Npc.ts(加字段定义)和 src/views/GameEditor/Npc/view.ts(决定它显示在哪个标签页), 不是 NpcEdit.vue。页面组件是通用的,它只负责按 schema 渲染。

这个后端有两种风格,别只记住一种

我第一版写的是「所有业务共用一个 /1000y/engine/cmd」—— 这是错的,而且方向反了。真实分布(统计 src/api/ 下所有 url: 字面量):

风格 端点 处数 怎么看出它在干什么
具名端点(多数派) /1000y/sj/packServer · /1000y/sj/serverKickAll · /1000y/sj/gameNoticeAdd 61 看 URL 就够了
cmd 分发 /1000y/engine/cmd 4 必须展开 Payload 看 cmd, 全项目共 15 个不同的 cmd 值
/1000y/sj/cmd 5
非业务 /user/login · /user/info · /document/* · /journal/create 6 看 URL

排障顺序:先看 Request URL。只有落在 .../cmd 的那 9 处才需要展开 Payload 看 cmd 字段。 读操作(查 NPC、查表)走 engine/cmd写操作(excel_modify)走 sj/cmd

❌ 值班必知:「保存失败」不等于「没写进去」

excel_modify()src/api/gm/index.ts:259-296一次调用会发两个请求

const originalRequest = request.post({ url: '/1000y/sj/cmd', data: { …, cmd: 'excel_modify' } })

// 注意是 .finally —— 不管第一个成没成功,都会发第二个
const additionalRequest = originalRequest.finally(() => {
  if (data.cmd === 'excel_modify') {
    return request.post({ url: '/1000y/sj/cmd', data: { …, cmd: 'excel_save_to_oss' } })
  }
})

return Promise.all([originalRequest, additionalRequest])

Promise.all 只要有一个 reject,整体就 reject。 所以界面弹「保存失败」时,实际有三种可能:

① 写表 ② OSS 镜像 界面提示 真实状态
✅ 成功 ✅ 成功 成功 都好了
✅ 成功 ❌ 失败 失败 表已经改了,只是镜像没同步 —— 重试会二次写入
❌ 失败 ❌ 失败 失败 确实没改

所以看到「保存失败」,正确动作不是重试,是先回读。 刷新页面或重新查一次那条数据,确认它到底改没改。

在 Network 面板怎么看:cmd 会看到 成对出现的两个 POST /1000y/sj/cmd, 展开 Payload 分别是 excel_modifyexcel_save_to_oss。看清是哪一个红了。

规范做法是把「主写入」和「镜像同步」的结果分开报告, 或者让镜像失败只降级为警告,而不是让整个保存显示为失败。

发布链路:两种任务,一道闸门

发布页在 /expert/release(专家模式 → 发布,组件是 src/views/Option/index.vue)。页面上是一排卡片, 但卡片背后其实只有两种任务类型,走完全不同的代码路径。

FIG. 05发布链路:SVN 型 vs Jenkins 型

点发布卡片 views/Option/index.vue:86 compileProject(item) 闸门:有没有在跑的 Jenkins.getActiveBuilds() Option/index.vue:89 拒绝,流程终止 「当前有打包任务在进行中」 Option/index.vue:109 无 → 按 item.type 分叉(Option/index.vue:113,119) TaskType.SVN 列出本地改动 complieproject_svn() utils/extend/svn.ts:161 弹窗逐个勾选 FileRestoreDialog.vue 可单独还原某个文件 svnCommit(files) utils/extend/svn.ts:268 TaskType.Jenkins 触发 job Jenkins.startBuild(jobName) Option/index.vue:121 轮询进度 checkstartBuild() · setInterval api/login/index.ts:105 结束:成功清任务 / 失败弹窗 失败不自动重试 SVN 型 动的是 本地磁盘 + 远端仓库 Jenkins 型 动的是 构建服务器

互斥规则只有一条,而且是全局的:只要 Jenkins 上有任何一个 job 在跑,所有发布卡片都点不动 —— 包括纯本地的 SVN 提交。 闸门在分叉之前Option/index.vue:89-111), 所以「我只是想提交几个文件,为什么提示有打包任务」是符合预期的,不是 bug。

⚠️ 隔离

轮询用的是裸 setInterval,没有超时上限。 src/api/login/index.ts:105-107 起一个定时器反复查构建状态, 只在拿到明确结束态时才 clearInterval。 如果 Jenkins 中途不可达、返回结构变了、或者 job 被人在 Jenkins 侧删了, 这个定时器会一直转下去,界面上的任务永远停在进行中。

规范做法是给轮询加最大次数或截止时间,超时后主动置为「状态未知」 并给用户一个刷新入口,而不是无限等。

排障提示:用户报「发布卡住不动了」,先问 Jenkins 页面上那个 job 到底是什么状态。八成 Jenkins 那边早就结束了,是这边的轮询没收到。 重新登录会重置 taskStore,是最快的自救手段。

站点版本 319ca82 · 2026-08-13 内容基线 gameboxclient @ 2026-08-12