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) 并不是「无权访问」,只是「不显示入口」。真正的授权只可能在服务端。 排障时如果有人说「他不该能进那个页面」,答案是前端从来没拦过。

钥匙链:为什么不登录就什么都干不了

这是整个项目最反直觉、也最值得先搞懂的一条链路。

正常项目里,SVN 地址、Jenkins 地址、对象存储密钥这些东西写在 .env 文件里,构建时打进去。这个项目不是

源码里用到了约 50 个 VITE_* 开头的配置键,但 .env / .env.base / .env.dev / .env.pro 四个文件加起来只定义了 11 个。剩下的 —— SVN 账号密码、Jenkins 地址账号 token、GM 接口地址、项目编码 pcode —— 全部是登录接口下发的,登录成功那一刻才被塞进内存。

FIG. 03钥匙链:登录如何点亮整个工具

登录页 views/ReLogin/index.vue POST /user/login → token POST /user/info → userInfo.game ★ loginSuccess() api/login/index.ts:72 initEnv() utils/extend/envOption.ts:8 GameEvn.ENV store/modules/GameEvn.ts VITE_SVN_USER / PASSWORD VITE_JENKINS_URL / TOKEN VITE_API_BASEPATH / PCODE localStorage 键名 GameENV · 明文 watch 写入 刷新后读回 ⚠ SVN / Jenkins 凭据明文落盘在这里 axios · getPcode() config/axios/service.ts:30 SVN · SvnOption() utils/extend/svn.ts:32 Jenkins · initConfig() utils/extend/jenkins/config.ts:125 COS · COS_Handler.init() src/App.vue:41 .env 只定义 11 个键,源码用到约 50 个 差额全靠 /user/info 下发 → 没登录 = 外部工具链全是哑的 initEnv() 末尾还会调 Jenkins.initJenkins(gameEnv) 建客户端(envOption.ts:78); 这就是为什么没登录时任何发布操作都会静默失败 —— 客户端根本没被创建。

排障用法:「点了发布没反应」「SVN 提交报没权限」这类问题, 第一步永远是打开 DevTools 控制台敲 JSON.parse(localStorage.getItem('GameENV')), 看该有的键是不是空的。空的就不是发布代码的问题,是登录没成功或服务端没下发。

❌ 别学

SVN 账号密码和 Jenkins token 明文写进 localStorage。 src/store/modules/GameEvn.ts:44-49 挂了一个深度 watchENV 一变就整体 setStorage('GameENV', nv), 而 ENV 里装着 svn_passwordjenkins_token。 localStorage 没有加密、没有过期、退出登录前一直留在磁盘上, 而渲染层又是 webSecurity: false 的全 Node 环境。

规范做法是凭据只留在主进程,渲染层要用就通过 IPC 请求主进程代为执行(「帮我提交这些文件」),而不是把密码交给渲染层自己拼命令行。 主进程侧可用 Electron safeStorage 走系统钥匙串加密。

你现在该做的:先别改,但把它记进风险清单 —— 这是将来跟安全或运维对话时你必须能说清楚的一条。

主业务:改一条 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 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 描述 写回 api/gm · excel_modify cmd: 'excel_modify' 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 渲染。

这个后端不是 REST

别按 REST 的直觉找接口。这个后端只有极少数几个 URL,业务动作全塞在 cmd 字段里:查 NPC 是 cmd: 'npc_search_ByMapId', 查表字段是 cmd: 'excel_search_name',改数据是 cmd: 'excel_modify'

后果是:你在浏览器 Network 面板里看到的请求全长一样 (都是 POST /1000y/engine/cmd),想知道某个请求在干什么, 必须展开 Payload 看 cmd。这是排障时最容易卡住新人的一点。

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

发布页在 /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,是最快的自救手段。