diff --git a/src/app/api/settings/telegram/route.ts b/src/app/api/settings/telegram/route.ts index be64f422..e1f16bc2 100644 --- a/src/app/api/settings/telegram/route.ts +++ b/src/app/api/settings/telegram/route.ts @@ -16,6 +16,7 @@ const TELEGRAM_KEYS = [ 'telegram_notify_error', 'telegram_notify_permission', 'telegram_bridge_allowed_users', + 'telegram_proxy_url', ] as const; export async function GET() { diff --git a/src/components/bridge/TelegramBridgeSection.tsx b/src/components/bridge/TelegramBridgeSection.tsx index 0f9cf6b9..c80cd42e 100644 --- a/src/components/bridge/TelegramBridgeSection.tsx +++ b/src/components/bridge/TelegramBridgeSection.tsx @@ -12,12 +12,14 @@ interface TelegramBridgeSettings { telegram_bot_token: string; telegram_chat_id: string; telegram_bridge_allowed_users: string; + telegram_proxy_url: string; } const DEFAULT_SETTINGS: TelegramBridgeSettings = { telegram_bot_token: "", telegram_chat_id: "", telegram_bridge_allowed_users: "", + telegram_proxy_url: "", }; export function TelegramBridgeSection() { @@ -25,6 +27,8 @@ export function TelegramBridgeSection() { const [botToken, setBotToken] = useState(""); const [chatId, setChatId] = useState(""); const [allowedUsers, setAllowedUsers] = useState(""); + const [proxyUrl, setProxyUrl] = useState(""); + const [proxyUrlError, setProxyUrlError] = useState(""); const [saving, setSaving] = useState(false); const [verifying, setVerifying] = useState(false); const [detecting, setDetecting] = useState(false); @@ -44,6 +48,7 @@ export function TelegramBridgeSection() { setBotToken(s.telegram_bot_token); setChatId(s.telegram_chat_id); setAllowedUsers(s.telegram_bridge_allowed_users); + setProxyUrl(s.telegram_proxy_url); } } catch { // ignore @@ -73,12 +78,27 @@ export function TelegramBridgeSection() { }; const handleSaveCredentials = () => { + // Validate proxy URL if provided + if (proxyUrl) { + try { + const parsed = new URL(proxyUrl); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + setProxyUrlError(t("telegram.proxyInvalidProtocol")); + return; + } + } catch { + setProxyUrlError(t("telegram.proxyInvalidUrl")); + return; + } + } + setProxyUrlError(""); const updates: Partial = {}; if (botToken && !botToken.startsWith("***")) { updates.telegram_bot_token = botToken; } updates.telegram_chat_id = chatId; updates.telegram_bridge_allowed_users = allowedUsers; + updates.telegram_proxy_url = proxyUrl; saveSettings(updates); }; @@ -223,6 +243,25 @@ export function TelegramBridgeSection() { {t("telegram.chatIdHint")}

+ +
+ + { setProxyUrl(e.target.value); setProxyUrlError(""); }} + placeholder={t("telegram.proxyPlaceholder")} + className={`font-mono text-sm${proxyUrlError ? " border-destructive" : ""}`} + /> + {proxyUrlError ? ( +

{proxyUrlError}

+ ) : ( +

+ {t("telegram.proxyHint")} +

+ )} +
diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 479d50f5..ec9943ae 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -540,6 +540,12 @@ const en = { 'telegram.step4': 'Click "Test Connection" to verify the token is valid', 'telegram.step5': 'Send /start to your bot, then click "Auto Detect" next to the Chat ID field', 'telegram.step6': 'Click "Save" to store your credentials', + 'telegram.proxy': 'Proxy Server', + 'telegram.proxyDesc': 'Optional HTTP/HTTPS proxy for Telegram API requests (useful in regions where Telegram is blocked)', + 'telegram.proxyPlaceholder': 'http://127.0.0.1:7890', + 'telegram.proxyHint': 'Leave blank to connect directly. Supports http:// and https:// proxy URLs.', + 'telegram.proxyInvalidUrl': 'Invalid proxy URL. Example: http://127.0.0.1:7890', + 'telegram.proxyInvalidProtocol': 'Only http:// and https:// proxy URLs are supported.', // ── Feishu (Bridge) ────────────────────────────────────── 'feishu.credentials': 'App Credentials', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index c239873d..88025691 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -537,6 +537,12 @@ const zh: Record = { 'telegram.step4': '点击「测试连接」验证 Token 是否有效', 'telegram.step5': '向您的 Bot 发送 /start,然后点击 Chat ID 旁的「自动检测」按钮', 'telegram.step6': '点击「保存」存储凭据', + 'telegram.proxy': '代理服务器', + 'telegram.proxyDesc': '可选的 HTTP/HTTPS 代理,用于 Telegram API 请求(在 Telegram 被封锁的地区很有用)', + 'telegram.proxyPlaceholder': 'http://127.0.0.1:7890', + 'telegram.proxyHint': '留空则直接连接。支持 http:// 和 https:// 代理地址。', + 'telegram.proxyInvalidUrl': '代理地址无效,示例:http://127.0.0.1:7890', + 'telegram.proxyInvalidProtocol': '仅支持 http:// 和 https:// 代理地址。', // ── Feishu (Bridge) ────────────────────────────────────── 'feishu.credentials': '应用凭据', diff --git a/src/lib/bridge/adapters/telegram-adapter.ts b/src/lib/bridge/adapters/telegram-adapter.ts index 173220b5..03e81c1d 100644 --- a/src/lib/bridge/adapters/telegram-adapter.ts +++ b/src/lib/bridge/adapters/telegram-adapter.ts @@ -15,7 +15,7 @@ import type { } from '../types'; import type { FileAttachment } from '@/types'; import { BaseChannelAdapter, registerAdapterFactory } from '../channel-adapter'; -import { callTelegramApi, sendMessageDraft } from './telegram-utils'; +import { callTelegramApi, sendMessageDraft, proxyFetchOptions } from './telegram-utils'; import { isImageEnabled, downloadPhoto, @@ -394,7 +394,8 @@ export class TelegramAdapter extends BaseChannelAdapter { const res = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(10_000), - }); + ...proxyFetchOptions(), + } as RequestInit); const data = await res.json(); if (data.ok && data.result?.id) { this.botUserId = String(data.result.id); @@ -484,7 +485,8 @@ export class TelegramAdapter extends BaseChannelAdapter { allowed_updates: ['message', 'callback_query'], }), signal: this.abortController?.signal, - }); + ...proxyFetchOptions(), + } as RequestInit); if (!this.running) break; diff --git a/src/lib/bridge/adapters/telegram-media.ts b/src/lib/bridge/adapters/telegram-media.ts index f8efda8d..5b2ffa1e 100644 --- a/src/lib/bridge/adapters/telegram-media.ts +++ b/src/lib/bridge/adapters/telegram-media.ts @@ -8,6 +8,7 @@ import type { FileAttachment } from '@/types'; import { getSetting } from '../../db'; +import { proxyFetchOptions } from './telegram-utils'; const TELEGRAM_API = 'https://api.telegram.org'; @@ -206,7 +207,8 @@ async function downloadFileById( headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_id: fileId }), signal: AbortSignal.timeout(15_000), - }); + ...proxyFetchOptions(), + } as RequestInit); const getFileData = await getFileRes.json(); if (!getFileData.ok || !getFileData.result?.file_path) { @@ -231,7 +233,8 @@ async function downloadFileById( const downloadUrl = `${TELEGRAM_API}/file/bot${botToken}/${filePath}`; const downloadRes = await fetch(downloadUrl, { signal: AbortSignal.timeout(60_000), - }); + ...proxyFetchOptions(), + } as RequestInit); if (!downloadRes.ok) { console.warn(`[telegram-media] Download failed: HTTP ${downloadRes.status}`); diff --git a/src/lib/bridge/adapters/telegram-utils.ts b/src/lib/bridge/adapters/telegram-utils.ts index 8f180abe..479f3345 100644 --- a/src/lib/bridge/adapters/telegram-utils.ts +++ b/src/lib/bridge/adapters/telegram-utils.ts @@ -5,8 +5,57 @@ * Extracted from telegram-bot.ts to avoid duplication. */ +import { getSetting } from '../../db'; + const TELEGRAM_API = 'https://api.telegram.org'; +// ── Proxy Support ───────────────────────────────────────────── + +/** + * Read the configured Telegram proxy URL from settings. + * Returns an empty string when no proxy is configured. + */ +export function getTelegramProxyUrl(): string { + return getSetting('telegram_proxy_url') || ''; +} + +/** + * Build extra fetch init options to route requests through the configured + * HTTP/HTTPS proxy (if any). Uses undici's ProxyAgent which is bundled + * with Node.js 22+ — no additional npm dependency required. + * + * Returns an empty object when no proxy is set, the URL is invalid, or + * when undici is unavailable. + */ +export function proxyFetchOptions(): Record { + const proxyUrl = getTelegramProxyUrl(); + if (!proxyUrl) return {}; + + // Validate that the proxy URL uses a supported protocol + let parsed: URL; + try { + parsed = new URL(proxyUrl); + } catch { + console.warn('[telegram] Invalid proxy URL (cannot parse), connecting without proxy:', proxyUrl); + return {}; + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + console.warn('[telegram] Unsupported proxy protocol (only http/https supported):', parsed.protocol); + return {}; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { ProxyAgent } = require('undici') as { + ProxyAgent: new (url: string) => { [Symbol.toStringTag]: string }; + }; + return { dispatcher: new ProxyAgent(proxyUrl) }; + } catch { + console.warn('[telegram] undici ProxyAgent unavailable, connecting without proxy'); + return {}; + } +} + export interface TelegramSendResult { ok: boolean; messageId?: string; @@ -45,7 +94,8 @@ export async function callTelegramApi( method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), - }); + ...proxyFetchOptions(), + } as RequestInit); const httpStatus = res.status; const data: TelegramApiResponse = await res.json(); if (!data.ok) {