一体系统 · 部署与运维说明
面向操作。要理解为什么这么切分,看 docs/技术架构总纲.md。 本文只讲怎么跑起来、改哪里、踩过哪些坑。
1. 系统长什么样
┌──────────────────────────────┐
浏览器 ────────► │ 单入口(一个端口/域名) │
└──────────────┬───────────────┘
│
┌────────────────────┴────────────────────┐
│ │
/ 作品侧 /forge/ 生产侧
┌──────────┴──────────┐ ┌──────────┴──────────┐
│ gamenet :8000 │ │ asset-forge :8788 │
│ · 前端产物 /app │ │ · 前端产物 / │
│ · /api/* 作品接口 │ │ · /api/* 生产接口 │
│ · /assets/models/*.glb │ ◄──── 入库 ───│ · /static/* 图与模型│
│ · /ws tick 心跳流 │ └─────────────────────┘
└─────────────────────┘
两个服务各自托管自己的前端,互不依赖。唯一的数据流是「入库」: 生产侧的成果复制进 game/assets/models/,作品侧实时扫描该目录(无需重启)。
2. 快速启动
Windows(本地开发)
run_all.bat :: 构建前端 + 起两个后端 + 起单入口代理
run_all.bat --no-build :: 跳过构建(只改了后端时更快)
打开:
| Purpose | 地址 |
|---|---|
| 作品 | http://127.0.0.1:8080 |
| 美术资源生成器 | http://127.0.0.1:8080/forge/ |
也可以单独起某个服务:
deploy\start_game.bat :: 只起作品后端(:8000)
deploy\start_forge.bat :: 只起资源生成器(:8788,前缀 /forge)
Linux(云端服务器)
bash run_all.sh # 构建 + 起全部,代理听 [服务器地址]:8080
bash run_all.sh --no-build # 跳过构建
bash run_all.sh --no-proxy # 只起两个后端,对外交给 Nginx
PORT=80 bash run_all.sh # 改代理端口
直接跑单个服务
# 作品后端
cd web/backend && python -m gamenet.server
# 资源生成器(注意前缀)
cd asset-forge/backend && ASSET_FORGE_BASE=/forge .venv/bin/python run.py
# 单入口代理
python tools/serve_single_entry.py --host [服务器地址] --port 8080
3. 生产部署(Nginx 单入口)
配置模板:deploy/nginx-menpai.conf
# 1) 构建前端。注意 asset-forge 必须带 VITE_APP_BASE —— 这是最常漏的一步
cd web/frontend && npm run build
cd asset-forge/frontend && VITE_APP_BASE=/forge/ npm run build
# 2) 起两个后端(生产建议交给 systemd 而非 run_all.sh)
cd web/backend && python -m gamenet.server
cd asset-forge/backend && ASSET_FORGE_BASE=/forge .venv/bin/python run.py
# 3) 装 Nginx 配置
cp deploy/nginx-menpai.conf /etc/nginx/conf.d/
# 改 server_name、root 等
nginx -t && nginx -s reload
三处必须一致,缺一不可:
| 位置 | 值 |
|---|---|
Nginx location /forge/ | /forge/ |
| 环境变量 | ASSET_FORGE_BASE=/forge |
| 前端构建 | VITE_APP_BASE=/forge/ |
任何一处不匹配,表现都是页面能开但图片/模型 404 或白屏。
4. 环境变量
作品后端(web/backend/gamenet/config.py)
| 变量 | 默认 | 说明 |
|---|---|---|
GAMENET_HOST | 127.0.0.1 | 监听地址。云端直连填 [服务器地址];经 Nginx 反代保持 127.0.0.1 |
GAMENET_PORT | 8000 | 监听端口 |
GAMENET_CORS | * | 允许的前端源,逗号分隔。生产应收紧为实际域名 |
GAMENET_ROOT | 自动推导 | 仓库根目录 |
GAMENET_DATA | <root>/game | 数据与资产根(data/ 与 assets/ 在其下) |
GAMENET_SAVES | <root>/web/backend/saves | 存档目录 |
GAMENET_FRONTEND | <root>/web/frontend/dist | 前端构建产物 |
GAMENET_LOG_LEVEL | info | uvicorn 日志级别 |
资源生成器(asset-forge/backend/app/config.py)
| 变量 | 默认 | 说明 |
|---|---|---|
ASSET_FORGE_HOST | 127.0.0.1 | 监听地址 |
ASSET_FORGE_PORT | 8788 | 监听端口 |
ASSET_FORGE_CORS | 本地 5178/8788 | 允许的前端源 |
ASSET_FORGE_BASE | 空 | 部署子路径前缀。挂 /forge/ 时必须设为 /forge |
ASSET_FORGE_DATA | <仓库根>/asset_forge_data | 数据根(inbox / work / export 等) |
ASSET_FORGE_FRONTEND | <root>/frontend/dist | 前端构建产物 |
5. 入库:生产 → 作品的最后一跳
为什么要它:以前生成器跑到 DONE 就停了,必须人工跑 tools/sync_assets.py 才能让作品看见新模型。这是整条工具链唯一的断头路。
现在:生成器界面「入库作品」页 → 点一下 → 刷新作品页面即可用。
界面
侧边栏「入库作品」。六个 KPI + 一条从「来源目录」到「作品资产库」的通道图 + 可筛选的资产清单。
来源目录(按此顺序查找同名文件)
| key | 含义 | 目录 |
|---|---|---|
inbox | AI 生成回传的原始 GLB | asset_forge_data/inbox/ |
work_04_handoff | 已导入的留档副本 | asset_forge_data/work/04_handoff/ |
model_forge_out | 程序化生成器产出 | model-forge/out/ |
程序化模型与 AI 模型走同一套命名校验、同一个落点,不再需要两个入口。
命名规范
<前缀>_<资产名>_<序号>.glb
bld_ 建筑 / chr_ 角色 / prp_ 道具 / dec_ 装饰 / env_ 地块
不合规的文件会被拒绝并列明原因,不会自动改名 —— 免得你以为入库了、 实际落了个名字乱猜的文件进作品。
四种状态
| 状态 | 含义 |
|---|---|
ready | 未入库,命名合规 → 可入库 |
update | 已入库但源文件更新 → 可覆盖 |
exists | 已入库且已最新 → 跳过 |
invalid | 命名不合规 → 拒绝 |
入库是幂等的,反复点没有副作用。复制后会回读校验大小,避免半截文件进作品。
超大模型提醒
原始 AI 产出动辄 30MB+。界面对 >5MB 的已入库文件会给出警示 —— 这类文件 直连进作品会严重拖慢首屏,建议先减面清理再入库。
6. 为什么作品不用重启
作品后端 /api/scene 每次都实时扫描 game/assets/models/ 目录 (gamenet/scene.py 的 available_models()),并把结果体现为 counts.models_on_disk 与 missing_models 两个字段。
所以入库完成后,作品侧刷新浏览器页面即可加载新模型。已实测验证: 从目录里抽走一个在用模型,missing_models 立刻多出一项;放回立刻归零 —— 全程未重启后端。
7. 踩过的坑(改之前先读这段)
7.1 代理转发必须带 Connection: close
tools/serve_single_entry.py 是字节流转发,靠"读到上游 EOF"界定响应体结束。 若上游保持 keep-alive,就永远读不到 EOF,连接一直挂着不释放 —— 浏览器单域名只有 6 个并发连接,几个大 GLB 就把槽位占死, 页面表现为卡在加载页,且控制台零报错(最难查的那种)。
WebSocket 升级请求则相反:绝不能带 Connection: close,否则握手必被拒。
7.2 WebSocket 握手头不能重复
Sec-WebSocket-Key / -Version / -Protocol / -Extensions 若在遍历请求头时 加一次、又在补齐 WS 头时加一次,重复头会让 uvicorn 判为非法请求(400)。 这些头已统一放进 _HOP_BY_HOP 跳过集合。
7.3 Host 头只能出现一次
代理早期版本先遍历原样带上 Host、末尾又补一个,uvicorn 直接回 Invalid HTTP request received。现在由 _build_request() 统一重写。
7.4 Vite 的 base 与后端静态挂载会撞车
作品前端早期用 base: '/',产物输出到 dist/assets/,而作品后端在 /assets 上挂了模型目录(game/assets)—— 两者路径冲突导致白屏。 现在作品前端 base: '/app/'。
7.5 判断元素是否真的可见,别信 innerText
隐藏元素照样返回文字。必须用 getBoundingClientRect() + getComputedStyle()。 HUD 外壳曾经整块不可见(hidden 类没被移除),而探针靠着 innerText "看到"了战力和银两,白查了很久。
8. 验收清单
改动涉及部署或服务配置时,跑这几项:
# 作品后端逻辑
cd web/backend && python -m gamenet.selftest # 期望 86 通过 / 0 失败
# 作品端到端(真点按钮)
cd web/tools && node e2e.cjs # 期望 28/28,控制台无错误
# 入库面板
cd asset-forge && node _shot_publish.js # 期望 22 通过 / 0 失败
# 单入口双服务
cd asset-forge && node _shot_single_entry.js # 期望 13 通过 / 0 失败
后两个需要有服务在跑:_shot_publish.js 打 127.0.0.1:5178(dev server), _shot_single_entry.js 打 127.0.0.1:8080(单入口代理)。
9. 目录速查
3d-game/
├── run_all.bat / run_all.sh 一键启动(Windows / Linux)
├── deploy/
│ ├── nginx-menpai.conf 生产单入口 Nginx 模板
│ ├── start_game.bat 单起作品后端
│ └── start_forge.bat 单起资源生成器
├── tools/
│ ├── serve_single_entry.py 单入口反向代理(无依赖)
│ └── sync_assets.py 旧的命令行入库(已被界面入库取代,保留备查)
├── web/ 作品
│ ├── backend/gamenet/ Python 后端(config.py 是配置单一入口)
│ ├── frontend/ three.js 前端
│ └── tools/e2e.cjs 端到端测试
├── asset-forge/ 美术资源生成器
│ ├── backend/app/services/publish.py 入库服务
│ ├── backend/app/routers/publish.py 入库接口
│ └── frontend/src/views/PublishView.vue 入库界面
├── game/ ⚠️ 不要删!后端的活数据源
│ ├── data/*.json 作品数值定义
│ └── assets/
│ ├── models/*.glb 3D 资产库(入库落点)
│ └── ui/*.png 界面图片(门人头像 / 任务横幅等 2D 占位图)
├── model-forge/ 程序化 GLB 生成器
└── _archive/godot_pc/ 原 Godot 版代码(已归档,仅备查)
界面图片怎么引:挂在/assets下 ⇒ 浏览器路径/assets/ui/<name>.png。 路径由后端/api/scene的ui_dir字段下发,前端一律读它、不许手拼 (与models_dir同一条规矩;将来整体挪子路径只改后端一处)。 新增界面图:丢进game/assets/ui/→ 前端按名字引用即可,无需重启后端 (/assets是静态挂载,实时读盘)。
⚠️game/这个名字有误导性 —— 它同时装着旧 PC 版的残留和 Web 版的命脉 (后端数据源 + 37 个 GLB)。永远不要rm -rf game/。