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(修完才让上面这条链路跑通)
_flip_backend里的 PID 提取从来没匹配上过:ss -lptn的输出里 pid 不是独立的空格分隔 token,是嵌在users:(("python",pid=NNNN,fd=14))里面的——tok.startswith("pid=")永远不命中,kill从来没真的执行。后果是"flip"只是在已被占用的端口上起了一个新 uvicorn 进程,因端口冲突 立刻退出,旧进程全程没受影响——不是数据损坏,但功能是假的。改用re.findall(r"pid=(\d+)", ...)。- 弹性调度器不知道"已经在切换中",会对同一个需求连续 flip 好几张卡:api.py 冷启动要
13-20 秒,跨了不止一个 10 秒的 tick。中间几轮 tick 看到目标分区的 ready 数还是 0,
误判成需求没满足,接着把第二张、第三张卡也切了过去——一个
pending=1的请求切了三张卡。 加了flipping: dict[port, (目标分区, 发起时间)]记账,效果数(ready + 正在切换中的) 满足需求就不再触发新的 flip,60 秒还没起来算超时放弃。 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。
播放/下载查出来的三个坑(用户反馈"播放和下载好像有问题"后排查,均已修)
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)重新拼。- 详情页每 5 秒轮询会把播放到一半的
<video>冲掉:refreshDetail()是整段innerHTML重建, 任务到了done/failed这种终态之后还继续每 5 秒重建一次,会把正在播放的<video>元素连根 拔掉重建,播放进度弹回 0。改成只有任务还在queued/running时才继续拉详情,终态之后不再重建。 - 下载接口不支持 HTTP Range,
<video>播放前的探测请求拿不到有效响应:之前经gateway.py转发(StreamingResponse(urlopen(...))),Range 请求一律回 200 而不是 206,也没有Content-Length/Accept-Ranges——<video>标签播放前会先探一次 Range 拿时长,探不到就卡在readyState=0播不出来(用curl/页面里直接fetch()都测不出来,因为它们不做这个探测)。 改成绕过 gateway 这层转发,直接连去真正跑这条任务的后端(api.py自己是 StarletteFileResponse,原生支持 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 就完事。
已知限制
- 鉴权:2026-08-29 已加。
H3_CONSOLE_AUTH_TOKEN环境变量设了才校验(两个 systemd 实例 都已配置同一个 token),/v1/*全部接口要求Authorization: Bearer <token>;页面本身 (/、/newtask.html、/task.html/{tid})和/assets/*静态文件不校验——前者是鸡生蛋 蛋生鸡(没加载出页面无从输入token),后者是因为服务器上的 GPU worker 要能反向拉取上传的 参考图/参考视频,且它们本身敏感度低。<video src>/下载<a href>是浏览器原生请求加不上 头,改成认?token=查询参数,跟 header 走同一份校验(server.py的_require_token)。 前端static/index.html首次调用 API 时prompt()弹一次令牌存localStorage,401 会清空 重弹。 - 任务持久化是 console 自己的 sqlite,和
api.py内存态之间有轮询间隔的短暂不一致(几秒级), 不追求强一致。 - 磁盘:
/只剩 29G,/data83% 满,上传接口目前只做单文件 20MB 上限校验,没做磁盘余量检查, 真到磁盘紧张时会和其他生成任务一样直接写失败,没有额外保护——按 DESIGN.md「开放风险」记录的, 真出问题再加。