- server.cjs: img-src 放行 http://100.83.103.1:7480,修复编辑页预览图被 CSP 拦截 - Knowledge.tsx: 新增隐藏 Form.Item name=imageUrl,修复 validateFields 提交时丢弃图片 URL - 部署至 100.83.103.1:5174(备份 /home/pan/backups/web-20260812-1605) - 补绑白僵病 image_url(UPDATE 1),更新故障排查/开发交接/变更记录
55 KiB
故障排查处理记录
一、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:
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: "通道不存在"。
原因:
- WVP API 路径错误:用了
/api/device/query/{deviceId}/channels,正确路径是/api/device/query/devices/{deviceId}/channels(多了devices)。 - 通道 ID 不匹配:EasyGBD 的通道 ID 不是设备 ID
34020000002000000001,而是34020000001310000001(类型编码 131 = 模拟通道)。
处理:
- 用正确路径调用
GET /api/device/query/devices/{deviceId}/sync同步通道。 - 从通道列表中获取实际通道 ID
34020000001310000001。 - 在前端「摄像头管理」中更新通道 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 端口:
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 调用了未定义的函数。
处理:
// 修改前
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/:
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。
处理:
- 修正数据库中已有记录的时间(减 8 小时)。
- 后续自动录制归档由服务器端 Python 脚本处理,脚本从文件名解析时间并带
+08:00时区,NestJS 内部 API 接收 ISO 8601 带时区的时间字符串。
八、自动录制归档 stopRecord 失败
现象:RecordingService 每 5 分钟触发归档时,stopRecord 返回 can not find the stream,归档流程中断。
原因:
- WVP 播放会话在 5 分钟后超时,ZLMediaKit 中的流自动关闭,
stopRecord找不到流。 stopRecord抛出异常后,整个archiveAndRestart方法中断,后续归档和重新录制不执行。
处理:
- 在
archiveAndRestart和stopRecording中将stopRecord包在 try-catch 中,失败只记警告,不中断归档流程。 - 在
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:
# 修改前
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 守护进程未运行。
处理:
- 手动启动 cron:
sudo cron - 配置 WSL2 启用 systemd,写入
/etc/wsl.conf:[boot] systemd=true - 在 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,不添加任何额外参数:
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:
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:
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 字节。
处理:
- 删除损坏的 AOF 文件:
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
- 重启 WVP 和 ZLMediaKit 容器,等待摄像头重新注册。
- 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:
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. 构建生产版本
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:
// 核心逻辑:
// 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. 启动命令
# 构建前端
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 存在两个问题:
- 服务未启动 -
systemctl status vernemq显示inactive,1883 端口未监听。 - 监听地址绑定了 127.0.0.1 - 配置文件
/etc/vernemq/vernemq.conf第291行为listener.tcp.name = 127.0.0.1:1883,只允许本机访问,外部连接被拒绝。
处理:
- 启动 VerneMQ 服务:
sudo systemctl start vernemq - 修改监听地址为
0.0.0.0:1883:sudo sed -i 's/^listener.tcp.name = 127.0.0.1:1883/listener.tcp.name = 0.0.0.0:1883/' /etc/vernemq/vernemq.conf - 重启服务:
sudo systemctl restart vernemq - 确认端口监听:
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 用户。
处理:
- 创建明文密码文件并加密:
echo "pan:pan" > /etc/vernemq/vmq.passwd vmq-passwd -U /etc/vernemq/vmq.passwd # 将明文转为哈希 - 设置文件权限:
chown vernemq:vernemq /etc/vernemq/vmq.passwd chmod 640 /etc/vernemq/vmq.passwd - 重启 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 跳过了登录流程。
处理:
- 在
android/app/src/main/AndroidManifest.xml的<application>标签中添加android:usesCleartextTraffic="true":
<application
android:name=".MainApplication"
android:usesCleartextTraffic="true"
...>
- 重新编译 Debug APK 并安装:
$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
- USB 连接时需设置 ADB 端口转发让手机访问 Metro:
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 才能加载新代码。
处理:
- 在
src/api/client.ts中新增resolveUrl函数,将相对路径转为完整 URL:
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;
};
- 在
src/screens/VideoScreen.tsx中导入并使用resolveUrl:
// 修改前
streamUrl: clip.playbackUrl,
// 修改后
streamUrl: resolveUrl(clip.playbackUrl),
- 修改代码后若普通 reload 不生效,需重启 Metro 并清除缓存:
# 先杀掉占用 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可强制清除 - 实时视频播放正常是因为
playCameraAPI 返回的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 配置:
// 修改前
{ 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 次):
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 错误
二十、设备注册成功但前端显示离线 + 录制状态不清理
现象:
- EasyGBD 日志显示注册成功,但 Web 前端摄像头管理页显示设备离线,无法查看实时视频。
- 在前端点击"开始录制",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,维护自己的recordingsmap。
当 GB28181 设备离线导致流 EOF 时,recorder-go 检测到流结束,会:
- 创建片段记录并调用
POST /api/v1/video/clips/internal通知 Go 后端 ✓ - 清理自己的
recordingsmap ✓ - 但未通知 Go 后端清理
activeRecordings✗
导致 Go 后端的 activeRecordings 一直保留过期记录,前端持续显示"录制中"。用户手动停止后,stopRecording 调用 media.StopPlay() 发送 BYE,但设备已离线无响应。再次点击"开始录制"时,StartPlay 向 WVP 发送 INVITE 失败(设备离线),返回 503。
处理:
修复:recorder-go 流结束时通知 Go 后端清理录制状态
-
Go 后端
video_record.go:新增内部接口POST /video/recordings/internal/end- 接收 recorder-go 的录制结束通知(校验
x-api-key) - 清理
activeRecordings内存表 - 调用
media.StopPlay()清理 WVP play session
- 接收 recorder-go 的录制结束通知(校验
-
Go 后端
auth.go:白名单添加/api/v1/video/recordings/internal/end -
recorder-go
main.go:在录制线程record()的 defer 中调用notifyRecordingEnd()- 无论流 EOF、错误、还是手动停止,都会通知 Go 后端
- Go 后端收到通知后清理
activeRecordings并停止 WVP session
涉及文件:
server-go/internal/handler/video_record.go-- 新增endRecordingInternalhandler 和路由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 不会转发到服务器。
处理:
-
确认服务器公网 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连接超时。 -
路由器端口转发:在路由器管理页面添加 TCP 端口转发规则,外部端口 1883 -> 内部 IP 192.168.1.58:1883。
-
断路器配网:长按按键 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
- 服务器:
-
保存后点击"设备重启"使配置生效。
注意事项:
- 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 不一致)。
处理:
-
修正数据库:将
device_key从8G2323080741518更新为862323080741518。 -
后端 IMEI 规范化:在
mqtt.go的handleMessage中,对imei字段做规范化处理,将G替换为6: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 部分设备的"发布主题"和"订阅主题"在配网页面的含义与后端的预期方向相反。设备把配网页面中填写的"订阅主题"作为实际发布方向使用。
处理:后端已适配此情况:
- 同时订阅
silk/+/+/+/up/telemetry和silk/+/+/+/down/cmd两个通配符主题 PublishToDevice方法自动检测设备上报主题方向:- 设备上报
/up/telemetry-> 命令发送到/down/cmd(正常方向) - 设备上报
/down/cmd-> 命令发送到/up/telemetry(反配方向)
- 设备上报
无需修改设备配网配置,后端自动兼容两种方向。
二十四、历史回看视频播放失败(S3 预签名 URL SignatureDoesNotMatch)
现象:前端「历史回看」点击播放按钮,提示"视频播放失败"。
症状链路(与第十二节"历史回看视频播放失败"完全一致):
- 前端
Video.tsx调用getClipPlayUrl(clipId)获取/api/v1/video/clips/:id/stream同源 URL VideoPlayer.tsx走video.src = url原生 MP4 分支- 浏览器发起 GET 请求,触发后端
streamCliphandler - 后端调用
transcode.StreamClip启动 ffmpeg 拉取 S3 预签名 URL 转封装为 fragmented MP4 - ffmpeg 报
HTTP error 403 Forbidden,后端返回 502 JSON{"error":"ffmpeg 异常退出: exit status 1"} - 浏览器
<video>收到非视频内容触发 error 事件 → "视频播放失败"
根因诊断过程:
- 直接 curl 预签名 URL,Ceph RGW 返回 XML
<Code>SignatureDoesNotMatch</Code>—— 排除了 access key/secret key 错误、CORS、网络等可能。 - 用 boto3 在 Windows 本地生成同样格式的预签名 URL(参数完全一致),curl 能返回 200 OK,ContentLength 47MB —— 证明 access/secret key 正确、bucket 可读、Ceph RGW 工作正常,问题在 Go 后端手动实现的 V4 签名计算结果错误。
- 检查 Go 后端进程实际环境变量:
/proc/140619/environ中只有S3_ACCESS_KEY、S3_BUCKET_ARCHIVE、S3_BUCKET_EVENTS、S3_ENDPOINT,没有S3_SECRET_KEY! - 检查
config.go,S3SecretKey的envDefault:""—— 默认值为空字符串。 - 检查
start.sh,没有export S3_SECRET_KEY=...。 - 结论: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 |
结论:本次症状与第十二节完全相同,但根因不同。属于配置疏忽导致的"重蹈症状之覆辙",但不是"重蹈根因之覆辙"。这提示:部署脚本和配置默认值都需要将敏感凭据完整列出,不能依赖"反正某个上游会传入"。
处理:
server-go/internal/config/config.go:将S3SecretKey默认值从""改为"Silk-App-Secret-2026!",与项目部署技能中的 Ceph S3 默认凭据保持一致。server-go/start-shipped.sh:新增export S3_SECRET_KEY='Silk-App-Secret-2026!',使部署脚本自包含所有必需环境变量。- 重新交叉编译后端,上传到
/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,视频无法显示。强制刷新浏览器无效。
原因(多层叠加):
- ZLMediaKit 短时间关流:
general.streamNoneReaderDelayMS=20000(20 秒无人观看即关流)+ WVPstream-on-demand=true(按需拉流,hook 返回"关闭=true"),导致流在 22 秒后被关闭 - 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 未正确应用该值 - CSP 阻止跨域 fetch:Go 后端
fixZlmPort函数返回绝对路径http://100.83.103.1:8081/rtp/...,但web/server.cjs的 CSP 头connect-src 'self' ws: wss:只允许同源请求,flv.js 跨域 fetch ZLMediaKit 时被浏览器阻止
处理:
- 调用 ZLMediaKit
setServerConfigAPI 修正运行时配置: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实现,留存服务器供后续修复使用
- 修改 WVP 配置
stream-on-demand: true → false,避免 hook 返回"关闭=true" - 修改
server-go/internal/service/media.go的fixZlmPort函数,提取/rtp/起始的相对路径返回(含 query string),让前端通过server.cjs内置的/rtp/代理转发到127.0.0.1:8081,避免跨域 - 重启 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.cjs5174 端口代理访问 FLV 流,能正常返回视频二进制数据 - 浏览器中 FLV 播放正常
遗留注意事项:WVP 重启可能会再次把 ZLM hook URL 覆盖回 127.0.0.1:18080。若 FLV 播放再次失败,登录服务器执行 python3 /home/pan/silk/fix_zlm_hook.py 即可修复。
二十六、历史回看查不到录像(前端时间范围硬编码 + RangePicker 清空不生效)
现象:/videos 页面"历史回看"Tab 查询历史录像,时间段留空也查不到任何记录。
原因:
- 数据库确实今天无新录像:数据库 17 条录像全部是 7月17日(昨天),今天 0 条新录像(无活跃录制任务)
- 前端时间范围硬编码:
web/src/pages/Video.tsxPlaybackTab 的handleSearch函数硬编码to: dayjs().toISOString(),即使清空 RangePicker 仍查"今天 00:00 到 现在" - RangePicker onChange 清空不更新 state:清空时
vals为 null,但 onChange 中没有处理 null 分支,dateRangestate 仍保持默认值[今天开始, 现在]
处理:
- 修改
dateRangestate 类型为[Dayjs, Dayjs] | null - 修改 RangePicker
onChange:清空时设为null表示查全部 - 修改
handleSearch:仅当dateRange非空时传 from/to,为 null 时查全部 - 修改自动刷新定时器同样支持 dateRange 为 null
验证:清空时间范围查询,能查到 7月17日的 17 条录像。
用户认知纠正:实时视频流(ZLMediaKit 接收 RTP 推流并分发 FLV/HLS)≠ 录像(recorder-go 从 ZLMediaKit 拉 FLV 流分段存到 Ceph S3)。实时视频能播放不代表在录像,必须通过"开始录制"按钮触发录制。
二十七、摄像头管理显示"开始录制"而非"正在录制"(录制状态查询改进)
现象:用户认为摄像头正在录像,但 /videos "摄像头管理"Tab 显示"开始录制"按钮(应为"正在录制"状态)。
原因:
- 实际未在录制:recorder-go
/record/status接口返回{"recordings":[]},确认无活跃录制任务 - 用户误解:用户把"实时视频流能播放"误判为"正在录像"
- 潜在的内存丢失问题:
server-go/internal/handler/video_record.go中activeRecordings是 Go 后端进程内存 map,Go 后端重启会清空。虽然当前未在录制,但如果录制中 Go 后端重启,会导致前端显示"开始录制"而 recorder-go 仍在实际录制
处理:
- 修改
listActiveRecordings函数:优先调用 recorder-go/record/status接口获取真实录制状态,并自愈内存 map(从 recorder-go 恢复 activeRecordings)。recorder-go 不可用时回退到内存 map - 修改路由注册:
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
现象:
- Web 前端、APP 上摄像头列表显示"离线",但摄像头实际已注册成功(WVP 设备列表
onLine=true) - 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 端口。
listCamerashandler 调用media.SyncDeviceStatus()查询 WVP/api/device/query/devices获取在线状态- WVP API 不可达 →
SyncDeviceStatus()返回 error → 摄像头is_online字段保持数据库中的旧值(false) - 前端读取
c.online字段显示"离线"
问题二:APP 直播流 502
video_stream.go 的 streamLive 函数在 StartPlay 成功后,直接使用 result.FLV 作为 http.Get() 的参数。但 WVP 返回的 FLV 地址是相对路径(如 /rtp/34020000002000000001_34020000001310000001.live.flv?...),Go 的 http.Get() 无法处理无协议头的 URL,返回 502 错误。
// 修复前(有 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 函数:
- 将函数参数从
transcode *service.TranscodeService改为cfg *config.Config - 在
sourceURL使用前,检测如果是相对路径则拼接cfg.ZLMAPIBase:
// 确保地址是完整的 ZLM 地址(WVP 返回的可能是相对路径 /rtp/...)
if strings.HasPrefix(sourceURL, "/") {
sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + sourceURL
}
- StartPlay 失败的降级分支也改用
cfg.ZLMAPIBase拼接(不再硬编码 IP):
sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + "/rtp/" + *camera.GbDeviceID + "_" + *camera.GbChannelID + ".live.flv?originTypeStr=rtp_push&videoCodec=H264"
- 同步更新路由注册:
streamLive(db, media, transcode)→streamLive(db, media, cfg)
验证:
/api/v1/video/cameras返回online=true, isOnline=true✓- WVP
/api/device/query/devices返回onLine=true✓ /api/v1/video/cameras/1/live/stream返回 HTTP 200,Content-Type:video/x-flv,12秒下载 2MB 有效 FLV 数据 ✓- APP 实时视频播放成功 ✓
- 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),但项目根目录下只有 .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(() => []) 静默返回空数组,显示"暂无遥测数据"。
处理:
-
创建
.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 -
清除 Metro 缓存重新打包:
.env在编译时读取,修改后需--reset-cache:$metroPid = (Get-NetTCPConnection -LocalPort 8081 -State Listen).OwningProcess Stop-Process -Id $metroPid -Force cd e:\silk\app; npx react-native start --reset-cache -
改进 401 拦截器:token 失效时自动跳转登录页(app/src/api/client.ts):
if (status === 401) { await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]); if (navigationRef) { navigationRef.navigate('Login'); } } -
重新登录:密码
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)。字体文件存在于 node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf,但 React Native 0.74 的 autolinking 不会自动复制字体到 android/app/src/main/assets/fonts/,需要手动配置。
字体缺失时,Icon 组件无法找到对应的字形(glyph),显示为占位符(.notdef 字符)。
处理:
-
复制字体文件到 Android assets 目录:
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 -
重新构建 APK(字体是原生资源,需重新打包,不能只 reload JS):
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 使用 <View> 作为内容容器,<View> 不支持滚动。当内容(用户头像 + 用户信息卡片 + 服务器配置卡片 + 关于卡片 + 退出登录按钮)总高度超过屏幕时,下方内容被裁剪且无法访问。
处理:将外层 <View> 替换为 <ScrollView>:
// 修改前
<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 为空)。
原因(两个独立问题,均为前端问题,后端与存储正常):
-
预览图打不开: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)。 -
保存不生效:
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/diseasesserver-go/server-go.log(实际生效日志,非/home/pan/silk/server-go.log):15:57:54POST images200;15:58:03PATCH200- Ceph
silk-images桶:存在knowledge/20260812/36a2bc56e466fb41.jpeg(286608 字节,image/jpeg),匿名 GET 200 - PostgreSQL:白僵病
image_url为 NULL;全表无任何病种有image_url - 部署产物校验:服务器
server.cjsmd5 与仓库一致(CSP 生效);新 bundle 中已含修复代码
处理:
- 前端修复(
web/src/pages/Knowledge.tsx、web/server.cjs):- 「症状图谱图片」后新增隐藏字段,使
imageUrl注册进表单并被validateFields()返回:<Form.Item name="imageUrl" hidden> <Input /> </Form.Item> - CSP
img-src放行开发环境 S3 源:img-src 'self' data: blob: http://100.83.103.1:7480
- 「症状图谱图片」后新增隐藏字段,使
- 构建与部署(本地
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;新 bundleindex-CSVEb7gG.js200;/api/v1/health200
- 备份旧产物:
- 数据补绑(用户确认直接改库):
结果:
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已无法登录(401 用户名或密码错误),本次 API 冒烟改用 DB 直接验证;如需 API 级冒烟请先确认当前 admin 密码 - 已知边界:编辑页若点「移除」图片再保存,
imageUrl会以undefined提交(键被省略),不会清空库里已有图片;如需「移除即清空」后续应显式传null - CSP 当前放行的是开发服务器 IP;生产切云 OSS/COS 时需把 OSS/CDN 域名加入
img-src,或改为同源代理