代码修改

This commit is contained in:
fengxiangdebaozi committed 2026-06-05 18:58:28 +08:00
1 parent 9bbd04d6cf
commit b0fdeab995
15 files changed
+467 -90

No files matched your search

+3 -3
View File
@@ -107,7 +107,7 @@ http://localhost:5173/#/embed/chats/new?sessionId=...&message=...&username=guest
## 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。
@@ -315,7 +315,7 @@ document.getElementById("deerflow-embed").src =
### 5.2 轮询直到结束
```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) {
const res = await fetch(
@@ -348,7 +348,7 @@ const timer = setInterval(async () => {
### 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 环境变量中配置:
+1 -1
View File
@@ -1,5 +1,5 @@
.sidebar-contnet {
position: fixed;
position: absolute;
left: 0;
top: 4rem;
z-index: 40;
@@ -48,11 +48,17 @@ import { useI18n } from "@/core/i18n/hooks";
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
import { installSkill } from "@/core/skills/api";
import { streamdownPlugins } from "@/core/streamdown";
import {
checkCodeFile,
getFileName,
isImageFile,
} from "@/core/utils/files";
import {
checkCodeFile,
getFileName,
isImageFile,
} from "@/core/utils/files";
import {
downloadMermaidSvgFile,
isMermaidFile,
openMermaidSvgInNewTab,
renderMermaidToSvg,
} from "@/core/utils/mermaid";
import { getBackendBaseURL } from "@/core/config";
import { resolveArtifactURL } from "@/core/artifacts/utils";
import { env } from "@/env";
@@ -129,10 +135,14 @@ export function ArtifactFileDetail({
return { isCodeFile: true, language: "markdown" };
}
return checkCodeFile(filepath);
}, [filepath, useWriteFileStreaming, isSkillFile]);
const isSupportPreview = useMemo(() => {
return language === "html" || language === "markdown";
}, [language]);
}, [filepath, useWriteFileStreaming, isSkillFile]);
const isSupportPreview = useMemo(() => {
return (
language === "html" ||
language === "markdown" ||
language === "mermaid"
);
}, [language]);
const { content, isApplyingEdit } = useArtifactContent({
threadId,
filepath: filepathFromProps,
@@ -150,11 +160,12 @@ export function ArtifactFileDetail({
const displayContent = canEditArtifact ? draftContent : (content ?? "");
const isHtmlStreaming = language === "html" && thread.isLoading;
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
const fileTypeLabel = useMemo(() => {
if (isImageFile(filepath)) return "IMG";
if (language === "html") return "HTML";
if (language === "markdown") return "MD";
if (isSkillFile) return "SKILL";
const fileTypeLabel = useMemo(() => {
if (isImageFile(filepath)) return "IMG";
if (isMermaidFile(filepath) || language === "mermaid") return "MMD";
if (language === "html") return "HTML";
if (language === "markdown") return "MD";
if (isSkillFile) return "SKILL";
if (language) return language.toUpperCase();
return "FILE";
}, [filepath, isSkillFile, language]);
@@ -423,9 +434,16 @@ export function ArtifactFileDetail({
toast.error("文件内容为空,暂时无法打开");
return;
}
const blobType =
language === "html"
? "text/html;charset=utf-8"
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 =
language === "html"
? "text/html;charset=utf-8"
: language === "markdown"
? "text/markdown;charset=utf-8"
: "text/plain;charset=utf-8";
@@ -435,15 +453,22 @@ export function ArtifactFileDetail({
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
}, [displayContent, language]);
const handleDownloadWriteFile = useCallback(() => {
const source = displayContent ?? "";
const handleDownloadWriteFile = useCallback(() => {
const source = displayContent ?? "";
if (!source.trim()) {
toast.error("文件内容为空,暂时无法下载");
return;
}
const blobType =
language === "html"
? "text/html;charset=utf-8"
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 =
language === "html"
? "text/html;charset=utf-8"
: language === "markdown"
? "text/markdown;charset=utf-8"
: "text/plain;charset=utf-8";
@@ -562,14 +587,27 @@ export function ArtifactFileDetail({
icon={SquareArrowOutUpRightIcon}
label={t.common.openInNewWindow}
tooltip={t.common.openInNewWindow}
onClick={async () => {
if (useWriteFileStreaming) {
handleOpenWriteFileInNewTab();
return;
}
try {
await openArtifactInNewTab({
filepath,
onClick={async () => {
if (useWriteFileStreaming) {
handleOpenWriteFileInNewTab();
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 {
await openArtifactInNewTab({
filepath,
threadId,
isMock,
publicShareCode,
@@ -614,14 +652,27 @@ export function ArtifactFileDetail({
icon={DownloadIcon}
label={t.common.download}
tooltip={t.common.download}
onClick={async () => {
if (useWriteFileStreaming) {
handleDownloadWriteFile();
return;
}
try {
await downloadArtifactFile({
filepath,
onClick={async () => {
if (useWriteFileStreaming) {
handleDownloadWriteFile();
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 {
await downloadArtifactFile({
filepath,
threadId,
isMock,
publicShareCode,
@@ -666,13 +717,15 @@ export function ArtifactFileDetail({
<ArrowDownIcon className="size-4" />
滑动到底部并继续自动下滑
</button>
)}
{isSupportPreview &&
viewMode === "preview" &&
(language === "markdown" || language === "html") && (
<ArtifactFilePreview
content={desensitize(displayContent ?? "")}
language={language ?? "text"}
)}
{isSupportPreview &&
viewMode === "preview" &&
(language === "markdown" ||
language === "html" ||
language === "mermaid") && (
<ArtifactFilePreview
content={desensitize(displayContent ?? "")}
language={language ?? "text"}
threadId={threadId}
publicShareCode={publicShareCode}
animated={showPreviewSwitchPulse}
@@ -768,13 +821,15 @@ export function ArtifactFilePreview({
publicShareCode?: string;
animated?: 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(() => {
if (language !== "html") {
setHtmlPreviewUrl(undefined);
return;
useEffect(() => {
if (language !== "html") {
setHtmlPreviewUrl(undefined);
return;
}
// 必须带 charset=utf-8:blob: URL 的 Content-Type 不含 charset 时,iframe 会按
@@ -785,9 +840,40 @@ export function ArtifactFilePreview({
setHtmlPreviewUrl(url);
return () => {
URL.revokeObjectURL(url);
};
}, [content, language]);
URL.revokeObjectURL(url);
};
}, [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") {
return (
@@ -821,9 +907,9 @@ export function ArtifactFilePreview({
);
}
if (language === "html") {
return (
<iframe
if (language === "html") {
return (
<iframe
className={cn(
"size-full bg-white",
animated && "animate-[sandbox-preview-enter_320ms_ease-out]",
@@ -832,8 +918,41 @@ export function ArtifactFilePreview({
sandbox="allow-scripts allow-forms"
src={htmlPreviewUrl}
/>
);
}
return null;
}
);
}
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;
}
+1 -1
View File
@@ -5,7 +5,7 @@ const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || "";
export const TOOLS_TOKEN_STORAGE_KEY = "tools_token";
export const EXTERNAL_USER_ID_STORAGE_KEY = "userId";
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 = {
data?: {
+1 -1
View File
@@ -1,4 +1,4 @@
import {
import {
CompassIcon,
GraduationCapIcon,
ImageIcon,
@@ -54,15 +54,13 @@ export const sidebarMenuItems: MenuItem[] = [
icon: HelpCircle,
children: [
{ 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: "qa-roundtable",
// label: "圆桌规划",
// icon: Bot,
// path: "/page/roundtable/planning",
// },
{
id: "multi-agent-roundtable",
label: "多智能体会商",
icon: Users,
path: "/page/strategy/qa/multi-agent-roundtable",
},
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
+5 -4
View File
@@ -94,10 +94,11 @@ const extensionMap: Record<string, string> = {
ini: "ini",
env: "dotenv",
// Markdown & Docs
md: "markdown",
mdx: "mdx",
rst: "rst",
// Markdown & Docs
md: "markdown",
mdx: "mdx",
mmd: "mermaid",
rst: "rst",
// SQL
sql: "sql",
+75
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
"use client";
"use client";
import {
ArrowLeftIcon,
+10
View File
@@ -39,6 +39,8 @@ import TaskAnalyzes from "@/strategy-components/components/task-components/TaskA
import { ActionAnalyzes } from "@/strategy-components/components/task-components/ActionAnalyzes";
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
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() {
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 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 { apiFetch } from "@/core/api/fetch-client";
import {
downloadMermaidSvgFile,
isMermaidFile,
renderMermaidToSvg,
} from "@/core/utils/mermaid";
import { buildArtifactDownloadUrl } from "@/roundtable-planning/api/multi-agent";
interface ArtifactPreviewModalProps {
@@ -61,6 +66,11 @@ function isMarkdown(mimeType: string | null, name: string): boolean {
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> = ({
threadId,
path,
@@ -71,6 +81,7 @@ export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
const [content, setContent] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [mermaidSvg, setMermaidSvg] = useState<string | null>(null);
// Esc closes the modal. Attached once per mount.
useEffect(() => {
@@ -116,11 +127,38 @@ export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
const handleDownload = useCallback(() => {
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 });
window.open(url, "_blank", "noopener,noreferrer");
}, [threadId, path]);
}, [content, mimeType, name, path, threadId]);
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 (
<div
@@ -170,7 +208,19 @@ export const ArtifactPreviewModal: React.FC<ArtifactPreviewModalProps> = ({
<div className="text-rose-600 text-xs py-6 text-center">
加载失败:{error}
</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}>
{content}
</ReactMarkdown>
@@ -648,7 +648,7 @@ function shouldPreviewInModal(file: { name: string; mimeType: string | null }):
const mime = file.mimeType ?? "";
if (mime.startsWith("text/")) 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 {