feat: 添加 Telegram 通知支持,更新配置和文档
This commit is contained in:
@@ -5,6 +5,9 @@ BALANCE_REFRESH_CRON=0 * * * *
|
||||
WEBHOOK_URL=
|
||||
BARK_URL=
|
||||
SERVERCHAN_KEY=
|
||||
TELEGRAM_ENABLED=false
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
PORT=4000
|
||||
DATA_DIR=./data
|
||||
NOTIFY_COOLDOWN_SEC=300
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
|
||||
- **自动签到**:Cron 定时执行,智能解析奖励金额,签到失败自动通知
|
||||
- **余额管理**:定时刷新,收入追踪,余额兜底估算,凭证过期自动重登录
|
||||
- **告警通知**:支持 Webhook / Bark / Server酱 / SMTP 邮件,覆盖余额不足、站点异常、签到失败等场景
|
||||
- **告警通知**:支持 Webhook / Bark / Server酱 / Telegram Bot / SMTP 邮件,覆盖余额不足、站点异常、签到失败等场景
|
||||
- **数据看板**:余额分布饼图、消费趋势图表、全局搜索、代理请求日志
|
||||
|
||||
<div align="center">
|
||||
|
||||
+3
-2
@@ -228,13 +228,14 @@ Each adapter supports: account login, balance queries, model enumeration, token
|
||||
|
||||
### Alerts & Notifications
|
||||
|
||||
Four notification channels supported:
|
||||
Five notification channels supported:
|
||||
|
||||
| Channel | Description |
|
||||
| --- | --- |
|
||||
| **Webhook** | Custom HTTP push |
|
||||
| **Bark** | iOS push notifications |
|
||||
| **ServerChan** | WeChat / Telegram notifications |
|
||||
| **ServerChan** | WeChat notifications |
|
||||
| **Telegram Bot** | Telegram message notifications |
|
||||
| **SMTP Email** | Standard email notifications |
|
||||
|
||||
Alert scenarios: low balance warning, site/account anomalies, check-in failures, proxy request failures, token expiry reminders, daily summary reports. Alert cooldown mechanism (default: 300 seconds) prevents duplicate notifications.
|
||||
|
||||
@@ -10,6 +10,9 @@ PROXY_TOKEN=change-me-proxy-sk-token
|
||||
PORT=4000
|
||||
NOTIFY_COOLDOWN_SEC=300
|
||||
ADMIN_IP_ALLOWLIST=
|
||||
TELEGRAM_ENABLED=false
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
|
||||
# Auto check-in cron (default: 8:00 AM every day)
|
||||
CHECKIN_CRON=0 8 * * *
|
||||
|
||||
@@ -123,6 +123,24 @@ Metapi 的路由引擎按多因子加权选择最优通道。
|
||||
| `SERVERCHAN_ENABLED` | 启用 Server酱 通知 | `true` |
|
||||
| `SERVERCHAN_KEY` | Server酱 SendKey | 空 |
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `TELEGRAM_ENABLED` | 启用 Telegram 通知 | `false` |
|
||||
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token(形如 `123456:abc`) | 空 |
|
||||
| `TELEGRAM_CHAT_ID` | 接收消息的 Chat ID(如 `-100xxxx` 或 `@channel`) | 空 |
|
||||
|
||||
**配置步骤:**
|
||||
|
||||
1. **创建 Bot**:在 Telegram 中搜索 [@BotFather](https://t.me/BotFather),发送 `/newbot`,按提示设置名称后获取 Bot Token(格式如 `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
|
||||
2. **获取 Chat ID**:
|
||||
- **个人聊天**:向你的 Bot 发送任意消息,然后访问 `https://api.telegram.org/bot<你的Token>/getUpdates`,在返回的 JSON 中找到 `chat.id`,或者在 Telegram 搜索 @userinfobot 或是 @getmyid_bot。点击 Start,它会立刻回复一串数字(通常是 9 到 10 位)。
|
||||
- **群组**:将 Bot 邀请进群组,在群内发送消息后同样通过 `getUpdates` 接口获取群组 Chat ID(通常为负数,如 `-1001234567890`)
|
||||
- **频道**:使用频道用户名,如 `@your_channel`(需先将 Bot 添加为频道管理员)
|
||||
3. **填入配置**:将获取的 Token 和 Chat ID 填入环境变量或在管理后台「通知设置」页面中配置
|
||||
4. **测试**:保存后点击页面上的「测试通知」按钮验证是否收到消息
|
||||
|
||||
### SMTP 邮件
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|
||||
@@ -33,6 +33,9 @@ export const config = {
|
||||
barkEnabled: parseBoolean(process.env.BARK_ENABLED, true),
|
||||
serverChanEnabled: parseBoolean(process.env.SERVERCHAN_ENABLED, true),
|
||||
serverChanKey: process.env.SERVERCHAN_KEY || '',
|
||||
telegramEnabled: parseBoolean(process.env.TELEGRAM_ENABLED, false),
|
||||
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || '',
|
||||
telegramChatId: process.env.TELEGRAM_CHAT_ID || '',
|
||||
smtpEnabled: parseBoolean(process.env.SMTP_ENABLED, false),
|
||||
smtpHost: process.env.SMTP_HOST || '',
|
||||
smtpPort: parseInt(process.env.SMTP_PORT || '587'),
|
||||
|
||||
@@ -88,6 +88,15 @@ try {
|
||||
const serverChanKey = parseSetting<string>('serverchan_key');
|
||||
if (typeof serverChanKey === 'string') config.serverChanKey = serverChanKey;
|
||||
|
||||
const telegramEnabled = parseSetting<boolean>('telegram_enabled');
|
||||
if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled;
|
||||
|
||||
const telegramBotToken = parseSetting<string>('telegram_bot_token');
|
||||
if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken;
|
||||
|
||||
const telegramChatId = parseSetting<string>('telegram_chat_id');
|
||||
if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId;
|
||||
|
||||
const smtpEnabled = parseSetting<boolean>('smtp_enabled');
|
||||
if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled;
|
||||
|
||||
|
||||
@@ -132,6 +132,38 @@ describe('settings and auth events', () => {
|
||||
expect(body.message).toContain('Webhook URL');
|
||||
});
|
||||
|
||||
it('rejects telegram config when bot token is missing but telegram is enabled', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
payload: {
|
||||
telegramEnabled: true,
|
||||
telegramChatId: '-1001234567890',
|
||||
telegramBotToken: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = response.json() as { message?: string };
|
||||
expect(body.message).toContain('Telegram Bot Token');
|
||||
});
|
||||
|
||||
it('rejects telegram config when chat id is missing but telegram is enabled', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
payload: {
|
||||
telegramEnabled: true,
|
||||
telegramBotToken: '123456:telegram-token',
|
||||
telegramChatId: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = response.json() as { message?: string };
|
||||
expect(body.message).toContain('Telegram Chat ID');
|
||||
});
|
||||
|
||||
it('persists and returns routing fallback unit cost from runtime settings', async () => {
|
||||
const updateResponse = await app.inject({
|
||||
method: 'PUT',
|
||||
|
||||
@@ -21,6 +21,9 @@ interface RuntimeSettingsBody {
|
||||
barkEnabled?: boolean;
|
||||
serverChanEnabled?: boolean;
|
||||
serverChanKey?: string;
|
||||
telegramEnabled?: boolean;
|
||||
telegramBotToken?: string;
|
||||
telegramChatId?: string;
|
||||
smtpEnabled?: boolean;
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
@@ -155,6 +158,20 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
config.serverChanKey = value.trim();
|
||||
return;
|
||||
}
|
||||
case 'telegram_enabled': {
|
||||
config.telegramEnabled = !!value;
|
||||
return;
|
||||
}
|
||||
case 'telegram_bot_token': {
|
||||
if (typeof value !== 'string') return;
|
||||
config.telegramBotToken = value.trim();
|
||||
return;
|
||||
}
|
||||
case 'telegram_chat_id': {
|
||||
if (typeof value !== 'string') return;
|
||||
config.telegramChatId = value.trim();
|
||||
return;
|
||||
}
|
||||
case 'smtp_enabled': {
|
||||
config.smtpEnabled = !!value;
|
||||
return;
|
||||
@@ -239,6 +256,9 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
|
||||
barkEnabled: config.barkEnabled,
|
||||
serverChanEnabled: config.serverChanEnabled,
|
||||
serverChanKeyMasked: maskSecret(config.serverChanKey),
|
||||
telegramEnabled: config.telegramEnabled,
|
||||
telegramBotTokenMasked: maskSecret(config.telegramBotToken),
|
||||
telegramChatId: config.telegramChatId,
|
||||
smtpEnabled: config.smtpEnabled,
|
||||
smtpHost: config.smtpHost,
|
||||
smtpPort: config.smtpPort,
|
||||
@@ -297,6 +317,30 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
const telegramTouched = body.telegramEnabled !== undefined
|
||||
|| body.telegramBotToken !== undefined
|
||||
|| body.telegramChatId !== undefined;
|
||||
const nextTelegramEnabled = body.telegramEnabled !== undefined
|
||||
? !!body.telegramEnabled
|
||||
: config.telegramEnabled;
|
||||
const nextTelegramBotToken = body.telegramBotToken !== undefined
|
||||
? String(body.telegramBotToken || '').trim()
|
||||
: config.telegramBotToken;
|
||||
const nextTelegramChatId = body.telegramChatId !== undefined
|
||||
? String(body.telegramChatId || '').trim()
|
||||
: config.telegramChatId;
|
||||
if (telegramTouched && nextTelegramEnabled) {
|
||||
if (!nextTelegramBotToken) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Bot Token 不能为空(启用 Telegram 时)' });
|
||||
}
|
||||
if (!nextTelegramBotToken.includes(':')) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Bot Token 格式无效(示例:123456:abcDEF)' });
|
||||
}
|
||||
if (!nextTelegramChatId) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Chat ID 不能为空(启用 Telegram 时)' });
|
||||
}
|
||||
}
|
||||
|
||||
if (body.checkinCron !== undefined) {
|
||||
if (!cron.validate(body.checkinCron)) {
|
||||
return reply.code(400).send({ success: false, message: '签到 Cron 表达式无效' });
|
||||
@@ -382,6 +426,30 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
upsertSetting('serverchan_key', config.serverChanKey);
|
||||
}
|
||||
|
||||
if (body.telegramEnabled !== undefined) {
|
||||
if (!!body.telegramEnabled !== config.telegramEnabled) {
|
||||
changedLabels.push('Telegram 开关');
|
||||
}
|
||||
config.telegramEnabled = !!body.telegramEnabled;
|
||||
upsertSetting('telegram_enabled', config.telegramEnabled);
|
||||
}
|
||||
|
||||
if (body.telegramBotToken !== undefined) {
|
||||
if (String(body.telegramBotToken || '').trim() !== config.telegramBotToken) {
|
||||
changedLabels.push('Telegram Bot Token');
|
||||
}
|
||||
config.telegramBotToken = String(body.telegramBotToken || '').trim();
|
||||
upsertSetting('telegram_bot_token', config.telegramBotToken);
|
||||
}
|
||||
|
||||
if (body.telegramChatId !== undefined) {
|
||||
if (String(body.telegramChatId || '').trim() !== config.telegramChatId) {
|
||||
changedLabels.push('Telegram Chat ID');
|
||||
}
|
||||
config.telegramChatId = String(body.telegramChatId || '').trim();
|
||||
upsertSetting('telegram_chat_id', config.telegramChatId);
|
||||
}
|
||||
|
||||
if (body.smtpEnabled !== undefined) {
|
||||
if (!!body.smtpEnabled !== config.smtpEnabled) {
|
||||
changedLabels.push('SMTP 开关');
|
||||
|
||||
@@ -187,4 +187,16 @@ describe('resolveUpstreamEndpointCandidates', () => {
|
||||
expect(isEndpointDowngradeError(405, '{"error":{"message":"Method Not Allowed"}}')).toBe(true);
|
||||
expect(isEndpointDowngradeError(400, '{"error":{"message":"unsupported endpoint","type":"invalid_request_error"}}')).toBe(true);
|
||||
});
|
||||
|
||||
it('treats Claude Code CLI-only restriction on responses as downgrade candidate', () => {
|
||||
const upstreamError = JSON.stringify({
|
||||
error: {
|
||||
code: 'invalid_request',
|
||||
type: 'new_api_error',
|
||||
message: '请勿在 Claude Code CLI 之外使用接口 (request id: abc123)',
|
||||
},
|
||||
});
|
||||
|
||||
expect(isEndpointDowngradeError(400, upstreamError)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,9 @@ describe('notifyService', () => {
|
||||
config.barkUrl = '';
|
||||
config.serverChanEnabled = false;
|
||||
config.serverChanKey = '';
|
||||
(config as any).telegramEnabled = false;
|
||||
(config as any).telegramBotToken = '';
|
||||
(config as any).telegramChatId = '';
|
||||
config.smtpEnabled = true;
|
||||
config.smtpHost = 'smtp.example.com';
|
||||
config.smtpPort = 465;
|
||||
@@ -116,4 +119,36 @@ describe('notifyService', () => {
|
||||
expect(payload?.text || '').toContain('Local Time:');
|
||||
expect(payload?.text || '').toContain('UTC Time:');
|
||||
});
|
||||
|
||||
it('sends telegram message when telegram channel is enabled', async () => {
|
||||
const { config } = await import('../config.js');
|
||||
(config as any).telegramEnabled = true;
|
||||
(config as any).telegramBotToken = '123456:telegram-token';
|
||||
(config as any).telegramChatId = '-1001234567890';
|
||||
config.smtpEnabled = false;
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const { sendNotification } = await import('./notifyService.js');
|
||||
await sendNotification('测试通知', 'message', 'warning', { bypassThrottle: true, throwOnFailure: true });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://api.telegram.org/bot123456:telegram-token/sendMessage',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const rawBody = fetchMock.mock.calls[0]?.[1] as { body?: string };
|
||||
const payload = JSON.parse(rawBody?.body || '{}') as { chat_id?: string; text?: string };
|
||||
expect(payload.chat_id).toBe('-1001234567890');
|
||||
expect(payload.text || '').toContain('Level: warning');
|
||||
expect(payload.text || '').toContain('Local Time:');
|
||||
expect(payload.text || '').toContain('UTC Time:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from './notificationThrottle.js';
|
||||
import { formatLocalDateTime, getResolvedTimeZone } from './localTimeService.js';
|
||||
|
||||
type NotificationChannel = 'webhook' | 'bark' | 'serverchan' | 'smtp';
|
||||
type NotificationChannel = 'webhook' | 'bark' | 'serverchan' | 'telegram' | 'smtp';
|
||||
|
||||
export type SendNotificationOptions = {
|
||||
bypassThrottle?: boolean;
|
||||
@@ -70,6 +70,18 @@ function buildTimeFootnote(now: Date): string {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildTelegramText(
|
||||
title: string,
|
||||
message: string,
|
||||
level: 'info' | 'warning' | 'error',
|
||||
timeFootnote: string,
|
||||
): string {
|
||||
const maxTextLength = 3900;
|
||||
const raw = `[metapi][${level.toUpperCase()}] ${title}\n\n${message}\n\nLevel: ${level}\n${timeFootnote}`;
|
||||
if (raw.length <= maxTextLength) return raw;
|
||||
return `${raw.slice(0, maxTextLength)}\n\n...(truncated)`;
|
||||
}
|
||||
|
||||
export async function sendNotification(
|
||||
title: string,
|
||||
message: string,
|
||||
@@ -163,6 +175,35 @@ export async function sendNotification(
|
||||
);
|
||||
}
|
||||
|
||||
if (config.telegramEnabled && config.telegramBotToken && config.telegramChatId) {
|
||||
const telegramApiUrl = `https://api.telegram.org/bot${config.telegramBotToken}/sendMessage`;
|
||||
const text = buildTelegramText(title, resolvedMessage, level, timeFootnote);
|
||||
tasks.push({
|
||||
channel: 'telegram',
|
||||
run: async () => {
|
||||
const response = await fetch(telegramApiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: config.telegramChatId,
|
||||
text,
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Telegram 响应状态 ${response.status}`);
|
||||
}
|
||||
let payload: { ok?: boolean; description?: string } | null = null;
|
||||
try {
|
||||
payload = await response.json() as { ok?: boolean; description?: string };
|
||||
} catch {}
|
||||
if (payload?.ok === false) {
|
||||
throw new Error(payload.description || 'Telegram 返回失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
config.smtpEnabled &&
|
||||
config.smtpHost &&
|
||||
|
||||
@@ -9,6 +9,8 @@ type RuntimeSettings = {
|
||||
webhookEnabled: boolean;
|
||||
barkEnabled: boolean;
|
||||
serverChanEnabled: boolean;
|
||||
telegramEnabled: boolean;
|
||||
telegramChatId: string;
|
||||
smtpEnabled: boolean;
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
@@ -18,6 +20,7 @@ type RuntimeSettings = {
|
||||
smtpFrom: string;
|
||||
smtpTo: string;
|
||||
serverChanKeyMasked?: string;
|
||||
telegramBotTokenMasked?: string;
|
||||
notifyCooldownSec: number;
|
||||
};
|
||||
|
||||
@@ -28,6 +31,8 @@ export default function NotificationSettings() {
|
||||
webhookEnabled: true,
|
||||
barkEnabled: true,
|
||||
serverChanEnabled: false,
|
||||
telegramEnabled: false,
|
||||
telegramChatId: '',
|
||||
smtpEnabled: false,
|
||||
smtpHost: '',
|
||||
smtpPort: 587,
|
||||
@@ -39,6 +44,7 @@ export default function NotificationSettings() {
|
||||
});
|
||||
|
||||
const [serverChanKey, setServerChanKey] = useState('');
|
||||
const [telegramBotToken, setTelegramBotToken] = useState('');
|
||||
const [smtpPass, setSmtpPass] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [savingNotify, setSavingNotify] = useState(false);
|
||||
@@ -67,6 +73,8 @@ export default function NotificationSettings() {
|
||||
webhookEnabled: runtimeInfo.webhookEnabled ?? true,
|
||||
barkEnabled: runtimeInfo.barkEnabled ?? true,
|
||||
serverChanEnabled: !!runtimeInfo.serverChanEnabled,
|
||||
telegramEnabled: !!runtimeInfo.telegramEnabled,
|
||||
telegramChatId: runtimeInfo.telegramChatId || '',
|
||||
smtpEnabled: !!runtimeInfo.smtpEnabled,
|
||||
smtpHost: runtimeInfo.smtpHost || '',
|
||||
smtpPort: Number(runtimeInfo.smtpPort) || 587,
|
||||
@@ -76,6 +84,7 @@ export default function NotificationSettings() {
|
||||
smtpFrom: runtimeInfo.smtpFrom || '',
|
||||
smtpTo: runtimeInfo.smtpTo || '',
|
||||
serverChanKeyMasked: runtimeInfo.serverChanKeyMasked || '',
|
||||
telegramBotTokenMasked: runtimeInfo.telegramBotTokenMasked || '',
|
||||
notifyCooldownSec: Number.isFinite(Number(runtimeInfo.notifyCooldownSec))
|
||||
? Math.max(0, Math.trunc(Number(runtimeInfo.notifyCooldownSec)))
|
||||
: 300,
|
||||
@@ -100,6 +109,8 @@ export default function NotificationSettings() {
|
||||
webhookEnabled: runtime.webhookEnabled,
|
||||
barkEnabled: runtime.barkEnabled,
|
||||
serverChanEnabled: runtime.serverChanEnabled,
|
||||
telegramEnabled: runtime.telegramEnabled,
|
||||
telegramChatId: runtime.telegramChatId,
|
||||
smtpEnabled: runtime.smtpEnabled,
|
||||
smtpHost: runtime.smtpHost,
|
||||
smtpPort: runtime.smtpPort,
|
||||
@@ -110,15 +121,18 @@ export default function NotificationSettings() {
|
||||
notifyCooldownSec: Math.max(0, Math.trunc(Number(runtime.notifyCooldownSec) || 0)),
|
||||
};
|
||||
if (serverChanKey.trim()) payload.serverChanKey = serverChanKey.trim();
|
||||
if (telegramBotToken.trim()) payload.telegramBotToken = telegramBotToken.trim();
|
||||
if (smtpPass.trim()) payload.smtpPass = smtpPass.trim();
|
||||
|
||||
const res = await api.updateRuntimeSettings(payload);
|
||||
setRuntime((prev) => ({
|
||||
...prev,
|
||||
serverChanKeyMasked: res.serverChanKeyMasked || prev.serverChanKeyMasked,
|
||||
telegramBotTokenMasked: res.telegramBotTokenMasked || prev.telegramBotTokenMasked,
|
||||
smtpPassMasked: res.smtpPassMasked || prev.smtpPassMasked,
|
||||
}));
|
||||
setServerChanKey('');
|
||||
setTelegramBotToken('');
|
||||
setSmtpPass('');
|
||||
toast.success('通知设置已保存');
|
||||
} catch (err: any) {
|
||||
@@ -286,6 +300,58 @@ export default function NotificationSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片:Telegram */}
|
||||
<div className="card animate-slide-up stagger-4" style={{ padding: 24, border: runtime.telegramEnabled ? '1px solid var(--color-primary)' : '1px solid var(--color-border-light)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'var(--color-primary-light)', color: 'var(--color-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 11l18-8-6 18-3-7-9-3z" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 15 }}>Telegram Bot</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>通过 Telegram 机器人推送消息通知</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: runtime.telegramEnabled ? 'var(--color-primary)' : 'var(--color-text-muted)' }}>启用 Telegram</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ width: 16, height: 16, cursor: 'pointer' }}
|
||||
checked={runtime.telegramEnabled}
|
||||
onChange={(e) => setRuntime((prev) => ({ ...prev, telegramEnabled: e.target.checked }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '16px 20px', opacity: runtime.telegramEnabled ? 1 : 0.6, transition: 'opacity 0.2s' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8, color: 'var(--color-text-secondary)' }}>Telegram Chat ID</div>
|
||||
<input
|
||||
value={runtime.telegramChatId}
|
||||
onChange={(e) => setRuntime((prev) => ({ ...prev, telegramChatId: e.target.value }))}
|
||||
placeholder="例如: -1001234567890 或 @your_channel"
|
||||
style={inputStyle}
|
||||
disabled={!runtime.telegramEnabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8, color: 'var(--color-text-secondary)' }}>
|
||||
Telegram Bot Token
|
||||
{runtime.telegramBotTokenMasked && <span style={{ color: 'var(--color-primary)', marginLeft: 8, fontSize: 12 }}>(当前已设置)</span>}
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={telegramBotToken}
|
||||
onChange={(e) => setTelegramBotToken(e.target.value)}
|
||||
placeholder="输入新的 Bot Token(留空则不改)"
|
||||
style={inputStyle}
|
||||
disabled={!runtime.telegramEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片:SMTP 邮件设置 */}
|
||||
<div className="card animate-slide-up stagger-4" style={{ padding: 24, border: runtime.smtpEnabled ? '1px solid var(--color-primary)' : '1px solid var(--color-border-light)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
|
||||
Reference in New Issue
Block a user