import { Alert, Button, Col, Descriptions, Drawer, Empty, Form, Input, InputNumber, Modal, Row, Select, Space, Spin, Switch, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; import { Activity, BellRing, BotMessageSquare, CircleCheck, CirclePlus, FlaskConical, Link2, Pause, Pencil, Play, RefreshCw, Save, Sparkles, Trash2, UserRoundCheck, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { Metric, PageHeader, Surface } from '@/components/Page'; import { useBotAccess } from '@/contexts/BotAccess'; import { KnowledgeCandidatesPanel, KnowledgeSourcesPanel, } from '@/pages/BusinessKnowledgeSources'; import { apiRequest, jsonOptions } from '@/services/api'; import type { AssistantAccountSettings, AssistantConnection, AssistantConversation, AssistantConversationStatus, AssistantKnowledgeEntry, AssistantRuntimeStatus, AssistantUsage, Paged, } from '@/types'; type ProviderForm = { base_url: string; api_key?: string; model: string; timeout_seconds: number; max_output_tokens: number; }; type KnowledgeForm = Pick< AssistantKnowledgeEntry, 'question' | 'aliases' | 'keywords' | 'answer' | 'tags' | 'priority' | 'enabled' >; const emptyPage = (): Paged => ({ items: [], total: 0, page: 1, page_size: 20 }); const conversationLabels: Record = { auto: '自动接待', handoff: '待人工', human_paused: '人工接管', closed: '已关闭', }; const conversationColors: Record = { auto: 'success', handoff: 'error', human_paused: 'warning', closed: 'default', }; const pollingStateLabels: Record = { starting: '启动中', running: '运行中', retrying: '重试中', blocked: '已阻塞', stopped: '已停止', error: '异常', }; function pollingStateLabel(state?: string): string { if (!state) return '未知'; return pollingStateLabels[state] || state; } function pollingStateTone( state?: string, ): 'default' | 'info' | 'success' | 'warning' | 'danger' { if (state === 'running') return 'success'; if (state === 'starting') return 'info'; if (state === 'retrying' || state === 'blocked') return 'warning'; if (state === 'error') return 'danger'; return 'default'; } function connectionName(connection: AssistantConnection): string { const name = [connection.user.first_name, connection.user.last_name].filter(Boolean).join(' '); return name || (connection.user.username ? `@${connection.user.username}` : connection.user.id); } function customerName(conversation: AssistantConversation): string { const customer = conversation.customer; if (!customer) return conversation.chat_id; const name = [customer.first_name, customer.last_name].filter(Boolean).join(' '); return name || (customer.username ? `@${customer.username}` : customer.id); } export default function BusinessAssistantPage() { const { bot } = useBotAccess(); const [loading, setLoading] = useState(false); const [status, setStatus] = useState(null); const [connections, setConnections] = useState([]); const [selectedConnection, setSelectedConnection] = useState(''); const [settings, setSettings] = useState(null); const [usage, setUsage] = useState(null); const [knowledge, setKnowledge] = useState>(emptyPage()); const [conversations, setConversations] = useState>(emptyPage()); const [conversationStatus, setConversationStatus] = useState(''); const [knowledgeQuery, setKnowledgeQuery] = useState(''); const [knowledgeOpen, setKnowledgeOpen] = useState(false); const [knowledgeTarget, setKnowledgeTarget] = useState(null); const [conversationDetail, setConversationDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [testQuestion, setTestQuestion] = useState(''); const [testResult, setTestResult] = useState | null>(null); const [feishuTesting, setFeishuTesting] = useState(false); const [providerForm] = Form.useForm(); const [settingsForm] = Form.useForm(); const [knowledgeForm] = Form.useForm(); const feishuWebhookDraft = Form.useWatch('feishu_webhook_url', settingsForm); const provider = bot?.business_assistant; const reloadTopLevel = async () => { setLoading(true); try { const [statusValue, connectionPage] = await Promise.all([ apiRequest('/business-assistant/status'), apiRequest>('/business-assistant/connections?page=1&page_size=100'), ]); setStatus(statusValue); setConnections(connectionPage.items); setSelectedConnection((current) => current && connectionPage.items.some((item) => item.connection_id === current) ? current : connectionPage.items[0]?.connection_id || '', ); } catch (error) { message.error(error instanceof Error ? error.message : '智能接待状态加载失败'); setConnections([]); } finally { setLoading(false); } }; const loadConnectionData = async ( connectionId: string, page = 1, filters?: { knowledgeQuery?: string; conversationStatus?: string }, ) => { if (!connectionId) { setSettings(null); setKnowledge(emptyPage()); setConversations(emptyPage()); setUsage(null); return; } const params = new URLSearchParams({ connection_id: connectionId }); const knowledgeParams = new URLSearchParams({ connection_id: connectionId, query: filters?.knowledgeQuery ?? knowledgeQuery, page: String(page), page_size: '20', }); const conversationParams = new URLSearchParams({ connection_id: connectionId, status: filters?.conversationStatus ?? conversationStatus, page: String(page), page_size: '20', }); setLoading(true); try { const [settingsValue, knowledgeValue, conversationValue, usageValue] = await Promise.all([ apiRequest(`/business-assistant/settings?${params}`), apiRequest>(`/business-assistant/knowledge?${knowledgeParams}`), apiRequest>( `/business-assistant/conversations?${conversationParams}`, ), apiRequest(`/business-assistant/usage?${params}`), ]); setSettings(settingsValue); settingsForm.setFieldsValue(settingsValue); setKnowledge(knowledgeValue); setConversations(conversationValue); setUsage(usageValue); } catch (error) { message.error(error instanceof Error ? error.message : '连接数据加载失败'); } finally { setLoading(false); } }; useEffect(() => { providerForm.setFieldsValue({ base_url: provider?.base_url || '', model: provider?.model || '', timeout_seconds: provider?.timeout_seconds || 30, max_output_tokens: provider?.max_output_tokens || 600, api_key: '', }); if (bot?.bot_id) void reloadTopLevel(); }, [bot?.bot_id]); useEffect(() => { void loadConnectionData(selectedConnection); }, [selectedConnection, conversationStatus]); const activeConnection = useMemo( () => connections.find((item) => item.connection_id === selectedConnection) || null, [connections, selectedConnection], ); const saveProvider = (values: ProviderForm) => { if (!bot) return; Modal.confirm({ title: '更新模型配置并重启机器人?', content: 'API Key 留空会保留现有密钥。工作进程重启期间会短暂暂停接待。', okText: '确认保存', cancelText: '取消', onOk: async () => { await apiRequest( `/bots/${bot.bot_id}`, jsonOptions('PUT', { business_assistant: values, confirm: true, }), ); message.success('模型配置已保存,机器人正在重启'); window.dispatchEvent(new Event('bot-profiles-changed')); await reloadTopLevel(); }, }); }; const saveSettings = (values: AssistantAccountSettings) => { Modal.confirm({ title: '保存该账号的接待设置?', content: values.assistant_enabled ? '保存后自动回复将按新规则运行。' : '保存后将停止自动回复。', okText: '确认保存', cancelText: '取消', onOk: async () => { const payload = { ...values }; if (!payload.feishu_webhook_url?.trim()) delete payload.feishu_webhook_url; if (!payload.feishu_webhook_signing_secret?.trim()) { delete payload.feishu_webhook_signing_secret; } const saved = await apiRequest( '/business-assistant/settings', jsonOptions('PUT', { ...payload, connection_id: selectedConnection, confirm: true }), ); setSettings(saved); settingsForm.setFieldsValue(saved); message.success('接待设置已保存'); await reloadTopLevel(); }, }); }; const testFeishuWebhook = () => { const values = settingsForm.getFieldsValue([ 'feishu_webhook_url', 'feishu_webhook_signing_secret', ]); const payload: Record = { connection_id: selectedConnection, confirm: true, }; if (values.feishu_webhook_url?.trim()) { payload.feishu_webhook_url = values.feishu_webhook_url.trim(); } if (values.feishu_webhook_signing_secret?.trim()) { payload.feishu_webhook_signing_secret = values.feishu_webhook_signing_secret.trim(); } Modal.confirm({ title: '发送飞书测试提醒?', content: '系统会向当前填写或已保存的群机器人 Webhook 发送一条测试消息。', okText: '发送测试', cancelText: '取消', onOk: async () => { setFeishuTesting(true); try { await apiRequest( '/business-assistant/feishu-webhook/test', jsonOptions('POST', payload), ); message.success('飞书测试提醒发送成功'); } finally { setFeishuTesting(false); } }, }); }; const openKnowledge = (entry?: AssistantKnowledgeEntry) => { setKnowledgeTarget(entry || null); knowledgeForm.setFieldsValue( entry || { question: '', aliases: [], keywords: [], answer: '', tags: [], priority: 0, enabled: true, }, ); setKnowledgeOpen(true); }; const saveKnowledge = (values: KnowledgeForm) => { Modal.confirm({ title: knowledgeTarget ? '保存知识条目?' : '创建知识条目?', okText: '确认保存', cancelText: '取消', onOk: async () => { const path = knowledgeTarget ? `/business-assistant/knowledge/${knowledgeTarget.entry_id}` : '/business-assistant/knowledge'; await apiRequest( path, jsonOptions(knowledgeTarget ? 'PUT' : 'POST', { ...values, connection_id: selectedConnection, confirm: true, }), ); setKnowledgeOpen(false); message.success(knowledgeTarget ? '知识条目已更新' : '知识条目已创建'); await loadConnectionData(selectedConnection); }, }); }; const runKnowledgeTest = async () => { if (!testQuestion.trim()) return; try { const result = await apiRequest>( '/business-assistant/knowledge/test', jsonOptions('POST', { connection_id: selectedConnection, text: testQuestion }), ); setTestResult(result); } catch (error) { setTestResult(null); message.error(error instanceof Error ? error.message : '测试失败'); } }; const loadConversationDetail = async (conversation: AssistantConversation) => { setDetailLoading(true); setConversationDetail(conversation); try { setConversationDetail( await apiRequest( `/business-assistant/conversations/${conversation.conversation_id}`, ), ); } catch (error) { message.error(error instanceof Error ? error.message : '会话详情加载失败'); } finally { setDetailLoading(false); } }; const conversationAction = ( conversation: AssistantConversation, action: 'pause' | 'resume' | 'close' | 'clear', ) => { const labels = { pause: '暂停自动回复', resume: '恢复自动回复', close: '关闭会话', clear: '清空会话' }; Modal.confirm({ title: `${labels[action]}?`, content: action === 'clear' ? '原始消息和长期摘要都会被删除,无法恢复。' : undefined, okText: '确认执行', okButtonProps: action === 'clear' ? { danger: true } : undefined, cancelText: '取消', onOk: async () => { await apiRequest( `/business-assistant/conversations/${conversation.conversation_id}/actions`, jsonOptions('POST', { action, confirm: true }), ); message.success(`${labels[action]}成功`); if (conversationDetail?.conversation_id === conversation.conversation_id) { setConversationDetail(null); } await loadConnectionData(selectedConnection); }, }); }; const knowledgeColumns: ColumnsType = [ { title: '标准问题', dataIndex: 'question', minWidth: 220, render: (value: string, entry) => (
{value}
{entry.keywords.map((keyword) => {keyword})}
), }, { title: '标准答案', dataIndex: 'answer', ellipsis: true, minWidth: 280 }, { title: '优先级', dataIndex: 'priority', width: 88 }, { title: '状态', dataIndex: 'enabled', width: 88, render: (enabled: boolean) => {enabled ? '启用' : '停用'}, }, { title: '操作', key: 'actions', width: 112, fixed: 'right', render: (_, entry) => ( ), }, { title: '状态', dataIndex: 'status', width: 110, render: (value: AssistantConversationStatus) => ( {conversationLabels[value]} ), }, { title: '摘要', dataIndex: 'summary', ellipsis: true, minWidth: 280, render: (value) => value || '暂无摘要' }, { title: '转人工原因', dataIndex: 'handoff_reason', width: 180, render: (value) => value || '—' }, { title: '最近消息', dataIndex: 'last_message_at', width: 160, render: (value: string) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—'), }, { title: '操作', key: 'actions', fixed: 'right', width: 112, render: (_, conversation) => ( {conversation.status === 'auto' ? ( } />
} tone={pollingStateTone(status?.polling_state)} /> } tone="info" /> } /> } tone={status?.usage.handoffs ? 'warning' : 'default'} />
{status?.last_error ? ( ) : null} {status?.webhook_conflict ? ( ) : null}
{provider?.configured ? '模型已配置' : '模型未配置'} {bot?.identity?.can_connect_to_business ? 'BotFather Business Mode 已开启' : '需要在 BotFather 开启 Business Mode'}
({ value: connection.connection_id, label: connectionName(connection), }))} onChange={setSelectedConnection} /> ) : undefined} > {!connections.length ? ( 尚无连接。请先在 BotFather 开启 Business Mode,再由账号本人进入 “设置 → Telegram Business/聊天自动化 → Chatbots”连接当前 Bot。 } /> ) : activeConnection ? ( {connectionName(activeConnection)} {activeConnection.is_enabled ? '已连接' : '已停用'} {activeConnection.rights.can_reply ? '允许回复' : '未授权'} {activeConnection.rights.can_read_messages ? '允许标记已读' : '未授权'} {activeConnection.last_event_at ? dayjs(activeConnection.last_event_at).format('YYYY-MM-DD HH:mm:ss') : '—'} ) : null} {activeConnection && settings ? ( <>
消息提醒 {settings.feishu_webhook_configured ? 'Webhook 已配置' : 'Webhook 未配置'} {settings.feishu_signing_secret_configured ? 签名已配置 : null} ({ validator: (_, value) => { if (getFieldValue('feishu_webhook_enabled') && !value && !settings.feishu_webhook_configured) { return Promise.reject(new Error('开启提醒前请填写 Webhook 地址')); } return Promise.resolve(); }, }), ]} >
), }, { key: 'knowledge', label: `知识库 (${knowledge.total})`, children: ( { setKnowledgeQuery(value); void loadConnectionData(selectedConnection, 1, { knowledgeQuery: value }); }} /> } >
void loadConnectionData(selectedConnection, page) }} scroll={{ x: 900 }} locale={{ emptyText: '尚无知识条目' }} />
测试问答 setTestQuestion(event.target.value)} placeholder="输入客户可能提出的问题" onPressEnter={() => void runKnowledgeTest()} /> {testResult ? ( ) : null}
), }, { key: 'knowledge-sources', label: '知识来源', children: , }, { key: 'knowledge-candidates', label: '知识候选', children: ( loadConnectionData(selectedConnection)} /> ), }, { key: 'conversations', label: `会话台 (${conversations.total})`, children: ( ({ value, label }))} onChange={(value) => setConversationStatus(value || '')} /> } >
void loadConnectionData(selectedConnection, page) }} scroll={{ x: 980 }} locale={{ emptyText: '暂无客户会话' }} /> ), }, ]} /> ) : null} setKnowledgeOpen(false)} onOk={() => knowledgeForm.submit()} okText="继续" cancelText="取消" width={720} destroyOnHidden>