feat(roundtable): 多智能体会商 + 业务链条页脱敏

- 区域级脱敏 Hook useDesensitizeSubtree:对左右侧面板、顶部标题、
  业务链条选择器/列表弹窗/列表页/配置页/映射页整片脱敏(文本节点,
  跳过 input/textarea 等可编辑控件)
- 消息列表正文保持原文,仅智能体名称脱敏(senderName / 席位名 / 流程图节点)
- Step3 总结/报告/大屏、推荐弹窗等结果面板脱敏
- 沙箱产物预览不脱敏:ArtifactFileDetail 加 disableDesensitize,会商沙箱传入
- desensitize.ts 新增 desensitizeDeep 递归脱敏结构化数据

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-16 16:43:29 +08:00
1 parent 0e4caa5a66
commit 45049975c2
29 files changed
+853 -66

No files matched your search

+17 -3
View File
@@ -31,15 +31,23 @@ import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
export default function AgentChatPage() {
export default function AgentChatPage({
embeddedAgentId,
}: {
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
embeddedAgentId?: string;
} = {}) {
const routeBase = useRouteBase();
const { t } = useI18n();
const [showFollowups, setShowFollowups] = useState(false);
const router = useRouter();
const { agent_id } = useParams<{
const { agent_id: agentIdFromPath } = useParams<{
agent_id: string;
}>();
const agent_id = embeddedAgentId ?? agentIdFromPath;
const isEmbedded = !!embeddedAgentId;
// If ChatPage stashed an opening message for this agent, skip the landing
// screen on first render so the user doesn't see a flash of the welcome UI
@@ -208,7 +216,13 @@ export default function AgentChatPage() {
size="sm"
variant="secondary"
onClick={() => {
router.push(`${routeBase}/agents/${agent_id}/chats/new`);
// 嵌入态:保持停留在通用问答右侧,开同一智能体的新对话;
// 非嵌入态:走该智能体的独立新对话页(按当前路由 base)。
router.push(
isEmbedded
? `/page/workspace/chats/new?agent=${agent_id}`
: `${routeBase}/agents/${agent_id}/chats/new`,
);
}}
>
<PlusSquare /> {t.agents.newChat}
@@ -31,10 +31,13 @@ import {
type BusinessMapping,
} from "@/strategy-components/api/business-mapping";
import { cn } from "@/lib/utils";
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
export default function BusinessChainMappingPage() {
const { theme } = useTheme();
const isDark = theme === "dark";
// 整片脱敏:业务链条映射页展示文本(链条名/描述/席位名/标题)。输入框 Hook 自动跳过。
const dzRef = useDesensitizeSubtree<HTMLDivElement>();
const [rows, setRows] = useState<BusinessMapping[]>([]);
const [agents, setAgents] = useState<Agent[]>([]);
@@ -160,7 +163,7 @@ export default function BusinessChainMappingPage() {
];
return (
<div className={cn(shellClass, "[&_.t-input]:rounded-[5px] [&_.t-input__wrap]:rounded-[5px]")}>
<div ref={dzRef} className={cn(shellClass, "[&_.t-input]:rounded-[5px] [&_.t-input__wrap]:rounded-[5px]")}>
<div className="mx-auto flex min-h-full max-w-7xl flex-col gap-6 px-6 py-7 lg:px-8">
{/* ── 页头 banner ── */}
+12 -4
View File
@@ -34,7 +34,7 @@ import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-rout
import { useSidebar } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { PanelLeftOpen } from "lucide-react";
import { useAgentSelector } from "@/core/agents";
import { useAgentSelector, useAgentsPaginated } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
@@ -130,6 +130,10 @@ export default function ChatPage() {
const browserContextAutoSubmitRef = useRef<string | null>(null);
const { agents: selectorAgents, isLoading: selectorLoading } =
useAgentSelector();
// 已选 agent 的存在性校验需要更宽的来源:侧边栏可选未配置进管理弹窗的 agent
// (用户自己的智能体),这些也算有效,故把「我的 / 广场」一并纳入校验集(缓存共享)。
const { agents: mineAgents } = useAgentsPaginated({ scope: "mine", page: 1, pageSize: 20 });
const { agents: squareAgents } = useAgentsPaginated({ scope: "square", page: 1, pageSize: 20 });
const { selectedArtifact } = useArtifacts();
useSpecificChatMode();
@@ -142,7 +146,7 @@ export default function ChatPage() {
setHasStartedConversation(false);
setPromptPrefixOn(false);
// Pre-select the agent the user last started a conversation with so a
// fresh chat defaults to it. Validated against the selector list below.
// fresh chat defaults to it. Validated against the known agent list below.
setSelectedAgentId(getLastAgentId());
}
}, [isNewThread, threadId]);
@@ -152,10 +156,14 @@ export default function ChatPage() {
// dead agent. Wait for the list to load before deciding.
useEffect(() => {
if (selectorLoading || !selectedAgentId) return;
if (!selectorAgents.some((a) => a.id === selectedAgentId)) {
// 校验集 = 管理弹窗 ∪ 我的 ∪ 广场;任一存在即视为有效(含用户未配置进管理弹窗
// 的自有智能体)。三者都还没加载出来时不做清除,避免把恢复值误删。
const known = [...selectorAgents, ...mineAgents, ...squareAgents];
if (known.length === 0) return;
if (!known.some((a) => a.id === selectedAgentId)) {
setSelectedAgentId(null);
}
}, [selectedAgentId, selectorAgents, selectorLoading]);
}, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]);
const { showNotification } = useNotification();
+16 -1
View File
@@ -145,6 +145,21 @@ export function ChatRuntime({ children }: { children: ReactNode }) {
);
}
/**
* 通用问答新对话内容区:默认渲染 ChatPage;当 URL 带 ``?agent=<id>``(点击左栏
* 「常用智能体」时由侧边栏写入)时,**在右侧问答区原地嵌入**该智能体的对话页
* (AgentChatPage),路由仍停留在 /chats/new,不跳转出去、不更换左侧栏。
* key 绑定 location.key —— 每次点击(即便同一 agent)都重开一个干净的会话。
*/
function NewChatContent() {
const location = useLocation();
const agentId = new URLSearchParams(location.search).get('agent');
if (agentId) {
return <AgentChatPage key={`${agentId}:${location.key}`} embeddedAgentId={agentId} />;
}
return <ChatPage />;
}
function NewChatEntryRoute({
layoutProps,
}: {
@@ -163,7 +178,7 @@ function NewChatEntryRoute({
return (
<WorkspaceLayout {...layoutProps} defaultHistorySidebarOpen>
<ChatRuntime>
<ChatPage />
<NewChatContent />
</ChatRuntime>
</WorkspaceLayout>
);