代码修改
This commit is contained in:
1 parent
9bbd04d6cf
commit
b0fdeab995
15 files changed
+403
-26
No files matched your search
@@ -107,7 +107,7 @@ http://localhost:5173/#/embed/chats/new?sessionId=...&message=...&username=guest
|
|||||||
|
|
||||||
## 4. 公开 HTTP API(无需鉴权)
|
## 4. 公开 HTTP API(无需鉴权)
|
||||||
|
|
||||||
前缀:`/api/public/embed/`(走 Gateway,与 `VITE_BACKEND_BASE_URL` 一致,默认 `http://127.0.0.1:8001`)。
|
前缀:`/api/public/embed/`(走 Gateway,与 `VITE_BACKEND_BASE_URL` 一致,默认 `http://47.88.25.99:7001/`)。
|
||||||
|
|
||||||
CSRF / Auth 中间件对 `/api/public/` 放行;请求 **不需要** Cookie。
|
CSRF / Auth 中间件对 `/api/public/` 放行;请求 **不需要** Cookie。
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ document.getElementById("deerflow-embed").src =
|
|||||||
### 5.2 轮询直到结束
|
### 5.2 轮询直到结束
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const API = "http://127.0.0.1:8001"; // VITE_BACKEND_BASE_URL
|
const API = "http://47.88.25.99:7001/"; // VITE_BACKEND_BASE_URL
|
||||||
|
|
||||||
async function pollUntilFinished(sessionId) {
|
async function pollUntilFinished(sessionId) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -348,7 +348,7 @@ const timer = setInterval(async () => {
|
|||||||
|
|
||||||
### 5.3 跨域注意
|
### 5.3 跨域注意
|
||||||
|
|
||||||
父页面从浏览器 `fetch` Gateway(如 `http://127.0.0.1:8001`)时,**Origin 必须是后端 CORS 白名单里的完整地址**(含协议、主机、端口),与 API 地址是否写 `localhost` / `127.0.0.1` 无关。
|
父页面从浏览器 `fetch` Gateway(如 `http://47.88.25.99:7001/`)时,**Origin 必须是后端 CORS 白名单里的完整地址**(含协议、主机、端口),与 API 地址是否写 `localhost` / `127.0.0.1` 无关。
|
||||||
|
|
||||||
**本地开发**(前端默认 `5174`)请在 Gateway 环境变量中配置:
|
**本地开发**(前端默认 `5174`)请在 Gateway 环境变量中配置:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.sidebar-contnet {
|
.sidebar-contnet {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 4rem;
|
top: 4rem;
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ import {
|
|||||||
getFileName,
|
getFileName,
|
||||||
isImageFile,
|
isImageFile,
|
||||||
} from "@/core/utils/files";
|
} from "@/core/utils/files";
|
||||||
|
import {
|
||||||
|
downloadMermaidSvgFile,
|
||||||
|
isMermaidFile,
|
||||||
|
openMermaidSvgInNewTab,
|
||||||
|
renderMermaidToSvg,
|
||||||
|
} from "@/core/utils/mermaid";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@@ -131,7 +137,11 @@ export function ArtifactFileDetail({
|
|||||||
return checkCodeFile(filepath);
|
return checkCodeFile(filepath);
|
||||||
}, [filepath, useWriteFileStreaming, isSkillFile]);
|
}, [filepath, useWriteFileStreaming, isSkillFile]);
|
||||||
const isSupportPreview = useMemo(() => {
|
const isSupportPreview = useMemo(() => {
|
||||||
return language === "html" || language === "markdown";
|
return (
|
||||||
|
language === "html" ||
|
||||||
|
language === "markdown" ||
|
||||||
|
language === "mermaid"
|
||||||
|
);
|
||||||
}, [language]);
|
}, [language]);
|
||||||
const { content, isApplyingEdit } = useArtifactContent({
|
const { content, isApplyingEdit } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
@@ -152,6 +162,7 @@ export function ArtifactFileDetail({
|
|||||||
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
|
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
|
||||||
const fileTypeLabel = useMemo(() => {
|
const fileTypeLabel = useMemo(() => {
|
||||||
if (isImageFile(filepath)) return "IMG";
|
if (isImageFile(filepath)) return "IMG";
|
||||||
|
if (isMermaidFile(filepath) || language === "mermaid") return "MMD";
|
||||||
if (language === "html") return "HTML";
|
if (language === "html") return "HTML";
|
||||||
if (language === "markdown") return "MD";
|
if (language === "markdown") return "MD";
|
||||||
if (isSkillFile) return "SKILL";
|
if (isSkillFile) return "SKILL";
|
||||||
@@ -423,6 +434,13 @@ export function ArtifactFileDetail({
|
|||||||
toast.error("文件内容为空,暂时无法打开");
|
toast.error("文件内容为空,暂时无法打开");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (language === "mermaid") {
|
||||||
|
void openMermaidSvgInNewTab(source).catch((error) => {
|
||||||
|
console.error("Failed to open mermaid artifact:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to open artifact");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const blobType =
|
const blobType =
|
||||||
language === "html"
|
language === "html"
|
||||||
? "text/html;charset=utf-8"
|
? "text/html;charset=utf-8"
|
||||||
@@ -441,6 +459,13 @@ export function ArtifactFileDetail({
|
|||||||
toast.error("文件内容为空,暂时无法下载");
|
toast.error("文件内容为空,暂时无法下载");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (language === "mermaid") {
|
||||||
|
void downloadMermaidSvgFile(source, filepath).catch((error) => {
|
||||||
|
console.error("Failed to download mermaid artifact:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Download failed");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const blobType =
|
const blobType =
|
||||||
language === "html"
|
language === "html"
|
||||||
? "text/html;charset=utf-8"
|
? "text/html;charset=utf-8"
|
||||||
@@ -567,6 +592,19 @@ export function ArtifactFileDetail({
|
|||||||
handleOpenWriteFileInNewTab();
|
handleOpenWriteFileInNewTab();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (language === "mermaid") {
|
||||||
|
try {
|
||||||
|
await openMermaidSvgInNewTab(displayContent ?? "");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to open artifact",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await openArtifactInNewTab({
|
await openArtifactInNewTab({
|
||||||
filepath,
|
filepath,
|
||||||
@@ -619,6 +657,19 @@ export function ArtifactFileDetail({
|
|||||||
handleDownloadWriteFile();
|
handleDownloadWriteFile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (language === "mermaid") {
|
||||||
|
try {
|
||||||
|
await downloadMermaidSvgFile(displayContent ?? "", filepath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Download failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await downloadArtifactFile({
|
await downloadArtifactFile({
|
||||||
filepath,
|
filepath,
|
||||||
@@ -669,7 +720,9 @@ export function ArtifactFileDetail({
|
|||||||
)}
|
)}
|
||||||
{isSupportPreview &&
|
{isSupportPreview &&
|
||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
(language === "markdown" || language === "html") && (
|
(language === "markdown" ||
|
||||||
|
language === "html" ||
|
||||||
|
language === "mermaid") && (
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
content={desensitize(displayContent ?? "")}
|
content={desensitize(displayContent ?? "")}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
@@ -770,6 +823,8 @@ export function ArtifactFilePreview({
|
|||||||
activeCursor?: boolean;
|
activeCursor?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [htmlPreviewUrl, setHtmlPreviewUrl] = useState<string>();
|
const [htmlPreviewUrl, setHtmlPreviewUrl] = useState<string>();
|
||||||
|
const [mermaidSvg, setMermaidSvg] = useState<string | null>(null);
|
||||||
|
const [mermaidError, setMermaidError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (language !== "html") {
|
if (language !== "html") {
|
||||||
@@ -789,6 +844,37 @@ export function ArtifactFilePreview({
|
|||||||
};
|
};
|
||||||
}, [content, language]);
|
}, [content, language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (language !== "mermaid") {
|
||||||
|
setMermaidSvg(null);
|
||||||
|
setMermaidError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setMermaidSvg(null);
|
||||||
|
setMermaidError(null);
|
||||||
|
|
||||||
|
void renderMermaidToSvg(content ?? "")
|
||||||
|
.then((svg) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setMermaidSvg(svg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to render mermaid preview:", error);
|
||||||
|
if (!cancelled) {
|
||||||
|
setMermaidError(
|
||||||
|
error instanceof Error ? error.message : "Mermaid 渲染失败",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [content, language]);
|
||||||
|
|
||||||
if (language === "markdown") {
|
if (language === "markdown") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -835,5 +921,38 @@ export function ArtifactFilePreview({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (language === "mermaid") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-artifact-preview-scroll
|
||||||
|
className={cn(
|
||||||
|
"size-full overflow-auto bg-[#fcfdfc] px-5 py-4",
|
||||||
|
animated && "animate-[sandbox-preview-enter_320ms_ease-out]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{mermaidSvg ? (
|
||||||
|
<div
|
||||||
|
className="mx-auto flex min-h-full items-start justify-center rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"
|
||||||
|
dangerouslySetInnerHTML={{ __html: mermaidSvg }}
|
||||||
|
/>
|
||||||
|
) : mermaidError ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||||
|
{mermaidError}
|
||||||
|
</div>
|
||||||
|
<pre className="whitespace-pre-wrap break-words rounded-xl bg-slate-100 p-4 text-xs text-slate-700">
|
||||||
|
{content ?? ""}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground flex size-full items-center justify-center gap-2 text-sm">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
正在生成图表预览
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || "";
|
|||||||
export const TOOLS_TOKEN_STORAGE_KEY = "tools_token";
|
export const TOOLS_TOKEN_STORAGE_KEY = "tools_token";
|
||||||
export const EXTERNAL_USER_ID_STORAGE_KEY = "userId";
|
export const EXTERNAL_USER_ID_STORAGE_KEY = "userId";
|
||||||
export const EXTERNAL_USER_INFO_STORAGE_KEY = "userInfo";
|
export const EXTERNAL_USER_INFO_STORAGE_KEY = "userInfo";
|
||||||
export const GENERAL_QA_ROUTE = "/page/strategy/qa/general";
|
export const GENERAL_QA_ROUTE = "/page/workspace/chats/new";
|
||||||
|
|
||||||
type UserDetailResponse = {
|
type UserDetailResponse = {
|
||||||
data?: {
|
data?: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
CompassIcon,
|
CompassIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
|||||||
@@ -54,15 +54,13 @@ export const sidebarMenuItems: MenuItem[] = [
|
|||||||
icon: HelpCircle,
|
icon: HelpCircle,
|
||||||
children: [
|
children: [
|
||||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||||
{ id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
{
|
||||||
// 「圆桌规划」入口暂时隐藏 —— 功能仍在 /page/roundtable/planning,只是不在侧边栏出现。
|
id: "multi-agent-roundtable",
|
||||||
// 需要重新开放时取消注释即可。
|
label: "多智能体会商",
|
||||||
// {
|
icon: Users,
|
||||||
// id: "qa-roundtable",
|
path: "/page/strategy/qa/multi-agent-roundtable",
|
||||||
// label: "圆桌规划",
|
},
|
||||||
// icon: Bot,
|
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
||||||
// path: "/page/roundtable/planning",
|
|
||||||
// },
|
|
||||||
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
|
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
|
||||||
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
|
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
|
||||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const extensionMap: Record<string, string> = {
|
|||||||
// Markdown & Docs
|
// Markdown & Docs
|
||||||
md: "markdown",
|
md: "markdown",
|
||||||
mdx: "mdx",
|
mdx: "mdx",
|
||||||
|
mmd: "mermaid",
|
||||||
rst: "rst",
|
rst: "rst",
|
||||||
|
|
||||||
// SQL
|
// SQL
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { getFileName } from "./files";
|
||||||
|
|
||||||
|
let mermaidInitialized = false;
|
||||||
|
|
||||||
|
export function isMermaidFile(filepath: string): boolean {
|
||||||
|
return /\.mmd$/i.test(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMermaidImageFileName(filepath: string, extension = "svg"): string {
|
||||||
|
return getFileName(filepath).replace(/\.mmd$/i, `.${extension}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMermaidModule() {
|
||||||
|
const mod = await import("mermaid");
|
||||||
|
const mermaid = mod.default;
|
||||||
|
if (!mermaidInitialized) {
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
securityLevel: "strict",
|
||||||
|
});
|
||||||
|
mermaidInitialized = true;
|
||||||
|
}
|
||||||
|
return mermaid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMermaidToSvg(source: string): Promise<string> {
|
||||||
|
const text = source.trim();
|
||||||
|
if (!text) {
|
||||||
|
throw new Error("Mermaid 内容为空,无法生成图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mermaid = await getMermaidModule();
|
||||||
|
const renderId = `mermaid-preview-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
const host = document.createElement("div");
|
||||||
|
host.style.position = "fixed";
|
||||||
|
host.style.left = "-10000px";
|
||||||
|
host.style.top = "-10000px";
|
||||||
|
host.style.pointerEvents = "none";
|
||||||
|
host.style.opacity = "0";
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { svg, bindFunctions } = await mermaid.render(renderId, text);
|
||||||
|
host.innerHTML = svg;
|
||||||
|
bindFunctions?.(host);
|
||||||
|
return host.innerHTML;
|
||||||
|
} finally {
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openMermaidSvgInNewTab(source: string): Promise<void> {
|
||||||
|
const svg = await renderMermaidToSvg(source);
|
||||||
|
const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
const win = window.open(objectUrl, "_blank", "noopener,noreferrer");
|
||||||
|
if (win) {
|
||||||
|
win.opener = null;
|
||||||
|
}
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 120_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadMermaidSvgFile(source: string, filepath: string): Promise<void> {
|
||||||
|
const svg = await renderMermaidToSvg(source);
|
||||||
|
const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = getMermaidImageFileName(filepath);
|
||||||
|
a.rel = "noopener";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import TaskAnalyzes from "@/strategy-components/components/task-components/TaskA
|
|||||||
import { ActionAnalyzes } from "@/strategy-components/components/task-components/ActionAnalyzes";
|
import { ActionAnalyzes } from "@/strategy-components/components/task-components/ActionAnalyzes";
|
||||||
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
|
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
|
||||||
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
|
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
|
||||||
|
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
|
||||||
|
import "@/roundtable-planning/styles/roundtable-planning.css";
|
||||||
|
|
||||||
export default function StrategyRoutes() {
|
export default function StrategyRoutes() {
|
||||||
return (
|
return (
|
||||||
@@ -59,6 +61,14 @@ export default function StrategyRoutes() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="qa/multi-agent-roundtable"
|
||||||
|
element={
|
||||||
|
<ChatRuntime>
|
||||||
|
<RoundtablePlanningPage />
|
||||||
|
</ChatRuntime>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{/* 任务管理 */}
|
{/* 任务管理 */}
|
||||||
<Route path="task-management" element={<TaskManagement />}>
|
<Route path="task-management" element={<TaskManagement />}>
|
||||||
<Route index element={<Navigate to="xk-task-jc" replace />} />
|
<Route index element={<Navigate to="xk-task-jc" replace />} />
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import remarkGfm from "remark-gfm";
|
|||||||
import { Download, FileText, Loader2, X } from "lucide-react";
|
import { Download, FileText, Loader2, X } from "lucide-react";
|
||||||
|
|
||||||
import { apiFetch } from "@/core/api/fetch-client";
|
import { apiFetch } from "@/core/api/fetch-client";
|
||||||
|
import {
|
||||||
|
downloadMermaidSvgFile,
|
||||||
|
isMermaidFile,
|
||||||
|
renderMermaidToSvg,
|
||||||
|
} from "@/core/utils/mermaid";
|
||||||
import { buildArtifactDownloadUrl } from "@/roundtable-planning/api/multi-agent";
|
import { buildArtifactDownloadUrl } from "@/roundtable-planning/api/multi-agent";
|
||||||
|
|
||||||
interface ArtifactPreviewModalProps {
|
interface ArtifactPreviewModalProps {
|
||||||
@@ -61,6 +66,11 @@ function isMarkdown(mimeType: string | null, name: string): boolean {
|
|||||||
return /\.(md|markdown)$/i.test(name);
|
return /\.(md|markdown)$/i.test(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMermaid(mimeType: string | null, name: string): boolean {
|
||||||
|
if (mimeType === "text/vnd.mermaid") return true;
|
||||||
|
return isMermaidFile(name);
|
||||||
|
}
|
||||||
|
|
||||||
export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
|
export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
|
||||||
threadId,
|
threadId,
|
||||||
path,
|
path,
|
||||||
@@ -71,6 +81,7 @@ export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
|
|||||||
const [content, setContent] = useState<string | null>(null);
|
const [content, setContent] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [mermaidSvg, setMermaidSvg] = useState<string | null>(null);
|
||||||
|
|
||||||
// Esc closes the modal. Attached once per mount.
|
// Esc closes the modal. Attached once per mount.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -116,11 +127,38 @@ export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
|
|||||||
|
|
||||||
const handleDownload = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
if (isMermaid(mimeType, name) && content) {
|
||||||
|
void downloadMermaidSvgFile(content, name).catch((err) => {
|
||||||
|
setError((err as Error).message);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = buildArtifactDownloadUrl(threadId, path, { download: true });
|
const url = buildArtifactDownloadUrl(threadId, path, { download: true });
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
}, [threadId, path]);
|
}, [content, mimeType, name, path, threadId]);
|
||||||
|
|
||||||
const renderAsMarkdown = isMarkdown(mimeType, name);
|
const renderAsMarkdown = isMarkdown(mimeType, name);
|
||||||
|
const renderAsMermaid = isMermaid(mimeType, name);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!renderAsMermaid || !content) {
|
||||||
|
setMermaidSvg(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
void renderMermaidToSvg(content)
|
||||||
|
.then((svg) => {
|
||||||
|
if (!cancelled) setMermaidSvg(svg);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled) setError((err as Error).message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [content, renderAsMermaid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -170,7 +208,19 @@ export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
|
|||||||
<div className="text-rose-600 text-xs py-6 text-center">
|
<div className="text-rose-600 text-xs py-6 text-center">
|
||||||
加载失败:{error}
|
加载失败:{error}
|
||||||
</div>
|
</div>
|
||||||
) : content === null ? null : renderAsMarkdown ? (
|
) : content === null ? null : renderAsMermaid ? (
|
||||||
|
mermaidSvg ? (
|
||||||
|
<div
|
||||||
|
className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"
|
||||||
|
dangerouslySetInnerHTML={{ __html: mermaidSvg }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-12 text-slate-400">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
||||||
|
<span className="text-xs">正在生成图表预览</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : renderAsMarkdown ? (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
|
||||||
{content}
|
{content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ function shouldPreviewInModal(file: { name: string; mimeType: string | null }):
|
|||||||
const mime = file.mimeType ?? "";
|
const mime = file.mimeType ?? "";
|
||||||
if (mime.startsWith("text/")) return true;
|
if (mime.startsWith("text/")) return true;
|
||||||
if (["application/json"].includes(mime)) return true;
|
if (["application/json"].includes(mime)) return true;
|
||||||
return /\.(md|markdown|txt|json|csv|log|yml|yaml|xml|html?)$/i.test(file.name);
|
return /\.(md|markdown|mmd|txt|json|csv|log|yml|yaml|xml|html?)$/i.test(file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(n: number): string {
|
function formatBytes(n: number): string {
|
||||||
|
|||||||
@@ -288,6 +288,8 @@ class RoundtableJobExecutor:
|
|||||||
idx = next((i for i, r in enumerate(runs) if r.get("id") == run_id), -1)
|
idx = next((i for i, r in enumerate(runs) if r.get("id") == run_id), -1)
|
||||||
created = runs[idx].get("createdAt") if idx >= 0 else datetime.now(UTC).isoformat()
|
created = runs[idx].get("createdAt") if idx >= 0 else datetime.now(UTC).isoformat()
|
||||||
prev_step3 = runs[idx].get("step3") if idx >= 0 else None
|
prev_step3 = runs[idx].get("step3") if idx >= 0 else None
|
||||||
|
# 本次有效 step3:新报告优先,否则保留上次(running 中间态 step3=None 不抹掉已存的)。
|
||||||
|
effective_step3 = step3 if step3 is not None else prev_step3
|
||||||
run = {
|
run = {
|
||||||
"id": run_id,
|
"id": run_id,
|
||||||
"title": "后台研讨",
|
"title": "后台研讨",
|
||||||
@@ -308,17 +310,21 @@ class RoundtableJobExecutor:
|
|||||||
"seatStubMessages": _seat_stub_messages(dialogues),
|
"seatStubMessages": _seat_stub_messages(dialogues),
|
||||||
"orchestrationMode": params.mode,
|
"orchestrationMode": params.mode,
|
||||||
"chain": params.chain,
|
"chain": params.chain,
|
||||||
"step3": step3 if step3 is not None else prev_step3,
|
"step3": effective_step3,
|
||||||
"jobId": params.job_id,
|
"jobId": params.job_id,
|
||||||
"jobStatus": status,
|
"jobStatus": status,
|
||||||
}
|
}
|
||||||
runs = [run if i == idx else r for i, r in enumerate(runs)] if idx >= 0 else [*runs, run]
|
runs = [run if i == idx else r for i, r in enumerate(runs)] if idx >= 0 else [*runs, run]
|
||||||
step2["runs"] = runs
|
step2["runs"] = runs
|
||||||
step2["activeRunId"] = run_id
|
step2["activeRunId"] = run_id
|
||||||
|
# 顶层 draft.step3 也写一份(同 Step2 把对话写进 run 的同一次落库一并带上)。
|
||||||
|
# 前端 hydrateStep3 用**顶层** draft.step3 重建报告气泡(结果绘制的聊天记录 + HTML 预览);
|
||||||
|
# 只存 run 内 step3 时,加载会被 hydrateStep3(draft.step3=None) 覆盖成空 → 报告聊天记录丢失。
|
||||||
|
update_kwargs: dict[str, Any] = {"step2": step2, "furthest_step": 3 if status == "done" else 2}
|
||||||
|
if effective_step3 is not None:
|
||||||
|
update_kwargs["step3"] = effective_step3
|
||||||
try:
|
try:
|
||||||
await self._draft_store.update_draft(
|
await self._draft_store.update_draft(params.draft_id, params.user_id, **update_kwargs)
|
||||||
params.draft_id, params.user_id, step2=step2, furthest_step=3 if status == "done" else 2
|
|
||||||
)
|
|
||||||
except Exception: # noqa: BLE001 — 写草稿失败不影响作业本身
|
except Exception: # noqa: BLE001 — 写草稿失败不影响作业本身
|
||||||
logger.warning("write run to draft %s failed", params.draft_id, exc_info=True)
|
logger.warning("write run to draft %s failed", params.draft_id, exc_info=True)
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -138,7 +138,11 @@ async def run_orchestration(
|
|||||||
|
|
||||||
_SEAT_NO_ASK = (
|
_SEAT_NO_ASK = (
|
||||||
"注意:这是自动化圆桌研讨,没有用户在线应答——**请勿向用户提问或等待确认**,"
|
"注意:这是自动化圆桌研讨,没有用户在线应答——**请勿向用户提问或等待确认**,"
|
||||||
"直接基于已知信息做出合理假设并产出完整的结构化交付(Markdown)。"
|
"直接基于已知信息做出合理假设,产出完整的结构化交付。\n"
|
||||||
|
"**交付方式(与前端手动研讨一致)**:请用 write_file 工具把你这一轮的完整交付整理成一份 "
|
||||||
|
"Markdown 文件,写入 /mnt/user-data/outputs/ 下(文件名体现你的职责,如 `情报收集.md`、"
|
||||||
|
"`风险评估.md`);同时在回复正文里给出这份交付的内容本身(供总控汇总),"
|
||||||
|
"不要只回复『已写入文件』这类空话。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""一次性巡检:把最新圆桌草稿里「后台作业 run」的关键字段 dump 到文件,
|
||||||
|
用于验证维度3 存储 + 报告 HTML + 席位 md 文件是否都对齐了。
|
||||||
|
|
||||||
|
用法(在 backend 目录):
|
||||||
|
PYTHONPATH=. uv run python scripts/inspect_latest_roundtable_run.py
|
||||||
|
|
||||||
|
输出写到 scripts/_roundtable_run_report.txt(UTF-8),直接打开看,避免 PowerShell GBK 乱码。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DB = Path(".deer-flow/data/deerflow.db")
|
||||||
|
OUT = Path("scripts/_roundtable_run_report.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def _loads(v):
|
||||||
|
if isinstance(v, (bytes, bytearray)):
|
||||||
|
v = v.decode("utf-8", "replace")
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
return json.loads(v)
|
||||||
|
except Exception:
|
||||||
|
return v
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
con = sqlite3.connect(str(DB))
|
||||||
|
con.row_factory = sqlite3.Row
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
# 最新一条有 step2.runs 的草稿
|
||||||
|
rows = con.execute(
|
||||||
|
"SELECT id, step2, step3 FROM roundtable_drafts ORDER BY updated_at DESC LIMIT 5"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
draft = None
|
||||||
|
top_step3 = None
|
||||||
|
for r in rows:
|
||||||
|
s2 = _loads(r["step2"]) or {}
|
||||||
|
if isinstance(s2, dict) and s2.get("runs"):
|
||||||
|
draft = (r["id"], s2)
|
||||||
|
top_step3 = _loads(r["step3"])
|
||||||
|
break
|
||||||
|
|
||||||
|
if not draft:
|
||||||
|
lines.append("没找到带 step2.runs 的草稿。")
|
||||||
|
OUT.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
print(f"written: {OUT}")
|
||||||
|
return
|
||||||
|
|
||||||
|
draft_id, s2 = draft
|
||||||
|
runs = s2.get("runs") or []
|
||||||
|
job_run = next((x for x in runs if str(x.get("id", "")).startswith("job-")), runs[-1])
|
||||||
|
|
||||||
|
lines.append(f"draft_id = {draft_id}")
|
||||||
|
lines.append(f"run.id = {job_run.get('id')} jobStatus = {job_run.get('jobStatus')} hasConsensus = {job_run.get('hasConsensus')}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 维度3:threadIds ──
|
||||||
|
lines.append("【threadIds】(应为真实 map,不是 null)")
|
||||||
|
lines.append(json.dumps(job_run.get("threadIds"), ensure_ascii=False, indent=2))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 维度3:各对话的 steps / agentThreadId / time ──
|
||||||
|
lines.append("【step2RoundtableDialogues 摘要】")
|
||||||
|
for d in job_run.get("step2RoundtableDialogues") or []:
|
||||||
|
steps = d.get("steps") or []
|
||||||
|
step_keys = [s.get("key") for s in steps]
|
||||||
|
lines.append(
|
||||||
|
f"- [{d.get('sender')}] time={d.get('time')!r} thread={d.get('agentThreadId')!r} "
|
||||||
|
f"steps={step_keys} content_len={len(d.get('content') or '')}"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 席位 md 文件:seatStubMessages 里有没有 write_file ──
|
||||||
|
lines.append("【seatStubMessages】(席位写的文件 → 文件卡片数据源)")
|
||||||
|
stubs = job_run.get("seatStubMessages") or {}
|
||||||
|
if not stubs:
|
||||||
|
lines.append(" (空 —— 席位没有写文件 / 没采集到)")
|
||||||
|
for tid, msgs in stubs.items():
|
||||||
|
for m in msgs:
|
||||||
|
for tc in m.get("tool_calls") or []:
|
||||||
|
args = tc.get("args") or {}
|
||||||
|
lines.append(f" thread={tid} {tc.get('name')} path={args.get('path')!r} content_len={len(str(args.get('content') or ''))}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 报告 HTML(P0)──
|
||||||
|
lines.append("【step3 报告】")
|
||||||
|
step3 = job_run.get("step3") or {}
|
||||||
|
html = step3.get("html") or ""
|
||||||
|
lines.append(f" html_len = {len(html)}")
|
||||||
|
lines.append(f" 含 <html 标签 = {'<html' in html.lower()} (True=真 HTML / False=纯文字回退=P0未解决)")
|
||||||
|
lines.append(f" summary = {(step3.get('summary') or '')[:120]!r}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 顶层 draft.step3(结果绘制聊天记录靠它重建报告气泡)──
|
||||||
|
lines.append("【顶层 draft.step3】(前端 hydrateStep3 用它重建报告聊天记录气泡)")
|
||||||
|
if not top_step3:
|
||||||
|
lines.append(" ❌ 空 —— 报告聊天记录加载后会丢失(本次修复点:应非空)")
|
||||||
|
else:
|
||||||
|
th = (top_step3.get("html") or "") if isinstance(top_step3, dict) else ""
|
||||||
|
lines.append(f" ✅ 已存 html_len={len(th)} summary={(top_step3.get('summary') or '')[:80]!r}" if isinstance(top_step3, dict) else f" {top_step3!r}")
|
||||||
|
|
||||||
|
OUT.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
print(f"written: {OUT.resolve()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user