message / frontend /src /plugins /PluginApiDocs.tsx
hunian
feat: 重构前端控制台和工具页面
a43ac26
Raw
History Blame Contribute Delete
8.62 kB
/**
* 插件 API 文档组件
*
* 展示 HTTP API 和 MCP 工具文档。
* 使用 console 语义 token 保持视觉一致。
*/
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Skeleton } from '@components/ui/skeleton';
import { Cpu, ArrowLeftRight, Globe } from 'lucide-react';
import { Badge } from '@components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@components/ui/tabs';
import { StatePanel } from '@components/console';
interface HttpApiEndpoint {
path: string;
methods: string[];
name: string;
description?: string;
}
interface MCPTool {
name: string;
full_name: string;
title: string;
description: string;
input: {
type: string;
properties: Record<string, any>;
required: string[];
};
output: {
type: string;
properties: Record<string, any>;
required: string[];
};
}
interface PluginDocs {
plugin_name: string;
http_api: HttpApiEndpoint[];
mcp_tools: MCPTool[];
}
// HTTP 方法对应的 console 状态色 class
const methodColorMap: Record<string, string> = {
GET: 'bg-console-status-enabled text-white',
POST: 'bg-console-status-info text-white',
DELETE: 'bg-console-status-error text-white',
PUT: 'bg-console-status-warning text-white',
};
export function PluginApiDocs() {
const { pluginName } = useParams<{ pluginName: string }>();
const [docs, setDocs] = useState<PluginDocs | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!pluginName) {
setLoading(false);
return;
}
const loadDocs = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/plugin-docs/${pluginName}`);
if (!response.ok) {
setError('加载文档失败');
return;
}
const data: PluginDocs = await response.json();
setDocs(data);
} catch (err) {
console.error('Failed to load docs:', err);
setError('加载文档失败');
}
setLoading(false);
};
loadDocs();
}, [pluginName]);
if (loading) {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-64 w-full" />
</div>
);
}
if (error) {
return (
<StatePanel
state="error"
title="加载失败"
description={error}
/>
);
}
if (!docs || (!docs.http_api.length && !docs.mcp_tools.length)) {
return (
<StatePanel
state="empty"
title="暂无 API 文档"
description="该插件暂无 API 文档"
/>
);
}
const hasHttpApi = docs.http_api.length > 0;
const hasMcpTools = docs.mcp_tools.length > 0;
// 如果只有一种,直接显示
if (!hasHttpApi) {
return <MCPToolsDisplay tools={docs.mcp_tools} />;
}
if (!hasMcpTools) {
return <HttpApiDisplay endpoints={docs.http_api} pluginName={pluginName!} />;
}
// 两种都有,显示 Tabs
return (
<Tabs defaultValue="http" className="w-full">
<TabsList>
<TabsTrigger value="http">HTTP API</TabsTrigger>
<TabsTrigger value="mcp">MCP 工具</TabsTrigger>
</TabsList>
<TabsContent value="http" className="mt-4">
<HttpApiDisplay endpoints={docs.http_api} pluginName={pluginName!} />
</TabsContent>
<TabsContent value="mcp" className="mt-4">
<MCPToolsDisplay tools={docs.mcp_tools} />
</TabsContent>
</Tabs>
);
}
function HttpApiDisplay({ endpoints, pluginName }: { endpoints: HttpApiEndpoint[]; pluginName: string }) {
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-console-status-info" />
<h3 className="text-sm font-medium text-console-text-primary">HTTP API 文档</h3>
</div>
<p className="text-sm text-console-text-secondary">
HTTP API 可通过 <code className="text-xs bg-console-surface-base px-1.5 py-0.5 rounded-console-sm text-console-text-primary font-mono">/plugins/{pluginName}/api</code> 路径调用。
</p>
<div className="space-y-3">
{endpoints.map((endpoint, index) => (
<div key={index} className="py-3 px-4 bg-console-surface-raised rounded-console-md border border-console-border">
<div className="flex items-center gap-2 flex-wrap">
{endpoint.methods.map((method) => (
<Badge
key={method}
className={`${methodColorMap[method] || 'bg-console-text-muted text-white'} text-xs font-mono`}
>
{method}
</Badge>
))}
<code className="text-xs bg-console-surface-base px-2 py-1 rounded-console-sm font-mono text-console-text-primary">
{endpoint.path}
</code>
</div>
<p className="text-sm text-console-text-secondary mt-2">
{endpoint.description || '无描述'}
</p>
</div>
))}
</div>
</div>
);
}
function MCPToolsDisplay({ tools }: { tools: MCPTool[] }) {
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-console-status-info" />
<h3 className="text-sm font-medium text-console-text-primary">MCP 工具文档</h3>
</div>
<p className="text-sm text-console-text-secondary">
MCP (Model Context Protocol) 工具可通过 <code className="text-xs bg-console-surface-base px-1.5 py-0.5 rounded-console-sm text-console-text-primary font-mono">/mcp</code> 端点调用,
用于 AI 模型与插件交互。
</p>
{tools.map((tool) => (
<div key={tool.full_name} className="py-3 px-4 bg-console-surface-raised rounded-console-md border border-console-border">
<div className="flex items-center gap-2">
<ArrowLeftRight className="h-4 w-4 text-console-text-secondary shrink-0" />
<span className="text-sm font-medium text-console-text-primary">
{tool.title || tool.name}
</span>
<Badge variant="outline" className="font-mono text-xs text-console-text-muted">
{tool.full_name}
</Badge>
</div>
<p className="text-sm text-console-text-secondary mt-2">
{tool.description}
</p>
{tool.input.properties && Object.keys(tool.input.properties).length > 0 && (
<div className="mt-3">
<h4 className="text-xs font-medium text-console-text-primary mb-2">输入参数</h4>
<div className="bg-console-surface-base rounded-console-sm p-3 space-y-2">
{Object.entries(tool.input.properties).map(([key, prop]) => (
<div key={key} className="flex items-start gap-2">
<code className="text-xs bg-console-surface-raised px-2 py-0.5 rounded-console-sm font-mono text-console-text-primary">
{key}
</code>
{tool.input.required.includes(key) && (
<Badge variant="destructive" className="text-xs">必需</Badge>
)}
<span className="text-xs text-console-text-secondary">
{(prop as any).description || (prop as any).type}
</span>
</div>
))}
</div>
</div>
)}
{tool.output.properties && Object.keys(tool.output.properties).length > 0 && (
<div className="mt-3">
<h4 className="text-xs font-medium text-console-text-primary mb-2">输出结果</h4>
<div className="bg-console-surface-base rounded-console-sm p-3 space-y-2">
{Object.entries(tool.output.properties).map(([key, prop]) => (
<div key={key} className="flex items-start gap-2">
<code className="text-xs bg-console-surface-raised px-2 py-0.5 rounded-console-sm font-mono text-console-text-primary">
{key}
</code>
<span className="text-xs text-console-text-secondary">
{(prop as any).description || (prop as any).type}
</span>
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
);
}