码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
ng-webot/web/src/pages/McpPage.tsx
McpPage.tsx4.2 KB
import { useState, useEffect, useCallback } from 'react'
import { useAuth } from '@/lib/auth'
import { api } from '@/lib/api'
import McpConfig from '@/components/McpConfig'
import { toast } from 'sonner'
import { Bot, Loader2, Puzzle } from 'lucide-react'

export default function McpPage() {
  const { bots, loading: authLoading } = useAuth()
  const [selBotId, setSelBotId] = useState<number | ''>('')
  const [settings, setSettings] = useState<Record<string, string>>({})
  const [saving, setSaving] = useState(false)
  const [loading, setLoading] = useState(false)

  // auto-select first bot
  useEffect(() => {
    if (!authLoading && bots.length > 0 && !selBotId) {
      setSelBotId(bots[0].id)
    }
  }, [authLoading, bots, selBotId])

  const loadSettings = useCallback(async (botId: number) => {
    if (!botId) return
    setLoading(true)
    try {
      const d = await api.post('/api/market/plugin_settings', { bot_id: botId, plugin_id: 'mcp' })
      if (!d.ok) throw new Error(d.msg)
      setSettings(d.settings || {})
    } catch (e: any) {
      toast.error('加载 MCP 配置失败:' + e.message)
      setSettings({})
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => {
    if (selBotId) loadSettings(selBotId)
  }, [selBotId, loadSettings])

  const saveSetting = useCallback(async (key: string, value: string): Promise<boolean> => {
    if (!selBotId) return false
    setSaving(true)
    try {
      const merged = { ...settings, [key]: value }
      const d = await api.post('/api/market/plugin_settings_save', {
        bot_id: selBotId, plugin_id: 'mcp', data: merged
      })
      if (!d.ok) throw new Error(d.msg)
      setSettings(merged)
      toast.success('MCP 配置已保存')
      return true
    } catch (e: any) {
      toast.error(e.message)
      return false
    } finally {
      setSaving(false)
    }
  }, [selBotId, settings])

  if (authLoading) {
    return (
      <div className="flex items-center justify-center py-20">
        <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
      </div>
    )
  }

  if (bots.length === 0) {
    return (
      <div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
        <Bot className="h-12 w-12 mb-4 text-gray-300" />
        <p className="text-sm">还没有机器人,请先在个人中心添加</p>
      </div>
    )
  }

  return (
    <div className="w-full px-4 py-6 sm:px-6 space-y-6">
      {/* header */}
      <div className="flex items-center gap-3">
        <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/30">
          <Puzzle className="h-5 w-5 text-purple-600 dark:text-purple-400" />
        </div>
        <div>
          <h1 className="text-xl font-bold">MCP 服务器管理</h1>
          <p className="text-sm text-muted-foreground">连接外部 MCP 服务器,赋予机器人更强的 AI 工具能力</p>
        </div>
      </div>

      {/* bot selector */}
      <div className="flex items-center gap-2 flex-wrap">
        <span className="text-sm text-muted-foreground">选择机器人:</span>
        {bots.map(b => (
          <button
            key={b.id}
            onClick={() => setSelBotId(b.id)}
            className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
              selBotId === b.id
                ? 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300'
                : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
            }`}
          >
            <Bot className="inline h-3 w-3 mr-1" />
            {b.name || b.id}
          </button>
        ))}
      </div>

      {/* config panel */}
      {loading ? (
        <div className="flex items-center justify-center py-12">
          <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
        </div>
      ) : selBotId !== '' ? (
        <McpConfig
          bot={{ id: selBotId }}
          settings={settings}
          onSave={saveSetting}
          saving={saving}
        />
      ) : null}
    </div>
  )
}