import { Alert, Button, Checkbox, Col, Descriptions, Form, Input, InputNumber, Modal, Row, Select, Space, Switch, Table, Tag, Tooltip, Typography, message, } from 'antd'; import { Bot, CirclePlus, KeyRound, Pencil, PlugZap, RefreshCw, Save, ShieldCheck, Trash2, } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { PageHeader, Surface } from '@/components/Page'; import { apiRequest, jsonOptions, setCsrfToken, setSelectedBotId } from '@/services/api'; import type { AdminUser, BotPermissionDefinition, BotProfile, BotRole, TelegramSettings, } from '@/types'; interface SystemSettings { web_address: string; bind_host: string; upload_limit_mb: number; supervisor_mode: boolean; bot: { connected: boolean; id: string; username: string; name: string }; } interface BotFormValues { label: string; bot_token?: string; enabled: boolean; sudo_users_id?: string; log_group_id?: string; gban_log_group_id?: string; message_dump_chat?: string; role_ids: string[]; } interface RoleFormValues { name: string; description?: string; permissions: string[]; } const TELEGRAM_APP_URL = 'https://my.telegram.org/apps'; const API_HASH_PATTERN = /^[A-Fa-f0-9]{32}$/; function runtimeTag(profile: BotProfile) { if (!profile.enabled) return 已停用; if (!profile.ready_to_connect) return 配置未完成; const state = profile.runtime?.state || 'stopped'; const values: Record = { running: { color: 'success', text: '运行中' }, starting: { color: 'processing', text: '启动中' }, stopping: { color: 'processing', text: '停止中' }, failed: { color: 'error', text: '启动失败' }, stopped: { color: 'default', text: '已停止' }, }; const current = values[state] || values.stopped; return ( {current.text} ); } export default function SettingsPage() { const [settings, setSettings] = useState(null); const [telegram, setTelegram] = useState(null); const [editingBot, setEditingBot] = useState(null); const [botModalOpen, setBotModalOpen] = useState(false); const [editingRole, setEditingRole] = useState(null); const [roleModalOpen, setRoleModalOpen] = useState(false); const [globalForm] = Form.useForm(); const [botForm] = Form.useForm(); const [roleForm] = Form.useForm(); const [passwordForm] = Form.useForm(); const reloadTelegram = useCallback(async () => { const current = await apiRequest('/settings/telegram'); setTelegram(current); globalForm.setFieldsValue({ api_id: current.api_id || undefined, api_hash: '' }); window.dispatchEvent(new Event('bot-profiles-changed')); }, [globalForm]); useEffect(() => { apiRequest('/settings').then(setSettings).catch((error) => message.error(error.message)); reloadTelegram().catch((error) => message.error(error.message)); }, [reloadTelegram]); const openBotModal = (profile?: BotProfile) => { const target = profile || null; setEditingBot(target); botForm.setFieldsValue( target ? { label: target.label, bot_token: '', enabled: target.enabled, sudo_users_id: target.sudo_users_id.join(' '), log_group_id: target.log_group_id, gban_log_group_id: target.gban_log_group_id, message_dump_chat: target.message_dump_chat, role_ids: target.role_ids || ['full_access'], } : { label: '', bot_token: '', enabled: true, sudo_users_id: '', log_group_id: '0', gban_log_group_id: '0', message_dump_chat: '0', role_ids: ['full_access'], }, ); setBotModalOpen(true); }; const saveBot = (values: BotFormValues) => { Modal.confirm({ title: editingBot ? '保存并重启该机器人?' : '创建并启动该机器人?', content: '启用且配置完整时,系统会立即连接 Telegram。', okText: '确认保存', cancelText: '取消', onOk: async () => { const path = editingBot ? `/bots/${editingBot.bot_id}` : '/bots'; const method = editingBot ? 'PUT' : 'POST'; const saved = await apiRequest( path, jsonOptions(method, { ...values, confirm: true }), ); setBotModalOpen(false); message.success(editingBot ? '机器人配置已更新' : '机器人已创建'); await reloadTelegram(); if (!editingBot) setSelectedBotId(saved.bot_id); }, }); }; const openRoleModal = (role?: BotRole) => { const target = role || null; setEditingRole(target); roleForm.setFieldsValue( target ? { name: target.name, description: target.description, permissions: target.permissions, } : { name: '', description: '', permissions: [], }, ); setRoleModalOpen(true); }; const saveRole = (values: RoleFormValues) => { Modal.confirm({ title: editingRole ? '保存角色权限?' : '创建职责角色?', content: editingRole ? '使用该角色的机器人会自动重启并应用新权限。' : '创建后可将角色分配给一个或多个机器人。', okText: '确认保存', cancelText: '取消', onOk: async () => { const path = editingRole ? `/roles/${editingRole.role_id}` : '/roles'; const method = editingRole ? 'PUT' : 'POST'; await apiRequest( path, jsonOptions(method, { ...values, confirm: true }), ); setRoleModalOpen(false); message.success(editingRole ? '角色权限已更新' : '职责角色已创建'); await reloadTelegram(); }, }); }; const roleNames = new Map( (telegram?.roles || []).map((role) => [role.role_id, role.name]), ); const permissionNames = new Map( (telegram?.permission_catalog || []).map((permission) => [ permission.key, permission.name, ]), ); const permissionGroups = (telegram?.permission_catalog || []).reduce< Record >((groups, permission) => { (groups[permission.group] ||= []).push(permission); return groups; }, {}); const botColumns = [ { title: '机器人', key: 'bot', render: (_: unknown, profile: BotProfile) => (
{profile.label}
{profile.identity?.username ? `@${profile.identity.username}` : '尚未验证身份'}
), }, { title: '令牌', dataIndex: 'bot_token_configured', width: 96, render: (configured: boolean) => ( {configured ? '已配置' : '未配置'} ), }, { title: '连接状态', key: 'runtime', width: 120, render: (_: unknown, profile: BotProfile) => runtimeTag(profile), }, { title: '职责角色', dataIndex: 'role_ids', minWidth: 180, render: (roleIds: string[]) => ( {(roleIds || []).map((roleId) => ( {roleNames.get(roleId) || roleId} ))} {!roleIds?.length ? 未分配职责 : null} ), }, { title: '媒体中转群', dataIndex: 'message_dump_chat', width: 150, render: (value: string) => (value && value !== '0' ? value : '未配置'), }, { title: '操作', key: 'actions', width: 190, fixed: 'right' as const, render: (_: unknown, profile: BotProfile) => ( } >
Modal.confirm({ title: '更新 Telegram 应用接口凭据?', content: '所有启用的机器人会依次重启并应用新配置。', okText: '确认更新', cancelText: '取消', onOk: async () => { await apiRequest( '/settings/telegram', jsonOptions('PUT', { ...values, confirm: true }), ); message.success('Telegram 应用接口配置已更新'); await reloadTelegram(); }, }) } > 来自{' '} my.telegram.org/apps ,不是机器人编号。 } rules={[{ required: true, message: '请输入 Telegram 应用编号' }]} >
{!telegram?.api_ready ? ( ) : null}
rowKey="bot_id" columns={botColumns} dataSource={telegram?.bots || []} pagination={false} scroll={{ x: 980 }} locale={{ emptyText: '尚未添加机器人' }} />
} onClick={() => openRoleModal()}> 新建角色 } >
rowKey="role_id" columns={roleColumns} dataSource={telegram?.roles || []} pagination={false} scroll={{ x: 760 }} />
{settings ? ( {settings.web_address} {settings.bind_host} {settings.upload_limit_mb} MB {settings.supervisor_mode ? '多机器人监管' : '机器人工作进程'} ) : null}
{ const user = await apiRequest( '/auth/password', jsonOptions('PUT', values), ); setCsrfToken(user.csrf_token); passwordForm.resetFields(); message.success('管理员密码已更新,其他会话已失效'); }} >
本地 0600 文件 scrypt + 独立盐 跨站请求防护校验 setBotModalOpen(false)} onOk={() => botForm.submit()} okText="继续" cancelText="取消" destroyOnHidden >
setRoleModalOpen(false)} onOk={() => roleForm.submit()} okText="继续" cancelText="取消" width={720} destroyOnHidden >
{Object.entries(permissionGroups).map(([group, permissions]) => (
{group} {permissions.map((permission) => ( {permission.name} {permission.description} ))}
))}
); }