feat(webdav): add WebDAV synchronization configuration and functionality

This commit is contained in:
HouYunFei
2026-06-08 14:59:30 +08:00
parent dd9347e85d
commit 39a1e859b1
10 changed files with 845 additions and 4 deletions
@@ -341,7 +341,7 @@ function NodeContent(props: NodeContentRendererProps) {
if (props.node.metadata?.status === "error") return <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} />;
const Renderer = nodeContentRenderers[props.node.type];
return <Renderer {...props} />;
return Renderer ? <Renderer {...props} /> : <UnknownNodeContent theme={props.theme} />;
}
const nodeContentRenderers = {
@@ -382,6 +382,14 @@ function ErrorContent({ node, theme, onRetry }: Pick<NodeContentRendererProps, "
);
}
function UnknownNodeContent({ theme }: Pick<NodeContentRendererProps, "theme">) {
return (
<div className="flex h-full w-full items-center justify-center text-sm" style={{ color: theme.node.placeholder }}>
未知节点
</div>
);
}
function TextContent({ node, theme, isEditingContent, textareaRef, mentionReferences, onContentChange, onStopEditing, onGenerateImage }: NodeContentRendererProps) {
return (
<div className="flex h-full w-full flex-col overflow-hidden pt-8">
@@ -28,6 +28,7 @@ type CanvasStore = {
openProject: (id: string) => CanvasProject | null;
renameProject: (id: string, title: string) => void;
deleteProjects: (ids: string[]) => void;
replaceProjects: (projects: CanvasProject[]) => void;
updateProject: (id: string, patch: Partial<Pick<CanvasProject, "nodes" | "connections" | "chatSessions" | "activeChatId" | "backgroundMode" | "showImageInfo" | "viewport">>) => void;
};
@@ -112,6 +113,7 @@ export const useCanvasStore = create<CanvasStore>()(
const projects = state.projects.filter((project) => !ids.includes(project.id));
return { projects };
}),
replaceProjects: (projects) => set({ projects }),
updateProject: (id, patch) =>
set((state) => ({
projects: state.projects.map((project) => (project.id === id ? { ...project, ...patch, updatedAt: new Date().toISOString() } : project)),
+59
View File
@@ -0,0 +1,59 @@
import { NextRequest } from "next/server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const WEBDAV_PROXY_TIMEOUT_MS = 120000;
export async function POST(request: NextRequest) {
const target = request.headers.get("x-webdav-target") || "";
const method = (request.headers.get("x-webdav-method") || "GET").toUpperCase();
if (!target) return new Response("Missing x-webdav-target", { status: 400 });
let url: URL;
try {
url = new URL(target);
} catch {
return new Response("Invalid x-webdav-target", { status: 400 });
}
if (url.protocol !== "http:" && url.protocol !== "https:") return new Response("Unsupported WebDAV target", { status: 400 });
const headers = new Headers();
copyHeader(request, headers, "x-webdav-authorization", "Authorization");
copyHeader(request, headers, "x-webdav-depth", "Depth");
copyHeader(request, headers, "x-webdav-destination", "Destination");
copyHeader(request, headers, "x-webdav-overwrite", "Overwrite");
copyHeader(request, headers, "x-webdav-content-type", "Content-Type");
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), WEBDAV_PROXY_TIMEOUT_MS);
try {
const body = method === "GET" || method === "HEAD" ? undefined : await request.arrayBuffer();
console.log(`[webdav-proxy] ${method} ${url.href} ${body?.byteLength || 0}B`);
const response = await fetch(url, { method, headers, body: body?.byteLength ? body : undefined, signal: controller.signal });
console.log(`[webdav-proxy] ${method} ${url.href} -> ${response.status}`);
return new Response(method === "HEAD" ? null : response.body, {
status: response.status,
headers: responseHeaders(response.headers),
});
} catch (error) {
if (error instanceof Error && error.name === "AbortError") return new Response("WebDAV proxy timeout", { status: 504 });
return new Response(error instanceof Error ? error.message : "WebDAV proxy error", { status: 502 });
} finally {
clearTimeout(timer);
}
}
function copyHeader(request: NextRequest, headers: Headers, from: string, to: string) {
const value = request.headers.get(from);
if (value) headers.set(to, value);
}
function responseHeaders(headers: Headers) {
const result = new Headers();
["content-type", "etag", "last-modified", "dav"].forEach((key) => {
const value = headers.get(key);
if (value) result.set(key, value);
});
return result;
}