后端: - 新增 task_kind=html_page 任务类型,与普通定时任务并存,配置存 execution_context(无需迁移) - 执行流水线:智能体检索数据 -> 选定模型渲染 HTML -> 清洗/校验 -> 失败自动修复 -> 完整性闸门(缺 doctype/html/head/body/title 或残留脚本即失败) -> 归档 index.html -> 落库 - 新增 scheduled_html_pages / html_page_favorites 表与 store;收藏去重(不可重复)、可删除、收藏时提取可复用风格 - HTML 预览接口(鉴权 + 公开)、收藏接口 - 渲染注入已启用的 HTML 技能(metadata.kind=html_page);全程使用用户所选模型,去除硬编码 GLM-5 - 定时任务跳过 TitleMiddleware,避免标题生成回退默认模型 前端: - 创建/编辑弹窗增加任务类型与 HTML 配置(页面要求/限制/参考收藏页/模型下拉) - 运行详情页与公开页 iframe 沙箱预览 HTML;收藏按钮(已收藏禁用) - 参考收藏页可选可删 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
420 lines
13 KiB
TypeScript
420 lines
13 KiB
TypeScript
import { apiFetch } from "@/core/api";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
|
|
import type {
|
|
HtmlPageFavorite,
|
|
PublishedScheduledTask,
|
|
ScheduledTask,
|
|
ScheduledTaskDeliveryProfile,
|
|
ScheduledTaskExecutionContext,
|
|
ScheduledTaskResultFileContent,
|
|
ScheduledTaskRun,
|
|
ScheduledTaskRunFileUpdateResponse,
|
|
ScheduledTaskRunHtml,
|
|
ScheduledTaskRunMessagesResponse,
|
|
ScheduledTaskSubscription,
|
|
SchedulerThread,
|
|
} from "./types";
|
|
|
|
const baseUrl = () => `${getBackendBaseURL()}/api/scheduled-tasks`;
|
|
|
|
async function parseResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
const error = await response
|
|
.json()
|
|
.catch(() => ({ detail: "Request failed" }));
|
|
throw new Error(error.detail ?? "Request failed");
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export async function getSchedulerThread() {
|
|
return parseResponse<SchedulerThread>(await apiFetch(`${baseUrl()}/thread`));
|
|
}
|
|
|
|
export interface ListScheduledTasksParams {
|
|
search?: string;
|
|
tagIds?: string[];
|
|
}
|
|
|
|
export async function listScheduledTasks(params: ListScheduledTasksParams = {}) {
|
|
const qs = new URLSearchParams();
|
|
if (params.search) qs.set("search", params.search);
|
|
for (const id of params.tagIds ?? []) qs.append("tag_ids", id);
|
|
const query = qs.toString();
|
|
return parseResponse<ScheduledTask[]>(
|
|
await apiFetch(`${baseUrl()}${query ? `?${query}` : ""}`),
|
|
);
|
|
}
|
|
|
|
export async function createScheduledTask(data: {
|
|
name: string;
|
|
description?: string | null;
|
|
prompt: string;
|
|
schedule: string;
|
|
timezone?: string | null;
|
|
execution_agent_name?: string | null;
|
|
require_markdown?: boolean;
|
|
execution_context?: ScheduledTaskExecutionContext | null;
|
|
}) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(baseUrl(), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function listScheduledTaskRuns(taskId: string) {
|
|
return parseResponse<ScheduledTaskRun[]>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/runs`),
|
|
);
|
|
}
|
|
|
|
export async function getScheduledTaskRun(runId: string) {
|
|
return parseResponse<ScheduledTaskRun>(
|
|
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}`),
|
|
);
|
|
}
|
|
|
|
export async function deleteScheduledTaskRun(runId: string) {
|
|
return parseResponse<{ success: boolean }>(
|
|
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}`, {
|
|
method: "DELETE",
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** Agent messages for a run's underlying agent run — used to inspect a stuck/running run. */
|
|
export async function getScheduledTaskRunMessages(runId: string) {
|
|
return parseResponse<ScheduledTaskRunMessagesResponse>(
|
|
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/messages`),
|
|
);
|
|
}
|
|
|
|
export function scheduledTaskRunDownloadUrl(runId: string) {
|
|
return `${baseUrl()}/runs/${encodeURIComponent(runId)}/download`;
|
|
}
|
|
|
|
export function scheduledTaskRunFileDownloadUrl(runId: string, fileIndex: number) {
|
|
return `${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}/download`;
|
|
}
|
|
|
|
export async function getScheduledTaskRunFileContent(runId: string, fileIndex: number) {
|
|
return parseResponse<ScheduledTaskResultFileContent>(
|
|
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`),
|
|
);
|
|
}
|
|
|
|
export async function updateScheduledTaskRunFile(
|
|
runId: string,
|
|
fileIndex: number,
|
|
content: string,
|
|
) {
|
|
return parseResponse<ScheduledTaskRunFileUpdateResponse>(
|
|
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/files/${fileIndex}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ content }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
function filenameFromContentDisposition(header: string | null): string | null {
|
|
if (!header) return null;
|
|
const star = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(header);
|
|
if (star) {
|
|
const raw = star[1].replace(/["']/g, "").trim();
|
|
try {
|
|
return decodeURIComponent(raw);
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|
|
const plain = /filename="?([^";\n]+)"?/i.exec(header);
|
|
return plain ? plain[1].trim() : null;
|
|
}
|
|
|
|
async function downloadResponse(response: Response, fallbackFilename: string) {
|
|
if (!response.ok) {
|
|
const error = await response
|
|
.json()
|
|
.catch(() => ({ detail: "Download failed" }));
|
|
const detail = error.detail;
|
|
if (typeof detail === "string") throw new Error(detail);
|
|
if (
|
|
detail &&
|
|
typeof detail === "object" &&
|
|
"message" in detail &&
|
|
typeof detail.message === "string"
|
|
) {
|
|
throw new Error(detail.message);
|
|
}
|
|
throw new Error("Download failed");
|
|
}
|
|
const blob = await response.blob();
|
|
const contentType = response.headers.get("Content-Type") ?? blob.type;
|
|
const fallbackByType =
|
|
contentType.includes("application/zip") || contentType.includes("application/x-zip-compressed")
|
|
? fallbackFilename.replace(/\.[^.]+$/, ".zip")
|
|
: fallbackFilename;
|
|
const filename =
|
|
filenameFromContentDisposition(response.headers.get("Content-Disposition")) ??
|
|
fallbackByType;
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
anchor.download = filename;
|
|
anchor.rel = "noopener";
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
|
}
|
|
|
|
export async function downloadScheduledTaskRun(runId: string) {
|
|
await downloadResponse(
|
|
await apiFetch(scheduledTaskRunDownloadUrl(runId)),
|
|
"scheduled-task-result.zip",
|
|
);
|
|
}
|
|
|
|
export async function downloadScheduledTaskRunFile(runId: string, fileIndex: number) {
|
|
await downloadResponse(
|
|
await apiFetch(scheduledTaskRunFileDownloadUrl(runId, fileIndex)),
|
|
"scheduled-task-file",
|
|
);
|
|
}
|
|
|
|
export async function getScheduledTaskDeliveryProfile() {
|
|
return parseResponse<ScheduledTaskDeliveryProfile>(
|
|
await apiFetch(`${baseUrl()}/delivery-profile`),
|
|
);
|
|
}
|
|
|
|
export async function updateScheduledTaskDeliveryProfile(data: {
|
|
element_user_id?: string | null;
|
|
}) {
|
|
return parseResponse<ScheduledTaskDeliveryProfile>(
|
|
await apiFetch(`${baseUrl()}/delivery-profile`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function updateScheduledTask(
|
|
taskId: string,
|
|
data: Partial<{
|
|
name: string;
|
|
description: string | null;
|
|
prompt: string;
|
|
schedule: string;
|
|
timezone: string;
|
|
enabled: boolean;
|
|
execution_agent_name: string | null;
|
|
require_markdown: boolean;
|
|
execution_context: ScheduledTaskExecutionContext | null;
|
|
}>,
|
|
) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function deleteScheduledTask(taskId: string) {
|
|
return parseResponse<{ success: boolean }>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}`, {
|
|
method: "DELETE",
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function pauseScheduledTask(taskId: string) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/pause`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function resumeScheduledTask(taskId: string) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/resume`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function triggerScheduledTask(taskId: string) {
|
|
return parseResponse<{ success: boolean; task: ScheduledTask }>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/trigger`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function listPublishedScheduledTasks(params: { limit?: number; offset?: number } = {}) {
|
|
const query = new URLSearchParams();
|
|
if (params.limit != null) query.set("limit", String(params.limit));
|
|
if (params.offset != null) query.set("offset", String(params.offset));
|
|
const suffix = query.toString();
|
|
return parseResponse<PublishedScheduledTask[]>(
|
|
await apiFetch(`${baseUrl()}/published${suffix ? `?${suffix}` : ""}`),
|
|
);
|
|
}
|
|
|
|
export async function publishScheduledTask(taskId: string) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/publish`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function unpublishScheduledTask(taskId: string) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/unpublish`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function subscribeScheduledTask(taskId: string, notifyElement: boolean) {
|
|
return parseResponse<ScheduledTaskSubscription>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ notify_element: notifyElement }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function updateScheduledTaskSubscription(taskId: string, notifyElement: boolean) {
|
|
return parseResponse<ScheduledTaskSubscription>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ notify_element: notifyElement }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function unsubscribeScheduledTask(taskId: string) {
|
|
return parseResponse<{ success: boolean }>(
|
|
await apiFetch(`${baseUrl()}/${encodeURIComponent(taskId)}/subscribe`, {
|
|
method: "DELETE",
|
|
}),
|
|
);
|
|
}
|
|
|
|
// --- Admin (cross-user management) ---
|
|
|
|
/** List every scheduled task across all users. Admin-only endpoint. */
|
|
export async function listAllScheduledTasks(search?: string) {
|
|
const qs = new URLSearchParams();
|
|
if (search) qs.set("search", search);
|
|
const query = qs.toString();
|
|
return parseResponse<ScheduledTask[]>(
|
|
await apiFetch(`${baseUrl()}/admin/all${query ? `?${query}` : ""}`),
|
|
);
|
|
}
|
|
|
|
/** List runs for any task regardless of owner. Admin-only endpoint. */
|
|
export async function adminListScheduledTaskRuns(taskId: string) {
|
|
return parseResponse<ScheduledTaskRun[]>(
|
|
await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}/runs`),
|
|
);
|
|
}
|
|
|
|
/** Update any task regardless of owner. Admin-only endpoint. */
|
|
export async function adminUpdateScheduledTask(
|
|
taskId: string,
|
|
data: Partial<{
|
|
name: string;
|
|
description: string | null;
|
|
prompt: string;
|
|
schedule: string;
|
|
timezone: string;
|
|
enabled: boolean;
|
|
execution_agent_name: string | null;
|
|
require_markdown: boolean;
|
|
}>,
|
|
) {
|
|
return parseResponse<ScheduledTask>(
|
|
await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** Delete any task regardless of owner. Admin-only endpoint. */
|
|
export async function adminDeleteScheduledTask(taskId: string) {
|
|
return parseResponse<{ success: boolean }>(
|
|
await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}`, {
|
|
method: "DELETE",
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** Trigger an immediate run of any task regardless of owner. Admin-only. */
|
|
export async function adminTriggerScheduledTask(taskId: string) {
|
|
return parseResponse<{ success: boolean; task: ScheduledTask }>(
|
|
await apiFetch(`${baseUrl()}/admin/${encodeURIComponent(taskId)}/trigger`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
}
|
|
|
|
// --- HTML page tasks ---
|
|
|
|
/** Fetch the generated HTML document for an ``html_page`` run. */
|
|
export async function getScheduledTaskRunHtml(runId: string) {
|
|
return parseResponse<ScheduledTaskRunHtml>(
|
|
await apiFetch(`${baseUrl()}/runs/${encodeURIComponent(runId)}/html`),
|
|
);
|
|
}
|
|
|
|
const favoritesUrl = () => `${getBackendBaseURL()}/api/html-page-favorites`;
|
|
|
|
export async function listHtmlPageFavorites() {
|
|
return parseResponse<HtmlPageFavorite[]>(await apiFetch(favoritesUrl()));
|
|
}
|
|
|
|
export async function getHtmlPageFavorite(favoriteId: string) {
|
|
return parseResponse<HtmlPageFavorite>(
|
|
await apiFetch(`${favoritesUrl()}/${encodeURIComponent(favoriteId)}`),
|
|
);
|
|
}
|
|
|
|
export async function createHtmlPageFavorite(data: {
|
|
page_id: string;
|
|
title?: string | null;
|
|
description?: string | null;
|
|
tags?: string[];
|
|
}) {
|
|
return parseResponse<HtmlPageFavorite>(
|
|
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",
|
|
}),
|
|
);
|
|
}
|