Files
opendance-web/deploy/proxy/README.md
T
zijunchiang 74332e20d8 docs(proxy): 更正「Gitea 附件通道坏了/磁盘写满」的误判 —— 实际是上传字段名写错
排查 Windows 整包上传时的 500 曾被判成"Gitea 宿主机磁盘写满"。**这个结论是错的**,
本次用对照实验确认并更正:Gitea 的 Release 附件接口 multipart 字段名是
**`attachment`**,用 `file=` 会得到

  HTTP 500 {"message":"","url":"https://gitea.kasugano.cn/api/swagger"}

空消息,极易误判成服务器故障。同一 1 KB 文件、同一 Release 的实测对照:
  -F "attachment=@probe.bin"                      → 201
  --data-binary @probe.bin (+?name=)              → 201
  -F "file=@probe.bin"                            → 500(空消息)

**仍然真实存在、且是唯一限制的是 IIS `maxAllowedContentLength`(默认 30,000,000 字节)**:
927 KB / 14 MB / 16,372,427 B 通过;30.5 MB / 38,245,602 B / 40 MB 一律 HTTP 413。

文档更正:
- deploy/proxy/README.md 第九节重写 9.1(含"误判更正"小节)与 9.3(复验命令改用
  正确字段名),并给出两种可直接照抄的上传写法;
- DSH 开发文档 v1.6 §5.7 的"阶段二"表格改为"误判更正"记录;
- opendance-windows/tasks/014-full-package.md 同步更正并附正确上传命令;
- 本仓库 README.md/.txt 的 v0.7.3 说明补第 6 条。

验证:npm run verify 通过(22 文件 / 281 项测试 + 启动冒烟);重新打包
opendance-web-v0.7.3.tar.gz(sha256 见 dist-release/SHA256SUMS)。
2026-09-17 03:39:36 +08:00

375 lines
15 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.
# 反向代理说明(nginx / IIS)
> **职责边界**:本 Web 端**只提供业务服务**(NestJS API + 管理端页面),
> **不部署、不配置反向代理**。反向代理由另外的服务或团队负责,本文档只提供配置说明。
>
> 安装脚本 `deploy/install.sh` 不会安装或修改任何反代软件。
## 一、需要转发的目标(两个端口,按用途分开)
本服务**同时监听两个端口**,职责不同:
| 端口 | 环境变量 | 默认 | 给谁用 | 提供什么 |
|--|--|--|--|--|
| 软件 API 端口 | `PORT` | `3000` | Windows/Godot 客户端、Android App | `/api`、`/health`、`/static`、`/contracts`; **不含管理页面**(未知路径 404) |
| 管理端端口 | `ADMIN_PORT` | `3001` | 浏览器(管理员) | 管理页面 + 上面全部接口 |
```
软件 API: http://<后端地址>:3000
管理端: http://<后端地址>:3001
```
两者都不需要在代理层托管静态文件,也不需要配置 SPA 回退(服务端已处理);
各用一条 `location /`(nginx)或全匹配规则(IIS)即可。
**推荐做法**:把两个端口挂在两个不同的站点/域名上,便于分别做访问控制,例如
- `https://dance-api.example.com` -> 转发到 `3000`(给客户端,可用 IP 白名单)
- `https://dance-admin.example.com` -> 转发到 `3001`(给管理员,建议加 Basic Auth / SSO 或只在内网放行)
如果客户端与管理端在同一个内网、也不想配域名,可以**让客户端直连 `3000`**(不经过反代),
只把管理端 `3001` 挂到反代上;反之亦可。
| 路径 | 内容 | 哪个端口提供 |
|--|--|--|
| `/`、`/assets/*` | 管理端页面与静态资源 | **仅管理端**;前端路由深链接(如 `/songs`)由服务端回退到 `index.html` |
| `/api/*` | REST API | 两个端口都提供(契约见 `contracts/rest-api.yaml`) |
| `/health` | 健康检查 | 两个端口都提供;可用作反代的存活探测,数据库不可用时返回 **503** |
| `/static/*` | 上传文件 | 两个端口都提供(模型 / 音频 / 关键点序列) |
| `/contracts/*` | 契约快照 | 两个端口都提供,只读 |
> Windows 客户端与 Android 之间的**关键点 WebSocket 链路不经过本服务**,
> 反代无需配置 WebSocket 转发。
>
> `ADMIN_PORT` 设为与 `PORT` 相同时是**单端口兼容模式**:两个端口合一,
> 管理页面重新出现在 API 端口上(此时反代只需要一条规则)。
## 二、后端绑定地址怎么选
`deploy/.env` 中的 `HOST`(软件 API 端口)与可选的 `ADMIN_HOST`(管理端端口,缺省跟随 `HOST`):
| 部署形态 | 建议值 | 说明 |
|--|--|--|
| 反代与后端**同机** | `HOST=127.0.0.1` | 后端不直接对外暴露,最安全 |
| 反代与后端**不同机** | `HOST=0.0.0.0` | 必须用防火墙把端口限制为仅反代来源可访问 |
| 只想让管理端在内网可见 | `ADMIN_HOST=<内网网卡地址>` | 与 `HOST=0.0.0.0` 组合使用,把管理端限制在内网 |
改完执行 `systemctl restart opendance-api`(或对应的 `ctl.sh restart`)生效。
## 三、必须同步调整的两项
1. **上传上限**:本服务默认允许 512 MiB(`UPLOAD_MAX_BYTES`)。
代理层默认值通常远小于此(nginx 1 MB、IIS 约 28.6 MB),不改会导致大模型上传失败。
2. **超时**:上传大文件与下载关键点序列可能较慢,代理的读写超时必须放大。
## 四、nginx 配置
完整样例见 [`nginx.conf.example`](nginx.conf.example)。核心内容:
```nginx
upstream opendance_api {
server 127.0.0.1:3000;
keepalive 16;
}
server {
listen 80;
server_name your.domain.com;
client_max_body_size 512m; # ← 必须放开,否则 VRM/FBX 上传失败
client_body_timeout 300s;
location / {
proxy_pass http://opendance_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 30s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off; # 大文件直接透传,不在代理落盘
}
}
```
安装位置与重载:
```bash
# Debian / Ubuntu
cp nginx.conf.example /etc/nginx/sites-available/opendance.conf
ln -s /etc/nginx/sites-available/opendance.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# RHEL 系
cp nginx.conf.example /etc/nginx/conf.d/opendance.conf
nginx -t && systemctl reload nginx
```
## 五、IIS 配置
IIS 需要先安装两个模块(**默认都没有**):
1. **URL Rewrite** - https://www.iis.net/downloads/microsoft/url-rewrite
2. **Application Request Routing (ARR)** - https://www.iis.net/downloads/microsoft/application-request-routing
装完 ARR 后还需启用代理(否则会报 404.4):
```
IIS 管理器 -> 选中服务器节点 -> Application Request Routing Cache
-> 右侧 Server Proxy Settings -> 勾选 Enable proxy -> 应用
```
完整 `web.config` 样例见 [`web.config.example`](web.config.example),放到站点根目录即可:
```xml
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="OpenDanceReverseProxy" stopProcessing="true">
<match url="(.*)" />
<action type="Rewrite" url="http://127.0.0.1:3000/{R:1}" />
</rule>
</rules>
</rewrite>
<security>
<requestFiltering>
<!-- 512 MiB:IIS 默认约 28.6 MB,不改大文件上传必失败 -->
<requestLimits maxAllowedContentLength="536870912" />
</requestFiltering>
</security>
<proxy preserveHostHeader="true" />
<serverRuntime uploadReadAheadSize="536870912" />
</system.webServer>
</configuration>
```
### IIS 容易踩的三个坑
1. **`requestLimits` 与 `serverRuntime` 要一起改**。只改前者,SSL 场景下仍会在
读取请求体时失败(`uploadReadAheadSize` 默认仅 48 KB)。
2. **代理超时改的是服务器级配置**,不是站点级。ARR 默认 120 秒,上传大文件会
中断。在 `applicationHost.config` 的 `<webProxy>` 节点调整:
```xml
<webProxy enabled="true" timeout="00:05:00" />
```
3. **`preserveHostHeader`** 需与 ARR 的 "Preserve host header" 设置一致,
否则后端收到的 Host 会变成 `127.0.0.1:3000`,影响日志与重定向。
## 六、HTTPS 与访问控制(可选)
- **HTTPS 终止在反代**:本服务只监听 HTTP,证书配置在 nginx/IIS 侧即可。
- **访问控制**:本版本**未内置登录认证**,如需公网访问,请在反代层加
Basic Auth、IP 白名单或对接已有 SSO。未加任何防护时请只在内网开放。
## 七、自检清单
配置完成后逐项确认:
```bash
# 0. 两个端口都在监听(双端口部署)
ss -tlnp | grep -E ':(3000|3001)'
# 1. 后端自身可用(绕过反代)
curl -i http://127.0.0.1:3000/health
curl -i http://127.0.0.1:3001/health
# 2. 经反代可访问
# <反代地址> 指你们自己的反代域名或 IP(由部署方决定),不是被脱敏的信息。
curl -i http://<反代地址>/health
# 3. 管理端页面可打开(应返回 HTML)
curl -i http://<反代地址>/ | head -5
# 4. 前端深链接可用(应返回同一份 HTML,而不是 404)
curl -i http://<反代地址>/songs | head -5
# 5. API 可用
curl -i http://<反代地址>/api/catalog/version
# 6. 上传上限已放开:找一个小于 UPLOAD_MAX_BYTES 的文件试传
# 若返回 413 说明代理层限制没改
```
| 现象 | 原因 |
|--|--|
| 反代 502 / 504 | 后端未启动,或 `HOST` 绑定地址不对(同机应为 `127.0.0.1`,异机应为 `0.0.0.0`) |
| 页面能开、接口 404 | 只转发了 `/`,需要把 `/api/`、`/health`、`/static/` 一并转发(最简单是全匹配) |
| 上传大文件 413 | 代理层上传上限没放开(见第三、四、五节) |
| 上传中途断开 | 代理超时太短 |
| `/songs` 直接访问 404 | SPA 回退被代理拦截;本服务已处理,代理**不要**自己加 `try_files`,全匹配转发即可 |
| 客户端接口全 404 | 把客户端的转发规则指到了管理端口之外,或误把 `/api` 前缀重写掉;客户端应转发到软件 API 端口 |
| 管理页面 404 | 把浏览器的转发规则指到了**软件 API 端口**(该端口不提供管理页面,这是有意设计);改指 `ADMIN_PORT` |
| 两个端口都转发到同一条规则 | 若确实想合并两个端口,设置 `ADMIN_PORT=PORT` 走单端口模式,而不是让反代去猜 |
| **Gitea 的 Release 附件上传 413** | 这是**另一台站点**(托管 Gitea 的 IIS),本服务的 `UPLOAD_MAX_BYTES` 管不到它;见第九节 |
## 八、还没有反代时:直连后端怎么访问
在运维侧配好反代之前,可以直接访问后端业务服务(本项目开箱即用,不依赖反代):
1. `deploy/.env` 中 `HOST=0.0.0.0`(若只想让某个网段访问,也可填具体网卡地址)
2. 防火墙放行后端端口(默认 `3000/tcp`)
3. 浏览器访问 `http://<服务器IP>:3000`
> 注意:服务默认在 **3000** 端口,不在 80/443。直接访问 `http://<服务器IP>/`
> 是打不开的--那需要反代把 80/443 转到 3000,属于本节的配置范围。
一条命令定位「服务起来了但外部访问不了」:
```bash
bash deploy/check-access.sh # 只读诊断,不改配置
bash deploy/check-access.sh /opt/opendance # 指定安装目录
```
脚本会打印 `.env` 中的 `HOST`/`PORT`、实际监听地址(重点看是否只监听
`127.0.0.1`)、回环与对外 IP 两次自测结果、80/443 上有没有反代、
firewalld / ufw / iptables 的放行情况,并直接给出修复命令。
---
## 九、Gitea 站点的附件上传上限(Windows 客户端整包必须放开)
> 本节与第一~五节是**两个不同的站点**:前者是本 Web 服务的反代,后者是托管
> **Gitea**(源码仓库 + Release 附件)的站点。Windows 客户端的发布整包走 Gitea,
> 因此**必须**单独放开 Gitea 站点的上限。
### 9.1 实测结论(含一次**误判更正**,请以本节为准)
#### A. IIS 的 `maxAllowedContentLength` 是**真实且当前仍在生效**的上限
| 上传体积 | 结果 |
|--|--|
| 927 KB / 14 MB / 16,372,427 B | 通过(HTTP 201) |
| 30.5 MB | **失败 HTTP 413**(IIS「请求实体过大」页面) |
| 38,245,602 B(Windows 整包) | **失败 HTTP 413** |
| 40 MB | **失败 HTTP 413** |
→ IIS 的默认值 **`maxAllowedContentLength = 30,000,000` 字节(≈28.6 MiB)** 没有被放开,
这就是 Windows 端整包(约 37 MB)至今还要拆分卷/无法上传的**唯一真实原因**。
(`exe` 约 104 MB 未压缩 + `pck`;`deploy/proxy` 之外的另一台站点,见本节开头说明。)
**放开办法**:见 9.2 的四条配置(IIS `requestLimits` + `uploadReadAheadSize`、
ARR `maxRequestBytes`、Gitea `app.ini` 的 `[attachment] MAX_SIZE`、前置 nginx 的
`client_max_body_size`),改完 `iisreset /noforce`。
#### B. 【更正】Gitea 附件接口是好的 —— 之前"HTTP 500 / 磁盘写满"是**请求写法错误**
排查过程中我一度判定"Gitea 附件通道坏了、宿主机磁盘写满",**这个结论是错的**,特此更正,
并把踩坑点记录下来:
Gitea `POST /repos/{owner}/{repo}/releases/{id}/assets` 的 multipart 字段名是
**`attachment`**(见 `/api/v1/swagger`),不是 `file`。用 `-F "file=@…"` 提交时
**Gitea 返回 HTTP 500,且响应体是空消息**:
```json
{"message":"","url":"https://<gitea>/api/swagger"}
```
这个空消息极像"服务器内部错误 / 磁盘写满",实际只是"没有找到名为 `attachment` 的表单字段"。
实测对照(同一个 1 KB 文件、同一个 Release):
| 写法 | 结果 |
|--|--|
| `-F "attachment=@probe.bin"` | **201** |
| `--data-binary @probe.bin` + `-H "Content-Type: application/octet-stream"`(URL 带 `?name=`) | **201** |
| `-F "file=@probe.bin"` | **500**(空消息) |
**正确的两种上传写法**(都可直接照抄):
```bash
# 写法 1:multipart,字段名必须是 attachment
curl -f -X POST \
-H "Authorization: token <token>" \
-F "attachment=@opendance-windows-vX.Y.Z.zip" \
"https://gitea.kasugano.cn/api/v1/repos/zijunchiang/opendance-windows/releases/<id>/assets?name=opendance-windows-vX.Y.Z.zip"
# 写法 2:裸 body + ?name=(大文件更省内存)
curl -f -X POST \
-H "Authorization: token <token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @SHA256SUMS \
"https://gitea.kasugano.cn/api/v1/repos/zijunchiang/opendance-web/releases/<id>/assets?name=SHA256SUMS"
```
> 另:`[attachment] MAX_SIZE` 仍然是需要放开的一项(默认可能只有 4~32 MB),
> 但它超限时返回的是**带消息**的错误,而不是上面那种空消息 500。
### 9.2 正确做法:放开 Gitea 站点上限到 256 MB
1. **IIS**(托管 Gitea 的站点根目录 `web.config`):
```xml
<configuration>
<system.webServer>
<security>
<requestFiltering>
<!-- 256 MiB;默认 30,000,000 字节,Windows 整包会被 413 掉 -->
<requestLimits maxAllowedContentLength="268435456" />
</requestFiltering>
</security>
<serverRuntime uploadReadAheadSize="268435456" />
</system.webServer>
</configuration>
```
`uploadReadAheadSize` 必须一起改(SSL 场景下默认仅 48 KB,会中途失败)。
2. **ARR**(服务器级,不是站点级)在 `applicationHost.config` 中同步放宽请求体上限:
```xml
<system.webServer>
<proxy>
<forwarding />
</proxy>
</system.webServer>
<webProxy enabled="true" timeout="00:05:00" />
<serverRuntime uploadReadAheadSize="268435456" />
```
并在 IIS 管理器 → Application Request Routing Cache → Server Proxy Settings
中确认请求体上限(`maxRequestBytes`)≥ `268435456`。
3. **Gitea 自身** `app.ini`:
```ini
[attachment]
; 默认通常为 4~32 MB,必须一起放大,否则 IIS 放行了 Gitea 仍会拒绝
MAX_SIZE = 256
```
4. **若 Gitea 前面还有 nginx**(IIS 之外或之内):
```nginx
client_max_body_size 256m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
```
5. 改完执行 `iisreset /noforce`(或只重启站点),再按下节复验。
> **注意**:`requestLimits` 只是第一道闸。IIS 放开后若变成 **500**(而不是 413),
> 说明请求体已经到达 Gitea —— 此时按 9.1 的结论排查 **Gitea 宿主机磁盘空间**
> (1 KB 也失败就说明与体积无关),然后再看 `[attachment] MAX_SIZE`。
### 9.3 复验方法(用**正确**的字段名,否则会得到误导性的空消息 500)
```bash
# 造一个 40 MB 文件,直接打 Gitea 的附件接口
head -c 40000000 /dev/urandom > /tmp/probe.bin
curl -i -X POST \
-H "Authorization: token <token>" \
-F "attachment=@/tmp/probe.bin" \
"https://gitea.kasugano.cn/api/v1/repos/zijunchiang/<repo>/releases/<id>/assets?name=probe.bin"
```
预期:
* 放开 IIS 前 → **HTTP 413**(IIS 的"请求实体过大")
* 放开 IIS 后 → **HTTP 201**(返回资产 JSON)
* 若得到 **HTTP 500 + `{"message":"","url":"…/api/swagger"}`** → 十有八九是**字段名写错**
(用了 `file` 而不是 `attachment`),不是服务器故障