mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
feat(history): support media-aware output refs
This commit is contained in:
+97
-19
@@ -23,6 +23,9 @@ COMFYUI_URL = (
|
||||
or "http://127.0.0.1:8188"
|
||||
)
|
||||
HISTORY_TIMEOUT = 5
|
||||
PREVIEWABLE_MEDIA_TYPES = ("images", "video", "audio", "3d", "text")
|
||||
THREE_D_EXTENSIONS = (".obj", ".fbx", ".gltf", ".glb", ".usdz")
|
||||
TEXT_PREVIEW_MAX_LENGTH = 1024
|
||||
|
||||
|
||||
def _pick_string(payload: Dict[str, Any], *keys: str) -> str:
|
||||
@@ -55,16 +58,70 @@ def _pick_asset_api_id(image_ref: Dict[str, Any]) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def normalize_history_image_ref(image_ref: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
if not isinstance(image_ref, dict):
|
||||
return None
|
||||
def _has_3d_extension(filename: str) -> bool:
|
||||
lower = filename.lower()
|
||||
return any(lower.endswith(ext) for ext in THREE_D_EXTENSIONS)
|
||||
|
||||
asset_hash = _pick_asset_hash(image_ref)
|
||||
asset_api_id = _pick_asset_api_id(image_ref)
|
||||
named_filename = _pick_string(image_ref, "filename", "name")
|
||||
|
||||
def _normalize_text_content(value: Any) -> Optional[Dict[str, Any]]:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value)
|
||||
if text == "":
|
||||
return None
|
||||
truncated = len(text) > TEXT_PREVIEW_MAX_LENGTH
|
||||
if truncated:
|
||||
text = text[:TEXT_PREVIEW_MAX_LENGTH]
|
||||
return {
|
||||
"filename": "",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
"media_type": "text",
|
||||
"asset_hash": "",
|
||||
"asset_api_id": "",
|
||||
"asset_api_required": False,
|
||||
"resolution": "inline_text",
|
||||
"view_url": "",
|
||||
"content": text,
|
||||
"text_truncated": truncated,
|
||||
}
|
||||
|
||||
|
||||
def normalize_history_output_ref(
|
||||
output_ref: Any, media_type: str = "images"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
resolved_media_type = (
|
||||
media_type if media_type in PREVIEWABLE_MEDIA_TYPES else "images"
|
||||
)
|
||||
|
||||
if not isinstance(output_ref, dict):
|
||||
if resolved_media_type == "text":
|
||||
return _normalize_text_content(output_ref)
|
||||
if (
|
||||
resolved_media_type == "3d"
|
||||
and isinstance(output_ref, str)
|
||||
and _has_3d_extension(output_ref)
|
||||
):
|
||||
output_ref = {"filename": output_ref, "type": "output", "subfolder": ""}
|
||||
else:
|
||||
return None
|
||||
|
||||
declared_media_type = _pick_string(output_ref, "media_type", "mediaType")
|
||||
if declared_media_type in PREVIEWABLE_MEDIA_TYPES:
|
||||
resolved_media_type = declared_media_type
|
||||
|
||||
text_content = _pick_string(output_ref, "content", "text")
|
||||
if resolved_media_type == "text" and text_content:
|
||||
text_ref = _normalize_text_content(text_content)
|
||||
if text_ref:
|
||||
return text_ref
|
||||
|
||||
asset_hash = _pick_asset_hash(output_ref)
|
||||
asset_api_id = _pick_asset_api_id(output_ref)
|
||||
named_filename = _pick_string(output_ref, "filename", "name")
|
||||
filename = named_filename or asset_hash or asset_api_id
|
||||
subfolder = _pick_string(image_ref, "subfolder")
|
||||
img_type = _pick_string(image_ref, "type") or "output"
|
||||
subfolder = _pick_string(output_ref, "subfolder")
|
||||
img_type = _pick_string(output_ref, "type") or "output"
|
||||
|
||||
if not filename:
|
||||
return None
|
||||
@@ -89,14 +146,21 @@ def normalize_history_image_ref(image_ref: Dict[str, Any]) -> Optional[Dict[str,
|
||||
"filename": filename,
|
||||
"subfolder": subfolder,
|
||||
"type": img_type,
|
||||
"media_type": resolved_media_type,
|
||||
"asset_hash": asset_hash,
|
||||
"asset_api_id": asset_api_id,
|
||||
"asset_api_required": asset_api_required,
|
||||
"resolution": resolution,
|
||||
"view_url": view_url,
|
||||
"content": "",
|
||||
"text_truncated": False,
|
||||
}
|
||||
|
||||
|
||||
def normalize_history_image_ref(image_ref: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return normalize_history_output_ref(image_ref, "images")
|
||||
|
||||
|
||||
def fetch_history(prompt_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch history for a given prompt_id from ComfyUI.
|
||||
@@ -116,22 +180,36 @@ def fetch_history(prompt_id: str) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def extract_output_refs(history_item: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Extract previewable media outputs from a history item."""
|
||||
results = []
|
||||
outputs = history_item.get("outputs", {})
|
||||
|
||||
for node_output in outputs.values():
|
||||
if not isinstance(node_output, dict):
|
||||
continue
|
||||
for media_type in PREVIEWABLE_MEDIA_TYPES:
|
||||
refs = node_output.get(media_type, [])
|
||||
if not isinstance(refs, list):
|
||||
continue
|
||||
for ref in refs:
|
||||
normalized = normalize_history_output_ref(ref, media_type)
|
||||
if normalized:
|
||||
results.append(normalized)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def extract_images(history_item: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract image outputs from a history item.
|
||||
Returns list of normalized image refs.
|
||||
"""
|
||||
results = []
|
||||
outputs = history_item.get("outputs", {})
|
||||
|
||||
for node_id, node_output in outputs.items():
|
||||
images = node_output.get("images", [])
|
||||
for img in images:
|
||||
normalized = normalize_history_image_ref(img)
|
||||
if normalized:
|
||||
results.append(normalized)
|
||||
|
||||
return results
|
||||
return [
|
||||
ref
|
||||
for ref in extract_output_refs(history_item)
|
||||
if ref.get("media_type") == "images"
|
||||
]
|
||||
|
||||
|
||||
def get_job_status(history_item: Optional[Dict[str, Any]]) -> str:
|
||||
|
||||
@@ -202,6 +202,64 @@ test.describe('R107 Live Backend Parity', () => {
|
||||
expect(assetApiCalls).toBe(0);
|
||||
});
|
||||
|
||||
test('Job Monitor surfaces non-image media outputs as safe fallbacks', async ({ page }) => {
|
||||
const jobId = "job-media-refs";
|
||||
let assetApiCalls = 0;
|
||||
|
||||
await page.route(`**/history/${jobId}`, async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
[jobId]: {
|
||||
status: { status_str: "success", completed: true },
|
||||
outputs: {
|
||||
"9": {
|
||||
video: [{ filename: "clip.webm", type: "output" }],
|
||||
audio: [{ filename: "sound.wav", type: "output" }],
|
||||
"3d": ["mesh.glb"],
|
||||
text: ["hello from text output"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/openclaw/trace/**', async route => {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'not_found' }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/assets**', async route => {
|
||||
assetApiCalls += 1;
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'asset_api_should_not_be_called' }),
|
||||
});
|
||||
});
|
||||
|
||||
await clickTab(page, 'Jobs');
|
||||
await page.locator('input[placeholder="prompt_id"]').fill(jobId);
|
||||
await page.getByText('Add').click();
|
||||
|
||||
const jobRow = page.locator('.openclaw-job-row').first();
|
||||
await expect(page.locator('.openclaw-kv-val.ok')).toHaveText('completed', { timeout: 10000 });
|
||||
await expect(jobRow.locator('img')).toHaveCount(0);
|
||||
await expect(jobRow.locator('.openclaw-job-output-media-fallback')).toHaveCount(3);
|
||||
await expect(jobRow.locator('.openclaw-job-output-media-fallback')).toContainText([
|
||||
'video output available',
|
||||
'audio output available',
|
||||
'3d output available',
|
||||
]);
|
||||
await expect(jobRow.locator('.openclaw-job-output-text')).toContainText('hello from text output');
|
||||
expect(assetApiCalls).toBe(0);
|
||||
});
|
||||
|
||||
test('Degraded Adapter / Fail Handling', async ({ page }) => {
|
||||
// Mock Planner Failure (503 Service Unavailable)
|
||||
await page.route('**/openclaw/assist/planner', async route => {
|
||||
|
||||
@@ -205,6 +205,75 @@ class TestComfyUIHistoryParsing(unittest.TestCase):
|
||||
self.assertEqual(images[0]["resolution"], "asset_api_required")
|
||||
self.assertEqual(images[0]["view_url"], "")
|
||||
|
||||
def test_extract_output_refs_collects_previewable_media_types(self):
|
||||
from services.comfyui_history import extract_output_refs
|
||||
|
||||
history_item = {
|
||||
"outputs": {
|
||||
"1": {
|
||||
"images": [{"filename": "image.png", "type": "output"}],
|
||||
"video": [
|
||||
{
|
||||
"filename": "clip.webm",
|
||||
"type": "output",
|
||||
"format": "video/webm",
|
||||
}
|
||||
],
|
||||
"audio": [{"filename": "sound.wav", "type": "output"}],
|
||||
"3d": ["mesh.glb"],
|
||||
"text": ["hello from text output"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputs = extract_output_refs(history_item)
|
||||
self.assertEqual(
|
||||
[output["media_type"] for output in outputs],
|
||||
["images", "video", "audio", "3d", "text"],
|
||||
)
|
||||
self.assertIn("filename=image.png", outputs[0]["view_url"])
|
||||
self.assertIn("filename=clip.webm", outputs[1]["view_url"])
|
||||
self.assertIn("filename=sound.wav", outputs[2]["view_url"])
|
||||
self.assertIn("filename=mesh.glb", outputs[3]["view_url"])
|
||||
self.assertEqual(outputs[4]["resolution"], "inline_text")
|
||||
self.assertEqual(outputs[4]["content"], "hello from text output")
|
||||
self.assertEqual(outputs[4]["view_url"], "")
|
||||
|
||||
def test_extract_images_remains_image_only_for_callbacks(self):
|
||||
from services.comfyui_history import extract_images
|
||||
|
||||
history_item = {
|
||||
"outputs": {
|
||||
"1": {
|
||||
"images": [{"filename": "image.png", "type": "output"}],
|
||||
"video": [{"filename": "clip.webm", "type": "output"}],
|
||||
"audio": [{"filename": "sound.wav", "type": "output"}],
|
||||
"3d": ["mesh.glb"],
|
||||
"text": ["hello"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
images = extract_images(history_item)
|
||||
self.assertEqual(len(images), 1)
|
||||
self.assertEqual(images[0]["filename"], "image.png")
|
||||
self.assertEqual(images[0]["media_type"], "images")
|
||||
|
||||
def test_extract_output_refs_bounds_inline_text(self):
|
||||
from services.comfyui_history import (
|
||||
TEXT_PREVIEW_MAX_LENGTH,
|
||||
extract_output_refs,
|
||||
)
|
||||
|
||||
long_text = "x" * (TEXT_PREVIEW_MAX_LENGTH + 10)
|
||||
history_item = {"outputs": {"1": {"text": [long_text]}}}
|
||||
|
||||
outputs = extract_output_refs(history_item)
|
||||
self.assertEqual(len(outputs), 1)
|
||||
self.assertEqual(outputs[0]["media_type"], "text")
|
||||
self.assertEqual(len(outputs[0]["content"]), TEXT_PREVIEW_MAX_LENGTH)
|
||||
self.assertTrue(outputs[0]["text_truncated"])
|
||||
|
||||
def test_get_job_status(self):
|
||||
from services.comfyui_history import get_job_status
|
||||
|
||||
|
||||
+98
-13
@@ -1,3 +1,7 @@
|
||||
const PREVIEWABLE_MEDIA_TYPES = new Set(["images", "video", "audio", "3d", "text"]);
|
||||
const THREE_D_EXTENSIONS = [".obj", ".fbx", ".gltf", ".glb", ".usdz"];
|
||||
const TEXT_PREVIEW_MAX_LENGTH = 1024;
|
||||
|
||||
function pickAssetHash(imageRef = {}) {
|
||||
if (!imageRef || typeof imageRef !== "object") {
|
||||
return "";
|
||||
@@ -56,19 +60,86 @@ function pickFilename(imageRef = {}) {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function normalizeComfyOutputRef(imageRef = {}) {
|
||||
const assetHash = pickAssetHash(imageRef);
|
||||
const assetApiId = pickAssetApiId(imageRef);
|
||||
const namedFilename = pickFilename(imageRef);
|
||||
function has3dExtension(filename = "") {
|
||||
return THREE_D_EXTENSIONS.some((ext) => String(filename).toLowerCase().endsWith(ext));
|
||||
}
|
||||
|
||||
function resolveMediaType(imageRef = {}, fallback = "images") {
|
||||
if (imageRef && typeof imageRef === "object") {
|
||||
const direct = typeof imageRef.media_type === "string"
|
||||
? imageRef.media_type.trim()
|
||||
: (typeof imageRef.mediaType === "string" ? imageRef.mediaType.trim() : "");
|
||||
if (PREVIEWABLE_MEDIA_TYPES.has(direct)) {
|
||||
return direct;
|
||||
}
|
||||
}
|
||||
return PREVIEWABLE_MEDIA_TYPES.has(fallback) ? fallback : "images";
|
||||
}
|
||||
|
||||
function normalizeTextOutputRef(value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
let content = String(value);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
const textTruncated = content.length > TEXT_PREVIEW_MAX_LENGTH;
|
||||
if (textTruncated) {
|
||||
content = content.slice(0, TEXT_PREVIEW_MAX_LENGTH);
|
||||
}
|
||||
return {
|
||||
filename: "",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "text",
|
||||
asset_hash: "",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "inline_text",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: false,
|
||||
content,
|
||||
text_truncated: textTruncated,
|
||||
viewParams: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeComfyOutputRef(imageRef = {}, mediaType = "images") {
|
||||
let outputRef = imageRef;
|
||||
const resolvedMediaType = resolveMediaType(outputRef, mediaType);
|
||||
|
||||
if (!outputRef || typeof outputRef !== "object") {
|
||||
if (resolvedMediaType === "text") {
|
||||
return normalizeTextOutputRef(outputRef);
|
||||
}
|
||||
if (resolvedMediaType === "3d" && typeof outputRef === "string" && has3dExtension(outputRef)) {
|
||||
outputRef = { filename: outputRef, type: "output", subfolder: "" };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const finalMediaType = resolveMediaType(outputRef, resolvedMediaType);
|
||||
const textContent = typeof outputRef.content === "string" && outputRef.content
|
||||
? outputRef.content
|
||||
: (typeof outputRef.text === "string" && outputRef.text ? outputRef.text : "");
|
||||
if (finalMediaType === "text" && textContent) {
|
||||
return normalizeTextOutputRef(textContent);
|
||||
}
|
||||
|
||||
const assetHash = pickAssetHash(outputRef);
|
||||
const assetApiId = pickAssetApiId(outputRef);
|
||||
const namedFilename = pickFilename(outputRef);
|
||||
const filename = namedFilename || assetHash || assetApiId;
|
||||
const subfolder = typeof imageRef.subfolder === "string" ? imageRef.subfolder : "";
|
||||
const type = typeof imageRef.type === "string" && imageRef.type ? imageRef.type : "output";
|
||||
const subfolder = typeof outputRef.subfolder === "string" ? outputRef.subfolder : "";
|
||||
const type = typeof outputRef.type === "string" && outputRef.type ? outputRef.type : "output";
|
||||
|
||||
if (!filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const explicitAssetApiRequired = imageRef.asset_api_required === true;
|
||||
const explicitAssetApiRequired = outputRef.asset_api_required === true;
|
||||
const assetApiRequired = Boolean(explicitAssetApiRequired || (assetApiId && !assetHash && !namedFilename));
|
||||
|
||||
// IMPORTANT: asset-backed refs still resolve through /view when possible; do
|
||||
@@ -89,29 +160,43 @@ export function normalizeComfyOutputRef(imageRef = {}) {
|
||||
filename,
|
||||
subfolder,
|
||||
type,
|
||||
media_type: finalMediaType,
|
||||
asset_hash: assetHash || "",
|
||||
asset_api_id: assetApiId || "",
|
||||
asset_api_required: assetApiRequired,
|
||||
resolution: assetApiRequired ? "asset_api_required" : "view",
|
||||
unsupported_reason: assetApiRequired ? "asset_api_required" : "",
|
||||
is_asset_backed: Boolean(assetHash || assetApiId),
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractHistoryImageRefs(historyItem = {}) {
|
||||
export function extractHistoryOutputRefs(historyItem = {}) {
|
||||
const results = [];
|
||||
const outputs = historyItem && typeof historyItem === "object" ? (historyItem.outputs || {}) : {};
|
||||
|
||||
for (const nodeOutput of Object.values(outputs)) {
|
||||
const images = Array.isArray(nodeOutput?.images) ? nodeOutput.images : [];
|
||||
for (const imageRef of images) {
|
||||
const normalized = normalizeComfyOutputRef(imageRef);
|
||||
if (normalized) {
|
||||
results.push(normalized);
|
||||
if (!nodeOutput || typeof nodeOutput !== "object") {
|
||||
continue;
|
||||
}
|
||||
for (const [mediaType, refs] of Object.entries(nodeOutput)) {
|
||||
if (!PREVIEWABLE_MEDIA_TYPES.has(mediaType) || !Array.isArray(refs)) {
|
||||
continue;
|
||||
}
|
||||
for (const imageRef of refs) {
|
||||
const normalized = normalizeComfyOutputRef(imageRef, mediaType);
|
||||
if (normalized) {
|
||||
results.push(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function extractHistoryImageRefs(historyItem = {}) {
|
||||
return extractHistoryOutputRefs(historyItem).filter((ref) => ref.media_type === "images");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Tracks prompt execution and displays outputs.
|
||||
*/
|
||||
import { openclawApi } from "../openclaw_api.js";
|
||||
import { extractHistoryImageRefs } from "../openclaw_asset_refs.js";
|
||||
import { extractHistoryOutputRefs } from "../openclaw_asset_refs.js";
|
||||
import { parseJsonSafe } from "../openclaw_utils.js";
|
||||
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
@@ -183,7 +183,7 @@ export const jobMonitorTab = {
|
||||
outputGrid.style.marginTop = "8px";
|
||||
|
||||
job.outputs.forEach((out) => {
|
||||
if (out.view_url) {
|
||||
if (out.media_type === "images" && out.view_url) {
|
||||
const img = document.createElement("img");
|
||||
img.src = out.view_url;
|
||||
img.style.maxWidth = "80px";
|
||||
@@ -196,6 +196,27 @@ export const jobMonitorTab = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (out.media_type === "text" && out.content) {
|
||||
const textOutput = document.createElement("div");
|
||||
textOutput.className = "openclaw-job-output-fallback openclaw-job-output-text";
|
||||
textOutput.style.width = "160px";
|
||||
textOutput.style.minHeight = "80px";
|
||||
textOutput.style.padding = "6px";
|
||||
textOutput.style.fontSize = "10px";
|
||||
textOutput.style.lineHeight = "1.35";
|
||||
textOutput.style.whiteSpace = "pre-wrap";
|
||||
textOutput.style.overflowWrap = "anywhere";
|
||||
textOutput.style.border = "1px dashed var(--border-color)";
|
||||
textOutput.style.borderRadius = "6px";
|
||||
textOutput.style.background = "var(--comfy-menu-bg, rgba(255,255,255,0.04))";
|
||||
textOutput.title = out.text_truncated ? "Text output truncated" : "Text output";
|
||||
textOutput.textContent = out.text_truncated
|
||||
? `${out.content}\n...`
|
||||
: out.content;
|
||||
outputGrid.appendChild(textOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
if (out.asset_api_required) {
|
||||
const fallback = document.createElement("div");
|
||||
fallback.className = "openclaw-job-output-fallback";
|
||||
@@ -214,6 +235,29 @@ export const jobMonitorTab = {
|
||||
fallback.title = out.asset_api_id || out.filename || "Asset API output";
|
||||
fallback.textContent = "Asset API output requires /api/assets. Preview disabled.";
|
||||
outputGrid.appendChild(fallback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (out.view_url) {
|
||||
const mediaFallback = document.createElement("div");
|
||||
mediaFallback.className = "openclaw-job-output-fallback openclaw-job-output-media-fallback";
|
||||
mediaFallback.style.width = "110px";
|
||||
mediaFallback.style.minHeight = "80px";
|
||||
mediaFallback.style.padding = "6px";
|
||||
mediaFallback.style.display = "flex";
|
||||
mediaFallback.style.alignItems = "center";
|
||||
mediaFallback.style.justifyContent = "center";
|
||||
mediaFallback.style.textAlign = "center";
|
||||
mediaFallback.style.fontSize = "10px";
|
||||
mediaFallback.style.lineHeight = "1.3";
|
||||
mediaFallback.style.border = "1px dashed var(--border-color)";
|
||||
mediaFallback.style.borderRadius = "6px";
|
||||
mediaFallback.style.background = "var(--comfy-menu-bg, rgba(255,255,255,0.04))";
|
||||
mediaFallback.style.cursor = "pointer";
|
||||
mediaFallback.title = out.filename;
|
||||
mediaFallback.textContent = `${out.media_type || "media"} output available. Open preview.`;
|
||||
mediaFallback.onclick = () => window.open(out.view_url, "_blank");
|
||||
outputGrid.appendChild(mediaFallback);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -277,7 +321,7 @@ async function startPolling(promptId, onUpdate) {
|
||||
|
||||
if (statusStr === "success" || historyItem.outputs) {
|
||||
job.status = "completed";
|
||||
job.outputs = extractImages(historyItem);
|
||||
job.outputs = extractOutputs(historyItem);
|
||||
saveJobs();
|
||||
onUpdate();
|
||||
clearInterval(pollIntervals[promptId]);
|
||||
@@ -287,15 +331,19 @@ async function startPolling(promptId, onUpdate) {
|
||||
|
||||
}
|
||||
|
||||
function extractImages(historyItem) {
|
||||
return extractHistoryImageRefs(historyItem).map((img) => ({
|
||||
function extractOutputs(historyItem) {
|
||||
return extractHistoryOutputRefs(historyItem).map((img) => ({
|
||||
filename: img.filename,
|
||||
subfolder: img.subfolder,
|
||||
type: img.type,
|
||||
media_type: img.media_type,
|
||||
asset_hash: img.asset_hash,
|
||||
asset_api_id: img.asset_api_id,
|
||||
asset_api_required: img.asset_api_required,
|
||||
resolution: img.resolution,
|
||||
content: img.content,
|
||||
text_truncated: img.text_truncated,
|
||||
unsupported_reason: img.unsupported_reason,
|
||||
view_url: openclawApi.buildViewUrlForRef(img),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { extractHistoryImageRefs, normalizeComfyOutputRef } from "../../openclaw_asset_refs.js";
|
||||
import {
|
||||
extractHistoryImageRefs,
|
||||
extractHistoryOutputRefs,
|
||||
normalizeComfyOutputRef,
|
||||
} from "../../openclaw_asset_refs.js";
|
||||
|
||||
describe("openclaw asset refs", () => {
|
||||
it("keeps classic history refs on the /view filename+type contract", () => {
|
||||
@@ -14,12 +18,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "result.png",
|
||||
subfolder: "session-a",
|
||||
type: "temp",
|
||||
media_type: "images",
|
||||
asset_hash: "",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: false,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "result.png",
|
||||
subfolder: "session-a",
|
||||
@@ -39,12 +46,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "preview.png",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "images",
|
||||
asset_hash: "blake3:abc123",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: true,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "blake3:abc123",
|
||||
},
|
||||
@@ -63,12 +73,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "uploaded.png",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "images",
|
||||
asset_hash: "blake3:def456",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: true,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "blake3:def456",
|
||||
},
|
||||
@@ -85,12 +98,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "hash-alias.png",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "images",
|
||||
asset_hash: "blake3:alias123",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: true,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "blake3:alias123",
|
||||
},
|
||||
@@ -109,12 +125,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "nested-hash-alias.png",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "images",
|
||||
asset_hash: "blake3:nested-alias",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: true,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "blake3:nested-alias",
|
||||
},
|
||||
@@ -132,12 +151,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "asset-only-42",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "images",
|
||||
asset_hash: "",
|
||||
asset_api_id: "asset-only-42",
|
||||
asset_api_required: true,
|
||||
resolution: "asset_api_required",
|
||||
unsupported_reason: "asset_api_required",
|
||||
is_asset_backed: true,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: null,
|
||||
});
|
||||
});
|
||||
@@ -168,12 +190,15 @@ describe("openclaw asset refs", () => {
|
||||
filename: "classic.png",
|
||||
subfolder: "",
|
||||
type: "output",
|
||||
media_type: "images",
|
||||
asset_hash: "",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: false,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "classic.png",
|
||||
type: "output",
|
||||
@@ -183,16 +208,70 @@ describe("openclaw asset refs", () => {
|
||||
filename: "temp-preview.png",
|
||||
subfolder: "preview",
|
||||
type: "temp",
|
||||
media_type: "images",
|
||||
asset_hash: "blake3:temp123",
|
||||
asset_api_id: "",
|
||||
asset_api_required: false,
|
||||
resolution: "view",
|
||||
unsupported_reason: "",
|
||||
is_asset_backed: true,
|
||||
content: "",
|
||||
text_truncated: false,
|
||||
viewParams: {
|
||||
filename: "blake3:temp123",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("extracts previewable media outputs while keeping image-only wrapper compatibility", () => {
|
||||
const historyItem = {
|
||||
outputs: {
|
||||
"1": {
|
||||
images: [{ filename: "classic.png", type: "output" }],
|
||||
video: [{ filename: "clip.webm", type: "output", format: "video/webm" }],
|
||||
audio: [{ filename: "sound.wav", type: "output" }],
|
||||
"3d": ["mesh.glb"],
|
||||
text: ["hello text"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const outputs = extractHistoryOutputRefs(historyItem);
|
||||
expect(outputs.map((output) => output.media_type)).toEqual([
|
||||
"images",
|
||||
"video",
|
||||
"audio",
|
||||
"3d",
|
||||
"text",
|
||||
]);
|
||||
expect(outputs[1]).toEqual(expect.objectContaining({
|
||||
filename: "clip.webm",
|
||||
media_type: "video",
|
||||
viewParams: { filename: "clip.webm", type: "output" },
|
||||
}));
|
||||
expect(outputs[3]).toEqual(expect.objectContaining({
|
||||
filename: "mesh.glb",
|
||||
media_type: "3d",
|
||||
}));
|
||||
expect(outputs[4]).toEqual(expect.objectContaining({
|
||||
media_type: "text",
|
||||
content: "hello text",
|
||||
resolution: "inline_text",
|
||||
viewParams: null,
|
||||
}));
|
||||
|
||||
expect(extractHistoryImageRefs(historyItem).map((output) => output.media_type)).toEqual([
|
||||
"images",
|
||||
]);
|
||||
});
|
||||
|
||||
it("bounds inline text output previews", () => {
|
||||
const longText = "x".repeat(1100);
|
||||
const output = extractHistoryOutputRefs({ outputs: { "1": { text: [longText] } } })[0];
|
||||
|
||||
expect(output.media_type).toBe("text");
|
||||
expect(output.content).toHaveLength(1024);
|
||||
expect(output.text_truncated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user