feat(定时任务): 模板数据转换器对话页加「页面模板+渲染预览」调试入口

- 后端 POST /api/scheduled-tasks/template-fill/render:不调模型,抽取回复里的 JSON
  → 校验 → 填充,返回完整 HTML + 校验错误/缺失字段(非法也照样填充便于看效果),
  只展示模板原样返回
- 前端仅 template-json-builder 智能体的 AgentChatPage:顶部加页面模板下拉 +「渲染预览」
  按钮,取最近一条 AI 回复填进所选模板,弹窗用 sandbox=allow-scripts iframe 预览 +
  校验状态;严格按 agent id 门控,不影响其他智能体对话页
- 文档同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-24 10:00:18 +08:00
1 parent f551c7af9b
commit ae3788a87d
5 files changed
+216 -2

No files matched your search

@@ -42,6 +42,26 @@ export async function listHtmlTemplates() {
return data.templates ?? [];
}
/** 「模板数据转换器」对话页「渲染预览」结果。 */
export type TemplateRenderResult = {
ok: boolean;
display_only?: boolean;
errors: string[];
gaps: string[];
html: string | null;
};
/** 调试用:把一段(智能体回复的)文本里的 JSON 填进所选 HTML 模板,返回完整 HTML 与校验信息。 */
export async function renderTemplateFill(templateId: string, text: string) {
return parseResponse<TemplateRenderResult>(
await apiFetch(`${baseUrl()}/template-fill/render`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template_id: templateId, text }),
}),
);
}
export interface ListScheduledTasksParams {
search?: string;
tagIds?: string[];
+150
View File
@@ -31,11 +31,32 @@ import { saveLastAgentId, useThreadSettings } from "@/core/settings";
import { stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { OptType, writeYLog } from "@/core/log/y-log";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
renderTemplateFill,
useHtmlTemplates,
type TemplateRenderResult,
} from "@/core/scheduled-tasks";
import { env } from "@/env";
import { readTaskContext, taskThreadMetadata } from "@/core/auth/task-purpose";
import { TaskPurposePanel } from "@/strategy-components/components/task-components/TaskPurposePanel";
import { desensitize } from "@/lib/desensitize";
/** 内置「模板数据转换器」agent 的稳定 id。仅此 agent 的对话页显示「页面模板 + 渲染预览」调试入口。 */
const TEMPLATE_BUILDER_AGENT_ID = "template-json-builder";
export default function AgentChatPage({
embeddedAgentId,
chatBasePath,
@@ -190,6 +211,41 @@ export default function AgentChatPage({
},
});
// ── 「模板数据转换器」对话页专属调试入口(仅此 agent)──────────────────────────
// 选一个页面模板,问答跑完后点「渲染预览」:取最近一条 AI 回复里的 JSON 填进模板,
// 弹出 HTML 预览 + 结构校验信息,用于调试该智能体的转换效果。
const isTemplateBuilder = agent_id === TEMPLATE_BUILDER_AGENT_ID;
const { data: debugTemplates = [] } = useHtmlTemplates(isTemplateBuilder);
const [debugTemplateId, setDebugTemplateId] = useState("");
const [debugPreviewOpen, setDebugPreviewOpen] = useState(false);
const [debugBusy, setDebugBusy] = useState(false);
const [debugPreview, setDebugPreview] = useState<TemplateRenderResult | null>(
null,
);
const runDebugPreview = useCallback(async () => {
if (!debugTemplateId) return;
const lastAi = [...thread.messages]
.reverse()
.find((m) => (m as { type?: string }).type === "ai");
const text = (lastAi ? textOfMessage(lastAi) : "") ?? "";
setDebugBusy(true);
setDebugPreview(null);
setDebugPreviewOpen(true);
try {
const result = await renderTemplateFill(debugTemplateId, text);
setDebugPreview(result);
} catch (error) {
setDebugPreview({
ok: false,
errors: [error instanceof Error ? error.message : "渲染失败"],
gaps: [],
html: null,
});
} finally {
setDebugBusy(false);
}
}, [debugTemplateId, thread.messages]);
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
setHasStartedConversation(true);
@@ -327,6 +383,36 @@ export default function AgentChatPage({
</div>
<div className="flex items-center">
{/* 「模板数据转换器」专属:选页面模板 + 渲染预览(调试该智能体效果)。 */}
{isTemplateBuilder && (
<div className="mr-2 flex items-center gap-1.5">
<Select
value={debugTemplateId || undefined}
onValueChange={setDebugTemplateId}
>
<SelectTrigger className="h-8 w-[150px] text-xs">
<SelectValue placeholder="选择页面模板" />
</SelectTrigger>
<SelectContent position="popper">
{debugTemplates.map((tpl) => (
<SelectItem key={tpl.id} value={tpl.id}>
{tpl.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Tooltip content="用最近一条回复里的 JSON 填进所选模板并预览">
<Button
size="sm"
variant="secondary"
disabled={!debugTemplateId || debugBusy}
onClick={runDebugPreview}
>
{debugBusy ? "渲染中…" : "渲染预览"}
</Button>
</Tooltip>
</div>
)}
{/* 3qfx/xdfx 任务深链:用户主动「新建对话」进问答页后,点此发出任务开场信息开始问答。
首次进入且无历史(taskAutoStart)会自动发,不显示此按钮。 */}
{taskStartText && isLandingState && !taskAutoStart && (
@@ -489,6 +575,70 @@ export default function AgentChatPage({
showRecentChats={!taskCtxOn}
/>
</div>
{/* 「模板数据转换器」调试预览弹窗(仅此 agent)。 */}
{isTemplateBuilder && (
<Dialog open={debugPreviewOpen} onOpenChange={setDebugPreviewOpen}>
<DialogContent className="flex h-[88vh] w-[92vw] max-w-[92vw] flex-col gap-0 p-0">
<DialogHeader className="border-b px-4 py-2">
<DialogTitle className="text-sm">
模板填充预览 · 渲染结果
</DialogTitle>
</DialogHeader>
<div className="max-h-28 overflow-y-auto border-b px-4 py-2 text-xs">
{debugBusy && (
<span className="text-muted-foreground">正在渲染…</span>
)}
{!debugBusy && debugPreview && (
<div className="flex flex-col gap-1">
<span
className={
debugPreview.html
? debugPreview.ok
? "text-emerald-600"
: "text-amber-600"
: "text-destructive"
}
>
{debugPreview.display_only
? "该模板为「只展示」,已原样输出现有数据"
: debugPreview.html
? debugPreview.ok
? "✓ 结构校验通过"
: "⚠ 结构校验有问题(已照常填充,可对照下方修正后再让智能体重答)"
: "✗ 渲染失败"}
</span>
{debugPreview.errors.length > 0 && (
<span className="text-destructive">
错误:{debugPreview.errors.join(";")}
</span>
)}
{debugPreview.gaps.length > 0 && (
<span className="text-muted-foreground">
缺失字段:{debugPreview.gaps.slice(0, 20).join("、")}
{debugPreview.gaps.length > 20 ? " …" : ""}
</span>
)}
</div>
)}
</div>
<div className="min-h-0 flex-1 bg-white">
{debugPreview?.html ? (
<iframe
title="模板填充预览"
srcDoc={debugPreview.html}
sandbox="allow-scripts"
className="size-full border-0"
/>
) : (
<div className="text-muted-foreground flex size-full items-center justify-center text-sm">
{debugBusy ? "正在渲染…" : "无可预览内容"}
</div>
)}
</div>
</DialogContent>
</Dialog>
)}
</ThreadContext.Provider>
);
}