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:
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[];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user