import { useEffect, useState } from 'react'; import { Avatar, Button, Checkbox, Descriptions, Drawer, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { WECOM_BOT_PERMISSION_GROUPS, WECOM_BOT_PERMISSION_LABELS, WECOM_BOT_ROLE_DEFAULT_PERMISSIONS, WECOM_BOT_ROLE_LABELS, WECOM_BOT_ROLES, type LlmApiConfigOptionDto, type KnowledgeBaseOptionDto, type WecomAibotRuntimeDto, type WecomBotDto, type WecomBotPermission, type WecomBotRole, } from '@dukang/shared-types'; import { request } from '../lib/api'; import { fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; import OssUpload from '../components/OssUpload'; type FormValues = { name: string; role: WecomBotRole; botId: string; secret?: string; avatarUrl?: string; welcome?: string; permissions: WecomBotPermission[]; reviewSuperAdminWecomUserIds?: string; aiEnabled: boolean; llmConfigId?: string | null; knowledgeBaseId?: string | null; enabled: boolean; sortOrder: number; }; function parseUserIdLines(text?: string): string[] { if (!text?.trim()) return []; return [...new Set(text.split(/[,,\s\n]+/).map((s) => s.trim()).filter(Boolean))]; } function formatUserIdLines(ids?: string[]): string { return (ids ?? []).join('\n'); } function mapDtoToForm(row: WecomBotDto): FormValues { return { name: row.name, role: row.role, botId: row.botId, secret: '', avatarUrl: row.avatarUrl || '', welcome: row.welcome || '', permissions: [...row.permissions], reviewSuperAdminWecomUserIds: formatUserIdLines(row.reviewSuperAdminWecomUserIds), aiEnabled: row.aiEnabled, llmConfigId: row.llmConfigId, knowledgeBaseId: row.knowledgeBaseId, enabled: row.enabled, sortOrder: row.sortOrder, }; } function samePermissionSet(a: WecomBotPermission[], b: WecomBotPermission[]): boolean { if (a.length !== b.length) return false; const setB = new Set(b); return a.every((p) => setB.has(p)); } const CREATE_DEFAULTS: FormValues = { name: '', role: 'CUSTOMER_SERVICE', botId: '', secret: '', avatarUrl: '', welcome: '', permissions: [...WECOM_BOT_ROLE_DEFAULT_PERMISSIONS.CUSTOMER_SERVICE], reviewSuperAdminWecomUserIds: '', aiEnabled: false, llmConfigId: null, knowledgeBaseId: null, enabled: true, sortOrder: 0, }; export default function WecomBotsPage() { const [filterForm] = Form.useForm(); const [form] = Form.useForm(); const [filters, setFilters] = useState>({}); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [formInitial, setFormInitial] = useState(null); const [formReady, setFormReady] = useState(false); const [saving, setSaving] = useState(false); const [detail, setDetail] = useState(null); const [runtime, setRuntime] = useState(); const [llmOptions, setLlmOptions] = useState([]); const [kbOptions, setKbOptions] = useState([]); const roleWatch = Form.useWatch('role', form); const permissionsWatch = Form.useWatch('permissions', form) as WecomBotPermission[] | undefined; const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/wecom-bots', () => { const qs = new URLSearchParams(); if (filters.name) qs.set('name', filters.name); if (filters.role) qs.set('role', filters.role); if (filters.enabled) qs.set('enabled', filters.enabled); return qs; }, [filters], ); const loadRuntime = () => request('/admin/wecom-bots/runtime') .then(setRuntime) .catch(() => {}); useEffect(() => { void loadRuntime(); }, [data]); useEffect(() => { void request<{ llmConfigs: LlmApiConfigOptionDto[]; knowledgeBases: KnowledgeBaseOptionDto[]; }>('/admin/wecom-bots/ai-options') .then((res) => { setLlmOptions(res.llmConfigs); setKbOptions(res.knowledgeBases); }) .catch(() => { setLlmOptions([]); setKbOptions([]); }); }, []); useEffect(() => { if (!modalOpen || !formReady || !formInitial) return; form.resetFields(); form.setFieldsValue(formInitial); }, [modalOpen, formReady, formInitial, form]); function closeModal() { setModalOpen(false); setFormReady(false); setFormInitial(null); setEditing(null); form.resetFields(); } function openCreate() { setEditing(null); setFormInitial({ ...CREATE_DEFAULTS }); setFormReady(true); setModalOpen(true); } async function openEdit(row: WecomBotDto) { setEditing(row); setFormInitial(null); setFormReady(false); setModalOpen(true); try { const detail = await request(`/admin/wecom-bots/${row.id}`); setEditing(detail); setFormInitial(mapDtoToForm(detail)); } catch (e) { message.error(e instanceof Error ? e.message : '加载机器人详情失败'); setFormInitial(mapDtoToForm(row)); } finally { setFormReady(true); } } async function submit() { const values = await form.validateFields(); const permissions = (form.getFieldValue('permissions') ?? values.permissions) as WecomBotPermission[]; if (!permissions?.length) { message.error('请至少选择一项权限'); return; } setSaving(true); try { const payload = { name: values.name.trim(), role: values.role, botId: values.botId.trim(), avatarUrl: values.avatarUrl?.trim() || null, welcome: values.welcome?.trim() || null, permissions, reviewSuperAdminWecomUserIds: parseUserIdLines(values.reviewSuperAdminWecomUserIds), aiEnabled: values.aiEnabled, llmConfigId: values.llmConfigId || null, knowledgeBaseId: values.knowledgeBaseId || null, enabled: values.enabled, sortOrder: values.sortOrder, }; if (editing) { const saved = await request(`/admin/wecom-bots/${editing.id}`, { method: 'PUT', body: JSON.stringify({ ...payload, secret: values.secret?.trim() || undefined, }), }); if (!samePermissionSet(permissions, saved.permissions)) { message.warning('权限与保存结果不一致,已用服务端数据刷新表单'); setFormInitial(mapDtoToForm(saved)); setEditing(saved); reload(); return; } message.success('已更新'); } else { if (!values.secret?.trim()) { message.error('请填写 Secret'); return; } await request('/admin/wecom-bots', { method: 'POST', body: JSON.stringify({ ...payload, secret: values.secret.trim(), }), }); message.success('已创建'); } closeModal(); reload(); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } } async function remove(id: string) { try { await request(`/admin/wecom-bots/${id}`, { method: 'DELETE' }); message.success('已删除'); reload(); } catch (e) { message.error(e instanceof Error ? e.message : '删除失败'); } } async function reloadConnections() { try { const st = await request('/admin/wecom-bots/reload', { method: 'POST', body: '{}', }); message.success('已重载长连接'); setRuntime(st); reload(); } catch (e) { message.error(e instanceof Error ? e.message : '重载失败'); } } const runtimeMap = new Map((runtime?.bots ?? []).map((b) => [b.id, b])); const columns: ColumnsType = [ { title: '头像', dataIndex: 'avatarUrl', width: 64, render: (url: string | null, row) => ( {row.name.slice(0, 1)} ), }, { title: '名称', dataIndex: 'name', width: 140, ellipsis: true }, { title: '角色', dataIndex: 'role', width: 120, render: (r: WecomBotRole) => WECOM_BOT_ROLE_LABELS[r] || r, }, { title: 'BotID', dataIndex: 'botId', width: 160, ellipsis: true }, { title: '权限', dataIndex: 'permissions', ellipsis: true, render: (perms: WecomBotPermission[]) => perms.map((p) => ( {WECOM_BOT_PERMISSION_LABELS[p] || p} )), }, { title: 'AI', width: 100, render: (_, row) => row.aiEnabled ? ( {row.llmConfigName || '已开'} ) : ( ), }, { title: '启用', dataIndex: 'enabled', width: 70, render: (v: boolean) => {v ? '是' : '否'}, }, { title: '连接', width: 80, render: (_, row) => { const rt = runtimeMap.get(row.id); if (!runtime?.masterEnabled) return 总开关关; if (!row.enabled) return 未启用; return ( {rt?.connected ? '已连接' : '未连接'} ); }, }, { title: '更新', dataIndex: 'updatedAt', width: 160, render: fmtTime }, { title: '操作', width: 180, fixed: 'right', render: (_, row) => ( remove(row.id)}> ), }, ]; return (
企微机器人 并列创建多个智能机器人,配置 BotID / Secret / 权限 / 角色 / 头像。总开关在「系统设置 → 功能开关」。 {runtime ? ` 当前总开关:${runtime.masterEnabled ? '开' : '关'}` : ''}
{ setFilters(v); setPage(1); }} >
{ setPage(p); setPageSize(ps); }, }} /> void submit()} confirmLoading={saving} width={640} destroyOnClose > {formReady && formInitial ? (
v?.length ? Promise.resolve() : Promise.reject(new Error('请至少选择一项权限')), }, ]} extra={ <> {roleWatch ? `角色默认:${WECOM_BOT_ROLE_DEFAULT_PERMISSIONS[roleWatch as WecomBotRole]?.map((p) => WECOM_BOT_PERMISSION_LABELS[p]).join('、') || '无'}` : null} {permissionsWatch?.length ? ( · 已选 {permissionsWatch.length} 项 ) : null} } > {WECOM_BOT_PERMISSION_GROUPS.map((group) => (
{group.label}
{group.permissions.map((p) => ( {WECOM_BOT_PERMISSION_LABELS[p]} ))}
))}
{(roleWatch === 'TECH_SUPPORT' || permissionsWatch?.includes('support_ticket.review')) && ( )} ({ value: o.id, label: `${o.name}(${o.documentCount} 篇)`, }))} /> ) : ( 加载中… )}
setDetail(null)}> {detail && ( {detail.name.slice(0, 1)} {detail.name} {WECOM_BOT_ROLE_LABELS[detail.role] || detail.role} {detail.botId} {detail.secretConfigured ? '已配置' : '未配置'} {detail.permissions.map((p) => WECOM_BOT_PERMISSION_LABELS[p] || p).join('、')} {detail.reviewSuperAdminWecomUserIds?.length ? detail.reviewSuperAdminWecomUserIds.join('、') : '—'} {detail.welcome || '—'} {detail.aiEnabled ? '开' : '关'} {detail.llmConfigName || '—'} {detail.knowledgeBaseName || '—'} {detail.enabled ? '是' : '否'} {detail.sortOrder} {fmtTime(detail.createdAt)} {fmtTime(detail.updatedAt)} )} ); }