fix: adapt frontend for legacy Chrome and runtime config

This commit is contained in:
fengxiangdebaozi committed 2026-07-11 16:55:08 +08:00
1 parent db2f21191c
commit 388a9133fc
84 files changed
+1025 -367

No files matched your search

+1 -1
View File
@@ -42,7 +42,7 @@ VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
VITE_NOTEBOOK_API_BASE_URL=http://47.88.25.99:8000
VITE_AIPI_FRONTEND_URL=http://47.88.25.99:3000
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
VITE_AIPI_CONSUMER_URL=http://10.19.200.5:8002/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=http://present.a.uat.4.cn/present/domain
# B 系统知识库 iframe 根地址
+1
View File
@@ -16,6 +16,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CM 助手</title>
<script src="/runtime-config.js"></script>
</head>
<body>
<div id="root"></div>
+4 -1
View File
@@ -10,7 +10,9 @@
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
@@ -110,6 +112,7 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.4",
"lightningcss": "^1.30.2",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
+9 -3
View File
@@ -1,5 +1,11 @@
import tailwindcss from "@tailwindcss/postcss";
import legacyColorFallback from "./scripts/postcss-legacy-color-fallback.mjs";
import lightningCssLegacyChrome from "./scripts/postcss-lightningcss-chrome109.mjs";
export default {
plugins: {
"@tailwindcss/postcss": {},
},
plugins: [
tailwindcss(),
legacyColorFallback(),
lightningCssLegacyChrome(),
],
};
+96
View File
@@ -0,0 +1,96 @@
// 前端运行时配置文件
//
// 说明:
// 1. 本文件会在打包时原样复制到 dist/runtime-config.js。
// 2. 应用启动时会优先读取 window.__CM_RUNTIME_CONFIG__ 里的配置。
// 3. 部署后如果需要切换接口地址,直接修改 dist/runtime-config.js 即可,不需要重新 pnpm build。
// 4. 这里的默认值来自 .env.network;如某个环境不用某项,可以留空字符串。
// 5. 布尔开关也用字符串形式填写:"true" / "false"。
window.__CM_RUNTIME_CONFIG__ = {
// DeerFlow Gateway 地址:前端自有后端接口,例如 /api/agents、/api/auth、/api/threads 等。
// VITE_BACKEND_BASE_URL: "http://10.19.200.5:2026",
// LangGraph 流式对话接口地址:普通问答、智能体对话等流式能力使用。
// VITE_LANGGRAPH_BASE_URL: "http://10.19.200.5:2026/api",
// Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_LANGGRAPH_BASE_URL。
VITE_CANVAS_LANGGRAPH_BASE_URL: "",
// 轻应用「新建笔记」系列接口地址。
VITE_NOTE_API_BASE_URL: "http://47.88.25.99:8000",
// CMZS 后端接口:任务管理、任务分析、三情分析、行动分析等业务接口。
VITE_API_BASE_URL: "https://ch1.b.uat.4.cn/zncm",
// 佳来模型对话接口。
VITE_AIPI_CHAT_URL: "https://ch1.b.uat.4.cn/gpt",
// 多轮检索/生成接口。
VITE_AIPI_GENERATE_CHAT_URL: "https://ch1.b.uat.4.cn/jialai_b1_chat",
// 任务问答接口。
VITE_AIPI_TASK_CHAT_URL: "https://ch1.b.uat.4.cn/qq_b1_chat",
// B 系统知识库前端地址:用于打开知识详情、原文详情等。
VITE_AIPI_KNOWLEDGE_URL: "https://chlib.b.uat.4.cn/knowlibrary",
// B1 COH 系统地址。
VITE_AIPI_B1COH_URL: "https://ch1.b.uat.4.cn/web",
// B2 行为体/网络资源系统地址。
VITE_AIPI_B2ACTOR_URL: "https://ty.b.uat.4.cn",
// B2 网络 XWT iframe 路径。
VITE_AIPI_B2ACTOR_XWT_PATH: "/fromB/toXWT?isFrame=1",
// 知识库 iframe 地址换取接口。
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL: "https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl",
// 知识库 iframe 皮肤参数。
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN: "red",
// MopeChat 接口地址。
VITE_AIPI_MOPECHAT_URL: "https://ch1.b.uat.4.cn/likun_b1_mope",
// 工作室/空间后端接口:空间列表、空间详情、素材上传等使用。
VITE_AIPI_BACKENP_URL: "http://47.88.25.99:8000",
// 工作室/空间后端别名:兼容旧配置命名,通常与 VITE_AIPI_BACKENP_URL 保持一致。
VITE_NOTEBOOK_API_BASE_URL: "http://47.88.25.99:8000",
// 工作室/空间前端地址:用于 auth/callback、新建对话等跳转。
VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000",
// Consumer 接口:深链任务详情 cop-task-detail、研判情报报告 selectSqReport 等使用。
VITE_AIPI_CONSUMER_URL: "http://10.19.200.5:8002/consumer",
// A 系统跳转地址:人物、组织、事件等列表项打开链接使用。
VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain",
// B 系统 knowes 根地址。
VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes",
// MaxKey 鉴权地址:onlineTicket 换 token 使用。
VITE_AUTH_BASE_URL: "https://auth.y.sit.4.cn",
// PG 中任务 iframe 地址。
VITE_AIPI_PGLINKTO_URL: "https://pg.b.uat.4.cn",
// XK 中任务 iframe 地址。
VITE_AIPI_XKLINKTO_URL: "https://kz.b.uat.4.cn",
// 是否使用真实任务看板数据:true=真实接口;false=本地 mock。
VITE_USE_REAL_DATA: "true",
// 登录深链任务详情是否使用本地 mock:false=真实接口;true=本地 mock。
VITE_MOCK_COP_TASK_DETAIL: "false",
// 大屏绘制 selectSqReport 是否使用本地 mock:false=真实接口;true=本地 mock。
VITE_MOCK_SQ_REPORT: "false",
// 静态站点模式开关;一般部署业务系统时保持 false。
VITE_STATIC_WEBSITE_ONLY: "false",
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。
VITE_GITHUB_OAUTH_TOKEN: "",
};
@@ -0,0 +1,109 @@
import fs from "node:fs";
import path from "node:path";
import postcss from "postcss";
const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i;
const assetsDir = path.resolve("dist/assets");
if (!fs.existsSync(assetsDir)) {
throw new Error("dist/assets 不存在,请先运行 pnpm build");
}
const cssFiles = fs
.readdirSync(assetsDir)
.filter((file) => file.endsWith(".css"));
if (cssFiles.length === 0) {
throw new Error("dist/assets 中没有 CSS 构建产物");
}
const failures = [];
let modernDeclarations = 0;
const requiredLegacyFallbacks = [
{
file: "src/components/ui/textarea.tsx",
pattern: /rows=\{rows\s*\?\?\s*3\}/,
description: "Textarea needs a native rows fallback",
},
{
file: "src/components/ai-elements/prompt-input.tsx",
pattern: /<InputGroup className="h-auto min-h-16">/,
description: "Prompt textarea must not rely on :has() for its parent height",
},
];
for (const file of cssFiles) {
const root = postcss.parse(
fs.readFileSync(path.join(assetsDir, file), "utf8"),
{ from: file },
);
root.walkDecls((declaration) => {
if (!unsupportedColor.test(declaration.value)) return;
modernDeclarations += 1;
let ancestor = declaration.parent;
while (ancestor) {
if (
ancestor.type === "atrule" &&
ancestor.name === "supports" &&
unsupportedColor.test(ancestor.params)
) {
return;
}
ancestor = ancestor.parent;
}
let previous = declaration.prev();
while (previous?.type === "comment") previous = previous.prev();
const hasSafeFallback =
!declaration.prop.startsWith("--") &&
previous?.type === "decl" &&
previous.prop === declaration.prop &&
!unsupportedColor.test(previous.value);
if (!hasSafeFallback) {
failures.push(
`${file}:${declaration.source?.start?.line ?? "?"} ${declaration.prop}: ${declaration.value}`,
);
}
});
}
function walk(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const fullPath = path.join(directory, entry.name);
return entry.isDirectory() ? walk(fullPath) : [fullPath];
});
}
for (const file of walk(path.resolve("src"))) {
if (!/\.[jt]sx?$/.test(file)) continue;
const source = fs.readFileSync(file, "utf8");
if (unsupportedColor.test(source)) {
failures.push(
`${path.relative(process.cwd(), file)} contains a runtime-only modern color function`,
);
}
}
for (const fallback of requiredLegacyFallbacks) {
const source = fs.readFileSync(path.resolve(fallback.file), "utf8");
if (!fallback.pattern.test(source)) {
failures.push(`${fallback.file}: ${fallback.description}`);
}
}
if (failures.length > 0) {
console.error("Legacy Chrome CSS compatibility check failed:\n");
console.error(failures.slice(0, 50).join("\n"));
if (failures.length > 50) {
console.error(`... and ${failures.length - 50} more`);
}
process.exit(1);
}
console.log(
`Legacy Chrome (100+) CSS compatibility check passed (${modernDeclarations} guarded modern declarations).`,
);
@@ -0,0 +1,70 @@
/**
* Tailwind 4 gracefully falls back from color-mix() to the base semantic
* color, but that turns utilities such as bg-primary/5 into an opaque fill on
* Chrome 109. Replace that fallback with an sRGB-channel based alpha color.
* The modern color-mix() declaration remains in its @supports block.
*/
const LEGACY_RGB_TOKENS = new Set([
"accent",
"background",
"border",
"brand-200",
"card",
"destructive",
"foreground",
"input",
"muted",
"muted-foreground",
"primary",
"primary-foreground",
"ring",
"secondary",
"sidebar-accent",
"sidebar-border",
"sidebar-foreground",
"workspace-brand",
]);
const TRANSPARENT_MIX_RE =
/color-mix\(\s*in\s+[\w-]+\s*,\s*var\(--([\w-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)/i;
function legacyAlphaColor(value) {
const match = value.match(TRANSPARENT_MIX_RE);
if (!match || !LEGACY_RGB_TOKENS.has(match[1])) return null;
const [, token, percentage] = match;
return `rgb(var(--legacy-${token}-rgb) / ${percentage}%)`;
}
export default function legacyColorFallback() {
return {
postcssPlugin: "postcss-legacy-color-fallback",
OnceExit(root) {
root.walkAtRules("supports", (atRule) => {
if (!atRule.params.includes("color-mix")) return;
atRule.walkDecls((modernDeclaration) => {
const fallbackValue = legacyAlphaColor(modernDeclaration.value);
if (!fallbackValue) return;
// Tailwind emits the old-browser declaration immediately before
// the nested @supports block, in the same utility/variant rule.
let fallbackDeclaration = atRule.prev();
while (fallbackDeclaration?.type === "comment") {
fallbackDeclaration = fallbackDeclaration.prev();
}
if (
fallbackDeclaration?.type === "decl" &&
fallbackDeclaration.prop === modernDeclaration.prop
) {
fallbackDeclaration.value = fallbackValue;
}
});
});
},
};
}
legacyColorFallback.postcss = true;
@@ -0,0 +1,33 @@
import postcss from "postcss";
import { transform } from "lightningcss";
const LEGACY_CHROME = 100 << 16;
/**
* Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the
* resulting CSS to Chrome 100-compatible syntax for both dev and production.
*/
export default function lightningCssLegacyChrome() {
return {
postcssPlugin: "postcss-lightningcss-legacy-chrome",
OnceExit(root, { result }) {
const filename = root.source?.input.file ?? result.opts.from ?? "style.css";
const output = transform({
filename,
code: Buffer.from(root.toString()),
targets: { chrome: LEGACY_CHROME },
minify: false,
});
const compiled = postcss.parse(output.code.toString(), { from: filename });
root.removeAll();
root.append(compiled.nodes);
for (const warning of output.warnings) {
result.warn(warning.message, { plugin: "lightningcss" });
}
},
};
}
lightningCssLegacyChrome.postcss = true;
@@ -846,7 +846,10 @@ export const PromptInput = ({
ref={formRef}
{...props}
>
<InputGroup>{children}</InputGroup>
{/* Do not rely on InputGroup's :has(textarea) selector here: Chrome
versions before 105 ignore :has(), retain h-9, and clip the whole
multi-line composer to a single row. */}
<InputGroup className="h-auto min-h-16">{children}</InputGroup>
</form>
</PromptInputValidationContext.Provider>
);
@@ -875,6 +878,7 @@ export type PromptInputTextareaProps = ComponentProps<
export const PromptInputTextarea = ({
onChange,
onInput,
className,
placeholder = "What would you like to know?",
...props
@@ -883,6 +887,37 @@ export const PromptInputTextarea = ({
const attachments = usePromptInputAttachments();
const sanitizeIncomingFiles = usePromptInputValidation();
const [isComposing, setIsComposing] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const resizeForLegacyChrome = useCallback((textarea: HTMLTextAreaElement) => {
if (
typeof CSS !== "undefined" &&
typeof CSS.supports === "function" &&
CSS.supports("field-sizing", "content")
) {
return;
}
// Chrome 109 ignores field-sizing: content. Preserve the same 4rem–12rem
// autosize range used by min-h-16/max-h-48 instead of leaving the prompt
// stuck at its minimum height.
textarea.style.height = "auto";
const height = Math.max(64, Math.min(textarea.scrollHeight, 192));
textarea.style.height = `${height}px`;
textarea.style.overflowY = textarea.scrollHeight > 192 ? "auto" : "hidden";
}, []);
// onInput does not run for a controlled value filled by a suggested question
// or restored draft. Resize once after every render in legacy Chrome too.
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
const frame = window.requestAnimationFrame(() => {
resizeForLegacyChrome(textarea);
});
return () => window.cancelAnimationFrame(frame);
}, [controller?.textInput.value, resizeForLegacyChrome]);
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
if (e.key === "Enter") {
@@ -965,9 +1000,15 @@ export const PromptInputTextarea = ({
<InputGroupTextarea
className={cn("field-sizing-content max-h-48 min-h-16", className)}
name="message"
ref={textareaRef}
rows={3}
onCompositionEnd={() => setIsComposing(false)}
onCompositionStart={() => setIsComposing(true)}
onKeyDown={handleKeyDown}
onInput={(event) => {
resizeForLegacyChrome(event.currentTarget);
onInput?.(event);
}}
onPaste={handlePaste}
placeholder={placeholder}
{...props}
@@ -26,6 +26,7 @@ import {
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { getStoredAuth } from "@/core/auth";
import { readRuntimeEnv } from "@/env";
import { queryClient } from "@/components/query-client-provider";
import { Checkbox } from "@/components/ui/checkbox";
import Empty from "./empty";
@@ -303,11 +304,11 @@ const DocumentUploadPopupContent: FC<{
const searchSpaceId = String(searchSpace?.id);
// 从环境变量获取配置
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
const consumerUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || "http://ch1.b.uat.4.cn/consumer";
const presentUrl = import.meta.env.VITE_AIPI_PRESENT_URL || "http://present.a.uat.4.cn/present/domain";
const knowesUrl = import.meta.env.VITE_AIPI_KNOWES_URL || "http://ch1.b.uat.4.cn/knowes";
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL || "http://chlib.b.uat.4.cn/knowlibrary";
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || "http://localhost:8001";
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || "http://ch1.b.uat.4.cn/consumer";
const presentUrl = readRuntimeEnv("VITE_AIPI_PRESENT_URL") || "http://present.a.uat.4.cn/present/domain";
const knowesUrl = readRuntimeEnv("VITE_AIPI_KNOWES_URL") || "http://ch1.b.uat.4.cn/knowes";
const knowledgeUrl = readRuntimeEnv("VITE_AIPI_KNOWLEDGE_URL") || "http://chlib.b.uat.4.cn/knowlibrary";
const [searchText, setSearchText] = useState("");
const [searchValue, setSearchValue] = useState("");
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { getStoredAuth } from "@/core/auth";
import { authenticatedFetch } from "@/components/assistant-ui/document-upload-popup";
import { readRuntimeEnv } from "@/env";
interface NetworkActorProps {
@@ -186,9 +187,9 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
const [actorCount, setActorCount] = useState<number>(0);
const [directionType, setDirectionType] = useState<string>('TW');
const directionValue = useRef<string>('TW');
const consumerUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || 'https://ch1.b.uat.4.cn/consumer';
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || 'https://ch1.b.uat.4.cn/consumer';
const backendUrl = typeof window !== 'undefined'
? (import.meta.env.VITE_BACKEND_BASE_URL || 'http://localhost:8001')
? (readRuntimeEnv("VITE_BACKEND_BASE_URL") || 'http://localhost:8001')
: 'http://localhost:8001';
const [selectedSearchSources, setSelectedSearchSources] = useState<UserDataItem[]>([]);
const [isImporting, setIsImporting] = useState(false);
@@ -508,8 +508,9 @@ export default function ProgressiveSkillsAnimation() {
"deploying",
"done",
].includes(phase) && (
<div className="text-base text-zinc-300">
<span className="text-purple-400">✨</span> Found 3 skills
<div className="flex items-center gap-2 text-base text-zinc-300">
<Sparkles size={16} className="text-purple-400" />
<span>Found 3 skills</span>
</div>
)}
@@ -526,8 +527,9 @@ export default function ProgressiveSkillsAnimation() {
].includes(phase) && (
<div className="mt-4">
<hr className="mb-3 border-zinc-700" />
<div className="mb-3 text-zinc-300">
🔬 Researching...
<div className="mb-3 inline-flex items-center gap-2 text-zinc-300">
<Search size={16} className="text-blue-400" />
<span>Researching...</span>
</div>
<div className="mb-3 space-y-2">
{/* Loading SKILL.md */}
@@ -623,7 +625,10 @@ export default function ProgressiveSkillsAnimation() {
className="mt-4"
>
<hr className="mb-3 border-zinc-700" />
<div className="mb-3 text-zinc-300">🔨 Building...</div>
<div className="mb-3 inline-flex items-center gap-2 text-zinc-300">
<FileText size={16} className="text-zinc-400" />
<span>Building...</span>
</div>
<div className="mb-3 flex items-center gap-2 pl-4 text-zinc-400">
<FileText size={16} />
<span>Loading frontend-design/SKILL.md...</span>
@@ -653,7 +658,10 @@ export default function ProgressiveSkillsAnimation() {
className="mt-4"
>
<hr className="mb-3 border-zinc-700" />
<div className="mb-3 text-zinc-300">🚀 Deploying...</div>
<div className="mb-3 inline-flex items-center gap-2 text-zinc-300">
<Terminal size={16} className="text-zinc-400" />
<span>Deploying...</span>
</div>
<div className="mb-3 space-y-2">
<div className="flex items-center gap-2 pl-4 text-zinc-400">
<FileText size={16} />
@@ -676,8 +684,9 @@ export default function ProgressiveSkillsAnimation() {
animate={{ opacity: 1, scale: 1 }}
className="mt-4 rounded-xl border border-green-500/30 bg-green-500/10 p-4"
>
<div className="text-lg font-medium text-green-500">
✅ Live at biotech-startup.vercel.app
<div className="inline-flex items-center gap-2 text-lg font-medium text-green-500">
<Check size={18} />
<span>Live at biotech-startup.vercel.app</span>
</div>
</motion.div>
)}
@@ -35,27 +35,27 @@ export function SandboxSection({ className }: { className?: string }) {
$ pip install -r requirements.txt
</TypingAnimation>
<AnimatedSpan delay={2000} className="text-green-500">
✔ Installed pygame
[ok] Installed pygame
</AnimatedSpan>
<TypingAnimation delay={2400}>
$ write game.py --lines 156
</TypingAnimation>
<AnimatedSpan delay={3200} className="text-blue-500">
✔ Written 156 lines
[ok] Written 156 lines
</AnimatedSpan>
<TypingAnimation delay={3600}>
$ python game.py --test
</TypingAnimation>
<AnimatedSpan delay={4200} className="text-green-500">
✔ All sprites loaded
[ok] All sprites loaded
</AnimatedSpan>
<AnimatedSpan delay={4500} className="text-green-500">
✔ Physics engine OK
[ok] Physics engine OK
</AnimatedSpan>
<AnimatedSpan delay={4800} className="text-green-500">
✔ 60 FPS stable
[ok] 60 FPS stable
</AnimatedSpan>
{/* Scene 2: Data Analysis */}
@@ -144,13 +144,14 @@ function InputGroupInput({
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
const InputGroupTextarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(function InputGroupTextarea({ className, ...props }, ref) {
return (
<Textarea
data-slot="input-group-control"
ref={ref}
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className,
@@ -158,7 +159,9 @@ function InputGroupTextarea({
{...props}
/>
);
}
});
InputGroupTextarea.displayName = "InputGroupTextarea";
export {
InputGroup,
+12 -3
View File
@@ -2,12 +2,19 @@ import * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(function Textarea({ className, rows, ...props }, ref) {
// `field-sizing` is not available in older Chrome. Keep a native rows
// fallback so a multi-line editor never collapses into a single line.
return (
<textarea
data-slot="textarea"
ref={ref}
rows={rows ?? 3}
className={cn(
"border-input placeholder:text-muted-foreground/60 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"border-input placeholder:text-muted-foreground/60 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] ",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
@@ -15,6 +22,8 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
{...props}
/>
);
}
});
Textarea.displayName = "Textarea";
export { Textarea };
@@ -3,7 +3,7 @@ export function EmptyDocumentState() {
return (
<div className="flex h-full items-center justify-center bg-white px-6 text-center dark:bg-slate-900">
<div className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 px-10 py-12 sm:px-12 dark:border-slate-700 dark:bg-slate-800/50">
<div className="mb-4 text-[40px] leading-none">📚</div>
<div className="mb-4 text-[28px] font-semibold leading-none text-slate-500 dark:text-slate-300">文档</div>
<div className="mb-2 text-lg font-bold text-slate-700 dark:text-slate-200">
选择一篇文档开始阅读
</div>
@@ -213,7 +213,7 @@ const errorToastDarkOptions = {
</div>
<div className="flex flex-1 flex-wrap items-center gap-1.5">
{summary && Object.keys(summary.by_category).length === 0 && (
<span className="text-muted-foreground text-sm">暂无失败记录 🎉</span>
<span className="text-muted-foreground text-sm">暂无失败记录</span>
)}
{summary &&
Object.entries(summary.by_category)
@@ -35,7 +35,7 @@ export function AgentWelcome({
className,
)}
>
<div className="bg-primary/10 ring-primary/15 mb-2 flex h-[72px] w-[72px] items-center justify-center rounded-3xl ring-1">
<div className="agent-avatar bg-primary/10 ring-primary/15 mb-2 flex h-[72px] w-[72px] items-center justify-center rounded-3xl ring-1">
<BotIcon className="text-primary h-9 w-9" />
</div>
<div className="text-3xl font-bold tracking-tight">
@@ -839,7 +839,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
<CardHeader className="min-w-0 pb-2">
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1 overflow-hidden">
@@ -894,7 +894,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
const listLayout = (
<div className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]">
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1 overflow-hidden">
@@ -980,7 +980,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
? "正在加载 SOUL.md..."
: "输入 SOUL.md 内容"
}
className="max-h-[45vh] min-h-48 resize-y font-mono text-xs"
className="max-h-[45vh] min-h-48 resize-y text-xs"
/>
</div>
<div className="space-y-2">
@@ -264,7 +264,7 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
isActive
? isDark
? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]"
: "border-primary/50 bg-primary/10 shadow-sm"
: "agent-sidebar-card--active border-primary/50 bg-primary/10 shadow-sm"
: isDark
? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60"
: "border-transparent hover:border-border hover:bg-accent/50",
@@ -520,7 +520,7 @@ export function AgentDetailPanel({
{/* ── Identity / 基础信息 ─────────────────────────────────────────── */}
<section className="bg-card/60 rounded-xl border p-4">
<div className="flex items-start gap-3">
<div className="bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1">
<div className="agent-avatar bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1">
<BotIcon className="text-primary size-6" />
</div>
<div className="min-w-0 flex-1 space-y-1.5">
@@ -830,8 +830,8 @@ export function AgentDetailPanel({
const panelChrome = (collapseButton: ReactNode) => (
<>
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
<span className="text-sm font-semibold">智能体信息</span>
<div className="flex items-center gap-1">
<span className="shrink-0 whitespace-nowrap text-sm font-semibold">智能体信息</span>
<div className="flex min-w-0 items-center gap-1">
{headerActions}
{compactAppearanceActions}
{collapseButton}
@@ -138,7 +138,7 @@ export function AgentModuleSettingsDialog({
key={agent.id}
className="flex items-start gap-3 rounded-xl border border-border/60 bg-card p-3"
>
<div className="bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg">
<Bot className="size-4" />
</div>
<div className="min-w-0 flex-1">
@@ -223,7 +223,7 @@ export function AgentModuleSidebar({
isActive
? isDark
? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]"
: "border-primary/50 bg-primary/10 shadow-sm"
: "agent-sidebar-card--active border-primary/50 bg-primary/10 shadow-sm"
: isDark
? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60"
: "border-transparent hover:border-border hover:bg-accent/50",
@@ -20,16 +20,16 @@ export function CompactWelcome({ className }: { className?: string }) {
className="mb-3.5 inline-flex items-center justify-center rounded-full p-3"
style={{
backgroundColor:
"color-mix(in srgb, var(--workspace-brand) 5%, transparent)",
"rgb(var(--legacy-workspace-brand-rgb) / 5%)",
border:
"1px solid color-mix(in srgb, var(--workspace-brand) 12%, transparent)",
"1px solid rgb(var(--legacy-workspace-brand-rgb) / 12%)",
}}
>
<div
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
style={{
backgroundImage:
"linear-gradient(to top right, var(--workspace-brand), color-mix(in srgb, var(--workspace-brand) 55%, white))",
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
}}
>
<span className="text-2xl font-black tracking-wide text-white italic">
@@ -38,8 +38,8 @@ export function CompactWelcome({ className }: { className?: string }) {
</div>
</div>
<h1 className="flex items-center justify-center gap-2 text-2xl font-bold tracking-tight text-foreground/90">
<span className="text-2xl">👋</span> 欢迎使用CM助手
<h1 className="flex items-center justify-center text-2xl font-bold tracking-tight text-foreground/90">
欢迎使用CM助手
</h1>
<p className="text-muted-foreground mx-auto mt-2 max-w-sm text-xs leading-relaxed">
基于先进深度推理模型,为您提供高效的市场分析、专业报告撰写、调研设计与协同脑暴支持。
@@ -320,7 +320,7 @@ export function GeneralQaSidebar() {
className={cn(
"flex h-10 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[5px] text-[14px] font-semibold transition-colors",
"border border-solid",
"bg-[color-mix(in_srgb,var(--primary)_14%,transparent)] hover:bg-[color-mix(in_srgb,var(--primary)_24%,transparent)]",
"bg-[rgb(var(--legacy-primary-rgb)/14%)] hover:bg-[rgb(var(--legacy-primary-rgb)/24%)]",
)}
>
<Plus className="size-4 stroke-[2.5]" />
@@ -353,7 +353,7 @@ export function GeneralQaSidebar() {
isActive
? isDark
? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]"
: "border-primary/50 bg-primary/10 shadow-sm"
: "agent-sidebar-card--active border-primary/50 bg-primary/10 shadow-sm"
: isDark
? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60"
: "border-transparent hover:border-border hover:bg-accent/50",
@@ -613,7 +613,7 @@ export function InputBox({
: "*:data-[slot='input-group']:rounded-3xl",
// 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边)
compact && isNewThread
? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[color-mix(in_srgb,var(--primary)_35%,transparent)] focus-within:ring-4 focus-within:ring-[color-mix(in_srgb,var(--primary)_5%,transparent)] focus-within:shadow-[0_10px_35px_color-mix(in_srgb,var(--primary)_4%,transparent)] dark:border-slate-700 dark:bg-slate-800"
? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[rgb(var(--legacy-primary-rgb)/35%)] focus-within:ring-4 focus-within:ring-[rgb(var(--legacy-primary-rgb)/5%)] focus-within:shadow-[0_10px_35px_rgb(var(--legacy-primary-rgb)/4%)] dark:border-slate-700 dark:bg-slate-800"
: // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
// 对话态:常规白卡 + 中性边框 + 轻阴影
isNewThread
@@ -682,13 +682,16 @@ export function InputBox({
"gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
context.writing_mode
? "border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200"
? "border border-amber-300 bg-amber-50 text-amber-700 ring-1 ring-amber-300/35 hover:bg-amber-100 dark:border-amber-300/70 dark:bg-amber-400/20 dark:text-amber-100 dark:ring-amber-300/35 dark:shadow-[0_0_0_1px_rgba(251,191,36,0.18),0_0_18px_rgba(251,191,36,0.18)] dark:hover:bg-amber-400/25"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleWritingModeToggle}
>
{context.writing_mode && (
<CheckIcon className="size-3 text-amber-700 dark:text-amber-100" />
)}
<FileTextIcon className="size-3" />
<span className="text-xs font-normal">写作</span>
</PromptInputButton>
@@ -705,13 +708,16 @@ export function InputBox({
"gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
memoryInjectionEnabled
? "border border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-950/40 dark:text-blue-200"
? "border border-blue-300 bg-blue-50 text-blue-700 ring-1 ring-blue-300/35 hover:bg-blue-100 dark:border-cyan-300/70 dark:bg-cyan-400/20 dark:text-cyan-50 dark:ring-cyan-300/35 dark:shadow-[0_0_0_1px_rgba(34,211,238,0.18),0_0_18px_rgba(34,211,238,0.18)] dark:hover:bg-cyan-400/25"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleMemoryInjectionToggle}
>
{memoryInjectionEnabled && (
<CheckIcon className="size-3 text-blue-700 dark:text-cyan-50" />
)}
<BrainCircuitIcon className="size-3" />
<span className="text-xs font-normal">记忆</span>
</PromptInputButton>
@@ -1166,7 +1172,7 @@ export function InputBox({
className={cn(
"rounded-full border-border bg-background shadow-sm hover:bg-muted dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none",
compact &&
"size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--primary-foreground)] hover:bg-[color-mix(in_srgb,var(--primary)_88%,black)] disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--primary-foreground)] dark:hover:bg-[color-mix(in_srgb,var(--primary)_88%,black)] dark:disabled:bg-muted",
"size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90 disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--primary-foreground)] dark:hover:opacity-90 dark:disabled:bg-muted",
)}
disabled={disabled}
variant="outline"
@@ -112,7 +112,7 @@ export function RecentRoundtableList() {
}
try {
await rename(id, title);
toast.success("✏️ 已重命名");
toast.success("已重命名");
} catch {
toast.error("重命名失败,请重试");
} finally {
@@ -126,7 +126,7 @@ export function RecentRoundtableList() {
if (!window.confirm("确定删除这条会商记录吗?此操作不可恢复。")) return;
try {
await remove(draft.id);
toast.success("🗑 已删除会商记录");
toast.success("已删除会商记录");
} catch {
toast.error("删除失败,请重试");
}
@@ -8,7 +8,7 @@ CM Assistant is an internal super-agent platform that orchestrates sub-agents, m
---
## 🚀 Core Features
## Core Features
* **Skills & Tools**: With built-in and extensible skills and tools, CM Assistant can do almost anything.
* **Sub-Agents**: Sub-Agents help the main agent to do the tasks that are too complex to be done by the main agent.
@@ -1,12 +1,12 @@
"use client";
import { useMemo } from "react";
import { CopyIcon, LogOutIcon, UserIcon } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { clearStoredAuth, getAccountDisplayName, getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { OptType, writeYLog } from "@/core/log/y-log";
import { clearYLogToken } from "@/core/log/y-log-token";
@@ -27,7 +27,10 @@ function copy(text: string, hint = "已复制") {
}
export function AccountSettingsPage() {
const auth = useMemo(() => getStoredAuth(), []);
// Subscribe to same-tab and cross-tab auth changes. The dialog may stay open
// while the active account is switched elsewhere.
useLiveAccount();
const auth = getStoredAuth();
if (!auth) {
return (
@@ -1,35 +1,28 @@
"use client";
import { User } from "lucide-react";
import { useMemo } from "react";
import { getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { useMyPosition } from "@/core/positions";
import { cn } from "@/lib/utils";
import { readUserInfo } from "@/strategy-components/lib/light-app";
/**
* 名称 + 岗位 用户标识。全量模式由 strategy-components/Header.tsx 顶栏右侧展示;
* 紧凑模式(不带全局顶栏)无处承载,故抽成共享组件,复用同一套数据来源
* (`readUserInfo().userName` 优先、回退 auth 邮箱前缀;岗位取 `useMyPosition`),
* (实时 auth 展示名;岗位取 `useMyPosition`),
* 供页面级顶栏(如 ChatPage)右侧复用,保证两套布局一致。
*
* 配色按当前内容区前景色(foreground / muted-foreground),适配聊天页浅底顶栏,
* 而非 Header 的白字贴 banner 方案。
*/
export function UserIdentityBadge({ className }: { className?: string }) {
const auth = getStoredAuth();
// userInfo belongs to external-app route parameters and can legitimately
// outlive a login switch. The visible account name must instead match the
// settings page's current auth identity.
const liveAccount = useLiveAccount();
const { data: myPosition } = useMyPosition();
const positionName = myPosition?.name?.trim() || "";
// 与 Header.tsx 一致:外部登录写入的真实用户名优先,缺省回退邮箱前缀。
const displayName = useMemo(() => {
const info = readUserInfo();
const name = typeof info.userName === "string" ? info.userName.trim() : "";
if (name) return name;
const email = auth?.email?.trim();
return email ? email.split("@")[0] : "用户";
}, [auth?.email]);
const displayName = liveAccount.displayName;
return (
<div
@@ -1,15 +1,13 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
import { AuroraText } from "../ui/aurora-text";
let waved = false;
export function Welcome({
className,
mode,
@@ -26,9 +24,6 @@ export function Welcome({
}
return ["var(--color-foreground)"];
}, [isUltra]);
useEffect(() => {
waved = true;
}, []);
return (
<div
className={cn(
@@ -38,12 +33,9 @@ export function Welcome({
>
<div className="text-[26px] font-medium tracking-tight text-foreground/85">
{searchParams.get("mode") === "skill" ? (
`✨ ${t.welcome.createYourOwnSkill} ✨`
t.welcome.createYourOwnSkill
) : (
<div className="flex items-center gap-2.5">
<div className={cn("inline-block text-2xl", !waved ? "animate-wave" : "")}>
{isUltra ? "🚀" : "👋"}
</div>
<div className="flex items-center">
<AuroraText colors={colors}>{t.welcome.greeting}</AuroraText>
</div>
)}
@@ -36,8 +36,8 @@ const NEW_CHAT_CLASS =
// 紧凑模式(CM 参考图):白卡描边、居中、主色 + 图标
const NEW_CHAT_COMPACT_CLASS =
"h-9 justify-center gap-2 rounded-xl bg-card text-[13px] font-semibold text-foreground transition-colors " +
"border border-[color-mix(in_srgb,var(--primary)_18%,transparent)] " +
"hover:bg-[color-mix(in_srgb,var(--primary)_4%,transparent)] hover:text-[var(--primary)] hover:border-[color-mix(in_srgb,var(--primary)_32%,transparent)] " +
"border border-[rgb(var(--legacy-primary-rgb)/18%)] " +
"hover:bg-[rgb(var(--legacy-primary-rgb)/4%)] hover:text-[var(--primary)] hover:border-[rgb(var(--legacy-primary-rgb)/32%)] " +
"data-[active=true]:bg-card data-[active=true]:text-[var(--primary)] data-[active=true]:shadow-none";
export function WorkspaceNavChatList({
@@ -18,6 +18,7 @@
import { requestCrudService } from "@/service/request-axios-service";
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
import { readRuntimeEnv, readRuntimeFlag } from "@/env";
import { fetchTaskDeeplinkConfig } from "@/core/auth/task-deeplink-config";
/** 本页输入 JSON 暂存的 sessionStorage key(拉取一次后复用,避免重复请求)。 */
@@ -89,7 +90,7 @@ export const DEMO_DASHBOARD_INPUT = {
/** 「研判情报报告」接口完整地址:`{VITE_AIPI_CONSUMER_URL}/cop/selectSqReport`。 */
function sqReportUrl(): string {
const value = (import.meta.env as Record<string, unknown>).VITE_AIPI_CONSUMER_URL;
const value = readRuntimeEnv("VITE_AIPI_CONSUMER_URL");
const base = typeof value === "string" ? value.replace(/\/+$/, "") : "";
return base ? `${base}/cop/selectSqReport` : "";
}
@@ -140,7 +141,7 @@ export function extractSqReport(body: unknown, taskId: string): unknown | null {
* 避免开发反复请求样例假数据。真实接口模式(false)下不读不写缓存,**始终拉取真实数据**。
*/
function inputCacheEnabled(): boolean {
return (import.meta.env as Record<string, unknown>).VITE_MOCK_SQ_REPORT === "true";
return readRuntimeFlag("VITE_MOCK_SQ_REPORT");
}
/** 读取暂存的输入 JSON(仅 mock 开发模式生效,真实模式恒返回 null 以强制重新拉取)。 */
+4
View File
@@ -22,6 +22,8 @@ import {
} from "@/core/auth/scoped-storage";
const AUTH_STORAGE_KEY = "deerflow.auth";
/** Fired in the current tab because the browser `storage` event only reaches other tabs. */
export const AUTH_STATE_CHANGED_EVENT = "deerflow:auth-state-changed";
function isBrowser(): boolean {
return typeof window !== "undefined";
@@ -48,6 +50,7 @@ export function setStoredAuth(auth: LoginByUsernameResponse) {
return;
}
authStorageSet(AUTH_STORAGE_KEY, JSON.stringify(auth));
window.dispatchEvent(new Event(AUTH_STATE_CHANGED_EVENT));
}
export function clearStoredAuth() {
@@ -55,6 +58,7 @@ export function clearStoredAuth() {
return;
}
authStorageRemove(AUTH_STORAGE_KEY);
window.dispatchEvent(new Event(AUTH_STATE_CHANGED_EVENT));
}
/** 与设置 → 个人信息页「用户名」一致:邮箱 @ 前缀,否则 user_id。 */
+3 -2
View File
@@ -18,6 +18,7 @@
import { requestCrudService } from "@/service/request-axios-service";
import { clearTaskContext, persistTaskContext } from "@/core/auth/task-purpose";
import { fetchTaskDeeplinkConfig } from "@/core/auth/task-deeplink-config";
import { readRuntimeEnv, readRuntimeFlag } from "@/env";
import { listBusinessMappings } from "@/strategy-components/api/business-mapping";
/** xdfx 业务码:深链传入的是「行动id」,需先调 action 接口解析出真正的「任务id」。 */
@@ -73,13 +74,13 @@ export function parseGoPath(goPath: string): ParsedGoPath | null {
}
function consumerBaseUrl(): string {
const value = import.meta.env.VITE_AIPI_CONSUMER_URL;
const value = readRuntimeEnv("VITE_AIPI_CONSUMER_URL");
return typeof value === "string" ? value.replace(/\/+$/, "") : "";
}
/** cop-task-detail 是否走本地假数据:开关 `VITE_MOCK_COP_TASK_DETAIL=true`(真实接口未就绪时调试用)。 */
function copTaskDetailMockEnabled(): boolean {
return (import.meta.env as Record<string, unknown>).VITE_MOCK_COP_TASK_DETAIL === "true";
return readRuntimeFlag("VITE_MOCK_COP_TASK_DETAIL");
}
/** cop-task-detail 本地假数据(与真实 data 字段同构;mock 开关开启时返回)。 */
@@ -2,7 +2,11 @@
import { useEffect, useRef, useState } from "react";
import { getAccountDisplayName, getStoredAuth } from "@/core/auth";
import {
AUTH_STATE_CHANGED_EVENT,
getAccountDisplayName,
getStoredAuth,
} from "@/core/auth";
import { isEmbedActive } from "@/core/embed/embed-session";
const AUTH_STORAGE_KEY = "deerflow.auth";
@@ -61,11 +65,13 @@ export function useLiveAccount(): LiveAccount {
if (!isEmbedActive()) {
window.addEventListener("storage", onStorage);
}
window.addEventListener(AUTH_STATE_CHANGED_EVENT, refresh);
window.addEventListener("focus", refresh);
document.addEventListener("visibilitychange", refresh);
refresh();
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener(AUTH_STATE_CHANGED_EVENT, refresh);
window.removeEventListener("focus", refresh);
document.removeEventListener("visibilitychange", refresh);
};
@@ -5,12 +5,13 @@ import {
persistExternalLoginParams,
} from "./external-login";
import { authStorageSet } from "@/core/auth/scoped-storage";
import { readRuntimeEnv } from "@/env";
const AUTH_BASE_URL = (import.meta.env.VITE_AUTH_BASE_URL || "").replace(
const AUTH_BASE_URL = (readRuntimeEnv("VITE_AUTH_BASE_URL") || "").replace(
/\/$/,
"",
);
const CONSUMER_URL = (import.meta.env.VITE_AIPI_CONSUMER_URL || "").replace(
const CONSUMER_URL = (readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || "").replace(
/\/$/,
"",
);
+2 -1
View File
@@ -1,5 +1,6 @@
import { requestCrudService } from "@/service/request-axios-service";
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
import { readRuntimeEnv } from "@/env";
import {
deleteExternalAccessTokenFields,
normalizeExternalAccessToken,
@@ -9,7 +10,7 @@ import {
writeStoredExternalAccessToken,
} from "@/core/auth/external-access-token";
const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || "";
const CONSUMER_URL = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || "";
export const TOOLS_TOKEN_STORAGE_KEY = "tools_token";
export const EXTERNAL_USER_ID_STORAGE_KEY = "userId";
@@ -2,13 +2,14 @@
import { authStorageGet } from "@/core/auth/scoped-storage";
import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token";
import { readRuntimeEnv } from "@/env";
function trimTrailingSlash(url: string): string {
return url.replace(/\/+$/, "");
}
function readEnv(key: string): string {
const value = import.meta.env[key];
const value = readRuntimeEnv(key);
return typeof value === "string" ? value.trim() : "";
}
+23 -18
View File
@@ -1,28 +1,33 @@
type EnvValue = string | undefined;
function readEnv(
key:
| "VITE_BACKEND_BASE_URL"
| "VITE_LANGGRAPH_BASE_URL"
| "VITE_CANVAS_LANGGRAPH_BASE_URL"
| "VITE_NOTE_API_BASE_URL"
| "VITE_AIPI_BACKENP_URL"
| "VITE_AIPI_FRONTEND_URL"
| "VITE_STATIC_WEBSITE_ONLY"
| "VITE_GITHUB_OAUTH_TOKEN",
): EnvValue {
declare global {
interface Window {
__CM_RUNTIME_CONFIG__?: Record<string, string | undefined>;
}
}
export function readRuntimeEnv(key: string): EnvValue {
const runtimeValue =
typeof window !== "undefined" ? window.__CM_RUNTIME_CONFIG__?.[key] : undefined;
if (typeof runtimeValue === "string" && runtimeValue.trim()) {
return runtimeValue.trim();
}
const value = import.meta.env[key];
return typeof value === "string" && value.trim() ? value : undefined;
}
export function readRuntimeFlag(key: string): boolean {
return readRuntimeEnv(key) === "true";
}
export const env = {
NEXT_PUBLIC_BACKEND_BASE_URL: readEnv("VITE_BACKEND_BASE_URL"),
NEXT_PUBLIC_LANGGRAPH_BASE_URL: readEnv("VITE_LANGGRAPH_BASE_URL"),
NEXT_PUBLIC_BACKEND_BASE_URL: readRuntimeEnv("VITE_BACKEND_BASE_URL"),
NEXT_PUBLIC_LANGGRAPH_BASE_URL: readRuntimeEnv("VITE_LANGGRAPH_BASE_URL"),
// Canvas page uses its own LangGraph base URL so it can be independently
// routed (e.g. a separate canvas-agent server). Falls back to the main URL.
NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL: readEnv("VITE_CANVAS_LANGGRAPH_BASE_URL"),
NEXT_PUBLIC_NOTE_API_BASE_URL: readEnv("VITE_NOTE_API_BASE_URL"),
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readEnv("VITE_AIPI_BACKENP_URL"),
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL: readRuntimeEnv("VITE_CANVAS_LANGGRAPH_BASE_URL"),
NEXT_PUBLIC_NOTE_API_BASE_URL: readRuntimeEnv("VITE_NOTE_API_BASE_URL"),
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readRuntimeEnv("VITE_AIPI_BACKENP_URL"),
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readRuntimeEnv("VITE_STATIC_WEBSITE_ONLY"),
GITHUB_OAUTH_TOKEN: readRuntimeEnv("VITE_GITHUB_OAUTH_TOKEN"),
};
@@ -14,20 +14,20 @@ import { cn } from '@/lib/utils';
export interface FlowStage {
key: string;
label: string;
emoji: string;
mark: string;
}
/** 固定流水线阶段(key 与后端 ai_writing_job_executor 的 stage key 一致)。 */
export const FLOW_STAGES: readonly FlowStage[] = [
{ key: 'intent', label: '意图理解', emoji: '🧭' },
{ key: 'research', label: '素材搜集', emoji: '🔍' },
{ key: 'material_confirm', label: '素材确认', emoji: '📚' },
{ key: 'outline', label: '大纲规划', emoji: '🗂️' },
{ key: 'outline_confirm', label: '大纲确认', emoji: '🧩' },
{ key: 'draft', label: '写作', emoji: '✍️' },
{ key: 'draft_confirm', label: '草稿确认', emoji: '📝' },
{ key: 'review', label: '编辑审核', emoji: '🧐' },
{ key: 'done', label: '定稿', emoji: '🏁' },
{ key: 'intent', label: '意图理解', mark: '意' },
{ key: 'research', label: '素材搜集', mark: '搜' },
{ key: 'material_confirm', label: '素材确认', mark: '素' },
{ key: 'outline', label: '大纲规划', mark: '纲' },
{ key: 'outline_confirm', label: '大纲确认', mark: '确' },
{ key: 'draft', label: '写作', mark: '写' },
{ key: 'draft_confirm', label: '草稿确认', mark: '稿' },
{ key: 'review', label: '编辑审核', mark: '审' },
{ key: 'done', label: '定稿', mark: '定' },
];
export const FLOW_STAGE_LABEL: Record<string, string> = Object.fromEntries(
@@ -40,12 +40,12 @@ const LINE = 'bg-slate-300 dark:bg-border';
function StageNode({
label,
emoji,
mark,
state,
note,
}: {
label: string;
emoji: string;
mark: string;
state: StageState;
note?: string | null;
}) {
@@ -55,14 +55,14 @@ function StageNode({
{state === 'active' && <span className="absolute inset-0 animate-ping rounded-full bg-primary/25" />}
<span
className={cn(
'relative flex size-11 items-center justify-center rounded-full border-2 bg-white text-lg transition-all dark:bg-card',
'relative flex size-11 items-center justify-center rounded-full border-2 bg-white text-base font-semibold text-primary transition-all dark:bg-card',
state === 'active' && 'border-primary shadow-[0_0_0_3px_rgba(22,119,255,0.12)]',
state === 'done' && 'border-emerald-400/70',
state === 'error' && 'border-destructive',
state === 'pending' && 'border-border opacity-45 grayscale',
)}
>
{emoji}
{mark}
{state === 'done' && (
<span className="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-2.5" />
@@ -127,7 +127,7 @@ export function AIWritingFlowChart({
{idx > 0 && <div className={cn('mt-[22px] h-px w-5 shrink-0', LINE)} />}
<StageNode
label={stage.label}
emoji={stage.emoji}
mark={stage.mark}
state={stateOf(idx)}
note={stage.key === activeKey ? note : undefined}
/>
@@ -49,6 +49,7 @@ const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; des
{ value: 'knowledge_base', label: '知识库检索', desc: '直连内网知识库检索接口,不走技能编排' },
{ value: 'notebook', label: '我的空间', desc: '从空间中检索素材' },
];
const SHOW_NOTEBOOK_MATERIAL_SOURCE = false;
const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222] dark:text-[#AACEFF]';
const FIELD_TRIGGER_CLASS =
@@ -97,6 +98,17 @@ export function AIWritingForm({
() => models.find((m) => m.name === modelName) ?? models[0],
[models, modelName],
);
const materialSourceOptions = useMemo(
() =>
SHOW_NOTEBOOK_MATERIAL_SOURCE || studioToken
? MATERIAL_SOURCE_OPTIONS
: MATERIAL_SOURCE_OPTIONS.filter((opt) => opt.value !== 'notebook'),
[studioToken],
);
const effectiveMaterialSource =
!SHOW_NOTEBOOK_MATERIAL_SOURCE && !studioToken && materialSource === 'notebook'
? 'general'
: materialSource;
// 初始化时加载文章类型
@@ -173,11 +185,11 @@ export function AIWritingForm({
addReferences,
skipResearch,
// 跳过素材收集时检索类型无意义,归一为 general
materialSource: skipResearch ? 'general' : materialSource,
materialSource: skipResearch ? 'general' : effectiveMaterialSource,
modelName: selectedModel?.name || modelName || undefined,
// 只有选了"我的空间"且未跳过素材收集才传笔记参数
noteSearchSpaceId: !skipResearch && materialSource === 'notebook' ? (selectedNotebookId ?? undefined) : undefined,
noteSourceIds: !skipResearch && materialSource === 'notebook' && selectedNotebookId != null ? selectedDocumentIds : undefined,
noteSearchSpaceId: !skipResearch && effectiveMaterialSource === 'notebook' ? (selectedNotebookId ?? undefined) : undefined,
noteSourceIds: !skipResearch && effectiveMaterialSource === 'notebook' && selectedNotebookId != null ? selectedDocumentIds : undefined,
});
}
@@ -307,7 +319,7 @@ export function AIWritingForm({
<div className="mt-4 space-y-2">
<label className={FIELD_LABEL_CLASS}>检索类型</label>
<Select
value={materialSource}
value={effectiveMaterialSource}
onValueChange={(v) => handleMaterialSourceChange(v as MaterialSource)}
disabled={disabled}
>
@@ -315,7 +327,7 @@ export function AIWritingForm({
<SelectValue placeholder="选择检索类型" />
</SelectTrigger>
<SelectContent>
{MATERIAL_SOURCE_OPTIONS.map((opt) => (
{materialSourceOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value} className="text-sm">
{opt.label}
<span className="ml-1 text-xs text-muted-foreground">— {opt.desc}</span>
@@ -325,7 +337,7 @@ export function AIWritingForm({
</Select>
{/* 选了"我的空间"才展示空间 / 文档选择器 */}
{materialSource === 'notebook' && (
{effectiveMaterialSource === 'notebook' && (
<div className="mt-2 space-y-2">
{/* studioToken 模式:空间已锁定,不展示选择器 */}
{!studioToken && (
@@ -46,6 +46,8 @@ const WRITING_STATUS_PILL: Record<string, { label: string; className: string; sp
error: { label: "出错", className: "text-rose-500" },
};
const SHOW_NOTEBOOK_SPACE_ENTRIES = false;
/** 单条历史记录项 —— 样式参考多智能体会商的 HistoryDropdown 列表项(圆角卡片 + 状态药丸 + 操作按钮)。 */
function WritingHistoryItem({
session,
@@ -83,8 +85,11 @@ function WritingHistoryItem({
return (
<li
className="px-3 py-2.5 transition-colors hover:bg-slate-50"
style={isCurrent ? { backgroundColor: "#ecfdf5" } : undefined}
className={cn(
"px-3 py-2.5 transition-colors hover:bg-slate-50 dark:hover:bg-white/10",
isCurrent &&
"bg-emerald-50 dark:border-l-2 dark:border-[#00e5ff] dark:bg-[#0b3a71]/80 dark:shadow-[inset_0_0_0_1px_rgba(64,150,255,0.24),0_0_18px_rgba(0,180,255,0.14)]",
)}
>
<div className="flex items-start gap-2">
<div className="min-w-0 flex-1">
@@ -101,7 +106,7 @@ function WritingHistoryItem({
}
}}
onBlur={commit}
className="w-full rounded border border-slate-300 px-2 py-1 text-[11.5px] font-semibold text-slate-800 outline-none focus:border-primary"
className="w-full rounded border border-slate-300 px-2 py-1 text-[11.5px] font-semibold text-slate-800 outline-none focus:border-primary dark:border-[#1b477e] dark:bg-[#022248] dark:text-[#e8edf5]"
/>
) : (
<button
@@ -132,7 +137,7 @@ function WritingHistoryItem({
type="button"
onClick={isBackground && onShowProgress ? onShowProgress : onSelect}
title={isBackground ? "查看进度" : "只读查看"}
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary"
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#8fd3ff]"
>
<Eye className="size-3.5" />
</button>
@@ -143,7 +148,7 @@ function WritingHistoryItem({
setRenaming(true);
}}
title="重命名"
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary"
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#8fd3ff]"
>
<Pencil className="size-3.5" />
</button>
@@ -151,7 +156,7 @@ function WritingHistoryItem({
type="button"
onClick={onDelete}
title="删除"
className="rounded p-1 text-slate-500 transition hover:bg-rose-50 hover:text-rose-600"
className="rounded p-1 text-slate-500 transition hover:bg-rose-50 hover:text-rose-600 dark:text-[#8aa0c4] dark:hover:bg-rose-500/15 dark:hover:text-rose-300"
>
<Trash2 className="size-3.5" />
</button>
@@ -248,6 +253,7 @@ export function ResearchSidebar({
</div>
)}
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden px-3 py-4">
{SHOW_NOTEBOOK_SPACE_ENTRIES && (
<section className="flex flex-col">
<div className="mb-3 flex items-center justify-between px-2">
<h2 className="text-[15px] font-bold tracking-wide text-slate-400">
@@ -345,6 +351,7 @@ export function ResearchSidebar({
</button>
</div>
</section>
)}
{/* 「历史记录」列表(样式参考多智能体会商):跟随侧栏自然向下滚动、不固定在底部。
点击打开历史回看(显示到右侧区域),✏️/双击改名,🗑删除,「挂起中」点击看进度。 */}
@@ -142,6 +142,7 @@ const DRAFT_ID = 'aw-draft';
const AI_WRITING_AGENT_MODULE_KEY = 'ai-writing';
const AI_WRITING_AGENT_MODULE_TITLE = 'AI 写作智能体';
const AI_WRITING_LAST_AGENT_KEY = `agent-module:${AI_WRITING_AGENT_MODULE_KEY}:last-agent`;
const SHOW_NOTEBOOK_SPACE_ENTRIES = false;
// Match the original canvas page proportions (chat=35, artifact=65)
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
@@ -567,7 +568,7 @@ function AIWritingBody({
<ArrowLeft className="size-4" />
返回写作
</button>
{rightView.kind === 'notebook' && (
{SHOW_NOTEBOOK_SPACE_ENTRIES && rightView.kind === 'notebook' && (
<button
type="button"
onClick={() => setRightView({ kind: 'allSpaces' })}
+3 -1
View File
@@ -567,7 +567,9 @@ export default function ChatPage() {
isLandingState &&
!compactLanding &&
"-translate-y-[calc(50vh-96px)]",
isLandingState
compactLanding
? "max-w-(--container-width-xl)"
: isLandingState
? "max-w-(--container-width-md)"
: "max-w-(--container-width-lg)",
)}
+1 -1
View File
@@ -613,7 +613,7 @@ export default function NewAgentPage() {
placeholder="写下智能体的人设、边界、工作方式和输出要求"
value={soulInput}
onChange={(event) => setSoulInput(event.currentTarget.value)}
className="max-h-[50vh] min-h-64 resize-y overflow-y-auto font-mono text-xs"
className="max-h-[50vh] min-h-64 resize-y overflow-y-auto text-xs"
/>
</div>
@@ -133,7 +133,7 @@ function DiagnosticRow({ item }: { item: DiagnosticItem }) {
) : null}
{item.userName ? (
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300">
👤 {item.userName}
用户 {item.userName}
</span>
) : null}
<span className="ml-auto text-xs text-slate-400 dark:text-muted-foreground">{fmtTime(item.createdAt)}</span>
@@ -31,6 +31,7 @@ import type { SourceItem } from "@/core/studio/types";
import { getStoredAuth } from "@/core/auth";
import { useTheme } from "@/contexts/ThemeContext";
import { useModels } from "@/core/models/hooks";
import { readRuntimeEnv } from "@/env";
import {
Select,
SelectContent,
@@ -259,6 +260,7 @@ export default function StudioNotebookDetailPage({
const notebookId =
notebookIdProp != null ? String(notebookIdProp) : params.notebookId;
const { theme } = useTheme(); // 跳转「我的空间」时带上当前主题 key
const notebookFrontendUrl = readRuntimeEnv("VITE_AIPI_FRONTEND_URL") || "";
const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback
const { models } = useModels();
const queryClient = useQueryClient();
@@ -343,7 +345,7 @@ export default function StudioNotebookDetailPage({
const handleDeleteSource = async (sourceId: number) => {
if (!notebook) return;
try {
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || "http://localhost:8001";
const response = await fetch(`${backendUrl}/api/v1/source/${sourceId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${studioToken ?? ""}` },
@@ -425,7 +427,7 @@ export default function StudioNotebookDetailPage({
const pollSourceStatus = useCallback(async (taskId: string) => {
if (!notebook) return;
try {
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || "http://localhost:8001";
const token = getStoredAuth()?.access_token ?? "";
const response = await fetch(`${backendUrl}/api/v1/task/${taskId}/status`, {
headers: { Authorization: `Bearer ${token}` },
@@ -581,7 +583,7 @@ export default function StudioNotebookDetailPage({
</div>
<div className="flex items-center gap-2">
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
href={`${notebookFrontendUrl}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
@@ -605,7 +607,7 @@ export default function StudioNotebookDetailPage({
<div className="relative z-10 flex flex-1 flex-col overflow-hidden">
{/* 右上角跳转按钮 */}
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
href={`${notebookFrontendUrl}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
target="_blank"
rel="noreferrer"
className="absolute right-6 top-4 z-10 flex items-center gap-1.5 rounded-full bg-muted/70 px-3 py-1.5 text-xs text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
@@ -157,7 +157,7 @@ export function DagFlowChart({
done={consensusState === "done"}
compact={compact}
/>
<MilestoneNode label="共识达成" emoji="🤝" state={consensusState} compact={compact} />
<MilestoneNode label="共识达成" mark="共" state={consensusState} compact={compact} />
{showReportMilestone && (
<>
<Connector
@@ -173,9 +173,9 @@ export function DagFlowChart({
)}
>
<span className="text-[9px] font-bold tracking-widest text-primary/70">并行生成</span>
<MilestoneNode label="总结报告" emoji="📝" state={summaryState} compact={compact} />
<MilestoneNode label="结果绘制" emoji="📊" state={reportState} compact={compact} />
<MilestoneNode label="大屏多页" emoji="🖥️" state={reportState} compact={compact} />
<MilestoneNode label="总结报告" mark="报" state={summaryState} compact={compact} />
<MilestoneNode label="结果绘制" mark="图" state={reportState} compact={compact} />
<MilestoneNode label="大屏多页" mark="屏" state={reportState} compact={compact} />
</div>
</>
)}
@@ -286,12 +286,12 @@ function FlowNode({
function MilestoneNode({
label,
emoji,
mark,
state,
compact,
}: {
label: string;
emoji: string;
mark: string;
state: ChainNodeState;
compact: boolean;
}) {
@@ -306,13 +306,14 @@ function MilestoneNode({
className={cn(
"relative flex items-center justify-center rounded-full border-2 transition-all",
ringSize,
compact ? "text-lg" : "text-2xl",
"font-semibold text-primary",
compact ? "text-base" : "text-xl",
state === "active" && "border-primary bg-primary/5 shadow-[0_0_0_4px_rgba(22,119,255,0.12)]",
state === "done" && "border-emerald-400/70 bg-emerald-500/5",
state === "pending" && "border-border bg-muted opacity-45 grayscale",
)}
>
{emoji}
{mark}
{state === "done" && (
<span className="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-2.5" />
@@ -106,12 +106,12 @@ function MiniNode({
label,
avatarType,
state,
emoji,
mark,
}: {
label: string;
avatarType?: string;
state: ChainNodeState;
emoji?: string;
mark?: string;
}) {
const tag = avatarType ? chainTagClasses(avatarType) : "";
return (
@@ -123,12 +123,13 @@ function MiniNode({
<span
className={cn(
"relative flex size-11 items-center justify-center rounded-full border-2 bg-white text-lg transition-all dark:bg-card",
mark && "font-semibold text-primary",
state === "active" && "border-primary shadow-[0_0_0_3px_rgba(22,119,255,0.12)]",
state === "done" && "border-emerald-400/70",
state === "pending" && "border-border opacity-45 grayscale",
)}
>
{emoji ?? (avatarType ? getAvatar(avatarType, "h-9 w-9") : null)}
{mark ?? (avatarType ? getAvatar(avatarType, "h-9 w-9") : null)}
{state === "done" && (
<span className="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-2.5" />
@@ -415,7 +416,7 @@ export function DagFlowMiniPanel({
})}
{/* 共识达成(底) */}
<MiniNode label="共识达成" emoji="🤝" state={consensusState} />
<MiniNode label="共识达成" mark="共" state={consensusState} />
</div>
</div>
@@ -97,7 +97,7 @@ export function DashboardChatPanel({
<div className="shrink-0 flex items-center justify-between gap-2 border-b border-slate-100 bg-white px-4 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 shrink-0 rounded-lg bg-gradient-to-tr from-primary to-[var(--brand-200)] flex items-center justify-center text-white text-xs shadow-sm">
📊
图
</div>
<div className="leading-tight min-w-0">
<div className="text-xs font-bold text-slate-800 truncate">大屏绘制智能体</div>
@@ -249,7 +249,7 @@ export function DispatchChainModal({
<Connector active={consensusActive} done={consensusDone} />
<MilestoneNode
label="共识达成"
emoji="🤝"
mark="共"
state={
consensusActive ? "active" : consensusDone ? "done" : "pending"
}
@@ -259,9 +259,9 @@ export function DispatchChainModal({
<Connector active={step3Active} done={step3Done} />
<div className="flex shrink-0 flex-col items-center gap-1.5 rounded-2xl border border-dashed border-primary/30 bg-primary/[0.04] px-2 pb-2 pt-1.5">
<span className="text-[9px] font-bold tracking-widest text-primary/70">并行生成</span>
<MilestoneNode label="总结报告" emoji="📝" state={step3State} />
<MilestoneNode label="结果绘制" emoji="📊" state={step3State} />
<MilestoneNode label="大屏多页" emoji="🖥️" state={step3State} />
<MilestoneNode label="总结报告" mark="报" state={step3State} />
<MilestoneNode label="结果绘制" mark="图" state={step3State} />
<MilestoneNode label="大屏多页" mark="屏" state={step3State} />
</div>
</div>
</div>
@@ -422,11 +422,11 @@ function FlowNode({
function MilestoneNode({
label,
emoji,
mark,
state,
}: {
label: string;
emoji: string;
mark: string;
state: ChainNodeState;
}) {
return (
@@ -437,14 +437,14 @@ function MilestoneNode({
)}
<span
className={cn(
"relative flex size-16 items-center justify-center rounded-full border-2 text-2xl transition-all",
"relative flex size-16 items-center justify-center rounded-full border-2 text-2xl font-semibold text-primary transition-all",
state === "active" &&
"border-primary bg-primary/5 shadow-[0_0_0_4px_rgba(22,119,255,0.12)]",
state === "done" && "border-emerald-400/70 bg-emerald-500/5",
state === "pending" && "border-border bg-muted opacity-45 grayscale",
)}
>
{emoji}
{mark}
{state === "done" && (
<span className="absolute -right-0.5 -top-0.5 flex size-5 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-3" />
@@ -705,7 +705,7 @@ export function RecommendAgentsDialog({
{state.phase === "history" && (
<div className="text-xs text-sky-700 dark:text-sky-300 p-2 border border-sky-300/50 dark:border-sky-700/50 rounded-md bg-sky-50 dark:bg-sky-950/30 flex items-center justify-between gap-2">
<span>
📌 这是本会话上次的推荐结果
这是本会话上次的推荐结果
{state.historyCreatedAt
? `(${new Date(state.historyCreatedAt).toLocaleString("zh-CN", { hour12: false })})`
: ""}
@@ -82,9 +82,9 @@ export function RoundtableJobNotifier({ children }: { children: ReactNode }) {
const handleTerminal = useCallback((job: RoundtableJob) => {
setWatchedJobIds((prev) => prev.filter((id) => id !== job.id));
if (job.status === "done") {
setNotice("🎉 后台研讨已完成!可在「历史记录」查看结论与报告");
setNotice("后台研讨已完成!可在「历史记录」查看结论与报告");
} else if (job.status === "error") {
setNotice("❌ 后台任务执行出错,请在「历史记录」中查看");
setNotice("后台任务执行出错,请在「历史记录」中查看");
}
// cancelled:用户主动取消,不提示。
}, []);
@@ -254,7 +254,7 @@ export function Step1Panel(props: Step1PanelProps) {
<div className="flex items-center justify-between border-t border-slate-50 pt-2 text-[#94a3b8]">
<div className="flex items-center gap-1.5">
<button
onClick={() => onTriggerToast("😄 表情包功能仍在微调状态(仅供演示)")}
onClick={() => onTriggerToast("表情包功能仍在微调状态(仅供演示)")}
className="p-1 hover:text-primary hover:bg-slate-50 rounded transition"
>
<Smile className="w-4 h-4" />
@@ -552,7 +552,7 @@ function AgentProgressPanel({
<div className="shrink-0 flex items-center justify-between gap-2 border-b border-slate-100 bg-white px-4 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 shrink-0 rounded-lg bg-gradient-to-tr from-primary to-[var(--brand-200)] flex items-center justify-center text-white text-xs shadow-sm">
📊
图
</div>
<div className="leading-tight min-w-0">
<div className="text-xs font-bold text-slate-800 truncate">方案总结智能体</div>
@@ -669,7 +669,7 @@ function AgentProgressPanel({
<div className="shrink-0 flex items-center justify-between gap-2 border-b border-slate-100 bg-white px-4 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 shrink-0 rounded-lg bg-gradient-to-tr from-primary to-[var(--brand-200)] flex items-center justify-center text-white text-xs shadow-sm">
📝
报
</div>
<div className="leading-tight min-w-0">
<div className="text-xs font-bold text-slate-800 truncate">方案总结报告智能体</div>
@@ -376,7 +376,7 @@ export function useDraftPersistence(
const id = await enqueueSave(furthest);
if (id) {
await refreshList();
triggerToast("💾 已保存为草稿");
triggerToast("已保存为草稿");
} else {
triggerToast("当前会话还没有可保存的内容");
}
@@ -490,15 +490,15 @@ export function useDraftPersistence(
draft = await apiGet(id);
} catch (err) {
console.warn("[drafts] 加载草稿失败:", err);
triggerToast("⚠️ 加载草稿失败,请重试");
triggerToast("加载草稿失败,请重试");
return false;
}
if (!draft) {
triggerToast("⚠️ 该草稿已不存在");
triggerToast("该草稿已不存在");
await refreshList();
return false;
}
applyLoadedDraft(draft, `📂 已加载草稿:${draft.title}`);
applyLoadedDraft(draft, `已加载草稿:${draft.title}`);
return true;
})();
},
@@ -535,13 +535,13 @@ export function useDraftPersistence(
await apiDelete(id);
} catch (err) {
console.warn("[drafts] 删除草稿失败:", err);
triggerToast("⚠️ 删除失败,请重试");
triggerToast("删除失败,请重试");
return;
}
await refreshList();
if (currentDraftIdRef.current === id) setCurrentDraftId(null);
if (renamingDraftId === id) setRenamingDraftId(null);
triggerToast("🗑 已删除历史记录");
triggerToast("已删除历史记录");
})();
},
[apiDelete, refreshList, renamingDraftId, setCurrentDraftId, triggerToast],
@@ -566,11 +566,11 @@ export function useDraftPersistence(
const updated = await apiUpdate(id, { title: trimmed });
if (updated) {
await refreshList();
triggerToast("✏️ 已重命名");
triggerToast("已重命名");
}
} catch (err) {
console.warn("[drafts] 重命名失败:", err);
triggerToast("⚠️ 重命名失败,请重试");
triggerToast("重命名失败,请重试");
} finally {
setRenamingDraftId(null);
setRenameDraftValue("");
@@ -288,7 +288,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
const hasFiles = !!(files && files.length > 0);
if (!trimmed && !hasFiles) return;
if (!intentThreadId) {
triggerToast("⚠️ 任务理解智能体尚未就绪,请稍候");
triggerToast("任务理解智能体尚未就绪,请稍候");
return;
}
// 重入保护用 ref 而不是 state:isIntentSending 是 React state,
@@ -379,7 +379,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
: m,
),
);
triggerToast(`❌ 文件上传失败: ${(err as Error).message}`);
triggerToast(`文件上传失败: ${(err as Error).message}`);
if (intentAborterRef.current === aborter) {
intentAborterRef.current = null;
}
@@ -517,7 +517,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
: resolvedSummary.objective,
},
]);
triggerToast("✅ 任务理解完成,可进入多智能体圆桌研讨");
triggerToast("任务理解完成,可进入多智能体圆桌研讨");
} else if (res.status === "clarification" && res.clarifications.length > 0) {
setPendingIntentClarificationMsgId(agentMsgId);
setIntentClarificationSelections((prev) => {
@@ -536,7 +536,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
setIntentInitError(res.content);
setIntentInitErrorDetail(null);
}
triggerToast(`❌ 智能体调用失败: ${res.content}`);
triggerToast(`智能体调用失败: ${res.content}`);
}
} catch (err) {
if ((err as Error).name === "AbortError" || aborter.signal.aborted) {
@@ -567,7 +567,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
: m,
),
);
triggerToast(`❌ 发送失败: ${(err as Error).message}`);
triggerToast(`发送失败: ${(err as Error).message}`);
}
} finally {
if (intentAborterRef.current === aborter) {
@@ -1340,13 +1340,13 @@ export function useStep2Orchestration(
const makeModelSwitchHandler = useCallback(
(bubbleId: number, isParallel: boolean) => (info: ModelSwitchInfo) => {
const who = info.agentName || "智能体";
triggerToast(`⚠️「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`);
triggerToast(`「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`);
if (isParallel) parallelStreamRef.current.delete(bubbleId);
const systemBubble: Step2Dialogue = {
id: Date.now() + Math.random(),
sender: "系统提示",
avatarType: "amber",
content: `⚠️ 模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`,
content: `模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`,
time: nowTime(),
confidence: "模型容错",
};
@@ -1824,7 +1824,7 @@ export function useStep2Orchestration(
highlightSpeaker(null);
setPendingClarification(leaderRes.clarification);
setClarificationDraft("");
triggerToast("❓ 总控需要补充信息,请回答下方问题后继续");
triggerToast("总控需要补充信息,请回答下方问题后继续");
break;
}
@@ -1843,7 +1843,7 @@ export function useStep2Orchestration(
setConsensusPercentage(100);
setHasConsensus(true);
triggerToast(
"🎉 圆桌研讨论证会圆满落幕!已取得 100% 专家组共识",
"圆桌研讨论证会圆满落幕!已取得 100% 专家组共识",
);
break;
}
@@ -1962,7 +1962,7 @@ export function useStep2Orchestration(
}
} catch (err) {
console.error("orchestration error:", err);
triggerToast(`❌ 研讨异常: ${(err as Error).message}`);
triggerToast(`研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
clearAllStreamingSpeakers();
@@ -2092,7 +2092,7 @@ export function useStep2Orchestration(
highlightSpeaker(null);
setPendingClarification(leaderRes.clarification);
setClarificationDraft("");
triggerToast("❓ 总控需要补充信息,请回答下方问题后继续");
triggerToast("总控需要补充信息,请回答下方问题后继续");
finishedAllSeats = false;
break;
}
@@ -2220,7 +2220,7 @@ export function useStep2Orchestration(
if (closeoutRes.content) setLastLeaderContent(closeoutRes.content);
setConsensusPercentage(100);
setHasConsensus(true);
triggerToast("🎉 业务链条研讨完成!已生成全案结论");
triggerToast("业务链条研讨完成!已生成全案结论");
} catch (err) {
if (closeoutAborter.signal.aborted || (err as Error).name === "AbortError") {
finalizeDialogue(closeoutBubbleId, "(已停止)", "已停止");
@@ -2234,7 +2234,7 @@ export function useStep2Orchestration(
}
} catch (err) {
console.error("chain orchestration error:", err);
triggerToast(`❌ 链条研讨异常: ${(err as Error).message}`);
triggerToast(`链条研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
clearAllStreamingSpeakers();
@@ -2283,7 +2283,7 @@ export function useStep2Orchestration(
async (seedMessage: string, ids: ThreadIdMap, coord: string) => {
const plan = orchestrationPlanRef.current;
if (!plan || plan.stages.length === 0) {
triggerToast("⚠️ 没有可执行的 DAG 编排计划");
triggerToast("没有可执行的 DAG 编排计划");
return;
}
if (isOrchestratingRef.current) return;
@@ -2398,7 +2398,7 @@ export function useStep2Orchestration(
finalizeDialogue(leaderBubbleId, leaderRes.content, "等待用户澄清");
setPendingClarification(leaderRes.clarification);
setClarificationDraft("");
triggerToast("❓ 总控需要补充信息,请回答下方问题后继续");
triggerToast("总控需要补充信息,请回答下方问题后继续");
finishedAllStages = false;
break;
}
@@ -2579,11 +2579,11 @@ export function useStep2Orchestration(
// 不收口(plan.finalSynthesis=false):全部 stage 跑完即视为完成,不再叫总控。
setConsensusPercentage(100);
setHasConsensus(true);
triggerToast("✅ 依赖编排各阶段已全部完成");
triggerToast("依赖编排各阶段已全部完成");
}
} catch (err) {
console.error("dag orchestration error:", err);
triggerToast(`❌ 依赖编排研讨异常: ${(err as Error).message}`);
triggerToast(`依赖编排研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
clearAllStreamingSpeakers();
@@ -2807,10 +2807,10 @@ export function useStep2Orchestration(
appendDialogue({
sender: "系统",
avatarType: "blue",
content: `✅ 圆桌已就绪:协调智能体 ${initRes.main_agent_name} 已上线,${Object.keys(initRes.thread_ids).length - 1} 个席位已分配独立线程。`,
content: `圆桌已就绪:协调智能体 ${initRes.main_agent_name} 已上线,${Object.keys(initRes.thread_ids).length - 1} 个席位已分配独立线程。`,
confidence: "系统就绪",
});
triggerToast("✅ 多智能体圆桌初始化完成");
triggerToast("多智能体圆桌初始化完成");
await runActiveOrchestration(
buildInitialIntent(),
initRes.thread_ids,
@@ -2820,7 +2820,7 @@ export function useStep2Orchestration(
console.error("init multi-agent failed:", err);
if (cancelled) return;
setInitError((err as Error).message);
triggerToast(`❌ 初始化失败: ${(err as Error).message}`);
triggerToast(`初始化失败: ${(err as Error).message}`);
} finally {
if (!cancelled) setIsInitializing(false);
}
@@ -2887,7 +2887,7 @@ export function useStep2Orchestration(
const userText = customAgentInput.trim();
if (!userText) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
triggerToast("圆桌尚未就绪,无法记录干预指令");
return;
}
@@ -2895,7 +2895,7 @@ export function useStep2Orchestration(
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${userText}`,
content: `(人工指令介入) ${userText}`,
confidence: "最高指令",
});
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请优先按这条新指令,通过 agent_orchestration 把活派给相关的真实席位去获取交付;若用户点名了某个席位(如"前端实现"),必须派给清单里对应的那个 agent_name。**不要**自己代答,也**不要**在没有真正看到该席位「完成…交付内容为:…」的情况下臆造交付或直接收口。只有当用户明确要求收口、或相关席位都已真实交付时,才综合输出结论。`;
@@ -2908,7 +2908,7 @@ export function useStep2Orchestration(
const hasFiles = !!(files && files.length > 0);
if (!userText && !hasFiles) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
triggerToast("圆桌尚未就绪,无法记录干预指令");
return;
}
@@ -2929,7 +2929,7 @@ export function useStep2Orchestration(
status: "uploaded",
}));
} catch (err) {
triggerToast(`❌ 文件上传失败: ${(err as Error).message}`);
triggerToast(`文件上传失败: ${(err as Error).message}`);
return;
}
}
@@ -2947,7 +2947,7 @@ export function useStep2Orchestration(
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${shownText}${fileNote}`,
content: `(人工指令介入) ${shownText}${fileNote}`,
confidence: "最高指令",
});
@@ -2977,7 +2977,7 @@ export function useStep2Orchestration(
const answer = rawAnswer.trim();
if (!answer) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,无法回复澄清");
triggerToast("圆桌尚未就绪,无法回复澄清");
return;
}
if (isOrchestratingRef.current) {
@@ -2988,7 +2988,7 @@ export function useStep2Orchestration(
appendDialogue({
sender: "用户 (澄清回复)",
avatarType: "human",
content: `🗣 回复总控提问:${answer}`,
content: `回复总控提问:${answer}`,
confidence: "澄清回复",
});
setPendingClarification(null);
@@ -3008,7 +3008,7 @@ export function useStep2Orchestration(
const handleBudgetChange = useCallback(
(newVal: number) => {
setBudgetLimit(newVal);
triggerToast(`💰 研商经费调优约束变更为 ${newVal} 万!各大智能体启动资源重算...`);
triggerToast(`研商经费调优约束变更为 ${newVal} 万!各大智能体启动资源重算...`);
setTimeout(() => {
const respTime = nowTime();
@@ -3017,13 +3017,13 @@ export function useStep2Orchestration(
let senderName = "风险审查智能体";
if (newVal < 400) {
agentOpinion = `⚠️ [预算警报] 经费压缩至 ${newVal} 万元,自动触发紧急安全妥协防御策略!我们将不得不裁剪旁路冷备份服务器配置,降低静态演练深度,算力容灾等级重下降至 Tier I (低安全水位)。`;
agentOpinion = `[预算警报] 经费压缩至 ${newVal} 万元,自动触发紧急安全妥协防御策略!我们将不得不裁剪旁路冷备份服务器配置,降低静态演练深度,算力容灾等级重下降至 Tier I (低安全水位)。`;
} else if (newVal > 540) {
agentOpinion = `🔋 [方案容量上升] 预算上调至充足的 ${newVal} 万元,资装弹性充裕!我已为 A 套极速构建增加了 4 组外部超大异地区容灾节点支持,灾备冗余度成功突破 Tier III+ 指标,高防高内聚!`;
agentOpinion = `[方案容量上升] 预算上调至充足的 ${newVal} 万元,资装弹性充裕!我已为 A 套极速构建增加了 4 组外部超大异地区容灾节点支持,灾备冗余度成功突破 Tier III+ 指标,高防高内聚!`;
speakerType = "purple";
senderName = "情报收集与适配";
} else {
agentOpinion = `⚡ [资源调配优化] 总体经费设定为 ${newVal} 万元,空间适中。我们重新规划了轻量容器 K3s 集成开销(约 ${Math.round(newVal * 0.45)} 万)和安全审计费比,总体博弈过程保持稳定态收敛。`;
agentOpinion = `[资源调配优化] 总体经费设定为 ${newVal} 万元,空间适中。我们重新规划了轻量容器 K3s 集成开销(约 ${Math.round(newVal * 0.45)} 万)和安全审计费比,总体博弈过程保持稳定态收敛。`;
speakerType = "pink";
senderName = "执行规划智能体";
}
@@ -3059,11 +3059,11 @@ export function useStep2Orchestration(
appendDialogue({
sender: `${roleName} (${targetModel})`,
avatarType: avatarColor,
content: `⚡ [模型偏好已保存] 本席位在下一轮研讨中将使用 ${targetModel} 进行推理。系统提示词与温度(${cfg?.temp ?? 0.2})当前仅在前端记录,等后端开放覆写后再同步生效。`,
content: `[模型偏好已保存] 本席位在下一轮研讨中将使用 ${targetModel} 进行推理。系统提示词与温度(${cfg?.temp ?? 0.2})当前仅在前端记录,等后端开放覆写后再同步生效。`,
confidence: "席位偏好",
});
setSelectedAgentId(null);
triggerToast(`⚙️ "${roleName}"已切换至 ${targetModel},下轮生效`);
triggerToast(`"${roleName}"已切换至 ${targetModel},下轮生效`);
},
[agentConfigs, agentNameToRole, appendDialogue, selectedAgents, selectedModel, triggerToast],
);
@@ -3162,7 +3162,7 @@ export function useStep2Orchestration(
// 与 handleInjectToRoundtable 末尾「直接启动新一轮编排」同构。
const resumeLiveTakeover = useCallback((): boolean => {
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 接管失败:未找到研讨线程,已切换为只读查看");
triggerToast("接管失败:未找到研讨线程,已切换为只读查看");
return false;
}
dagPlanExecutedRef.current = true;
@@ -3178,7 +3178,7 @@ export function useStep2Orchestration(
threadIdsRef.current,
coordinatorNameRef.current,
);
triggerToast("🎮 已接管至前端,继续实时研讨");
triggerToast("已接管至前端,继续实时研讨");
return true;
}, [abortAllInFlight, runActiveOrchestration, triggerToast]);
@@ -776,7 +776,7 @@ export function useStep3Report(
setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
bubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
@@ -1097,7 +1097,7 @@ export function useStep3Summary(
setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
bubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
@@ -1228,7 +1228,7 @@ export function useStep3Summary(
setDialogueConfidence(extractBubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
extractBubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
@@ -1518,7 +1518,7 @@ export function useStep3Summary(
},
});
if (ingestRunRef.current !== myRun || aborter.signal.aborted) return;
finalizeDialogue(bubbleId, res.content || `✅ 已完成「${title}」数据入库。`, "入库完成");
finalizeDialogue(bubbleId, res.content || `已完成「${title}」数据入库。`, "入库完成");
setIngestStatus("done");
persistSummary(); // 把入库气泡 + ingestStatus 落进草稿
} catch (err) {
@@ -406,8 +406,8 @@ export function stripFlowFence(text: string, streaming: boolean): string {
const tail = close >= 0 ? after.slice(close + 3) : "";
const placeholder =
close < 0 && streaming
? "\n\n> 📊 正在输出结构化流程数据,流程图画布同步绘制中…"
: "\n\n> 📊 已生成结构化流程数据,可在右侧「流程图」画布查看(点击节点看详情)。";
? "\n\n> 正在输出结构化流程数据,流程图画布同步绘制中…"
: "\n\n> 已生成结构化流程数据,可在右侧「流程图」画布查看(点击节点看详情)。";
return `${text.slice(0, start).trimEnd()}${placeholder}${tail}`;
}
@@ -138,8 +138,8 @@ export function stripReportFence(text: string, streaming: boolean): string {
// ── 解析(容错:取最后一个 report-json 代码块;缺字段给安全默认) ─────────────────
const FENCE = "```report-json";
const STREAM_MSG = "\n\n> 📊 正在输出大屏结构化数据,右侧沙箱稍后自动渲染…";
const DONE_MSG = "\n\n> 📊 已生成大屏数据,右侧沙箱已渲染预览(可继续输入修改要求调整)。";
const STREAM_MSG = "\n\n> 正在输出大屏结构化数据,右侧沙箱稍后自动渲染…";
const DONE_MSG = "\n\n> 已生成大屏数据,右侧沙箱已渲染预览(可继续输入修改要求调整)。";
/** 文本是否像本契约 JSON(含关键键),避免把无关 {} 误当数据。 */
function looksLikeReportJson(s: string): boolean {
@@ -753,7 +753,7 @@ export function renderDashboardHtml(
const allActive = seatBase ? `${ovBase},${seatBase}` : ovBase;
const navActiveCss = `${allActive}{background:var(--navactive);color:var(--primary);box-shadow:0 1px 5px rgba(15,23,42,.1);font-weight:700;border-color:${P.primary}33}`;
const KPI_ICONS = ["📊", "📈", "⚠️", "🧩", "🏆", "📅"];
const KPI_ICONS = ["数", "趋", "险", "组", "优", "期"];
const kpis = data.kpis
.map((k, i) => {
const ac = P.accents[i % P.accents.length];
@@ -787,7 +787,7 @@ export function renderDashboardHtml(
const console = data.charts.length
? `<section class="console">
<div class="chead"><h2 class="sec">📊 数据研判中心</h2><span class="status">STATUS: REAL-TIME CONSOLE</span></div>
<div class="chead"><h2 class="sec">数据研判中心</h2><span class="status">STATUS: REAL-TIME CONSOLE</span></div>
<div class="charts3">${data.charts.map(chartHtml).join("")}</div>
</section>`
: "";
@@ -799,7 +799,7 @@ export function renderDashboardHtml(
<a class="more" href="#consensus-modal">查看研讨详情 →</a>
</div>
${console}
<h2 class="sec">📋 席位交付总览 <small>点击卡片进入对应席位专页</small></h2>
<h2 class="sec">席位交付总览 <small>点击卡片进入对应席位专页</small></h2>
<div class="seats">${seatCards}</div>
${timeline}
</section>`;
@@ -955,7 +955,7 @@ body:has(.page:target) #overview:not(:target){display:none}
.fill{height:100%;border-radius:99px}
.chips{display:flex;flex-direction:column;gap:6px;margin-top:12px;padding-top:11px;border-top:1px solid var(--line)}
.chip{font-size:10px;padding:3px 8px;border-radius:4px;border-left:2px solid;display:flex;align-items:center;gap:5px}
.chip::before{content:"⚠";font-size:9px}
.chip::before{content:"!";font-size:9px}
.tl{margin-top:2px}
.tl ol{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:11px}
.tl li{position:relative;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--primary);background:var(--track);border-radius:0 5px 5px 0}
@@ -911,7 +911,7 @@ export default function BusinessChainEditorPage() {
R_CARD,
)}
>
🪶
链
</div>
<div className="leading-tight">
<div className="text-base font-bold text-foreground">业务链条配置</div>
@@ -175,7 +175,7 @@ export default function BusinessChainListPage() {
toast.success(
chain.isPublic
? `已取消发布「${chain.title || "未命名链条"}」`
: `🌍「${chain.title || "未命名链条"}」已发布到公共选用库`,
: `「${chain.title || "未命名链条"}」已发布到公共选用库`,
);
} catch (err) {
toast.error(`操作失败:${err instanceof Error ? err.message : String(err)}`);
@@ -196,7 +196,7 @@ export default function BusinessChainListPage() {
await deleteChain(chain.id);
await refresh(scope);
await loadStats();
toast.success(`🗑️ 已删除链条「${name}」`);
toast.success(`已删除链条「${name}」`);
setPendingDelete(null);
} catch (err) {
toast.error(`删除失败:${err instanceof Error ? err.message : String(err)}`);
@@ -1052,13 +1052,13 @@ export default function RoundtablePlanningPage({
const candidates = filterRecommendCandidates(agents);
const result = buildChainSeatSelection(chain, candidates);
if (!result) {
triggerToast("⚠️ 该业务链条暂无法选用(匹配到的席位不足)");
triggerToast("该业务链条暂无法选用(匹配到的席位不足)");
return;
}
setAutoChainResult(result);
triggerToast(`✅ 已选择业务链「${chain.title}」,完成第一步澄清后点「进入多智能体研讨」`);
triggerToast(`已选择业务链「${chain.title}」,完成第一步澄清后点「进入多智能体研讨」`);
} catch {
triggerToast("❌ 选择业务链失败,请重试");
triggerToast("选择业务链失败,请重试");
}
},
[autoChainResult, triggerToast],
@@ -1276,7 +1276,7 @@ export default function RoundtablePlanningPage({
navigate("/page/strategy/admin/roundtable-diagnostics");
return;
}
triggerToast("⚙️ 系统管理面板拉取中(暂不开放外部审计)");
triggerToast("系统管理面板拉取中(暂不开放外部审计)");
}, [buildWujieConsensusPayload, emitConsensusToParent, inWujieEmbed, navigate, triggerToast]);
// C4(无界回传):第二步**本次 live 研讨** newly 达成共识时,把总控结论 + 各智能体交付广播给父页面。
@@ -1576,7 +1576,7 @@ export default function RoundtablePlanningPage({
const handleStartNewTask = useCallback(() => {
resetToStep1();
triggerToast("✨ 已开始新任务,请在第一步与任务理解智能体澄清需求");
triggerToast("已开始新任务,请在第一步与任务理解智能体澄清需求");
}, [resetToStep1, triggerToast]);
// 深链接(taskId)模式专用的「重新分析」:清空第一步会话、保留当前任务(prefill 不动),并复位
@@ -1585,7 +1585,7 @@ export default function RoundtablePlanningPage({
const handleReanalyze = useCallback(() => {
autoStartedRef.current = false;
resetToStep1();
triggerToast("🔄 已重新分析当前任务");
triggerToast("已重新分析当前任务");
}, [resetToStep1, triggerToast]);
// 无界抽屉保活复用:父页面重开抽屉时 `bus.$emit('roundtable:reset')` → 子应用回到第一步。
@@ -1684,7 +1684,7 @@ export default function RoundtablePlanningPage({
const handleBackgroundSuspend = useCallback(() => {
const selected = step2.selectedAgents;
if (selected.length === 0) {
triggerToast("⚠️ 请先选择研讨席位再后台挂起");
triggerToast("请先选择研讨席位再后台挂起");
return;
}
// —— 跳转前同步冻结所有需要的输入(reset 之后这些 live 状态都会被清空)——
@@ -1706,7 +1706,7 @@ export default function RoundtablePlanningPage({
// 立刻:标记「挂起中」+ 跳回第一步 + 提示(不等接口)。
if (draftId) markPendingSuspend(draftId);
resetToStep1();
triggerToast("🚀 已转入后台,任务会自动跑完研讨、总结报告与结果绘制;可在「历史记录」查看进度");
triggerToast("已转入后台,任务会自动跑完研讨、总结报告与结果绘制;可在「历史记录」查看进度");
// 后台 fire-and-forget:用冻结快照存稿 → 去重 → 建作业 → 注册完成通知。失败只提示、不挡跳转。
void (async () => {
@@ -1742,7 +1742,7 @@ export default function RoundtablePlanningPage({
watchId = job.id;
}
} catch (err) {
triggerToast(`❌ 后台挂起失败:${(err as Error).message}`);
triggerToast(`后台挂起失败:${(err as Error).message}`);
} finally {
// 注册到全工作区通知器,作业完成时**跨页**弹「达成共识同款」提示。
if (watchId) watchJob(watchId);
@@ -1842,7 +1842,7 @@ export default function RoundtablePlanningPage({
id: Date.now() + Math.random(),
sender: "agent",
senderName: "系统提示",
content: "⚠️ 您跳过了当前追问,系统已使用深度预测模型填充默认最稳健配置。",
content: "您跳过了当前追问,系统已使用深度预测模型填充默认最稳健配置。",
time: nowTime(),
});
triggerToast("已跳过,自动填充最优配置");
@@ -1854,7 +1854,7 @@ export default function RoundtablePlanningPage({
sender: "agent",
senderName: "任务理解智能体",
content:
"🔎 为了使方案时间边界更加安全:请问72小时内是否允许团队进行全仿真机房集成演练?这需要额外调拨3% of 预算冗余,但安全系数能提升12%。",
"为了使方案时间边界更加安全:请问72小时内是否允许团队进行全仿真机房集成演练?这需要额外调拨3% of 预算冗余,但安全系数能提升12%。",
time: nowTime(),
});
triggerToast("追问生成就绪!");
@@ -2153,7 +2153,7 @@ export default function RoundtablePlanningPage({
onFollowUp={handleAgentFollowUp}
onEnterRoundtable={() => {
if (!step1.intentReady) {
triggerToast("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
triggerToast("请先与任务理解智能体完成澄清,生成任务摘要后再进入");
return;
}
// 会商自动化串联(模块 C):若任务页带来的业务链已解析成功,跳过选链弹窗,
@@ -2223,12 +2223,12 @@ export default function RoundtablePlanningPage({
onSavePersona={step2.handleSaveAgentAndRecalculate}
onProceedToStep3={() => {
if (!step2.hasConsensus) {
triggerToast("⚠️ 圆桌尚未自然收敛,请等待总控派活全部完成");
triggerToast("圆桌尚未自然收敛,请等待总控派活全部完成");
return;
}
setCurrentStep(3);
triggerToast(
"🎉 多角色智能体博弈共识达成,最终交付全套路线成果文档及方案图!",
"多角色智能体博弈共识达成,最终交付全套路线成果文档及方案图!",
);
}}
onBackgroundSuspend={handleBackgroundSuspend}
@@ -2345,7 +2345,7 @@ function StepNavigation({
locked={step2Locked}
onClick={() => {
if (step2Locked) {
triggerToast("⚠️ 请先在第一步完成任务理解,确认[INTENT_READY]信号后再进入");
triggerToast("请先在第一步完成任务理解,确认[INTENT_READY]信号后再进入");
return;
}
onStepClick(2);
@@ -2361,8 +2361,8 @@ function StepNavigation({
if (step3Locked) {
triggerToast(
intentReady
? "⚠️ 请先在第二步达成共识(总控自然收敛)再进入"
: "⚠️ 请先依次完成第一步和第二步",
? "请先在第二步达成共识(总控自然收敛)再进入"
: "请先依次完成第一步和第二步",
);
return;
}
@@ -2414,7 +2414,7 @@ function StepButton({
{index}
</span>
<span>{label}</span>
{locked && <span className="text-[9px] ml-0.5">🔒</span>}
{locked && <span className="ml-0.5 text-[9px]">锁</span>}
{active && (
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-8 h-1 bg-primary rounded-full" />
)}
@@ -4265,7 +4265,7 @@ function RightSidebar({
<span className="text-slate-400 font-medium">共 {timelineEntries.length} 条记录</span>
<button
onClick={() =>
triggerToast(`📊 正在渲染全部 ${timelineEntries.length} 个历史事件归档`)
triggerToast(`正在渲染全部 ${timelineEntries.length} 个历史事件归档`)
}
className="text-primary hover:text-[var(--brand-900)] font-bold hover:underline"
>
@@ -104,6 +104,24 @@
--brand-900: #4096ff; /* 红强调 / 按钮 hover → 蓝(zs-002-05) */
--brand-950: #91caff; /* 深红文字 → 浅蓝(zs-002-03,深底上可读) */
/* Tailwind opacity fallbacks used by Chrome 109. Keep these in sync with
* the scoped semantic tokens above, otherwise /5, /20, etc. become opaque. */
--legacy-background-rgb: 6 14 36;
--legacy-foreground-rgb: 197 207 224;
--legacy-card-rgb: 3 38 80;
--legacy-muted-rgb: 2 24 58;
--legacy-muted-foreground-rgb: 110 136 176;
--legacy-secondary-rgb: 26 47 90;
--legacy-accent-rgb: 26 47 90;
--legacy-primary-rgb: 22 119 255;
--legacy-primary-foreground-rgb: 255 255 255;
--legacy-ring-rgb: 22 119 255;
--legacy-border-rgb: 43 143 254;
--legacy-input-rgb: 90 169 255;
--legacy-destructive-rgb: 245 34 45;
--legacy-workspace-brand-rgb: 22 119 255;
--legacy-brand-200-rgb: 22 51 95;
/* —— 圆桌局部别名(回落到上面已覆盖的 token) —— */
--rt-page-bg: var(--background);
--rt-primary: var(--primary);
@@ -134,22 +152,30 @@
--muted: #0f1c3d;
--secondary: #1a2f5a;
--accent: #1a2f5a;
--legacy-background-rgb: 18 35 71;
--legacy-card-rgb: 12 24 54;
--legacy-muted-rgb: 15 28 61;
--legacy-secondary-rgb: 26 47 90;
--legacy-accent-rgb: 26 47 90;
background-color: var(--background);
background-image: none;
}
.dark .roundtable-planning ::-webkit-scrollbar-thumb,
.roundtable-planning--dark ::-webkit-scrollbar-thumb {
background: rgb(32 132 255 / 70%);
background: color-mix(in oklch, var(--primary) 45%, var(--border));
}
.dark .roundtable-planning ::-webkit-scrollbar-thumb:hover,
.roundtable-planning--dark ::-webkit-scrollbar-thumb:hover {
background: rgb(29 128 255 / 82%);
background: color-mix(in oklch, var(--primary) 65%, var(--border));
}
.dark .roundtable-planning .custom-scrollbar-inner::-webkit-scrollbar-thumb,
.roundtable-planning--dark .custom-scrollbar-inner::-webkit-scrollbar-thumb {
background: rgb(22 119 255 / 35%);
background: color-mix(in oklch, var(--primary) 35%, transparent);
}
@@ -216,15 +242,16 @@
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.from-\[var\(--brand-200\)\]) {
--tw-gradient-from: color-mix(in oklch, var(--accent) 60%, transparent) !important;
--tw-gradient-from: rgb(26 47 90 / 60%) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.via-\[var\(--brand-200\)\]) {
--tw-gradient-via: color-mix(in oklch, var(--accent) 60%, transparent) !important;
--tw-gradient-via: rgb(26 47 90 / 60%) !important;
}
/* ---- 品牌浅红底(brand-200 等) → accent ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark) [class*="brand-200"] {
background-color: rgb(26 47 90 / 70%) !important;
background-color: color-mix(in oklch, var(--accent) 70%, transparent) !important;
}
@@ -323,12 +350,13 @@
.focus-within\:ring-\[var\(--brand-200\)\]\/50:focus-within,
.focus\:ring-\[var\(--brand-200\)\]\/50:focus
) {
--tw-ring-color: color-mix(in oklch, var(--primary) 30%, transparent) !important;
--tw-ring-color: rgb(22 119 255 / 30%) !important;
}
/* ---- 选中边框 border-primary → 主色描边(柔化) ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-primary,
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-2.border-primary {
border-color: rgb(32 132 255 / 70%) !important;
border-color: color-mix(in oklch, var(--primary) 55%, var(--border)) !important;
}
@@ -341,10 +369,12 @@
/* —— 成功 · emerald —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-emerald-50, .bg-emerald-50\/40, .bg-emerald-50\/50, .bg-emerald-100, .bg-emerald-100\/40) {
background-color: #054059 !important;
background-color: color-mix(in oklch, #10b981 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.border-emerald-200, .border-emerald-200\/40, .border-emerald-200\/50) {
border-color: #20a1ca !important;
border-color: color-mix(in oklch, #10b981 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -358,6 +388,7 @@
.bg-amber-50, .bg-amber-50\/60, .bg-amber-100, .bg-amber-100\/40, .bg-amber-200,
.hover\:bg-amber-100:hover, .hover\:bg-amber-200:hover
) {
background-color: #333e42 !important;
background-color: color-mix(in oklch, #f59e0b 20%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -365,6 +396,7 @@
.border-amber-100, .border-amber-200, .border-amber-200\/40, .border-amber-300,
.border-amber-300\/50, .border-amber-500, .border-amber-700\/50
) {
border-color: #809598 !important;
border-color: color-mix(in oklch, #f59e0b 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -372,12 +404,14 @@
color: #fcd34d !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .focus\:ring-amber-200:focus {
--tw-ring-color: rgb(245 158 11 / 35%) !important;
--tw-ring-color: color-mix(in oklch, #f59e0b 35%, transparent) !important;
}
/* —— 错误 · red / rose —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-red-50, .bg-red-200, .bg-rose-50, .hover\:bg-rose-50:hover) {
background-color: #2d2b4e !important;
background-color: color-mix(in oklch, #ef4444 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -387,19 +421,23 @@
/* —— 信息 · sky(部分组件已自带 dark: 变体,这里兜底无 dark: 的) —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark) .bg-sky-50 {
background-color: #053d6f !important;
background-color: color-mix(in oklch, #0ea5e9 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.border-sky-200, .border-sky-300\/50) {
border-color: #1f98f5 !important;
border-color: color-mix(in oklch, #0ea5e9 42%, var(--border)) !important;
}
/* —— 装饰 · purple —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-purple-50, .bg-purple-100\/40) {
background-color: #212e6e !important;
background-color: color-mix(in oklch, #a855f7 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-purple-200 {
border-color: #5f77fb !important;
border-color: color-mix(in oklch, #a855f7 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -409,12 +447,15 @@
/* —— 装饰 · orange / pink —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark) .bg-orange-50 {
background-color: #2f3144 !important;
background-color: color-mix(in oklch, #f97316 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-orange-200\/50 {
border-color: #828bc7 !important;
border-color: color-mix(in oklch, #f97316 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .bg-pink-100\/40 {
background-color: #2e2862 !important;
background-color: color-mix(in oklch, #ec4899 18%, var(--card)) !important;
}
@@ -554,6 +595,7 @@
.dark .custom-scrollbar-inner::-webkit-scrollbar-thumb,
.roundtable-planning--dark .custom-scrollbar-inner::-webkit-scrollbar-thumb {
background: rgb(36 135 254 / 65%);
background: color-mix(in oklch, var(--primary) 35%, var(--border));
}
@@ -10,7 +10,8 @@ import {
ThemeSwitcher,
} from "@/components/workspace/appearance-controls";
import { NotificationDialog } from "@/components/workspace/notification-dialog";
import { getStoredAuth, getAccountDisplayName } from "@/core/auth";
import { getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { isWujieEnv } from "@/core/embed/wujie";
import { useUnreadCount } from "@/core/notifications";
import { useMyPosition } from "@/core/positions";
@@ -48,13 +49,14 @@ const Header = ({ collapsed }: HeaderProps) => {
const { data: unreadCount = 0 } = useUnreadCount();
const navigate = useNavigate();
const { pathname } = useLocation();
const liveAccount = useLiveAccount();
const auth = getStoredAuth();
const isAdmin = auth?.system_role === "admin";
const visibleMenuItems = useSidebarMenuItems(!!isAdmin);
const { data: myPosition } = useMyPosition();
const positionName = myPosition?.name?.trim() || "";
const displayName = getAccountDisplayName(auth);
const displayName = liveAccount.displayName;
const showTopNav = isDark && !collapsed && !isWujieEnv();
// 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。
// 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏,
@@ -30,6 +30,7 @@ import {
import { requestCrudService } from '@/service/request-axios-service';
import { authStorageGet } from '@/core/auth/scoped-storage';
import { copyTextToClipboard } from '@/core/clipboard';
import { readRuntimeEnv } from '@/env';
/**
* 通用问答组件
@@ -38,11 +39,11 @@ import { copyTextToClipboard } from '@/core/clipboard';
* @param initialMode - 初始模式(可选)
*/
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const chatUrl = import.meta.env.VITE_AIPI_CHAT_URL || '';
const generateUrl = import.meta.env.VITE_AIPI_GENERATE_CHAT_URL || '';
const taskChatUrl = import.meta.env.VITE_AIPI_TASK_CHAT_URL || '';
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
const chatUrl = readRuntimeEnv('VITE_AIPI_CHAT_URL') || '';
const generateUrl = readRuntimeEnv('VITE_AIPI_GENERATE_CHAT_URL') || '';
const taskChatUrl = readRuntimeEnv('VITE_AIPI_TASK_CHAT_URL') || '';
const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL');
const service = requestCrudService;
const GeneralQA: React.FC<{
onNavigate?: (page: string) => void;
@@ -24,6 +24,7 @@ import { useTheme } from '@/contexts/ThemeContext';
import MarkdownRenderer from '@/strategy-components/MarkdownRenderer';
import { SourceItem } from './GeneralQA.types';
import '../qa/qa-animations.css';
import { readRuntimeEnv } from '@/env';
/**
* 单条检索结果。
@@ -241,7 +242,7 @@ export function ChatMessage<T extends QACommonMessage>({
const isDark = theme === 'dark';
const isUser = message.role === 'user';
const hasTrace = !isUser && !!(message.singleRagTrace || message.thinkingTrace);
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL');
const [visible, setVisible] = useState(false); // 打开详情对话框
const [listData, setListData] = useState<SourceItem>({
title: '',
@@ -531,9 +532,9 @@ const RetrievalList: React.FC<RetrievalListProps> = ({ items }) => {
title: '',
content: '',
});
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
const b1SearchUrl = import.meta.env.VITE_AIPI_B1COH_URL;
const b2SearchUrl = import.meta.env.VITE_AIPI_B2ACTOR_URL;
const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL');
const b1SearchUrl = readRuntimeEnv('VITE_AIPI_B1COH_URL');
const b2SearchUrl = readRuntimeEnv('VITE_AIPI_B2ACTOR_URL');
const showDialog = (item: QARetrievalItem) => {
console.log('item', item)
if (item.type === 'more') {
@@ -13,6 +13,8 @@
--yellow: #f59e0b;
--red: #dc2626;
--purple: #7c3aed;
--accent-rgb: 220 38 38;
--text-muted-rgb: 107 114 128;
--button-text: #ffffff;
--surface-hover: rgba(220, 38, 38, 0.04);
--surface-hover-strong: rgba(220, 38, 38, 0.08);
@@ -55,6 +57,8 @@
--yellow: #f59e0b;
--red: #f43f5e;
--purple: #8b5cf6;
--accent-rgb: 59 130 246;
--text-muted-rgb: 148 163 184;
--button-text: #ffffff;
--surface-hover: rgba(255, 255, 255, 0.03);
--surface-hover-strong: rgba(255, 255, 255, 0.06);
@@ -137,6 +141,7 @@
}
.mope-chat .app-header {
background: var(--surface);
background: color-mix(in srgb, var(--surface) 92%, var(--accent) 8%);
border-bottom: 1px solid var(--border);
padding: 14px 18px;
@@ -179,6 +184,7 @@
flex-shrink: 0;
overflow-y: auto;
border-right: 1px solid var(--border);
background: var(--surface);
background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
}
@@ -194,6 +200,7 @@
flex-shrink: 0;
overflow-y: auto;
border-left: 1px solid var(--border);
background: var(--surface);
background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
}
@@ -322,11 +329,11 @@
letter-spacing: 0.3px;
}
.mope-chat .badge-starting { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.mope-chat .badge-diverging { background: color-mix(in srgb, var(--yellow) 15%, transparent); color: var(--yellow); }
.mope-chat .badge-closing { background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
.mope-chat .badge-done { background: color-mix(in srgb, var(--purple) 15%, transparent); color: var(--purple); }
.mope-chat .badge-default { background: color-mix(in srgb, var(--text-muted) 15%, transparent); color: var(--text-muted); }
.mope-chat .badge-starting { background: rgb(var(--accent-rgb) / 15%); background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.mope-chat .badge-diverging { background: rgb(245 158 11 / 15%); background: color-mix(in srgb, var(--yellow) 15%, transparent); color: var(--yellow); }
.mope-chat .badge-closing { background: rgb(16 185 129 / 15%); background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
.mope-chat .badge-done { background: rgb(139 92 246 / 15%); background: color-mix(in srgb, var(--purple) 15%, transparent); color: var(--purple); }
.mope-chat .badge-default { background: rgb(var(--text-muted-rgb) / 15%); background: color-mix(in srgb, var(--text-muted) 15%, transparent); color: var(--text-muted); }
/* Spinner */
@keyframes spin {
@@ -133,6 +133,7 @@
.config-mode-switch {
display: inline-flex;
background: var(--surface);
background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
border: 1px solid var(--border);
border-radius: 12px;
@@ -281,6 +282,7 @@
.config-textarea::placeholder,
.config-input::placeholder,
.config-json-editor::placeholder {
color: rgb(var(--text-muted-rgb) / 72%);
color: color-mix(in srgb, var(--text-muted) 72%, transparent);
}
@@ -389,7 +389,9 @@
margin-top: 10px;
margin-bottom: 0;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
border: 1px solid rgb(var(--accent-rgb) / 28%);
border-color: color-mix(in srgb, var(--accent) 28%, transparent);
box-shadow: 0 8px 18px rgb(var(--accent-rgb) / 10%);
box-shadow: 0 8px 18px color-mix(in srgb, var(--accent) 10%, transparent);
}
@@ -8,6 +8,7 @@ import { Col, Loading, Pagination, Progress, Row } from 'tdesign-react';
import { requestCrudService } from '@/service/request-axios-service';
import { ActionColumn } from './ActionColumn';
import { resolveStatusIcon } from './icons';
import { readRuntimeEnv } from '@/env';
interface ActionAnalyzesProps {
TaskType?: string;
@@ -17,7 +18,7 @@ interface ActionAnalyzesProps {
}
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category, subCategory}) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { Col, Pagination, Progress, Row } from 'tdesign-react';
import { requestCrudService } from '@/service/request-axios-service';
import { resolveStatusIcon } from './icons';
import { readRuntimeEnv } from '@/env';
interface ActionColumnProps {
TaskType?: string;
@@ -21,7 +22,7 @@ interface ActionColumnProps {
}
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
export const ActionColumn: React.FC<ActionColumnProps> = ({ title, category, subCategory, icon, columnName, columnIndex, actionCount, actionList }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { KanbanColumn } from './KanbanColumn';
import { TaskListItem } from './TaskManagement';
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
import { readRuntimeEnv } from '@/env';
interface TaskListPageProps {
title: string;
@@ -25,7 +26,7 @@ interface StateItem {
type ColumnType = 'unanalyzed' | 'running' | 'completed';
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
/** 本页对应的业务码(3QFX = 3Q);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
const BUSINESS_CODE = '3Q';
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { KanbanColumn } from './KanbanColumn';
import { TaskListItem } from './TaskManagement';
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
import { readRuntimeEnv } from '@/env';
interface TaskListPageProps {
title: string;
@@ -25,7 +26,7 @@ interface StateItem {
type ColumnType = 'unanalyzed' | 'running' | 'completed';
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
/** 本页对应的业务码(任务FX = 6BF);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
const BUSINESS_CODE = '6BF';
+216 -80
View File
@@ -135,23 +135,6 @@
}
}
--animate-wave: wave 0.6s ease-in-out 2;
@keyframes wave {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(20deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(20deg);
}
}
--animate-breathe-stop: breathe-stop 1.8s ease-in-out infinite;
@keyframes breathe-stop {
0%,
@@ -253,73 +236,113 @@
--brand-900: #cf1322;
--brand-950: #a8071a;
--radius: 0.75rem;
--background: oklch(1 0 0);
--foreground: oklch(0.22 0.02 25);
--card: oklch(1 0.002 95);
--card-foreground: oklch(0.22 0.02 25);
--popover: oklch(1 0.002 95);
--popover-foreground: oklch(0.22 0.02 25);
--primary: oklch(0.52 0.19 27);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.955 0.008 95);
--secondary-foreground: oklch(0.32 0.04 25);
--muted: oklch(0.965 0.006 95);
--muted-foreground: oklch(0.48 0.02 25);
--accent: oklch(0.96 0.04 25);
--accent-foreground: oklch(0.35 0.12 25);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.9 0.008 95);
--input: oklch(0.9 0.01 95);
--ring: oklch(0.52 0.19 27 / 0.35);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.995 0.003 95);
--sidebar-foreground: oklch(0.28 0.02 25);
--sidebar-primary: oklch(0.52 0.19 27);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.978 0.018 70);
--sidebar-accent-foreground: oklch(0.32 0.05 32);
--sidebar-border: oklch(0.92 0.01 85);
--sidebar-ring: oklch(0.52 0.19 27 / 0.4);
/* Keep the base design tokens in legacy color syntax. CSS custom
properties containing oklch() make every dependent Tailwind color rule
invalid in Chrome 109 and below. */
--background: #ffffff;
--foreground: #231716;
--card: #ffffff;
--card-foreground: #231716;
--popover: #ffffff;
--popover-foreground: #231716;
--primary: #be2323;
--primary-foreground: #fcfcfc;
--secondary: #f2f0ea;
--secondary-foreground: #513b38;
--muted: #f5f3ef;
--muted-foreground: #695958;
--accent: #ffedeb;
--accent-foreground: #7b2727;
--destructive: #e40014;
--border: #dfded8;
--input: #e0ded7;
--ring: rgba(190, 35, 35, 0.35);
--chart-1: #d94a32;
--chart-2: #188f95;
--chart-3: #4f6680;
--chart-4: #e8a72a;
--chart-5: #db8d16;
--sidebar: #fdfcfb;
--sidebar-foreground: #322524;
--sidebar-primary: #be2323;
--sidebar-primary-foreground: #fcfcfc;
--sidebar-accent: #fff6ec;
--sidebar-accent-foreground: #633a32;
--sidebar-border: #e8e4dd;
--sidebar-ring: rgba(190, 35, 35, 0.4);
/* Chrome 109 fallback channels for Tailwind opacity modifiers. */
--legacy-accent-rgb: 255 237 235;
--legacy-background-rgb: 255 255 255;
--legacy-border-rgb: 223 222 216;
--legacy-brand-200-rgb: 254 237 237;
--legacy-card-rgb: 255 255 255;
--legacy-destructive-rgb: 228 0 20;
--legacy-foreground-rgb: 35 23 22;
--legacy-input-rgb: 224 222 215;
--legacy-muted-rgb: 245 243 239;
--legacy-muted-foreground-rgb: 105 89 88;
--legacy-primary-rgb: 190 35 35;
--legacy-primary-foreground-rgb: 252 252 252;
--legacy-ring-rgb: 190 35 35;
--legacy-secondary-rgb: 242 240 234;
--legacy-sidebar-accent-rgb: 255 246 236;
--legacy-sidebar-border-rgb: 232 228 221;
--legacy-sidebar-foreground-rgb: 50 37 36;
--legacy-workspace-brand-rgb: 245 34 45;
}
.dark {
--workspace-brand: #3b82f6;
--workspace-brand-foreground: #ffffff;
--background: oklch(0.208 0.042 264);
--foreground: oklch(0.96 0.005 240);
--card: oklch(0.265 0.04 260);
--card-foreground: oklch(0.96 0.005 240);
--popover: oklch(0.245 0.04 262);
--popover-foreground: oklch(0.96 0.005 240);
--primary: oklch(0.623 0.214 259);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.32 0.038 260);
--secondary-foreground: oklch(0.96 0.005 240);
--muted: oklch(0.30 0.035 260);
--muted-foreground: oklch(0.68 0.02 240);
--accent: oklch(0.35 0.06 260);
--accent-foreground: oklch(0.96 0.005 240);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 12%);
--input: oklch(1 0 0 / 14%);
--ring: oklch(0.623 0.214 259 / 0.45);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.185 0.04 265);
--sidebar-foreground: oklch(0.94 0.005 240);
--sidebar-primary: oklch(0.623 0.214 259);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.30 0.055 260);
--sidebar-accent-foreground: oklch(0.96 0.005 240);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.623 0.214 259 / 0.45);
--background: #0e172b;
--foreground: #eff2f5;
--card: #1a2539;
--card-foreground: #eff2f5;
--popover: #17243a;
--popover-foreground: #eff2f5;
--primary: #2b81ff;
--primary-foreground: #fcfcfc;
--secondary: #273346;
--secondary-foreground: #eff2f5;
--muted: #232e40;
--muted-foreground: #8e9aa4;
--accent: #283b5a;
--accent-foreground: #eff2f5;
--destructive: #ff6568;
--border: rgba(255, 255, 255, 0.12);
--input: rgba(255, 255, 255, 0.14);
--ring: rgba(43, 129, 255, 0.45);
--chart-1: #236bdb;
--chart-2: #51c49d;
--chart-3: #e8b02a;
--chart-4: #a65be5;
--chart-5: #e65a42;
--sidebar: #0d1830;
--sidebar-foreground: #e8ecee;
--sidebar-primary: #2b81ff;
--sidebar-primary-foreground: #fcfcfc;
--sidebar-accent: #1d2e49;
--sidebar-accent-foreground: #eff2f5;
--sidebar-border: rgba(255, 255, 255, 0.1);
--sidebar-ring: rgba(43, 129, 255, 0.45);
--legacy-accent-rgb: 40 59 90;
--legacy-background-rgb: 14 23 43;
--legacy-border-rgb: 255 255 255;
--legacy-card-rgb: 26 37 57;
--legacy-destructive-rgb: 255 101 104;
--legacy-foreground-rgb: 239 242 245;
--legacy-input-rgb: 255 255 255;
--legacy-muted-rgb: 35 46 64;
--legacy-muted-foreground-rgb: 142 154 164;
--legacy-primary-rgb: 43 129 255;
--legacy-primary-foreground-rgb: 252 252 252;
--legacy-ring-rgb: 43 129 255;
--legacy-secondary-rgb: 39 51 70;
--legacy-sidebar-accent-rgb: 29 46 73;
--legacy-sidebar-border-rgb: 255 255 255;
--legacy-sidebar-foreground-rgb: 232 236 238;
--legacy-workspace-brand-rgb: 59 130 246;
font-weight: 300;
}
@@ -364,6 +387,24 @@
--sidebar-accent-foreground: #0056c6;
--sidebar-border: #e6ebf2;
--sidebar-ring: rgb(0 98 217 / 0.4);
--legacy-accent-rgb: 234 244 255;
--legacy-background-rgb: 255 255 255;
--legacy-border-rgb: 219 219 219;
--legacy-brand-200-rgb: 234 244 255;
--legacy-card-rgb: 255 255 255;
--legacy-destructive-rgb: 245 34 45;
--legacy-foreground-rgb: 61 61 61;
--legacy-input-rgb: 219 219 219;
--legacy-muted-rgb: 243 245 248;
--legacy-muted-foreground-rgb: 127 127 127;
--legacy-primary-rgb: 0 98 217;
--legacy-primary-foreground-rgb: 255 255 255;
--legacy-ring-rgb: 0 98 217;
--legacy-secondary-rgb: 244 249 255;
--legacy-sidebar-accent-rgb: 234 244 255;
--legacy-sidebar-border-rgb: 230 235 242;
--legacy-sidebar-foreground-rgb: 61 61 61;
--legacy-workspace-brand-rgb: 0 98 217;
}
@layer base {
@@ -698,7 +739,7 @@
}
.dark .workspace-panel {
border: 1px solid oklch(1 0 0 / 10%);
border: 1px solid rgba(255, 255, 255, 0.1);
background: var(--card);
box-shadow: none;
}
@@ -1004,6 +1045,7 @@
--container-width-sm: calc(var(--spacing) * 144);
--container-width-md: calc(var(--spacing) * 204);
--container-width-lg: calc(var(--spacing) * 256);
--container-width-xl: calc(var(--spacing) * 280);
}
/* ── Landing 态输入框 Gemini 风格:旋转彩色描边 + 多色柔光阴影 ───────── */
@@ -1237,3 +1279,97 @@ html.lightblue .tiptap-writing-editor .ProseMirror ::selection {
color: #1890ff;
z-index: 2;
}
/* --------------------------------------------------------------------------
* Legacy Chrome visual fallbacks
*
* Tailwind v4 deliberately uses color-mix() for `bg-primary/10` and similar
* opacity utilities. In Chrome versions without color-mix(), its generated
* fallback is the fully opaque primary color. That turns pale selected cards
* into solid red blocks and makes same-color icons disappear. Keep critical
* agent surfaces on classic rgba()/hex colours instead.
* -------------------------------------------------------------------------- */
.agent-avatar {
background-color: #feeded;
color: #cf1322;
box-shadow: inset 0 0 0 1px rgba(190, 35, 35, 0.15);
}
.dark .agent-avatar {
background-color: #1b355d;
color: #ffffff;
box-shadow: inset 0 0 0 1px rgba(91, 157, 255, 0.38);
}
.lightblue .agent-avatar {
background-color: #eaf4ff;
color: #0062d9;
box-shadow: inset 0 0 0 1px rgba(0, 98, 217, 0.16);
}
.agent-sidebar-card--active {
background-color: #feeded;
border-color: rgba(190, 35, 35, 0.5);
}
.dark .agent-sidebar-card--active {
background-color: #0b4fae;
border-color: #2b8ffe;
}
.lightblue .agent-sidebar-card--active {
background-color: #eaf4ff;
border-color: rgba(0, 98, 217, 0.5);
}
@supports not (color: oklch(1 0 0)) {
.bg-primary\/5 { background-color: rgba(190, 35, 35, 0.05); }
.bg-primary\/8 { background-color: rgba(190, 35, 35, 0.08); }
.bg-primary\/10 { background-color: rgba(190, 35, 35, 0.10); }
.bg-primary\/14 { background-color: rgba(190, 35, 35, 0.14); }
.bg-primary\/15 { background-color: rgba(190, 35, 35, 0.15); }
.bg-primary\/20 { background-color: rgba(190, 35, 35, 0.20); }
.bg-primary\/25 { background-color: rgba(190, 35, 35, 0.25); }
.bg-primary\/30 { background-color: rgba(190, 35, 35, 0.30); }
.bg-primary\/60 { background-color: rgba(190, 35, 35, 0.60); }
.bg-primary\/70 { background-color: rgba(190, 35, 35, 0.70); }
.bg-primary\/90 { background-color: rgba(190, 35, 35, 0.90); }
.border-primary\/15 { border-color: rgba(190, 35, 35, 0.15); }
.border-primary\/20 { border-color: rgba(190, 35, 35, 0.20); }
.border-primary\/25 { border-color: rgba(190, 35, 35, 0.25); }
.border-primary\/30 { border-color: rgba(190, 35, 35, 0.30); }
.border-primary\/35 { border-color: rgba(190, 35, 35, 0.35); }
.border-primary\/40 { border-color: rgba(190, 35, 35, 0.40); }
.border-primary\/50 { border-color: rgba(190, 35, 35, 0.50); }
.border-primary\/60 { border-color: rgba(190, 35, 35, 0.60); }
.ring-primary\/15 { --tw-ring-color: rgba(190, 35, 35, 0.15); }
.ring-primary\/20 { --tw-ring-color: rgba(190, 35, 35, 0.20); }
.ring-primary\/30 { --tw-ring-color: rgba(190, 35, 35, 0.30); }
.ring-primary\/40 { --tw-ring-color: rgba(190, 35, 35, 0.40); }
.ring-primary\/50 { --tw-ring-color: rgba(190, 35, 35, 0.50); }
.ring-primary\/60 { --tw-ring-color: rgba(190, 35, 35, 0.60); }
.dark .bg-primary\/5 { background-color: rgba(43, 129, 255, 0.05); }
.dark .bg-primary\/8 { background-color: rgba(43, 129, 255, 0.08); }
.dark .bg-primary\/10 { background-color: rgba(43, 129, 255, 0.10); }
.dark .bg-primary\/14 { background-color: rgba(43, 129, 255, 0.14); }
.dark .bg-primary\/15 { background-color: rgba(43, 129, 255, 0.15); }
.dark .bg-primary\/20 { background-color: rgba(43, 129, 255, 0.20); }
.dark .bg-primary\/25 { background-color: rgba(43, 129, 255, 0.25); }
.dark .bg-primary\/30 { background-color: rgba(43, 129, 255, 0.30); }
.dark .bg-primary\/60 { background-color: rgba(43, 129, 255, 0.60); }
.dark .bg-primary\/70 { background-color: rgba(43, 129, 255, 0.70); }
.dark .bg-primary\/90 { background-color: rgba(43, 129, 255, 0.90); }
.lightblue .bg-primary\/5 { background-color: rgba(0, 98, 217, 0.05); }
.lightblue .bg-primary\/8 { background-color: rgba(0, 98, 217, 0.08); }
.lightblue .bg-primary\/10 { background-color: rgba(0, 98, 217, 0.10); }
.lightblue .bg-primary\/14 { background-color: rgba(0, 98, 217, 0.14); }
.lightblue .bg-primary\/15 { background-color: rgba(0, 98, 217, 0.15); }
.lightblue .bg-primary\/20 { background-color: rgba(0, 98, 217, 0.20); }
.lightblue .bg-primary\/25 { background-color: rgba(0, 98, 217, 0.25); }
.lightblue .bg-primary\/30 { background-color: rgba(0, 98, 217, 0.30); }
.lightblue .bg-primary\/60 { background-color: rgba(0, 98, 217, 0.60); }
.lightblue .bg-primary\/70 { background-color: rgba(0, 98, 217, 0.70); }
.lightblue .bg-primary\/90 { background-color: rgba(0, 98, 217, 0.90); }
}
+4 -3
View File
@@ -9,22 +9,23 @@ import {
matchSqReportMock,
sqReportMockAdapter,
} from '@/strategy-components/components/task-components/sq-report-mock-data'
import { readRuntimeFlag } from '@/env'
// 任务看板(任务分析 / 三情分析)数据源开关:
// VITE_USE_REAL_DATA=true → 走真实接口;其它(含未设置)→ 这两个接口走本地假数据。
// 提交的 .env.development/.env.production/.env.network/.env.sit 默认 true(真实数据),
// 仅本机 .env 默认 false(假数据)。改 env 后需重启 dev server 生效。
const USE_REAL_DATA = import.meta.env.VITE_USE_REAL_DATA === 'true'
const USE_REAL_DATA = readRuntimeFlag('VITE_USE_REAL_DATA')
// 任务详情(cop-task-detail,登录深链用)数据源开关:
// 默认真实数据;仅当 VITE_MOCK_COP_TASK_DETAIL=true 时走本地假数据(consumer 外网不可达时调试用)。
// 与 USE_REAL_DATA 独立。改 env 后需重启 dev server 生效。
const MOCK_COP_TASK_DETAIL = import.meta.env.VITE_MOCK_COP_TASK_DETAIL === 'true'
const MOCK_COP_TASK_DETAIL = readRuntimeFlag('VITE_MOCK_COP_TASK_DETAIL')
// 研判情报报告(selectSqReport,大屏绘制智能体深链用)数据源开关:
// 默认真实数据;仅当 VITE_MOCK_SQ_REPORT=true 时走本地假数据(真实接口未就绪时调试用)。
// 与上面两个开关独立。改 env 后需重启 dev server 生效。
const MOCK_SQ_REPORT = import.meta.env.VITE_MOCK_SQ_REPORT === 'true'
const MOCK_SQ_REPORT = readRuntimeFlag('VITE_MOCK_SQ_REPORT')
// axios.interceptors
const service = axios.create({
+16
View File
@@ -6,9 +6,21 @@ import { defineConfig } from "vite";
import sourceIdentifierPlugin from "vite-plugin-source-identifier";
const rootDir = fileURLToPath(new URL(".", import.meta.url));
// Tailwind 4 emits CSS cascade layers, which require Chrome 99+. Chrome 100
// is therefore the practical lower bound for this Win7 legacy build.
const LEGACY_CHROME = 100 << 16;
export default defineConfig({
plugins: [sourceIdentifierPlugin(), react()],
css: {
// Also used by the production CSS minifier. Development CSS is handled
// by the matching PostCSS compatibility pass in postcss.config.js.
lightningcss: {
targets: {
chrome: LEGACY_CHROME,
},
},
},
resolve: {
alias: [
{
@@ -34,6 +46,10 @@ export default defineConfig({
],
},
"base":"./",
build: {
cssMinify: "lightningcss",
target: "chrome100",
},
server: {
// 与 scripts/env.sh FRONTEND_PORT=5174、圆桌 wujie 嵌入文档一致。
port: 5173,
+1 -1
View File
@@ -281,7 +281,7 @@ Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship
**Partial-result harvest + query simplification (检不出/超时的容错).** When `researcher_skill_agent: true` runs a non-native skill via `run_skill_via_real_agent` (the deployment's real path — the skill, e.g. `knowledge-search-v2`, is invoked by the real agent through `bash python3 scripts/search.py "<query>"`), one writing run issues many queries and **some time out** (the skill script prints `{"success":false,"error":"请求超时…","count":0}`) while others succeed (`{"success":true,"count":30,"results":[…]}`). Two fixes so successful data is never dropped: (1) `_drive()` captures every **ToolMessage** stdout and `_harvest_tool_results()` parses the skill's JSON **by reusing the lead-agent 参考文献 mode's proven extractor** (`deerflow.runtime.references`: `_parse_json_like` + `_first_list` + `_normalize_reference_item`) — the same logic that already extracts skill JSON into citations 100% reliably on ChatPage/AgentChatPage, with its huge field-name union (title/m_title/name, content/page_content/content_preview/snippet/…, recUuid/uuid/docId/…) and per-skill display-config support — plus skips `success:false` outputs — so even if the model's final `{materials:[]}` summary gives up (confused by the timeouts), the successful queries' real results are still collected; the function merges model materials + harvested results (dedup by url/recUuid/title, capped at `max_results`). **The harvest+merge (`_finalize`) runs on EVERY exit path — normal end, overall timeout, AND `await task` exceptions (e.g. `GraphRecursionError`)** — so when the run is cut off before the model writes its summary (observed: 10 queries × ~2 graph steps > the old `recursion_limit`, so the agent errored out with the real 30+ results already in `tool_outputs`), those results are still salvaged instead of returning `[]`. `recursion_limit` was also raised from `_REAL_AGENT_MAX_TURNS` to `_REAL_AGENT_MAX_TURNS*2+4` (it counts graph super-steps ≈ 2 per model→tool round, not model turns) so the model normally has room to finish its summary. **Early-stop on `max_results`:** `_drive` re-harvests after every tool output and, once the deduped harvested count reaches `max_results` (= `per_skill` = `max_materials`), **breaks the nested `astream`** and emits a 「已获取 N 条素材,达到上限,停止本技能检索」 step — without it a weak model keeps issuing new queries forever (observed: 100+ step-bar entries, materials far over the limit). Because the skill now returns exactly `max_materials`, the researcher loop's own `collect_limit` early-stop fires right after, so later configured skills don't run either. (2) The model is instructed (in `_MATERIAL_OUTPUT_CONTRACT`) that a 0-result/timeout query must be **retried with a simpler, core query** (drop topic suffixes like 竞选/诉讼/政策, keep the core entity: 「特朗普竞选」→「特朗普」) and that any data already retrieved must always be kept. The same deterministic simplifier `search/base.py::simplify_query` is wired into `IntranetSearchProvider.search`: an empty first attempt retries once with the simplified query. **(3) Root cause of the instability — bash output middle-truncation.** The skill prints its results as JSON to stdout, and the `bash` tool **middle-truncates** any output over `sandbox.bash_output_max_chars` (default **20000**, inserting a `... [middle truncated …] ...` marker). A `count:30` skill response (~30 records × ~600 chars) exceeds 20000 → the JSON is cut in the middle → **unparseable as a whole** by both the model and `_try_load_json` → all materials lost (while a small `count:9` response parses fine — hence the flapping). Fix: when whole-JSON parse yields no items, `_harvest_tool_results` falls back to `_scan_balanced_json_objects` — a brace-depth/string-aware scanner that recovers **every individually-complete `{…}` record object** from the truncated text (the records on each side of the truncation marker are still valid JSON), so ~16+16 records survive a 20000-char cut, far above `max_materials`. This is why the lead-agent chat pages (`/page/workspace/chats/new`, agent chat) call the same skill with 100% success — they only need the model to summarize prose from whatever it sees, never to parse a structured list, so truncation doesn't hurt them; ai-writing needs the structured records, so it must salvage them itself. Tests: `tests/test_ai_writing_skill_real_agent.py`, `tests/test_ai_writing_query_simplify.py`.
**Agentic skill execution (`ai_writing.researcher_skill_agent`, default false).** By default a configured non-native skill is *not* run agent-style — the node just turns its `description` into a query for the generic search provider. With this flag on, the directed-skill branch instead runs `nodes/_skill_agent.py::run_skill_search_agent`: it injects the skill's **SKILL.md** (read from `skill.skill_file`) alongside the researcher SOUL as the system prompt, binds a `search` tool to the researcher model, and runs a bounded (`_MAX_TURNS=4`) tool-call loop so the model drives the retrieval per the skill's instructions; the `SearchResult`s the tool returns are harvested directly as materials (no fuzzy text extraction). Requires a tool-calling model; if the model can't tool-call (returns no `tool_calls`) or the agent yields nothing, `_execute_skill` falls back to the legacy description→query search so materials are never empty. The built-in `knowledge-base-search` native concurrent path is unchanged. Tests: `tests/test_ai_writing_skill_agent.py`.
**Agentic skill execution (`ai_writing.researcher_skill_agent`).** When this flag is on (the deployment `config.yaml` enables it), the directed-skill branch runs `nodes/_skill_agent.py::run_skill_via_real_agent`: it reuses `SubagentExecutor` with the full tool stack, sandbox middleware, and a one-skill whitelist so the skill can `read_file` its `SKILL.md`, call `bash`, MCP tools, or whatever the skill instructions require. The nested run now passes `agent_id=ai-writing-researcher` in both `config.configurable` and runtime `context`, matching normal AgentChat skill visibility/whitelist behavior. It also appends a human-turn hard directive listing the exact SKILL.md path and skill directory, requiring weak models to first `read_file` the SKILL.md and then `cd <skill-dir> && python/python3 scripts/*.py ...` when the skill is script-backed; the model is explicitly forbidden from returning `materials` before executing the skill's required tools. If the real-agent path fails or yields no materials, `_execute_skill` still falls back to the legacy description→query search so materials are never empty. The built-in `knowledge-base-search` native concurrent path is unchanged. Tests: `tests/test_ai_writing_skill_real_agent.py`.
**Targeted user revision (局部修改 — 只改命中章节).** When the user submits a free-text 修改意见 on the draft-confirm card (`action == "user_revise"`), `writer_draft_node` no longer always rewrites the whole article section-by-section. Before the full-rewrite loop it tries `_targeted_user_revision` (`nodes/writer_draft.py`): `_split_draft_sections` parses the previous `current_draft.full_markdown` into `(article_title, [(section_title, body)], references_block)` (level-1 `#` title, level-2 `##` sections, trailing `## 参考文献` pulled out and kept verbatim), then `_detect_revision_scope` runs a small `llm_json` classifier (prompts `DRAFT_REVISE_SCOPE_{SYSTEM,USER}`) returning **per-section targets** `[{index, new_title, rewrite_body}]`: it maps the note to the section indices it targets ("第一段/开头/引言"→0, "结尾/结论"→last, by title/topic) **and** distinguishes a **章节标题改名** (`new_title` set, `rewrite_body=false` → rename only, body untouched) from a **正文修改** (`rewrite_body=true`). Title-only renames apply directly to the assembled `## {new_title}` heading **and are written back into `current_outline.sections[i].section_title`** (so the renamed title persists through confirm/finalize/history — this fixes "改章节标题不生效"). Body rewrites run `_stream_revise_section` (prompt `DRAFT_REVISE_SECTION_USER`, streamed as `draft_chunk`). **Only hit sections are touched; unhit sections are preserved byte-for-byte and are NOT re-emitted to the stream** — the frontend clears the right canvas to its initial placeholder on entering `revising` and shows only the live rewrite of the changed section(s), so the user sees「清空 → 流式重写 → 终稿」rather than the whole article rebuilding. The classifier returns `whole_article=true` (整体性意见 like 「整体语气更正式」/「全文都改」, **structure changes** 加章/删章/调整顺序, or a **文章总标题** change) → `_detect_revision_scope` yields `None` → `_targeted_user_revision` returns `None` → the node **falls back to the whole-article rewrite** (which still runs `_revise_outline_with_notes` for title/structure changes). Also falls back when there is no prior draft, an empty note, or scope-detection errors. Editor-review revisions (`accept_review`) keep the holistic rewrite. Reassembly preserves the article title + structure and re-appends the original references block; word count is computed on the body (excluding references). Frontend: `isRevising` in `AIWritingState` (`types/ai-writing.ts`) is set in `useAIWritingStream.ts`'s `intervention_submitted` (`ns === 'revising'`) and cleared on `draft_ready`; `AIWritingDraftPanel.tsx` shows the initial `DraftWaitingPlaceholder` while `isRevising && streamingDraftSections.length === 0`, then the streaming preview once chunks arrive. Tests: `tests/test_ai_writing_targeted_revision.py`.
+1 -1
View File
@@ -137,7 +137,7 @@ Custom agents are indexed in the `agents` database table. The `id` column is the
### AI Writing Intranet Retrieval
AI 写作的「知识库检索」会直连 `ai_writing.intranet_search_url` 内网 ES/知识库接口,并默认跳过代理环境变量和 HTTPS 证书校验,适配自签名或内部 CA 证书的内网服务。需要强校验时可在 `config.yaml` 里设置 `ai_writing.intranet_verify_ssl: true`。
AI 写作的「知识库检索」会直连 `ai_writing.intranet_search_url` 内网 ES/知识库接口,并默认跳过代理环境变量和 HTTPS 证书校验,适配自签名或内部 CA 证书的内网服务。需要强校验时可在 `config.yaml` 里设置 `ai_writing.intranet_verify_ssl: true`。通用检索里的非内置技能在 `ai_writing.researcher_skill_agent: true` 时会用真实 agent 运行时执行技能:以 `ai-writing-researcher` 身份读取该技能的 `SKILL.md`,并按技能要求通过 `bash` 运行脚本,弱模型也会在本轮任务里收到明确的读文件和脚本执行指令。
### IM Channels
@@ -311,8 +311,13 @@ async def run_skill_via_real_agent(
skill_dir = skill.get_container_path(base_path)
except Exception: # noqa: BLE001
skill_dir = ""
try:
skill_file_path = skill.get_container_file_path(base_path)
except Exception: # noqa: BLE001
skill_file_path = f"{skill_dir}/SKILL.md" if skill_dir else ""
skill_loc_hint = (
f"\n\n【技能目录(重要)】技能「{skill_name}」在容器内的目录是:{skill_dir}\n"
f"- 技能说明文件是:{skill_file_path}\n"
f"- 运行该技能自带的脚本时,**务必先 `cd {skill_dir}` 再执行**"
f"(例如 `cd {skill_dir} && python3 scripts/xxx.py ...`),或对脚本用绝对路径;\n"
"- **不要用相对路径**(如 `python3 scripts/xxx.py`),否则当前目录不对会报 No such file。"
@@ -326,6 +331,20 @@ async def run_skill_via_real_agent(
if keywords:
task_lines.append(f"可参考的检索方向:{', '.join(keywords[:8])}")
task_lines.append(f"目标素材数量约 {max_results} 条。")
if skill_file_path or skill_dir:
task_lines.extend([
"",
"【必须使用技能(弱模型也必须遵守)】",
f"- 当前指定技能:{skill_name}",
f"- SKILL.md 路径:{skill_file_path or '(见上方 <skill> 内容)'}",
f"- 技能目录:{skill_dir or '(未知)'}",
"- 你的下一步必须先调用 `read_file` 读取这个 SKILL.md(即使上方已有技能摘要,也要读取完整说明),"
"再按 SKILL.md 的脚本/接口流程获取真实数据。",
"- 如果 SKILL.md 提到 `scripts/*.py`,必须调用 `bash` 实际运行脚本;优先使用 "
f"`cd {skill_dir or '<技能目录>'} && python scripts/xxx.py ...` 或 "
f"`cd {skill_dir or '<技能目录>'} && python3 scripts/xxx.py ...`。",
"- 禁止第一轮就直接输出 JSON;没有执行过 read_file 和技能要求的脚本/工具前,不能输出 `materials`。",
])
# 弱模型强提示:明确要求「先动手执行技能脚本」,避免它直接吐空 JSON 不检索。
task_lines.append(
f"现在请**立刻开始执行检索**:按上面挂载技能的 SKILL.md,用 bash 运行它的脚本"
@@ -365,10 +384,13 @@ async def run_skill_via_real_agent(
# recursion_limit 计的是 graph 超级步(每个「模型→工具」回合≈2 步),不能直接等于 max_turns,
# 否则模型做了 N 个检索词后还没来得及写最终汇总就被 GraphRecursionError 截断(实测 10 个
# 检索词≈20 步 > 16,结果汇总丢失)。给到 ~2×turns + 余量,让模型正常情况下能写完汇总。
run_config: dict = {"recursion_limit": _REAL_AGENT_MAX_TURNS * 2 + 4}
run_context: dict = {}
run_config: dict = {
"recursion_limit": _REAL_AGENT_MAX_TURNS * 2 + 4,
"configurable": {"agent_id": RESEARCHER_AGENT_ID},
}
run_context: dict = {"agent_id": RESEARCHER_AGENT_ID, "agent_name": RESEARCHER_AGENT_ID}
if thread_id:
run_config["configurable"] = {"thread_id": thread_id}
run_config["configurable"]["thread_id"] = thread_id
run_context["thread_id"] = thread_id
if app_config is not None:
run_context["app_config"] = app_config