fix(media): normalize Windows file URLs

This commit is contained in:
Hongpeng Jin 2026-01-29 16:46:21 -05:00
parent 4583f88626
commit b78aa7dc9e
2 changed files with 35 additions and 1 deletions

View File

@ -27,6 +27,27 @@ describe("splitMediaFromOutput", () => {
expect(result.text).toBe("");
});
it("normalizes Windows file URLs", () => {
const result = splitMediaFromOutput("MEDIA:file:///C:/Users/pete/My%20File.png");
const expected =
process.platform === "win32" ? "C:\\Users\\pete\\My File.png" : "C:/Users/pete/My File.png";
expect(result.mediaUrls).toEqual([expected]);
expect(result.text).toBe("");
});
it("accepts Windows drive letter paths", () => {
const result = splitMediaFromOutput("MEDIA:C:/Users/pete/My File.png");
expect(result.mediaUrls).toEqual(["C:/Users/pete/My File.png"]);
expect(result.text).toBe("");
});
it("accepts Windows drive letter paths with backslashes", () => {
const filePath = "C:\\Users\\pete\\My File.png";
const result = splitMediaFromOutput(`MEDIA:${filePath}`);
expect(result.mediaUrls).toEqual([filePath]);
expect(result.text).toBe("");
});
it("keeps audio_as_voice detection stable across calls", () => {
const input = "Hello [[audio_as_voice]]";
const first = splitMediaFromOutput(input);

View File

@ -1,5 +1,7 @@
// Shared helpers for parsing MEDIA tokens from command/stdout text.
import { fileURLToPath } from "node:url";
import { parseFenceSpans } from "../markdown/fences.js";
import { parseAudioTag } from "./audio-tags.js";
@ -7,7 +9,15 @@ import { parseAudioTag } from "./audio-tags.js";
export const MEDIA_TOKEN_RE = /\bMEDIA:\s*`?([^\n]+)`?/gi;
export function normalizeMediaSource(src: string) {
return src.startsWith("file://") ? src.replace("file://", "") : src;
if (!src.startsWith("file://")) return src;
try {
const filePath = fileURLToPath(src);
// Normalize drive-letter paths that appear as `/C:/...` on non-Windows platforms.
return filePath.replace(/^\/([a-zA-Z]:[\\/])/, "$1");
} catch {
// Best-effort fallback for malformed file URLs.
return src.replace("file://", "");
}
}
function cleanCandidate(raw: string) {
@ -19,6 +29,8 @@ function isValidMedia(candidate: string, opts?: { allowSpaces?: boolean }) {
if (candidate.length > 4096) return false;
if (!opts?.allowSpaces && /\s/.test(candidate)) return false;
if (/^https?:\/\//i.test(candidate)) return true;
if (/^[a-zA-Z]:[\\/]/.test(candidate)) return true;
if (candidate.startsWith("\\\\")) return true;
if (candidate.startsWith("/")) return true;
if (candidate.startsWith("./")) return true;
if (candidate.startsWith("../")) return true;
@ -118,6 +130,7 @@ export function splitMediaFromOutput(raw: string): {
trimmedPayload.startsWith("./") ||
trimmedPayload.startsWith("../") ||
trimmedPayload.startsWith("~") ||
/^[a-zA-Z]:[\\/]/.test(trimmedPayload) ||
trimmedPayload.startsWith("file://");
if (
!unwrapped &&