feat(定时任务): 内置「模板数据转换器」按 HTML 模板填充 JSON 生成大屏

- 模板填充引擎 template_fill.py:模板注册表 + schema 骨架 + 结构校验 + 行级安全填充;
  模板自带 DATA 即 schema 来源。no.1 可转换填充(含 MANIFEST 更新时间刷新),
  no.2 暂为「只展示」(convertible=False) 原样输出已有数据
- 内置 agent template-json-builder(seed + SOUL),随 lifespan 注入,自动出现在
  定时任务「执行智能体」下拉
- 调度器新增模板填充执行分支 _run_template_fill_attempt:源 JSON→大模型转换→
  结构校验→失败回喂重试;空源/只展示模板直接用模板默认数据,绝不失败
- API:GET /api/scheduled-tasks/html-templates
- 前端:选中该 agent 后条件渲染「HTML模板」下拉 + .json 上传,只展示型模板隐藏 JSON 输入
- 文档同步(前后端 CLAUDE.md)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-23 18:00:00 +08:00
1 parent 1a2ef0a537
commit cd33aaeded
17 files changed
+1511 -57

No files matched your search

@@ -10,6 +10,7 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
ArrowLeft,
ArrowRight,
ExternalLink,
FileSearch,
@@ -95,16 +96,27 @@ export function TaskJumpBar({
});
};
const nextButtons = jumpButtons.filter(isNextTaskButton);
const menuButtons = jumpButtons.filter((btn) => !isNextTaskButton(btn));
// 底部栏只放「上一步 / 下一步」:prev 组渲染在 next 组之前 → 视觉上「上一步」在「下一步」左边
// (容器 justify-end 右对齐,数组先后即左右先后)。其余按钮进右侧悬浮菜单。
const prevButtons = jumpButtons.filter(isPrevTaskButton);
const nextButtons = jumpButtons.filter((btn) => isNextTaskButton(btn) && !isPrevTaskButton(btn));
const barButtons = [...prevButtons, ...nextButtons];
const menuButtons = jumpButtons.filter(
(btn) => !isNextTaskButton(btn) && !isPrevTaskButton(btn),
);
return (
<>
{menuButtons.length > 0 && <TaskSideJumpMenu buttons={menuButtons} onRun={handleRun} />}
{nextButtons.length > 0 && (
{barButtons.length > 0 && (
<div className="border-border/70 bg-background/90 flex shrink-0 flex-wrap items-center justify-end gap-3 border-t px-5 py-3 backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/85">
{nextButtons.map((btn) => (
<TaskJumpButton key={btn.id} button={btn} onRun={handleRun} />
{barButtons.map((btn) => (
<TaskJumpButton
key={btn.id}
button={btn}
onRun={handleRun}
variant={isPrevTaskButton(btn) ? "secondary" : "primary"}
/>
))}
</div>
)}
@@ -122,21 +134,35 @@ function isNextTaskButton(btn: TaskButton) {
return btn.id.toLowerCase().includes("next") || btn.label.trim() === "下一步";
}
/** 「上一步」按钮:id 含 prev 或文案为「上一步」——底部栏里渲染到「下一步」左边。 */
function isPrevTaskButton(btn: TaskButton) {
return btn.id.toLowerCase().includes("prev") || btn.label.trim() === "上一步";
}
function TaskJumpButton({
button,
onRun,
variant = "primary",
}: {
button: TaskButton;
onRun: (button: TaskButton) => void;
/** primary=下一步(实心主色)/ secondary=上一步(描边次要,置于下一步左侧)。 */
variant?: "primary" | "secondary";
}) {
const styles =
variant === "secondary"
? "border-[var(--color-main)]/40 bg-transparent text-[var(--color-main)] hover:bg-[var(--color-main)]/10 dark:text-[#7eb6ff] dark:border-[#7eb6ff]/40 dark:hover:bg-[#7eb6ff]/10"
: "border-transparent bg-[var(--color-main)] text-white shadow-sm hover:bg-[var(--brand-950)]";
return (
<Button
className="h-11 gap-2 border-transparent bg-[var(--color-main)] px-6 text-base text-white shadow-sm hover:bg-[var(--brand-950)]"
variant={variant === "secondary" ? "outline" : "default"}
className={cn("h-11 gap-2 px-6 text-base", styles)}
onClick={() => onRun(button)}
title={desensitize(button.label)}
>
{variant === "secondary" && <ArrowLeft className="h-5 w-5 shrink-0" />}
<span className="truncate">{desensitize(button.label)}</span>
{renderTaskJumpIcon(button, "h-5 w-5 shrink-0")}
{variant === "primary" && renderTaskJumpIcon(button, "h-5 w-5 shrink-0")}
</Button>
);
}