mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(ui): proxy canonical inbound media previews (#100725)
Co-authored-by: Cornna <96944678+ymylive@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8b2e9ddc64
commit
3170bfa489
@@ -346,6 +346,70 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("renders a canonical inbound image through the ticketed media route", async () => {
|
||||
const context = await newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const requestedMediaUrls: URL[] = [];
|
||||
await page.route("**/__openclaw__/assistant-media?**", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
requestedMediaUrls.push(url);
|
||||
expect(url.searchParams.get("source")).toBe("media://inbound/telegram-photo.png");
|
||||
if (url.searchParams.get("meta") === "1") {
|
||||
expect(request.headers().authorization).toBe("Bearer e2e-device-token");
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
available: true,
|
||||
mediaTicket: "ticket-inbound",
|
||||
mediaTicketExpiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
expect(url.searchParams.get("mediaTicket")).toBe("ticket-inbound");
|
||||
await route.fulfill({
|
||||
contentType: "image/png",
|
||||
body: Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Zl1sAAAAASUVORK5CYII=",
|
||||
"base64",
|
||||
),
|
||||
});
|
||||
});
|
||||
await installMockGateway(page, {
|
||||
historyMessages: [
|
||||
{
|
||||
id: "user-inbound-media-ref",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "🖼️ Attached image" }],
|
||||
MediaPath: "media://inbound/telegram-photo.png",
|
||||
MediaType: "image/png",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
await expect.poll(() => requestedMediaUrls.length, { timeout: 10_000 }).toBe(2);
|
||||
const image = page.getByAltText("Attached image");
|
||||
await image.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await expect
|
||||
.poll(() =>
|
||||
image.evaluate((element) =>
|
||||
element instanceof HTMLImageElement && element.complete ? element.naturalWidth : 0,
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
} finally {
|
||||
await closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("opens current context and latest-run usage from the composer ring", async () => {
|
||||
const context = await newBrowserContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -2076,6 +2076,94 @@ describe("grouped chat rendering", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders canonical inbound transcript images through the authenticated media route", async () => {
|
||||
resetAssistantAttachmentAvailabilityCacheForTest();
|
||||
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
||||
const mediaUrl = new URL(url, "http://control.test");
|
||||
expect(mediaUrl.pathname).toBe("/openclaw/__openclaw__/assistant-media");
|
||||
expect([...mediaUrl.searchParams.keys()].toSorted()).toEqual(["meta", "source"]);
|
||||
expect(mediaUrl.searchParams.get("meta")).toBe("1");
|
||||
expect(mediaUrl.searchParams.get("source")).toBe("media://inbound/telegram-photo.png");
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer session-token");
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => mediaTicketPayload("ticket-inbound"),
|
||||
};
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
|
||||
const container = document.createElement("div");
|
||||
const renderMessage = () =>
|
||||
renderGroupedMessage(
|
||||
container,
|
||||
{
|
||||
id: "user-inbound-media-ref",
|
||||
role: "user",
|
||||
content: "",
|
||||
MediaPath: "media://inbound/telegram-photo.png",
|
||||
MediaType: "image/png",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"user",
|
||||
{
|
||||
showToolCalls: false,
|
||||
basePath: "/openclaw",
|
||||
assistantAttachmentAuthToken: "session-token",
|
||||
localMediaPreviewRoots: [],
|
||||
onRequestUpdate: renderMessage,
|
||||
},
|
||||
);
|
||||
|
||||
renderMessage();
|
||||
await flushAssistantAttachmentAvailabilityChecks();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
container.querySelector<HTMLImageElement>(".chat-message-image")?.getAttribute("src"),
|
||||
).toBe(
|
||||
"/openclaw/__openclaw__/assistant-media?source=media%3A%2F%2Finbound%2Ftelegram-photo.png&mediaTicket=ticket-inbound",
|
||||
);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"media://outbound/photo.png",
|
||||
"media://inbound/",
|
||||
"media://inbound/nested%2Fphoto.png",
|
||||
"media://inbound/%00.png",
|
||||
"media://inbound/nested/../photo.png",
|
||||
"media://inbound/%2e%2e/photo.png",
|
||||
"media://inbound/..",
|
||||
"media://inbound/photo.png?raw=1",
|
||||
"media://inbound/photo.png#preview",
|
||||
])("does not proxy non-canonical inbound media ref %s", (source) => {
|
||||
resetAssistantAttachmentAvailabilityCacheForTest();
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
|
||||
const container = document.createElement("div");
|
||||
renderGroupedMessage(
|
||||
container,
|
||||
{
|
||||
id: "user-invalid-inbound-media-ref",
|
||||
role: "user",
|
||||
content: "",
|
||||
MediaPath: source,
|
||||
MediaType: "image/png",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"user",
|
||||
{
|
||||
showToolCalls: false,
|
||||
assistantAttachmentAuthToken: "session-token",
|
||||
localMediaPreviewRoots: [],
|
||||
},
|
||||
);
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("fetches managed chat images with auth and renders blob previews", async () => {
|
||||
resetAssistantAttachmentAvailabilityCacheForTest();
|
||||
const managedChatImageUrl =
|
||||
|
||||
@@ -1228,6 +1228,7 @@ function isLocalAssistantAttachmentSource(source: string): boolean {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isCanonicalInboundMediaSource(trimmed) ||
|
||||
trimmed.startsWith("file://") ||
|
||||
trimmed.startsWith("~") ||
|
||||
trimmed.startsWith("/") ||
|
||||
@@ -1235,11 +1236,30 @@ function isLocalAssistantAttachmentSource(source: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isCanonicalInboundMediaSource(source: string): boolean {
|
||||
// Match the raw one-segment form first; URL parsing would erase dot segments.
|
||||
const match = /^media:\/\/inbound\/([^/?#]+)$/i.exec(source.trim());
|
||||
if (!match?.[1]) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const id = decodeURIComponent(match[1]);
|
||||
return (
|
||||
id !== "." && id !== ".." && !id.includes("/") && !id.includes("\\") && !id.includes("\0")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLocalAttachmentPath(source: string): string | null {
|
||||
const trimmed = source.trim();
|
||||
if (!isLocalAssistantAttachmentSource(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
if (isCanonicalInboundMediaSource(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.startsWith("file://")) {
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
@@ -1290,6 +1310,9 @@ function isLocalAttachmentPreviewAllowed(
|
||||
source: string,
|
||||
localMediaPreviewRoots: readonly string[],
|
||||
): boolean {
|
||||
if (isCanonicalInboundMediaSource(source)) {
|
||||
return true;
|
||||
}
|
||||
const normalizedSource = normalizeLocalAttachmentPath(source);
|
||||
const comparableSources = normalizedSource
|
||||
? [canonicalizeLocalPathForComparison(normalizedSource)]
|
||||
|
||||
Reference in New Issue
Block a user