diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx index a4cb7294..501f7831 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx @@ -31,6 +31,7 @@ import { } from "./services/core/auth"; import { AUTH_KEYS, normalizeAgentEngine } from "@shared/constants"; import type { QuickInitConfig } from "@shared/types/quickInit"; +import type { UpdateState } from "@shared/types/updateTypes"; import { t, getCurrentLang } from "./services/core/i18n"; import SetupWizard from "./components/setup/SetupWizard"; import LoginPage from "./components/setup/LoginPage"; @@ -43,7 +44,10 @@ import LogViewer from "./components/pages/LogViewer"; import PermissionsPage from "./components/pages/PermissionsPage"; import SessionsPage from "./components/pages/SessionsPage"; import AiChatPage from "./components/pages/AiChatPage"; +import AutomationPage from "./components/pages/AutomationPage"; +import KnowledgePage from "./components/pages/KnowledgePage"; import SkillsMarketplacePage from "./components/pages/SkillsMarketplacePage"; +import AgentMarketplacePage from "./components/pages/AgentMarketplacePage"; import McpMarketplacePage from "./components/pages/McpMarketplacePage"; import WorkflowMarketplacePage from "./components/pages/WorkflowMarketplacePage"; import MCPSettings from "./components/settings/MCPSettings"; @@ -52,6 +56,7 @@ import AppSidebar, { LegacyTabKey, } from "./components/layout/AppSidebar"; import type { WebviewHeaderActions } from "./components/pages/SessionsPage"; +import type { AiAgentLaunchContext } from "./services/core/aiChat"; import { createLogger } from "./services/utils/rendererLog"; import styles from "./styles/components/App.module.css"; import { lightTheme, darkTheme } from "./styles/theme"; @@ -99,7 +104,10 @@ type TabKey = | "client" | "sessions" | "legacySessions" + | "automation" + | "knowledge" | "skills" + | "agents" | "mcpLibrary" | "workflow" | "mcp" @@ -109,6 +117,11 @@ type TabKey = | "logs" | "about"; +/** macOS/Linux 无 download-progress 时,用本地模拟进度避免长期显示 0%。 */ +const UPDATE_SIMULATED_PROGRESS_CAP = 90; +const UPDATE_SIMULATED_PROGRESS_INTERVAL_MS = 500; +const UPDATE_SIMULATED_DURATION_MS = 45_000; + // 服务状态接口(与 ClientPage 共享) export interface ServiceItem { key: string; @@ -322,6 +335,8 @@ function App() { const [activeTab, setActiveTab] = useState("client"); const [activeNavKey, setActiveNavKey] = useState("overview"); const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false); + const [aiAgentLaunchContext, setAiAgentLaunchContext] = + useState(null); const [webviewActions, setWebviewActions] = useState(null); const [username, setUsername] = useState(""); @@ -335,6 +350,13 @@ function App() { const servicesPollTimer = useRef | null>(null); /** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */ const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0); + const [updateState, setUpdateState] = useState({ + status: "idle", + }); + const [sidebarSimulatedPercent, setSidebarSimulatedPercent] = useState(0); + const sidebarSimulatedIntervalRef = useRef | null>(null); const getStartupServiceKeys = useCallback(async (): Promise => { const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"]; if (!FEATURES.ENABLE_GUI_AGENT_SERVER) return keys; @@ -815,6 +837,55 @@ function App() { }; }, [isSetupComplete]); + // ============================================ + // 监听更新状态(侧栏品牌区展示) + // ============================================ + useEffect(() => { + const handler = (state: UpdateState) => { + if (state) setUpdateState(state); + }; + window.electronAPI?.on("update:status", handler as any); + window.electronAPI?.app?.getUpdateState?.()?.then((state) => { + if (state) setUpdateState(state); + }); + return () => { + window.electronAPI?.off("update:status", handler as any); + }; + }, []); + + useEffect(() => { + const isDownloading = updateState.status === "downloading"; + const hasRealProgress = updateState.progress != null; + + if (isDownloading && !hasRealProgress) { + setSidebarSimulatedPercent(0); + const increment = + (UPDATE_SIMULATED_PROGRESS_CAP / UPDATE_SIMULATED_DURATION_MS) * + UPDATE_SIMULATED_PROGRESS_INTERVAL_MS; + const id = setInterval(() => { + setSidebarSimulatedPercent((prev) => { + const next = prev + increment; + return next >= UPDATE_SIMULATED_PROGRESS_CAP + ? UPDATE_SIMULATED_PROGRESS_CAP + : next; + }); + }, UPDATE_SIMULATED_PROGRESS_INTERVAL_MS); + sidebarSimulatedIntervalRef.current = id; + return () => { + clearInterval(id); + sidebarSimulatedIntervalRef.current = null; + }; + } + + if (!isDownloading || hasRealProgress) { + if (sidebarSimulatedIntervalRef.current) { + clearInterval(sidebarSimulatedIntervalRef.current); + sidebarSimulatedIntervalRef.current = null; + } + setSidebarSimulatedPercent(0); + } + }, [updateState.status, updateState.progress]); + // ============================================ // 监听托盘/菜单事件 // ============================================ @@ -987,28 +1058,25 @@ function App() { } if (key === "newTask") { + setAiAgentLaunchContext(null); setSessionsAutoOpen(true); setActiveTab("sessions"); return; } if (key === "aiChat") { + setAiAgentLaunchContext(null); setActiveTab("sessions"); return; } - if (key === "skills") { - setActiveTab("skills"); + if (key === "automation") { + setActiveTab("automation"); return; } - if (key === "mcpLibrary") { - setActiveTab("mcpLibrary"); - return; - } - - if (key === "workflow") { - setActiveTab("workflow"); + if (key === "knowledge") { + setActiveTab("knowledge"); return; } @@ -1031,12 +1099,114 @@ function App() { const navByLegacyTab: Partial> = { client: "overview", sessions: "aiChat", - mcp: "mcpLibrary", + automation: "automation", + skills: "overview", + agents: "overview", + mcpLibrary: "overview", + workflow: "overview", + mcp: "systemSettings", settings: "systemSettings", + dependencies: "systemSettings", + permissions: "systemSettings", + logs: "systemSettings", + about: "systemSettings", }; setActiveNavKey(navByLegacyTab[key] || "systemSettings"); }, []); + const handleDownloadUpdate = useCallback(async () => { + if (updateState.canAutoUpdate === false) { + await window.electronAPI?.app?.openReleasesPage?.(); + return; + } + + try { + setUpdateState((prev) => ({ + ...prev, + status: "downloading", + progress: undefined, + })); + const res = await window.electronAPI?.app?.downloadUpdate?.(); + if (!res || !res.success) { + message.error(res?.error || t("Claw.About.downloadFailed")); + setUpdateState((prev) => ({ + ...prev, + status: "available", + })); + } + } catch { + message.error(t("Claw.About.downloadFailed")); + setUpdateState((prev) => ({ + ...prev, + status: "available", + })); + } + }, [updateState.canAutoUpdate]); + + const handleInstallUpdate = useCallback(async () => { + try { + const res = await window.electronAPI?.app?.installUpdate?.(); + if (res && !res.success) { + message.error(res.error || t("Claw.About.installFailed")); + } + } catch { + message.error(t("Claw.About.installFailed")); + } + }, []); + + const sidebarUpdateNotice = useMemo(() => { + if (updateState.status === "available") { + return { + tone: "available" as const, + label: + updateState.canAutoUpdate === false + ? t("Claw.App.UpdateTag.newVersion", { + version: updateState.version, + }) + : t("Claw.App.UpdateTag.download", { + version: updateState.version, + }), + onClick: handleDownloadUpdate, + }; + } + + if (updateState.status === "downloading") { + return { + tone: "downloading" as const, + label: t("Claw.App.UpdateTag.downloading", { + percent: Math.round( + updateState.progress?.percent ?? sidebarSimulatedPercent, + ), + }), + }; + } + + if (updateState.status === "downloaded") { + return { + tone: "install" as const, + label: t("Claw.About.installUpdate"), + onClick: handleInstallUpdate, + }; + } + + return undefined; + }, [ + handleDownloadUpdate, + handleInstallUpdate, + sidebarSimulatedPercent, + updateState.canAutoUpdate, + updateState.progress?.percent, + updateState.status, + updateState.version, + ]); + + const handleStartAgentChat = useCallback((context: AiAgentLaunchContext) => { + setAiAgentLaunchContext(context); + setSessionsAutoOpen(false); + setActiveNavKey("aiChat"); + setActiveTab("sessions"); + }, []); + // ============================================ // 平台检测 // ============================================ @@ -1157,6 +1327,7 @@ function App() { email={userEmail} onlineStatus={onlineStatus} isMacOS={isMacOS} + updateNotice={sidebarUpdateNotice} onNavSelect={handleNavSelect} onLegacySelect={handleLegacySelect} onLogout={handleSidebarLogout} @@ -1202,6 +1373,7 @@ function App() { {activeTab === "sessions" && ( setSessionsAutoOpen(false)} /> )} @@ -1211,7 +1383,12 @@ function App() { onWebviewChange={setWebviewActions} /> )} + {activeTab === "automation" && } + {activeTab === "knowledge" && } {activeTab === "skills" && } + {activeTab === "agents" && ( + + )} {activeTab === "mcpLibrary" && } {activeTab === "workflow" && } {activeTab === "mcp" && } diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx index 69d2f0a3..3c148dac 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx @@ -7,7 +7,6 @@ import { BuildOutlined, CommentOutlined, DatabaseOutlined, - DeploymentUnitOutlined, FileTextOutlined, FolderOutlined, InfoCircleOutlined, @@ -15,6 +14,7 @@ import { MenuFoldOutlined, PlusCircleOutlined, ProjectOutlined, + RobotOutlined, SafetyOutlined, SettingOutlined, TeamOutlined, @@ -32,16 +32,17 @@ export type NavKey = | "knowledge" | "document" | "dataFetch" - | "skills" - | "mcpLibrary" - | "workflow" - | "plugins" | "toolIntegration" | "systemSettings"; export type LegacyTabKey = | "client" | "sessions" + | "automation" + | "skills" + | "agents" + | "mcpLibrary" + | "workflow" | "mcp" | "settings" | "dependencies" @@ -62,12 +63,19 @@ interface LegacyItem { icon: React.ReactNode; } +interface UpdateNotice { + tone: "available" | "downloading" | "install"; + label: string; + onClick?: () => void; +} + interface AppSidebarProps { activeNavKey: NavKey; username: string; email?: string; onlineStatus: boolean | null; isMacOS: boolean; + updateNotice?: UpdateNotice; onNavSelect: (key: NavKey) => void; onLegacySelect: (key: LegacyTabKey) => void; onLogout: () => void; @@ -81,35 +89,13 @@ const workspaceItems: NavItem[] = [ icon: , badge: "3", }, - { - key: "automation", - label: "自动化", - icon: , - badge: "运行中", - }, + { key: "automation", label: "自动化", icon: }, { key: "aiChat", label: "AI 对话", icon: }, { key: "knowledge", label: "知识库", icon: }, { key: "document", label: "文档编辑", icon: }, { key: "dataFetch", label: "智能取数", icon: }, ]; -const marketItems: NavItem[] = [ - { key: "skills", label: "技能库", icon: , badge: "获取" }, - { key: "mcpLibrary", label: "MCP 库", icon: , badge: "获取" }, - { - key: "workflow", - label: "工作流", - icon: , - badge: "安装", - }, - { - key: "plugins", - label: "插件", - icon: , - badge: "安装", - }, -]; - const settingItems: NavItem[] = [ { key: "toolIntegration", label: "工具集成", icon: }, { key: "systemSettings", label: "系统设置", icon: }, @@ -119,6 +105,10 @@ function getLegacyItems(isMacOS: boolean): LegacyItem[] { const items: LegacyItem[] = [ { key: "client", label: "客户端", icon: }, { key: "sessions", label: "会话", icon: }, + { key: "skills", label: "技能库", icon: }, + { key: "agents", label: "智能体", icon: }, + { key: "mcpLibrary", label: "MCP 库", icon: }, + { key: "workflow", label: "工作流", icon: }, { key: "mcp", label: "MCP", icon: }, { key: "settings", label: "设置", icon: }, { key: "dependencies", label: "依赖", icon: }, @@ -176,6 +166,7 @@ export default function AppSidebar({ email, onlineStatus, isMacOS, + updateNotice, onNavSelect, onLegacySelect, onLogout, @@ -184,6 +175,11 @@ export default function AppSidebar({ const displayEmail = email || "xingyu@feitian.com"; const legacyItems = getLegacyItems(isMacOS); const online = onlineStatus !== false; + const updateNoticeToneClass = updateNotice + ? styles[ + `updateNotice${updateNotice.tone[0].toUpperCase()}${updateNotice.tone.slice(1)}` + ] + : ""; const historyMenu = (
@@ -209,6 +205,16 @@ export default function AppSidebar({
{APP_DISPLAY_NAME}
DIGITAL EMPLOYEE
+ {updateNotice && ( + + )}
-
-
扩展市场
- {marketItems.map((item) => - renderNavItem(item, activeNavKey, onNavSelect), - )} -
-
设置
{settingItems.map((item) => diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AgentMarketplacePage.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AgentMarketplacePage.tsx new file mode 100644 index 00000000..8d9f001f --- /dev/null +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AgentMarketplacePage.tsx @@ -0,0 +1,748 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Empty, Input, Modal, Spin, message } from "antd"; +import { + CheckOutlined, + CommentOutlined, + RobotOutlined, + SearchOutlined, +} from "@ant-design/icons"; +import { + collectAgent, + fetchPublishedAgentDetail, + fetchPublishedAgents, + fetchPublishedCategories, + PublishedAgent, + PublishedAgentDetail, + PublishedCategory, +} from "../../services/core/marketplace"; +import { + addAiAgentPublishedAgentComponent, + AiAgentContext, + AiAgentLaunchContext, + AiManualComponent, + fetchAiAgentComponents, + resolveAiAgentContext, +} from "../../services/core/aiChat"; +import styles from "../../styles/components/WorkflowMarketplacePage.module.css"; + +const PAGE_SIZE = 48; +const ALL_CATEGORY = ""; + +const FALLBACK_CATEGORIES = [ + { key: ALL_CATEGORY, label: "全部" }, + { key: "Agent", label: "智能体" }, + { key: "ChatBot", label: "对话助手" }, + { key: "Efficiency", label: "效率办公" }, + { key: "DataAnalysis", label: "数据分析" }, + { key: "Knowledge", label: "知识问答" }, +]; + +const ICON_COLORS = [ + "#006d68", + "#3b7cff", + "#8f62ff", + "#ff9f43", + "#19b87a", + "#d85d8f", +]; + +interface AgentMarketplacePageProps { + onStartChat?: (context: AiAgentLaunchContext) => void; +} + +function getAgentCategories(categories: PublishedCategory[]) { + const agentCategory = categories.find((item) => item.type === "Agent"); + return agentCategory?.children || []; +} + +function getAgentId(agent: PublishedAgent): number { + return agent.targetId || agent.id; +} + +function getAuthorName(agent: PublishedAgent): string { + const user = agent.publishUser; + return user?.name || user?.nickName || user?.username || "陇电数字员工"; +} + +function getInitial(name?: string): string { + const trimmed = (name || "").trim(); + if (!trimmed) return "智"; + return trimmed.slice(0, 1).toUpperCase(); +} + +function getStableSeed(agent: PublishedAgent): number { + const raw = agent.name || String(getAgentId(agent)); + return raw.split("").reduce((sum, char) => sum + char.charCodeAt(0), 0); +} + +function getIconColor(agent: PublishedAgent): string { + return ICON_COLORS[getStableSeed(agent) % ICON_COLORS.length]; +} + +function formatMetric(value?: number): string { + if (!value) return "0"; + if (value >= 10000) { + return `${Number((value / 10000).toFixed(value >= 100000 ? 0 : 1))}W`; + } + if (value >= 1000) { + return `${Number((value / 1000).toFixed(value >= 10000 ? 0 : 1))}K`; + } + return String(value); +} + +function getUsageCount(agent: PublishedAgent): number { + const statistics = agent.statistics || {}; + return ( + Number(statistics.conversationCount) || + Number(statistics.userCount) || + Number(statistics.usedCount) || + Number(statistics.collectCount) || + Number(statistics.viewCount) || + 0 + ); +} + +function getRating(agent: PublishedAgent): string { + return (4.5 + (getStableSeed(agent) % 5) / 10).toFixed(1); +} + +function formatDateTime(value?: string): string { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function AgentIcon({ agent }: { agent: PublishedAgent }) { + if (agent.icon) { + return ; + } + return ( + {getInitial(agent.name)} + ); +} + +function getGuideText(item?: { + content?: string; + question?: string; + info?: string; +}): string { + return item?.content || item?.question || item?.info || ""; +} + +export default function AgentMarketplacePage({ + onStartChat, +}: AgentMarketplacePageProps) { + const [categories, setCategories] = useState([]); + const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY); + const [keywordInput, setKeywordInput] = useState(""); + const [keyword, setKeyword] = useState(""); + const [agents, setAgents] = useState([]); + const [total, setTotal] = useState(0); + const [loadingCategories, setLoadingCategories] = useState(false); + const [loadingAgents, setLoadingAgents] = useState(false); + const [joiningIds, setJoiningIds] = useState>(new Set()); + const [error, setError] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); + const [detailItem, setDetailItem] = useState(null); + const [detailData, setDetailData] = useState( + null, + ); + const [detailLoading, setDetailLoading] = useState(false); + const [detailError, setDetailError] = useState(null); + const [agentContext, setAgentContext] = useState(null); + const [joinedAgentMap, setJoinedAgentMap] = useState< + Map + >(new Map()); + const [loadingComponents, setLoadingComponents] = useState(false); + + const visibleCategories = useMemo(() => { + if (categories.length === 0) return FALLBACK_CATEGORIES; + return [ + { key: ALL_CATEGORY, label: "全部" }, + ...categories.map((item) => ({ key: item.key, label: item.label })), + ]; + }, [categories]); + + const loadCategories = useCallback(async () => { + setLoadingCategories(true); + try { + const result = await fetchPublishedCategories(); + setCategories(getAgentCategories(result)); + } catch (e) { + console.error("[AgentMarketplace] Failed to load categories:", e); + } finally { + setLoadingCategories(false); + } + }, []); + + const loadAgents = useCallback(async () => { + setLoadingAgents(true); + setError(null); + try { + const result = await fetchPublishedAgents({ + page: 1, + pageSize: PAGE_SIZE, + category: activeCategory, + kw: keyword, + }); + setAgents(result.records || []); + setTotal(result.total || 0); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : "智能体加载失败"; + setError(errorMessage); + setAgents([]); + setTotal(0); + } finally { + setLoadingAgents(false); + } + }, [activeCategory, keyword]); + + const loadAgentComponents = useCallback(async () => { + setLoadingComponents(true); + try { + const context = await resolveAiAgentContext(); + setAgentContext(context); + const components = await fetchAiAgentComponents(context.agentId); + const agentMap = new Map(); + components + .filter((item) => String(item.type || "").toLowerCase() === "agent") + .forEach((item) => { + const targetId = Number(item.targetId || 0); + if (targetId) agentMap.set(targetId, item); + }); + setJoinedAgentMap(agentMap); + } catch (e) { + console.error("[AgentMarketplace] Failed to load agent components:", e); + setAgentContext(null); + setJoinedAgentMap(new Map()); + } finally { + setLoadingComponents(false); + } + }, []); + + useEffect(() => { + loadCategories(); + }, [loadCategories]); + + useEffect(() => { + loadAgentComponents(); + }, [loadAgentComponents]); + + useEffect(() => { + loadAgents(); + }, [loadAgents]); + + const handleSearch = useCallback(() => { + setKeyword(keywordInput.trim()); + }, [keywordInput]); + + const markAgentJoined = useCallback((agentId: number) => { + setAgents((prev) => + prev.map((item) => + getAgentId(item) === agentId ? { ...item, collect: true } : item, + ), + ); + setDetailData((prev) => + prev && getAgentId(prev) === agentId ? { ...prev, collect: true } : prev, + ); + setDetailItem((prev) => + prev && getAgentId(prev) === agentId ? { ...prev, collect: true } : prev, + ); + }, []); + + const loadAgentDetail = useCallback(async (agent: PublishedAgent) => { + const agentId = getAgentId(agent); + const detail = await fetchPublishedAgentDetail(agentId); + return { + ...agent, + ...detail, + icon: detail.icon || agent.icon, + targetId: agentId, + collect: detail.collect || agent.collect, + }; + }, []); + + const openDetail = useCallback( + async (agent: PublishedAgent) => { + setDetailOpen(true); + setDetailItem(agent); + setDetailData(null); + setDetailError(null); + setDetailLoading(true); + + try { + const detail = await loadAgentDetail(agent); + setDetailData(detail); + } catch (e) { + console.error("[AgentMarketplace] Failed to load detail:", e); + setDetailError(e instanceof Error ? e.message : "智能体详情加载失败"); + } finally { + setDetailLoading(false); + } + }, + [loadAgentDetail], + ); + + const closeDetail = useCallback(() => { + setDetailOpen(false); + setDetailItem(null); + setDetailData(null); + setDetailError(null); + }, []); + + const handleJoin = useCallback( + async (agent: PublishedAgent) => { + const publishedAgentId = getAgentId(agent); + if (joinedAgentMap.has(publishedAgentId)) return; + + setJoiningIds((prev) => new Set(prev).add(publishedAgentId)); + try { + const context = agentContext || (await resolveAiAgentContext()); + if (!agentContext) setAgentContext(context); + if (context.agentId === publishedAgentId) { + throw new Error("不能将当前智能体加入自身对话组件"); + } + + const detail = + detailData && getAgentId(detailData) === publishedAgentId + ? detailData + : await loadAgentDetail(agent); + const componentId = await addAiAgentPublishedAgentComponent({ + agentId: context.agentId, + targetAgentId: publishedAgentId, + }); + const component: AiManualComponent = { + id: componentId, + name: detail.name || "未命名智能体", + description: detail.description, + icon: detail.icon, + type: "Agent", + targetId: publishedAgentId, + defaultSelected: 0, + }; + setJoinedAgentMap((prev) => { + const next = new Map(prev); + next.set(publishedAgentId, component); + return next; + }); + markAgentJoined(publishedAgentId); + try { + await collectAgent(publishedAgentId); + } catch (collectError) { + console.warn( + "[AgentMarketplace] Agent joined but collect sync failed:", + collectError, + ); + } + message.success("智能体已加入 AI 对话可选组件"); + } catch (e) { + console.error("[AgentMarketplace] Failed to join agent:", e); + message.error(e instanceof Error ? e.message : "智能体加入失败"); + } finally { + setJoiningIds((prev) => { + const next = new Set(prev); + next.delete(publishedAgentId); + return next; + }); + } + }, + [ + agentContext, + detailData, + joinedAgentMap, + loadAgentDetail, + markAgentJoined, + ], + ); + + const handleStartChat = useCallback( + (agent: PublishedAgent) => { + const agentId = getAgentId(agent); + if (!agentId) { + message.error("智能体信息不完整,无法开始对话"); + return; + } + onStartChat?.({ + agentId, + name: agent.name, + icon: agent.icon, + description: agent.description, + source: "marketplace", + launchKey: Date.now(), + }); + }, + [onStartChat], + ); + + const detailDisplayItem = detailData || detailItem; + const detailAgentId = detailDisplayItem ? getAgentId(detailDisplayItem) : 0; + const detailJoined = detailAgentId + ? joinedAgentMap.has(detailAgentId) + : false; + const guideQuestions = + detailData?.guidQuestionDtos || + detailData?.openingGuidQuestions?.map((item) => ({ content: item })) || + []; + + return ( +
+
+
+ 扩展市场 > 智能体 +
+
+

智能体

+ {total} 个智能体 +
+
+ +
+ } + placeholder="搜索智能体、名称或描述..." + value={keywordInput} + onChange={(event) => setKeywordInput(event.target.value)} + onPressEnter={handleSearch} + /> + +
+ {loadingCategories ? ( + 分类加载中... + ) : ( + visibleCategories.map((category) => ( + + )) + )} +
+ +
+ {loadingAgents ? ( +
+ + 智能体加载中... +
+ ) : error ? ( +
+ + +
+ ) : agents.length === 0 ? ( +
+ +
+ ) : ( +
+ {agents.map((agent) => { + const agentId = getAgentId(agent); + const joined = joinedAgentMap.has(agentId); + const isSelf = agentContext?.agentId === agentId; + return ( +
openDetail(agent)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + openDetail(agent); + } + }} + > +
+
+ +
+
+

{agent.name}

+
+ {getAuthorName(agent)} +
+
+
+ +
+ + {agent.category || "智能体"} + + {joined && ( + 已加入 + )} +
+ +

+ {agent.description || "暂无智能体描述"} +

+ +
+
+ {formatMetric(getUsageCount(agent))} + 次对话 + + ★ {getRating(agent)} + +
+
+ + + +
+
+
+ ); + })} +
+ )} +
+
+ + + {detailLoading && !detailDisplayItem ? ( +
+ + 智能体详情加载中... +
+ ) : !detailDisplayItem ? ( + + ) : ( +
+
+
+ +
+
+
+

{detailDisplayItem.name || "未命名智能体"}

+ + {detailDisplayItem.category || "智能体"} + + {detailJoined && ( + 已加入 + )} +
+
+ {getAuthorName(detailDisplayItem)} + {(detailDisplayItem.modified || + detailDisplayItem.created) && ( + + {formatDateTime( + detailDisplayItem.modified || detailDisplayItem.created, + )} + + )} + + {formatMetric(getUsageCount(detailDisplayItem))} 次对话 + +
+
+
+ + +
+
+ + {detailError ? ( +
+ + +
+ ) : ( + <> + {detailLoading && ( +
+ + 正在刷新详情... +
+ )} +
+
+

智能体说明

+ 用于判断适用场景和调用方式 +
+

+ {detailDisplayItem.description || "暂无智能体描述"} +

+ {detailDisplayItem.remark && ( +

+ {detailDisplayItem.remark} +

+ )} +
+ +
+
+ + {formatMetric(getUsageCount(detailDisplayItem))} + + 对话次数 +
+
+ + {formatMetric( + Number(detailDisplayItem.statistics?.collectCount || 0), + )} + + 收藏 +
+
+ {guideQuestions.length} + 引导问题 +
+
+ {getRating(detailDisplayItem)} + 评分 +
+
+ +
+
+

开场与引导

+ 直接对话时会作为使用提示 +
+ {detailData?.openingChatMsg && ( +

+ {detailData.openingChatMsg} +

+ )} + {guideQuestions.length === 0 ? ( +
暂无引导问题
+ ) : ( +
+ {guideQuestions.slice(0, 6).map((item, index) => ( +
+
+ 问题 {index + 1} +
+ 对话 +

{getGuideText(item) || "暂无内容"}

+ - +
+ ))} +
+ )} +
+ +
+
+

如何使用

+

+ 点击“开始对话”会直接使用该发布智能体创建会话;点击“加入对话”会把它加入当前客户端智能体的可选组件,回到 + AI 对话后在输入区选择“智能体”,即可随消息调用。 +

+
+ {detailJoined && 已加入对话组件} +
+ + )} +
+ )} +
+
+ ); +} diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AiChatPage.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AiChatPage.tsx index 47940943..83bca98c 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AiChatPage.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AiChatPage.tsx @@ -18,7 +18,11 @@ import { message, } from "antd"; import { + ApiOutlined, + BookOutlined, + CheckOutlined, CopyOutlined, + DeploymentUnitOutlined, LoadingOutlined, PaperClipOutlined, PlusOutlined, @@ -31,6 +35,7 @@ import { } from "@ant-design/icons"; import { AiAgentContext, + AiAgentLaunchContext, AiAttachment, AiChatEvent, AiConversationInfo, @@ -42,28 +47,55 @@ import { AiSkillMention, AiStreamController, AiUploadFileInfo, + addAiAgentMcpComponent, createAiConversation, createLocalAssistantMessage, createLocalUserMessage, + enableAiAgentKnowledgeForChat, + fetchAiAgentComponents, fetchAiConversationDetail, fetchAiConversations, fetchAiMessages, fetchAiModelOptions, fetchAtSkills, + isAiKnowledgeChatSelectable, + isAiWorkflowChatSelectable, resolveAiAgentContext, sendAiMessageStream, stopAiConversation, updateAiConversationTopic, uploadAiAttachment, } from "../../services/core/aiChat"; +import { + fetchDeployedMcps, + fetchSpaces, +} from "../../services/core/marketplace"; +import type { + McpDeployedItem, + SpaceInfo, +} from "../../services/core/marketplace"; import styles from "../../styles/components/AiChatPage.module.css"; interface AiChatPageProps { autoNew?: boolean; + launchContext?: AiAgentLaunchContext | null; onAutoNewConsumed?: () => void; } const MESSAGE_PAGE_SIZE = 20; +const MCP_PAGE_SIZE = 100; + +interface AiMcpToolOption { + key: string; + mcpId: number; + mcpName: string; + toolName: string; + description?: string; + icon?: string; + spaceName?: string; + componentId?: number; + component?: AiManualComponent; +} function formatConversationTime(value?: string): string { if (!value) return ""; @@ -97,6 +129,96 @@ function isMcpComponent(item: AiManualComponent): boolean { return String(item.type || "").toLowerCase() === "mcp"; } +function isWorkflowComponent(item: AiManualComponent): boolean { + return isAiWorkflowChatSelectable(item); +} + +function isAgentComponent(item: AiManualComponent): boolean { + return String(item.type || "").toLowerCase() === "agent"; +} + +function isKnowledgeComponent(item: AiManualComponent): boolean { + return isAiKnowledgeChatSelectable(item); +} + +function safeParseJson(value: unknown, fallback: T): T { + if (!value) return fallback; + if (typeof value !== "string") return value as T; + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } +} + +function getSpaceName(space?: SpaceInfo): string { + return space?.name || space?.spaceName || "未知空间"; +} + +function getMcpToolKey(mcpId: number, toolName: string): string { + return `${mcpId}:${toolName}`; +} + +function getManualMcpToolName(component: AiManualComponent): string { + const bindConfig = safeParseJson>( + component.bindConfig, + {}, + ); + const bindToolName = + typeof bindConfig.toolName === "string" ? bindConfig.toolName : ""; + if (bindToolName) return bindToolName; + if (component.toolName) return component.toolName; + const name = component.name || ""; + if (name.includes("/")) return name.split("/").pop()?.trim() || name; + return name; +} + +function getManualMcpKey(component: AiManualComponent): string { + const toolName = getManualMcpToolName(component); + const targetId = Number(component.targetId || 0); + if (targetId && toolName) return getMcpToolKey(targetId, toolName); + return `manual:${component.id}`; +} + +function getManualMcpLabel(component: AiManualComponent): string { + return component.name || getManualMcpToolName(component) || "MCP"; +} + +function getManualWorkflowLabel(component: AiManualComponent): string { + return component.name || "工作流"; +} + +function getManualAgentLabel(component: AiManualComponent): string { + return component.name || "智能体"; +} + +function getManualKnowledgeLabel(component: AiManualComponent): string { + return component.name || "知识库"; +} + +function getMcpTools(item: McpDeployedItem): Array<{ + name: string; + description?: string; +}> { + const config = safeParseJson>( + item.deployedConfig || item.mcpConfig, + {}, + ); + const tools = Array.isArray(config.tools) ? config.tools : []; + const components = + tools.length === 0 && Array.isArray(config.components) + ? config.components + : []; + const rawTools = tools.length > 0 ? tools : components; + return rawTools + .map((tool: any) => ({ + name: String(tool.name || tool.toolName || "").trim(), + description: + typeof tool.description === "string" ? tool.description : undefined, + })) + .filter((tool) => !!tool.name); +} + function normalizeProcessingInfo(data: any): AiProcessingInfo { const result = data?.result || {}; return { @@ -245,6 +367,7 @@ function renderProcessing(processingList?: AiProcessingInfo[]) { export default function AiChatPage({ autoNew, + launchContext, onAutoNewConsumed, }: AiChatPageProps) { const [agentContext, setAgentContext] = useState(null); @@ -255,6 +378,9 @@ export default function AiChatPage({ >(null); const [conversationDetail, setConversationDetail] = useState(null); + const [agentManualComponents, setAgentManualComponents] = useState< + AiManualComponent[] + >([]); const [messages, setMessages] = useState([]); const [messageInput, setMessageInput] = useState(""); const [uploadedFiles, setUploadedFiles] = useState([]); @@ -271,6 +397,24 @@ export default function AiChatPage({ const [skillKeyword, setSkillKeyword] = useState(""); const [skillOptions, setSkillOptions] = useState([]); const [mcpPanelOpen, setMcpPanelOpen] = useState(false); + const [workflowPanelOpen, setWorkflowPanelOpen] = useState(false); + const [agentPanelOpen, setAgentPanelOpen] = useState(false); + const [knowledgePanelOpen, setKnowledgePanelOpen] = useState(false); + const [mcpToolOptions, setMcpToolOptions] = useState([]); + const [loadingMcpOptions, setLoadingMcpOptions] = useState(false); + const [loadingWorkflowComponents, setLoadingWorkflowComponents] = + useState(false); + const [connectingMcpKeys, setConnectingMcpKeys] = useState>( + () => new Set(), + ); + const [mcpLoadError, setMcpLoadError] = useState(null); + const [workflowLoadError, setWorkflowLoadError] = useState( + null, + ); + const [agentLoadError, setAgentLoadError] = useState(null); + const [knowledgeLoadError, setKnowledgeLoadError] = useState( + null, + ); const [loadingContext, setLoadingContext] = useState(true); const [loadingList, setLoadingList] = useState(false); const [loadingConversation, setLoadingConversation] = useState(false); @@ -292,11 +436,32 @@ export default function AiChatPage({ [conversationDetail, conversations, selectedConversationId], ); - const manualComponents = conversationDetail?.agent?.manualComponents || []; + const manualComponents = agentManualComponents; const mcpComponents = useMemo( () => manualComponents.filter(isMcpComponent), [manualComponents], ); + const workflowComponents = useMemo( + () => manualComponents.filter(isWorkflowComponent), + [manualComponents], + ); + const agentComponents = useMemo( + () => manualComponents.filter(isAgentComponent), + [manualComponents], + ); + const knowledgeComponents = useMemo( + () => manualComponents.filter(isKnowledgeComponent), + [manualComponents], + ); + + const mcpComponentMap = useMemo(() => { + const map = new Map(); + mcpComponents.forEach((component) => { + map.set(getManualMcpKey(component), component); + map.set(`component:${component.id}`, component); + }); + return map; + }, [mcpComponents]); const selectedComponentIds = useMemo( () => new Set(selectedComponents.map((item) => `${item.type}:${item.id}`)), @@ -308,11 +473,73 @@ export default function AiChatPage({ [selectedSkills], ); + const selectedMcpComponents = useMemo(() => { + return selectedComponents + .filter((item) => String(item.type).toLowerCase() === "mcp") + .map((item) => mcpComponentMap.get(`component:${item.id}`)) + .filter(Boolean) as AiManualComponent[]; + }, [mcpComponentMap, selectedComponents]); + + const workflowComponentMap = useMemo(() => { + const map = new Map(); + workflowComponents.forEach((component) => { + map.set(component.id, component); + }); + return map; + }, [workflowComponents]); + + const selectedWorkflowComponents = useMemo(() => { + return selectedComponents + .filter((item) => String(item.type).toLowerCase() === "workflow") + .map((item) => workflowComponentMap.get(item.id)) + .filter(Boolean) as AiManualComponent[]; + }, [selectedComponents, workflowComponentMap]); + + const agentComponentMap = useMemo(() => { + const map = new Map(); + agentComponents.forEach((component) => { + map.set(component.id, component); + }); + return map; + }, [agentComponents]); + + const selectedAgentComponents = useMemo(() => { + return selectedComponents + .filter((item) => String(item.type).toLowerCase() === "agent") + .map((item) => agentComponentMap.get(item.id)) + .filter(Boolean) as AiManualComponent[]; + }, [agentComponentMap, selectedComponents]); + + const knowledgeComponentMap = useMemo(() => { + const map = new Map(); + knowledgeComponents.forEach((component) => { + map.set(component.id, component); + if (component.targetId) map.set(component.targetId, component); + }); + return map; + }, [knowledgeComponents]); + + const selectedKnowledgeComponents = useMemo(() => { + return selectedComponents + .filter((item) => String(item.type).toLowerCase() === "knowledge") + .map((item) => knowledgeComponentMap.get(item.id)) + .filter(Boolean) as AiManualComponent[]; + }, [knowledgeComponentMap, selectedComponents]); + const activeModelName = useMemo(() => { const current = modelOptions.find((item) => item.id === selectedModelId); return current?.name || current?.model || "默认模型"; }, [modelOptions, selectedModelId]); + const activeChatAgentName = useMemo( + () => + launchContext?.name || + conversationDetail?.agent?.name || + selectedConversation?.agent?.name || + "陇电 AI 助手", + [conversationDetail, launchContext, selectedConversation], + ); + const scrollToBottom = useCallback((smooth = false) => { requestAnimationFrame(() => { const node = scrollRef.current; @@ -352,10 +579,14 @@ export default function AiChatPage({ setLoadingConversation(true); setSkillPanelOpen(false); setMcpPanelOpen(false); + setWorkflowPanelOpen(false); + setAgentPanelOpen(false); + setKnowledgePanelOpen(false); topicUpdatedRef.current = false; try { const detail = await fetchAiConversationDetail(conversationId); setConversationDetail(detail); + setAgentManualComponents(detail.agent?.manualComponents || []); const initialMessages = detail.messageList || []; setMessages(initialMessages); setHasMoreMessages(initialMessages.length > 0); @@ -396,14 +627,240 @@ export default function AiChatPage({ } }, [agentContext, openConversation]); + const loadMcpToolOptions = useCallback(async () => { + setLoadingMcpOptions(true); + setMcpLoadError(null); + try { + const spaces = await fetchSpaces().catch(() => [] as SpaceInfo[]); + const spaceMap = new Map(spaces.map((space) => [space.id, space])); + const pages = + spaces.length > 0 + ? await Promise.all( + spaces.map((space) => + fetchDeployedMcps({ + page: 1, + pageSize: MCP_PAGE_SIZE, + spaceId: space.id, + justReturnSpaceData: true, + }).catch(() => null), + ), + ) + : [ + await fetchDeployedMcps({ + page: 1, + pageSize: MCP_PAGE_SIZE, + justReturnSpaceData: true, + }).catch(() => null), + ]; + + const deployedItems = Array.from( + new Map( + pages + .flatMap((page) => page?.records || []) + .map((item) => [item.id, item]), + ).values(), + ); + + const manualByKey = new Map(); + mcpComponents.forEach((component) => { + manualByKey.set(getManualMcpKey(component), component); + }); + + const options: AiMcpToolOption[] = deployedItems.flatMap((item) => { + const tools = getMcpTools(item); + return tools.map((tool) => { + const key = getMcpToolKey(item.id, tool.name); + const component = manualByKey.get(key); + return { + key, + mcpId: item.id, + mcpName: item.name || "未命名 MCP", + toolName: tool.name, + description: tool.description || item.description, + icon: item.icon, + spaceName: item.spaceId + ? getSpaceName(spaceMap.get(Number(item.spaceId))) + : undefined, + componentId: component?.id, + component, + }; + }); + }); + + const optionKeys = new Set(options.map((item) => item.key)); + const manualOnlyOptions = mcpComponents + .map((component) => { + const key = getManualMcpKey(component); + if (optionKeys.has(key)) return null; + return { + key, + mcpId: Number(component.targetId || 0), + mcpName: getManualMcpLabel(component).split("/")[0] || "MCP", + toolName: getManualMcpToolName(component), + description: component.description, + icon: component.icon, + componentId: component.id, + component, + } as AiMcpToolOption; + }) + .filter(Boolean) as AiMcpToolOption[]; + + setMcpToolOptions([...manualOnlyOptions, ...options]); + } catch (error) { + setMcpLoadError( + error instanceof Error ? error.message : "MCP 列表加载失败", + ); + setMcpToolOptions( + mcpComponents.map((component) => ({ + key: getManualMcpKey(component), + mcpId: Number(component.targetId || 0), + mcpName: getManualMcpLabel(component).split("/")[0] || "MCP", + toolName: getManualMcpToolName(component), + description: component.description, + icon: component.icon, + componentId: component.id, + component, + })), + ); + } finally { + setLoadingMcpOptions(false); + } + }, [mcpComponents]); + + const refreshAgentComponents = useCallback(async () => { + if (!agentContext) return; + setLoadingWorkflowComponents(true); + setWorkflowLoadError(null); + setAgentLoadError(null); + setKnowledgeLoadError(null); + try { + const components = await fetchAiAgentComponents(agentContext.agentId); + const componentKeys = new Set( + components.map( + (component) => + `${String(component.type).toLowerCase()}:${component.id}`, + ), + ); + setAgentManualComponents(components); + setConversationDetail((prev) => { + if (!prev) return prev; + return { + ...prev, + agent: { + ...(prev.agent || {}), + manualComponents: components, + }, + }; + }); + setSelectedComponents((prev) => + prev.filter((component) => + componentKeys.has( + `${String(component.type).toLowerCase()}:${component.id}`, + ), + ), + ); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "组件列表加载失败"; + setWorkflowLoadError(errorMessage); + setAgentLoadError(errorMessage); + setKnowledgeLoadError(errorMessage); + } finally { + setLoadingWorkflowComponents(false); + } + }, [agentContext]); + + const refreshSelectedKnowledgeBeforeSend = useCallback( + async (componentsToSend: AiSelectedComponent[]) => { + const selectedKnowledge = componentsToSend.filter( + (component) => String(component.type).toLowerCase() === "knowledge", + ); + if (!selectedKnowledge.length || !agentContext) return componentsToSend; + + const fetchedComponents = await fetchAiAgentComponents( + agentContext.agentId, + ); + const nextComponents = [...fetchedComponents]; + const enabledKnowledge = new Map(); + + for (const selected of selectedKnowledge) { + const componentIndex = nextComponents.findIndex( + (component) => + String(component.type || "").toLowerCase() === "knowledge" && + (component.id === selected.id || + Number(component.targetId || 0) === selected.id), + ); + if (componentIndex < 0) { + throw new Error( + "已选知识库不在当前智能体组件中,请回到知识库页面重新点击加入对话后再发送。", + ); + } + + const component = nextComponents[componentIndex]; + const enabledComponent = isAiKnowledgeChatSelectable(component) + ? component + : await enableAiAgentKnowledgeForChat(component); + + nextComponents[componentIndex] = enabledComponent; + enabledKnowledge.set(selected.id, enabledComponent); + enabledKnowledge.set(enabledComponent.id, enabledComponent); + if (enabledComponent.targetId) { + enabledKnowledge.set(enabledComponent.targetId, enabledComponent); + } + } + + const nextSelectedComponents = componentsToSend.map((component) => { + if (String(component.type).toLowerCase() !== "knowledge") { + return component; + } + const enabledComponent = enabledKnowledge.get(component.id); + return enabledComponent + ? { + id: enabledComponent.id, + type: enabledComponent.type || "Knowledge", + } + : component; + }); + + setAgentManualComponents(nextComponents); + setConversationDetail((prev) => { + if (!prev) return prev; + return { + ...prev, + agent: { + ...(prev.agent || {}), + manualComponents: nextComponents, + }, + }; + }); + setSelectedComponents(nextSelectedComponents); + + return nextSelectedComponents; + }, + [agentContext], + ); + useEffect(() => { let disposed = false; (async () => { setLoadingContext(true); try { - const context = await resolveAiAgentContext(); + const baseContext = await resolveAiAgentContext(); + const context = launchContext?.agentId + ? { ...baseContext, agentId: launchContext.agentId } + : baseContext; if (disposed) return; + streamRef.current?.abort(); setAgentContext(context); + setSelectedConversationId(null); + setConversationDetail(null); + setAgentManualComponents([]); + setMessages([]); + setSelectedSkills([]); + setSelectedComponents([]); + setUploadedFiles([]); + setMessageInput(""); + topicUpdatedRef.current = false; const [models, list] = await Promise.all([ fetchAiModelOptions(context.agentId).catch(() => []), fetchAiConversations({ agentId: context.agentId, limit: 30 }), @@ -412,7 +869,14 @@ export default function AiChatPage({ setModelOptions(models); setSelectedModelId(models[0]?.id); setConversations(list); - if (list[0]) await openConversation(list[0].id); + if (launchContext?.source === "marketplace") { + const conversation = await createAiConversation(context.agentId); + if (disposed) return; + setConversations((prev) => [conversation, ...prev]); + await openConversation(conversation.id); + } else if (list[0]) { + await openConversation(list[0].id); + } } catch (error) { message.error( error instanceof Error ? error.message : "初始化 AI 对话失败", @@ -425,7 +889,12 @@ export default function AiChatPage({ disposed = true; streamRef.current?.abort(); }; - }, []); + }, [ + launchContext?.agentId, + launchContext?.launchKey, + launchContext?.source, + openConversation, + ]); useEffect(() => { if (!agentContext) return; @@ -462,6 +931,26 @@ export default function AiChatPage({ }; }, [skillKeyword, skillPanelOpen, skillTab]); + useEffect(() => { + if (!mcpPanelOpen) return; + loadMcpToolOptions(); + }, [loadMcpToolOptions, mcpPanelOpen]); + + useEffect(() => { + if (!workflowPanelOpen) return; + refreshAgentComponents(); + }, [refreshAgentComponents, workflowPanelOpen]); + + useEffect(() => { + if (!agentPanelOpen) return; + refreshAgentComponents(); + }, [agentPanelOpen, refreshAgentComponents]); + + useEffect(() => { + if (!knowledgePanelOpen) return; + refreshAgentComponents(); + }, [knowledgePanelOpen, refreshAgentComponents]); + useEffect(() => { scrollToBottom(false); }, [messages, scrollToBottom]); @@ -531,6 +1020,101 @@ export default function AiChatPage({ }); }; + const toggleMcpComponentById = (componentId: number) => { + setSelectedComponents((prev) => { + const key = `Mcp:${componentId}`; + if (prev.some((item) => `${item.type}:${item.id}` === key)) { + return prev.filter((item) => `${item.type}:${item.id}` !== key); + } + return [...prev, { id: componentId, type: "Mcp" }]; + }); + }; + + const setMcpConnecting = (key: string, connecting: boolean) => { + setConnectingMcpKeys((prev) => { + const next = new Set(prev); + if (connecting) { + next.add(key); + } else { + next.delete(key); + } + return next; + }); + }; + + const connectAndSelectMcpTool = async (option: AiMcpToolOption) => { + if (option.componentId) { + toggleMcpComponentById(option.componentId); + return; + } + if (!agentContext) { + message.error("当前客户端未绑定智能体"); + return; + } + if (!option.mcpId || !option.toolName) { + message.error("MCP 工具信息不完整"); + return; + } + + setMcpConnecting(option.key, true); + try { + const componentId = await addAiAgentMcpComponent({ + agentId: agentContext.agentId, + mcpId: option.mcpId, + toolName: option.toolName, + }); + const component: AiManualComponent = { + id: componentId, + name: `${option.mcpName}/${option.toolName}`, + description: option.description, + icon: option.icon, + type: "Mcp", + targetId: option.mcpId, + toolName: option.toolName, + bindConfig: { toolName: option.toolName }, + defaultSelected: 1, + }; + + setConversationDetail((prev) => { + if (!prev) return prev; + const existing = prev.agent?.manualComponents || []; + const nextManualComponents = existing.some( + (item) => item.id === componentId, + ) + ? existing + : [...existing, component]; + return { + ...prev, + agent: { + ...(prev.agent || {}), + manualComponents: nextManualComponents, + }, + }; + }); + setAgentManualComponents((prev) => { + if (prev.some((item) => item.id === componentId)) return prev; + return [...prev, component]; + }); + setMcpToolOptions((prev) => + prev.map((item) => + item.key === option.key ? { ...item, componentId, component } : item, + ), + ); + setSelectedComponents((prev) => { + const key = `Mcp:${componentId}`; + if (prev.some((item) => `${item.type}:${item.id}` === key)) { + return prev; + } + return [...prev, { id: componentId, type: "Mcp" }]; + }); + message.success("MCP 已连接并选中"); + } catch (error) { + message.error(error instanceof Error ? error.message : "MCP 连接失败"); + } finally { + setMcpConnecting(option.key, false); + } + }; + const updateTopicAfterFirstMessage = async ( conversationId: number, text: string, @@ -571,6 +1155,17 @@ export default function AiChatPage({ const detail = await ensureConversationForSend(); if (!detail || !agentContext) return; + let componentsForSend = selectedComponents; + try { + componentsForSend = + await refreshSelectedKnowledgeBeforeSend(selectedComponents); + } catch (error) { + message.error( + error instanceof Error ? error.message : "知识库组件状态校验失败", + ); + return; + } + const conversationId = detail.id; const attachments = uploadedFiles.map(toAttachment); const localUserMessage = createLocalUserMessage(text, attachments); @@ -599,7 +1194,7 @@ export default function AiChatPage({ message: text, attachments, debug: false, - selectedComponents, + selectedComponents: componentsForSend, sandboxId: agentContext.configId ? String(agentContext.configId) : undefined, @@ -706,31 +1301,275 @@ export default function AiChatPage({ const mcpPanel = (
- {mcpComponents.length === 0 ? ( - +
+ 可用 MCP + 连接后可随本次消息发送 +
+
+ {loadingMcpOptions ? ( +
+ + MCP 加载中... +
+ ) : mcpLoadError ? ( +
+ +
+ ) : mcpToolOptions.length === 0 ? ( +
+ +
) : ( - mcpComponents.map((component) => { - const active = selectedComponentIds.has( - `${component.type}:${component.id}`, - ); - return ( - - ); - }) +
+ {mcpToolOptions.map((option) => { + const componentId = option.componentId; + const active = + !!componentId && selectedComponentIds.has(`Mcp:${componentId}`); + const connecting = connectingMcpKeys.has(option.key); + const connected = !!componentId; + return ( + + ); + })} +
+ )} +
+ ); + + const workflowPanel = ( +
+
+
+ 可用工作流 + 选择后可随本次消息发送 +
+
+ {loadingWorkflowComponents ? ( +
+ + 工作流加载中... +
+ ) : workflowLoadError ? ( +
+ +
+ ) : workflowComponents.length === 0 ? ( +
+ +
+ ) : ( +
+ {workflowComponents.map((component) => { + const active = selectedComponentIds.has( + `${component.type}:${component.id}`, + ); + return ( + + ); + })} +
+ )} +
+ ); + + const agentPanel = ( +
+
+
+ 可用智能体 + 选择后可随本次消息作为协作智能体调用 +
+
+ {loadingWorkflowComponents ? ( +
+ + 智能体加载中... +
+ ) : agentLoadError ? ( +
+ +
+ ) : agentComponents.length === 0 ? ( +
+ +
+ ) : ( +
+ {agentComponents.map((component) => { + const active = selectedComponentIds.has( + `${component.type}:${component.id}`, + ); + return ( + + ); + })} +
+ )} +
+ ); + + const knowledgePanel = ( +
+
+
+ 可用知识库 + 选择后会先检索命中的知识库内容 +
+
+ {loadingWorkflowComponents ? ( +
+ + 知识库加载中... +
+ ) : knowledgeLoadError ? ( +
+ +
+ ) : knowledgeComponents.length === 0 ? ( +
+ +
+ ) : ( +
+ {knowledgeComponents.map((component) => { + const active = selectedComponentIds.has( + `${component.type}:${component.id}`, + ); + return ( + + ); + })} +
)}
); @@ -776,7 +1615,7 @@ export default function AiChatPage({ return (
-

你好,我是陇电 AI 助手

+

你好,我是{activeChatAgentName}

我可以帮你处理工作任务、编写代码、分析数据、生成文档。

{suggestions.length > 0 && (
@@ -908,6 +1747,12 @@ export default function AiChatPage({
陇电数字员工 > AI 对话 + {launchContext?.name && ( + <> + > + {launchContext.name} + + )}
@@ -941,7 +1786,12 @@ export default function AiChatPage({