Files
opendance-web/DSH.md
T

376 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DSH.md -- opendance-web(OpenDance Studio 项目一:Web 管理服务端)
## 0. 本文件的作用
本文件是 **opendance-web 仓库内**的 DSH 执行约束。顶层说明书见用户提供的
《DSH 开发文档:OpenDance Studio》,二者冲突时以顶层文档为准。
## 1. 项目定位
Web 管理服务端是另两端(Windows 客户端、Android 辅助 App)的**唯一数据源**与
**契约权威**。它管理歌曲、VRM 模型与动作来源,并输出冻结的 REST / WebSocket /
数据模型契约。
```
现有 MySQL 实例(外部,本项目不部署)
│
▼
opendance-web ←── 本仓库
│ 冻结 REST API + 数据模型 + WebSocket 关键点协议
├──▶ opendance-windows
└──▶ opendance-android
```
## 2. 技术栈
| 组件 | 选型 |
|--|--|
| 后端 | NestJS 11 |
| ORM | TypeORM 0.3 |
| 数据库 | **现有 MySQL 兼容实例**(当前部署为 MariaDB 10.11.19),mysql2 驱动 |
| 管理前端 | React 18 + Vite + TypeScript |
| 3D 预览 | three.js + @pixiv/three-vrm |
| 多格式解析 | BVH / VMD 内置纯 TS 解析器;FBX/glTF 走可选外部 Assimp |
| 模型主力格式 | **GLB / VRM 为一等公民**:`.glb`/`.vrm`/`.gltf` 直接导入(不重编码),`.pmx`/`.fbx` 服务端转换(优先 GLB、其次 VRM) |
| 模型检查 / 缩略图 | `models/model-inspector.ts`(面数 / 顶点 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性)、`models/model-thumbnail.ts`(纯 TS 光栅化 PNG,零原生依赖) |
| 模型压缩包 | 纯 TS ZIP 读取器(`common/zip-reader.ts`)+ 自动搜索包内模型 + 外部 Assimp 转换(`models/model-convert.ts`);GLB 打包 `models/gltf-packer.ts`、贴图内嵌 `models/glb-textures.ts`、GLB->VRM 封装 `models/vrm-writer.ts` |
| 默认模型种子 | `web-server/src/config/default-model.ts`(服务端/CLI 写入,运行期只读) |
| 认证 | **本版本不含认证**(内网部署);上线前须补 ≥ T-WEB-011 |
## 3. 单次会话加载范围
允许加载:
1. 本文件 `DSH.md`
2. `project.yaml`
3. 当前 `tasks/xxx.md`
4. 该任务文件中 `load` 列出的文件
5. `contracts/` 中被明确引用的**锚点片段**
禁止加载:
- `opendance-windows` / `opendance-android` 的任何源码
- 非当前任务的 `tasks/*.md`
- 完整 `contracts/` 目录(只引用锚点)
- `node_modules/`、`dist/`、构建产物、二进制、模型文件、上传存储
## 4. 上下文预算
| 项 | 建议值 |
|--|--|
| 单任务文件数 | ≤ 6 |
| 单任务代码行数 | ≤ 1500 |
| 单任务 Token | ≤ 12K |
| 契约引用 | 只引用锚点,不加载全文 |
## 5. 目录结构
```
opendance-web/
├── project.yaml # 任务清单与状态
├── DSH.md # 本文件
├── tasks/ # 单任务说明书(001~010)
├── contracts/ # ★ 跨端契约(权威)
│ ├── VERSION # 契约版本(唯一来源)
│ ├── rest-api.yaml # OpenAPI 3.0
│ ├── websocket-keypoints.md # 239 字节关键点帧 + 控制消息
│ ├── data-models.md # 资源字段与默认模型选择规则
│ ├── skeleton-topology.md # MediaPipe 33 点拓扑 + 关节角
│ ├── model-formats.md # 支持格式、预处理产物、上传限制
│ ├── database.md # 现有 MySQL 连接与迁移约定
│ └── open-source-credits.md # 开源声明锚点(三端共用)
├── web-server/ # NestJS 服务端
│ ├── src/ # 源码(entities / models / preprocess)
│ │ ├── common/zip-reader.ts # 纯 TS ZIP 读取器(STORE/DEFLATE、ZIP64、zip-slip 防护)
│ │ ├── models/ # 模型上传 + 自动搜索/转换 + 分析 + 缩略图
│ │ │ ├── model-convert.ts # 选主模型 -> 直接导入或 Assimp 转换 -> 回读校验
│ │ │ ├── gltf-packer.ts # glTF(.gltf +.bin + 贴图)-> GLB 打包
│ │ │ ├── glb-textures.ts # 把外部贴图内嵌进 GLB 的 BIN chunk
│ │ │ ├── vrm-writer.ts # GLB -> VRM 1.0(写 VRMC_vrm 扩展)
│ │ │ ├── model-inspector.ts # 面数 / 顶点 / 骨骼树 / humanoid 覆盖 / 可驱动性
│ │ │ ├── model-thumbnail.ts # 纯 TS 光栅化 512×512 PNG(含蒙皮,零原生依赖)
│ │ │ ├── default-model-source.ts # 渲染 / 写入 src/config/default-model.ts
│ │ │ └── vrm-inspector.ts
│ │ ├── config/default-model.ts # 默认模型种子(由审核 / CLI 写入,运行期只读)
│ │ ├── migrations/ # TypeORM 迁移(AddModelConversionFields / AddModelStats)
│ │ └── preprocess/ # BVH / VMD / FBX -> MediaPipe33
│ │ ├── vmd-parser.ts # VMD 二进制 + Shift-JIS + 贝塞尔插值
│ │ ├── mmd-skeleton.ts # 标准 MMD 骨架(静止比例实测自参考 PMX)
│ │ ├── mmd-pose.ts # MMD FK + 两骨骼腿部 IK + 左手系->右手系 Z 镜像
│ │ └── retarget.ts # worldFramesToKeypointFrames():BVH / VMD 共用
│ ├── admin/ # React 管理端
│ │ ├── src/components/ModelReviewPanel.tsx # 模型审核面板 + 骨骼详细界面
│ │ └── src/lib/bone-tree.ts # 骨骼树过滤(搜索保留祖先链)
│ ├── test/ # 单元测试 + 端到端冒烟(10 文件 / 168 项)
│ │ # mmd-vmd 31 / model-archive 24 / model-inspector 25
│ │ # / model-thumbnail 8 / admin-bone-filter 8
│ │ # / preprocess 25 / init-env 16 / admin-port 16
│ │ # / access-log 9 / vrm-inspector 6
│ └── scripts/ # migrate.mjs / check-contracts.mjs / check-routes.mjs
│ # / inspect-vmd.mjs(VMD 诊断)
│ # / import-default-model.mjs(默认模型导入 / 补写源码)
├── deploy/ # ★ 业务服务部署(不含 MySQL、不含反代)
│ ├── init-env.sh # ★ 首次启动的.env 交互引导(install.sh 缺.env 时调用)
│ ├── install.sh # 一键部署(systemd / OpenRC / SysVinit / 后台进程)
│ ├── uninstall.sh
│ ├── ctl.sh # 无 init 系统时的进程控制
│ ├──.env.example
│ ├── systemd/opendance-api.service
│ ├── openrc/opendance-api
│ ├── initd/opendance-api
│ └── proxy/ # ★ 反代配置说明(nginx / IIS,仅文档不部署)
├── scripts/package-release.sh # 发布产物打包
└── THIRD_PARTY.md
```
## 6. 硬性约束
1. **不部署 MySQL**。数据库为部署方提供的现有实例,连接信息只经环境变量注入。
**也不使用 Docker**,**也不部署反向代理**:本端只提供业务服务
(API + 自带的管理端页面),反代由另外的服务负责,
本项目只在 `deploy/proxy/` 下提供 IIS 与 nginx 的配置说明文档。
业务服务监听**两个端口**(软件 API 端口 `PORT` + 管理端端口 `ADMIN_PORT`),
约定见 9。
2. **`DB_SYNCHRONIZE` 生产必须为 `false`**,所有表结构变更走 migration;每个
migration 必须可逆。
3. **绝不把凭证写入仓库**。`.env` 已在 `.gitignore` 中,只提交 `.env.example`。
4. **来源五元组强制必填**:`source_type` / `source_name` / `source_url` / `license`
/ `author`。任何资产入库缺一即 400。
5. **契约先行**:改接口先改 `contracts/`,再提升 `contracts/VERSION`,同步
`web-server/src/config/contract-versions.ts`,最后才改实现。
`npm run contracts:check` 在构建前拦截漂移。
6. **不使用 SMPL** 及其衍生拓扑(许可风险),统一 MediaPipe 33 点。
7. **上传文件永不使用原始文件名作为路径**,一律 `sha256` 去重后归档。
8. **GLB / VRM 是模型主力格式**:服务端与客户端(Godot 内置 `GLTFDocument`)都把
这两种格式作为一等公民。`.glb` / `.vrm` / `.gltf` **直接导入、不重编码**;
`.pmx` / `.fbx` 才由服务端转换,且**优先 GLB、其次 VRM**。
humanoid 候选表必须与客户端 `opendance-windows/src/model/humanoid_map.gd` 逐字对齐,
由 `test/model-inspector.spec.ts` 断言兜底。
9. **模型上传一律 `pending`(待审核)**:上传不得自动发布、不得自动设为默认模型。
默认模型只在审核通过勾选 `setPrimary:true`(`POST /api/reviews/model/:id/approve`)
或显式调用 `POST /api/models/:id/primary`(仅 `published`,否则 409)时设置。
## 7. 每任务的执行步骤
```bash
# 1. 静态与单元校验
cd web-server
npm run contracts:check
npm run typecheck
npm run test
# 2. 构建
npm run build
# 3. 端到端冒烟(需数据库可连通)
npm run start:prod &
npm run test:smoke
# 4. 数据库迁移
npm run migration:run
# 5. 提交
git checkout -b task/T-WEB-0XX
git add. && git commit -m "feat(web): <scope>"
git push origin task/T-WEB-0XX
# Gitea 创建 PR -> 合并到 dev
```
## 8. 环境变量
见 `web-server/.env.example` 与 `deploy/.env.example`。首次部署的 `deploy/.env`
可由 `deploy/init-env.sh` 交互生成(见 12)。生产关键项:
```
DB_HOST DB_PORT DB_USER DB_PASS DB_NAME
DB_SYNCHRONIZE=false
PORT=3000 # 软件 API 端口(客户端调用,不含管理页面)
ADMIN_PORT=3001 # 管理端端口(默认 PORT + 1;等于 PORT 为单端口兼容模式)
HOST=0.0.0.0 # 软件 API 监听地址(未设置时默认即 0.0.0.0)
ADMIN_HOST=0.0.0.0 # 可选,管理端监听地址,默认跟随 HOST
STORAGE_DIR=/data/storage
UPLOAD_MAX_BYTES=536870912
ASSIMP_BIN=assimp # 可选;上传.zip 模型包或.pmx/.fbx 单文件时必需
MODEL_CONVERT_FORMAT=glb # PMX/FBX 交付格式:glb(默认)| vrm
# vrm = GLB 之上补 VRMC_vrm 扩展;humanoid 不全则回退 GLB
# 产物一律 `assimp info` 回读校验
MODEL_CONVERT_PMX="Hatsune Miku.pmx" # 压缩包内优先选用的 PMX 文件名(大小写不敏感)
MODEL_CONVERT_TIMEOUT_MS=180000
DEFAULT_MODEL_ID=1 # 可选;未设置时按 src/config/default-model.ts 的种子匹配
CONTRACTS_DIR=/contracts # 可选,读取实时契约版本
```
## 9. 双端口约定(软件 API 端口 / 管理端端口)
- **软件 API 端口**:`PORT`,默认 **3000**。给 Windows/Godot 客户端与 Android App
调用,只提供 `/api`、`/health`、`/static`、`/contracts`;**不提供管理页面**,
`/`、`/songs` 等一律 404(有意设计,避免管理页暴露在客户端调用的端口上)。
- **管理端端口**:`ADMIN_PORT`,默认 `PORT + 1`(即 **3001**)。给浏览器访问,
提供管理页面(含 SPA 深链接回退)与上面全部接口。
- `ADMIN_HOST` 可选,默认跟随 `HOST`;可用它把管理端限制在本机或内网。
- `ADMIN_PORT === PORT` 为**单端口兼容模式**:管理页面回到 API 端口,日志提示
「未分端口」,只 warn 不中止启动。
- 端口非法(非 1..65535 整数)在生产环境会中断启动。
- 启动日志分别打印两行:`软件 API 端口:...` 与 `管理端端口:...`。
- 反向代理按用途分别转发两个端口;`/health` 两个端口都提供,可用于存活探测。
细则见 `deploy/proxy/README.md` 与 `tasks/010-deploy.md`。
## 10. VMD 动作通路(MikuMikuDance)
```
.vmd ─▶ vmd-parser.ts ─▶ mmd-skeleton.ts ─▶ mmd-pose.ts ─▶ retarget.ts
二进制 + Shift-JIS 标准 MMD 骨架 FK + 腿部 IK worldFramesToKeypointFrames()
+ 贝塞尔插值 + Z 镜像 (与 BVH 共用)
```
- `MotionFormat.Vmd = 'vmd'`,扩展名 `.vmd`,固定 **30 fps**(上传时填写的 `fps` 被忽略)。
- VMD 二进制:30 字节头 + 20 字节 Shift-JIS 模型名 + 111 字节/条的骨骼关键帧 + morph 段;
只有旋转(少数骨骼带位移)、不含骨骼长度,因此必须靠 `mmd-skeleton.ts` 的标准骨架
(静止比例从参考 PMX 实测)做 FK。
- **腿部驱动自动判定**:`足IK` 有非零位移 -> 两骨骼解析 IK(膝盖朝角色正面弯,
不可达时收敛为伸直);否则用 FK 旋转。部分动作数据把腿烘焙成 FK。
- **手性**:MMD 为左手系,统一做 Z 镜像(旋转 `(x,y,z,w) -> (-x,-y,z,w)`、位移 z 取反)
后进入右手系通路。由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored`
下「膝盖朝前」占 94.1%,不转换仅 5.9%。
- `worldFramesToKeypointFrames()` 为 BVH / VMD 共用:骨盆归零 / 自适应缩放 /
图像空间投影 / 9 个关节角完全一致。
- 诊断工具:`node scripts/inspect-vmd.mjs 动作.vmd [--frames N] [--json out.json]`
(打印头部/模型名/帧数/轨道数、未识别骨骼、IK 还是 FK、关键点序列异常统计、膝肘角分布)。
- 契约 `contracts/model-formats.md` 2.3;测试 `test/mmd-vmd.spec.ts`(31 项)。
## 11. 模型格式策略、转换、分析与审核
**GLB / VRM 是主力格式**(服务端与客户端都一等公民),其余格式在服务端转换。
```
POST /api/models ─┬─.glb /.vrm /.gltf ─▶ importModelFile() 直接导入(不重编码)
├─.pmx /.fbx ─────────▶ convertModelFile() Assimp -> GLB(首选)
└─.zip ─▶ zip-reader.ts ─▶ 自动搜索包内模型
├─.vrm /.glb /.gltf ─▶ 直接导入
└─.pmx /.fbx ─▶ model-convert.ts
├─ 1) assimp export ->.glb
├─ 2) 失败 ->.gltf -> gltf-packer.ts 打包回 GLB
├─ 3) MODEL_CONVERT_FORMAT=vrm -> vrm-writer.ts 补 VRMC_vrm
└─ 每步 assimp info 回读校验
─▶ 入库(status=pending)
─▶ model-inspector.ts 分析 -> models.stats
─▶ model-thumbnail.ts 缩略图 -> models.thumbnail_asset_id
```
- `POST /api/models` 的 `file` 接受 `.glb` / `.vrm` / `.gltf` / `.pmx` / `.fbx` / `.zip`。
- 新增模块(均在 `web-server/src/`):
- `common/zip-reader.ts`:纯 TS 极简 ZIP 读取器(中央目录/EOCD、STORE+DEFLATE、
ZIP64 大小字段、Shift-JIS/GBK 文件名回退、拒绝 zip-slip 与加密条目、
不支持的压缩方式报错)。
- `models/model-convert.ts`:解压 -> 自动搜索主模型 -> 直接导入或 Assimp 转换 ->
回读校验 -> 回退。
- `models/gltf-packer.ts`:把 Assimp 导出的分离 `.gltf` + `.bin` + 贴图自行打包成 GLB。
- `models/glb-textures.ts`:把 GLB 里 Assimp 写成外部 URI 的贴图内嵌进 BIN chunk。
- `models/vrm-writer.ts`:GLB -> VRM 1.0,只改 JSON chunk 写 `extensions.VRMC_vrm`,
不做任何几何/材质损失。
- `models/model-inspector.ts`:解析 GLB/VRM,算面数 / 顶点 / 贴图 / **骨骼树** /
humanoid 覆盖 / 可驱动性(缺哪些必需骨骼)/ warnings。
- `models/model-thumbnail.ts`:纯 TS 软件光栅化 512×512 PNG(含蒙皮),零原生依赖。
- `models/default-model-source.ts`:渲染 / 写入 `src/config/default-model.ts`。
- `config/default-model.ts`:默认模型种子(审核 / CLI 写入,运行期只读)。
- **压缩包自动搜索**:优先级 `.vrm` -> `.glb` -> `.gltf` -> `.pmx` -> `.fbx`;同一优先级内
按 `MODEL_CONVERT_PMX` 指名字 -> 目录最浅 -> 文件最大。命中 glTF 系格式时**直接导入**,
只有 `.pmx`/`.fbx` 才转换。压缩包原件存为 `model-archive` 资产。
- **转换与回退**:GLB 直出失败 -> glTF 再自行打包;`MODEL_CONVERT_FORMAT=vrm` 时在 GLB
之上补 `VRMC_vrm`(humanoid 不全则**回退交付 GLB**并把缺失骨骼写进 `conversion_note`)。
**每一步产物都必须用 `assimp info` 回读校验**:实测 Assimp 会"写出损坏文件"
(FBX 导出退出码 11、回读报 `FBX-DOM no Objects dictionary found`),只看退出码
会把坏文件入库。
- **入库后自动**:`inspectModelFile()` 分析写入 `models.stats`
(迁移 `AddModelStats1789460000000`,新增 `models.stats TEXT`);
`renderModelThumbnail()` 写入 `models.thumbnail_asset_id` / `thumbnailUrl`。
**两者都是"尽力而为"**:失败只记 warning,不影响入库。
- **状态与默认模型(硬性)**:上传**一律 `status=pending`**,`is_primary=false`,
不自动发布、不自动设为默认。默认模型只在
① 审核通过带 `setPrimary:true`(`POST /api/reviews/model/:id/approve`)或
② `POST /api/models/:id/primary`(仅 `published`,否则 **409**)时设置,此时写库
`is_primary` 并把身份写进源码种子。
`GET /api/catalog/export` 的 `defaultModelId` 解析顺序为 `DEFAULT_MODEL_ID` -> 源码种子
(按**上传压缩包**的 sha256 匹配;产物 sha256 每次转换都变,只作记录)
-> 已发布且 is_primary 的 VRM -> 任意已发布 VRM -> id 最小的已发布模型。
- **溯源**:压缩包原件记入 `models.source_asset_id`
(迁移 `AddModelConversionFields1789450000000`);`models.conversion_note` 记转换说明。
- **失败**:包内没有可用模型、Assimp 不可用、两条路径都失败 -> **HTTP 422**,
不产生模型记录。
- **文件名编码**:`common/upload.util.ts` 的 `decodeUploadName()` 修正 multipart
文件名的 latin1/UTF-8 乱码(中文文件名原先存成 `模å_...`)。
- 契约:`contracts/model-formats.md` 1.1、`contracts/data-models.md` 4.1;
DTO 新增 `conversionNote` / `fileSha256` / `stats` / `thumbnailUrl`。
### 11.1 审核要求(模型)
管理端「审核」页对 `pending` 模型展示 **模型审核面板**
(`admin/src/components/ModelReviewPanel.tsx`):
- **指标**:体积 / 格式 / 容器 / VRM 版本 / 面数 / 顶点 / 网格 / 材质 / 贴图 /
骨骼 / 节点 / 形变目标 / 动画。
- **humanoid 覆盖**:已映射 / 总数(22 根)+ 明细徽章;**可驱动性结论**直接给出
缺哪些必需骨骼(16 根必需骨全齐才算可驱动);并列出 warnings。
- **骨骼详细界面**:扁平骨骼树按 `depth` 缩进,显示骨骼名、是否参与蒙皮、
humanoid 映射徽章、子节点数;支持按名搜索(`admin/src/lib/bone-tree.ts`
保留祖先链)、「仅显示蒙皮骨骼」开关、限高滚动 + 只渲染前 400 行。
- **通过**:可勾选「同时设为全局默认模型」-> 请求体 `setPrimary:true`。
- **驳回**:必须填写审核意见(400)。
- 实测:`POST /api/reviews/model/22/approve` 带 `setPrimary:true` -> 200、
`status=published`、`isPrimary=true`,源码种子写入 `sourceSha256=72eb6882...`;
重建后 `defaultModelId` 由 VRM 变为 **22**。
### 11.2 实测数据(v0.2.0)
- 上传含 `Hatsune Miku.pmx` 的 MMD 压缩包 -> **HTTP 201 / 0.86s**、`format=glb`、
`status=pending`、`conversionNote=Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`;
分析 `vertices=7155 triangles=9071 materials=3 textures=2 images=2 nodes=240
bones=238 skins=1 morphTargets=0 humanoidMapped=0/22 drivable=false
maxBoneDepth=23`,`missingRequiredBones` 为全部 16 根(日文 MMD 骨名无法映射)。
- 直接上传 `.glb`:**HTTP 201 / 0.36s**、`conversionNote=直接导入(GLB,未做重编码)`,
入库文件 sha256 与源文件**一致**。
- 缩略图:512×512 PNG / 15,043 字节,纯 TS 光栅化(含蒙皮),目视确认为居中的
T-pose 角色图;真实 10MB VRM(83 关节、5307 顶点)单张耗时 60-121ms(中位 79ms)。
- 管理端 3D 预览放开到 `vrm/glb/gltf`(`fbx` 仍提示"仅归档,请先转换为 GLB/VRM");
列表页显示 40px 缩略图。
- 测试:`npx vitest run` -> **10 个文件 / 168 项通过**
(`model-inspector` 25、`model-thumbnail` 8、`admin-bone-filter` 8、
`model-archive` 24、`mmd-vmd` 31、`preprocess` 25、`init-env` 16、
`admin-port` 16、`access-log` 9、`vrm-inspector` 6)。
## 12. 首次启动的.env 引导(deploy/init-env.sh)与监听地址
- `deploy/init-env.sh`:交互式逐项询问 `DB_HOST / DB_PORT / DB_USER / DB_PASS /
DB_NAME / PORT / ADMIN_PORT / HOST`,回车保留默认;口令 `read -rs` 不回显、
按约定写盘不加引号;校验必填与端口范围;可用 Node + mysql2 做连通性自检
(口令只经环境变量传递、失败仅告警);只替换/追加这 8 个键、保留模板注释,
写完 `chmod 600` 并回读校验;结尾打印摘要(口令 `***`)与变更列表。
- 开关:`--force` / `--check` / `-y|--yes|--non-interactive` / `-i|--interactive` /
`--skip-db-check` / `--dir` / `--env-file` / `--example` / `--timeout` / `-h`;
环境变量 `OPENDANCE_NONINTERACTIVE` / `OPENDANCE_INTERACTIVE` /
`OPENDANCE_SKIP_DB_CHECK` / `OPENDANCE_ENV_FILE` / `OPENDANCE_ENV_EXAMPLE` /
`OPENDANCE_PROMPT_TIMEOUT`。
- **非交互绝不挂住**:stdin 非 TTY 且是 `</dev/null`/关闭,或带 `-y` 等开关 ->
只校验不提问;所有 read 带超时,EOF/超时立即 `exit 1`。
- 接入点:`deploy/install.sh` 缺 `deploy/.env` 时先调用 `init-env.sh` 引导,
失败才 die(若 `$APP_DIR/deploy/.env` 已存在则复用,保持原语义)。
- **监听地址默认 `0.0.0.0`**:`configuration.ts` 的
`bindHost = process.env.HOST?? '0.0.0.0'`,`adminHost` 默认跟随 `HOST`
(同样回落 `0.0.0.0`);`deploy/.env.example` 与 `web-server/.env.example`
均为 `HOST=0.0.0.0`;`install.sh` 的 `BIND_HOST="${HOST:-0.0.0.0}"`,
`init-env.sh` 的 HOST 默认值也是 `0.0.0.0`。
(`install.sh` 的 `HEALTH_HOST`/`API_HOST` 是**本机健康检查**用的回环地址,
与对外监听的默认值无关。)
- 测试 `test/init-env.spec.ts`(16 项)。
## 13. 上下文超标时的拆分策略
| 超标原因 | 拆分方式 |
|--|--|
| 单任务涉及多个端点 | 按端点拆 |
| 单文件过大 | 按类/函数拆,先生成接口再分实现 |
| 契约全文加载 | 改用锚点引用 |
| 需要参考其他项目 | 只读契约,不读源码 |
| UI 与逻辑耦合 | UI 任务只调用已定义接口 |
| 测试与实现同任务 | 测试单独任务,共享接口摘要 |