| |
| |
| |
| |
| |
| |
|
|
| 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[]; |
| } |
|
|
| |
| 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!} />; |
| } |
|
|
| |
| 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> |
| ); |
| } |
|
|