接入tgbot

This commit is contained in:
mtvpls
2026-06-29 16:40:03 +08:00
parent b1aad6a9bc
commit 5d605710f3
27 changed files with 2079 additions and 11 deletions
+252
View File
@@ -44,6 +44,7 @@ import {
Palette,
Plus,
Search,
Send,
Settings,
Smartphone,
Tablet,
@@ -13939,6 +13940,241 @@ const XiaoyaConfigComponent = ({
);
};
// Telegram Bot 配置组件
const TelegramConfigComponent = ({
config,
refreshConfig,
}: {
config: AdminConfig | null;
refreshConfig: () => Promise<void>;
}) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [enabled, setEnabled] = useState(false);
const [botToken, setBotToken] = useState('');
const [botUsername, setBotUsername] = useState('');
const [webhookSecret, setWebhookSecret] = useState('');
const [apiProxy, setApiProxy] = useState('');
const [apiBaseUrl, setApiBaseUrl] = useState('');
const [loginEnabled, setLoginEnabled] = useState(true);
const [bindingEnabled, setBindingEnabled] = useState(true);
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const [defaultNotifications, setDefaultNotifications] = useState(true);
const [testChatId, setTestChatId] = useState('');
useEffect(() => {
const telegram = config?.TelegramConfig;
if (telegram) {
setEnabled(Boolean(telegram.enabled));
setBotToken(telegram.botToken || '');
setBotUsername(telegram.botUsername || '');
setWebhookSecret(telegram.webhookSecret || '');
setApiProxy(telegram.apiProxy || '');
setApiBaseUrl(telegram.apiBaseUrl || '');
setLoginEnabled(telegram.loginEnabled !== false);
setBindingEnabled(telegram.bindingEnabled !== false);
setNotificationsEnabled(telegram.notificationsEnabled !== false);
setDefaultNotifications(telegram.defaultNotifications !== false);
}
}, [config]);
const buildConfig = (): AdminConfig['TelegramConfig'] => ({
enabled,
botToken,
botUsername: botUsername.replace(/^@/, ''),
webhookSecret,
apiProxy,
apiBaseUrl,
loginEnabled,
bindingEnabled,
notificationsEnabled,
defaultNotifications,
});
const handleSave = async () => {
await withLoading('saveTelegram', async () => {
try {
const response = await fetch('/api/admin/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'save', config: buildConfig() }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || '保存失败');
showSuccess('Telegram 配置保存成功', showAlert);
await refreshConfig();
} catch (error) {
showError(error instanceof Error ? error.message : '保存失败', showAlert);
throw error;
}
});
};
const handleSetWebhook = async () => {
await withLoading('setTelegramWebhook', async () => {
try {
if (!enabled || !botToken.trim() || !botUsername.trim() || !webhookSecret.trim()) {
throw new Error('请先填写 Bot Token、Bot 用户名 和 Webhook Secret');
}
const webhookUrlValue = webhookSecret === '******'
? ''
: `${window.location.origin}/api/telegram/webhook/${webhookSecret}`;
const response = await fetch('/api/admin/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'set_webhook',
config: buildConfig(),
webhookUrl: webhookUrlValue,
origin: window.location.origin,
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const telegramDetail = data.telegram
? `HTTP ${data.telegram.status || '-'},响应:${data.telegram.body || data.telegram.statusText || '-'}`
: '';
throw new Error(`${data.error || 'Webhook 设置失败'}${telegramDetail}`);
}
showSuccess('Webhook 设置成功', showAlert);
} catch (error) {
showError(error instanceof Error ? error.message : 'Webhook 设置失败', showAlert);
throw error;
}
});
};
const handleTest = async () => {
if (!testChatId.trim()) {
showError('请输入测试 Chat ID', showAlert);
return;
}
await withLoading('testTelegram', async () => {
try {
const response = await fetch('/api/admin/telegram', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'test', config: buildConfig(), testChatId: testChatId.trim() }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || '发送失败');
showSuccess('测试消息发送成功', showAlert);
} catch (error) {
showError(error instanceof Error ? error.message : '发送失败', showAlert);
throw error;
}
});
};
const webhookUrl = webhookSecret
? `${typeof window !== 'undefined' ? window.location.origin : ''}/api/telegram/webhook/${webhookSecret === '******' ? '<secret>' : webhookSecret}`
: '';
return (
<div className='space-y-6'>
<div className='bg-sky-50 dark:bg-sky-900/20 border border-sky-200 dark:border-sky-800 rounded-lg p-4'>
<h3 className='text-sm font-medium text-sky-900 dark:text-sky-100 mb-2'>
Telegram Bot
</h3>
<div className='text-sm text-sky-800 dark:text-sky-200 space-y-1'>
<p> Telegram</p>
<p> Webhook Telegram Bot API </p>
<p> Bot Token Webhook Secret </p>
</div>
</div>
<div className='space-y-4'>
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
<div>
<h3 className='text-sm font-medium text-gray-900 dark:text-white'> Telegram Bot</h3>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'> Telegram </p>
</div>
<button
onClick={() => setEnabled(!enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>Bot Token *</label>
<input type='password' value={botToken} onChange={(e) => setBotToken(e.target.value)} placeholder='123456:ABC...' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>Bot *</label>
<input type='text' value={botUsername} onChange={(e) => setBotUsername(e.target.value)} placeholder='your_bot' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
</div>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>Webhook Secret</label>
<input type='password' value={webhookSecret} onChange={(e) => setWebhookSecret(e.target.value)} placeholder='建议填写随机长字符串' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
{webhookUrl && <p className='mt-2 break-all text-xs text-gray-500 dark:text-gray-400'>Webhook URL{webhookUrl}</p>}
<div className='mt-3 flex flex-col gap-2 sm:flex-row'>
<button onClick={handleSetWebhook} disabled={isLoading('setTelegramWebhook')} className={`w-full sm:w-auto ${buttonStyles.primary}`}>{isLoading('setTelegramWebhook') ? '设置中...' : '一键设置 Webhook'}</button>
</div>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'></label>
<input type='text' value={apiProxy} onChange={(e) => setApiProxy(e.target.value)} placeholder='http://127.0.0.1:7890' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>Node Cloudflare/Edge </p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'> Base URL</label>
<input type='text' value={apiBaseUrl} onChange={(e) => setApiBaseUrl(e.target.value)} placeholder='https://telegram-api.example.com' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> https://api.telegram.org。</p>
</div>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3 rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
{[
['允许绑定', bindingEnabled, setBindingEnabled],
['允许 Telegram 登录', loginEnabled, setLoginEnabled],
['启用 Telegram 通知', notificationsEnabled, setNotificationsEnabled],
['新绑定默认开启通知', defaultNotifications, setDefaultNotifications],
].map(([label, value, setter]) => (
<label key={label as string} className='flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300'>
<input type='checkbox' checked={value as boolean} onChange={(e) => (setter as (value: boolean) => void)(e.target.checked)} />
{label as string}
</label>
))}
</div>
<div className='rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'> Chat ID</label>
<div className='flex flex-col gap-2 sm:flex-row'>
<input type='text' value={testChatId} onChange={(e) => setTestChatId(e.target.value)} placeholder='用户或群组 chat_id' className='min-w-0 flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white' />
<button onClick={handleTest} disabled={isLoading('testTelegram')} className={`w-full shrink-0 sm:w-auto ${buttonStyles.primary}`}>{isLoading('testTelegram') ? '发送中...' : '测试'}</button>
</div>
</div>
<div className='flex justify-end'>
<button onClick={handleSave} disabled={isLoading('saveTelegram')} className={buttonStyles.success}>
{isLoading('saveTelegram') ? '保存中...' : '保存配置'}
</button>
</div>
</div>
<AlertModal
isOpen={alertModal.isOpen}
onClose={hideAlert}
type={alertModal.type}
title={alertModal.title}
message={alertModal.message}
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
</div>
);
};
// 邮件配置组件
const EmailConfigComponent = ({
config,
@@ -16757,6 +16993,7 @@ function AdminPageClient() {
customAdFilter: false,
themeConfig: false,
emailConfig: false,
telegramConfig: false,
});
// 获取管理员配置
@@ -17392,6 +17629,21 @@ function AdminPageClient() {
/>
</CollapsibleTab>
{/* Telegram Bot 配置标签 */}
<CollapsibleTab
title='Telegram Bot'
icon={
<Send size={20} className='text-gray-600 dark:text-gray-400' />
}
isExpanded={expandedTabs.telegramConfig}
onToggle={() => toggleTab('telegramConfig')}
>
<TelegramConfigComponent
config={config}
refreshConfig={fetchConfig}
/>
</CollapsibleTab>
{/* 分类配置标签 */}
<CollapsibleTab
title='分类配置'