README.md 12.2 KB

H3 Console

给 minimax-h3/ 的四后端 API(gateway.py + 4× api.py)加一层 Web 管理台: 左侧任务记录列表 + 右侧新建/详情,上传首尾帧/参考图 → 提交生成 → 点开任务看状态和结果。 不重做调度,只加 gateway.py 没有的东西(Web UI、文件上传、任务持久化、按需 fl2va/ref2va 弹性分卡)。

需求/架构决策见 <code>DESIGN.md</code>——先读那份,尤其「四点五、弹性 partition 调度器」。

页面结构

左侧栏固定:顶部一条新建任务(选中态=当前在填新表单),下面是任务记录列表,按提交时间倒序, 点哪条就直接跳到那条的详情。右侧是新建表单(三个 tab:文生/首尾帧/参考图)或任务详情二选一。

URL 是真实可分享的路径,不是纯前端状态:

  • /newtask.html(或裸 /)— 新建任务页
  • /task.html/<任务id> — 某个任务的详情页,刷新、直接打开、发链接给别人都能定位到同一条

这三条路径背后是同一个 static/index.html,server.py 里 /task.html/{tid} 和 /newtask.html 两条路由原样返回这份文件,靠前端 JS 读 location.pathname 决定渲染哪个视图——不是三个页面文件, 省得同一份侧边栏/样式在多个 html 里重复维护。提交完新任务会自动跳到 /task.html/<新id>。

状态(2026-08-20)

已部署到服务器,端口 8080,跑在 /data/models/minimax-h3/console/,弹性调度器全链路验证通过。

本地冒烟测试:上传图片 → 拒绝非图片文件 → 提交生成任务 → 轮询到 done → 下载代理拿到合法 mp4 → sqlite 影子记录核对无误 → 浏览器打开首页、三个 tab 切换、任务列表内联播放,均正常。

服务器上用真实 GPU 做的弹性调度全链路验证(这部分本地测不了,见 DESIGN.md「四点五」): 上传首帧图 → 提交 fl2va 任务(当时四卡全 ref2va)→ console 正确返回 queued(不是 503)→ supervisor 30 秒后选中一个空闲 ref2va 后端 flip 成 fl2va → reconciler 在新后端 ready 后自动把 排队的任务提交上去 → 任务 running → done → 通过 console 下载代理拿到合法 mp4。全程无人工干预。

部署过程中揪出的两个真实 bug(修完才让上面这条链路跑通)

  1. _flip_backend 里的 PID 提取从来没匹配上过:ss -lptn 的输出里 pid 不是独立的空格分隔 token,是嵌在 users:(("python",pid=NNNN,fd=14)) 里面的——tok.startswith("pid=") 永远不命中, kill 从来没真的执行。后果是"flip"只是在已被占用的端口上起了一个新 uvicorn 进程,因端口冲突 立刻退出,旧进程全程没受影响——不是数据损坏,但功能是假的。改用 re.findall(r"pid=(\d+)", ...)。
  2. 弹性调度器不知道"已经在切换中",会对同一个需求连续 flip 好几张卡:api.py 冷启动要 13-20 秒,跨了不止一个 10 秒的 tick。中间几轮 tick 看到目标分区的 ready 数还是 0, 误判成需求没满足,接着把第二张、第三张卡也切了过去——一个 pending=1 的请求切了三张卡。 加了 flipping: dict[port, (目标分区, 发起时间)] 记账,效果数(ready + 正在切换中的) 满足需求就不再触发新的 flip,60 秒还没起来算超时放弃。
  3. pkill -f "<pattern>" 杀掉了自己所在的 SSH 会话:这条不是 console 代码的 bug,是我操作 服务器时踩的坑——start-all.sh 注释里其实早就警告过这个("pgrep -f 会匹配到自己那条命令行"), 这次是真的踩了一遍:pkill -f "uvicorn server:app.*8080" 这条命令的完整命令行本身就包含 被搜索的那串文本,pkill -f 把自己所在的 shell 一起杀了,SSH 连接直接断,命令链后半截全没执行。 之后全程改用 ss -lptn "sport = :PORT" | grep -o "pid=..." 按端口精确定位再 kill,不再用 pkill -f。

播放/下载查出来的三个坑(用户反馈"播放和下载好像有问题"后排查,均已修)

  1. download/poll 字段抄了 gateway 内部 id:GET /v1/videos/{id} 原样带回 api.py/gateway.py 响应里的这两个字段,值是 gateway 内部那个 id,不是 console 自己对外用的 id——直接用这两个字段 拼链接会 404。页面本身没受影响(前端一直是自己用 j.id 拼的 URL,没读这两个字段),但接口本身 是错的。_row_view() 里统一改成用 row['id'](console 自己的 id)重新拼。
  2. 详情页每 5 秒轮询会把播放到一半的 <video> 冲掉:refreshDetail() 是整段 innerHTML 重建, 任务到了 done/failed 这种终态之后还继续每 5 秒重建一次,会把正在播放的 <video> 元素连根 拔掉重建,播放进度弹回 0。改成只有任务还在 queued/running 时才继续拉详情,终态之后不再重建。
  3. 下载接口不支持 HTTP Range,<video> 播放前的探测请求拿不到有效响应:之前经 gateway.py 转发(StreamingResponse(urlopen(...))),Range 请求一律回 200 而不是 206,也没有 Content-Length/Accept-Ranges——<video> 标签播放前会先探一次 Range 拿时长,探不到就卡在 readyState=0 播不出来(用 curl/页面里直接 fetch() 都测不出来,因为它们不做这个探测)。 改成绕过 gateway 这层转发,直接连去真正跑这条任务的后端(api.py 自己是 Starlette FileResponse,原生支持 Range),把状态码/头原样带回来。

顺带加了个磁盘兜底:如果目标后端进程已经重启、忘了这条任务(api.py 当时 JOBS 还没 落盘,2026-08-20 之后这个根因已经在 api.py 修了,见 minimax-h3/API.md「任务记录会落盘」), console 会用自己 sqlite 里存的 file 路径直接读磁盘发送——两台机器是同一台,文件一直都在, 只是那个进程的内存不认得这个 id 了。根因修了之后这层兜底退化成第二道保险,不是唯一防线。

怎么起(本地开发)

cd h3-console
./run.sh    # 默认 H3_GATEWAY_URL=http://192.168.110.67:8000,端口 8090

浏览器打开 http://localhost:8090。三个 tab(文生视频/首尾帧/参考图)+ 任务列表都在首页。

⚠️ 本地开发时上传功能不完整:H3_CONSOLE_PUBLIC_BASE 没配的话 /v1/assets 会直接报错—— 因为上传图片的 URL 最终要被服务器上的 api.py worker 反向拉取,本地 Mac 猜不出自己对服务器 可达的地址。要在本地测完整的首尾帧/参考图流程,配成 Mac 在局域网里的地址:

H3_CONSOLE_PUBLIC_BASE=http://<你的 Mac 局域网 IP>:8090 ./run.sh

前提是服务器能反向连回你的 Mac(同一局域网通常没问题;接了会劫持路由的 VPN 就不行, HANDOFF.md 里记过一次这个坑:nc -z 会给假阳性,出问题用 ping+route -n get 确认路径)。 不配这个变量时,文生视频(不需要上传)那个 tab 可以正常测。

部署到服务器(已完成,2026-08-20)

rsync -av --exclude='.venv' --exclude='__pycache__' --exclude='console.sqlite3' --exclude='assets/*' \
  h3-console/ admin@192.168.110.67:/data/models/minimax-h3/console/
ssh admin@192.168.110.67
cd /data/models/minimax-h3/console
H3_GATEWAY_URL=http://127.0.0.1:8000 \
H3_CONSOLE_PUBLIC_BASE=http://192.168.110.67:8080 \
H3_ENABLE_SUPERVISOR=1 \
PORT=8080 \
setsid nohup ./run.sh > console.log 2>&1 < /dev/null &

外加第二个实例专供公网子路径(同一个目录,端口和环境变量不同,其它不变):

cd /data/models/minimax-h3/console
H3_GATEWAY_URL=http://127.0.0.1:8000 \
H3_CONSOLE_PUBLIC_BASE=http://192.168.110.67:8080 \
H3_CONSOLE_BASE_PATH=/console \
PORT=8081 \
setsid nohup ./run.sh > console-subpath.log 2>&1 < /dev/null &

对外访问原计划用独立域名 gateway.wudaima.com(配置已存在,见 DESIGN.md 问题2)—— 2026-08-20 实测坐实这条线路走不通,缺口在 frp 服务端 60.205.214.169 那边(本机这边 frpc-qwen-client 配置是对的、早就 start proxy success 注册上了,问题不在这台机器,详见 顶层 CHANGELOG.md「排查清楚:gateway.wudaima.com 公网转发为什么一直不通」)——不是这台 机器能改的,需要能登录 60.205.214.169 的人补上,暂不等它。

改走 qwen.wudaima.com/console 子路径(同一天改的,详见 CHANGELOG「h3-console 公网访问 改走 qwen.wudaima.com/console 子路径」),已验证可用:

  • 局域网直连:http://192.168.110.67:8080(端口 8080 的实例,H3_CONSOLE_BASE_PATH 留空, 开着弹性调度器)
  • 公网:http://qwen.wudaima.com/console(另起的端口 8081 实例,H3_CONSOLE_BASE_PATH=/console, 不开弹性调度器——两个进程的调度器同时抢着 flip 分区会重现"一个请求切三张卡"那个老 bug, 这件事只能一个进程做主)

两个进程共享同一个工作目录(同一份 console.sqlite3、assets/),局域网和公网看到的任务列表、 上传的图片是同一份数据,不会分叉。server.py 支持 H3_CONSOLE_BASE_PATH 这个环境变量后, static/index.html 里所有原来硬编码的绝对路径都改成了拼 H3_BASE 前缀(页面里的占位符 __H3_BASE_PATH__ 由 server.py 渲染时替换),留空就是原来的根路径行为,两种模式都有本地 uvicorn 验证过。frp 那边在 frpc.qwen.ini 里新增了一段 locations = /console 的代理, 跟 qwen-llm(没有 locations,相当于兜底捕获其它所有路径)共用同一个 custom_domains, 两边不冲突。

重启/重新部署时不要用 pkill -f(见上面「揪出的坑」第3条,会误杀自己的 SSH 会话),改用:

PID=$(ss -lptn "sport = :8080" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u)
[ -n "$PID" ] && kill $PID

开机自启(2026-08-25 补):两个实例现在都由 systemd 管理——/etc/systemd/system/ h3-console-lan.service(8080)、h3-console-public.service(8081),本地副本见 systemd/ 目录,已 enable。ExecStart 直接指向 run.sh(脚本末尾 exec uvicorn, 进程替换后 systemd 能正确跟踪),日志仍 append 到原来的 console-8080.log/ console-subpath.log(不改用 journald),加了 Restart=on-failure。改配置后用 systemctl restart h3-console-lan h3-console-public 生效,不用再手动 kill 端口。

部署后验过:curl http://192.168.110.67:8080/v1/backends 看到 4/4 ready;提交一个 fl2va 首尾帧任务,console.log 里能看到 [supervisor] flip port=... ref2va -> fl2va,30 秒后(空闲 判定)触发一次、只触发一次,新后端 ready 后任务自动提交并跑完,下载代理拿到合法 mp4。

回滚

console 本身是独立进程,出问题直接 kill console 进程即可,不影响四个 H3 后端和 gateway。

⚠️ 2026-08-20 起这条不再完全成立:api.py 加了任务记录落盘(见 minimax-h3/API.md 「任务记录会落盘」),这是碰了 api.py 本身的改动,不属于 console 这一层。要回滚这部分, 需要把服务器上的 /data/models/minimax-h3/api.py 换回旧版本,再按「只重启空闲的那张卡」的 纪律逐张滚动重启四个后端(参考本文档「部署过程中揪出的两个真实 bug」同样的操作方式)—— 不能像 console 自己的改动那样一个 kill 就完事。

已知限制

  • 鉴权:暂未加(DESIGN.md 问题3,跑通了再加)。部署到公网前必须补,否则和现有服务一样是 无鉴权可直调,风险更高——console 多了写文件(上传)的能力。
  • 任务持久化是 console 自己的 sqlite,和 api.py 内存态之间有轮询间隔的短暂不一致(几秒级), 不追求强一致。
  • 磁盘:/ 只剩 29G,/data 83% 满,上传接口目前只做单文件 20MB 上限校验,没做磁盘余量检查, 真到磁盘紧张时会和其他生成任务一样直接写失败,没有额外保护——按 DESIGN.md「开放风险」记录的, 真出问题再加。