618 lines
23 KiB
TypeScript
618 lines
23 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import {
|
||
MessageSquare,
|
||
Plus,
|
||
Settings,
|
||
CheckCircle2,
|
||
XCircle,
|
||
RefreshCw,
|
||
Copy,
|
||
ExternalLink,
|
||
Webhook,
|
||
Bot,
|
||
Loader2,
|
||
Send,
|
||
Key,
|
||
Globe,
|
||
Smartphone,
|
||
} from "lucide-react"
|
||
|
||
// 渠道接口
|
||
interface Channel {
|
||
id: string
|
||
name: string
|
||
type: 'feishu' | 'wecom' | 'wechat' | 'api' | 'webhook'
|
||
status: 'connected' | 'disconnected' | 'pending'
|
||
config: {
|
||
appId?: string
|
||
appSecret?: string
|
||
webhookUrl?: string
|
||
token?: string
|
||
encryptKey?: string
|
||
}
|
||
stats: {
|
||
totalMessages: number
|
||
todayMessages: number
|
||
lastMessage?: string
|
||
}
|
||
createdAt: string
|
||
enabled: boolean
|
||
}
|
||
|
||
// 渠道类型配置 - 包含详细配置步骤
|
||
const CHANNEL_TYPES = {
|
||
feishu: {
|
||
name: '飞书',
|
||
icon: '🪶',
|
||
color: 'bg-blue-100 text-blue-700',
|
||
description: '对接飞书机器人,支持私聊和群聊',
|
||
configFields: ['appId', 'appSecret', 'encryptKey', 'token'],
|
||
steps: [
|
||
'1. 登录飞书开放平台 open.feishu.cn',
|
||
'2. 创建企业自建应用 → 机器人',
|
||
'3. 获取 App ID 和 App Secret',
|
||
'4. 配置事件订阅 URL: {apiBaseUrl}/feishu/events',
|
||
'5. 添加权限: im:message, im:message.group_at_msg',
|
||
'6. 将机器人添加到目标群聊',
|
||
'7. @机器人 即可开始对话',
|
||
],
|
||
},
|
||
wecom: {
|
||
name: '企业微信',
|
||
icon: '💼',
|
||
color: 'bg-green-100 text-green-700',
|
||
description: '对接企业微信应用或机器人',
|
||
configFields: ['corpId', 'agentId', 'secret', 'token', 'encodingAESKey'],
|
||
steps: [
|
||
'1. 登录企业微信管理后台 work.weixin.qq.com',
|
||
'2. 应用管理 → 自建应用 → 创建应用',
|
||
'3. 获取 CorpID、AgentId、Secret',
|
||
'4. 设置接收消息 URL: {apiBaseUrl}/wecom/callback',
|
||
'5. 配置 Token 和 EncodingAESKey',
|
||
'6. 开启客户联系、消息推送等权限',
|
||
'7. 发消息给应用即可触发对话',
|
||
],
|
||
},
|
||
wechat: {
|
||
name: '微信',
|
||
icon: '💬',
|
||
color: 'bg-emerald-100 text-emerald-700',
|
||
description: '通过存客宝对接微信私域',
|
||
configFields: ['ckbApiKey', 'ckbSecret'],
|
||
steps: [
|
||
'1. 登录存客宝后台 admin.cunkebao.com',
|
||
'2. 系统设置 → API配置',
|
||
'3. 获取 API Key 和 Secret',
|
||
'4. 配置消息回调 URL: {apiBaseUrl}/ckb/webhook',
|
||
'5. 在卡若AI配置存客宝凭证',
|
||
'6. 微信好友发消息即可触发AI回复',
|
||
],
|
||
},
|
||
api: {
|
||
name: 'API接口',
|
||
icon: '🔌',
|
||
color: 'bg-purple-100 text-purple-700',
|
||
description: '标准REST API,供第三方系统调用',
|
||
configFields: ['apiKey'],
|
||
steps: [
|
||
'1. 在本页面生成 API Key',
|
||
'2. 调用方式: POST {apiBaseUrl}/chat',
|
||
'3. Header: Authorization: Bearer {API_KEY}',
|
||
'4. Body: { "message": "你的问题" }',
|
||
'5. 响应: { "success": true, "response": {...} }',
|
||
],
|
||
},
|
||
webhook: {
|
||
name: 'Webhook',
|
||
icon: '🔗',
|
||
color: 'bg-orange-100 text-orange-700',
|
||
description: '通用Webhook回调接口',
|
||
configFields: ['webhookUrl', 'secret'],
|
||
steps: [
|
||
'1. 配置接收 Webhook 的目标 URL',
|
||
'2. 设置签名密钥 (可选)',
|
||
'3. 神射手会在以下事件时推送:',
|
||
' - 新用户画像生成',
|
||
' - 流量包创建完成',
|
||
' - 报告生成完成',
|
||
'4. 推送格式: { "event": "...", "data": {...} }',
|
||
],
|
||
},
|
||
}
|
||
|
||
// 预定义渠道
|
||
const DEFAULT_CHANNELS: Channel[] = [
|
||
{
|
||
id: 'ch_feishu_1',
|
||
name: '卡若AI飞书机器人',
|
||
type: 'feishu',
|
||
status: 'connected',
|
||
config: {
|
||
appId: 'cli_a6xxx',
|
||
appSecret: '********',
|
||
token: 'karuo_token',
|
||
},
|
||
stats: {
|
||
totalMessages: 12580,
|
||
todayMessages: 156,
|
||
lastMessage: '5分钟前',
|
||
},
|
||
createdAt: '2024-01-15',
|
||
enabled: true,
|
||
},
|
||
{
|
||
id: 'ch_wecom_1',
|
||
name: '企业微信-神射手助手',
|
||
type: 'wecom',
|
||
status: 'connected',
|
||
config: {
|
||
appId: 'ww_xxx',
|
||
},
|
||
stats: {
|
||
totalMessages: 8920,
|
||
todayMessages: 89,
|
||
lastMessage: '12分钟前',
|
||
},
|
||
createdAt: '2024-02-01',
|
||
enabled: true,
|
||
},
|
||
{
|
||
id: 'ch_wechat_1',
|
||
name: '存客宝微信通道',
|
||
type: 'wechat',
|
||
status: 'connected',
|
||
config: {},
|
||
stats: {
|
||
totalMessages: 45600,
|
||
todayMessages: 320,
|
||
lastMessage: '1分钟前',
|
||
},
|
||
createdAt: '2024-01-01',
|
||
enabled: true,
|
||
},
|
||
{
|
||
id: 'ch_api_1',
|
||
name: '神射手API',
|
||
type: 'api',
|
||
status: 'connected',
|
||
config: {
|
||
apiKey: 'sk-archer-xxx',
|
||
},
|
||
stats: {
|
||
totalMessages: 156000,
|
||
todayMessages: 2340,
|
||
lastMessage: '刚刚',
|
||
},
|
||
createdAt: '2024-01-01',
|
||
enabled: true,
|
||
},
|
||
]
|
||
|
||
export default function ChannelsPage() {
|
||
const [channels, setChannels] = useState<Channel[]>(DEFAULT_CHANNELS)
|
||
const [loading, setLoading] = useState(false)
|
||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||
const [showConfigDialog, setShowConfigDialog] = useState(false)
|
||
const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null)
|
||
const [newChannelType, setNewChannelType] = useState<keyof typeof CHANNEL_TYPES>('feishu')
|
||
const [testLoading, setTestLoading] = useState<string | null>(null)
|
||
|
||
// API端点基础URL - 动态获取当前域名
|
||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||
|
||
useEffect(() => {
|
||
// 获取当前部署的域名
|
||
const host = typeof window !== 'undefined' ? window.location.origin : ''
|
||
setApiBaseUrl(`${host}/api/shensheshou`)
|
||
}, [])
|
||
|
||
const formatNumber = (num: number): string => {
|
||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||
return num.toLocaleString()
|
||
}
|
||
|
||
// 测试连接
|
||
const testConnection = async (channelId: string) => {
|
||
setTestLoading(channelId)
|
||
await new Promise(r => setTimeout(r, 2000))
|
||
setTestLoading(null)
|
||
}
|
||
|
||
// 切换启用状态
|
||
const toggleChannel = (channelId: string) => {
|
||
setChannels(channels.map(ch =>
|
||
ch.id === channelId ? { ...ch, enabled: !ch.enabled } : ch
|
||
))
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
const copyToClipboard = (text: string) => {
|
||
navigator.clipboard.writeText(text)
|
||
}
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
switch (status) {
|
||
case 'connected':
|
||
return <Badge className="bg-green-100 text-green-700"><CheckCircle2 className="h-3 w-3 mr-1" />已连接</Badge>
|
||
case 'disconnected':
|
||
return <Badge className="bg-red-100 text-red-700"><XCircle className="h-3 w-3 mr-1" />未连接</Badge>
|
||
default:
|
||
return <Badge className="bg-yellow-100 text-yellow-700"><RefreshCw className="h-3 w-3 mr-1" />配置中</Badge>
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||
<div className="p-6 space-y-6">
|
||
{/* 顶部标题 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">渠道配置</h1>
|
||
<p className="text-sm text-gray-500 mt-1">配置飞书、企微、微信等渠道,让AI助手可通过多渠道访问</p>
|
||
</div>
|
||
<Button onClick={() => setShowAddDialog(true)}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
添加渠道
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">接入渠道</p>
|
||
<p className="text-2xl font-bold">{channels.length}</p>
|
||
</div>
|
||
<Webhook className="h-8 w-8 text-blue-500 opacity-50" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">已连接</p>
|
||
<p className="text-2xl font-bold text-green-600">{channels.filter(c => c.status === 'connected').length}</p>
|
||
</div>
|
||
<CheckCircle2 className="h-8 w-8 text-green-500 opacity-50" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">今日消息</p>
|
||
<p className="text-2xl font-bold">{formatNumber(channels.reduce((sum, c) => sum + c.stats.todayMessages, 0))}</p>
|
||
</div>
|
||
<MessageSquare className="h-8 w-8 text-purple-500 opacity-50" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">累计消息</p>
|
||
<p className="text-2xl font-bold">{formatNumber(channels.reduce((sum, c) => sum + c.stats.totalMessages, 0))}</p>
|
||
</div>
|
||
<Send className="h-8 w-8 text-orange-500 opacity-50" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* API接入说明 */}
|
||
<Card className="border-0 shadow-sm bg-gradient-to-r from-blue-50 to-purple-50">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-start justify-between">
|
||
<div>
|
||
<h3 className="font-semibold text-gray-900 mb-1">🔗 API接入端点</h3>
|
||
<p className="text-sm text-gray-600 mb-2">第三方系统可通过以下接口与神射手Skill通信</p>
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<code className="bg-white px-3 py-1.5 rounded text-sm font-mono text-purple-600">
|
||
POST {apiBaseUrl}/chat
|
||
</code>
|
||
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(`${apiBaseUrl}/chat`)}>
|
||
<Copy className="h-3 w-3" />
|
||
</Button>
|
||
<span className="text-xs text-gray-500">智能对话</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<code className="bg-white px-3 py-1.5 rounded text-sm font-mono text-purple-600">
|
||
POST {apiBaseUrl}/query
|
||
</code>
|
||
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(`${apiBaseUrl}/query`)}>
|
||
<Copy className="h-3 w-3" />
|
||
</Button>
|
||
<span className="text-xs text-gray-500">用户查询</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Button variant="outline" size="sm">
|
||
<ExternalLink className="h-4 w-4 mr-1" />
|
||
API文档
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 渠道列表 */}
|
||
<div className="space-y-3">
|
||
{channels.map(channel => {
|
||
const typeConfig = CHANNEL_TYPES[channel.type]
|
||
|
||
return (
|
||
<Card key={channel.id} className="border-0 shadow-sm bg-white/80 hover:shadow-md transition-all">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
<div className={`w-12 h-12 rounded-xl ${typeConfig.color.split(' ')[0]} flex items-center justify-center text-2xl`}>
|
||
{typeConfig.icon}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="font-semibold text-gray-900">{channel.name}</h3>
|
||
<Badge className={typeConfig.color}>{typeConfig.name}</Badge>
|
||
{getStatusBadge(channel.status)}
|
||
</div>
|
||
<p className="text-sm text-gray-500 mt-0.5">{typeConfig.description}</p>
|
||
<div className="flex items-center gap-4 mt-1 text-xs text-gray-500">
|
||
<span>今日: {formatNumber(channel.stats.todayMessages)} 条</span>
|
||
<span>累计: {formatNumber(channel.stats.totalMessages)} 条</span>
|
||
{channel.stats.lastMessage && (
|
||
<span>最近消息: {channel.stats.lastMessage}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<Switch
|
||
checked={channel.enabled}
|
||
onCheckedChange={() => toggleChannel(channel.id)}
|
||
/>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => testConnection(channel.id)}
|
||
disabled={testLoading === channel.id}
|
||
>
|
||
{testLoading === channel.id ? (
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
) : (
|
||
<RefreshCw className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedChannel(channel)
|
||
setShowConfigDialog(true)
|
||
}}
|
||
>
|
||
<Settings className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 添加渠道弹窗 */}
|
||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||
<DialogContent className="max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>添加渠道</DialogTitle>
|
||
<DialogDescription>选择要接入的渠道类型</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="grid grid-cols-2 gap-3 py-4">
|
||
{Object.entries(CHANNEL_TYPES).map(([key, config]) => (
|
||
<div
|
||
key={key}
|
||
onClick={() => setNewChannelType(key as keyof typeof CHANNEL_TYPES)}
|
||
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
||
newChannelType === key ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-2">{config.icon}</div>
|
||
<h4 className="font-semibold text-gray-900">{config.name}</h4>
|
||
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{config.description}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>取消</Button>
|
||
<Button onClick={() => {
|
||
// 跳转到配置页面
|
||
setShowAddDialog(false)
|
||
setShowConfigDialog(true)
|
||
setSelectedChannel({
|
||
id: `ch_${newChannelType}_new`,
|
||
name: '',
|
||
type: newChannelType,
|
||
status: 'pending',
|
||
config: {},
|
||
stats: { totalMessages: 0, todayMessages: 0 },
|
||
createdAt: new Date().toISOString().split('T')[0],
|
||
enabled: false,
|
||
})
|
||
}}>
|
||
下一步:配置
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 配置弹窗 */}
|
||
<Dialog open={showConfigDialog} onOpenChange={setShowConfigDialog}>
|
||
<DialogContent className="max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>
|
||
{selectedChannel?.id.includes('new') ? '配置新渠道' : '编辑渠道配置'}
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
{selectedChannel && CHANNEL_TYPES[selectedChannel.type]?.description}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
{selectedChannel && (
|
||
<div className="space-y-4 py-4">
|
||
{/* 配置步骤指南 */}
|
||
<div className="p-3 rounded-lg bg-blue-50 border border-blue-100">
|
||
<h4 className="font-medium text-blue-900 mb-2 flex items-center gap-2">
|
||
📋 {CHANNEL_TYPES[selectedChannel.type].name} 配置步骤
|
||
</h4>
|
||
<ol className="text-xs text-blue-800 space-y-1">
|
||
{(CHANNEL_TYPES[selectedChannel.type] as any).steps?.map((step: string, i: number) => (
|
||
<li key={i} className="flex items-start gap-1">
|
||
<span>{step.replace('{apiBaseUrl}', apiBaseUrl || 'https://your-domain.com/api/shensheshou')}</span>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>渠道名称</Label>
|
||
<Input
|
||
placeholder={`例如:${CHANNEL_TYPES[selectedChannel.type].name}机器人`}
|
||
defaultValue={selectedChannel.name}
|
||
/>
|
||
</div>
|
||
|
||
{selectedChannel.type === 'feishu' && (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>App ID</Label>
|
||
<Input placeholder="cli_xxx" defaultValue={selectedChannel.config.appId} />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>App Secret</Label>
|
||
<Input type="password" placeholder="输入App Secret" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>Verification Token</Label>
|
||
<Input placeholder="用于验证请求来源" defaultValue={selectedChannel.config.token} />
|
||
</div>
|
||
<div className="p-3 rounded-lg bg-blue-50 text-sm">
|
||
<p className="font-medium text-blue-800 mb-1">📋 飞书配置步骤</p>
|
||
<ol className="text-blue-700 text-xs space-y-1 list-decimal list-inside">
|
||
<li>在飞书开放平台创建应用</li>
|
||
<li>添加机器人能力并设置权限</li>
|
||
<li>配置事件订阅回调地址</li>
|
||
<li>填入上述配置信息</li>
|
||
</ol>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{selectedChannel.type === 'wecom' && (
|
||
<>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>企业ID (CorpId)</Label>
|
||
<Input placeholder="ww_xxx" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>应用ID (AgentId)</Label>
|
||
<Input placeholder="1000001" />
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>应用Secret</Label>
|
||
<Input type="password" placeholder="输入应用Secret" />
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{selectedChannel.type === 'wechat' && (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>存客宝API Key</Label>
|
||
<Input placeholder="从存客宝获取" />
|
||
</div>
|
||
<div className="p-3 rounded-lg bg-green-50 text-sm">
|
||
<p className="font-medium text-green-800 mb-1">💡 微信通道说明</p>
|
||
<p className="text-green-700 text-xs">
|
||
微信私域通过存客宝SDK接入,需要在存客宝平台配置API密钥
|
||
</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{selectedChannel.type === 'api' && (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>API Key</Label>
|
||
<div className="flex gap-2">
|
||
<Input value="sk-archer-xxx-xxx-xxx" readOnly className="font-mono" />
|
||
<Button variant="outline" size="icon">
|
||
<Copy className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>调用示例</Label>
|
||
<pre className="p-3 rounded-lg bg-gray-900 text-green-400 text-xs overflow-x-auto">
|
||
{`curl -X POST ${apiBaseUrl}/chat \\
|
||
-H "Authorization: Bearer sk-archer-xxx" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{"message": "查询用户 13800138000"}'`}
|
||
</pre>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{selectedChannel.type === 'webhook' && (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>Webhook回调URL</Label>
|
||
<Input placeholder="https://your-server.com/webhook" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>签名密钥(可选)</Label>
|
||
<Input placeholder="用于验证回调请求" />
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowConfigDialog(false)}>取消</Button>
|
||
<Button onClick={() => {
|
||
setShowConfigDialog(false)
|
||
// 保存配置逻辑
|
||
}}>
|
||
保存配置
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|