# H3 Console 给 `minimax-h3/` 的四后端 API(`gateway.py` + 4× `api.py`)加一层 Web 管理台: 左侧任务记录列表 + 右侧新建/详情,上传首尾帧/参考图 → 提交生成 → 点开任务看状态和结果。 不重做调度,只加 gateway.py 没有的东西(Web UI、文件上传、任务持久化、按需 fl2va/ref2va 弹性分卡)。 需求/架构决策见 [`DESIGN.md`](DESIGN.md)——**先读那份**,尤其「四点五、弹性 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 ""` 杀掉了自己所在的 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`。 ## 怎么起(本地开发) ```bash 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 在局域网里的地址: ```bash H3_CONSOLE_PUBLIC_BASE=http://<你的 Mac 局域网 IP>:8090 ./run.sh ``` 前提是服务器能反向连回你的 Mac(同一局域网通常没问题;接了会劫持路由的 VPN 就不行, `HANDOFF.md` 里记过一次这个坑:`nc -z` 会给假阳性,出问题用 `ping`+`route -n get` 确认路径)。 不配这个变量时,文生视频(不需要上传)那个 tab 可以正常测。 ## 部署到服务器(已完成,2026-08-20) ```bash 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 & ``` 对外访问用现成的 FRP 隧道 `gateway.wudaima.com`(配置已存在、一直空挂,见 DESIGN.md 问题2)—— 这一步还没验证公网访问,只验证过局域网内 `http://192.168.110.67:8080` 直连。 重启/重新部署时**不要用 `pkill -f`**(见上面「揪出的坑」第3条,会误杀自己的 SSH 会话),改用: ```bash PID=$(ss -lptn "sport = :8080" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u) [ -n "$PID" ] && kill $PID ``` **部署后验过**:`curl http://192.168.110.67:8080/v1/backends` 看到 4/4 ready;提交一个 fl2va 首尾帧任务,`console.log` 里能看到 `[supervisor] flip port=... ref2va -> fl2va`,30 秒后(空闲 判定)触发一次、只触发一次,新后端 ready 后任务自动提交并跑完,下载代理拿到合法 mp4。 ## 回滚 console 是独立进程,不碰 `gateway.py`/`api.py` 一行代码,出问题直接 `kill` console 进程即可, 四个 H3 后端和 gateway 不受影响(不像 `deploy-scripts/` 那套需要镜像级回滚)。 ## 已知限制 - 鉴权:暂未加(`DESIGN.md` 问题3,跑通了再加)。部署到公网前必须补,否则和现有服务一样是 无鉴权可直调,风险更高——console 多了写文件(上传)的能力。 - 任务持久化是 console 自己的 sqlite,和 `api.py` 内存态之间有轮询间隔的短暂不一致(几秒级), 不追求强一致。 - 磁盘:`/` 只剩 29G,`/data` 83% 满,上传接口目前只做单文件 20MB 上限校验,没做磁盘余量检查, 真到磁盘紧张时会和其他生成任务一样直接写失败,没有额外保护——按 DESIGN.md「开放风险」记录的, 真出问题再加。