Files
deerflow-code/frontend-web/src/core/sites/api.ts
T

245 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Web App 站点产物 API 客户端:能力探测 / 站点列表 / 构建 / 预览签名。
*
* 所有接口在后端能力不可用时返回 `{ available: false, reasons }`(HTTP 200),
* 调用方据此优雅降级——入口按钮不渲染、预览页显示未启用态。
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
export interface WebAppCapabilityCheck {
ok: boolean;
detail: string;
}
export interface WebAppCapability {
available: boolean;
enabled: boolean;
template_dir: string;
node_bin: string;
checks: Record<string, WebAppCapabilityCheck>;
reasons?: string[];
}
export interface SiteNavItem {
page: string;
label: string;
}
export interface SiteDistInfo {
exists: boolean;
file_count?: number;
total_bytes?: number;
mtime?: number | null;
}
export interface SiteBuildResult {
success?: boolean;
error?: string;
duration_ms?: number;
stdout_tail?: string;
stderr_tail?: string;
finished_at?: number;
}
export interface SiteBuildJob {
job_id: string;
status: "running" | "success" | "failed";
started_at?: number;
finished_at?: number | null;
error?: string;
result?: SiteBuildResult;
}
export interface SiteInfo {
name: string;
title: string;
description: string;
nav: SiteNavItem[];
map: Record<string, unknown>;
manifest_mtime: number | null;
dist: SiteDistInfo;
last_build: SiteBuildResult | null;
build_job?: SiteBuildJob;
}
export interface SiteListResponse {
available: boolean;
enabled?: boolean;
reasons?: string[];
sites?: SiteInfo[];
}
/** 跨对话站点 gallery 条目:站点信息 + 来源会话 */
export interface SiteGalleryItem extends SiteInfo {
thread_id: string;
thread_title: string;
build_job?: SiteBuildJob;
}
export interface SiteGalleryResponse {
available: boolean;
enabled?: boolean;
reasons?: string[];
sites?: SiteGalleryItem[];
}
export interface SiteBuildStatusResponse {
available: boolean;
job_id?: string;
status?: "running" | "success" | "failed" | "idle";
started_at?: number;
finished_at?: number | null;
error?: string;
result?: SiteBuildResult;
last_build?: SiteBuildResult | null;
}
export interface SitePreviewUrlResponse {
available: boolean;
reasons?: string[];
url_path?: string;
token?: string;
expires_at?: number;
}
async function readJson<T>(res: Response, ctx: string): Promise<T> {
if (!res.ok) {
let detail = `${ctx} 失败(HTTP ${res.status})`;
try {
const body = (await res.json()) as { detail?: string };
if (body?.detail) detail = String(body.detail);
} catch {
// keep default
}
throw new Error(detail);
}
return (await res.json()) as T;
}
const baseUrl = () => `${getBackendBaseURL()}/api`;
export async function fetchWebAppCapability(): Promise<WebAppCapability> {
const res = await apiFetch(`${baseUrl()}/web-app/capability`);
return readJson<WebAppCapability>(res, "站点能力探测");
}
export async function listSites(threadId: string): Promise<SiteListResponse> {
const res = await apiFetch(`${baseUrl()}/threads/${encodeURIComponent(threadId)}/sites`);
return readJson<SiteListResponse>(res, "站点列表");
}
export async function listAllSites(): Promise<SiteGalleryResponse> {
const res = await apiFetch(`${baseUrl()}/web-app/sites`);
return readJson<SiteGalleryResponse>(res, "站点 gallery");
}
export async function buildSite(
threadId: string,
siteName: string,
): Promise<{ available: boolean; job_id?: string; status?: string; reasons?: string[] }> {
const res = await apiFetch(
`${baseUrl()}/threads/${encodeURIComponent(threadId)}/sites/${encodeURIComponent(siteName)}/build`,
{ method: "POST" },
);
return readJson(res, "触发构建");
}
export async function fetchSiteBuildStatus(
threadId: string,
siteName: string,
): Promise<SiteBuildStatusResponse> {
const res = await apiFetch(
`${baseUrl()}/threads/${encodeURIComponent(threadId)}/sites/${encodeURIComponent(siteName)}/build-status`,
);
return readJson<SiteBuildStatusResponse>(res, "构建状态");
}
export async function mintSitePreviewUrl(
threadId: string,
siteName: string,
): Promise<SitePreviewUrlResponse> {
const res = await apiFetch(
`${baseUrl()}/threads/${encodeURIComponent(threadId)}/sites/${encodeURIComponent(siteName)}/preview-url`,
{ method: "POST" },
);
return readJson<SitePreviewUrlResponse>(res, "签发预览地址");
}
/** 站点预览页路由(HashRouter 顶层,独立风格页面) */
export function siteStudioRoute(threadId: string, siteName: string): string {
return `/sites/${encodeURIComponent(threadId)}/${encodeURIComponent(siteName)}`;
}
const CAPABILITY_KEY = ["web-app", "capability"] as const;
const SITES_KEY = (threadId: string) => ["web-app", "sites", threadId] as const;
export function useWebAppCapability(enabled = true) {
return useQuery({
queryKey: CAPABILITY_KEY,
queryFn: fetchWebAppCapability,
enabled,
staleTime: 60_000,
gcTime: 5 * 60_000,
retry: 1,
refetchOnWindowFocus: false,
});
}
export function useSites(threadId: string | undefined, options?: { refetchInterval?: number }) {
return useQuery({
queryKey: SITES_KEY(threadId ?? ""),
queryFn: () => listSites(threadId as string),
enabled: !!threadId,
refetchInterval: options?.refetchInterval ?? false,
refetchOnWindowFocus: false,
});
}
export function useAllSites() {
return useQuery({
queryKey: ["web-app", "all-sites"] as const,
queryFn: listAllSites,
staleTime: 15_000,
refetchOnWindowFocus: false,
});
}
export function useSiteBuildStatus(
threadId: string | undefined,
siteName: string | undefined,
options?: {
refetchInterval?: number | false | ((query: { state: { data: SiteBuildStatusResponse | undefined } }) => number | false);
},
) {
return useQuery({
queryKey: ["web-app", "build-status", threadId ?? "", siteName ?? ""] as const,
queryFn: () => fetchSiteBuildStatus(threadId as string, siteName as string),
enabled: !!threadId && !!siteName,
refetchInterval: options?.refetchInterval ?? false,
refetchOnWindowFocus: false,
});
}
export function useBuildSiteMutation(threadId: string | undefined, siteName: string | undefined) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => buildSite(threadId as string, siteName as string),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SITES_KEY(threadId ?? "") });
void queryClient.invalidateQueries({
queryKey: ["web-app", "build-status", threadId ?? "", siteName ?? ""],
});
},
});
}
export function useMintPreviewUrlMutation(threadId: string | undefined, siteName: string | undefined) {
return useMutation({
mutationFn: () => mintSitePreviewUrl(threadId as string, siteName as string),
});
}