完善客户端市场与对话能力
This commit is contained in:
@@ -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<TabKey>("client");
|
||||
const [activeNavKey, setActiveNavKey] = useState<NavKey>("overview");
|
||||
const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false);
|
||||
const [aiAgentLaunchContext, setAiAgentLaunchContext] =
|
||||
useState<AiAgentLaunchContext | null>(null);
|
||||
const [webviewActions, setWebviewActions] =
|
||||
useState<WebviewHeaderActions | null>(null);
|
||||
const [username, setUsername] = useState<string>("");
|
||||
@@ -335,6 +350,13 @@ function App() {
|
||||
const servicesPollTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
/** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */
|
||||
const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0);
|
||||
const [updateState, setUpdateState] = useState<UpdateState>({
|
||||
status: "idle",
|
||||
});
|
||||
const [sidebarSimulatedPercent, setSidebarSimulatedPercent] = useState(0);
|
||||
const sidebarSimulatedIntervalRef = useRef<ReturnType<
|
||||
typeof setInterval
|
||||
> | null>(null);
|
||||
const getStartupServiceKeys = useCallback(async (): Promise<string[]> => {
|
||||
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<Record<LegacyTabKey, NavKey>> = {
|
||||
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" && (
|
||||
<AiChatPage
|
||||
autoNew={sessionsAutoOpen}
|
||||
launchContext={aiAgentLaunchContext}
|
||||
onAutoNewConsumed={() => setSessionsAutoOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -1211,7 +1383,12 @@ function App() {
|
||||
onWebviewChange={setWebviewActions}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "automation" && <AutomationPage />}
|
||||
{activeTab === "knowledge" && <KnowledgePage />}
|
||||
{activeTab === "skills" && <SkillsMarketplacePage />}
|
||||
{activeTab === "agents" && (
|
||||
<AgentMarketplacePage onStartChat={handleStartAgentChat} />
|
||||
)}
|
||||
{activeTab === "mcpLibrary" && <McpMarketplacePage />}
|
||||
{activeTab === "workflow" && <WorkflowMarketplacePage />}
|
||||
{activeTab === "mcp" && <MCPSettings />}
|
||||
|
||||
@@ -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: <PlusCircleOutlined />,
|
||||
badge: "3",
|
||||
},
|
||||
{
|
||||
key: "automation",
|
||||
label: "自动化",
|
||||
icon: <ThunderboltOutlined />,
|
||||
badge: "运行中",
|
||||
},
|
||||
{ key: "automation", label: "自动化", icon: <ThunderboltOutlined /> },
|
||||
{ key: "aiChat", label: "AI 对话", icon: <CommentOutlined /> },
|
||||
{ key: "knowledge", label: "知识库", icon: <BookOutlined /> },
|
||||
{ key: "document", label: "文档编辑", icon: <FileTextOutlined /> },
|
||||
{ key: "dataFetch", label: "智能取数", icon: <DatabaseOutlined /> },
|
||||
];
|
||||
|
||||
const marketItems: NavItem[] = [
|
||||
{ key: "skills", label: "技能库", icon: <BuildOutlined />, badge: "获取" },
|
||||
{ key: "mcpLibrary", label: "MCP 库", icon: <ApiOutlined />, badge: "获取" },
|
||||
{
|
||||
key: "workflow",
|
||||
label: "工作流",
|
||||
icon: <ProjectOutlined />,
|
||||
badge: "安装",
|
||||
},
|
||||
{
|
||||
key: "plugins",
|
||||
label: "插件",
|
||||
icon: <DeploymentUnitOutlined />,
|
||||
badge: "安装",
|
||||
},
|
||||
];
|
||||
|
||||
const settingItems: NavItem[] = [
|
||||
{ key: "toolIntegration", label: "工具集成", icon: <ToolOutlined /> },
|
||||
{ key: "systemSettings", label: "系统设置", icon: <SettingOutlined /> },
|
||||
@@ -119,6 +105,10 @@ function getLegacyItems(isMacOS: boolean): LegacyItem[] {
|
||||
const items: LegacyItem[] = [
|
||||
{ key: "client", label: "客户端", icon: <AppstoreOutlined /> },
|
||||
{ key: "sessions", label: "会话", icon: <TeamOutlined /> },
|
||||
{ key: "skills", label: "技能库", icon: <BuildOutlined /> },
|
||||
{ key: "agents", label: "智能体", icon: <RobotOutlined /> },
|
||||
{ key: "mcpLibrary", label: "MCP 库", icon: <ApiOutlined /> },
|
||||
{ key: "workflow", label: "工作流", icon: <ProjectOutlined /> },
|
||||
{ key: "mcp", label: "MCP", icon: <ApiOutlined /> },
|
||||
{ key: "settings", label: "设置", icon: <SettingOutlined /> },
|
||||
{ key: "dependencies", label: "依赖", icon: <FolderOutlined /> },
|
||||
@@ -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 = (
|
||||
<div className={styles.historyMenu}>
|
||||
@@ -209,6 +205,16 @@ export default function AppSidebar({
|
||||
<div className={styles.brandText}>
|
||||
<div className={styles.brandName}>{APP_DISPLAY_NAME}</div>
|
||||
<div className={styles.brandSubTitle}>DIGITAL EMPLOYEE</div>
|
||||
{updateNotice && (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.updateNotice} ${updateNoticeToneClass}`}
|
||||
onClick={updateNotice.onClick}
|
||||
disabled={!updateNotice.onClick}
|
||||
>
|
||||
{updateNotice.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Popover
|
||||
content={historyMenu}
|
||||
@@ -234,13 +240,6 @@ export default function AppSidebar({
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className={styles.navSection}>
|
||||
<div className={styles.sectionTitle}>扩展市场</div>
|
||||
{marketItems.map((item) =>
|
||||
renderNavItem(item, activeNavKey, onNavSelect),
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className={styles.navSection}>
|
||||
<div className={styles.sectionTitle}>设置</div>
|
||||
{settingItems.map((item) =>
|
||||
|
||||
@@ -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 <img className={styles.workflowIconImage} src={agent.icon} alt="" />;
|
||||
}
|
||||
return (
|
||||
<span className={styles.workflowIconText}>{getInitial(agent.name)}</span>
|
||||
);
|
||||
}
|
||||
|
||||
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<PublishedCategory[]>([]);
|
||||
const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY);
|
||||
const [keywordInput, setKeywordInput] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [agents, setAgents] = useState<PublishedAgent[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loadingCategories, setLoadingCategories] = useState(false);
|
||||
const [loadingAgents, setLoadingAgents] = useState(false);
|
||||
const [joiningIds, setJoiningIds] = useState<Set<number>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailItem, setDetailItem] = useState<PublishedAgent | null>(null);
|
||||
const [detailData, setDetailData] = useState<PublishedAgentDetail | null>(
|
||||
null,
|
||||
);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState<string | null>(null);
|
||||
const [agentContext, setAgentContext] = useState<AiAgentContext | null>(null);
|
||||
const [joinedAgentMap, setJoinedAgentMap] = useState<
|
||||
Map<number, AiManualComponent>
|
||||
>(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<number, AiManualComponent>();
|
||||
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 (
|
||||
<div className={styles.page}>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.breadcrumb}>
|
||||
扩展市场 <span>></span> <strong>智能体</strong>
|
||||
</div>
|
||||
<div className={styles.titleRow}>
|
||||
<h1 className={styles.title}>智能体</h1>
|
||||
<span className={styles.totalText}>{total} 个智能体</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className={styles.content}>
|
||||
<Input
|
||||
className={styles.search}
|
||||
size="large"
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索智能体、名称或描述..."
|
||||
value={keywordInput}
|
||||
onChange={(event) => setKeywordInput(event.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
|
||||
<div className={styles.categoryBar}>
|
||||
{loadingCategories ? (
|
||||
<span className={styles.categoryLoading}>分类加载中...</span>
|
||||
) : (
|
||||
visibleCategories.map((category) => (
|
||||
<button
|
||||
key={category.key || "all"}
|
||||
type="button"
|
||||
className={`${styles.categoryChip} ${
|
||||
activeCategory === category.key
|
||||
? styles.categoryChipActive
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveCategory(category.key)}
|
||||
>
|
||||
{category.label}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className={styles.workflowArea}>
|
||||
{loadingAgents ? (
|
||||
<div className={styles.centerState}>
|
||||
<Spin />
|
||||
<span>智能体加载中...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className={styles.centerState}>
|
||||
<Empty description={error} />
|
||||
<Button type="primary" onClick={loadAgents}>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<div className={styles.centerState}>
|
||||
<Empty description="暂无智能体" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.grid}>
|
||||
{agents.map((agent) => {
|
||||
const agentId = getAgentId(agent);
|
||||
const joined = joinedAgentMap.has(agentId);
|
||||
const isSelf = agentContext?.agentId === agentId;
|
||||
return (
|
||||
<article
|
||||
className={styles.card}
|
||||
key={agentId}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => openDetail(agent)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openDetail(agent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.cardHeader}>
|
||||
<div
|
||||
className={styles.workflowIcon}
|
||||
style={{ backgroundColor: getIconColor(agent) }}
|
||||
>
|
||||
<AgentIcon agent={agent} />
|
||||
</div>
|
||||
<div className={styles.workflowMeta}>
|
||||
<h2 className={styles.workflowName}>{agent.name}</h2>
|
||||
<div className={styles.author}>
|
||||
{getAuthorName(agent)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.badgeRow}>
|
||||
<span className={styles.badge}>
|
||||
{agent.category || "智能体"}
|
||||
</span>
|
||||
{joined && (
|
||||
<span className={styles.usedBadge}>已加入</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className={styles.description}>
|
||||
{agent.description || "暂无智能体描述"}
|
||||
</p>
|
||||
|
||||
<div className={styles.cardFooter}>
|
||||
<div className={styles.metrics}>
|
||||
<span>{formatMetric(getUsageCount(agent))}</span>
|
||||
<span>次对话</span>
|
||||
<span className={styles.rating}>
|
||||
★ {getRating(agent)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.cardActions}>
|
||||
<Button
|
||||
className={styles.detailButton}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openDetail(agent);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.detailButton}
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CommentOutlined />}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleStartChat(agent);
|
||||
}}
|
||||
>
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
className={
|
||||
joined ? styles.usedButton : styles.useButton
|
||||
}
|
||||
type={joined ? "default" : "primary"}
|
||||
size="small"
|
||||
icon={joined ? <CheckOutlined /> : <RobotOutlined />}
|
||||
loading={joiningIds.has(agentId) || loadingComponents}
|
||||
disabled={joined || isSelf}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleJoin(agent);
|
||||
}}
|
||||
>
|
||||
{joined ? "已加入" : isSelf ? "当前" : "加入"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Modal
|
||||
className={styles.detailModal}
|
||||
width={900}
|
||||
open={detailOpen}
|
||||
title={null}
|
||||
footer={null}
|
||||
onCancel={closeDetail}
|
||||
destroyOnClose
|
||||
>
|
||||
{detailLoading && !detailDisplayItem ? (
|
||||
<div className={styles.detailLoading}>
|
||||
<Spin />
|
||||
<span>智能体详情加载中...</span>
|
||||
</div>
|
||||
) : !detailDisplayItem ? (
|
||||
<Empty description="暂无智能体详情" />
|
||||
) : (
|
||||
<div className={styles.detailContent}>
|
||||
<div className={styles.detailHero}>
|
||||
<div
|
||||
className={styles.detailIcon}
|
||||
style={{ backgroundColor: getIconColor(detailDisplayItem) }}
|
||||
>
|
||||
<AgentIcon agent={detailDisplayItem} />
|
||||
</div>
|
||||
<div className={styles.detailTitleBlock}>
|
||||
<div className={styles.detailTitleRow}>
|
||||
<h2>{detailDisplayItem.name || "未命名智能体"}</h2>
|
||||
<span className={styles.badge}>
|
||||
{detailDisplayItem.category || "智能体"}
|
||||
</span>
|
||||
{detailJoined && (
|
||||
<span className={styles.usedBadge}>已加入</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.detailMeta}>
|
||||
<span>{getAuthorName(detailDisplayItem)}</span>
|
||||
{(detailDisplayItem.modified ||
|
||||
detailDisplayItem.created) && (
|
||||
<span>
|
||||
{formatDateTime(
|
||||
detailDisplayItem.modified || detailDisplayItem.created,
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{formatMetric(getUsageCount(detailDisplayItem))} 次对话
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cardActions}>
|
||||
<Button
|
||||
className={styles.detailButton}
|
||||
type="text"
|
||||
icon={<CommentOutlined />}
|
||||
onClick={() => handleStartChat(detailDisplayItem)}
|
||||
>
|
||||
开始对话
|
||||
</Button>
|
||||
<Button
|
||||
className={
|
||||
detailJoined ? styles.usedButton : styles.useButton
|
||||
}
|
||||
type={detailJoined ? "default" : "primary"}
|
||||
icon={detailJoined ? <CheckOutlined /> : <RobotOutlined />}
|
||||
loading={joiningIds.has(detailAgentId)}
|
||||
disabled={
|
||||
detailJoined || agentContext?.agentId === detailAgentId
|
||||
}
|
||||
onClick={() => handleJoin(detailDisplayItem)}
|
||||
>
|
||||
{detailJoined
|
||||
? "已加入"
|
||||
: agentContext?.agentId === detailAgentId
|
||||
? "当前智能体"
|
||||
: "加入对话"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detailError ? (
|
||||
<div className={styles.detailError}>
|
||||
<Empty description={detailError} />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => detailItem && openDetail(detailItem)}
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{detailLoading && (
|
||||
<div className={styles.detailInlineLoading}>
|
||||
<Spin size="small" />
|
||||
<span>正在刷新详情...</span>
|
||||
</div>
|
||||
)}
|
||||
<section className={styles.detailSection}>
|
||||
<div className={styles.detailSectionHeader}>
|
||||
<h3>智能体说明</h3>
|
||||
<span>用于判断适用场景和调用方式</span>
|
||||
</div>
|
||||
<p className={styles.detailDescription}>
|
||||
{detailDisplayItem.description || "暂无智能体描述"}
|
||||
</p>
|
||||
{detailDisplayItem.remark && (
|
||||
<p className={styles.detailRemark}>
|
||||
{detailDisplayItem.remark}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className={styles.detailStats}>
|
||||
<div>
|
||||
<strong>
|
||||
{formatMetric(getUsageCount(detailDisplayItem))}
|
||||
</strong>
|
||||
<span>对话次数</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{formatMetric(
|
||||
Number(detailDisplayItem.statistics?.collectCount || 0),
|
||||
)}
|
||||
</strong>
|
||||
<span>收藏</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{guideQuestions.length}</strong>
|
||||
<span>引导问题</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{getRating(detailDisplayItem)}</strong>
|
||||
<span>评分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={styles.detailSection}>
|
||||
<div className={styles.detailSectionHeader}>
|
||||
<h3>开场与引导</h3>
|
||||
<span>直接对话时会作为使用提示</span>
|
||||
</div>
|
||||
{detailData?.openingChatMsg && (
|
||||
<p className={styles.detailRemark}>
|
||||
{detailData.openingChatMsg}
|
||||
</p>
|
||||
)}
|
||||
{guideQuestions.length === 0 ? (
|
||||
<div className={styles.emptyLine}>暂无引导问题</div>
|
||||
) : (
|
||||
<div className={styles.argTable}>
|
||||
{guideQuestions.slice(0, 6).map((item, index) => (
|
||||
<div
|
||||
className={styles.argRow}
|
||||
key={`${getGuideText(item)}-${index}`}
|
||||
>
|
||||
<div className={styles.argName}>
|
||||
<strong>问题 {index + 1}</strong>
|
||||
</div>
|
||||
<span>对话</span>
|
||||
<p>{getGuideText(item) || "暂无内容"}</p>
|
||||
<code>-</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className={styles.usageGuide}>
|
||||
<div>
|
||||
<h3>如何使用</h3>
|
||||
<p>
|
||||
点击“开始对话”会直接使用该发布智能体创建会话;点击“加入对话”会把它加入当前客户端智能体的可选组件,回到
|
||||
AI 对话后在输入区选择“智能体”,即可随消息调用。
|
||||
</p>
|
||||
</div>
|
||||
{detailJoined && <span>已加入对话组件</span>}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,18 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Empty, Input, Spin, message } from "antd";
|
||||
import { Button, Empty, Input, Modal, Spin, message } from "antd";
|
||||
import { ApiOutlined, CheckOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import type { McpServerEntry, McpServersConfig } from "@shared/types/electron";
|
||||
import {
|
||||
exportMcpServerConfig,
|
||||
fetchDeployedMcpDetail,
|
||||
fetchDeployedMcps,
|
||||
fetchSpaces,
|
||||
McpComponentDetail,
|
||||
McpDeployedItem,
|
||||
McpDeployedDetail,
|
||||
McpParamDetail,
|
||||
McpResourceDetail,
|
||||
McpRuntimeConfig,
|
||||
SpaceInfo,
|
||||
} from "../../services/core/marketplace";
|
||||
import styles from "../../styles/components/McpMarketplacePage.module.css";
|
||||
@@ -17,6 +23,31 @@ interface LocalMcpItem extends McpDeployedItem {
|
||||
spaceName?: string;
|
||||
}
|
||||
|
||||
interface LocalMcpDetail extends McpDeployedDetail {
|
||||
spaceName?: string;
|
||||
}
|
||||
|
||||
interface CapabilityDisplayItem {
|
||||
name?: string;
|
||||
uri?: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
inputArgs?: McpParamDetail[];
|
||||
outputArgs?: McpParamDetail[];
|
||||
inputSchema?: unknown;
|
||||
type?: string;
|
||||
targetId?: number;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
const INSTALL_TYPE_LABELS: Record<string, string> = {
|
||||
NPX: "NPX",
|
||||
UVX: "UVX",
|
||||
SSE: "SSE",
|
||||
STREAMABLE_HTTP: "Streamable HTTP",
|
||||
COMPONENT: "组件",
|
||||
};
|
||||
|
||||
function safeParseJson<T>(value: unknown, fallback: T): T {
|
||||
if (!value) return fallback;
|
||||
if (typeof value !== "string") return value as T;
|
||||
@@ -150,6 +181,166 @@ function getMatchedServerId(
|
||||
return servers[itemId] ? itemId : null;
|
||||
}
|
||||
|
||||
function getInstallTypeLabel(value: unknown): string {
|
||||
if (typeof value !== "string" || !value) return "自定义";
|
||||
return INSTALL_TYPE_LABELS[value] || value;
|
||||
}
|
||||
|
||||
function getDeployStatusLabel(value: unknown): string {
|
||||
if (typeof value !== "string" || !value) return "已部署";
|
||||
const labels: Record<string, string> = {
|
||||
Initialization: "初始化",
|
||||
Deploying: "部署中",
|
||||
Deployed: "已部署",
|
||||
DeployFailed: "部署失败",
|
||||
Stopped: "已停用",
|
||||
};
|
||||
return labels[value] || value;
|
||||
}
|
||||
|
||||
function formatDateTime(value: unknown): string {
|
||||
if (typeof value !== "string" || !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 getRuntimeConfig(
|
||||
item?: Pick<LocalMcpItem, "deployedConfig" | "mcpConfig"> | null,
|
||||
): McpRuntimeConfig {
|
||||
const deployedConfig = safeParseJson<McpRuntimeConfig | null>(
|
||||
item?.deployedConfig,
|
||||
null,
|
||||
);
|
||||
const mcpConfig = safeParseJson<McpRuntimeConfig | null>(
|
||||
item?.mcpConfig,
|
||||
null,
|
||||
);
|
||||
return deployedConfig || mcpConfig || {};
|
||||
}
|
||||
|
||||
function asCapabilityArray<T extends CapabilityDisplayItem>(
|
||||
value: unknown,
|
||||
): T[] {
|
||||
return Array.isArray(value) ? (value as T[]) : [];
|
||||
}
|
||||
|
||||
function getCapabilityCounts(item?: LocalMcpItem | null) {
|
||||
const config = getRuntimeConfig(item);
|
||||
return {
|
||||
tools: asCapabilityArray(config.tools).length,
|
||||
resources: asCapabilityArray(config.resources).length,
|
||||
prompts: asCapabilityArray(config.prompts).length,
|
||||
components: asCapabilityArray(config.components).length,
|
||||
};
|
||||
}
|
||||
|
||||
function getSchemaParams(schema: unknown): McpParamDetail[] {
|
||||
if (!schema || typeof schema !== "object") return [];
|
||||
const schemaObject = schema as {
|
||||
properties?: Record<string, Record<string, unknown>>;
|
||||
required?: string[];
|
||||
};
|
||||
const properties = schemaObject.properties;
|
||||
if (!properties || typeof properties !== "object") return [];
|
||||
|
||||
return Object.entries(properties).map(([name, config]) => ({
|
||||
key: name,
|
||||
name,
|
||||
displayName: String(config?.title || name),
|
||||
description:
|
||||
typeof config?.description === "string" ? config.description : "",
|
||||
dataType: typeof config?.type === "string" ? config.type : undefined,
|
||||
require: schemaObject.required?.includes(name),
|
||||
}));
|
||||
}
|
||||
|
||||
function getInputParams(item: CapabilityDisplayItem): McpParamDetail[] {
|
||||
if (Array.isArray(item.inputArgs) && item.inputArgs.length > 0) {
|
||||
return item.inputArgs;
|
||||
}
|
||||
return getSchemaParams(item.inputSchema);
|
||||
}
|
||||
|
||||
function getParamName(param: McpParamDetail): string {
|
||||
return (
|
||||
param.displayName ||
|
||||
param.name ||
|
||||
(param.key !== undefined ? String(param.key) : "") ||
|
||||
"未命名参数"
|
||||
);
|
||||
}
|
||||
|
||||
function renderParamList(params: McpParamDetail[]) {
|
||||
if (params.length === 0) return null;
|
||||
return (
|
||||
<div className={styles.paramList}>
|
||||
{params.slice(0, 8).map((param, index) => (
|
||||
<span
|
||||
className={styles.param}
|
||||
key={`${getParamName(param)}-${index}`}
|
||||
title={param.description}
|
||||
>
|
||||
{getParamName(param)}
|
||||
{param.dataType && <em>{param.dataType}</em>}
|
||||
{(param.require || param.required) && <strong>必填</strong>}
|
||||
</span>
|
||||
))}
|
||||
{params.length > 8 && (
|
||||
<span className={styles.param}>+{params.length - 8}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailCapabilitySection({
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
emptyText,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
items: CapabilityDisplayItem[];
|
||||
emptyText: string;
|
||||
}) {
|
||||
return (
|
||||
<section className={styles.detailSection}>
|
||||
<div className={styles.detailSectionHeader}>
|
||||
<h3>{title}</h3>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<div className={styles.emptyLine}>{emptyText}</div>
|
||||
) : (
|
||||
<div className={styles.capabilityList}>
|
||||
{items.map((item, index) => {
|
||||
const params = getInputParams(item);
|
||||
const name = item.name || item.uri || item.toolName || "未命名能力";
|
||||
return (
|
||||
<div className={styles.capabilityItem} key={`${name}-${index}`}>
|
||||
<div className={styles.capabilityTop}>
|
||||
<strong>{name}</strong>
|
||||
{item.mimeType && <span>{item.mimeType}</span>}
|
||||
{item.type && <span>{item.type}</span>}
|
||||
</div>
|
||||
{item.description && <p>{item.description}</p>}
|
||||
{renderParamList(params)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function McpMarketplacePage() {
|
||||
const [keywordInput, setKeywordInput] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
@@ -158,6 +349,11 @@ export default function McpMarketplacePage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [connectingIds, setConnectingIds] = useState<Set<number>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailItem, setDetailItem] = useState<LocalMcpItem | null>(null);
|
||||
const [detailData, setDetailData] = useState<LocalMcpDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState<string | null>(null);
|
||||
|
||||
const loadMcpConfigs = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -311,6 +507,45 @@ export default function McpMarketplacePage() {
|
||||
setKeyword(keywordInput.trim());
|
||||
}, [keywordInput]);
|
||||
|
||||
const openDetail = useCallback(async (item: LocalMcpItem) => {
|
||||
setDetailOpen(true);
|
||||
setDetailItem(item);
|
||||
setDetailData(null);
|
||||
setDetailError(null);
|
||||
setDetailLoading(true);
|
||||
|
||||
try {
|
||||
const detail = await fetchDeployedMcpDetail(item.id);
|
||||
if (!detail) {
|
||||
throw new Error("MCP 详情不存在");
|
||||
}
|
||||
setDetailData({
|
||||
...item,
|
||||
...detail,
|
||||
icon: detail.icon || item.icon,
|
||||
spaceName: item.spaceName,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[McpMarketplace] Failed to load MCP detail:", e);
|
||||
setDetailError(e instanceof Error ? e.message : "MCP 详情加载失败");
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const closeDetail = useCallback(() => {
|
||||
setDetailOpen(false);
|
||||
setDetailItem(null);
|
||||
setDetailData(null);
|
||||
setDetailError(null);
|
||||
}, []);
|
||||
|
||||
const detailDisplayItem = detailData || detailItem;
|
||||
const detailConfig = getRuntimeConfig(detailDisplayItem);
|
||||
const detailConnected = detailDisplayItem
|
||||
? !!getMatchedServerId(localConfig, detailDisplayItem)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<header className={styles.header}>
|
||||
@@ -358,8 +593,21 @@ export default function McpMarketplacePage() {
|
||||
<div className={styles.grid}>
|
||||
{filteredItems.map((item) => {
|
||||
const connected = !!getMatchedServerId(localConfig, item);
|
||||
const counts = getCapabilityCounts(item);
|
||||
return (
|
||||
<article className={styles.card} key={item.id}>
|
||||
<article
|
||||
className={styles.card}
|
||||
key={item.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => openDetail(item)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openDetail(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.cardHeader}>
|
||||
<div className={styles.mcpIcon}>
|
||||
<McpIcon item={item} />
|
||||
@@ -379,24 +627,41 @@ export default function McpMarketplacePage() {
|
||||
</p>
|
||||
<div className={styles.cardFooter}>
|
||||
<div className={styles.metrics}>
|
||||
<span>{item.deployStatus || "Deployed"}</span>
|
||||
<span>{getDeployStatusLabel(item.deployStatus)}</span>
|
||||
{item.serverName && <span>{item.serverName}</span>}
|
||||
{counts.tools > 0 && <span>{counts.tools} 个工具</span>}
|
||||
</div>
|
||||
<div className={styles.cardActions}>
|
||||
<Button
|
||||
className={styles.detailButton}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openDetail(item);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
className={
|
||||
connected
|
||||
? styles.connectedButton
|
||||
: styles.connectButton
|
||||
}
|
||||
type={connected ? "default" : "primary"}
|
||||
size="small"
|
||||
icon={connected ? <CheckOutlined /> : <ApiOutlined />}
|
||||
loading={connectingIds.has(item.id)}
|
||||
disabled={connected}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleConnect(item);
|
||||
}}
|
||||
>
|
||||
{connected ? "已连接" : "连接"}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className={
|
||||
connected
|
||||
? styles.connectedButton
|
||||
: styles.connectButton
|
||||
}
|
||||
type={connected ? "default" : "primary"}
|
||||
size="small"
|
||||
icon={connected ? <CheckOutlined /> : <ApiOutlined />}
|
||||
loading={connectingIds.has(item.id)}
|
||||
disabled={connected}
|
||||
onClick={() => handleConnect(item)}
|
||||
>
|
||||
{connected ? "已连接" : "连接"}
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@@ -405,6 +670,166 @@ export default function McpMarketplacePage() {
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Modal
|
||||
className={styles.detailModal}
|
||||
width={860}
|
||||
open={detailOpen}
|
||||
title={null}
|
||||
footer={null}
|
||||
onCancel={closeDetail}
|
||||
destroyOnClose
|
||||
>
|
||||
{detailLoading && !detailDisplayItem ? (
|
||||
<div className={styles.detailLoading}>
|
||||
<Spin />
|
||||
<span>MCP 详情加载中...</span>
|
||||
</div>
|
||||
) : !detailDisplayItem ? (
|
||||
<Empty description="暂无 MCP 详情" />
|
||||
) : (
|
||||
<div className={styles.detailContent}>
|
||||
<div className={styles.detailHero}>
|
||||
<div className={styles.detailIcon}>
|
||||
<McpIcon item={detailDisplayItem} />
|
||||
</div>
|
||||
<div className={styles.detailTitleBlock}>
|
||||
<div className={styles.detailTitleRow}>
|
||||
<h2>{detailDisplayItem.name || "未命名 MCP"}</h2>
|
||||
<span className={styles.badge}>自定义</span>
|
||||
</div>
|
||||
<div className={styles.detailMeta}>
|
||||
<span>{detailDisplayItem.spaceName || "未知空间"}</span>
|
||||
<span>{getCreatorName(detailDisplayItem)}</span>
|
||||
<span>
|
||||
{getInstallTypeLabel(detailDisplayItem.installType)}
|
||||
</span>
|
||||
<span>
|
||||
{getDeployStatusLabel(detailDisplayItem.deployStatus)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className={
|
||||
detailConnected
|
||||
? styles.connectedButton
|
||||
: styles.connectButton
|
||||
}
|
||||
type={detailConnected ? "default" : "primary"}
|
||||
icon={detailConnected ? <CheckOutlined /> : <ApiOutlined />}
|
||||
loading={connectingIds.has(detailDisplayItem.id)}
|
||||
disabled={detailConnected}
|
||||
onClick={() => handleConnect(detailDisplayItem)}
|
||||
>
|
||||
{detailConnected ? "已连接" : "连接"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{detailError ? (
|
||||
<div className={styles.detailError}>
|
||||
<Empty description={detailError} />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => detailItem && openDetail(detailItem)}
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{detailLoading && (
|
||||
<div className={styles.detailInlineLoading}>
|
||||
<Spin size="small" />
|
||||
<span>正在刷新详情...</span>
|
||||
</div>
|
||||
)}
|
||||
<p className={styles.detailDescription}>
|
||||
{detailDisplayItem.description || "暂无 MCP 服务描述"}
|
||||
</p>
|
||||
<div className={styles.detailStats}>
|
||||
<div>
|
||||
<strong>
|
||||
{asCapabilityArray(detailConfig.tools).length}
|
||||
</strong>
|
||||
<span>工具</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{asCapabilityArray(detailConfig.resources).length}
|
||||
</strong>
|
||||
<span>资源</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{asCapabilityArray(detailConfig.prompts).length}
|
||||
</strong>
|
||||
<span>提示词</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{asCapabilityArray(detailConfig.components).length}
|
||||
</strong>
|
||||
<span>组件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailInfoGrid}>
|
||||
{detailDisplayItem.serverName && (
|
||||
<div>
|
||||
<span>服务名称</span>
|
||||
<strong>{detailDisplayItem.serverName}</strong>
|
||||
</div>
|
||||
)}
|
||||
{detailDisplayItem.deployed && (
|
||||
<div>
|
||||
<span>部署时间</span>
|
||||
<strong>
|
||||
{formatDateTime(detailDisplayItem.deployed)}
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
{detailDisplayItem.modified && (
|
||||
<div>
|
||||
<span>更新时间</span>
|
||||
<strong>
|
||||
{formatDateTime(detailDisplayItem.modified)}
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DetailCapabilitySection
|
||||
title="工具"
|
||||
description="MCP 可被模型调用的工具能力"
|
||||
items={asCapabilityArray(detailConfig.tools)}
|
||||
emptyText="暂无工具信息"
|
||||
/>
|
||||
<DetailCapabilitySection
|
||||
title="资源"
|
||||
description="MCP 暴露的资源读取能力"
|
||||
items={asCapabilityArray<McpResourceDetail>(
|
||||
detailConfig.resources,
|
||||
)}
|
||||
emptyText="暂无资源信息"
|
||||
/>
|
||||
<DetailCapabilitySection
|
||||
title="提示词"
|
||||
description="MCP 暴露的提示词模板"
|
||||
items={asCapabilityArray(detailConfig.prompts)}
|
||||
emptyText="暂无提示词信息"
|
||||
/>
|
||||
<DetailCapabilitySection
|
||||
title="组件"
|
||||
description="组件型 MCP 绑定的本地能力"
|
||||
items={asCapabilityArray<McpComponentDetail>(
|
||||
detailConfig.components,
|
||||
)}
|
||||
emptyText="暂无组件信息"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Empty, Input, Spin, message } from "antd";
|
||||
import { Button, Empty, Input, Modal, Spin, message } from "antd";
|
||||
import {
|
||||
CheckOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
@@ -7,11 +7,23 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
collectWorkflow,
|
||||
fetchPublishedWorkflowDetail,
|
||||
fetchPublishedCategories,
|
||||
fetchPublishedWorkflows,
|
||||
PublishedCategory,
|
||||
PublishedWorkflowDetail,
|
||||
PublishedWorkflow,
|
||||
WorkflowArgDetail,
|
||||
} from "../../services/core/marketplace";
|
||||
import {
|
||||
addAiAgentWorkflowComponent,
|
||||
AiAgentContext,
|
||||
AiManualComponent,
|
||||
enableAiAgentWorkflowForChat,
|
||||
fetchAiAgentComponents,
|
||||
isAiWorkflowChatSelectable,
|
||||
resolveAiAgentContext,
|
||||
} from "../../services/core/aiChat";
|
||||
import styles from "../../styles/components/WorkflowMarketplacePage.module.css";
|
||||
|
||||
const PAGE_SIZE = 48;
|
||||
@@ -100,6 +112,92 @@ function WorkflowIcon({ workflow }: { workflow: PublishedWorkflow }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getWorkflowId(workflow: PublishedWorkflow): number {
|
||||
return workflow.targetId || workflow.id;
|
||||
}
|
||||
|
||||
function getWorkflowJoinState(
|
||||
component?: AiManualComponent,
|
||||
): "none" | "pending" | "ready" {
|
||||
if (!component) return "none";
|
||||
return isAiWorkflowChatSelectable(component) ? "ready" : "pending";
|
||||
}
|
||||
|
||||
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 getArgName(arg: WorkflowArgDetail): string {
|
||||
return (
|
||||
arg.displayName ||
|
||||
arg.name ||
|
||||
(arg.key !== undefined ? String(arg.key) : "") ||
|
||||
"未命名参数"
|
||||
);
|
||||
}
|
||||
|
||||
function getArgType(arg: WorkflowArgDetail): string {
|
||||
return arg.dataType || arg.inputType || "未指定";
|
||||
}
|
||||
|
||||
function ArgList({
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
emptyText,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
items?: WorkflowArgDetail[];
|
||||
emptyText: string;
|
||||
}) {
|
||||
const list = items || [];
|
||||
return (
|
||||
<section className={styles.detailSection}>
|
||||
<div className={styles.detailSectionHeader}>
|
||||
<h3>{title}</h3>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
{list.length === 0 ? (
|
||||
<div className={styles.emptyLine}>{emptyText}</div>
|
||||
) : (
|
||||
<div className={styles.argTable}>
|
||||
<div className={styles.argTableHead}>
|
||||
<span>参数</span>
|
||||
<span>类型</span>
|
||||
<span>说明</span>
|
||||
<span>默认值</span>
|
||||
</div>
|
||||
{list.map((arg, index) => (
|
||||
<div className={styles.argRow} key={`${getArgName(arg)}-${index}`}>
|
||||
<div className={styles.argName}>
|
||||
<strong>{getArgName(arg)}</strong>
|
||||
{(arg.require || arg.required) && <em>必填</em>}
|
||||
</div>
|
||||
<span>{getArgType(arg)}</span>
|
||||
<p>{arg.description || "暂无说明"}</p>
|
||||
<code>
|
||||
{arg.bindValue !== undefined && arg.bindValue !== null
|
||||
? String(arg.bindValue)
|
||||
: "-"}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkflowMarketplacePage() {
|
||||
const [categories, setCategories] = useState<PublishedCategory[]>([]);
|
||||
const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY);
|
||||
@@ -111,6 +209,18 @@ export default function WorkflowMarketplacePage() {
|
||||
const [loadingWorkflows, setLoadingWorkflows] = useState(false);
|
||||
const [usingIds, setUsingIds] = useState<Set<number>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailItem, setDetailItem] = useState<PublishedWorkflow | null>(null);
|
||||
const [detailData, setDetailData] = useState<PublishedWorkflowDetail | null>(
|
||||
null,
|
||||
);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState<string | null>(null);
|
||||
const [agentContext, setAgentContext] = useState<AiAgentContext | null>(null);
|
||||
const [joinedWorkflowMap, setJoinedWorkflowMap] = useState<
|
||||
Map<number, AiManualComponent>
|
||||
>(new Map());
|
||||
const [loadingComponents, setLoadingComponents] = useState(false);
|
||||
|
||||
const visibleCategories = useMemo(() => {
|
||||
if (categories.length === 0) return FALLBACK_CATEGORIES;
|
||||
@@ -158,6 +268,36 @@ export default function WorkflowMarketplacePage() {
|
||||
loadCategories();
|
||||
}, [loadCategories]);
|
||||
|
||||
const loadAgentComponents = useCallback(async () => {
|
||||
setLoadingComponents(true);
|
||||
try {
|
||||
const context = await resolveAiAgentContext();
|
||||
setAgentContext(context);
|
||||
const components = await fetchAiAgentComponents(context.agentId);
|
||||
const workflowMap = new Map<number, AiManualComponent>();
|
||||
components
|
||||
.filter((item) => String(item.type || "").toLowerCase() === "workflow")
|
||||
.forEach((item) => {
|
||||
const targetId = Number(item.targetId || 0);
|
||||
if (targetId) workflowMap.set(targetId, item);
|
||||
});
|
||||
setJoinedWorkflowMap(workflowMap);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"[WorkflowMarketplace] Failed to load agent components:",
|
||||
e,
|
||||
);
|
||||
setAgentContext(null);
|
||||
setJoinedWorkflowMap(new Map());
|
||||
} finally {
|
||||
setLoadingComponents(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAgentComponents();
|
||||
}, [loadAgentComponents]);
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkflows();
|
||||
}, [loadWorkflows]);
|
||||
@@ -166,32 +306,155 @@ export default function WorkflowMarketplacePage() {
|
||||
setKeyword(keywordInput.trim());
|
||||
}, [keywordInput]);
|
||||
|
||||
const handleUse = useCallback(async (workflow: PublishedWorkflow) => {
|
||||
if (workflow.collect) return;
|
||||
const markWorkflowJoined = useCallback((workflowId: number) => {
|
||||
setWorkflows((prev) =>
|
||||
prev.map((item) =>
|
||||
getWorkflowId(item) === workflowId ? { ...item, collect: true } : item,
|
||||
),
|
||||
);
|
||||
setDetailData((prev) =>
|
||||
prev && getWorkflowId(prev) === workflowId
|
||||
? { ...prev, collect: true }
|
||||
: prev,
|
||||
);
|
||||
setDetailItem((prev) =>
|
||||
prev && getWorkflowId(prev) === workflowId
|
||||
? { ...prev, collect: true }
|
||||
: prev,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const workflowId = workflow.targetId || workflow.id;
|
||||
setUsingIds((prev) => new Set(prev).add(workflowId));
|
||||
try {
|
||||
await collectWorkflow(workflowId);
|
||||
setWorkflows((prev) =>
|
||||
prev.map((item) =>
|
||||
(item.targetId || item.id) === workflowId
|
||||
? { ...item, collect: true }
|
||||
: item,
|
||||
),
|
||||
);
|
||||
message.success("已启用工作流");
|
||||
} catch (e) {
|
||||
console.error("[WorkflowMarketplace] Failed to use workflow:", e);
|
||||
} finally {
|
||||
setUsingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(workflowId);
|
||||
const addJoinedWorkflow = useCallback(
|
||||
(workflow: PublishedWorkflow, component: AiManualComponent) => {
|
||||
const workflowId = getWorkflowId(workflow);
|
||||
setJoinedWorkflowMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(workflowId, component);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
markWorkflowJoined(workflowId);
|
||||
},
|
||||
[markWorkflowJoined],
|
||||
);
|
||||
|
||||
const loadWorkflowDetail = useCallback(
|
||||
async (workflow: PublishedWorkflow) => {
|
||||
const workflowId = getWorkflowId(workflow);
|
||||
const detail = await fetchPublishedWorkflowDetail(workflowId);
|
||||
return {
|
||||
...workflow,
|
||||
...detail,
|
||||
icon: detail.icon || workflow.icon,
|
||||
targetId: workflowId,
|
||||
collect: detail.collect || workflow.collect,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const openDetail = useCallback(
|
||||
async (workflow: PublishedWorkflow) => {
|
||||
setDetailOpen(true);
|
||||
setDetailItem(workflow);
|
||||
setDetailData(null);
|
||||
setDetailError(null);
|
||||
setDetailLoading(true);
|
||||
|
||||
try {
|
||||
const detail = await loadWorkflowDetail(workflow);
|
||||
setDetailData(detail);
|
||||
} catch (e) {
|
||||
console.error("[WorkflowMarketplace] Failed to load detail:", e);
|
||||
setDetailError(e instanceof Error ? e.message : "工作流详情加载失败");
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
},
|
||||
[loadWorkflowDetail],
|
||||
);
|
||||
|
||||
const closeDetail = useCallback(() => {
|
||||
setDetailOpen(false);
|
||||
setDetailItem(null);
|
||||
setDetailData(null);
|
||||
setDetailError(null);
|
||||
}, []);
|
||||
|
||||
const handleUse = useCallback(
|
||||
async (workflow: PublishedWorkflow) => {
|
||||
const workflowId = getWorkflowId(workflow);
|
||||
const joinedComponent = joinedWorkflowMap.get(workflowId);
|
||||
if (getWorkflowJoinState(joinedComponent) === "ready") return;
|
||||
|
||||
setUsingIds((prev) => new Set(prev).add(workflowId));
|
||||
|
||||
try {
|
||||
const context = agentContext || (await resolveAiAgentContext());
|
||||
if (!agentContext) setAgentContext(context);
|
||||
const detail =
|
||||
detailData && getWorkflowId(detailData) === workflowId
|
||||
? detailData
|
||||
: await loadWorkflowDetail(workflow);
|
||||
let component = joinedComponent;
|
||||
if (!component) {
|
||||
const componentId = await addAiAgentWorkflowComponent({
|
||||
agentId: context.agentId,
|
||||
workflowId,
|
||||
});
|
||||
const components = await fetchAiAgentComponents(context.agentId);
|
||||
component =
|
||||
components.find((item) => item.id === componentId) ||
|
||||
({
|
||||
id: componentId,
|
||||
name: detail.name || "未命名工作流",
|
||||
description: detail.description,
|
||||
icon: detail.icon,
|
||||
type: "Workflow",
|
||||
targetId: workflowId,
|
||||
defaultSelected: 0,
|
||||
} satisfies AiManualComponent);
|
||||
}
|
||||
|
||||
const selectableComponent = isAiWorkflowChatSelectable(component)
|
||||
? component
|
||||
: await enableAiAgentWorkflowForChat(component);
|
||||
addJoinedWorkflow(detail, selectableComponent);
|
||||
try {
|
||||
await collectWorkflow(workflowId);
|
||||
} catch (collectError) {
|
||||
console.warn(
|
||||
"[WorkflowMarketplace] Workflow joined but collect sync failed:",
|
||||
collectError,
|
||||
);
|
||||
}
|
||||
message.success("工作流已加入 AI 对话可选组件");
|
||||
} catch (e) {
|
||||
console.error("[WorkflowMarketplace] Failed to join workflow:", e);
|
||||
message.error(e instanceof Error ? e.message : "工作流加入失败");
|
||||
} finally {
|
||||
setUsingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(workflowId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
addJoinedWorkflow,
|
||||
agentContext,
|
||||
detailData,
|
||||
joinedWorkflowMap,
|
||||
loadWorkflowDetail,
|
||||
],
|
||||
);
|
||||
|
||||
const detailDisplayItem = detailData || detailItem;
|
||||
const detailJoinState = detailDisplayItem
|
||||
? getWorkflowJoinState(
|
||||
joinedWorkflowMap.get(getWorkflowId(detailDisplayItem)),
|
||||
)
|
||||
: "none";
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<header className={styles.header}>
|
||||
@@ -257,10 +520,25 @@ export default function WorkflowMarketplacePage() {
|
||||
) : (
|
||||
<div className={styles.grid}>
|
||||
{workflows.map((workflow) => {
|
||||
const workflowId = workflow.targetId || workflow.id;
|
||||
const used = !!workflow.collect;
|
||||
const workflowId = getWorkflowId(workflow);
|
||||
const joinedComponent = joinedWorkflowMap.get(workflowId);
|
||||
const joinState = getWorkflowJoinState(joinedComponent);
|
||||
const joined = joinState === "ready";
|
||||
const pendingJoin = joinState === "pending";
|
||||
return (
|
||||
<article className={styles.card} key={workflowId}>
|
||||
<article
|
||||
className={styles.card}
|
||||
key={workflowId}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => openDetail(workflow)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openDetail(workflow);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.cardHeader}>
|
||||
<div
|
||||
className={styles.workflowIcon}
|
||||
@@ -280,6 +558,11 @@ export default function WorkflowMarketplacePage() {
|
||||
<span className={styles.badge}>
|
||||
{workflow.category || "工作流"}
|
||||
</span>
|
||||
{joinState !== "none" && (
|
||||
<span className={styles.usedBadge}>
|
||||
{joined ? "已加入" : "待启用"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className={styles.description}>
|
||||
@@ -294,17 +577,47 @@ export default function WorkflowMarketplacePage() {
|
||||
★ {getRating(workflow)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
className={used ? styles.usedButton : styles.useButton}
|
||||
type={used ? "default" : "primary"}
|
||||
size="small"
|
||||
icon={used ? <CheckOutlined /> : undefined}
|
||||
loading={usingIds.has(workflowId)}
|
||||
disabled={used}
|
||||
onClick={() => handleUse(workflow)}
|
||||
>
|
||||
{used ? "已使用" : "使用"}
|
||||
</Button>
|
||||
<div className={styles.cardActions}>
|
||||
<Button
|
||||
className={styles.detailButton}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openDetail(workflow);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
className={
|
||||
joined ? styles.usedButton : styles.useButton
|
||||
}
|
||||
type={joined ? "default" : "primary"}
|
||||
size="small"
|
||||
icon={
|
||||
joined ? (
|
||||
<CheckOutlined />
|
||||
) : (
|
||||
<DeploymentUnitOutlined />
|
||||
)
|
||||
}
|
||||
loading={
|
||||
usingIds.has(workflowId) || loadingComponents
|
||||
}
|
||||
disabled={joined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleUse(workflow);
|
||||
}}
|
||||
>
|
||||
{joined
|
||||
? "已加入"
|
||||
: pendingJoin
|
||||
? "启用对话"
|
||||
: "使用"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@@ -313,6 +626,169 @@ export default function WorkflowMarketplacePage() {
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Modal
|
||||
className={styles.detailModal}
|
||||
width={900}
|
||||
open={detailOpen}
|
||||
title={null}
|
||||
footer={null}
|
||||
onCancel={closeDetail}
|
||||
destroyOnClose
|
||||
>
|
||||
{detailLoading && !detailDisplayItem ? (
|
||||
<div className={styles.detailLoading}>
|
||||
<Spin />
|
||||
<span>工作流详情加载中...</span>
|
||||
</div>
|
||||
) : !detailDisplayItem ? (
|
||||
<Empty description="暂无工作流详情" />
|
||||
) : (
|
||||
<div className={styles.detailContent}>
|
||||
<div className={styles.detailHero}>
|
||||
<div
|
||||
className={styles.detailIcon}
|
||||
style={{ backgroundColor: getIconColor(detailDisplayItem) }}
|
||||
>
|
||||
<WorkflowIcon workflow={detailDisplayItem} />
|
||||
</div>
|
||||
<div className={styles.detailTitleBlock}>
|
||||
<div className={styles.detailTitleRow}>
|
||||
<h2>{detailDisplayItem.name || "未命名工作流"}</h2>
|
||||
<span className={styles.badge}>
|
||||
{detailDisplayItem.category || "工作流"}
|
||||
</span>
|
||||
{detailJoinState !== "none" && (
|
||||
<span className={styles.usedBadge}>
|
||||
{detailJoinState === "ready" ? "已加入" : "待启用"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.detailMeta}>
|
||||
<span>{getAuthorName(detailDisplayItem)}</span>
|
||||
{detailDisplayItem.created && (
|
||||
<span>{formatDateTime(detailDisplayItem.created)}</span>
|
||||
)}
|
||||
<span>
|
||||
{formatMetric(getUsageCount(detailDisplayItem))} 次使用
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className={
|
||||
detailJoinState === "ready"
|
||||
? styles.usedButton
|
||||
: styles.useButton
|
||||
}
|
||||
type={detailJoinState === "ready" ? "default" : "primary"}
|
||||
icon={
|
||||
detailJoinState === "ready" ? (
|
||||
<CheckOutlined />
|
||||
) : (
|
||||
<DeploymentUnitOutlined />
|
||||
)
|
||||
}
|
||||
loading={usingIds.has(getWorkflowId(detailDisplayItem))}
|
||||
disabled={detailJoinState === "ready"}
|
||||
onClick={() => handleUse(detailDisplayItem)}
|
||||
>
|
||||
{detailJoinState === "ready"
|
||||
? "已加入"
|
||||
: detailJoinState === "pending"
|
||||
? "启用对话"
|
||||
: "使用"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{detailError ? (
|
||||
<div className={styles.detailError}>
|
||||
<Empty description={detailError} />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => detailItem && openDetail(detailItem)}
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{detailLoading && (
|
||||
<div className={styles.detailInlineLoading}>
|
||||
<Spin size="small" />
|
||||
<span>正在刷新详情...</span>
|
||||
</div>
|
||||
)}
|
||||
<section className={styles.detailSection}>
|
||||
<div className={styles.detailSectionHeader}>
|
||||
<h3>工作流说明</h3>
|
||||
<span>用于判断适用场景和调用方式</span>
|
||||
</div>
|
||||
<p className={styles.detailDescription}>
|
||||
{detailDisplayItem.description || "暂无工作流描述"}
|
||||
</p>
|
||||
{detailDisplayItem.remark && (
|
||||
<p className={styles.detailRemark}>
|
||||
{detailDisplayItem.remark}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className={styles.detailStats}>
|
||||
<div>
|
||||
<strong>{(detailData?.inputArgs || []).length}</strong>
|
||||
<span>入参</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{(detailData?.outputArgs || []).length}</strong>
|
||||
<span>出参</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{formatMetric(getUsageCount(detailDisplayItem))}
|
||||
</strong>
|
||||
<span>使用次数</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{getRating(detailDisplayItem)}</strong>
|
||||
<span>评分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArgList
|
||||
title="入参配置"
|
||||
description="在 AI 对话中调用工作流时需要填写或绑定的参数"
|
||||
items={detailData?.inputArgs}
|
||||
emptyText="暂无入参配置"
|
||||
/>
|
||||
<ArgList
|
||||
title="出参配置"
|
||||
description="工作流执行完成后会返回的结果字段"
|
||||
items={detailData?.outputArgs}
|
||||
emptyText="暂无出参配置"
|
||||
/>
|
||||
|
||||
<section className={styles.usageGuide}>
|
||||
<div>
|
||||
<h3>如何使用</h3>
|
||||
<p>
|
||||
点击“使用”后,系统会将该工作流加入当前智能体的 AI
|
||||
对话可选组件。回到 AI
|
||||
对话后,在输入区选择“工作流”,即可随消息一起调用。
|
||||
</p>
|
||||
</div>
|
||||
{detailJoinState !== "none" && (
|
||||
<span>
|
||||
{detailJoinState === "ready"
|
||||
? "已加入对话组件"
|
||||
: "需要启用后才会出现在 AI 对话工作流列表"}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ body {
|
||||
.app-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -211,8 +212,10 @@ body {
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 20px 24px;
|
||||
background: var(--color-bg-layout);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -72,13 +72,29 @@ export interface AiSelectedComponent {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface AiComponentBindConfig {
|
||||
invokeType?: string;
|
||||
defaultSelected?: number;
|
||||
inputArgBindConfigs?: unknown[];
|
||||
argBindConfigs?: unknown[];
|
||||
outputArgBindConfigs?: unknown[];
|
||||
async?: number;
|
||||
asyncReplyContent?: string;
|
||||
directOutput?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AiManualComponent {
|
||||
id: number;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
targetId?: number;
|
||||
toolName?: string;
|
||||
bindConfig?: string | AiComponentBindConfig;
|
||||
defaultSelected?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AiSkillMention {
|
||||
@@ -215,6 +231,15 @@ export interface AiAgentContext {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface AiAgentLaunchContext {
|
||||
agentId: number;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
source?: "marketplace" | "default";
|
||||
launchKey?: number;
|
||||
}
|
||||
|
||||
export interface AiStreamController {
|
||||
abort: () => void;
|
||||
done: Promise<void>;
|
||||
@@ -270,6 +295,18 @@ function appendParams(url: string, params?: Record<string, unknown>): string {
|
||||
return query ? `${url}?${query}` : url;
|
||||
}
|
||||
|
||||
function parseBindConfig(
|
||||
bindConfig?: AiManualComponent["bindConfig"],
|
||||
): AiComponentBindConfig {
|
||||
if (!bindConfig) return {};
|
||||
if (typeof bindConfig !== "string") return { ...bindConfig };
|
||||
try {
|
||||
return JSON.parse(bindConfig) as AiComponentBindConfig;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function aiRequest<T>(
|
||||
path: string,
|
||||
options: AiRequestOptions = {},
|
||||
@@ -422,6 +459,172 @@ export async function fetchAiModelOptions(
|
||||
);
|
||||
}
|
||||
|
||||
export async function addAiAgentMcpComponent(params: {
|
||||
agentId: number;
|
||||
mcpId: number;
|
||||
toolName: string;
|
||||
}): Promise<number> {
|
||||
return addAiAgentComponent({
|
||||
agentId: params.agentId,
|
||||
type: "Mcp",
|
||||
targetId: params.mcpId,
|
||||
toolName: params.toolName,
|
||||
});
|
||||
}
|
||||
|
||||
export async function addAiAgentWorkflowComponent(params: {
|
||||
agentId: number;
|
||||
workflowId: number;
|
||||
}): Promise<number> {
|
||||
return addAiAgentComponent({
|
||||
agentId: params.agentId,
|
||||
type: "Workflow",
|
||||
targetId: params.workflowId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function addAiAgentKnowledgeComponent(params: {
|
||||
agentId: number;
|
||||
knowledgeId: number;
|
||||
}): Promise<number> {
|
||||
return addAiAgentComponent({
|
||||
agentId: params.agentId,
|
||||
type: "Knowledge",
|
||||
targetId: params.knowledgeId,
|
||||
});
|
||||
}
|
||||
|
||||
export function isAiKnowledgeChatSelectable(
|
||||
component: AiManualComponent,
|
||||
): boolean {
|
||||
if (String(component.type || "").toLowerCase() !== "knowledge") return false;
|
||||
const bindConfig = parseBindConfig(component.bindConfig);
|
||||
const invokeType = String(bindConfig.invokeType || "");
|
||||
const defaultSelected =
|
||||
component.defaultSelected ?? bindConfig.defaultSelected;
|
||||
return invokeType === "MANUAL" && Number(defaultSelected) === 1;
|
||||
}
|
||||
|
||||
export async function enableAiAgentKnowledgeForChat(
|
||||
component: AiManualComponent,
|
||||
): Promise<AiManualComponent> {
|
||||
const bindConfig = parseBindConfig(component.bindConfig);
|
||||
const nextBindConfig: AiComponentBindConfig = {
|
||||
...bindConfig,
|
||||
invokeType: "MANUAL",
|
||||
defaultSelected: 1,
|
||||
matchingDegree: bindConfig.matchingDegree ?? 0.5,
|
||||
maxRecallCount: bindConfig.maxRecallCount ?? 5,
|
||||
noneRecallReplyType: bindConfig.noneRecallReplyType || "DEFAULT",
|
||||
searchStrategy: bindConfig.searchStrategy || "MIXED",
|
||||
};
|
||||
|
||||
await aiRequest<null>("/api/agent/component/knowledge/update", {
|
||||
method: "POST",
|
||||
data: {
|
||||
id: component.id,
|
||||
targetId: component.targetId ?? null,
|
||||
name: component.name,
|
||||
icon: component.icon,
|
||||
description: component.description,
|
||||
bindConfig: nextBindConfig,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...component,
|
||||
bindConfig: nextBindConfig,
|
||||
defaultSelected: nextBindConfig.defaultSelected,
|
||||
};
|
||||
}
|
||||
|
||||
export async function addAiAgentPublishedAgentComponent(params: {
|
||||
agentId: number;
|
||||
targetAgentId: number;
|
||||
}): Promise<number> {
|
||||
if (params.agentId === params.targetAgentId) {
|
||||
throw new Error("不能将当前智能体加入自身对话组件");
|
||||
}
|
||||
return addAiAgentComponent({
|
||||
agentId: params.agentId,
|
||||
type: "Agent",
|
||||
targetId: params.targetAgentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function isAiWorkflowChatSelectable(
|
||||
component: AiManualComponent,
|
||||
): boolean {
|
||||
if (String(component.type || "").toLowerCase() !== "workflow") return false;
|
||||
const invokeType = String(
|
||||
parseBindConfig(component.bindConfig).invokeType || "",
|
||||
);
|
||||
return invokeType === "MANUAL" || invokeType === "MANUAL_ON_DEMAND";
|
||||
}
|
||||
|
||||
export async function enableAiAgentWorkflowForChat(
|
||||
component: AiManualComponent,
|
||||
): Promise<AiManualComponent> {
|
||||
const bindConfig = parseBindConfig(component.bindConfig);
|
||||
const inputArgBindConfigs =
|
||||
bindConfig.inputArgBindConfigs || bindConfig.argBindConfigs || [];
|
||||
const nextBindConfig: AiComponentBindConfig = {
|
||||
...bindConfig,
|
||||
inputArgBindConfigs,
|
||||
argBindConfigs: inputArgBindConfigs,
|
||||
outputArgBindConfigs: bindConfig.outputArgBindConfigs || [],
|
||||
invokeType: "MANUAL_ON_DEMAND",
|
||||
defaultSelected: bindConfig.defaultSelected ?? 0,
|
||||
async: bindConfig.async ?? 0,
|
||||
asyncReplyContent: bindConfig.asyncReplyContent || "",
|
||||
directOutput: bindConfig.directOutput ?? 0,
|
||||
};
|
||||
|
||||
await aiRequest<null>("/api/agent/component/workflow/update", {
|
||||
method: "POST",
|
||||
data: {
|
||||
id: component.id,
|
||||
targetId: component.targetId ?? null,
|
||||
name: component.name,
|
||||
icon: component.icon,
|
||||
description: component.description,
|
||||
bindConfig: nextBindConfig,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...component,
|
||||
bindConfig: nextBindConfig,
|
||||
defaultSelected: nextBindConfig.defaultSelected,
|
||||
};
|
||||
}
|
||||
|
||||
export async function addAiAgentComponent(params: {
|
||||
agentId: number;
|
||||
type: string;
|
||||
targetId: number | string;
|
||||
toolName?: string;
|
||||
}): Promise<number> {
|
||||
return aiRequest<number>("/api/agent/component/add", {
|
||||
method: "POST",
|
||||
data: {
|
||||
agentId: params.agentId,
|
||||
type: params.type,
|
||||
targetId: params.targetId,
|
||||
...(params.toolName ? { toolName: params.toolName } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAiAgentComponents(
|
||||
agentId: number,
|
||||
): Promise<AiManualComponent[]> {
|
||||
return aiRequest<AiManualComponent[]>(
|
||||
`/api/agent/component/list/${agentId}`,
|
||||
{ method: "GET", showError: false },
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAtSkills(params: {
|
||||
type: "all" | "recent" | "favorite";
|
||||
kw?: string;
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
import { message } from "antd";
|
||||
import {
|
||||
AUTH_KEYS,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
DEFAULT_SERVER_HOST,
|
||||
} from "@shared/constants";
|
||||
import { getDomainTokenKey } from "@shared/utils/domain";
|
||||
import { getCurrentAuth, normalizeServerHost } from "./auth";
|
||||
|
||||
const SUCCESS_CODE = "0000";
|
||||
const MAX_KNOWLEDGE_FILE_SIZE = 100 * 1024 * 1024;
|
||||
|
||||
export const KNOWLEDGE_UPLOAD_SUFFIX = [
|
||||
"doc",
|
||||
"docx",
|
||||
"pdf",
|
||||
"md",
|
||||
"json",
|
||||
"txt",
|
||||
"jpg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"svg",
|
||||
"heic",
|
||||
"mp4",
|
||||
"mkv",
|
||||
"mov",
|
||||
"webm",
|
||||
"mp3",
|
||||
"aac",
|
||||
"wav",
|
||||
"flac",
|
||||
"ogg",
|
||||
"opus",
|
||||
];
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string;
|
||||
message?: string;
|
||||
data: T;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
interface KnowledgeRequestOptions {
|
||||
method?: "GET" | "POST";
|
||||
data?: unknown;
|
||||
params?: Record<string, unknown>;
|
||||
headers?: Record<string, string>;
|
||||
showError?: boolean;
|
||||
timeout?: number;
|
||||
formData?: FormData;
|
||||
responseType?: "json" | "blob";
|
||||
}
|
||||
|
||||
export interface KnowledgePageResult<T> {
|
||||
records: T[];
|
||||
total: number;
|
||||
size?: number;
|
||||
current?: number;
|
||||
pages?: number;
|
||||
}
|
||||
|
||||
export interface KnowledgeComponent {
|
||||
id: number;
|
||||
targetId?: number;
|
||||
type?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
fileSize?: number;
|
||||
docCount?: number;
|
||||
spaceId?: number;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
creatorName?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KnowledgeInfo {
|
||||
id: number;
|
||||
spaceId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
dataType?: number;
|
||||
icon?: string;
|
||||
embeddingModelId?: number;
|
||||
chatModelId?: number;
|
||||
fileSize?: number;
|
||||
pubStatus?: string;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
creatorName?: string;
|
||||
workflowId?: string | number;
|
||||
workflowName?: string;
|
||||
workflowDescription?: string;
|
||||
workflowIcon?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KnowledgeModelOption {
|
||||
id: number;
|
||||
name?: string;
|
||||
model?: string;
|
||||
description?: string;
|
||||
enabled?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KnowledgeDocumentStatus {
|
||||
docStatus?: string;
|
||||
docStatusCode?: number;
|
||||
docStatusDesc?: string;
|
||||
docStatusReason?: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeDocumentInfo extends KnowledgeDocumentStatus {
|
||||
id: number;
|
||||
kbId: number;
|
||||
name: string;
|
||||
docUrl?: string;
|
||||
pubStatus?: string;
|
||||
hasQa?: boolean;
|
||||
hasEmbedding?: boolean;
|
||||
fileSize?: number;
|
||||
spaceId?: number;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
creatorName?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KnowledgeUploadFileInfo {
|
||||
key?: string;
|
||||
url: string;
|
||||
name: string;
|
||||
type?: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface KnowledgeRawSegmentInfo {
|
||||
id: number;
|
||||
docId: number;
|
||||
kbId?: number;
|
||||
rawTxt: string;
|
||||
sortIndex?: number;
|
||||
spaceId?: number;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KnowledgeQaInfo {
|
||||
id: number;
|
||||
kbId?: number;
|
||||
docId?: number;
|
||||
rawId?: number;
|
||||
question: string;
|
||||
answer: string;
|
||||
hasEmbedding?: boolean;
|
||||
spaceId?: number;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KnowledgeConfigSaveParams {
|
||||
id?: number;
|
||||
spaceId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
embeddingModelId: number;
|
||||
icon?: string;
|
||||
dataType?: number;
|
||||
workflowId?: number | string | null;
|
||||
}
|
||||
|
||||
function appendParams(url: string, params?: Record<string, unknown>): string {
|
||||
if (!params) return url;
|
||||
const target = new URL(url);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
target.searchParams.set(key, String(value));
|
||||
}
|
||||
});
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
async function getBaseUrl(): Promise<string> {
|
||||
const auth = await getCurrentAuth();
|
||||
if (auth.userInfo?.currentDomain) {
|
||||
return normalizeServerHost(auth.userInfo.currentDomain);
|
||||
}
|
||||
|
||||
const config = (await window.electronAPI?.settings.get("step1_config")) as {
|
||||
serverHost?: string;
|
||||
} | null;
|
||||
|
||||
return normalizeServerHost(config?.serverHost || DEFAULT_SERVER_HOST);
|
||||
}
|
||||
|
||||
async function getAuthToken(domain: string): Promise<string | null> {
|
||||
const oneShotToken = (await window.electronAPI?.settings.get(
|
||||
AUTH_KEYS.AUTH_TOKEN,
|
||||
)) as string | null;
|
||||
if (oneShotToken) return oneShotToken;
|
||||
|
||||
const domainTokenKey = getDomainTokenKey(domain);
|
||||
return (await window.electronAPI?.settings.get(domainTokenKey)) as
|
||||
| string
|
||||
| null;
|
||||
}
|
||||
|
||||
async function getAuthHeaders(domain: string): Promise<Record<string, string>> {
|
||||
const token = await getAuthToken(domain);
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
function normalizeAssetUrl(
|
||||
baseUrl: string,
|
||||
value?: string,
|
||||
): string | undefined {
|
||||
if (!value) return value;
|
||||
if (/^(https?:)?\/\//i.test(value) || value.startsWith("data:")) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return new URL(value, baseUrl).toString();
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
async function knowledgeRequest<T>(
|
||||
path: string,
|
||||
options: KnowledgeRequestOptions = {},
|
||||
): Promise<T> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const authHeaders = await getAuthHeaders(baseUrl);
|
||||
const finalUrl = appendParams(`${baseUrl}${path}`, options.params);
|
||||
const isFormData = !!options.formData;
|
||||
const response = await fetch(finalUrl, {
|
||||
method: options.method || "GET",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept:
|
||||
options.responseType === "blob"
|
||||
? "*/*"
|
||||
: "application/json, text/plain, */*",
|
||||
...(isFormData ? {} : { "Content-Type": "application/json" }),
|
||||
...authHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
signal: AbortSignal.timeout(options.timeout || DEFAULT_API_TIMEOUT),
|
||||
...(isFormData
|
||||
? { body: options.formData }
|
||||
: options.data !== undefined
|
||||
? { body: JSON.stringify(options.data) }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
if (options.showError !== false) message.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (options.responseType === "blob") {
|
||||
return (await response.blob()) as T;
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiResponse<T>;
|
||||
if (result.code !== SUCCESS_CODE) {
|
||||
const errorMessage = result.message || "请求失败";
|
||||
if (options.showError !== false) message.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
function normalizeKnowledgeComponent(
|
||||
baseUrl: string,
|
||||
item: KnowledgeComponent,
|
||||
): KnowledgeComponent {
|
||||
return {
|
||||
...item,
|
||||
icon: normalizeAssetUrl(baseUrl, item.icon),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeKnowledgeInfo(
|
||||
baseUrl: string,
|
||||
item: KnowledgeInfo,
|
||||
): KnowledgeInfo {
|
||||
return {
|
||||
...item,
|
||||
icon: normalizeAssetUrl(baseUrl, item.icon),
|
||||
};
|
||||
}
|
||||
|
||||
export function getKnowledgeId(item: KnowledgeComponent | KnowledgeInfo) {
|
||||
const targetId = (item as { targetId?: number | string }).targetId;
|
||||
return Number(targetId || item.id);
|
||||
}
|
||||
|
||||
export async function fetchKnowledgeComponents(
|
||||
spaceId: number,
|
||||
): Promise<KnowledgeComponent[]> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const list = await knowledgeRequest<KnowledgeComponent[]>(
|
||||
`/api/component/list/${spaceId}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
return (list || [])
|
||||
.filter((item) => String(item.type || "").toLowerCase() === "knowledge")
|
||||
.map((item) => normalizeKnowledgeComponent(baseUrl, item));
|
||||
}
|
||||
|
||||
export async function fetchEmbeddingModels(
|
||||
spaceId?: number,
|
||||
): Promise<KnowledgeModelOption[]> {
|
||||
return knowledgeRequest<KnowledgeModelOption[]>("/api/model/list", {
|
||||
method: "POST",
|
||||
data: {
|
||||
...(spaceId ? { spaceId } : {}),
|
||||
modelType: "Embeddings",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createKnowledgeConfig(
|
||||
params: KnowledgeConfigSaveParams,
|
||||
): Promise<number> {
|
||||
return knowledgeRequest<number>("/api/knowledge/config/add", {
|
||||
method: "POST",
|
||||
data: {
|
||||
spaceId: params.spaceId,
|
||||
name: params.name,
|
||||
description: params.description || "",
|
||||
dataType: params.dataType || 1,
|
||||
icon: params.icon || "",
|
||||
embeddingModelId: params.embeddingModelId,
|
||||
workflowId: params.workflowId ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeConfig(
|
||||
params: KnowledgeConfigSaveParams,
|
||||
): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/config/update", {
|
||||
method: "POST",
|
||||
data: {
|
||||
id: params.id,
|
||||
spaceId: params.spaceId,
|
||||
name: params.name,
|
||||
description: params.description || "",
|
||||
dataType: params.dataType || 1,
|
||||
icon: params.icon || "",
|
||||
embeddingModelId: params.embeddingModelId,
|
||||
workflowId: params.workflowId ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchKnowledgeDetail(
|
||||
knowledgeId: number,
|
||||
): Promise<KnowledgeInfo> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const detail = await knowledgeRequest<KnowledgeInfo>(
|
||||
"/api/knowledge/config/detailById",
|
||||
{
|
||||
method: "GET",
|
||||
params: { id: knowledgeId },
|
||||
},
|
||||
);
|
||||
return normalizeKnowledgeInfo(baseUrl, detail);
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeConfig(
|
||||
knowledgeId: number,
|
||||
): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/config/deleteById", {
|
||||
method: "GET",
|
||||
params: { id: knowledgeId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchKnowledgeDocuments(params: {
|
||||
kbId: number;
|
||||
name?: string;
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<KnowledgePageResult<KnowledgeDocumentInfo>> {
|
||||
return knowledgeRequest<KnowledgePageResult<KnowledgeDocumentInfo>>(
|
||||
"/api/knowledge/document/list",
|
||||
{
|
||||
method: "POST",
|
||||
data: {
|
||||
queryFilter: {
|
||||
kbId: params.kbId,
|
||||
name: params.name || undefined,
|
||||
},
|
||||
current: params.current || 1,
|
||||
pageSize: params.pageSize || 48,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function validateKnowledgeFile(file: File): string | null {
|
||||
if (!file.name.includes(".")) return "请上传正确的文件类型";
|
||||
const suffix = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
if (!KNOWLEDGE_UPLOAD_SUFFIX.includes(suffix)) {
|
||||
return "暂不支持该文件类型";
|
||||
}
|
||||
if (file.size > MAX_KNOWLEDGE_FILE_SIZE) {
|
||||
return "单个文件不能超过 100MB";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function uploadKnowledgeFile(
|
||||
file: File,
|
||||
): Promise<KnowledgeUploadFileInfo> {
|
||||
const validationError = validateKnowledgeFile(file);
|
||||
if (validationError) throw new Error(validationError);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const result = await knowledgeRequest<{
|
||||
key?: string;
|
||||
url?: string;
|
||||
fileName?: string;
|
||||
mimeType?: string;
|
||||
size?: number;
|
||||
}>("/api/file/upload", {
|
||||
method: "POST",
|
||||
formData,
|
||||
timeout: DEFAULT_API_TIMEOUT * 2,
|
||||
});
|
||||
|
||||
if (!result?.url) {
|
||||
throw new Error("上传成功但未返回文件地址");
|
||||
}
|
||||
|
||||
return {
|
||||
key: result.key || "",
|
||||
url: result.url,
|
||||
name: result.fileName || file.name,
|
||||
type: result.mimeType || file.type,
|
||||
size: result.size || file.size,
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadKnowledgeAsset(file: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const result = await knowledgeRequest<{
|
||||
url?: string;
|
||||
}>("/api/file/upload", {
|
||||
method: "POST",
|
||||
formData,
|
||||
timeout: DEFAULT_API_TIMEOUT * 2,
|
||||
});
|
||||
|
||||
if (!result?.url) {
|
||||
throw new Error("上传成功但未返回文件地址");
|
||||
}
|
||||
|
||||
return result.url;
|
||||
}
|
||||
|
||||
export async function addKnowledgeDocuments(params: {
|
||||
kbId: number;
|
||||
files: KnowledgeUploadFileInfo[];
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/document/add", {
|
||||
method: "POST",
|
||||
data: {
|
||||
kbId: params.kbId,
|
||||
fileList: params.files.map((file) => ({
|
||||
name: file.name,
|
||||
docUrl: file.url,
|
||||
fileSize: file.size,
|
||||
})),
|
||||
autoSegmentConfigFlag: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function addCustomKnowledgeDocument(params: {
|
||||
kbId: number;
|
||||
name: string;
|
||||
fileContent: string;
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/document/customAdd", {
|
||||
method: "POST",
|
||||
data: {
|
||||
kbId: params.kbId,
|
||||
name: params.name,
|
||||
fileContent: params.fileContent,
|
||||
autoSegmentConfigFlag: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeDocumentName(params: {
|
||||
docId: number;
|
||||
name: string;
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/document/updateDocName", {
|
||||
method: "POST",
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeDocument(docId: number): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/document/deleteById", {
|
||||
method: "GET",
|
||||
params: { id: docId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryKnowledgeDocumentStatus(
|
||||
docIds: number[],
|
||||
): Promise<KnowledgeDocumentInfo[]> {
|
||||
return knowledgeRequest<KnowledgeDocumentInfo[]>(
|
||||
"/api/knowledge/document/queryDocStatus",
|
||||
{
|
||||
method: "POST",
|
||||
data: { docIds },
|
||||
showError: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateKnowledgeDocumentQa(
|
||||
docId: number,
|
||||
): Promise<null> {
|
||||
return knowledgeRequest<null>(`/api/knowledge/document/generateQA/${docId}`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateKnowledgeDocumentEmbeddings(
|
||||
docId: number,
|
||||
): Promise<null> {
|
||||
return knowledgeRequest<null>(
|
||||
`/api/knowledge/document/doc/generateEmbeddings/${docId}`,
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function retryKnowledgeDocumentTask(docId: number): Promise<null> {
|
||||
return knowledgeRequest<null>(
|
||||
`/api/knowledge/document/doc/autoRetryTaskByDocId/${docId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchKnowledgeRawSegments(params: {
|
||||
docId: number;
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<KnowledgePageResult<KnowledgeRawSegmentInfo>> {
|
||||
return knowledgeRequest<KnowledgePageResult<KnowledgeRawSegmentInfo>>(
|
||||
"/api/knowledge/rawSegment/list",
|
||||
{
|
||||
method: "POST",
|
||||
data: {
|
||||
queryFilter: { docId: params.docId },
|
||||
current: params.current || 1,
|
||||
pageSize: params.pageSize || 30,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function addKnowledgeRawSegment(params: {
|
||||
spaceId: number;
|
||||
docId: number;
|
||||
rawTxt: string;
|
||||
sortIndex?: number;
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/rawSegment/add", {
|
||||
method: "POST",
|
||||
data: {
|
||||
spaceId: params.spaceId,
|
||||
docId: params.docId,
|
||||
rawTxt: params.rawTxt,
|
||||
sortIndex: params.sortIndex || 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeRawSegment(params: {
|
||||
spaceId: number;
|
||||
rawSegmentId: number;
|
||||
docId: number;
|
||||
rawTxt: string;
|
||||
sortIndex?: number;
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/rawSegment/update", {
|
||||
method: "POST",
|
||||
data: {
|
||||
spaceId: params.spaceId,
|
||||
rawSegmentId: params.rawSegmentId,
|
||||
docId: params.docId,
|
||||
rawTxt: params.rawTxt,
|
||||
sortIndex: params.sortIndex || 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeRawSegment(
|
||||
rawSegmentId: number,
|
||||
): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/rawSegment/deleteById", {
|
||||
method: "GET",
|
||||
params: { id: rawSegmentId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchKnowledgeQaList(params: {
|
||||
kbId: number;
|
||||
spaceId?: number;
|
||||
question?: string;
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<KnowledgePageResult<KnowledgeQaInfo>> {
|
||||
return knowledgeRequest<KnowledgePageResult<KnowledgeQaInfo>>(
|
||||
"/api/knowledge/qa/list",
|
||||
{
|
||||
method: "POST",
|
||||
data: {
|
||||
queryFilter: {
|
||||
kbId: params.kbId,
|
||||
spaceId: params.spaceId,
|
||||
question: params.question || undefined,
|
||||
},
|
||||
current: params.current || 1,
|
||||
pageSize: params.pageSize || 20,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function addKnowledgeQa(params: {
|
||||
kbId: number;
|
||||
spaceId?: number;
|
||||
question: string;
|
||||
answer: string;
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/qa/add", {
|
||||
method: "POST",
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeQa(params: {
|
||||
id: number;
|
||||
question: string;
|
||||
answer: string;
|
||||
}): Promise<null> {
|
||||
return knowledgeRequest<null>("/api/knowledge/qa/update", {
|
||||
method: "POST",
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeQa(id: number): Promise<null> {
|
||||
const formData = new FormData();
|
||||
formData.append("id", String(id));
|
||||
return knowledgeRequest<null>("/api/knowledge/qa/deleteById", {
|
||||
method: "POST",
|
||||
formData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function importKnowledgeQaExcel(params: {
|
||||
kbId: number;
|
||||
file: File;
|
||||
}): Promise<null> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
formData.append("kbId", String(params.kbId));
|
||||
return knowledgeRequest<null>("/api/knowledge/qa/importExcel", {
|
||||
method: "POST",
|
||||
formData,
|
||||
timeout: DEFAULT_API_TIMEOUT * 2,
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadKnowledgeQaTemplate(): Promise<Blob> {
|
||||
return knowledgeRequest<Blob>("/api/knowledge/qa/downloadExcelTemplate", {
|
||||
method: "GET",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
@@ -87,6 +87,66 @@ export interface PublishedWorkflow {
|
||||
publishUser?: PublishedUser;
|
||||
paymentRequired?: boolean;
|
||||
subscribed?: boolean;
|
||||
allowCopy?: number;
|
||||
spaceId?: number;
|
||||
created?: string;
|
||||
remark?: string | null;
|
||||
}
|
||||
|
||||
export interface WorkflowArgDetail {
|
||||
key?: string | number;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
dataType?: string;
|
||||
inputType?: string;
|
||||
require?: boolean;
|
||||
required?: boolean;
|
||||
enable?: boolean;
|
||||
bindValue?: string | number | boolean | null;
|
||||
children?: WorkflowArgDetail[];
|
||||
subArgs?: WorkflowArgDetail[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PublishedWorkflowDetail extends PublishedWorkflow {
|
||||
inputArgs?: WorkflowArgDetail[];
|
||||
outputArgs?: WorkflowArgDetail[];
|
||||
}
|
||||
|
||||
export interface PublishedAgent {
|
||||
id: number;
|
||||
targetId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
category?: string;
|
||||
collect?: boolean;
|
||||
statistics?: PublishedSkillStatistics;
|
||||
publishUser?: PublishedUser;
|
||||
paymentRequired?: boolean;
|
||||
subscribed?: boolean;
|
||||
allowCopy?: number;
|
||||
spaceId?: number;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
remark?: string | null;
|
||||
targetSubType?: "ChatBot" | "PageApp" | string;
|
||||
}
|
||||
|
||||
export interface PublishedAgentDetail extends PublishedAgent {
|
||||
agentId?: number;
|
||||
type?: string;
|
||||
openingChatMsg?: string;
|
||||
openingGuidQuestions?: string[];
|
||||
guidQuestionDtos?: Array<{
|
||||
content?: string;
|
||||
question?: string;
|
||||
info?: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
manualComponents?: unknown[];
|
||||
variables?: unknown[];
|
||||
}
|
||||
|
||||
export interface FetchPublishedSkillsParams {
|
||||
@@ -101,6 +161,15 @@ export interface FetchPublishedWorkflowsParams {
|
||||
pageSize: number;
|
||||
category: string;
|
||||
kw?: string;
|
||||
spaceId?: number;
|
||||
justReturnSpaceData?: boolean;
|
||||
}
|
||||
|
||||
export interface FetchPublishedAgentsParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
category: string;
|
||||
kw?: string;
|
||||
}
|
||||
|
||||
export interface McpConfigItem {
|
||||
@@ -146,6 +215,60 @@ export interface McpDeployedItem {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface McpParamDetail {
|
||||
key?: string | number;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
dataType?: string;
|
||||
require?: boolean;
|
||||
required?: boolean;
|
||||
subArgs?: McpParamDetail[];
|
||||
children?: McpParamDetail[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface McpToolDetail {
|
||||
name?: string;
|
||||
description?: string;
|
||||
inputArgs?: McpParamDetail[];
|
||||
outputArgs?: McpParamDetail[];
|
||||
inputSchema?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface McpResourceDetail extends McpToolDetail {
|
||||
uri?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface McpComponentDetail {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
targetId?: number;
|
||||
toolName?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface McpRuntimeConfig {
|
||||
serverConfig?: string;
|
||||
components?: McpComponentDetail[];
|
||||
tools?: McpToolDetail[];
|
||||
resources?: McpResourceDetail[];
|
||||
prompts?: McpToolDetail[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface McpDeployedDetail extends McpDeployedItem {
|
||||
mcpConfig?: McpRuntimeConfig;
|
||||
deployedConfig?: McpRuntimeConfig;
|
||||
permissions?: string[];
|
||||
created?: string;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
export interface FetchMcpConfigsParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
@@ -309,6 +432,23 @@ export async function fetchPublishedWorkflows(
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPublishedWorkflowDetail(
|
||||
workflowId: number,
|
||||
): Promise<PublishedWorkflowDetail> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const detail = await marketplaceRequest<PublishedWorkflowDetail>(
|
||||
`/api/published/workflow/${workflowId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...detail,
|
||||
icon: normalizeAssetUrl(baseUrl, detail.icon),
|
||||
};
|
||||
}
|
||||
|
||||
export async function collectWorkflow(workflowId: number): Promise<void> {
|
||||
await marketplaceRequest<null>(
|
||||
`/api/published/workflow/collect/${workflowId}`,
|
||||
@@ -327,6 +467,61 @@ export async function unCollectWorkflow(workflowId: number): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchPublishedAgents(
|
||||
params: FetchPublishedAgentsParams,
|
||||
): Promise<MarketplacePage<PublishedAgent>> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const page = await marketplaceRequest<MarketplacePage<PublishedAgent>>(
|
||||
"/api/published/agent/list",
|
||||
{
|
||||
method: "POST",
|
||||
data: {
|
||||
...params,
|
||||
targetType: "Agent",
|
||||
targetSubType: "ChatBot",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...page,
|
||||
records: (page.records || []).map((item) => ({
|
||||
...item,
|
||||
icon: normalizeAssetUrl(baseUrl, item.icon),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPublishedAgentDetail(
|
||||
agentId: number,
|
||||
): Promise<PublishedAgentDetail> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const detail = await marketplaceRequest<PublishedAgentDetail>(
|
||||
`/api/published/agent/${agentId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...detail,
|
||||
icon: normalizeAssetUrl(baseUrl, detail.icon),
|
||||
targetId: detail.targetId || detail.agentId || agentId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function collectAgent(agentId: number): Promise<void> {
|
||||
await marketplaceRequest<null>(`/api/user/agent/collect/${agentId}`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function unCollectAgent(agentId: number): Promise<void> {
|
||||
await marketplaceRequest<null>(`/api/user/agent/unCollect/${agentId}`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchMcpConfigs(
|
||||
params: FetchMcpConfigsParams,
|
||||
): Promise<MarketplacePage<McpConfigItem>> {
|
||||
@@ -400,6 +595,24 @@ export async function exportMcpServerConfig(mcpId: number): Promise<unknown> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchDeployedMcpDetail(
|
||||
mcpId: number,
|
||||
): Promise<McpDeployedDetail | null> {
|
||||
const baseUrl = await getBaseUrl();
|
||||
const detail = await marketplaceRequest<McpDeployedDetail | null>(
|
||||
`/api/mcp/${mcpId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!detail) return null;
|
||||
return {
|
||||
...detail,
|
||||
icon: normalizeAssetUrl(baseUrl, detail.icon),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMcpDetail(
|
||||
uid: string,
|
||||
): Promise<McpConfigItem | null> {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
grid-template-columns: clamp(220px, 22vw, 280px) minmax(0, 1fr);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: #f5f6fb;
|
||||
color: #15171a;
|
||||
}
|
||||
@@ -22,7 +25,9 @@
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-width: 100%;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #dfe3e8;
|
||||
}
|
||||
@@ -111,6 +116,8 @@
|
||||
grid-template-rows: 56px minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
background: #f5f6fb;
|
||||
}
|
||||
|
||||
@@ -118,13 +125,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 0 24px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #dfe3e8;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #68717b;
|
||||
@@ -147,7 +157,8 @@
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 240px;
|
||||
min-width: 0;
|
||||
max-width: min(240px, 36%);
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
overflow: hidden;
|
||||
@@ -169,6 +180,7 @@
|
||||
.chatBody {
|
||||
min-height: 0;
|
||||
padding: 28px 32px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -235,6 +247,7 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
@@ -262,7 +275,8 @@
|
||||
}
|
||||
|
||||
.messageBubble {
|
||||
max-width: min(720px, 76%);
|
||||
min-width: 0;
|
||||
max-width: min(720px, calc(100% - 48px));
|
||||
padding: 12px 14px;
|
||||
color: #1a2026;
|
||||
background: #ffffff;
|
||||
@@ -271,13 +285,16 @@
|
||||
}
|
||||
|
||||
.userRow .messageBubble {
|
||||
max-width: min(720px, 82%);
|
||||
color: #ffffff;
|
||||
background: #00746f;
|
||||
border-radius: 12px 12px 2px;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
@@ -287,8 +304,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
overflow-wrap: anywhere;
|
||||
color: #5d6873;
|
||||
background: #f1f4f6;
|
||||
border-left: 3px solid #b6c4c7;
|
||||
@@ -311,7 +330,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 240px;
|
||||
max-width: min(240px, 100%);
|
||||
padding: 5px 8px;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
@@ -345,6 +364,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 28px;
|
||||
padding: 5px 8px;
|
||||
color: #56616c;
|
||||
@@ -370,7 +390,9 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
margin-top: 8px;
|
||||
overflow-wrap: anywhere;
|
||||
color: #76818c;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -392,7 +414,9 @@
|
||||
}
|
||||
|
||||
.composer {
|
||||
min-width: 0;
|
||||
padding: 14px 32px 12px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #dfe3e8;
|
||||
}
|
||||
@@ -408,7 +432,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 240px;
|
||||
max-width: min(240px, 100%);
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
@@ -424,16 +448,20 @@
|
||||
|
||||
.inputRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 72px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(64px, 72px);
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-width: 0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
border-radius: 9px;
|
||||
background: #00746f;
|
||||
@@ -444,7 +472,9 @@
|
||||
.composerToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
margin-top: 8px;
|
||||
color: #8c959f;
|
||||
@@ -452,15 +482,19 @@
|
||||
}
|
||||
|
||||
.toolbarSpacer {
|
||||
flex: 1;
|
||||
flex: 1 1 80px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
flex: 0 1 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modelSelect {
|
||||
width: 150px;
|
||||
flex: 0 1 150px;
|
||||
width: clamp(118px, 16vw, 150px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hiddenInput {
|
||||
@@ -469,12 +503,17 @@
|
||||
|
||||
.skillPanel,
|
||||
.mcpPanel {
|
||||
width: 320px;
|
||||
width: min(320px, calc(100vw - 48px));
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
.skillList,
|
||||
.mcpPanel {
|
||||
width: min(380px, calc(100vw - 48px));
|
||||
max-height: 420px;
|
||||
}
|
||||
|
||||
.skillList,
|
||||
.mcpList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -482,6 +521,47 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mcpList {
|
||||
max-height: 330px;
|
||||
}
|
||||
|
||||
.mcpPanelHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcpPanelHeader div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mcpPanelHeader strong {
|
||||
color: #202932;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mcpPanelHeader small {
|
||||
color: #7a858f;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mcpPanelState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 160px;
|
||||
color: #7a858f;
|
||||
}
|
||||
|
||||
.skillItem,
|
||||
.mcpItem {
|
||||
display: flex;
|
||||
@@ -498,6 +578,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mcpItem {
|
||||
align-items: flex-start;
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.skillItem:hover,
|
||||
.mcpItem:hover,
|
||||
.skillItem.selected,
|
||||
@@ -525,8 +610,21 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.mcpItemIcon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-top: 2px;
|
||||
color: #00746f;
|
||||
background: #e5f4f2;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.skillMeta,
|
||||
.mcpItem span {
|
||||
.mcpMeta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@@ -534,7 +632,7 @@
|
||||
}
|
||||
|
||||
.skillMeta strong,
|
||||
.mcpItem strong {
|
||||
.mcpMeta strong {
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
@@ -543,7 +641,8 @@
|
||||
}
|
||||
|
||||
.skillMeta small,
|
||||
.mcpItem small {
|
||||
.mcpMeta small,
|
||||
.mcpMeta em {
|
||||
overflow: hidden;
|
||||
color: #7a858f;
|
||||
font-size: 12px;
|
||||
@@ -552,9 +651,23 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mcpMeta em {
|
||||
margin-top: 3px;
|
||||
color: #98a1a9;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.mcpItemAction {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 5px;
|
||||
color: #00746f;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.page {
|
||||
grid-template-columns: 236px minmax(0, 1fr);
|
||||
grid-template-columns: clamp(200px, 26vw, 236px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.threadRail {
|
||||
@@ -572,6 +685,10 @@
|
||||
max-width: 86%;
|
||||
}
|
||||
|
||||
.assistantRow .messageBubble {
|
||||
max-width: calc(100% - 44px);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: none;
|
||||
}
|
||||
@@ -580,13 +697,85 @@
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.threadRail {
|
||||
display: none;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 0.8fr) minmax(0, 1.2fr);
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #dfe3e8;
|
||||
}
|
||||
|
||||
.suggestionList {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.newButton {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.search {
|
||||
height: 34px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.recentLabel {
|
||||
grid-column: 1 / -1;
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: row;
|
||||
min-height: 54px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.conversationItem {
|
||||
flex: 0 0 min(180px, 52vw);
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.chatShell {
|
||||
grid-template-rows: 48px minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.chatHeader,
|
||||
.chatBody,
|
||||
.composer {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.chatHeader {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modelBadge {
|
||||
max-width: 40%;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.messageBubble,
|
||||
.userRow .messageBubble,
|
||||
.assistantRow .messageBubble {
|
||||
max-width: calc(100% - 42px);
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
grid-template-columns: minmax(0, 1fr) 64px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.composerToolbar :global(.ant-btn) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.modelSelect {
|
||||
flex-basis: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,56 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.updateNotice {
|
||||
max-width: 128px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 6px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid transparent;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
opacity 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
|
||||
.updateNotice:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.updateNotice:not(:disabled):hover {
|
||||
opacity: 0.84;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.updateNoticeAvailable {
|
||||
background: rgba(82, 196, 26, 0.12);
|
||||
border-color: rgba(82, 196, 26, 0.25);
|
||||
color: #3f9f12;
|
||||
}
|
||||
|
||||
.updateNoticeDownloading {
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
border-color: rgba(22, 119, 255, 0.2);
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.updateNoticeInstall {
|
||||
background: rgba(250, 173, 20, 0.12);
|
||||
border-color: rgba(250, 173, 20, 0.25);
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.historyTrigger {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
.page {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #f7f6f3;
|
||||
color: #20211f;
|
||||
}
|
||||
|
||||
.header,
|
||||
.detailHeader {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 28px 32px 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
color: #8a8d89;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.breadcrumb span {
|
||||
color: #b7bab4;
|
||||
}
|
||||
|
||||
.breadcrumb strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #006d68;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.titleRow h1,
|
||||
.detailTitleBlock h1 {
|
||||
margin: 0;
|
||||
color: #20211f;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.titleRow p,
|
||||
.detailTitleBlock p {
|
||||
margin: 8px 0 0;
|
||||
color: #888b85;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.content,
|
||||
.detailContent {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 24px 32px 32px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.toolbar,
|
||||
.tabToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
margin-bottom: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search,
|
||||
.innerSearch {
|
||||
width: min(420px, 100%);
|
||||
}
|
||||
|
||||
.toolbarCreateButton {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.search :global(.ant-input-affix-wrapper),
|
||||
.search:global(.ant-input-affix-wrapper),
|
||||
.innerSearch :global(.ant-input-affix-wrapper),
|
||||
.innerSearch:global(.ant-input-affix-wrapper) {
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
border-color: #dcd9d1;
|
||||
background: #ffffff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-left: auto;
|
||||
color: #888b85;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.count span {
|
||||
margin-right: 6px;
|
||||
color: #007c78;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.listArea {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.centerState {
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #777b75;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 202px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 18px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e8e4dc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 7px rgba(32, 33, 31, 0.05);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #bddbd7;
|
||||
box-shadow: 0 8px 22px rgba(32, 33, 31, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.knowledgeIcon,
|
||||
.detailIcon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: #e2f0ec;
|
||||
color: #007c78;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.detailIcon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
flex: 0 0 52px;
|
||||
}
|
||||
|
||||
.knowledgeIconImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.knowledgeIconText {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cardTitle h3 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #20211f;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cardTitle span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #8a8d89;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
display: -webkit-box;
|
||||
min-height: 44px;
|
||||
margin: 14px 0 0;
|
||||
overflow: hidden;
|
||||
color: #5e625c;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.cardMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
padding-top: 14px;
|
||||
color: #8a8d89;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cardMeta span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detailHeader {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detailTitleBlock {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 52px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.detailTitleBlock h1 {
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detailActions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.statRow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e8e4dc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.statCard span {
|
||||
display: block;
|
||||
color: #888b85;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.statCard strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
color: #20211f;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabPane {
|
||||
min-width: 0;
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
|
||||
.documentSelect {
|
||||
width: min(340px, 100%);
|
||||
}
|
||||
|
||||
.hiddenInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.iconUploadRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.iconUploadCard {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
color: #3f423d;
|
||||
text-align: left;
|
||||
background: #fbfaf8;
|
||||
border: 1px solid #dedbd2;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease;
|
||||
}
|
||||
|
||||
.iconUploadCard:hover {
|
||||
background: #ffffff;
|
||||
border-color: #9bc9c2;
|
||||
}
|
||||
|
||||
.iconUploadCard:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.iconPreview {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
color: #007c78;
|
||||
background: #e2f0ec;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.iconPreview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.iconPreview :global(.anticon) {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.iconUploadText {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.iconUploadText strong {
|
||||
overflow: hidden;
|
||||
color: #20211f;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.iconUploadText span {
|
||||
overflow: hidden;
|
||||
color: #888b85;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mutedText {
|
||||
color: #888b85;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tableActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
color: #007c78;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.linkButton span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.segmentText,
|
||||
.qaText {
|
||||
margin: 0;
|
||||
color: #3f423d;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.segmentText {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
}
|
||||
|
||||
.qaText {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.graphPlaceholder {
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 32px;
|
||||
color: #747872;
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
border: 1px dashed #d8d4ca;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.graphPlaceholder :global(.anticon) {
|
||||
color: #007c78;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.graphPlaceholder h3 {
|
||||
margin: 0;
|
||||
color: #20211f;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.graphPlaceholder p {
|
||||
max-width: 520px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.statRow {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.header,
|
||||
.detailHeader {
|
||||
padding: 20px 20px 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.content,
|
||||
.detailContent {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.detailActions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.statRow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,22 @@
|
||||
border: 1px solid #e8e4dc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 7px rgba(32, 33, 31, 0.05);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #bddbd7;
|
||||
box-shadow: 0 8px 22px rgba(32, 33, 31, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card:focus-visible {
|
||||
outline: 2px solid rgba(0, 114, 109, 0.28);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
@@ -214,16 +230,36 @@
|
||||
.metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
color: #5d605b;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rating {
|
||||
color: #ff9d18;
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.detailButton:global(.ant-btn) {
|
||||
height: 32px;
|
||||
padding: 0 6px;
|
||||
color: #00726d;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.detailButton:global(.ant-btn):hover {
|
||||
color: #00645f !important;
|
||||
background: #e6f4f2 !important;
|
||||
}
|
||||
|
||||
.connectButton:global(.ant-btn-primary) {
|
||||
min-width: 58px;
|
||||
height: 32px;
|
||||
@@ -258,6 +294,307 @@
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.detailModal :global(.ant-modal-content) {
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detailModal :global(.ant-modal-close) {
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.detailLoading {
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #777b75;
|
||||
}
|
||||
|
||||
.detailContent {
|
||||
max-height: min(760px, calc(100vh - 96px));
|
||||
overflow: auto;
|
||||
background: #fbfaf8;
|
||||
}
|
||||
|
||||
.detailHero {
|
||||
display: grid;
|
||||
grid-template-columns: 56px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 28px 30px 22px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #ebe7df;
|
||||
}
|
||||
|
||||
.detailIcon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.detailIcon .mcpIcon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.detailTitleBlock {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detailTitleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detailTitleRow h2 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #20211f;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.detailMeta {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: #777b75;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.detailMeta span {
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid #ddd8cf;
|
||||
}
|
||||
|
||||
.detailMeta span:last-child {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.detailError {
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.detailInlineLoading {
|
||||
margin: 18px 30px 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #777b75;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detailDescription {
|
||||
margin: 22px 30px 0;
|
||||
color: #4f534d;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.detailStats {
|
||||
margin: 20px 30px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detailStats div,
|
||||
.detailInfoGrid div {
|
||||
min-width: 0;
|
||||
border: 1px solid #ebe7df;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.detailStats div {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.detailStats strong {
|
||||
display: block;
|
||||
color: #00726d;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.detailStats span,
|
||||
.detailInfoGrid span {
|
||||
color: #83877f;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.detailInfoGrid {
|
||||
margin: 12px 30px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detailInfoGrid div {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.detailInfoGrid strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #2c2f2a;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.detailSection {
|
||||
margin: 18px 30px 0;
|
||||
}
|
||||
|
||||
.detailSection:last-child {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.detailSectionHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detailSectionHeader h3 {
|
||||
margin: 0;
|
||||
color: #20211f;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.detailSectionHeader span {
|
||||
color: #8a8d89;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.emptyLine {
|
||||
padding: 16px;
|
||||
border: 1px dashed #ded9d0;
|
||||
border-radius: 8px;
|
||||
color: #8a8d89;
|
||||
background: #ffffff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.capabilityList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.capabilityItem {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid #ebe7df;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.capabilityTop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.capabilityTop strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #20211f;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.capabilityTop span {
|
||||
flex: 0 0 auto;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 2px 7px;
|
||||
border-radius: 10px;
|
||||
background: #f0eee8;
|
||||
color: #6d706a;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.capabilityItem p {
|
||||
margin: 8px 0 0;
|
||||
color: #62665f;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.paramList {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.param {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
padding: 3px 7px;
|
||||
border-radius: 6px;
|
||||
background: #eef7f5;
|
||||
color: #006d68;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.param em {
|
||||
color: #748079;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.param strong {
|
||||
color: #c06a19;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.header {
|
||||
height: auto;
|
||||
@@ -286,4 +623,35 @@
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detailHero {
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
padding: 24px 22px 18px;
|
||||
}
|
||||
|
||||
.detailHero > :global(.ant-btn) {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.detailIcon,
|
||||
.detailIcon .mcpIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.detailStats,
|
||||
.detailInfoGrid,
|
||||
.capabilityList {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detailDescription,
|
||||
.detailStats,
|
||||
.detailInfoGrid,
|
||||
.detailSection,
|
||||
.detailInlineLoading {
|
||||
margin-left: 22px;
|
||||
margin-right: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,22 @@
|
||||
border: 1px solid #e8e4dc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 7px rgba(32, 33, 31, 0.05);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #bddbd7;
|
||||
box-shadow: 0 8px 22px rgba(32, 33, 31, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card:focus-visible {
|
||||
outline: 2px solid rgba(0, 114, 109, 0.28);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
@@ -208,6 +224,8 @@
|
||||
|
||||
.badgeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@@ -225,6 +243,20 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.usedBadge {
|
||||
max-width: 92px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 4px 10px;
|
||||
border-radius: 13px;
|
||||
background: #edf4ff;
|
||||
color: #2968bf;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 1;
|
||||
margin: 18px 0 18px;
|
||||
@@ -247,16 +279,36 @@
|
||||
.metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
color: #8a8d87;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rating {
|
||||
color: #ff9d18;
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.detailButton:global(.ant-btn) {
|
||||
height: 32px;
|
||||
padding: 0 6px;
|
||||
color: #00726d;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.detailButton:global(.ant-btn):hover {
|
||||
color: #00645f !important;
|
||||
background: #e6f4f2 !important;
|
||||
}
|
||||
|
||||
.useButton:global(.ant-btn-primary) {
|
||||
min-width: 58px;
|
||||
height: 32px;
|
||||
@@ -272,7 +324,7 @@
|
||||
}
|
||||
|
||||
.usedButton:global(.ant-btn) {
|
||||
min-width: 74px;
|
||||
min-width: 86px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
@@ -281,6 +333,325 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.detailModal :global(.ant-modal-content) {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detailModal :global(.ant-modal-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detailModal :global(.ant-modal-close) {
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.detailLoading {
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #777b75;
|
||||
}
|
||||
|
||||
.detailContent {
|
||||
max-height: min(780px, calc(100vh - 96px));
|
||||
overflow: auto;
|
||||
background: #fbfaf8;
|
||||
}
|
||||
|
||||
.detailHero {
|
||||
display: grid;
|
||||
grid-template-columns: 56px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 28px 30px 22px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #ebe7df;
|
||||
}
|
||||
|
||||
.detailIcon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.detailIcon .workflowIconImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detailTitleBlock {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detailTitleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detailTitleRow h2 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #20211f;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.detailMeta {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: #777b75;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.detailMeta span {
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid #ddd8cf;
|
||||
}
|
||||
|
||||
.detailMeta span:last-child {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.detailError {
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.detailInlineLoading {
|
||||
margin: 18px 30px 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #777b75;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detailSection {
|
||||
margin: 20px 30px 0;
|
||||
}
|
||||
|
||||
.detailSection:last-child {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.detailSectionHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detailSectionHeader h3,
|
||||
.usageGuide h3 {
|
||||
margin: 0;
|
||||
color: #20211f;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.detailSectionHeader span {
|
||||
color: #8a8d89;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.detailDescription,
|
||||
.detailRemark {
|
||||
margin: 0;
|
||||
color: #4f534d;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.detailRemark {
|
||||
margin-top: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebe7df;
|
||||
color: #62665f;
|
||||
}
|
||||
|
||||
.detailStats {
|
||||
margin: 20px 30px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detailStats div {
|
||||
min-width: 0;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #ebe7df;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.detailStats strong {
|
||||
display: block;
|
||||
color: #00726d;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.detailStats span {
|
||||
color: #83877f;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.emptyLine {
|
||||
padding: 16px;
|
||||
border: 1px dashed #ded9d0;
|
||||
border-radius: 8px;
|
||||
color: #8a8d89;
|
||||
background: #ffffff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.argTable {
|
||||
overflow: auto;
|
||||
border: 1px solid #ebe7df;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.argTableHead,
|
||||
.argRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(140px, 1fr) 110px minmax(220px, 1.6fr) 130px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.argTableHead {
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid #ebe7df;
|
||||
background: #f4f2ed;
|
||||
color: #777b75;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.argRow {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #f0ece4;
|
||||
color: #4f534d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.argRow:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.argName {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.argName strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #20211f;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.argName em {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
background: #fff4e5;
|
||||
color: #c06a19;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.argRow p,
|
||||
.argRow code {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.argRow code {
|
||||
color: #6f756d;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.usageGuide {
|
||||
margin: 20px 30px 30px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 8px;
|
||||
background: #eef7f5;
|
||||
border: 1px solid #d7ebe7;
|
||||
}
|
||||
|
||||
.usageGuide p {
|
||||
margin: 6px 0 0;
|
||||
color: #4f5e59;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.usageGuide span {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 9px;
|
||||
border-radius: 11px;
|
||||
background: #fff4e5;
|
||||
color: #a85f18;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.header {
|
||||
padding: 22px 22px 0;
|
||||
@@ -301,4 +672,35 @@
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detailHero {
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
padding: 24px 22px 18px;
|
||||
}
|
||||
|
||||
.detailHero > :global(.ant-btn) {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.detailIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.detailStats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.detailSection,
|
||||
.detailStats,
|
||||
.detailInlineLoading,
|
||||
.usageGuide {
|
||||
margin-left: 22px;
|
||||
margin-right: 22px;
|
||||
}
|
||||
|
||||
.usageGuide {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user