Commit 46fa2657cce067cfa3e56c29e1b683e7f6840b5a

Authored by yibo
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
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, '&lt;')}</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,'&lt;')}</dd> 1054 <dt>Prompt</dt><dd>${(req.prompt || '').replace(/</g,'&lt;')}</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)