forked from boos/BoosAPI
fix: improve tester timeouts and telegram proxy config
This commit is contained in:
@@ -32,6 +32,12 @@ describe('buildConfig', () => {
|
||||
expect(config.listenHost).toBe('127.0.0.1');
|
||||
});
|
||||
|
||||
it('defaults telegram api base url to the official endpoint', () => {
|
||||
const config = buildConfig({});
|
||||
|
||||
expect(config.telegramApiBaseUrl).toBe('https://api.telegram.org');
|
||||
});
|
||||
|
||||
it('accepts JSON request bodies larger than Fastify default 1 MiB', async () => {
|
||||
const app = Fastify(buildFastifyOptions(buildConfig({})));
|
||||
const largeText = 'a'.repeat(2 * 1024 * 1024);
|
||||
|
||||
@@ -61,6 +61,7 @@ export function buildConfig(env: NodeJS.ProcessEnv) {
|
||||
serverChanEnabled: parseBoolean(env.SERVERCHAN_ENABLED, true),
|
||||
serverChanKey: env.SERVERCHAN_KEY || '',
|
||||
telegramEnabled: parseBoolean(env.TELEGRAM_ENABLED, false),
|
||||
telegramApiBaseUrl: 'https://api.telegram.org',
|
||||
telegramBotToken: env.TELEGRAM_BOT_TOKEN || '',
|
||||
telegramChatId: env.TELEGRAM_CHAT_ID || '',
|
||||
smtpEnabled: parseBoolean(env.SMTP_ENABLED, false),
|
||||
|
||||
@@ -175,6 +175,11 @@ function applyRuntimeSettings(settingsMap: Map<string, string>) {
|
||||
const telegramEnabled = parseSettingFromMap<boolean>(settingsMap, 'telegram_enabled');
|
||||
if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled;
|
||||
|
||||
const telegramApiBaseUrl = parseSettingFromMap<string>(settingsMap, 'telegram_api_base_url');
|
||||
if (typeof telegramApiBaseUrl === 'string' && telegramApiBaseUrl.trim()) {
|
||||
config.telegramApiBaseUrl = telegramApiBaseUrl.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
const telegramBotToken = parseSettingFromMap<string>(settingsMap, 'telegram_bot_token');
|
||||
if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken;
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@ describe('settings and auth events', () => {
|
||||
config.logCleanupProgramLogsEnabled = false;
|
||||
config.logCleanupRetentionDays = 30;
|
||||
config.routingFallbackUnitCost = 1;
|
||||
(config as any).telegramEnabled = false;
|
||||
(config as any).telegramApiBaseUrl = 'https://api.telegram.org';
|
||||
(config as any).telegramBotToken = '';
|
||||
(config as any).telegramChatId = '';
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -170,6 +174,49 @@ describe('settings and auth events', () => {
|
||||
expect(body.message).toContain('Telegram Chat ID');
|
||||
});
|
||||
|
||||
it('persists and returns telegram api base url from runtime settings', async () => {
|
||||
const updateResponse = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
payload: {
|
||||
telegramApiBaseUrl: 'https://tg-proxy.example.com/custom/',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateResponse.statusCode).toBe(200);
|
||||
const updated = updateResponse.json() as { telegramApiBaseUrl?: string };
|
||||
expect(updated.telegramApiBaseUrl).toBe('https://tg-proxy.example.com/custom');
|
||||
expect((config as any).telegramApiBaseUrl).toBe('https://tg-proxy.example.com/custom');
|
||||
|
||||
const saved = await db.select().from(schema.settings).where(eq(schema.settings.key, 'telegram_api_base_url')).get();
|
||||
expect(saved?.value).toBe(JSON.stringify('https://tg-proxy.example.com/custom'));
|
||||
|
||||
const getResponse = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/runtime',
|
||||
});
|
||||
expect(getResponse.statusCode).toBe(200);
|
||||
const runtime = getResponse.json() as { telegramApiBaseUrl?: string };
|
||||
expect(runtime.telegramApiBaseUrl).toBe('https://tg-proxy.example.com/custom');
|
||||
});
|
||||
|
||||
it('rejects invalid telegram api base url when telegram is enabled', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
payload: {
|
||||
telegramEnabled: true,
|
||||
telegramBotToken: '123456:telegram-token',
|
||||
telegramChatId: '-1001234567890',
|
||||
telegramApiBaseUrl: 'not-a-url',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = response.json() as { message?: string };
|
||||
expect(body.message).toContain('Telegram API Base URL');
|
||||
});
|
||||
|
||||
it('persists and returns routing fallback unit cost from runtime settings', async () => {
|
||||
const updateResponse = await app.inject({
|
||||
method: 'PUT',
|
||||
|
||||
@@ -40,6 +40,7 @@ interface RuntimeSettingsBody {
|
||||
serverChanEnabled?: boolean;
|
||||
serverChanKey?: string;
|
||||
telegramEnabled?: boolean;
|
||||
telegramApiBaseUrl?: string;
|
||||
telegramBotToken?: string;
|
||||
telegramChatId?: string;
|
||||
smtpEnabled?: boolean;
|
||||
@@ -137,6 +138,10 @@ function isValidHttpUrl(raw: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTelegramApiBaseUrl(raw: string): string {
|
||||
return String(raw || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
switch (key) {
|
||||
case 'checkin_cron': {
|
||||
@@ -223,6 +228,11 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
config.telegramEnabled = !!value;
|
||||
return;
|
||||
}
|
||||
case 'telegram_api_base_url': {
|
||||
if (typeof value !== 'string') return;
|
||||
config.telegramApiBaseUrl = normalizeTelegramApiBaseUrl(value) || 'https://api.telegram.org';
|
||||
return;
|
||||
}
|
||||
case 'telegram_bot_token': {
|
||||
if (typeof value !== 'string') return;
|
||||
config.telegramBotToken = value.trim();
|
||||
@@ -322,6 +332,7 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
|
||||
serverChanEnabled: config.serverChanEnabled,
|
||||
serverChanKeyMasked: maskSecret(config.serverChanKey),
|
||||
telegramEnabled: config.telegramEnabled,
|
||||
telegramApiBaseUrl: config.telegramApiBaseUrl,
|
||||
telegramBotTokenMasked: maskSecret(config.telegramBotToken),
|
||||
telegramChatId: config.telegramChatId,
|
||||
smtpEnabled: config.smtpEnabled,
|
||||
@@ -452,11 +463,15 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const telegramTouched = body.telegramEnabled !== undefined
|
||||
|| body.telegramApiBaseUrl !== undefined
|
||||
|| body.telegramBotToken !== undefined
|
||||
|| body.telegramChatId !== undefined;
|
||||
const nextTelegramEnabled = body.telegramEnabled !== undefined
|
||||
? !!body.telegramEnabled
|
||||
: config.telegramEnabled;
|
||||
const nextTelegramApiBaseUrl = body.telegramApiBaseUrl !== undefined
|
||||
? normalizeTelegramApiBaseUrl(body.telegramApiBaseUrl)
|
||||
: config.telegramApiBaseUrl;
|
||||
const nextTelegramBotToken = body.telegramBotToken !== undefined
|
||||
? String(body.telegramBotToken || '').trim()
|
||||
: config.telegramBotToken;
|
||||
@@ -473,6 +488,11 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
if (!nextTelegramChatId) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Chat ID 不能为空(启用 Telegram 时)' });
|
||||
}
|
||||
if (nextTelegramApiBaseUrl && !isValidHttpUrl(nextTelegramApiBaseUrl)) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram API Base URL 无效,请填写 http/https 地址' });
|
||||
}
|
||||
} else if (body.telegramApiBaseUrl !== undefined && nextTelegramApiBaseUrl && !isValidHttpUrl(nextTelegramApiBaseUrl)) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram API Base URL 无效,请填写 http/https 地址' });
|
||||
}
|
||||
|
||||
if (body.checkinCron !== undefined) {
|
||||
@@ -639,6 +659,16 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
upsertSetting('telegram_enabled', config.telegramEnabled);
|
||||
}
|
||||
|
||||
if (body.telegramApiBaseUrl !== undefined) {
|
||||
const normalizedTelegramApiBaseUrl = normalizeTelegramApiBaseUrl(body.telegramApiBaseUrl);
|
||||
const nextTelegramApiBaseUrl = normalizedTelegramApiBaseUrl || 'https://api.telegram.org';
|
||||
if (nextTelegramApiBaseUrl !== config.telegramApiBaseUrl) {
|
||||
changedLabels.push('Telegram API Base URL');
|
||||
}
|
||||
config.telegramApiBaseUrl = nextTelegramApiBaseUrl;
|
||||
upsertSetting('telegram_api_base_url', config.telegramApiBaseUrl);
|
||||
}
|
||||
|
||||
if (body.telegramBotToken !== undefined) {
|
||||
if (String(body.telegramBotToken || '').trim() !== config.telegramBotToken) {
|
||||
changedLabels.push('Telegram Bot Token');
|
||||
|
||||
@@ -198,4 +198,28 @@ describe('notifyService', () => {
|
||||
expect(payload.text || '').toContain('Local Time:');
|
||||
expect(payload.text || '').toContain('UTC Time:');
|
||||
});
|
||||
|
||||
it('uses TELEGRAM_API_BASE_URL when configured', 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 as any).telegramApiBaseUrl = 'https://tg-proxy.example.com/custom/';
|
||||
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).toHaveBeenCalledWith(
|
||||
'https://tg-proxy.example.com/custom/bot123456:telegram-token/sendMessage',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,7 +218,8 @@ export async function sendNotification(
|
||||
}
|
||||
|
||||
if (config.telegramEnabled && config.telegramBotToken && config.telegramChatId) {
|
||||
const telegramApiUrl = `https://api.telegram.org/bot${config.telegramBotToken}/sendMessage`;
|
||||
const telegramApiBaseUrl = String(config.telegramApiBaseUrl || 'https://api.telegram.org').replace(/\/+$/, '');
|
||||
const telegramApiUrl = `${telegramApiBaseUrl}/bot${config.telegramBotToken}/sendMessage`;
|
||||
const text = buildTelegramText(title, resolvedMessage, level, timeFootnote);
|
||||
tasks.push({
|
||||
channel: 'telegram',
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { api, type ProxyTestRequestEnvelope } from './api.js';
|
||||
import { persistAuthSession } from './authSession.js';
|
||||
|
||||
function createMemoryStorage() {
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
getItem(key: string) {
|
||||
return store.has(key) ? store.get(key)! : null;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store.set(key, value);
|
||||
},
|
||||
removeItem(key: string) {
|
||||
store.delete(key);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function installPendingFetch() {
|
||||
const fetchMock = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
|
||||
const signal = init?.signal;
|
||||
if (!signal) return;
|
||||
if (signal.aborted) {
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
return;
|
||||
}
|
||||
signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
|
||||
}));
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
describe('api proxy test timeout handling', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal('localStorage', createMemoryStorage());
|
||||
persistAuthSession(globalThis.localStorage as Storage, 'token-1');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('keeps image generation proxy tests alive past the default 30 second timeout', async () => {
|
||||
installPendingFetch();
|
||||
|
||||
const payload: ProxyTestRequestEnvelope = {
|
||||
method: 'POST',
|
||||
path: '/v1/images/generations',
|
||||
requestKind: 'json',
|
||||
jsonBody: {
|
||||
model: 'gemini-imagen',
|
||||
prompt: 'banana cat',
|
||||
},
|
||||
};
|
||||
|
||||
let settled = false;
|
||||
const promise = api.proxyTest(payload);
|
||||
const handled = promise
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error: Error) => ({ ok: false as const, error }))
|
||||
.finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
expect(settled).toBe(false);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
const result = await handled;
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
throw new Error('Expected image generation proxy test to time out');
|
||||
}
|
||||
expect(result.error.message).toBe('请求超时(150s)');
|
||||
});
|
||||
|
||||
it('still uses the default 30 second timeout for generic proxy tests', async () => {
|
||||
installPendingFetch();
|
||||
|
||||
const payload: ProxyTestRequestEnvelope = {
|
||||
method: 'POST',
|
||||
path: '/v1/embeddings',
|
||||
requestKind: 'json',
|
||||
jsonBody: {
|
||||
model: 'text-embedding-3-small',
|
||||
input: 'hello',
|
||||
},
|
||||
};
|
||||
|
||||
const promise = api.proxyTest(payload).catch((error: Error) => error);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await expect(promise).resolves.toMatchObject({ message: '请求超时(30s)' });
|
||||
});
|
||||
});
|
||||
+26
-3
@@ -135,6 +135,17 @@ export type ProxyTestRequestEnvelope = {
|
||||
multipartFiles?: ProxyTestMultipartFile[];
|
||||
};
|
||||
|
||||
const DEFAULT_PROXY_TEST_TIMEOUT_MS = 30_000;
|
||||
const LONG_RUNNING_PROXY_TEST_TIMEOUT_MS = 150_000;
|
||||
|
||||
function resolveProxyTestTimeoutMs(data: ProxyTestRequestEnvelope) {
|
||||
if (data.jobMode) return LONG_RUNNING_PROXY_TEST_TIMEOUT_MS;
|
||||
if (data.path === '/v1/images/generations') return LONG_RUNNING_PROXY_TEST_TIMEOUT_MS;
|
||||
if (data.path === '/v1/images/edits') return LONG_RUNNING_PROXY_TEST_TIMEOUT_MS;
|
||||
if (data.path === '/v1/videos' && data.method === 'POST') return LONG_RUNNING_PROXY_TEST_TIMEOUT_MS;
|
||||
return DEFAULT_PROXY_TEST_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
export type ProxyTestJobResponse = {
|
||||
jobId: string;
|
||||
status: 'pending' | 'succeeded' | 'failed' | 'cancelled';
|
||||
@@ -425,13 +436,25 @@ export const api = {
|
||||
getTestChatJob: (jobId: string) => request(`/api/test/chat/jobs/${encodeURIComponent(jobId)}`),
|
||||
deleteTestChatJob: (jobId: string) => request(`/api/test/chat/jobs/${encodeURIComponent(jobId)}`, { method: 'DELETE' }),
|
||||
startProxyTestJob: (data: ProxyTestRequestEnvelope) =>
|
||||
request('/api/test/proxy/jobs', { method: 'POST', body: JSON.stringify(data) }),
|
||||
request('/api/test/proxy/jobs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
timeoutMs: resolveProxyTestTimeoutMs(data),
|
||||
}),
|
||||
getProxyTestJob: (jobId: string) => request(`/api/test/proxy/jobs/${encodeURIComponent(jobId)}`),
|
||||
deleteProxyTestJob: (jobId: string) => request(`/api/test/proxy/jobs/${encodeURIComponent(jobId)}`, { method: 'DELETE' }),
|
||||
testProxy: (data: ProxyTestRequestEnvelope) =>
|
||||
request('/api/test/proxy', { method: 'POST', body: JSON.stringify(data) }),
|
||||
request('/api/test/proxy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
timeoutMs: resolveProxyTestTimeoutMs(data),
|
||||
}),
|
||||
proxyTest: (data: ProxyTestRequestEnvelope) =>
|
||||
request('/api/test/proxy', { method: 'POST', body: JSON.stringify(data) }),
|
||||
request('/api/test/proxy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
timeoutMs: resolveProxyTestTimeoutMs(data),
|
||||
}),
|
||||
testChat: (data: TestChatRequestPayload) =>
|
||||
request('/api/test/chat', { method: 'POST', body: JSON.stringify(data) }),
|
||||
testProxyStream: async (data: ProxyTestRequestEnvelope, signal?: AbortSignal) => {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, create, type ReactTestInstance } from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ToastProvider } from '../components/Toast.js';
|
||||
import NotificationSettings from './NotificationSettings.js';
|
||||
|
||||
const { apiMock } = vi.hoisted(() => ({
|
||||
apiMock: {
|
||||
getRuntimeSettings: vi.fn(),
|
||||
updateRuntimeSettings: vi.fn(),
|
||||
testNotification: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../api.js', () => ({
|
||||
api: apiMock,
|
||||
}));
|
||||
|
||||
function collectText(node: ReactTestInstance): string {
|
||||
return (node.children || []).map((child) => {
|
||||
if (typeof child === 'string') return child;
|
||||
return collectText(child);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function flushMicrotasks() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe('NotificationSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiMock.getRuntimeSettings.mockResolvedValue({
|
||||
webhookUrl: '',
|
||||
barkUrl: '',
|
||||
webhookEnabled: true,
|
||||
barkEnabled: true,
|
||||
serverChanEnabled: false,
|
||||
telegramEnabled: true,
|
||||
telegramApiBaseUrl: 'https://tg-proxy.example.com',
|
||||
telegramChatId: '-1001234567890',
|
||||
telegramBotTokenMasked: '1234****token',
|
||||
smtpEnabled: false,
|
||||
smtpHost: '',
|
||||
smtpPort: 587,
|
||||
smtpSecure: false,
|
||||
smtpUser: '',
|
||||
smtpFrom: '',
|
||||
smtpTo: '',
|
||||
notifyCooldownSec: 300,
|
||||
});
|
||||
apiMock.updateRuntimeSettings.mockResolvedValue({
|
||||
success: true,
|
||||
telegramApiBaseUrl: 'https://proxy.example.com/custom',
|
||||
telegramBotTokenMasked: '1234****token',
|
||||
});
|
||||
apiMock.testNotification.mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads and saves telegram api base url', async () => {
|
||||
let root: ReturnType<typeof create> | null = null;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter>
|
||||
<ToastProvider>
|
||||
<NotificationSettings />
|
||||
</ToastProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const proxyInput = root.root.find((node) => (
|
||||
node.type === 'input'
|
||||
&& node.props.placeholder === '例如: https://your-proxy.example.com'
|
||||
));
|
||||
expect(proxyInput.props.value).toBe('https://tg-proxy.example.com');
|
||||
|
||||
await act(async () => {
|
||||
proxyInput.props.onChange({ target: { value: 'https://proxy.example.com/custom/' } });
|
||||
});
|
||||
|
||||
const saveButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
&& collectText(node).includes('保存通知设置')
|
||||
));
|
||||
await act(async () => {
|
||||
saveButton.props.onClick();
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(apiMock.updateRuntimeSettings).toHaveBeenCalledWith(expect.objectContaining({
|
||||
telegramApiBaseUrl: 'https://proxy.example.com/custom/',
|
||||
}));
|
||||
} finally {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ type RuntimeSettings = {
|
||||
barkEnabled: boolean;
|
||||
serverChanEnabled: boolean;
|
||||
telegramEnabled: boolean;
|
||||
telegramApiBaseUrl: string;
|
||||
telegramChatId: string;
|
||||
smtpEnabled: boolean;
|
||||
smtpHost: string;
|
||||
@@ -32,6 +33,7 @@ export default function NotificationSettings() {
|
||||
barkEnabled: true,
|
||||
serverChanEnabled: false,
|
||||
telegramEnabled: false,
|
||||
telegramApiBaseUrl: 'https://api.telegram.org',
|
||||
telegramChatId: '',
|
||||
smtpEnabled: false,
|
||||
smtpHost: '',
|
||||
@@ -74,6 +76,7 @@ export default function NotificationSettings() {
|
||||
barkEnabled: runtimeInfo.barkEnabled ?? true,
|
||||
serverChanEnabled: !!runtimeInfo.serverChanEnabled,
|
||||
telegramEnabled: !!runtimeInfo.telegramEnabled,
|
||||
telegramApiBaseUrl: runtimeInfo.telegramApiBaseUrl || 'https://api.telegram.org',
|
||||
telegramChatId: runtimeInfo.telegramChatId || '',
|
||||
smtpEnabled: !!runtimeInfo.smtpEnabled,
|
||||
smtpHost: runtimeInfo.smtpHost || '',
|
||||
@@ -110,6 +113,7 @@ export default function NotificationSettings() {
|
||||
barkEnabled: runtime.barkEnabled,
|
||||
serverChanEnabled: runtime.serverChanEnabled,
|
||||
telegramEnabled: runtime.telegramEnabled,
|
||||
telegramApiBaseUrl: runtime.telegramApiBaseUrl,
|
||||
telegramChatId: runtime.telegramChatId,
|
||||
smtpEnabled: runtime.smtpEnabled,
|
||||
smtpHost: runtime.smtpHost,
|
||||
@@ -325,6 +329,19 @@ export default function NotificationSettings() {
|
||||
</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 style={{ gridColumn: '1 / -1' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8, color: 'var(--color-text-secondary)' }}>Telegram API Base URL</div>
|
||||
<input
|
||||
value={runtime.telegramApiBaseUrl}
|
||||
onChange={(e) => setRuntime((prev) => ({ ...prev, telegramApiBaseUrl: e.target.value }))}
|
||||
placeholder="例如: https://your-proxy.example.com"
|
||||
style={inputStyle}
|
||||
disabled={!runtime.telegramEnabled}
|
||||
/>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||
留空或使用默认值时直连官方 Telegram API;如需国内反代,可填写反代前缀。
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8, color: 'var(--color-text-secondary)' }}>Telegram Chat ID</div>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user