From 41e3647f0a6a7def2c18e8fb01c4ee5d6fd68ef6 Mon Sep 17 00:00:00 2001
From: root
Date: Tue, 29 Sep 2026 01:58:28 +0800
Subject: [PATCH] feat(web): render agent reports as safe-subset Markdown
Co-Authored-By: Claude Opus 5.5
---
.../components/agent/agent-conversation.tsx | 6 +-
.../app/components/agentui/markdown.test.ts | 74 +++++
apps/web/app/components/agentui/markdown.tsx | 292 ++++++++++++++++++
3 files changed, 370 insertions(+), 2 deletions(-)
create mode 100644 apps/web/app/components/agentui/markdown.test.ts
create mode 100644 apps/web/app/components/agentui/markdown.tsx
diff --git a/apps/web/app/components/agent/agent-conversation.tsx b/apps/web/app/components/agent/agent-conversation.tsx
index f117740f..c58ea56e 100644
--- a/apps/web/app/components/agent/agent-conversation.tsx
+++ b/apps/web/app/components/agent/agent-conversation.tsx
@@ -36,6 +36,7 @@ import { ApprovalCard, type ApprovalCardStatus } from "~/components/agentui/appr
import { type CitationItem, CitationList } from "~/components/agentui/citations.tsx";
import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx";
import { FileDiff } from "~/components/agentui/file-diff.tsx";
+import { Markdown } from "~/components/agentui/markdown.tsx";
import {
Message,
MessageAvatar,
@@ -427,7 +428,8 @@ export function AgentConversation({
announce={false}
showActions={!active && idx === lastText}
>
- {it.text}
+ {/* 流式中按原文显示(半截 Markdown 会闪),结束后按安全子集渲染 */}
+ {streaming ? it.text : }
);
@@ -724,7 +726,7 @@ export function FinalReport({
模型说的(未经核查)
- {said || "(没有文字汇报)"}
+ {said ? : "(没有文字汇报)"}
仅供参考,不作为完成依据。
diff --git a/apps/web/app/components/agentui/markdown.test.ts b/apps/web/app/components/agentui/markdown.test.ts
new file mode 100644
index 00000000..402ca90a
--- /dev/null
+++ b/apps/web/app/components/agentui/markdown.test.ts
@@ -0,0 +1,74 @@
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vitest";
+import { Markdown, parseMarkdown, safeHref } from "./markdown.tsx";
+
+const html = (text: string) => renderToStaticMarkup(createElement(Markdown, { text }));
+
+describe("Markdown 安全子集", () => {
+ it("块级:标题、列表、表格、代码块、引用、分隔线", () => {
+ const src = [
+ "## 结论",
+ "",
+ "**当前订阅 2 个**,来源 `inventory.query`:",
+ "",
+ "| # | 标题 | 年份 |",
+ "|---|:---|---:|",
+ "| 1 | 沙丘2 | 2024 |",
+ "| 2 | 沙丘:预言 | 2024 |",
+ "",
+ "- 缺 1080p",
+ "- 缺 4K",
+ "",
+ "1. 先搜",
+ "2. 再提交",
+ "",
+ "> 仅供参考",
+ "",
+ "---",
+ "```json",
+ '{"a":1}',
+ "```",
+ ].join("\n");
+ const blocks = parseMarkdown(src);
+ expect(blocks.map((b) => b.type)).toEqual(["heading", "paragraph", "table", "list", "list", "quote", "hr", "code"]);
+ const out = html(src);
+ expect(out).toContain("", () => {
+ const out = html(
+ "
[点我](javascript:alert(1)) [文档](https://example.com/a)",
+ );
+ expect(out).not.toContain("