diff --git a/frontend-web/src/core/scheduled-tasks/api.ts b/frontend-web/src/core/scheduled-tasks/api.ts index 248e0fe3..c7b1a3af 100644 --- a/frontend-web/src/core/scheduled-tasks/api.ts +++ b/frontend-web/src/core/scheduled-tasks/api.ts @@ -2,12 +2,15 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; import type { + HtmlPageFavorite, PublishedScheduledTask, ScheduledTask, ScheduledTaskDeliveryProfile, + ScheduledTaskExecutionContext, ScheduledTaskResultFileContent, ScheduledTaskRun, ScheduledTaskRunFileUpdateResponse, + ScheduledTaskRunHtml, ScheduledTaskRunMessagesResponse, ScheduledTaskSubscription, SchedulerThread, @@ -52,6 +55,7 @@ export async function createScheduledTask(data: { timezone?: string | null; execution_agent_name?: string | null; require_markdown?: boolean; + execution_context?: ScheduledTaskExecutionContext | null; }) { return parseResponse( await apiFetch(baseUrl(), { @@ -212,6 +216,7 @@ export async function updateScheduledTask( enabled: boolean; execution_agent_name: string | null; require_markdown: boolean; + execution_context: ScheduledTaskExecutionContext | null; }>, ) { return parseResponse( @@ -368,3 +373,47 @@ export async function adminTriggerScheduledTask(taskId: string) { }), ); } + +// --- HTML page tasks --- + +/** Fetch the generated HTML document for an ``html_page`` run. */ +export async function getScheduledTaskRunHtml(runId: string) { + return parseResponse( + await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/html`), + ); +} + +const favoritesUrl = () => `${getBackendBaseURL()}/api/html-page-favorites`; + +export async function listHtmlPageFavorites() { + return parseResponse(await apiFetch(favoritesUrl())); +} + +export async function getHtmlPageFavorite(favoriteId: string) { + return parseResponse( + await apiFetch(`${favoritesUrl()}/${encodeURIComponent(favoriteId)}`), + ); +} + +export async function createHtmlPageFavorite(data: { + page_id: string; + title?: string | null; + description?: string | null; + tags?: string[]; +}) { + return parseResponse( + await apiFetch(favoritesUrl(), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }), + ); +} + +export async function deleteHtmlPageFavorite(favoriteId: string) { + return parseResponse<{ success: boolean }>( + await apiFetch(`${favoritesUrl()}/${encodeURIComponent(favoriteId)}`, { + method: "DELETE", + }), + ); +} diff --git a/frontend-web/src/core/scheduled-tasks/hooks.ts b/frontend-web/src/core/scheduled-tasks/hooks.ts index 454198fe..3f4f1b3a 100644 --- a/frontend-web/src/core/scheduled-tasks/hooks.ts +++ b/frontend-web/src/core/scheduled-tasks/hooks.ts @@ -10,14 +10,18 @@ import { adminListScheduledTaskRuns, adminTriggerScheduledTask, adminUpdateScheduledTask, + createHtmlPageFavorite, createScheduledTask, + deleteHtmlPageFavorite, deleteScheduledTask, deleteScheduledTaskRun, getSchedulerThread, getScheduledTaskDeliveryProfile, getScheduledTaskRun, + getScheduledTaskRunHtml, getScheduledTaskRunMessages, listAllScheduledTasks, + listHtmlPageFavorites, listPublishedScheduledTasks, listScheduledTaskRuns, listScheduledTasks, @@ -112,6 +116,42 @@ export function useScheduledTaskRunMessages( }); } +/** Generated HTML for an ``html_page`` run; polls while the run is in flight. */ +export function useScheduledTaskRunHtml( + runId?: string | null, + options: { enabled?: boolean; live?: boolean } = {}, +) { + return useQuery({ + queryKey: ["scheduled-tasks", "runs", runId, "html"], + queryFn: () => getScheduledTaskRunHtml(runId!), + enabled: !!runId && (options.enabled ?? true), + refetchOnWindowFocus: false, + retry: false, + refetchInterval: options.live ? 5000 : false, + }); +} + +/** The current user's bookmarked HTML pages. */ +export function useHtmlPageFavorites(enabled: boolean = true) { + return useQuery({ + queryKey: ["html-page-favorites"], + queryFn: listHtmlPageFavorites, + enabled, + refetchOnWindowFocus: false, + }); +} + +/** Create / delete HTML page favorites. */ +export function useHtmlPageFavoriteActions() { + const queryClient = useQueryClient(); + const invalidate = () => + queryClient.invalidateQueries({ queryKey: ["html-page-favorites"] }); + return { + create: useMutation({ mutationFn: createHtmlPageFavorite, onSuccess: invalidate }), + remove: useMutation({ mutationFn: deleteHtmlPageFavorite, onSuccess: invalidate }), + }; +} + export function useScheduledTaskDeliveryProfile() { return useQuery({ queryKey: ["scheduled-tasks", "delivery-profile"], diff --git a/frontend-web/src/core/scheduled-tasks/types.ts b/frontend-web/src/core/scheduled-tasks/types.ts index 284a5801..3c3cc8c6 100644 --- a/frontend-web/src/core/scheduled-tasks/types.ts +++ b/frontend-web/src/core/scheduled-tasks/types.ts @@ -11,6 +11,26 @@ export type ScheduledTaskSubscription = { updated_at?: string | null; }; +/** Task kinds. A bare/absent kind is the regular agent task. */ +export type ScheduledTaskKind = "agent" | "html_page"; + +/** Config for an ``html_page`` task, persisted inside ``execution_context``. + * Data gathering is delegated to the agent's own search skills, so there is no + * explicit search query / data source — the page prompt drives everything. */ +export type HtmlPageConfig = { + page_prompt?: string | null; + negative_prompt?: string | null; + reference_favorite_id?: string | null; + model_name?: string | null; +}; + +export type ScheduledTaskExecutionContext = { + require_markdown?: boolean; + task_kind?: ScheduledTaskKind; + html_config?: HtmlPageConfig; + [key: string]: unknown; +}; + export type ScheduledTask = { task_id: string; name: string; @@ -21,11 +41,11 @@ export type ScheduledTask = { timezone: string; enabled: boolean; /** - * Free-form execution context persisted with the task. ``require_markdown`` - * lives here (rather than a dedicated column) so the feature needs no schema - * migration. + * Free-form execution context persisted with the task. ``require_markdown``, + * ``task_kind`` and ``html_config`` live here (rather than dedicated columns) + * so the feature needs no schema migration. */ - execution_context?: { require_markdown?: boolean; [key: string]: unknown } | null; + execution_context?: ScheduledTaskExecutionContext | null; next_run_at?: string | null; last_run_at?: string | null; last_status?: string | null; @@ -70,7 +90,7 @@ export type ScheduledTaskResultFile = { path?: string | null; mime_type?: string | null; size?: number | null; - kind?: "markdown" | "attachment" | "artifact" | null; + kind?: "markdown" | "attachment" | "artifact" | "html" | null; }; export type ScheduledTaskResultFileContent = { @@ -86,9 +106,42 @@ export type ScheduledTaskResultFileContent = { export type ScheduledTaskRunResult = { title: string; content: string; - format: "markdown" | "text" | "json" | "mixed"; + format: "markdown" | "text" | "json" | "mixed" | "html"; files: ScheduledTaskResultFile[]; created_at: string; + /** Present for ``html_page`` runs: the full generated HTML document. */ + html?: string | null; + style_summary?: string | null; + layout_summary?: string | null; +}; + +/** Response of the run/latest HTML preview endpoints. */ +export type ScheduledTaskRunHtml = { + run_id: string; + task_id?: string | null; + title?: string | null; + html?: string | null; + style_summary?: string | null; + layout_summary?: string | null; + page_id?: string | null; + created_at?: string | null; +}; + +/** A user-bookmarked generated HTML page. */ +export type HtmlPageFavorite = { + id: string; + user_id: string; + page_id: string; + title: string; + description?: string | null; + tags?: string[]; + html_snapshot?: string; + style_summary?: string | null; + layout_summary?: string | null; + content_structure_summary?: string | null; + reuse_prompt?: string | null; + created_at?: string | null; + updated_at?: string | null; }; export type ScheduledTaskRun = { diff --git a/frontend-web/src/pages/PublicScheduledTaskPage.tsx b/frontend-web/src/pages/PublicScheduledTaskPage.tsx index ef623b9d..87300e09 100644 --- a/frontend-web/src/pages/PublicScheduledTaskPage.tsx +++ b/frontend-web/src/pages/PublicScheduledTaskPage.tsx @@ -68,9 +68,11 @@ type PublicRunFile = { type PublicRunResult = { title: string; content: string; - format: "markdown" | "text" | "json" | "mixed"; + format: "markdown" | "text" | "json" | "mixed" | "html"; files: PublicRunFile[]; created_at: string; + /** Present for ``html_page`` runs: the full generated HTML document. */ + html?: string | null; }; type PublicRun = { @@ -126,6 +128,12 @@ async function fetchPublicRunFile(runId: string, index: number): Promise { + return fetchJson(`${publicBaseUrl()}/runs/${encodeURIComponent(runId)}/html`); +} + async function updatePublicRunFile( runId: string, index: number, @@ -296,6 +304,13 @@ export default function PublicScheduledTaskPage() { if (run.status !== "succeeded") return false; const result = run.result; if (!result) return false; + // HTML page runs always have something to render (the iframe preview). + const isHtml = + result.format === "html" || + (result.files ?? []).some( + (file) => file.kind === "html" || (file.name ?? "").toLowerCase().endsWith(".html"), + ); + if (isHtml) return true; const hasBody = !!result.content && result.content.trim().length > 0; const hasMarkdownFile = (result.files ?? []).some( (file) => isMarkdownFile(file) && isFileAvailable(file), @@ -550,6 +565,23 @@ type ContentTab = { function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) { const queryClient = useQueryClient(); + // HTML page run: render a sandboxed iframe preview instead of the MD tabs. + const isHtmlRun = + run.result?.format === "html" || + (run.result?.files ?? []).some( + (f) => f.kind === "html" || (f.name ?? "").toLowerCase().endsWith(".html"), + ); + // Prefer the inline copy; fall back to the LONGTEXT-backed /html endpoint for + // large pages whose HTML was not inlined into result_json. + const { data: htmlData } = useQuery({ + queryKey: ["public-scheduled-task", "html", run.id], + queryFn: () => fetchPublicRunHtml(run.id), + enabled: isHtmlRun && !run.result?.html, + refetchOnWindowFocus: false, + retry: false, + }); + const htmlContent = run.result?.html ?? htmlData?.html ?? null; + // Skip files whose archive is missing on disk — otherwise the public viewer // would render a tab that 404s with "Result file is no longer available". const markdownFiles = useMemo( @@ -719,56 +751,74 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) { 下载原文件 )} - - {isEditing ? ( - <> + {!isHtmlRun && ( + + )} + {!isHtmlRun && + (isEditing ? ( + <> + + + + ) : ( - - - ) : ( - - )} + ))} - {tabs.length === 0 ? ( + {isHtmlRun ? ( +
+ {htmlContent ? ( +