Files
deerflow-code/frontend-web/src/core/artifacts/loader.ts
T

128 lines
3.9 KiB
TypeScript

import type { BaseStream } from "@langchain/langgraph-sdk/react";
import { apiFetch } from "../api";
import type { AgentThreadState } from "../threads";
import { resolveStreamingWriteFileArgs } from "./parse-partial-json";
import { urlOfArtifact } from "./utils";
export async function loadArtifactContent({
filepath,
threadId,
isMock,
publicShareCode,
}: {
filepath: string;
threadId: string;
isMock?: boolean;
publicShareCode?: string;
}) {
let enhancedFilepath = filepath;
if (filepath.endsWith(".skill")) {
enhancedFilepath = filepath + "/SKILL.md";
}
const url = urlOfArtifact({ filepath: enhancedFilepath, threadId, isMock, publicShareCode });
const response = await apiFetch(url);
const text = await response.text();
if (!response.ok) {
let detail = text.trim();
try {
const parsed = JSON.parse(text) as { detail?: unknown; message?: unknown };
const parsedDetail =
typeof parsed.detail === "string"
? parsed.detail
: typeof parsed.message === "string"
? parsed.message
: "";
if (parsedDetail) {
detail = parsedDetail;
}
} catch {
// Keep the raw response text as the diagnostic detail.
}
throw new Error(
`Failed to load artifact (${response.status})${detail ? `: ${detail}` : ""}`,
);
}
return { content: text, url };
}
export function loadArtifactContentFromToolCall({
url: urlString,
thread,
}: {
url: string;
thread: BaseStream<AgentThreadState>;
}) {
const url = new URL(urlString);
const toolCallId = url.searchParams.get("tool_call_id");
const messageId = url.searchParams.get("message_id");
if (messageId && toolCallId) {
const message = thread.messages.find((message) => message.id === messageId);
if (message?.type === "ai") {
const toolCall = message.tool_calls?.find(
(candidate) => candidate.id === toolCallId,
);
if (toolCall) {
const { content } = resolveStreamingWriteFileArgs(message, toolCall);
if (content) return content;
}
const chunkOnly = resolveStreamingWriteFileArgs(message, {
id: toolCallId,
name: "write_file",
});
if (chunkOnly.content) return chunkOnly.content;
}
}
}
function normalizeArtifactPath(path: string): string {
let normalized = path.trim();
const first = normalized[0];
const last = normalized.at(-1);
if (
normalized.length >= 2 &&
((first === '"' && last === '"') ||
(first === "'" && last === "'") ||
(first === "`" && last === "`"))
) {
normalized = normalized.slice(1, -1).trim();
}
return normalized.replace(/\\/g, "/").replace(/^\/+/, "");
}
/**
* Last-resort preview for an output whose artifact HTTP request is unavailable.
*
* A completed `write_file` can be selected before its backing sandbox file is
* visible through the artifacts endpoint (or when a deployment temporarily
* serves a stale mount). The original tool arguments still contain the
* Markdown, so use the newest matching write as a read-only display fallback.
*/
export function loadArtifactContentFromLatestWriteFile({
filepath,
thread,
}: {
filepath: string;
thread: BaseStream<AgentThreadState>;
}): string | undefined {
const normalizedFilepath = normalizeArtifactPath(filepath);
if (!normalizedFilepath) return undefined;
for (const message of [...thread.messages].reverse()) {
if (message.type !== "ai") continue;
for (const toolCall of [...(message.tool_calls ?? [])].reverse()) {
if (toolCall.name !== "write_file") continue;
const args = resolveStreamingWriteFileArgs(message, toolCall);
if (
args.path &&
args.content &&
normalizeArtifactPath(args.path) === normalizedFilepath
) {
return args.content;
}
}
}
return undefined;
}