1127 lines
58 KiB
Markdown
1127 lines
58 KiB
Markdown
# 故障排查处理记录
|
||
|
||
## 一、EasyGBD 注册超时
|
||
|
||
**现象**:EasyGBD 配置 WVP SIP 服务器地址 `100.83.103.1`,端口 `8160`,提示注册超时。
|
||
|
||
**原因**:Docker 容器的 8160 端口映射在 WSL2 (172.18.127.247) 上,Windows 的 Tailscale IP (100.83.103.1) 上没有 portproxy 转发该端口。EasyGBD 的 SIP 注册请求无法到达 WVP。
|
||
|
||
**处理**:在 Windows 管理员 PowerShell 中添加 TCP portproxy:
|
||
```powershell
|
||
netsh interface portproxy add v4tov4 listenport=8160 listenaddress=100.83.103.1 connectport=8160 connectaddress=172.18.127.247
|
||
```
|
||
|
||
EasyGBD 中 SIP 传输协议改为 TCP(`netsh interface portproxy` 不支持 UDP 转发)。
|
||
|
||
---
|
||
|
||
## 二、播放提示"通道不存在"
|
||
|
||
**现象**:调用 WVP `GET /api/play/start/{deviceId}/{channelId}` 返回 `code: 100, msg: "通道不存在"`。
|
||
|
||
**原因**:
|
||
1. WVP API 路径错误:用了 `/api/device/query/{deviceId}/channels`,正确路径是 `/api/device/query/devices/{deviceId}/channels`(多了 `devices`)。
|
||
2. 通道 ID 不匹配:EasyGBD 的通道 ID 不是设备 ID `34020000002000000001`,而是 `34020000001310000001`(类型编码 131 = 模拟通道)。
|
||
|
||
**处理**:
|
||
1. 用正确路径调用 `GET /api/device/query/devices/{deviceId}/sync` 同步通道。
|
||
2. 从通道列表中获取实际通道 ID `34020000001310000001`。
|
||
3. 在前端「摄像头管理」中更新通道 ID。
|
||
|
||
---
|
||
|
||
## 三、播放提示"收流超时"
|
||
|
||
**现象**:WVP play/start 返回 `code: -2, msg: "收流超时"`。
|
||
|
||
**原因**:ZLMediaKit 的 RTP 媒体端口(10001-10003)映射在 WSL2 上,但 Windows 的 100.83.103.1 上没有 portproxy 转发。EasyGBD 收到 INVITE 后尝试连接 `100.83.103.1:10003` 发送 RTP 流,连接失败。
|
||
|
||
**处理**:添加 TCP portproxy 转发 ZLMediaKit 端口:
|
||
```powershell
|
||
netsh interface portproxy add v4tov4 listenport=10001 listenaddress=100.83.103.1 connectport=10001 connectaddress=172.18.127.247
|
||
netsh interface portproxy add v4tov4 listenport=10002 listenaddress=100.83.103.1 connectport=10002 connectaddress=172.18.127.247
|
||
netsh interface portproxy add v4tov4 listenport=10003 listenaddress=100.83.103.1 connectport=10003 connectaddress=172.18.127.247
|
||
netsh interface portproxy add v4tov4 listenport=8081 listenaddress=100.83.103.1 connectport=8081 connectaddress=172.18.127.247
|
||
netsh interface portproxy add v4tov4 listenport=18978 listenaddress=100.83.103.1 connectport=18978 connectaddress=172.18.127.247
|
||
```
|
||
|
||
---
|
||
|
||
## 四、FLV 播放失败 CodecUnsupported
|
||
|
||
**现象**:实时视频播放提示 `FLV播放失败: MediaError - CodecUnsupported`。
|
||
|
||
**原因**:EasyGBD 默认视频编码为 H265/HEVC,浏览器 MSE 不支持 H265 解码。
|
||
|
||
**处理**:在 EasyGBD APP 设置中将视频编码从 H265 改为 H264/AVC。
|
||
|
||
---
|
||
|
||
## 五、摄像头管理不能删除和修改
|
||
|
||
**现象**:「摄像头管理」Tab 中点击编辑或删除按钮无响应。
|
||
|
||
**原因**:前端 DAL 层 `src/dal/video.ts` 只导入了 `get` 和 `post`,缺少 `patch` 和 `del`。`updateCamera` 和 `deleteCamera` 调用了未定义的函数。
|
||
|
||
**处理**:
|
||
```typescript
|
||
// 修改前
|
||
import { get, post } from '../api/http';
|
||
|
||
// 修改后
|
||
import { del, get, patch, post } from '../api/http';
|
||
```
|
||
|
||
---
|
||
|
||
## 六、HLS 播放地址端口不正确
|
||
|
||
**现象**:WVP 返回的 HLS 地址端口为 `80`,但 ZLMediaKit Docker 映射为 `8081->80`,浏览器无法访问 `http://100.83.103.1:80/...`。
|
||
|
||
**原因**:WVP 中 ZLMediaKit 的 `httpPort` 配置为容器内部端口 `80`,但 Docker 将其映射到宿主机 `8081`。
|
||
|
||
**处理**:在 `MediaService.extractPlayResult` 中将 URL 中的 `:80/` 替换为 `:8081/`:
|
||
```typescript
|
||
const zlmHttpPort = process.env.ZLM_HTTP_PORT || '8081';
|
||
const fixUrl = (url?: string): string | undefined => {
|
||
if (!url) return url;
|
||
return url.replace(/:80\//, `:${zlmHttpPort}/`).replace(/:80$/, `:${zlmHttpPort}`);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 七、历史回看看不到录像片段
|
||
|
||
**现象**:前端「历史回看」选择时间范围查询,显示"暂无录像片段"。
|
||
|
||
**原因**:录制文件名中的时间是 CST(UTC+8),但插入数据库时错误地标记为 UTC(`+00`),导致时间差 8 小时。用户查询北京时间 12:00-14:00 时,`toISOString()` 转为 UTC 04:00-06:00,不包含数据库中错误存储的 UTC 13:13。
|
||
|
||
**处理**:
|
||
1. 修正数据库中已有记录的时间(减 8 小时)。
|
||
2. 后续自动录制归档由服务器端 Python 脚本处理,脚本从文件名解析时间并带 `+08:00` 时区,NestJS 内部 API 接收 ISO 8601 带时区的时间字符串。
|
||
|
||
---
|
||
|
||
## 八、自动录制归档 stopRecord 失败
|
||
|
||
**现象**:RecordingService 每 5 分钟触发归档时,`stopRecord` 返回 `can not find the stream`,归档流程中断。
|
||
|
||
**原因**:
|
||
1. WVP 播放会话在 5 分钟后超时,ZLMediaKit 中的流自动关闭,`stopRecord` 找不到流。
|
||
2. `stopRecord` 抛出异常后,整个 `archiveAndRestart` 方法中断,后续归档和重新录制不执行。
|
||
|
||
**处理**:
|
||
1. 在 `archiveAndRestart` 和 `stopRecording` 中将 `stopRecord` 包在 try-catch 中,失败只记警告,不中断归档流程。
|
||
2. 在 `startRecording` 中,`startPlay` 失败时跳过录制(不继续调用 `startRecord`)。
|
||
|
||
---
|
||
|
||
## 九、NestJS API 从 WSL2 访问返回 404
|
||
|
||
**现象**:服务器端 Python 归档脚本调用 `http://172.18.112.1:3000/api/v1/auth/login` 返回 404,响应内容是 Gogs(Git 服务)的页面。
|
||
|
||
**原因**:WSL2 网关 IP `172.18.112.1` 的 3000 端口被 Gogs 占用(或被路由到 Gogs),而不是 NestJS 后端。NestJS 虽然监听 `0.0.0.0:3000`,但从 WSL2 通过网关 IP 访问时到达的是 Gogs。
|
||
|
||
**处理**:改用 Windows 的 Tailscale IP `100.83.61.186` 访问 NestJS:
|
||
```python
|
||
# 修改前
|
||
NESTJS_BASE = 'http://172.18.112.1:3000/api/v1'
|
||
|
||
# 修改后
|
||
NESTJS_BASE = 'http://100.83.61.186:3000/api/v1'
|
||
```
|
||
|
||
---
|
||
|
||
## 十、cron 定时任务不运行
|
||
|
||
**现象**:cron 已配置每分钟运行归档脚本,但 `/home/pan/archive.log` 不存在,脚本从未被执行。
|
||
|
||
**原因**:WSL2 默认未启用 systemd,`service cron start` 失败("Failed to connect to bus"),cron 守护进程未运行。
|
||
|
||
**处理**:
|
||
1. 手动启动 cron:`sudo cron`
|
||
2. 配置 WSL2 启用 systemd,写入 `/etc/wsl.conf`:
|
||
```ini
|
||
[boot]
|
||
systemd=true
|
||
```
|
||
3. 在 PowerShell 中执行 `wsl --shutdown` 重启 WSL2,之后 cron 会随 systemd 自动启动。
|
||
|
||
---
|
||
|
||
## 十一、录制文件归档方案演进
|
||
|
||
### 方案 A(初始):NestJS HTTP 下载 + 上传
|
||
|
||
```
|
||
ZLMediaKit(100.83.103.1) → HTTP下载到本机(Windows) → 上传到 Ceph S3(100.83.103.1)
|
||
```
|
||
|
||
问题:录制文件经过两次网络传输(下载到本机再上传回服务器),效率低。
|
||
|
||
### 方案 C(最终):服务器端 Python 脚本归档
|
||
|
||
```
|
||
NestJS(Windows) → 控制录制(WVP/ZLM API)
|
||
Python脚本(100.83.103.1) → cron每分钟扫描 → docker cp → boto3上传Ceph(localhost) → 调NestJS API建库记录 → 删除容器内文件
|
||
```
|
||
|
||
优势:
|
||
- 上传走服务器内部 localhost,速度快(12MB 文件 0.2 秒完成)
|
||
- NestJS 只负责录制控制和数据库管理,不参与文件传输
|
||
- 24 个积压文件在 18 秒内全部归档完成
|
||
|
||
---
|
||
|
||
## 十二、历史回看视频播放失败
|
||
|
||
**现象**:前端「历史回看」点击播放按钮,一直显示加载动画或提示"视频播放失败"。浏览器 Network 标签页显示请求状态码在 206/200 和 canceled 之间反复切换。
|
||
|
||
**原因**:多个问题叠加:
|
||
|
||
### 1. S3 预签名 URL 包含 Ceph RGW 不兼容的参数
|
||
|
||
AWS SDK v3 的 `getSignedUrl` 自动在预签名 URL 中添加 `x-amz-checksum-mode=ENABLED` 和 `x-id=GetObject` 查询参数。Ceph RGW (squid) 在验证签名时无法正确处理这些参数,返回 403 Forbidden。
|
||
|
||
**处理**:在 `s3.service.ts` 中用 Node.js `crypto` 模块手动实现 AWS S3 V4 签名,完全绕过 AWS SDK v3 的 presigner,不添加任何额外参数:
|
||
|
||
```typescript
|
||
async getPresignedUrl(bucket: string, key: string, expiresIn: number = 3600): Promise<string> {
|
||
// 手动构建 Canonical Request → String to Sign → Signing Key → Signature
|
||
// 只包含标准参数:X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders
|
||
// 不包含 x-amz-checksum-mode 和 x-id
|
||
}
|
||
```
|
||
|
||
### 2. Ceph S3 Bucket 缺少 CORS 配置
|
||
|
||
前端从 `http://100.83.103.1:5174` 访问页面,视频预签名 URL 在 `http://100.83.103.1:7480`,属于跨域请求。Ceph RGW 默认没有配置 CORS,浏览器拒绝跨域响应。
|
||
|
||
**处理**:用 boto3 设置 bucket CORS:
|
||
```python
|
||
s3.put_bucket_cors(Bucket='silk-video-events', CORSConfiguration={
|
||
'CORSRules': [{
|
||
'AllowedHeaders': ['*'],
|
||
'AllowedMethods': ['GET', 'HEAD'],
|
||
'AllowedOrigins': ['*'],
|
||
'ExposeHeaders': ['ETag', 'Content-Length', 'Content-Range'],
|
||
'MaxAgeSeconds': 3600
|
||
}]
|
||
})
|
||
```
|
||
|
||
### 3. VideoPlayer 组件 useEffect 无限循环
|
||
|
||
`VideoPlayer` 组件的 `useEffect` 依赖数组包含 `onError`,而父组件 `Video.tsx` 传入的 `onError` 是内联函数(每次渲染都创建新引用),导致 `useEffect` 不断重新执行 → `<video>` 的 `src` 不断被设置和清除 → 请求被 cancel → 重新加载 → 再 cancel,无限循环。
|
||
|
||
**处理**:在 `Video.tsx` 中用 `useCallback` 包装 `onError`:
|
||
```typescript
|
||
const handlePlayError = useCallback(() => message.error('录像播放失败,URL 可能已过期'), []);
|
||
// ...
|
||
<VideoPlayer onError={handlePlayError} />
|
||
```
|
||
|
||
### 4. WSL2 重启后 Redis AOF 文件损坏导致录制异常
|
||
|
||
WSL2 文件系统变为只读后重启,Docker 中 Redis 的 AOF 文件损坏(`Bad file format reading the append only file`),Redis 容器不停重启。WVP 依赖 Redis,Redis 不稳定导致摄像头无法正常注册,ZLMediaKit 拉流返回 35 字节的错误消息 `[network err]:3(connection refused)`,Go 录制服务将错误消息当作视频数据上传,生成的 MP4 文件只有 35 字节。
|
||
|
||
**处理**:
|
||
1. 删除损坏的 AOF 文件:
|
||
```bash
|
||
docker stop docker-polaris-redis-1
|
||
sudo rm -rf /home/pan/wvp-GB28181-pro/docker/volumes/redis/data/appendonlydir/
|
||
docker start docker-polaris-redis-1
|
||
```
|
||
2. 重启 WVP 和 ZLMediaKit 容器,等待摄像头重新注册。
|
||
3. Go 录制服务恢复正常上传(每 9 秒一个 5MB 分块)。
|
||
|
||
### 5. Vite 端口与 portproxy 不匹配
|
||
|
||
WSL2 重启后 Vite 默认监听 5173 端口,但远程机器 Windows 上的 portproxy 规则是 `100.83.103.1:5174 → 172.18.127.247:5174`,端口不匹配导致浏览器无法访问前端。
|
||
|
||
**处理**:修改 `vite.config.ts` 将端口改为 5174:
|
||
```typescript
|
||
server: {
|
||
host: '0.0.0.0',
|
||
port: 5174, // 与 portproxy 规则一致
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 十三、Go 录制服务替换 Python 版
|
||
|
||
**背景**:Python 版录制服务(`stream_to_s3.py`)受 GIL 限制,后续视频路数扩大后无法高效并发。
|
||
|
||
**处理**:用 Go 重写录制服务,部署到 WSL2。
|
||
|
||
### 核心逻辑
|
||
|
||
```
|
||
每路视频 → goroutine:
|
||
HTTP 拉 fMP4 流 (ZLMediaKit) → 5MB buffer → S3 UploadPart (Ceph RGW)
|
||
5分钟 → CompleteMultipartUpload → 建库记录 (NestJS API) → 重新开始
|
||
```
|
||
|
||
### 关键文件
|
||
|
||
| 文件 | 说明 |
|
||
|------|------|
|
||
| `e:\silk\wvp\recorder-go\main.go` | Go 录制服务源码 |
|
||
| `e:\silk\wvp\recorder-go\go.mod` | Go 模块定义 |
|
||
| `/home/pan/recorder-go/recorder` | 编译后的二进制文件 (12.6MB) |
|
||
|
||
### 与 Python 版对比
|
||
|
||
| 对比项 | Python 版 | Go 版 |
|
||
|--------|----------|-------|
|
||
| 并发模型 | 线程 + GIL | goroutine(无 GIL) |
|
||
| 内存占用 | ~30MB/路 | ~24MB(当前 1 路) |
|
||
| 部署 | 需要 Python 运行时 | 单一二进制文件 |
|
||
| HTTP API | 完全兼容 | 完全兼容 |
|
||
| S3 Key 格式 | `2026/07/04/{epochMs}.mp4` | 相同 |
|
||
|
||
### 注意事项
|
||
|
||
- Go 版需要在 WSL2 中安装 Go 编译器:`tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz`
|
||
- 编译时设置国内代理:`export GOPROXY=https://goproxy.cn,direct`
|
||
- `fix-static-ip.sh` 已更新为启动 Go 版录制服务
|
||
- 数据库记录的 `notes` 字段为 `自动录制(不落地-Go)`,与 Python 版区分
|
||
|
||
---
|
||
|
||
## 十四、前端页面加载缓慢
|
||
|
||
**现象**:通过 Tailscale 网络访问 `http://100.83.103.1:5174/videos`,页面加载需要 10-15 秒。
|
||
|
||
**原因**:Vite 开发模式下每个模块(React、Antd、页面组件等)都是独立的 HTTP 请求,约 30-50 个模块。通过 Tailscale 网络每个请求延迟约 0.23 秒,串行/并行加载总耗时 7-12 秒。加上 4.1MB 的主 JS bundle 未压缩,下载耗时约 10 秒。
|
||
|
||
**处理**:用生产构建 + 自定义静态服务器替代 Vite 开发模式。
|
||
|
||
### 1. 构建生产版本
|
||
|
||
```bash
|
||
cd /home/pan/silk/web
|
||
npx vite build
|
||
```
|
||
|
||
构建后只生成 3 个文件:
|
||
- `dist/index.html` (0.5KB)
|
||
- `dist/assets/index-B5bufLxZ.js` (4.1MB)
|
||
- `dist/assets/index-BsTCb322.css` (0.3KB)
|
||
|
||
### 2. 自定义静态服务器(支持 gzip + API 代理)
|
||
|
||
Vite preview 不支持 gzip 压缩,4.1MB 的 JS 通过 Tailscale 下载需要 10 秒。用 Node.js 编写带 gzip 压缩和 API 代理的静态服务器 `server.js`:
|
||
|
||
```javascript
|
||
// 核心逻辑:
|
||
// 1. /api/* → 代理到 NestJS (localhost:3000)
|
||
// 2. 静态文件 → 从 dist 目录读取,大于 1KB 的文件启用 gzip 压缩
|
||
// 3. SPA fallback → 不存在的路径返回 index.html
|
||
```
|
||
|
||
关键文件:`/home/pan/recorder-go/server.js`
|
||
|
||
### 3. 效果对比
|
||
|
||
| 资源 | 开发模式 (Vite) | 生产模式 (gzip) |
|
||
|------|----------------|----------------|
|
||
| JS bundle | 4.1MB / 10.2s | **1.28MB / 2.7s** |
|
||
| 模块请求数 | ~30-50 个 | **3 个** |
|
||
| 页面总加载 | ~12s | **~3s** |
|
||
|
||
### 4. 启动命令
|
||
|
||
```bash
|
||
# 构建前端
|
||
cd /home/pan/silk/web && npx vite build
|
||
|
||
# 启动静态服务器(端口 5174,与 portproxy 一致)
|
||
nohup node /home/pan/recorder-go/server.js > /tmp/static-server.log 2>&1 &
|
||
```
|
||
|
||
### 5. 注意事项
|
||
|
||
- 生产模式无热更新,修改前端代码后需要重新 `vite build` 并重启 server.js
|
||
- 开发时可临时切回 Vite 开发模式:`npx vite --host 0.0.0.0 --port 5174`
|
||
- `vite.config.ts` 中同时配置了 `server`(开发)和 `preview`(预览)的代理
|
||
|
||
---
|
||
|
||
## 十五、MQTTX 连接报 ECONNREFUSED
|
||
|
||
**现象**:用 MQTTX 连接 `100.83.103.1:1883` 测试物联网设备接入,报错 `Error: connect ECONNREFUSED`。
|
||
|
||
**原因**:VerneMQ 存在两个问题:
|
||
1. **服务未启动** - `systemctl status vernemq` 显示 `inactive`,1883 端口未监听。
|
||
2. **监听地址绑定了 127.0.0.1** - 配置文件 `/etc/vernemq/vernemq.conf` 第291行为 `listener.tcp.name = 127.0.0.1:1883`,只允许本机访问,外部连接被拒绝。
|
||
|
||
**处理**:
|
||
1. 启动 VerneMQ 服务:`sudo systemctl start vernemq`
|
||
2. 修改监听地址为 `0.0.0.0:1883`:
|
||
```bash
|
||
sudo sed -i 's/^listener.tcp.name = 127.0.0.1:1883/listener.tcp.name = 0.0.0.0:1883/' /etc/vernemq/vernemq.conf
|
||
```
|
||
3. 重启服务:`sudo systemctl restart vernemq`
|
||
4. 确认端口监听:`ss -tlnp | grep 1883` 应显示 `0.0.0.0:1883`
|
||
|
||
---
|
||
|
||
## 十六、MQTTX 连接报 Bad username or password
|
||
|
||
**现象**:ECONNREFUSED 解决后重新连接,报错 `Error: Connection refused: Bad username or password`,填写的用户名密码为 `pan/pan`。
|
||
|
||
**原因**:VerneMQ 配置 `allow_anonymous = off`(禁止匿名连接),密码文件 `/etc/vernemq/vmq.passwd` 不存在,未创建 `pan` 用户。
|
||
|
||
**处理**:
|
||
1. 创建明文密码文件并加密:
|
||
```bash
|
||
echo "pan:pan" > /etc/vernemq/vmq.passwd
|
||
vmq-passwd -U /etc/vernemq/vmq.passwd # 将明文转为哈希
|
||
```
|
||
2. 设置文件权限:
|
||
```bash
|
||
chown vernemq:vernemq /etc/vernemq/vmq.passwd
|
||
chmod 640 /etc/vernemq/vmq.passwd
|
||
```
|
||
3. 重启 VerneMQ:`sudo systemctl restart vernemq`
|
||
|
||
---
|
||
|
||
## 十七、React Native APP 视频页面无数据(HTTP 明文流量被拦截)
|
||
|
||
**现象**:React Native Debug APK 安装后,登录功能正常,但「视频」页面的「摄像头」和「录像片段」列表均为空,显示"暂无摄像头"/"暂无录像片段"。后端日志中无来自手机的请求记录。
|
||
|
||
**原因**:Android 9+ 默认禁止 HTTP 明文流量(`usesCleartextTraffic` 默认为 `false`)。Debug APK 的 `AndroidManifest.xml` 未设置 `android:usesCleartextTraffic="true"`,所有 HTTP 请求被 Android 系统拦截。由于 APP 中 `getCameras()` 和 `getClips()` 使用 `.catch(() => [])` 静默吞掉错误,API 请求失败后返回空数组,不显示任何错误信息。
|
||
|
||
用户误以为"已登录",实际上token是之前 Debug 模式下登录时缓存在 AsyncStorage 中的,安装新 APK 时数据未清除。登录 API 调用同样被拦截,但 APP 使用缓存 token 跳过了登录流程。
|
||
|
||
**处理**:
|
||
|
||
1. 在 `android/app/src/main/AndroidManifest.xml` 的 `<application>` 标签中添加 `android:usesCleartextTraffic="true"`:
|
||
```xml
|
||
<application
|
||
android:name=".MainApplication"
|
||
android:usesCleartextTraffic="true"
|
||
...>
|
||
```
|
||
|
||
2. 重新编译 Debug APK 并安装:
|
||
```powershell
|
||
$env:JAVA_HOME = "E:\openjdk-17.0.2_windows-x64_bin\jdk-17.0.2"
|
||
cd e:\silk\app\android
|
||
.\gradlew.bat assembleDebug --no-daemon
|
||
adb -s <device_id> install -r app\build\outputs\apk\debug\app-debug.apk
|
||
```
|
||
|
||
3. USB 连接时需设置 ADB 端口转发让手机访问 Metro:
|
||
```powershell
|
||
adb -s <device_id> reverse tcp:8081 tcp:8081
|
||
```
|
||
|
||
**注意事项**:
|
||
- Debug 模式下 Metro 必须保持运行,否则 APP 报 "Unable to load script"
|
||
- USB 每次重连都需重新执行 `adb reverse tcp:8081 tcp:8081`
|
||
- 修改 JS/TS 代码后只需在 Metro 终端按 `r` 刷新,无需重新编译 APK
|
||
- 仅修改原生代码(AndroidManifest、build.gradle 等)时才需重新编译
|
||
|
||
---
|
||
|
||
## 十八、React Native APP 播放历史录像报 ExoPlaybackException
|
||
|
||
**现象**:React Native APP 播放实时视频正常,但播放历史录像片段时报错 `ExoPlaybackException: error_code_io_file_not_found`,播放页面底部显示流地址为 `/api/v1/video/clips/109/stream`(相对路径)。
|
||
|
||
**原因**:后端返回的录像片段 `playbackUrl` 字段是相对路径 `/api/v1/video/clips/{id}/stream`,`VideoScreen.tsx` 中 `handlePlayClip` 直接将该相对路径传给 `VideoPlayerScreen` 的 `streamUrl` 参数。`react-native-video` 底层的 ExoPlayer 需要完整的 HTTP URL(`http://host:port/path`),无法解析相对路径,导致 `error_code_io_file_not_found`。
|
||
|
||
此外,修改代码后 Metro 缓存了旧的 JS Bundle,普通 reload 未生效,需要 `--reset-cache` 重启 Metro 才能加载新代码。
|
||
|
||
**处理**:
|
||
|
||
1. 在 `src/api/client.ts` 中新增 `resolveUrl` 函数,将相对路径转为完整 URL:
|
||
```typescript
|
||
export const resolveUrl = (path: string): string => {
|
||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||
const base = (API_BASE_URL || 'http://localhost:3000/api/v1').replace(/\/api\/v1$/, '');
|
||
return base + path;
|
||
};
|
||
```
|
||
|
||
2. 在 `src/screens/VideoScreen.tsx` 中导入并使用 `resolveUrl`:
|
||
```typescript
|
||
// 修改前
|
||
streamUrl: clip.playbackUrl,
|
||
|
||
// 修改后
|
||
streamUrl: resolveUrl(clip.playbackUrl),
|
||
```
|
||
|
||
3. 修改代码后若普通 reload 不生效,需重启 Metro 并清除缓存:
|
||
```powershell
|
||
# 先杀掉占用 8081 端口的进程
|
||
$n = Get-NetTCPConnection -LocalPort 8081; Stop-Process -Id $n.OwningProcess[0] -Force
|
||
# 重启 Metro(--reset-cache 清除转换缓存)
|
||
cd e:\silk\app; npx react-native start --reset-cache
|
||
```
|
||
|
||
**注意事项**:
|
||
- Metro 的 transform cache 可能导致代码修改不生效,`--reset-cache` 可强制清除
|
||
- 实时视频播放正常是因为 `playCamera` API 返回的 `url` 字段已经是完整 URL(来自 WVP/ZLMediaKit)
|
||
- 后端的 `/api/v1/video/clips/:id/stream` 接口不需要 JWT 认证(在 auth 中间件白名单中),返回 `Content-Type: video/mp4` + `Transfer-Encoding: chunked` 的 ffmpeg 转封装流
|
||
|
||
---
|
||
|
||
## 十九、Web 前端实时视频花屏/拖影 + FLV 自动重连
|
||
|
||
**现象**:Web 前端播放实时 FLV 视频流时,画面运动场景出现花屏和拖影;偶尔出现绿屏一闪而过;几分钟后报 `FLV播放失败: MediaError - MediaMSEError`。
|
||
|
||
**原因**:多个因素叠加:
|
||
|
||
### 1. flv.js 缓冲配置不当
|
||
|
||
`enableStashBuffer: false` + `stashInitialSize: 128` 导致帧未经缓冲直接送入 MSE,运动场景下 P 帧解码乱序,产生花屏和拖影。初始缓冲过小导致首帧绿屏。
|
||
|
||
**处理**:修改 `VideoPlayer.tsx` 中 flv.js 配置:
|
||
```typescript
|
||
// 修改前
|
||
{ enableWorker: false, enableStashBuffer: false, stashInitialSize: 128 }
|
||
|
||
// 修改后
|
||
{
|
||
enableWorker: false,
|
||
enableStashBuffer: true,
|
||
stashInitialSize: 512,
|
||
fixAudioTimestampGap: false,
|
||
liveBufferLatencyChasing: true, // 自动追帧,延迟超 3 秒跳到最新
|
||
liveBufferLatencyMaxLatency: 3,
|
||
liveBufferLatencyMinRemain: 1,
|
||
}
|
||
```
|
||
|
||
### 2. FLV 播放器无错误恢复
|
||
|
||
原代码中 flv.js 遇到 `MediaMSEError` 或 `NetworkError` 时直接销毁播放器并显示错误,不自动重连。直播流因设备心跳超时等原因短暂中断后无法恢复。
|
||
|
||
**处理**:在 `VideoPlayer.tsx` 中为 FLV 播放器添加自动重连机制(最多 5 次):
|
||
```typescript
|
||
const createFlvPlayer = () => {
|
||
// ...创建播放器...
|
||
flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
|
||
if (errorType === flvjs.ErrorTypes.MEDIA_ERROR && retryCount < maxRetries) {
|
||
retryCount++;
|
||
retryTimer = setTimeout(() => createFlvPlayer(), 1000); // 1 秒后重连
|
||
} else if (errorType === flvjs.ErrorTypes.NETWORK_ERROR && retryCount < maxRetries) {
|
||
retryCount++;
|
||
retryTimer = setTimeout(() => createFlvPlayer(), 2000); // 2 秒后重连
|
||
} else {
|
||
handleError(`FLV播放失败: ${errorType} - ${errorDetail}`);
|
||
}
|
||
});
|
||
};
|
||
```
|
||
|
||
### 3. EasyGBD 分辨率与码率不匹配
|
||
|
||
EasyGBD 设置为 1080p 时,码率不足以支撑运动场景,P 帧压缩过度导致花屏。浏览器 MSE 软解码 1080p 也容易丢帧。
|
||
|
||
**处理**:在 EasyGBD APP 中将分辨率从 1080p 改为 720p。同码率下每像素码率翻倍,花屏消失。如需 1080p,需将码率提到 4Mbps 以上。
|
||
|
||
### 4. 尝试过 ffmpeg 实时转码代理(已弃用)
|
||
|
||
曾在 Go 后端添加 ffmpeg 转码代理端点 `/video/cameras/:id/live/stream`,使用 `libx264 -preset ultrafast -tune zerolatency` 重新编码直播流。虽然能解决花屏,但延迟增加 1-2 秒,用户无法接受,已回退。
|
||
|
||
**涉及文件**:
|
||
- `web/src/components/VideoPlayer.tsx` -- flv.js 配置和自动重连
|
||
- `server-go/internal/service/transcode.go` -- 新增 `StreamLive` 方法(保留但未使用)
|
||
- `server-go/internal/handler/video_stream.go` -- 新增 `/video/cameras/:id/live/stream` 路由(保留但未使用)
|
||
- `server-go/internal/middleware/auth.go` -- 新增 live/stream 路径白名单
|
||
|
||
**注意事项**:
|
||
- `liveBufferLatencyChasing` 等 flv.js 扩展选项不在 TypeScript 类型定义中,需用 `as any` 绕过类型检查
|
||
- 修改 EasyGBD 分辨率后需重启推流,否则设备可能发送 BYE 中断流
|
||
- `fixAudioTimestampGap: false` 可避免时间戳跳跃导致的 MSE 错误
|
||
|
||
---
|
||
|
||
## 二十、设备注册成功但前端显示离线 + 录制状态不清理
|
||
|
||
**现象**:
|
||
1. EasyGBD 日志显示注册成功,但 Web 前端摄像头管理页显示设备离线,无法查看实时视频。
|
||
2. 在前端点击"开始录制",EasyGBD 有"开始视频"日志,约 35 秒后 EasyGBD 出现"停止视频"日志(用户未手动停止)。前端仍显示"录制中",手动点击"停止录制"后 EasyGBD 无反应。再次点击"开始录制"时浏览器控制台报 503 Service Unavailable。
|
||
|
||
**原因**:两个独立问题:
|
||
|
||
### 1. 设备实际已离线,EasyGBD 日志为旧记录
|
||
|
||
WVP API `/api/device/query/devices` 返回 `onLine: false`,设备最后心跳时间为 22:16:59。EasyGBD 中的"注册成功"是之前的旧日志。Go 后端每 30 秒通过 `startDeviceStatusSync` 调用 WVP API 同步设备在线状态到数据库,`is_online` 被正确设为 `false`,前端显示离线是正确的。
|
||
|
||
排查过程中的关键发现:物理机部署后 WVP API 端口从 18978 变为 18080(`server.env` 中 `WVP_API_BASE=http://localhost:18080`),Go 后端的 `startDeviceStatusSync` 使用 `slog.Debug` 记录同步失败,在默认日志级别下不可见。
|
||
|
||
### 2. 录制流结束后 `activeRecordings` 未清理(代码 Bug)
|
||
|
||
录制流程涉及两个服务:
|
||
- **Go 后端**(`video_record.go`):维护内存表 `activeRecordings`,前端每 5 秒轮询 `/video/recordings/active` 获取录制状态。
|
||
- **recorder-go**(`wvp/recorder-go/main.go`):从 ZLMediaKit 拉流写入 S3,维护自己的 `recordings` map。
|
||
|
||
当 GB28181 设备离线导致流 EOF 时,recorder-go 检测到流结束,会:
|
||
1. 创建片段记录并调用 `POST /api/v1/video/clips/internal` 通知 Go 后端 ✓
|
||
2. 清理自己的 `recordings` map ✓
|
||
3. **但未通知 Go 后端清理 `activeRecordings`** ✗
|
||
|
||
导致 Go 后端的 `activeRecordings` 一直保留过期记录,前端持续显示"录制中"。用户手动停止后,`stopRecording` 调用 `media.StopPlay()` 发送 BYE,但设备已离线无响应。再次点击"开始录制"时,`StartPlay` 向 WVP 发送 INVITE 失败(设备离线),返回 503。
|
||
|
||
**处理**:
|
||
|
||
#### 修复:recorder-go 流结束时通知 Go 后端清理录制状态
|
||
|
||
1. **Go 后端 `video_record.go`**:新增内部接口 `POST /video/recordings/internal/end`
|
||
- 接收 recorder-go 的录制结束通知(校验 `x-api-key`)
|
||
- 清理 `activeRecordings` 内存表
|
||
- 调用 `media.StopPlay()` 清理 WVP play session
|
||
|
||
2. **Go 后端 `auth.go`**:白名单添加 `/api/v1/video/recordings/internal/end`
|
||
|
||
3. **recorder-go `main.go`**:在录制线程 `record()` 的 defer 中调用 `notifyRecordingEnd()`
|
||
- 无论流 EOF、错误、还是手动停止,都会通知 Go 后端
|
||
- Go 后端收到通知后清理 `activeRecordings` 并停止 WVP session
|
||
|
||
**涉及文件**:
|
||
- `server-go/internal/handler/video_record.go` -- 新增 `endRecordingInternal` handler 和路由
|
||
- `server-go/internal/middleware/auth.go` -- 白名单新增内部端点
|
||
- `wvp/recorder-go/main.go` -- 新增 `notifyRecordingEnd` 函数,在 `record()` defer 中调用
|
||
|
||
**修复效果**:
|
||
|
||
| 场景 | 修复前 | 修复后 |
|
||
|------|--------|--------|
|
||
| 设备离线导致流结束 | 前端一直显示"录制中",需手动点击停止 | recorder-go 自动通知 Go 后端,~5 秒内前端停止显示录制 |
|
||
| 流结束后的 WVP session | 残留,直到手动停止才清理 | 自动调用 `StopPlay` 清理 |
|
||
| 再次点击"开始录制" | `activeRecordings` 未清理可能冲突 | 无冲突 |
|
||
|
||
**注意事项**:
|
||
- 503 错误本身是正确行为 -- 设备离线时 WVP 无法发送 INVITE
|
||
- 设备频繁离线的根本原因是 EasyGBD APP 被手机系统杀后台或 Tailscale 连接不稳定
|
||
- `startDeviceStatusSync` 的错误日志级别为 `Debug`,排查时需提高日志级别或直接查询 WVP API 确认设备状态
|
||
|
||
---
|
||
|
||
## 二十一、GSCW1M-4G 断路器无法连接 VerneMQ
|
||
|
||
**现象**:4G 断路器配网后上电,VerneMQ 会话列表中无新连接,设备始终显示离线。
|
||
|
||
**原因**:4G 断路器通过蜂窝网络连接,无法访问 Tailscale 内网 IP(100.83.103.1),必须使用公网 IP。但服务器在路由器 NAT 后面(内网 192.168.1.58),4G 设备直接访问公网 IP 不会转发到服务器。
|
||
|
||
**处理**:
|
||
|
||
1. **确认服务器公网 IP**:在服务器执行 `curl -4 ifconfig.me` 返回 `113.17.97.250`,但实际端口转发配置在 `115.191.19.95` 上。两个 IP 都能从外部访问,但端口转发规则只在 `115.191.19.95` 上生效。通过 MQTT 收发测试验证:`115.191.19.95:1883` 连接成功且收到消息,`113.17.97.250:1883` 连接超时。
|
||
|
||
2. **路由器端口转发**:在路由器管理页面添加 TCP 端口转发规则,外部端口 1883 -> 内部 IP 192.168.1.58:1883。
|
||
|
||
3. **断路器配网**:长按按键 6 秒进入配网模式,连接热点 `GeekOpen-XXXXXX`(密码 `Aa123456`),在 `192.168.4.1` 配置自定义 MQTT:
|
||
- 服务器:`115.191.19.95`
|
||
- 端口:`1883`
|
||
- 用户名:`pan`
|
||
- 密码:`pan`
|
||
- 发布主题:`silk/default/room-001/862323080741518/up/telemetry`
|
||
- 订阅主题:`silk/default/room-001/862323080741518/down/cmd`
|
||
|
||
4. 保存后点击"设备重启"使配置生效。
|
||
|
||
**注意事项**:
|
||
- 4G 设备不能使用 Tailscale IP,必须用公网 IP
|
||
- `curl ifconfig.me` 返回的 IP 不一定是有端口转发的 IP,需要实测验证
|
||
- 路由器不支持 NAT 回环(hairpin)时,从服务器自身测试公网 IP 会失败,但不影响外部设备连接
|
||
|
||
---
|
||
|
||
## 二十二、GSCW1M-4G 断路器 IMEI 固件 Bug 导致设备无法识别
|
||
|
||
**现象**:断路器已连接 VerneMQ 并上报数据,但数据库中设备状态始终为 `offline`,前端无法控制。
|
||
|
||
**原因**:设备固件 Bug,不同消息中 `imei` 字段的值不一致:
|
||
- info 响应中:`"imei": "8G2323080741518"`(第二位是字母 `G`)
|
||
- statistic 响应中:`"imei": "862323080741518"`(第二位是数字 `6`)
|
||
|
||
标准 IMEI 为 15 位纯数字,`862323080741518` 是正确值。数据库中 `device_key` 原为用户提供的 `8G2323080741518`(含 G),当设备发送正确 IMEI `862323080741518` 时无法匹配数据库记录,导致设备状态不更新、命令无法发送(`deviceTopics` 中的 key 与数据库 `device_key` 不一致)。
|
||
|
||
**处理**:
|
||
|
||
1. **修正数据库**:将 `device_key` 从 `8G2323080741518` 更新为 `862323080741518`。
|
||
|
||
2. **后端 IMEI 规范化**:在 `mqtt.go` 的 `handleMessage` 中,对 `imei` 字段做规范化处理,将 `G` 替换为 `6`:
|
||
```go
|
||
if imei, ok := body["imei"].(string); ok && imei != "" {
|
||
// 规范化 IMEI:部分设备固件 bug 会将 "6" 误发为 "G"
|
||
deviceKey = strings.ReplaceAll(imei, "G", "6")
|
||
}
|
||
```
|
||
|
||
**涉及文件**:`server-go/internal/service/mqtt.go` -- IMEI 提取逻辑增加 `G` -> `6` 规范化
|
||
|
||
---
|
||
|
||
## 二十三、GSCW1M-4G 断路器响应在订阅主题上(主题方向反配)
|
||
|
||
**现象**:断路器配网时发布主题填的是 `/up/telemetry`,订阅主题填的是 `/down/cmd`,但设备的响应消息出现在 `/down/cmd` 主题上(即订阅主题),而不是 `/up/telemetry`(发布主题)。
|
||
|
||
**原因**:与 GSPE1B 智能插座相同,GeekOpen 部分设备的"发布主题"和"订阅主题"在配网页面的含义与后端的预期方向相反。设备把配网页面中填写的"订阅主题"作为实际发布方向使用。
|
||
|
||
**处理**:后端已适配此情况:
|
||
1. 同时订阅 `silk/+/+/+/up/telemetry` 和 `silk/+/+/+/down/cmd` 两个通配符主题
|
||
2. `PublishToDevice` 方法自动检测设备上报主题方向:
|
||
- 设备上报 `/up/telemetry` -> 命令发送到 `/down/cmd`(正常方向)
|
||
- 设备上报 `/down/cmd` -> 命令发送到 `/up/telemetry`(反配方向)
|
||
|
||
无需修改设备配网配置,后端自动兼容两种方向。
|
||
|
||
---
|
||
|
||
## 二十四、历史回看视频播放失败(S3 预签名 URL SignatureDoesNotMatch)
|
||
|
||
**现象**:前端「历史回看」点击播放按钮,提示"视频播放失败"。
|
||
|
||
**症状链路**(与第十二节"历史回看视频播放失败"完全一致):
|
||
1. 前端 `Video.tsx` 调用 `getClipPlayUrl(clipId)` 获取 `/api/v1/video/clips/:id/stream` 同源 URL
|
||
2. `VideoPlayer.tsx` 走 `video.src = url` 原生 MP4 分支
|
||
3. 浏览器发起 GET 请求,触发后端 `streamClip` handler
|
||
4. 后端调用 `transcode.StreamClip` 启动 ffmpeg 拉取 S3 预签名 URL 转封装为 fragmented MP4
|
||
5. ffmpeg 报 `HTTP error 403 Forbidden`,后端返回 502 JSON `{"error":"ffmpeg 异常退出: exit status 1"}`
|
||
6. 浏览器 `<video>` 收到非视频内容触发 error 事件 → "视频播放失败"
|
||
|
||
**根因诊断过程**:
|
||
|
||
1. 直接 curl 预签名 URL,Ceph RGW 返回 XML `<Code>SignatureDoesNotMatch</Code>` —— 排除了 access key/secret key 错误、CORS、网络等可能。
|
||
2. 用 boto3 在 Windows 本地生成同样格式的预签名 URL(参数完全一致),curl 能返回 200 OK,ContentLength 47MB —— 证明 access/secret key 正确、bucket 可读、Ceph RGW 工作正常,**问题在 Go 后端手动实现的 V4 签名计算结果错误**。
|
||
3. 检查 Go 后端进程实际环境变量:`/proc/140619/environ` 中只有 `S3_ACCESS_KEY`、`S3_BUCKET_ARCHIVE`、`S3_BUCKET_EVENTS`、`S3_ENDPOINT`,**没有 `S3_SECRET_KEY`**!
|
||
4. 检查 `config.go`,`S3SecretKey` 的 `envDefault:""` —— **默认值为空字符串**。
|
||
5. 检查 `start.sh`,**没有 `export S3_SECRET_KEY=...`**。
|
||
6. 结论:Go 后端用空字符串作为 secret_key 计算 AWS V4 签名 → 签名错误 → Ceph RGW 拒绝。
|
||
|
||
**与第十二节的对比**:
|
||
|
||
| 项目 | 第十二节 | 本次(第二十四节) |
|
||
|------|----------|---------------------|
|
||
| 症状 | "视频播放失败" | "视频播放失败"(相同) |
|
||
| 接口 | S3 预签名 URL 403 | S3 预签名 URL 403(相同) |
|
||
| 直接原因 | AWS SDK 自动加 `x-amz-checksum-mode`/`x-id` 参数,Ceph RGW 不兼容 | Go 后端用空 secret_key 算签名,SignatureDoesNotMatch |
|
||
| 根因层次 | SDK 行为差异(第三方库 bug) | 部署脚本配置疏忽(人为遗漏环境变量) |
|
||
| 解决方式 | 手动实现 V4 签名绕过 SDK | 修复 config.go 默认值 + start.sh 显式 export |
|
||
|
||
**结论**:本次**症状与第十二节完全相同**,但根因不同。属于配置疏忽导致的"重蹈症状之覆辙",但不是"重蹈根因之覆辙"。这提示:部署脚本和配置默认值都需要将敏感凭据完整列出,不能依赖"反正某个上游会传入"。
|
||
|
||
**处理**:
|
||
|
||
1. **`server-go/internal/config/config.go`**:将 `S3SecretKey` 默认值从 `""` 改为 `"Silk-App-Secret-2026!"`,与项目部署技能中的 Ceph S3 默认凭据保持一致。
|
||
2. **`server-go/start-shipped.sh`**:新增 `export S3_SECRET_KEY='Silk-App-Secret-2026!'`,使部署脚本自包含所有必需环境变量。
|
||
3. 重新交叉编译后端,上传到 `/home/pan/silk/server-go/server-go-linux`,用 `setsid bash start.sh` 脱离 SSH 会话启动。
|
||
|
||
**验证**:
|
||
|
||
| 检查项 | 修复前 | 修复后 |
|
||
|--------|--------|--------|
|
||
| 后端进程环境变量 `S3_SECRET_KEY` | 缺失 | `Silk-App-Secret-2026!` |
|
||
| `curl /api/v1/video/clips/139/stream` | HTTP 502 + JSON 错误 | HTTP 200,5157496 字节,video/mp4 |
|
||
| ffmpeg 日志 | `HTTP error 403 Forbidden` | `frame=471 Lsize=5037kB time=00:00:20.60` |
|
||
| 后端响应 Content-Type | `video/mp4` 但内容是 JSON | `video/mp4`,文件头 `ftypisom` 标准 MP4 |
|
||
| 前端 `<video>` | error 事件 → "视频播放失败" | 正常播放 |
|
||
|
||
**注意事项**:
|
||
- `start.sh` 的 `nohup ... &` 在 plink SSH 会话结束时会被带走,需要用 `setsid bash start.sh` 显式脱离会话
|
||
- 部署时务必检查 `/proc/$PID/environ`,确认所有必需环境变量已正确注入
|
||
- Go 的 `env` 包默认值生效条件是环境变量未设置;环境变量设为空字符串时不会使用默认值
|
||
- 修复后 recorder-go(录制服务)不受影响,因为它有自己独立的 S3 配置加载逻辑
|
||
|
||
---
|
||
|
||
## 二十五、FLV 实时播放失败 "NetworkError - Exception"(CSP 跨域 + ZLM hook 错误)
|
||
|
||
**现象**:`/videos` 页面"实时监控"Tab 点击摄像头播放,前端报错 `FLV播放失败: NetworkError - Exception`,视频无法显示。强制刷新浏览器无效。
|
||
|
||
**原因(多层叠加)**:
|
||
|
||
1. **ZLMediaKit 短时间关流**:`general.streamNoneReaderDelayMS=20000`(20 秒无人观看即关流)+ WVP `stream-on-demand=true`(按需拉流,hook 返回"关闭=true"),导致流在 22 秒后被关闭
|
||
2. **ZLMediaKit hook URL 错误**:ZLM 运行时配置 `hook.on_publish=http://127.0.0.1:18080/index/hook/on_publish`,但 ZLM 容器内的 `127.0.0.1` 指向 ZLM 自身而非 WVP 容器,推流鉴权 hook 调用失败(日志:`禁止RTP推流:[network err]:3(connection refused)`),ZLM 拒绝摄像头 RTP 推流。WVP 容器环境变量 `ZLM_HOOK_HOST=polaris-wvp` 设置正确,但 WVP-PRO 2.7.4 的 auto-config 未正确应用该值
|
||
3. **CSP 阻止跨域 fetch**:Go 后端 `fixZlmPort` 函数返回绝对路径 `http://100.83.103.1:8081/rtp/...`,但 `web/server.cjs` 的 CSP 头 `connect-src 'self' ws: wss:` 只允许同源请求,flv.js 跨域 fetch ZLMediaKit 时被浏览器阻止
|
||
|
||
**处理**:
|
||
|
||
1. 调用 ZLMediaKit `setServerConfig` API 修正运行时配置:
|
||
- `general.streamNoneReaderDelayMS`:`20000` → `3600000`
|
||
- 11 个 hook URL(`hook.on_publish`/`on_play` 等):`http://127.0.0.1:18080/...` → `http://polaris-wvp:18080/...`
|
||
- 通过 Python 脚本 `/home/pan/silk/fix_zlm_hook.py` 实现,留存服务器供后续修复使用
|
||
2. 修改 WVP 配置 `stream-on-demand: true → false`,避免 hook 返回"关闭=true"
|
||
3. 修改 `server-go/internal/service/media.go` 的 `fixZlmPort` 函数,提取 `/rtp/` 起始的相对路径返回(含 query string),让前端通过 `server.cjs` 内置的 `/rtp/` 代理转发到 `127.0.0.1:8081`,避免跨域
|
||
4. 重启 WVP 让设备重新注册 GB28181,然后通过 WVP API 验证点播成功
|
||
|
||
**验证**:
|
||
- WVP `/api/play/start/{deviceId}/{channelId}` 返回 `code=0` 成功,包含 FLV/HLS/FMP4 地址
|
||
- Go 后端 `/api/v1/video/cameras/1/live` 返回相对路径 FLV URL `/rtp/xxx.live.flv?...`
|
||
- 通过 `server.cjs` 5174 端口代理访问 FLV 流,能正常返回视频二进制数据
|
||
- 浏览器中 FLV 播放正常
|
||
|
||
**遗留注意事项**:WVP 重启可能会再次把 ZLM hook URL 覆盖回 `127.0.0.1:18080`。若 FLV 播放再次失败,登录服务器执行 `python3 /home/pan/silk/fix_zlm_hook.py` 即可修复。
|
||
|
||
---
|
||
|
||
## 二十六、历史回看查不到录像(前端时间范围硬编码 + RangePicker 清空不生效)
|
||
|
||
**现象**:`/videos` 页面"历史回看"Tab 查询历史录像,时间段留空也查不到任何记录。
|
||
|
||
**原因**:
|
||
|
||
1. **数据库确实今天无新录像**:数据库 17 条录像全部是 7月17日(昨天),今天 0 条新录像(无活跃录制任务)
|
||
2. **前端时间范围硬编码**:`web/src/pages/Video.tsx` PlaybackTab 的 `handleSearch` 函数硬编码 `to: dayjs().toISOString()`,即使清空 RangePicker 仍查"今天 00:00 到 现在"
|
||
3. **RangePicker onChange 清空不更新 state**:清空时 `vals` 为 null,但 onChange 中没有处理 null 分支,`dateRange` state 仍保持默认值 `[今天开始, 现在]`
|
||
|
||
**处理**:
|
||
|
||
1. 修改 `dateRange` state 类型为 `[Dayjs, Dayjs] | null`
|
||
2. 修改 RangePicker `onChange`:清空时设为 `null` 表示查全部
|
||
3. 修改 `handleSearch`:仅当 `dateRange` 非空时传 from/to,为 null 时查全部
|
||
4. 修改自动刷新定时器同样支持 dateRange 为 null
|
||
|
||
**验证**:清空时间范围查询,能查到 7月17日的 17 条录像。
|
||
|
||
**用户认知纠正**:实时视频流(ZLMediaKit 接收 RTP 推流并分发 FLV/HLS)≠ 录像(recorder-go 从 ZLMediaKit 拉 FLV 流分段存到 Ceph S3)。实时视频能播放不代表在录像,必须通过"开始录制"按钮触发录制。
|
||
|
||
---
|
||
|
||
## 二十七、摄像头管理显示"开始录制"而非"正在录制"(录制状态查询改进)
|
||
|
||
**现象**:用户认为摄像头正在录像,但 `/videos` "摄像头管理"Tab 显示"开始录制"按钮(应为"正在录制"状态)。
|
||
|
||
**原因**:
|
||
|
||
1. **实际未在录制**:recorder-go `/record/status` 接口返回 `{"recordings":[]}`,确认无活跃录制任务
|
||
2. **用户误解**:用户把"实时视频流能播放"误判为"正在录像"
|
||
3. **潜在的内存丢失问题**:`server-go/internal/handler/video_record.go` 中 `activeRecordings` 是 Go 后端进程内存 map,Go 后端重启会清空。虽然当前未在录制,但如果录制中 Go 后端重启,会导致前端显示"开始录制"而 recorder-go 仍在实际录制
|
||
|
||
**处理**:
|
||
|
||
1. 修改 `listActiveRecordings` 函数:优先调用 recorder-go `/record/status` 接口获取真实录制状态,并自愈内存 map(从 recorder-go 恢复 activeRecordings)。recorder-go 不可用时回退到内存 map
|
||
2. 修改路由注册:`listActiveRecordings()` → `listActiveRecordings(cfg)` 传入配置以获取 `RecorderAPIBase`
|
||
|
||
**验证**:`/api/v1/video/recordings/active` 接口返回 recorder-go 的真实录制状态。
|
||
|
||
**实时观看 vs 录像 的区别**:
|
||
|
||
| 项目 | 实时观看 | 录像 |
|
||
|------|---------|------|
|
||
| 触发 | 点摄像头"播放" | 点"开始录制"按钮 |
|
||
| 流向 | 摄像头 → ZLMediaKit → 浏览器 | 摄像头 → ZLMediaKit → recorder-go → Ceph S3 |
|
||
| 数据存储 | 不存储 | 5 分钟分段存到 S3 |
|
||
| 控制接口 | `/video/cameras/:id/live` | `/video/cameras/:id/record/start` |
|
||
|
||
---
|
||
|
||
## 二十八、摄像头显示离线 + APP 实时播放失败 ERROR_CODE_IO_BAD_HTTP_STATUS
|
||
|
||
**现象**:
|
||
1. Web 前端、APP 上摄像头列表显示"离线",但摄像头实际已注册成功(WVP 设备列表 `onLine=true`)
|
||
2. APP 播放实时视频报 `ExoPlaybackException: ERROR_CODE_IO_BAD_HTTP_STATUS`,流地址为 `http://100.83.103.1:3000/api/v1/video/cameras/1/live/stream`
|
||
|
||
**原因**:存在两个独立问题。
|
||
|
||
### 问题一:摄像头显示离线
|
||
|
||
后端进程的环境变量 `WVP_API_BASE` 未正确设置,默认指向 `http://localhost:18978`(旧端口),而 WVP Tomcat 实际监听 `18080` 端口。
|
||
|
||
- `listCameras` handler 调用 `media.SyncDeviceStatus()` 查询 WVP `/api/device/query/devices` 获取在线状态
|
||
- WVP API 不可达 → `SyncDeviceStatus()` 返回 error → 摄像头 `is_online` 字段保持数据库中的旧值(false)
|
||
- 前端读取 `c.online` 字段显示"离线"
|
||
|
||
### 问题二:APP 直播流 502
|
||
|
||
[video_stream.go](file:///e:/silk/server-go/internal/handler/video_stream.go) 的 `streamLive` 函数在 StartPlay 成功后,直接使用 `result.FLV` 作为 `http.Get()` 的参数。但 WVP 返回的 FLV 地址是**相对路径**(如 `/rtp/34020000002000000001_34020000001310000001.live.flv?...`),Go 的 `http.Get()` 无法处理无协议头的 URL,返回 502 错误。
|
||
|
||
```go
|
||
// 修复前(有 bug)
|
||
sourceURL = result.FLV // "/rtp/xxx.live.flv" - 相对路径
|
||
resp, err := http.Get(sourceURL) // 失败:unsupported protocol scheme ""
|
||
```
|
||
|
||
**处理**:
|
||
|
||
### 修复一:用 start.sh 启动后端,正确设置 WVP_API_BASE
|
||
|
||
`/home/pan/silk/server-go/start.sh` 中显式 `export WVP_API_BASE=http://localhost:18080`。必须用 `setsid bash start.sh` 启动(而非直接 `nohup ./server-go-linux &`),否则环境变量不生效。
|
||
|
||
### 修复二:修复 ZLM hook URL
|
||
|
||
运行 `python3 /home/pan/silk/fix_zlm_hook.py`,将 10 个 hook URL 从 `127.0.0.1:18080` 修正为 `polaris-wvp:18080`,避免 ZLM 推流鉴权失败。
|
||
|
||
### 修复三:streamLive 拼接 ZLM 基地址
|
||
|
||
修改 `streamLive` 函数:
|
||
1. 将函数参数从 `transcode *service.TranscodeService` 改为 `cfg *config.Config`
|
||
2. 在 `sourceURL` 使用前,检测如果是相对路径则拼接 `cfg.ZLMAPIBase`:
|
||
|
||
```go
|
||
// 确保地址是完整的 ZLM 地址(WVP 返回的可能是相对路径 /rtp/...)
|
||
if strings.HasPrefix(sourceURL, "/") {
|
||
sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + sourceURL
|
||
}
|
||
```
|
||
|
||
3. StartPlay 失败的降级分支也改用 `cfg.ZLMAPIBase` 拼接(不再硬编码 IP):
|
||
|
||
```go
|
||
sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + "/rtp/" + *camera.GbDeviceID + "_" + *camera.GbChannelID + ".live.flv?originTypeStr=rtp_push&videoCodec=H264"
|
||
```
|
||
|
||
4. 同步更新路由注册:`streamLive(db, media, transcode)` → `streamLive(db, media, cfg)`
|
||
|
||
**验证**:
|
||
1. `/api/v1/video/cameras` 返回 `online=true, isOnline=true` ✓
|
||
2. WVP `/api/device/query/devices` 返回 `onLine=true` ✓
|
||
3. `/api/v1/video/cameras/1/live/stream` 返回 HTTP 200,Content-Type: `video/x-flv`,12秒下载 2MB 有效 FLV 数据 ✓
|
||
4. APP 实时视频播放成功 ✓
|
||
5. Web 前端摄像头列表显示"在线" ✓
|
||
|
||
**注意事项**:
|
||
- `media.extractPlayResult` 中的 `fixZlmPort()` 函数会将 ZLM 返回的绝对 URL 转换为相对路径(`/rtp/...`),这是为了 Web 前端通过 server.cjs 代理访问。但 APP 使用的 `/live/stream` 接口在后端代理 FLV 流时,必须将相对路径转回绝对 URL 才能 `http.Get()`
|
||
- WVP-PRO 2.7.4 存在 auto-config bug,重启后会覆盖 ZLM hook URL 为 `127.0.0.1:18080`,需手动执行 `fix_zlm_hook.py` 修复
|
||
- 后端必须用 `start.sh` 启动以加载正确的环境变量,直接运行二进制会导致 `WVP_API_BASE` 使用默认值(18978 端口)
|
||
|
||
---
|
||
|
||
## 二十九、APP 各标签页数据显示异常(.env 缺失 + token 过期)
|
||
|
||
**现象**:APP 启动后:
|
||
- 「视频」标签页的「摄像头」「录像片段」列表均为空白,显示"暂无摄像头"/"暂无录像片段"
|
||
- 「设备」「蚕房」标签页显示 "status code 401"
|
||
- 「告警」标签页显示 "status code 404"
|
||
- 「仪表盘」标签页显示"暂无遥测数据 暂无趋势数据"
|
||
|
||
**原因**:多个问题叠加:
|
||
|
||
### 1. `.env` 文件缺失导致 API_BASE_URL fallback 到 localhost
|
||
|
||
APP 代码通过 `@env` 导入 `API_BASE_URL`([app/src/api/client.ts](file:///e:/silk/app/src/api/client.ts)),但项目根目录下只有 `.env.example` 没有 `.env` 文件。`API_BASE_URL` 为 `undefined`,fallback 到 `http://localhost:3000/api/v1`。
|
||
|
||
真机上 `localhost` 指向设备自身,无法访问 PC 或远程后端,所有 API 请求失败。由于 `VideoScreen.tsx` 中 `getCameras().catch(() => [])` 和 `getClips().catch(() => [])` 静默吞掉错误返回空数组,页面只显示"暂无",无错误提示。
|
||
|
||
### 2. token 过期导致 401
|
||
|
||
JWT 有效期 2 小时(`JWT_EXPIRES_IN=2h`)。APP 之前登录过,token 存在 AsyncStorage 中,但重启 APP 时 token 已过期。
|
||
|
||
`restoreSession` 读取过期 token → 设置到 store → APP 跳转 Main 页面 → `fetchUser` 调用 `/auth/me` 返回 401 → 触发 `logout` 清空 AsyncStorage。但此时 Dashboard 等页面已开始加载数据,请求因无 token 返回 401 "未提供认证信息"。
|
||
|
||
### 3. 401 拦截器未跳转登录页
|
||
|
||
原 401 拦截器只清除 AsyncStorage 中的 token,未调用 navigation 跳转到 Login 页面,导致用户停留在 Main 页面看到一堆 401 错误。
|
||
|
||
### 4. 告警页面的 404 实际是 401
|
||
|
||
后端 `/alarms` 路由存在且正常。APP 显示的 "status code 404" 是 axios 默认错误消息格式(`err.message`),实际请求因无 token 返回 401。
|
||
|
||
### 5. 仪表盘"暂无遥测数据"是 token 问题
|
||
|
||
后端 `/telemetry` 路由正常返回数据。但因请求携带过期 token 返回 401,`getTelemetry().catch(() => [])` 静默返回空数组,显示"暂无遥测数据"。
|
||
|
||
**处理**:
|
||
|
||
1. **创建 `.env` 文件**:在 `app/` 目录下创建 `.env`,配置后端地址(手机有 Tailscale 可直接访问):
|
||
```
|
||
API_BASE_URL=http://100.83.103.1:3000/api/v1
|
||
WS_BASE_URL=ws://100.83.103.1:3000/ws
|
||
```
|
||
|
||
2. **清除 Metro 缓存重新打包**:`.env` 在编译时读取,修改后需 `--reset-cache`:
|
||
```powershell
|
||
$metroPid = (Get-NetTCPConnection -LocalPort 8081 -State Listen).OwningProcess
|
||
Stop-Process -Id $metroPid -Force
|
||
cd e:\silk\app; npx react-native start --reset-cache
|
||
```
|
||
|
||
3. **改进 401 拦截器**:token 失效时自动跳转登录页([app/src/api/client.ts](file:///e:/silk/app/src/api/client.ts)):
|
||
```typescript
|
||
if (status === 401) {
|
||
await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]);
|
||
if (navigationRef) {
|
||
navigationRef.navigate('Login');
|
||
}
|
||
}
|
||
```
|
||
|
||
4. **重新登录**:密码 `silk@123`
|
||
|
||
**验证**(Metro 日志):
|
||
```
|
||
POST /auth/login -> 200
|
||
GET /rooms -> 200
|
||
GET /devices -> 200
|
||
GET /alarms -> 200
|
||
GET /telemetry -> 200
|
||
GET /video/clips -> 200
|
||
GET /video/cameras -> 200
|
||
```
|
||
|
||
**注意事项**:
|
||
- `.env` 文件不提交到 git,部署到新环境时需手动创建
|
||
- `adb reverse tcp:3000 tcp:3000` 可作为 localhost fallback 的临时方案,但手机重连后失效
|
||
- 手机有 Tailscale 时可直接访问 `100.83.103.1:3000`,无需 adb reverse
|
||
|
||
---
|
||
|
||
## 三十、APP 图标显示为占位符(react-native-vector-icons 字体未链接)
|
||
|
||
**现象**:APP 底部标签栏(仪表盘、蚕房、设备、告警、视频)的图标、设置页面用户头像旁的列表图标、各页面的按钮图标均显示为占位符(方块或空白),而非预期的 Material Design 图标。
|
||
|
||
**原因**:`react-native-vector-icons` 的字体文件未复制到 Android 的 `assets/fonts/` 目录。
|
||
|
||
APP 使用 `react-native-vector-icons` 的 `MaterialCommunityIcons` 字体渲染图标([app/src/navigation/AppNavigator.tsx](file:///e:/silk/app/src/navigation/AppNavigator.tsx))。字体文件存在于 `node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf`,但 React Native 0.74 的 autolinking 不会自动复制字体到 `android/app/src/main/assets/fonts/`,需要手动配置。
|
||
|
||
字体缺失时,`Icon` 组件无法找到对应的字形(glyph),显示为占位符(.notdef 字符)。
|
||
|
||
**处理**:
|
||
|
||
1. 复制字体文件到 Android assets 目录:
|
||
```powershell
|
||
New-Item -ItemType Directory -Force -Path "e:\silk\app\android\app\src\main\assets\fonts"
|
||
Copy-Item "e:\silk\app\node_modules\react-native-vector-icons\Fonts\MaterialCommunityIcons.ttf" `
|
||
"e:\silk\app\android\app\src\main\assets\fonts\" -Force
|
||
```
|
||
|
||
2. 重新构建 APK(字体是原生资源,需重新打包,不能只 reload JS):
|
||
```powershell
|
||
cd e:\silk\app; npx react-native run-android
|
||
```
|
||
|
||
**注意事项**:
|
||
- React Native 0.74 的 `react-native.config.js` 不支持 `project.android.assets` 字段(会报 `Config Validation Error`),需直接复制字体文件
|
||
- 仅修改 JS/TS 代码只需 Metro reload,但修改原生资源(字体、AndroidManifest、build.gradle 等)必须重新 `run-android`
|
||
- `react-native-paper` 的 `List.Icon`、`IconButton` 也依赖 `MaterialCommunityIcons` 字体
|
||
|
||
---
|
||
|
||
## 三十一、APP 设置页面无法向下滚动
|
||
|
||
**现象**:点击 APP 右上角齿轮图标进入设置页面后,内容超出屏幕底部,但无法向下滑动查看"服务器配置""关于""退出登录"等下方内容。
|
||
|
||
**原因**:[SettingsScreen.tsx](file:///e:/silk/app/src/screens/SettingsScreen.tsx) 使用 `<View>` 作为内容容器,`<View>` 不支持滚动。当内容(用户头像 + 用户信息卡片 + 服务器配置卡片 + 关于卡片 + 退出登录按钮)总高度超过屏幕时,下方内容被裁剪且无法访问。
|
||
|
||
**处理**:将外层 `<View>` 替换为 `<ScrollView>`:
|
||
```typescript
|
||
// 修改前
|
||
<View style={styles.content}>
|
||
{/* 内容 */}
|
||
</View>
|
||
|
||
// 修改后
|
||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.content}>
|
||
{/* 内容 */}
|
||
</ScrollView>
|
||
```
|
||
|
||
对应 styles 新增 `scrollView: { flex: 1 }`,`content` 样式改为 `contentContainerStyle` 使用。
|
||
|
||
**注意事项**:
|
||
- React Native 中 `<View>` 不支持滚动,内容超出屏幕需用 `<ScrollView>` 或 `<FlatList>`
|
||
- `ScrollView` 的 `style` 设置容器样式(如 flex、背景色),`contentContainerStyle` 设置内容样式(如 padding)
|
||
- 仅修改 JS/TS 代码,Metro reload 即可生效,无需重新构建 APK
|
||
|
||
---
|
||
|
||
## 三十二、Web 知识库症状图谱预览打不开且保存不生效
|
||
|
||
**现象**:Web「知识库 → 蚕病百科」编辑病种时上传新的症状图谱:编辑弹窗里的预览图打不开;点击提交提示"保存成功",但重新打开/刷新后图谱仍未绑定(`image_url` 为空)。
|
||
|
||
**原因(两个独立问题,均为前端问题,后端与存储正常)**:
|
||
|
||
1. **预览图打不开:CSP 拦截跨源图片**
|
||
`web/server.cjs` 的 `Content-Security-Policy` 为 `img-src 'self' data: blob:`,只允许同源图片。上传成功后返回的图片 URL 是 `http://100.83.103.1:7480/silk-images/...`(Ceph RGW,端口 7480 与页面 5174 不同源),浏览器直接拦截,图片本身可匿名访问(200 image/jpeg)。
|
||
|
||
2. **保存不生效:`imageUrl` 未注册到表单,提交时被丢弃**
|
||
`web/src/pages/Knowledge.tsx` 上传成功后执行 `form.setFieldValue('imageUrl', res.url)`,但「症状图谱图片」的 `Form.Item` 只有 label、没有 `name="imageUrl"`。antd 的 `form.validateFields()` 只返回已注册(有 `Form.Item name`)的字段,`imageUrl` 不在其中,因此 `PATCH /knowledge/diseases/:id` 的请求体里根本没有 `imageUrl`,后端收到 200 但 `image_url` 列不更新(`updated_at` 会变)。
|
||
|
||
**排查证据(开发服务器 100.83.103.1)**:
|
||
- `web/web.log`:`POST /api/v1/knowledge/images` → `PATCH /api/v1/knowledge/diseases/17b1cae4-21cc-4996-9db3-96f785aef800` → `GET /api/v1/knowledge/diseases`
|
||
- `server-go/server-go.log`(实际生效日志,非 `/home/pan/silk/server-go.log`):15:57:54 `POST images` 200;15:58:03 `PATCH` 200
|
||
- Ceph `silk-images` 桶:存在 `knowledge/20260812/36a2bc56e466fb41.jpeg`(286608 字节,image/jpeg),匿名 GET 200
|
||
- PostgreSQL:白僵病 `image_url` 为 NULL;全表无任何病种有 `image_url`
|
||
- 部署产物校验:服务器 `server.cjs` md5 与仓库一致(CSP 生效);新 bundle 中已含修复代码
|
||
|
||
**处理**:
|
||
|
||
1. 前端修复(`web/src/pages/Knowledge.tsx`、`web/server.cjs`):
|
||
- 「症状图谱图片」后新增隐藏字段,使 `imageUrl` 注册进表单并被 `validateFields()` 返回:
|
||
```tsx
|
||
<Form.Item name="imageUrl" hidden>
|
||
<Input />
|
||
</Form.Item>
|
||
```
|
||
- CSP `img-src` 放行开发环境 S3 源:`img-src 'self' data: blob: http://100.83.103.1:7480`
|
||
2. 构建与部署(本地 `npm run lint`、`npm run build` 均通过):
|
||
- 备份旧产物:`/home/pan/backups/web-20260812-1605/`(dist、server.cjs、web.log)
|
||
- 上传新 `dist` 与 `server.cjs`,重启 node(旧 pid 3764542 → 新 pid 3803472)
|
||
- 验证:`http://localhost:5174/` 200;CSP 头含 `http://100.83.103.1:7480`;新 bundle `index-CSVEb7gG.js` 200;`/api/v1/health` 200
|
||
3. 数据补绑(用户确认直接改库):
|
||
```sql
|
||
UPDATE diseases
|
||
SET image_url = 'http://100.83.103.1:7480/silk-images/knowledge/20260812/36a2bc56e466fb41.jpeg',
|
||
updated_at = now()
|
||
WHERE id = '17b1cae4-21cc-4996-9db3-96f785aef800';
|
||
```
|
||
结果:`UPDATE 1`,查询确认 `image_url` 已写入;图片匿名 GET 200。
|
||
|
||
**验证结果**:DB 行已绑定图谱 URL;CSP 已放行;部署产物包含隐藏字段注册;lint/build 通过(详见上文)。
|
||
|
||
**回滚点**:
|
||
- 前端回滚:恢复 `/home/pan/backups/web-20260812-1605/` 下的旧 `dist`/`server.cjs` 并重启 node
|
||
- 数据回滚:`UPDATE diseases SET image_url = NULL, updated_at = now() WHERE id = '17b1cae4-21cc-4996-9db3-96f785aef800';` 或 `pg_restore` 恢复 `/home/pan/backups/diseases-20260812-1605.dump`
|
||
|
||
**注意事项**:
|
||
- `/home/pan/silk` 不是 git 仓库,无法打 tag 回滚,只能依赖文件备份(本次已备份)
|
||
- 部署指南中的 `admin/admin123` 已失效;已用当前 admin 账号完成 API 级冒烟(登录成功 → `GET /api/v1/knowledge/diseases` 返回白僵病 `imageUrl` 已绑定),密码本身不写入文档
|
||
- 已知边界:编辑页若点「移除」图片再保存,`imageUrl` 会以 `undefined` 提交(键被省略),不会清空库里已有图片;如需「移除即清空」后续应显式传 `null`
|
||
- CSP 当前放行的是开发服务器 IP;生产切云 OSS/COS 时需把 OSS/CDN 域名加入 `img-src`,或改为同源代理
|
||
|
||
---
|
||
|
||
## 2026-08-12/13 部署与开发环境踩坑合集(重要)
|
||
|
||
### 1. NetBird 登录过期与隧道闪断
|
||
|
||
**现象**:SSH 突然超时;`netbird status` 显示 `peer login has expired, please log in once more`。
|
||
**处理**:`netbird login --management-url https://115.191.19.95:6680` 重新 SSO(需用户在浏览器完成,链接有时效),再 `netbird up`;数据面恢复需要几十秒,可用 ping/端口探测确认后再继续。
|
||
|
||
### 2. 服务器 pip 慢 / 未配置国内源
|
||
|
||
**现象**:服务器 pip 安装依赖极慢(官方 PyPI)。
|
||
**处理**:已持久化配置清华源 `pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple`(写入 `/home/pan/.config/pip/pip.conf`,对 pan 全部 venv 生效)。另需 `python3.10-venv` 才能建 venv。
|
||
|
||
### 3. web 构建报 `WebAssembly.Memory.grow(): Maximum memory size exceeded`
|
||
|
||
**现象**:`npm run build` 在 rolldown 压缩阶段失败(超大单 chunk)。
|
||
**处理**:`vite.config.ts` 增加 `build.rollupOptions.output.manualChunks`(函数形式,按 react/antd/pro/echarts/vendor 拆分),构建通过。
|
||
|
||
### 4. 服务器上覆盖运行中的二进制报 `text file busy`
|
||
|
||
**现象**:`cp` 新二进制到正在运行的 `server-go-linux` 报 ETXTBSY。
|
||
**处理**:先 `mv` 旧文件(如 `server-go-linux.old-<时间戳>`)再 `cp` 新文件,最后 `fuser -k 3000/tcp` + `start.sh` 重启。
|
||
|
||
### 5. Gin 路由注册相关三个坑(2026-08-13 #24 部署)
|
||
|
||
- `/rooms/:id/health-profile` 与已有 `/rooms/:id` 冲突 → 启动 panic `wildcard segment conflicts`;改为独立路径 `/health-profiles/:roomId`。
|
||
- 同一路由注册函数被调两次 → panic `handlers are already registered for path ...`;检查 main.go 是否重复注册。
|
||
- 用 apply_patch 新增文件时若文件已存在会覆盖旧内容:新增 `handler/health.go` 覆盖了原 `/health` 路由(health 变 404);已恢复原函数并把健康画像独立为 `RegisterHealthProfileRoutes`。
|
||
- 路由参数名与 handler 读取名不一致(`:roomId` 却 `c.Param("id")`)→ 返回 404/参数为空;保持命名一致。
|
||
|
||
### 6. PowerShell 传中文 JSON 导致请求 400
|
||
|
||
**现象**:`curl -d '{"disease":"白僵病"}'`(PowerShell 控制台按 GBK 编码)→ 后端 JSON 解析失败返回 400。
|
||
**处理**:冒烟测试避免在 `-d` 中直接写中文;改用 ASCII 字段值,或通过文件/脚本传 UTF-8 字节。
|
||
|
||
### 7. tar 打包报「时间戳是未来」等警告
|
||
|
||
**现象**:Windows tar 打包 dist 时提示时间戳在未来(非致命)。
|
||
**处理**:忽略即可;不影响部署。
|
||
|
||
### 8. SSH 长命令整链被 `&` 后台化
|
||
|
||
**现象**:一条部署命令里 `A && B & C` 语法把整条链后台化,导致命令“卡住”超时。
|
||
**处理**:后台启动(node/uvicorn)单独一条命令,先 `fuser -k` + 换 dist/二进制,再单独 `nohup ... &` 启动。
|