Commit 46fa2657cce067cfa3e56c29e1b683e7f6840b5a
1 parent
4c45f719
h3-console: 任务详情页加原始参数/ComfyUI history 两个排查字段,结果视频挪到顶部
排查任务时经常要么去问 sqlite 里存的原始请求参数,要么去查 ComfyUI 自己的 /history
拿实际发给模型的完整 workflow graph——两者详情页都没有,只能手动 SSH 上去查。
- raylight/server.py 加 GET /v1/videos/{jid}/comfy_history,透传 ComfyUI 的
/history/{prompt_id}(这份数据在 ComfyUI 进程内存里,没有落盘,ComfyUI 重启后
查不到,接口会给出明确提示而不是裸错误)。
- h3-console/server.py 加对应代理。
- 详情页底部加两个默认折叠的区块:原始参数(req 原样 JSON,纯前端渲染,不用额外
请求)、ComfyUI History(懒加载,点开才去问后端,避免 queued/running 状态每次
轮询详情都多打一次 ComfyUI)。
- 顺带把生成结果视频从页面底部挪到状态/操作按钮那一行下面,不用再滚到底部看结果。
已在服务器重启验证:接口对已经清空 history 的任务(ComfyUI 重启后)正确返回明确
错误而不是裸 500;重启期间 14 条排队任务原样保留。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PA1AWh8e7wJ3vyhKBAEQux
Showing
4 changed files
with
94 additions
and
1 deletions
h3-console/server.py
| @@ -434,6 +434,23 @@ async def prioritize_video(jid: str): | @@ -434,6 +434,23 @@ async def prioritize_video(jid: str): | ||
| 434 | return job | 434 | return job |
| 435 | 435 | ||
| 436 | 436 | ||
| 437 | +@api.get("/v1/videos/{jid}/comfy_history") | ||
| 438 | +async def get_comfy_history(jid: str): | ||
| 439 | + with db() as con: | ||
| 440 | + row = con.execute("SELECT gateway_id, backend FROM jobs WHERE id=?", (jid,)).fetchone() | ||
| 441 | + if row is None: | ||
| 442 | + raise HTTPException(404, "无此任务") | ||
| 443 | + if not row["gateway_id"] or not row["backend"]: | ||
| 444 | + raise HTTPException(409, "任务还在本地排队,尚未分配到可用后端") | ||
| 445 | + try: | ||
| 446 | + resp = await client.get(f"{row['backend']}/v1/videos/{row['gateway_id']}/comfy_history") | ||
| 447 | + except httpx.HTTPError: | ||
| 448 | + raise HTTPException(502, "查询 ComfyUI history 发不到后端,稍后再试") | ||
| 449 | + if resp.status_code >= 400: | ||
| 450 | + raise HTTPException(resp.status_code, resp.text) | ||
| 451 | + return resp.json() | ||
| 452 | + | ||
| 453 | + | ||
| 437 | @api.get("/v1/videos/{jid}") | 454 | @api.get("/v1/videos/{jid}") |
| 438 | async def get_video(jid: str): | 455 | async def get_video(jid: str): |
| 439 | with db() as con: | 456 | with db() as con: |
h3-console/static/index.html
| @@ -84,6 +84,11 @@ | @@ -84,6 +84,11 @@ | ||
| 84 | #detail .dl dd { margin: 0; word-break: break-all; } | 84 | #detail .dl dd { margin: 0; word-break: break-all; } |
| 85 | #detail video { max-width: 100%; margin-top: 12px; border-radius: 6px; } | 85 | #detail video { max-width: 100%; margin-top: 12px; border-radius: 6px; } |
| 86 | #detail .download { display: inline-block; margin-top: 10px; font-size: 13px; } | 86 | #detail .download { display: inline-block; margin-top: 10px; font-size: 13px; } |
| 87 | + #detail .raw-block { margin-top: 14px; font-size: 13px; } | ||
| 88 | + #detail .raw-block summary { cursor: pointer; opacity: .7; } | ||
| 89 | + #detail .raw-block summary:hover { opacity: 1; } | ||
| 90 | + #detail .raw-block pre { max-height: 400px; overflow: auto; background: rgba(128,128,128,.1); | ||
| 91 | + padding: 10px; border-radius: 6px; font-size: 12px; white-space: pre-wrap; word-break: break-all; } | ||
| 87 | </style> | 92 | </style> |
| 88 | </head> | 93 | </head> |
| 89 | <body> | 94 | <body> |
| @@ -1025,10 +1030,26 @@ async function refreshDetail() { | @@ -1025,10 +1030,26 @@ async function refreshDetail() { | ||
| 1025 | if (imgTags || videoTags) refThumbs += `<div class="thumbrow">${imgTags}${videoTags}</div>`; | 1030 | if (imgTags || videoTags) refThumbs += `<div class="thumbrow">${imgTags}${videoTags}</div>`; |
| 1026 | if (audioTags) refThumbs += `<div class="thumbrow">${audioTags}</div>`; | 1031 | if (audioTags) refThumbs += `<div class="thumbrow">${audioTags}</div>`; |
| 1027 | 1032 | ||
| 1033 | + // 2026-09-03:原始请求参数(req 原样 JSON)跟 ComfyUI history(实际发给模型的完整 | ||
| 1034 | + // workflow graph + 执行结果)都放页面最底部——排查问题时才需要看,默认折叠不占地方。 | ||
| 1035 | + // history 不跟着 resultHtml 一起渲染,是懒加载(点了才去问 ComfyUI),因为 queued/running | ||
| 1036 | + // 状态的任务每几秒轮询一次详情,没必要每次都多打一次 ComfyUI 的 /history。 | ||
| 1037 | + const rawReqHtml = `<details class="raw-block"> | ||
| 1038 | + <summary>用户提交的原始参数(JSON)</summary> | ||
| 1039 | + <pre>${JSON.stringify(req, null, 2).replace(/</g, '<')}</pre> | ||
| 1040 | + </details>`; | ||
| 1041 | + const comfyHistoryHtml = j.comfy_prompt_id | ||
| 1042 | + ? `<details class="raw-block" ontoggle="if(this.open) loadComfyHistory('${j.id}')"> | ||
| 1043 | + <summary>ComfyUI History(实际发给模型的 workflow + 执行结果)</summary> | ||
| 1044 | + <pre id="comfy-history-${j.id}">点开后加载中…</pre> | ||
| 1045 | + </details>` | ||
| 1046 | + : ''; | ||
| 1047 | + | ||
| 1028 | box.innerHTML = ` | 1048 | box.innerHTML = ` |
| 1029 | <div class="back" onclick="showCreate()">← 新建任务</div> | 1049 | <div class="back" onclick="showCreate()">← 新建任务</div> |
| 1030 | <h2>${j.id}</h2> | 1050 | <h2>${j.id}</h2> |
| 1031 | <div>${statusLabel(j.status)}${j.status === 'queued' && j.queue_position ? ` <span style="opacity:.7;font-size:13px">排第 ${j.queue_position} 位</span>` : ''} <button class="submit" style="padding:4px 12px;margin-left:8px" onclick="rerunJob('${j.id}')">重跑</button>${j.status === 'queued' ? ` <button style="padding:4px 12px;margin-left:8px" onclick="prioritizeJob('${j.id}')">优先插队</button> <button style="padding:4px 12px;margin-left:8px" onclick="cancelJob('${j.id}',false)">取消</button>` : ''}${j.status === 'running' ? ` <button style="padding:4px 12px;margin-left:8px;color:#c33" onclick="cancelJob('${j.id}',true)">停止</button>` : ''}</div> | 1051 | <div>${statusLabel(j.status)}${j.status === 'queued' && j.queue_position ? ` <span style="opacity:.7;font-size:13px">排第 ${j.queue_position} 位</span>` : ''} <button class="submit" style="padding:4px 12px;margin-left:8px" onclick="rerunJob('${j.id}')">重跑</button>${j.status === 'queued' ? ` <button style="padding:4px 12px;margin-left:8px" onclick="prioritizeJob('${j.id}')">优先插队</button> <button style="padding:4px 12px;margin-left:8px" onclick="cancelJob('${j.id}',false)">取消</button>` : ''}${j.status === 'running' ? ` <button style="padding:4px 12px;margin-left:8px;color:#c33" onclick="cancelJob('${j.id}',true)">停止</button>` : ''}</div> |
| 1052 | + ${resultHtml} | ||
| 1032 | <dl class="dl"> | 1053 | <dl class="dl"> |
| 1033 | <dt>Prompt</dt><dd>${(req.prompt || '').replace(/</g,'<')}</dd> | 1054 | <dt>Prompt</dt><dd>${(req.prompt || '').replace(/</g,'<')}</dd> |
| 1034 | <dt>分区</dt><dd>${j.partition || '—'}</dd> | 1055 | <dt>分区</dt><dd>${j.partition || '—'}</dd> |
| @@ -1045,11 +1066,22 @@ async function refreshDetail() { | @@ -1045,11 +1066,22 @@ async function refreshDetail() { | ||
| 1045 | <dt>峰值显存</dt><dd>${j.peak_vram_gb ? j.peak_vram_gb + ' GB' : '—'}</dd> | 1066 | <dt>峰值显存</dt><dd>${j.peak_vram_gb ? j.peak_vram_gb + ' GB' : '—'}</dd> |
| 1046 | </dl> | 1067 | </dl> |
| 1047 | ${refThumbs} | 1068 | ${refThumbs} |
| 1048 | - ${resultHtml} | 1069 | + ${rawReqHtml} |
| 1070 | + ${comfyHistoryHtml} | ||
| 1049 | `; | 1071 | `; |
| 1050 | } catch (e) { /* 暂时连不上,保留当前显示,下一轮再试 */ } | 1072 | } catch (e) { /* 暂时连不上,保留当前显示,下一轮再试 */ } |
| 1051 | } | 1073 | } |
| 1052 | 1074 | ||
| 1075 | +async function loadComfyHistory(id) { | ||
| 1076 | + const pre = document.getElementById(`comfy-history-${id}`); | ||
| 1077 | + if (!pre) return; | ||
| 1078 | + try { | ||
| 1079 | + const r = await fetch(`${H3_BASE}/v1/videos/${id}/comfy_history`); | ||
| 1080 | + if (!r.ok) { pre.textContent = '加载失败: ' + (await r.text()); return; } | ||
| 1081 | + pre.textContent = JSON.stringify(await r.json(), null, 2); | ||
| 1082 | + } catch (e) { pre.textContent = '加载失败: ' + e.message; } | ||
| 1083 | +} | ||
| 1084 | + | ||
| 1053 | async function refreshJobs() { | 1085 | async function refreshJobs() { |
| 1054 | if (viewingTrash) { await loadTrash(); renderSidebar(); return; } | 1086 | if (viewingTrash) { await loadTrash(); renderSidebar(); return; } |
| 1055 | try { | 1087 | try { |
raylight/scripts/server.py
| @@ -700,6 +700,28 @@ def get_video(jid: str): | @@ -700,6 +700,28 @@ def get_video(jid: str): | ||
| 700 | return out | 700 | return out |
| 701 | 701 | ||
| 702 | 702 | ||
| 703 | +@app.get("/v1/videos/{jid}/comfy_history") | ||
| 704 | +def get_comfy_history(jid: str): | ||
| 705 | + """透传 ComfyUI 自己的 /history/{prompt_id}——带完整的原始 workflow graph + 执行结果, | ||
| 706 | + 这份数据只在 ComfyUI 进程内存里,没有落盘,ComfyUI 重启(包括 start.sh 整体重启)后就 | ||
| 707 | + 查不到了,不是本服务的 bug。""" | ||
| 708 | + j = JOBS.get(jid) | ||
| 709 | + if j is None: | ||
| 710 | + raise HTTPException(404, "无此任务") | ||
| 711 | + if not j.comfy_prompt_id: | ||
| 712 | + raise HTTPException(409, "这条任务还没提交给 ComfyUI,没有 history") | ||
| 713 | + try: | ||
| 714 | + r = httpx.get(f"{COMFY_BASE}/history/{j.comfy_prompt_id}", timeout=5.0) | ||
| 715 | + except Exception as e: | ||
| 716 | + raise HTTPException(502, f"查询 ComfyUI history 失败: {type(e).__name__}: {e}") | ||
| 717 | + if r.status_code != 200: | ||
| 718 | + raise HTTPException(r.status_code, r.text) | ||
| 719 | + data = r.json() | ||
| 720 | + if j.comfy_prompt_id not in data: | ||
| 721 | + raise HTTPException(404, "ComfyUI 没有这条 prompt_id 的 history了(大概率是 ComfyUI 进程后来重启过,内存记录被清空)") | ||
| 722 | + return data[j.comfy_prompt_id] | ||
| 723 | + | ||
| 724 | + | ||
| 703 | @app.get("/v1/videos/{jid}/file") | 725 | @app.get("/v1/videos/{jid}/file") |
| 704 | def get_video_file(jid: str): | 726 | def get_video_file(jid: str): |
| 705 | j = JOBS.get(jid) | 727 | j = JOBS.get(jid) |
raylight/server.py
| @@ -700,6 +700,28 @@ def get_video(jid: str): | @@ -700,6 +700,28 @@ def get_video(jid: str): | ||
| 700 | return out | 700 | return out |
| 701 | 701 | ||
| 702 | 702 | ||
| 703 | +@app.get("/v1/videos/{jid}/comfy_history") | ||
| 704 | +def get_comfy_history(jid: str): | ||
| 705 | + """透传 ComfyUI 自己的 /history/{prompt_id}——带完整的原始 workflow graph + 执行结果, | ||
| 706 | + 这份数据只在 ComfyUI 进程内存里,没有落盘,ComfyUI 重启(包括 start.sh 整体重启)后就 | ||
| 707 | + 查不到了,不是本服务的 bug。""" | ||
| 708 | + j = JOBS.get(jid) | ||
| 709 | + if j is None: | ||
| 710 | + raise HTTPException(404, "无此任务") | ||
| 711 | + if not j.comfy_prompt_id: | ||
| 712 | + raise HTTPException(409, "这条任务还没提交给 ComfyUI,没有 history") | ||
| 713 | + try: | ||
| 714 | + r = httpx.get(f"{COMFY_BASE}/history/{j.comfy_prompt_id}", timeout=5.0) | ||
| 715 | + except Exception as e: | ||
| 716 | + raise HTTPException(502, f"查询 ComfyUI history 失败: {type(e).__name__}: {e}") | ||
| 717 | + if r.status_code != 200: | ||
| 718 | + raise HTTPException(r.status_code, r.text) | ||
| 719 | + data = r.json() | ||
| 720 | + if j.comfy_prompt_id not in data: | ||
| 721 | + raise HTTPException(404, "ComfyUI 没有这条 prompt_id 的 history了(大概率是 ComfyUI 进程后来重启过,内存记录被清空)") | ||
| 722 | + return data[j.comfy_prompt_id] | ||
| 723 | + | ||
| 724 | + | ||
| 703 | @app.get("/v1/videos/{jid}/file") | 725 | @app.get("/v1/videos/{jid}/file") |
| 704 | def get_video_file(jid: str): | 726 | def get_video_file(jid: str): |
| 705 | j = JOBS.get(jid) | 727 | j = JOBS.get(jid) |