feat(scheduled-tasks): 新增 HTML 页面定时任务(生成/预览/收藏/技能)
后端: - 新增 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>
This commit is contained in:
1 parent
e1e9be8daa
commit
d9afaca51f
22 files changed
+1996
-107
No files matched your search
@@ -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<PublicF
|
||||
);
|
||||
}
|
||||
|
||||
type PublicRunHtml = { run_id: string; title?: string | null; html?: string | null };
|
||||
|
||||
async function fetchPublicRunHtml(runId: string): Promise<PublicRunHtml> {
|
||||
return fetchJson<PublicRunHtml>(`${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 }) {
|
||||
下载原文件
|
||||
</a>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={handleDownloadWord}>
|
||||
<FileText className="size-4" />
|
||||
下载 Word
|
||||
</Button>
|
||||
{isEditing ? (
|
||||
<>
|
||||
{!isHtmlRun && (
|
||||
<Button size="sm" variant="outline" onClick={handleDownloadWord}>
|
||||
<FileText className="size-4" />
|
||||
下载 Word
|
||||
</Button>
|
||||
)}
|
||||
{!isHtmlRun &&
|
||||
(isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setDraft(activeSource);
|
||||
}}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
<X className="size-4" />
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="size-4" />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setDraft(activeSource);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
disabled={saveMutation.isPending}
|
||||
disabled={fileIsLoading}
|
||||
>
|
||||
<X className="size-4" />
|
||||
取消
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="size-4" />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDraft(activeSource);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
disabled={fileIsLoading}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="min-h-0 flex-1 overflow-hidden p-0">
|
||||
{tabs.length === 0 ? (
|
||||
{isHtmlRun ? (
|
||||
<div className="h-full p-4">
|
||||
{htmlContent ? (
|
||||
<iframe
|
||||
title="HTML 预览"
|
||||
sandbox="allow-same-origin"
|
||||
srcDoc={htmlContent}
|
||||
className="h-full min-h-[60vh] w-full rounded-md border bg-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-muted-foreground rounded-md border p-3 text-sm">
|
||||
HTML 页面尚未生成或正在生成中。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : tabs.length === 0 ? (
|
||||
<div className="text-muted-foreground p-6 text-sm">无可用内容。</div>
|
||||
) : (
|
||||
<Tabs
|
||||
|
||||
Reference in new issue
Block a user