feat: 支持 Telegram 代理和自定义反代
This commit is contained in:
1 parent
1634225e31
commit
29582f9124
4 files changed
+290
-27
No files matched your search
@@ -37,6 +37,7 @@ require (
|
|||||||
github.com/valyala/fasttemplate v1.2.2
|
github.com/valyala/fasttemplate v1.2.2
|
||||||
go.uber.org/zap v1.28.0
|
go.uber.org/zap v1.28.0
|
||||||
golang.org/x/crypto v0.54.0
|
golang.org/x/crypto v0.54.0
|
||||||
|
golang.org/x/net v0.57.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/text v0.40.0
|
golang.org/x/text v0.40.0
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
@@ -85,7 +86,6 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/net v0.57.0 // indirect
|
|
||||||
golang.org/x/sync v0.22.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -22,6 +24,7 @@ import (
|
|||||||
"github.com/pika-monitor/pika/internal/utils"
|
"github.com/pika-monitor/pika/internal/utils"
|
||||||
"github.com/valyala/fasttemplate"
|
"github.com/valyala/fasttemplate"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
"gopkg.in/gomail.v2"
|
"gopkg.in/gomail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -540,8 +543,62 @@ func (n *Notifier) sendFeishu(ctx context.Context, webhook, signSecret, message
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildTelegramWebhookURL(botToken, apiBaseURL string) (string, error) {
|
||||||
|
if apiBaseURL == "" {
|
||||||
|
apiBaseURL = "https://api.telegram.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(apiBaseURL)
|
||||||
|
if err != nil || parsedURL.Host == "" || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") {
|
||||||
|
return "", fmt.Errorf("Telegram 自定义反代地址必须是有效的 HTTP/HTTPS 地址")
|
||||||
|
}
|
||||||
|
if parsedURL.RawQuery != "" || parsedURL.Fragment != "" {
|
||||||
|
return "", fmt.Errorf("Telegram 自定义反代地址不能包含查询参数或片段")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL.Path = strings.TrimRight(parsedURL.Path, "/") + "/bot" + botToken + "/sendMessage"
|
||||||
|
return parsedURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTelegramHTTPClient(proxyURL string) (*http.Client, error) {
|
||||||
|
if proxyURL == "" {
|
||||||
|
return sharedHTTPClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(proxyURL)
|
||||||
|
if err != nil || parsedURL.Host == "" {
|
||||||
|
return nil, fmt.Errorf("Telegram 代理地址无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "http":
|
||||||
|
transport.Proxy = http.ProxyURL(parsedURL)
|
||||||
|
case "socks5":
|
||||||
|
dialer, err := proxy.FromURL(parsedURL, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建 Telegram SOCKS5 代理失败: %w", err)
|
||||||
|
}
|
||||||
|
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if contextDialer, ok := dialer.(proxy.ContextDialer); ok {
|
||||||
|
return contextDialer.DialContext(ctx, network, address)
|
||||||
|
}
|
||||||
|
return dialer.Dial(network, address)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Telegram 代理仅支持 http:// 或 socks5://")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Client{Timeout: 10 * time.Second, Transport: transport}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// sendTelegram 发送 Telegram 通知
|
// sendTelegram 发送 Telegram 通知
|
||||||
func (n *Notifier) sendTelegram(ctx context.Context, botToken, chatID, message string) error {
|
func (n *Notifier) sendTelegram(ctx context.Context, botToken, chatID, message, proxyURL, apiBaseURL string) error {
|
||||||
if utf8.RuneCountInString(message) > 4096 {
|
if utf8.RuneCountInString(message) > 4096 {
|
||||||
n.logger.Warn("Telegram消息过长,进行截断",
|
n.logger.Warn("Telegram消息过长,进行截断",
|
||||||
zap.Int("originalLen", utf8.RuneCountInString(message)),
|
zap.Int("originalLen", utf8.RuneCountInString(message)),
|
||||||
@@ -551,14 +608,22 @@ func (n *Notifier) sendTelegram(ctx context.Context, botToken, chatID, message s
|
|||||||
message = string(runes[:4093]) + "..."
|
message = string(runes[:4093]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
webhookURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
|
webhookURL, err := buildTelegramWebhookURL(botToken, apiBaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient, err := newTelegramHTTPClient(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"text": message,
|
"text": message,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := n.sendJSONRequest(ctx, webhookURL, body)
|
_, err = n.sendJSONRequestWithClient(ctx, httpClient, webhookURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -719,6 +784,10 @@ func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertReco
|
|||||||
|
|
||||||
// retryDoWithBackoff 带重试和退避的 HTTP 请求执行
|
// retryDoWithBackoff 带重试和退避的 HTTP 请求执行
|
||||||
func (n *Notifier) retryDoWithBackoff(ctx context.Context, createReq func() (*http.Request, error)) (*http.Response, error) {
|
func (n *Notifier) retryDoWithBackoff(ctx context.Context, createReq func() (*http.Request, error)) (*http.Response, error) {
|
||||||
|
return n.retryDoWithClientBackoff(ctx, sharedHTTPClient, createReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) retryDoWithClientBackoff(ctx context.Context, httpClient *http.Client, createReq func() (*http.Request, error)) (*http.Response, error) {
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for i := 0; i < maxRetries; i++ {
|
for i := 0; i < maxRetries; i++ {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -739,7 +808,7 @@ func (n *Notifier) retryDoWithBackoff(ctx context.Context, createReq func() (*ht
|
|||||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := sharedHTTPClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -815,6 +884,10 @@ func (n *Notifier) sendCustomWebhook(ctx context.Context, config map[string]inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) sendJSONRequest(ctx context.Context, url string, body interface{}) ([]byte, error) {
|
func (n *Notifier) sendJSONRequest(ctx context.Context, url string, body interface{}) ([]byte, error) {
|
||||||
|
return n.sendJSONRequestWithClient(ctx, sharedHTTPClient, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) sendJSONRequestWithClient(ctx context.Context, httpClient *http.Client, url string, body interface{}) ([]byte, error) {
|
||||||
data, err := json.Marshal(body)
|
data, err := json.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("序列化请求体失败: %w", err)
|
return nil, fmt.Errorf("序列化请求体失败: %w", err)
|
||||||
@@ -829,7 +902,7 @@ func (n *Notifier) sendJSONRequest(ctx context.Context, url string, body interfa
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := n.retryDoWithBackoff(ctx, createReq)
|
resp, err := n.retryDoWithClientBackoff(ctx, httpClient, createReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("发送请求失败: %w", err)
|
return nil, fmt.Errorf("发送请求失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -932,7 +1005,10 @@ func (n *Notifier) sendTelegramByConfig(ctx context.Context, config map[string]i
|
|||||||
return fmt.Errorf("Telegram 配置缺少 chatID")
|
return fmt.Errorf("Telegram 配置缺少 chatID")
|
||||||
}
|
}
|
||||||
|
|
||||||
return n.sendTelegram(ctx, botToken, chatID, message)
|
proxyURL, _ := config["proxyURL"].(string)
|
||||||
|
apiBaseURL, _ := config["apiBaseURL"].(string)
|
||||||
|
|
||||||
|
return n.sendTelegram(ctx, botToken, chatID, message, proxyURL, apiBaseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendEmailByConfig 根据配置发送邮件通知
|
// sendEmailByConfig 根据配置发送邮件通知
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildTelegramWebhookURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
apiBaseURL string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "default", want: "https://api.telegram.org/bot123:abc/sendMessage"},
|
||||||
|
{name: "custom host", apiBaseURL: "https://telegram.example.com/", want: "https://telegram.example.com/bot123:abc/sendMessage"},
|
||||||
|
{name: "custom path", apiBaseURL: "http://telegram.example.com/api", want: "http://telegram.example.com/api/bot123:abc/sendMessage"},
|
||||||
|
{name: "unsupported scheme", apiBaseURL: "ftp://telegram.example.com", wantErr: true},
|
||||||
|
{name: "query is rejected", apiBaseURL: "https://telegram.example.com/?key=value", wantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := buildTelegramWebhookURL("123:abc", tt.apiBaseURL)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildTelegramWebhookURL() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("buildTelegramWebhookURL() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTelegramHTTPClient(t *testing.T) {
|
||||||
|
for _, proxyURL := range []string{
|
||||||
|
"http://user:pass@127.0.0.1:7890",
|
||||||
|
"socks5://user:pass@127.0.0.1:1080",
|
||||||
|
} {
|
||||||
|
t.Run(proxyURL, func(t *testing.T) {
|
||||||
|
client, err := newTelegramHTTPClient(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("newTelegramHTTPClient() error = %v", err)
|
||||||
|
}
|
||||||
|
if client == sharedHTTPClient {
|
||||||
|
t.Fatal("proxied Telegram client unexpectedly uses the shared client")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := newTelegramHTTPClient("https://127.0.0.1:7890"); err == nil {
|
||||||
|
t.Fatal("expected HTTPS proxy scheme to be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendTelegramWithCustomAPIBaseURL(t *testing.T) {
|
||||||
|
var gotBody map[string]interface{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/telegram/bot123:abc/sendMessage" {
|
||||||
|
t.Errorf("request path = %q", r.URL.Path)
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||||
|
t.Errorf("decode request body: %v", err)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
notifier := NewNotifier(zap.NewNop())
|
||||||
|
err := notifier.sendTelegram(context.Background(), "123:abc", "456", "hello", "", server.URL+"/telegram/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sendTelegram() error = %v", err)
|
||||||
|
}
|
||||||
|
if gotBody["chat_id"] != "456" || gotBody["text"] != "hello" {
|
||||||
|
t.Fatalf("request body = %#v", gotBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendTelegramWithHTTPProxy(t *testing.T) {
|
||||||
|
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Scheme != "http" || r.URL.Host != "telegram.invalid" {
|
||||||
|
t.Errorf("proxy request URL = %q", r.URL.String())
|
||||||
|
}
|
||||||
|
wantProxyAuthorization := "Basic " + base64.StdEncoding.EncodeToString([]byte("proxy-user:proxy-password"))
|
||||||
|
if got := r.Header.Get("Proxy-Authorization"); got != wantProxyAuthorization {
|
||||||
|
t.Errorf("Proxy-Authorization = %q, want %q", got, wantProxyAuthorization)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||||
|
}))
|
||||||
|
defer proxyServer.Close()
|
||||||
|
|
||||||
|
parsedProxyURL, err := url.Parse(proxyServer.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parsedProxyURL.User = url.UserPassword("proxy-user", "proxy-password")
|
||||||
|
|
||||||
|
notifier := NewNotifier(zap.NewNop())
|
||||||
|
err = notifier.sendTelegram(
|
||||||
|
context.Background(),
|
||||||
|
"123:abc",
|
||||||
|
"456",
|
||||||
|
"hello",
|
||||||
|
parsedProxyURL.String(),
|
||||||
|
"http://telegram.invalid",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sendTelegram() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,43 @@ const NotificationChannels = () => {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateTelegramProxyURL = (_: any, value?: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proxyURL = new URL(value);
|
||||||
|
if (!['http:', 'socks5:'].includes(proxyURL.protocol) || !proxyURL.hostname) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch {
|
||||||
|
return Promise.reject(new Error('请输入 http:// 或 socks5:// 开头的有效代理地址'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateTelegramAPIBaseURL = (_: any, value?: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiBaseURL = new URL(value);
|
||||||
|
if (
|
||||||
|
!['http:', 'https:'].includes(apiBaseURL.protocol)
|
||||||
|
|| !apiBaseURL.hostname
|
||||||
|
|| apiBaseURL.search
|
||||||
|
|| apiBaseURL.hash
|
||||||
|
) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch {
|
||||||
|
return Promise.reject(new Error('请输入 http:// 或 https:// 开头的有效反代地址'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 监听各个通知渠道的启用状态
|
// 监听各个通知渠道的启用状态
|
||||||
const dingtalkEnabled = Form.useWatch('dingtalkEnabled', form);
|
const dingtalkEnabled = Form.useWatch('dingtalkEnabled', form);
|
||||||
const wecomEnabled = Form.useWatch('wecomEnabled', form);
|
const wecomEnabled = Form.useWatch('wecomEnabled', form);
|
||||||
@@ -179,6 +216,8 @@ const NotificationChannels = () => {
|
|||||||
formValues.telegramEnabled = channel.enabled;
|
formValues.telegramEnabled = channel.enabled;
|
||||||
formValues.telegramBotToken = channel.config?.botToken || '';
|
formValues.telegramBotToken = channel.config?.botToken || '';
|
||||||
formValues.telegramChatID = channel.config?.chatID || '';
|
formValues.telegramChatID = channel.config?.chatID || '';
|
||||||
|
formValues.telegramProxyURL = channel.config?.proxyURL || '';
|
||||||
|
formValues.telegramAPIBaseURL = channel.config?.apiBaseURL || '';
|
||||||
} else if (channel.type === 'email') {
|
} else if (channel.type === 'email') {
|
||||||
formValues.emailEnabled = channel.enabled;
|
formValues.emailEnabled = channel.enabled;
|
||||||
formValues.emailSmtpHost = channel.config?.smtpHost || '';
|
formValues.emailSmtpHost = channel.config?.smtpHost || '';
|
||||||
@@ -268,6 +307,8 @@ const NotificationChannels = () => {
|
|||||||
config: {
|
config: {
|
||||||
botToken: values.telegramBotToken || '',
|
botToken: values.telegramBotToken || '',
|
||||||
chatID: values.telegramChatID || '',
|
chatID: values.telegramChatID || '',
|
||||||
|
proxyURL: values.telegramProxyURL || '',
|
||||||
|
apiBaseURL: values.telegramAPIBaseURL || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -457,26 +498,47 @@ const NotificationChannels = () => {
|
|||||||
);
|
);
|
||||||
case 'telegram':
|
case 'telegram':
|
||||||
return (
|
return (
|
||||||
<div className={fieldGridClass}>
|
<div className="space-y-0">
|
||||||
<Form.Item
|
<div className={fieldGridClass}>
|
||||||
label="Bot Token"
|
<Form.Item
|
||||||
name="telegramBotToken"
|
label="Bot Token"
|
||||||
rules={[
|
name="telegramBotToken"
|
||||||
{required: true, message: '请输入 Bot Token'},
|
rules={[
|
||||||
{validator: validateToken}
|
{required: true, message: '请输入 Bot Token'},
|
||||||
]}
|
{validator: validateToken}
|
||||||
tooltip="通过 @BotFather 创建机器人后获得的 token"
|
]}
|
||||||
>
|
tooltip="通过 @BotFather 创建机器人后获得的 token"
|
||||||
<Input.Password placeholder="输入 Bot Token"/>
|
>
|
||||||
</Form.Item>
|
<Input.Password placeholder="输入 Bot Token"/>
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
label="Chat ID"
|
<Form.Item
|
||||||
name="telegramChatID"
|
label="Chat ID"
|
||||||
rules={[{required: true, message: '请输入 Chat ID'}]}
|
name="telegramChatID"
|
||||||
tooltip="可以是用户 ID、群组 ID 或频道 ID,通过 @userinfobot 等机器人获取"
|
rules={[{required: true, message: '请输入 Chat ID'}]}
|
||||||
>
|
tooltip="可以是用户 ID、群组 ID 或频道 ID,通过 @userinfobot 等机器人获取"
|
||||||
<Input placeholder="输入 Chat ID,例如:123456789"/>
|
>
|
||||||
</Form.Item>
|
<Input placeholder="输入 Chat ID,例如:123456789"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="代理地址(HTTP/SOCKS5,可选)"
|
||||||
|
name="telegramProxyURL"
|
||||||
|
rules={[{validator: validateTelegramProxyURL}]}
|
||||||
|
tooltip="支持 HTTP 和 SOCKS5 代理;如需认证,请将账号密码写在代理 URL 中"
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="例如:http://user:pass@127.0.0.1:7890"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="自定义反代地址(可选)"
|
||||||
|
name="telegramAPIBaseURL"
|
||||||
|
rules={[{validator: validateTelegramAPIBaseURL}]}
|
||||||
|
tooltip="Telegram Bot API 的反向代理基址;请求路径会自动拼接 /bot{token}/sendMessage"
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:https://telegram.example.com"/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'email':
|
case 'email':
|
||||||
|
|||||||
Reference in new issue
Block a user