Skip to content

Commit 98f8ec1

Browse files
committed
fix(ui): proxy inbound media previews
1 parent 64b9b60 commit 98f8ec1

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

ui/src/ui/chat/grouped-render.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,57 @@ describe("grouped chat rendering", () => {
15821582
vi.unstubAllGlobals();
15831583
});
15841584

1585+
it("renders canonical inbound media refs through the Control UI media route", async () => {
1586+
resetAssistantAttachmentAvailabilityCacheForTest();
1587+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
1588+
const mediaUrl = new URL(url, "http://control.test");
1589+
expect(mediaUrl.pathname).toBe("/openclaw/__openclaw__/assistant-media");
1590+
expect([...mediaUrl.searchParams.keys()].toSorted()).toEqual(["meta", "source"]);
1591+
expect(mediaUrl.searchParams.get("meta")).toBe("1");
1592+
expect(mediaUrl.searchParams.get("source")).toBe("media://inbound/telegram-photo.png");
1593+
const headers = init?.headers as Headers;
1594+
expect(headers.get("Authorization")).toBe("Bearer session-token");
1595+
return {
1596+
ok: true,
1597+
json: async () => mediaTicketPayload("ticket-inbound"),
1598+
};
1599+
});
1600+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
1601+
1602+
const container = document.createElement("div");
1603+
const renderMessage = () =>
1604+
renderGroupedMessage(
1605+
container,
1606+
{
1607+
id: "user-inbound-media-ref",
1608+
role: "user",
1609+
content: "",
1610+
MediaPath: "media://inbound/telegram-photo.png",
1611+
MediaType: "image/png",
1612+
timestamp: Date.now(),
1613+
},
1614+
"user",
1615+
{
1616+
showToolCalls: false,
1617+
basePath: "/openclaw",
1618+
assistantAttachmentAuthToken: "session-token",
1619+
localMediaPreviewRoots: [],
1620+
onRequestUpdate: renderMessage,
1621+
},
1622+
);
1623+
1624+
renderMessage();
1625+
await flushAssistantAttachmentAvailabilityChecks();
1626+
1627+
expect(fetchMock).toHaveBeenCalledTimes(1);
1628+
expect(
1629+
container.querySelector<HTMLImageElement>(".chat-message-image")?.getAttribute("src"),
1630+
).toBe(
1631+
"/openclaw/__openclaw__/assistant-media?source=media%3A%2F%2Finbound%2Ftelegram-photo.png&mediaTicket=ticket-inbound",
1632+
);
1633+
vi.unstubAllGlobals();
1634+
});
1635+
15851636
it("fetches managed chat images with auth and renders blob previews", async () => {
15861637
resetAssistantAttachmentAvailabilityCacheForTest();
15871638
const managedChatImageUrl =

ui/src/ui/chat/grouped-render.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,18 +1013,39 @@ function isLocalAssistantAttachmentSource(source: string): boolean {
10131013
return false;
10141014
}
10151015
return (
1016+
isCanonicalInboundMediaRef(trimmed) ||
10161017
trimmed.startsWith("file://") ||
10171018
trimmed.startsWith("~") ||
10181019
trimmed.startsWith("/") ||
10191020
/^[a-zA-Z]:[\\/]/.test(trimmed)
10201021
);
10211022
}
10221023

1024+
function isCanonicalInboundMediaRef(source: string): boolean {
1025+
try {
1026+
const parsed = new URL(source.trim());
1027+
const id = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
1028+
return (
1029+
parsed.protocol === "media:" &&
1030+
parsed.hostname === "inbound" &&
1031+
Boolean(id) &&
1032+
!id.includes("/") &&
1033+
!id.includes("\\") &&
1034+
!id.includes("\0")
1035+
);
1036+
} catch {
1037+
return false;
1038+
}
1039+
}
1040+
10231041
function normalizeLocalAttachmentPath(source: string): string | null {
10241042
const trimmed = source.trim();
10251043
if (!isLocalAssistantAttachmentSource(trimmed)) {
10261044
return null;
10271045
}
1046+
if (isCanonicalInboundMediaRef(trimmed)) {
1047+
return null;
1048+
}
10281049
if (trimmed.startsWith("file://")) {
10291050
try {
10301051
const url = new URL(trimmed);
@@ -1075,6 +1096,9 @@ function isLocalAttachmentPreviewAllowed(
10751096
source: string,
10761097
localMediaPreviewRoots: readonly string[],
10771098
): boolean {
1099+
if (isCanonicalInboundMediaRef(source)) {
1100+
return true;
1101+
}
10781102
const normalizedSource = normalizeLocalAttachmentPath(source);
10791103
const comparableSources = normalizedSource
10801104
? [canonicalizeLocalPathForComparison(normalizedSource)]

0 commit comments

Comments
 (0)