mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(artifacts): preserve literal percent sequences in file names (#5817)
This commit is contained in:
parent
5804ee1a7e
commit
ad58e8aa7d
@ -1749,6 +1749,10 @@ an explicit **Load full file** action before fetching the remainder or mounting
|
||||
the full code editor. Active HTML, XHTML, and SVG artifacts remain forced
|
||||
downloads at the Gateway boundary.
|
||||
|
||||
Artifact previews and downloads preserve literal percent sequences in file names:
|
||||
`report%20final.md` and `report final.md` remain distinct files. Markdown links
|
||||
continue to accept URL-encoded paths.
|
||||
|
||||
The `write_file` guidance reflects the active model's output token limit, including custom-agent and thinking-mode overrides. For longer documents, the agent is guided to write sections with `append=True`; models without a known limit receive no numeric budget hint.
|
||||
|
||||
With `AioSandboxProvider`, shell execution runs inside isolated containers. With `LocalSandboxProvider`, file tools still map to per-thread directories on the host, but host `bash` is disabled by default because it is not a secure isolation boundary. Re-enable host bash only for fully trusted local workflows. Host bash commands have a wall-clock timeout, and long-lived processes should be started in the background with output redirected to a workspace log. On Windows, Git Bash/MSYS argument-conversion exclusions are limited to safe non-root virtual path prefixes, so host-native CLI launchers retain their normal MSYS compatibility. When the local sandbox falls back to PowerShell, it captures output as UTF-8 so CJK text does not depend on the Gateway host locale.
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
### Data Flow
|
||||
|
||||
Artifact state, upload metadata, and workspace changes carry raw filesystem
|
||||
paths. `urlOfArtifact` and `resolveArtifactURL` encode those paths without
|
||||
decoding literal percent sequences. Only Markdown destinations are URL inputs:
|
||||
`resolveMarkdownArtifactURL` decodes once at that boundary, and relative image
|
||||
resolution matches the decoded name against raw artifact paths before encoding.
|
||||
|
||||
1. Optional composer helpers such as `core/input-polish` can rewrite the local draft before submission, and `core/voice-input` can transcribe browser microphone input into that same local draft; confirmed user input then flows to thread hooks (`core/threads/hooks.ts`) → LangGraph SDK streaming
|
||||
2. Stream events update thread state (messages, artifacts, todos, goal). The main thread stream uses the LangGraph SDK's `throttle: true` mode so updates received in the same macrotask coalesce before React is notified; do not replace it with a numeric delay without validating the SDK's trailing-debounce behavior on a continuous stream.
|
||||
File-tool artifact auto-open work must run in an effect with timer cleanup; never schedule timers while rendering streamed `write_file` or `str_replace` updates.
|
||||
|
||||
@ -21,9 +21,11 @@ function splitPathSuffix(src: string) {
|
||||
}
|
||||
|
||||
function encodeArtifactPath(filepath: string) {
|
||||
// This is a filesystem path, not a URL: a literal "%20" must stay part
|
||||
// of the filename after the server decodes the request path once.
|
||||
return filepath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(decodePathSegment(segment)))
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
@ -98,7 +100,9 @@ export function resolveArtifactURL(absolutePath: string, threadId: string) {
|
||||
|
||||
export function resolveMarkdownArtifactURL(src: string, threadId: string) {
|
||||
const { path, suffix } = splitPathSuffix(src);
|
||||
return `${resolveArtifactURL(path, threadId)}${suffix}`;
|
||||
// Markdown destinations already carry URL escapes; decode only at this
|
||||
// boundary before handing the raw path to the artifact URL builder.
|
||||
return `${resolveArtifactURL(decodeRelativeArtifactPath(path), threadId)}${suffix}`;
|
||||
}
|
||||
|
||||
export function resolveMessageImageURL(
|
||||
|
||||
@ -104,10 +104,61 @@ describe("artifact URL helpers", () => {
|
||||
expect(
|
||||
resolveArtifactURL("/mnt/user-data/outputs/a%23b%3F.txt", "thread-1"),
|
||||
).toBe(
|
||||
"/api/threads/thread-1/artifacts/mnt/user-data/outputs/a%23b%3F.txt",
|
||||
"/api/threads/thread-1/artifacts/mnt/user-data/outputs/a%2523b%253F.txt",
|
||||
);
|
||||
});
|
||||
|
||||
test.each(["report%20final.md", "a%2Fb.txt", "a%2520b.png"])(
|
||||
"preserves literal percent escapes in the raw artifact path %s",
|
||||
async (filename) => {
|
||||
const { resolveArtifactURL, urlOfArtifact } =
|
||||
await loadFreshArtifactUtils();
|
||||
const filepath = `/mnt/user-data/outputs/${filename}`;
|
||||
for (const url of [
|
||||
urlOfArtifact({ filepath, threadId: "thread-1" }),
|
||||
urlOfArtifact({ filepath, threadId: "thread-1", download: true }),
|
||||
urlOfArtifact({ filepath, threadId: "thread-1", isMock: true }),
|
||||
resolveArtifactURL(filepath, "thread-1"),
|
||||
]) {
|
||||
const pathname = new URL(url, "http://localhost").pathname;
|
||||
expect(decodeURIComponent(pathname.split("/artifacts")[1]!)).toBe(
|
||||
filepath,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("decodes Markdown URLs once before resolving the raw artifact path", async () => {
|
||||
const { resolveMarkdownArtifactURL, resolveMessageImageURL } =
|
||||
await loadFreshArtifactUtils();
|
||||
const rawPath = "/mnt/user-data/outputs/report%20final.png";
|
||||
const expected =
|
||||
"/api/threads/thread-1/artifacts/mnt/user-data/outputs/report%2520final.png?v=2#detail";
|
||||
|
||||
expect(
|
||||
resolveMarkdownArtifactURL(
|
||||
"/mnt/user-data/outputs/report%2520final.png?v=2#detail",
|
||||
"thread-1",
|
||||
),
|
||||
).toBe(expected);
|
||||
expect(
|
||||
resolveMessageImageURL("report%2520final.png?v=2#detail", "thread-1", [
|
||||
rawPath,
|
||||
]),
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
test("preserves percent escapes in static-demo file names", async () => {
|
||||
setEnv("NEXT_PUBLIC_STATIC_WEBSITE_ONLY", "true");
|
||||
const { urlOfArtifact } = await loadFreshArtifactUtils();
|
||||
expect(
|
||||
urlOfArtifact({
|
||||
filepath: "/mnt/user-data/outputs/report%20final.md",
|
||||
threadId: "thread-1",
|
||||
}),
|
||||
).toBe("/demo/threads/thread-1/user-data/outputs/report%2520final.md");
|
||||
});
|
||||
|
||||
test("preserves markdown query and fragment suffixes on artifact URLs", async () => {
|
||||
const { resolveMarkdownArtifactURL, resolveMessageImageURL } =
|
||||
await loadFreshArtifactUtils();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user