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

+40 -8
View File
@@ -7,7 +7,8 @@
* 任务深链配置读出 rwfx 的 4 个历史按钮,3qfx/xdfx 为空)——这样真实跳转地址通过实时
* config 进种子,避免写死;一旦保存过,后端即唯一数据源。
*
* 按钮两类(``linkType``):``url`` 外链(``window.open``,可选追加 ``?id=任务id``)/
* 按钮两类(``linkType``):``url``(外链 ``window.open``;**或应用内路由**——target 以 ``/`` 开头
* 时按内部路由处理:``self`` 走 react-router 同窗导航、``blank`` 拼 hash 路由新标签;可选追加 ``?{idParam}=id``)/
* ``business`` 内部切换到另一业务(复用 [[task-deeplink]] 的 ``resolveTaskDeeplinkTarget``)。
*/
@@ -20,6 +21,9 @@ import {
resolveTaskDeeplinkTarget,
} from "@/core/auth/task-deeplink";
/** 「大屏绘制」独立页应用内路由(深链「上一步」默认指向它,跳转追加 ?taskId=)。 */
export const DASHBOARD_AGENT_ROUTE = "/page/strategy/dashboard-agent";
/** 任务工作区业务码(与路由 taskBase 一一对应)。 */
export type TaskBusiness = "rwfx" | "3qfx" | "xdfx";
@@ -94,7 +98,8 @@ export const TASK_BUSINESS_TARGETS: { code: TaskBusiness; label: string }[] = [
/**
* 代码种子默认按钮(仅在后端整张表为空时使用):
* - rwfx:历史 4 个按钮(编辑/下一步/3q详情,地址取自实时任务深链配置)+ 切换到三情/行动分析;
* - rwfx:历史按钮(编辑/下一步/3q详情,地址取自实时任务深链配置)+「上一步」+ 切换到三情/行动分析;
* 「上一步」紧邻「下一步」左侧(底部栏 prev 组在 next 组前),默认指向应用内大屏绘制页(?taskId=任务id);
* - 3qfx / xdfx:互相切换到另外两个业务。
* 即「三个业务都带切换按钮」。已保存过则用页面「导入默认」把缺失项补进来。
*/
@@ -147,6 +152,22 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
enabled: true,
sortOrder: 0,
},
{
// 上一步:放在「下一步」左边(底部快捷跳转栏,prev 组渲染在 next 组之前)。
// 默认指向**应用内**「大屏绘制」页 /page/strategy/dashboard-agent(内部路由,以 / 开头),
// 携带 ?taskId=任务id(大屏页按 taskId 取数);openMode=self → 同窗 react-router 导航。
id: "rwfx-prev",
business: "rwfx",
label: "上一步",
linkType: "url",
target: DASHBOARD_AGENT_ROUTE,
appendTaskId: true,
idParam: "taskId",
idKind: "task",
openMode: "self",
enabled: true,
sortOrder: 1,
},
{
id: "rwfx-next",
business: "rwfx",
@@ -158,7 +179,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 1,
sortOrder: 2,
},
{
id: "rwfx-detail3q",
@@ -171,11 +192,11 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 2,
sortOrder: 3,
},
sw("rwfx-switch3q", "rwfx", "切换到三情分析", "3qfx", 3),
sw("rwfx-switchxd", "rwfx", "切换到行动分析", "xdfx", 4),
pz("rwfx-purpose", "rwfx", 5),
sw("rwfx-switch3q", "rwfx", "切换到三情分析", "3qfx", 4),
sw("rwfx-switchxd", "rwfx", "切换到行动分析", "xdfx", 5),
pz("rwfx-purpose", "rwfx", 6),
// 3qfx(三情分析):切换到任务/行动分析
sw("3qfx-switchrw", "3qfx", "切换到任务分析", "rwfx", 0),
sw("3qfx-switchxd", "3qfx", "切换到行动分析", "xdfx", 1),
@@ -237,7 +258,18 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
const id = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId;
const param = (btn.idParam || "id").trim() || "id";
const url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
// 打开方式:self=当前窗口(嵌入态即当前 iframe);否则 blank=新窗口(默认)。
// 应用内部路由(以单个 / 开头,如「上一步」→ 大屏绘制页):
// self → 走 react-router 同窗导航(不刷新、保留 SPA);blank → 拼成当前源的 hash 路由新标签打开。
if (base.startsWith("/") && !base.startsWith("//")) {
if (btn.openMode === "blank") {
const full = `${window.location.origin}${window.location.pathname}#${url}`;
window.open(full, "_blank", "noopener,noreferrer");
} else {
ctx.navigate(url);
}
return;
}
// 外链:self=当前窗口(嵌入态即当前 iframe);否则 blank=新窗口(默认)。
if (btn.openMode === "self") {
window.location.assign(url);
} else {
@@ -3,6 +3,7 @@ import { getBackendBaseURL } from "@/core/config";
import type {
HtmlPageFavorite,
HtmlTemplateOption,
PublishedScheduledTask,
ScheduledTask,
ScheduledTaskDeliveryProfile,
@@ -32,6 +33,14 @@ export async function getSchedulerThread() {
return parseResponse<SchedulerThread>(await apiFetch(`${baseUrl()}/thread`));
}
/** 可选的 HTML 模板清单(模板数据转换器填充用)。 */
export async function listHtmlTemplates() {
const data = await parseResponse<{ templates: HtmlTemplateOption[] }>(
await apiFetch(`${baseUrl()}/html-templates`),
);
return data.templates ?? [];
}
export interface ListScheduledTasksParams {
search?: string;
tagIds?: string[];
@@ -16,6 +16,7 @@ import {
deleteScheduledTask,
deleteScheduledTaskRun,
getSchedulerThread,
listHtmlTemplates,
getScheduledTaskDeliveryProfile,
getScheduledTaskRun,
getScheduledTaskRunHtml,
@@ -45,6 +46,17 @@ export function useSchedulerThread() {
});
}
/** 可选的 HTML 模板清单(模板数据转换器填充用)。模板是后端静态注册,长缓存即可。 */
export function useHtmlTemplates(enabled = true) {
return useQuery({
queryKey: ["scheduled-tasks", "html-templates"],
queryFn: listHtmlTemplates,
enabled,
staleTime: 5 * 60_000,
refetchOnWindowFocus: false,
});
}
/** A task is currently executing — used to drive live polling of the task list. */
function taskIsLive(status?: string | null): boolean {
return status === "running" || status === "pending";
@@ -24,8 +24,27 @@ export type HtmlPageConfig = {
model_name?: string | null;
};
/** 选中内置「模板数据转换器」agent 后,要把源 JSON 填充进的 HTML 模板。
* 持久化在 ``execution_context.template_config`` 里(无独立列)。 */
export type TemplateFillConfig = {
template_id?: string | null;
/** 可选:覆盖转换所用模型(留空走 agent.model / 系统默认)。 */
model_name?: string | null;
};
/** ``GET /api/scheduled-tasks/html-templates`` 返回的一条模板选项。 */
export type HtmlTemplateOption = {
id: string;
name: string;
description?: string | null;
/** false = 暂只「直接展示已有数据」,不需要也不会用源 JSON 转换填充。 */
convertible?: boolean;
};
export type ScheduledTaskExecutionContext = {
require_markdown?: boolean;
/** 模板填充模式:选中「模板数据转换器」+ 选模板后填充进固定 HTML 模板。 */
template_config?: TemplateFillConfig;
/**
* The unified "必须生成 HTML" switch. The task form no longer splits "普通任务 /
* HTML 页面" into separate kinds — instead ``require_html`` and