mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
gui: decode percent-encoded non-ASCII artifact paths
micromark percent-encodes non-ASCII characters in link hrefs, so an artifact link to a Chinese/Japanese/Korean filename dispatched the encoded path and the backend 404ed on the literal %E6… name. Decode the path (and strip a leading slash so it stays workspace-relative) before opening.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { Markdown, OPEN_ARTIFACT_EVENT } from "./Markdown";
|
||||
import { Markdown, OPEN_ARTIFACT_EVENT, normalizeArtifactPath } from "./Markdown";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -35,4 +35,30 @@ describe("Markdown artifact links", () => {
|
||||
render(<Markdown text="[](artifact:out/report.pdf)" />);
|
||||
expect(screen.getByTestId("artifact-chip").textContent).toContain("report.pdf");
|
||||
});
|
||||
|
||||
// Chinese (and other non-ASCII) filenames: micromark percent-encodes the href; without
|
||||
// decode the backend looks up the literal %E6… path and returns "not found".
|
||||
it("decodes percent-encoded non-ASCII artifact paths before opening", () => {
|
||||
const seen: string[] = [];
|
||||
const listener = (e: Event) => seen.push((e as CustomEvent).detail.path);
|
||||
window.addEventListener(OPEN_ARTIFACT_EVENT, listener);
|
||||
|
||||
render(<Markdown text="📄 [Monthly report](artifact:reports/2026-04_月度报告.md)" />);
|
||||
const chip = screen.getByTestId("artifact-chip");
|
||||
expect(chip.getAttribute("title")).toBe("reports/2026-04_月度报告.md");
|
||||
fireEvent.click(chip);
|
||||
expect(seen).toEqual(["reports/2026-04_月度报告.md"]);
|
||||
|
||||
window.removeEventListener(OPEN_ARTIFACT_EVENT, listener);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeArtifactPath", () => {
|
||||
it("decodes percent-encoded segments and strips a leading slash", () => {
|
||||
expect(normalizeArtifactPath("reports/2026-04_%E6%9C%88%E5%BA%A6%E6%8A%A5%E5%91%8A.md")).toBe(
|
||||
"reports/2026-04_月度报告.md",
|
||||
);
|
||||
expect(normalizeArtifactPath("/reports/foo.md")).toBe("reports/foo.md");
|
||||
expect(normalizeArtifactPath("reports/ascii.md")).toBe("reports/ascii.md");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,16 +10,36 @@ import { Icon } from "./Icon";
|
||||
// the session's artifact list, App un-hides the rail.
|
||||
export const OPEN_ARTIFACT_EVENT = "ocw-open-artifact";
|
||||
|
||||
/** Normalize an artifact: href path for the session workspace.
|
||||
*
|
||||
* react-markdown / micromark percent-encodes non-ASCII URL characters, so
|
||||
* `artifact:reports/报告.md` becomes `artifact:reports/%E6%8A%A5%E5%91%8A.md`.
|
||||
* The backend looks up the literal filesystem path — encoded names 404 as "not found".
|
||||
* Also strip a single leading `/` so `artifact:/reports/x.md` stays workspace-relative
|
||||
* (Path(workspace) / "/abs" would otherwise escape the workspace root).
|
||||
*/
|
||||
export function normalizeArtifactPath(raw: string): string {
|
||||
let path = raw;
|
||||
try {
|
||||
path = decodeURIComponent(raw);
|
||||
} catch {
|
||||
// malformed % sequences — keep raw
|
||||
}
|
||||
if (path.startsWith("/")) path = path.replace(/^\/+/, "");
|
||||
return path;
|
||||
}
|
||||
|
||||
function ArtifactChip({ path, title }: { path: string; title: string }) {
|
||||
const { t } = useTranslation();
|
||||
const file = path.split("/").pop() || path;
|
||||
const resolved = normalizeArtifactPath(path);
|
||||
const file = resolved.split("/").pop() || resolved;
|
||||
return (
|
||||
<button
|
||||
className="art-chip"
|
||||
data-testid="artifact-chip"
|
||||
title={path}
|
||||
title={resolved}
|
||||
onClick={() =>
|
||||
window.dispatchEvent(new CustomEvent(OPEN_ARTIFACT_EVENT, { detail: { path } }))
|
||||
window.dispatchEvent(new CustomEvent(OPEN_ARTIFACT_EVENT, { detail: { path: resolved } }))
|
||||
}
|
||||
>
|
||||
<span className="art-chip-ico">
|
||||
|
||||
Reference in New Issue
Block a user