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) {