feat(channel): 为 Telegram 和微信企业号添加代理支持

新增 HTTP 客户端工厂函数以支持通过代理发送请求,代理地址可在频道配置中设置。同时修复了微信企业号 TokenStore 更新时代理地址未同步的问题,并确保 Token 存储键值包含代理信息以避免冲突。
This commit is contained in:
2026-02-11 09:44:32 +08:00
parent fbbfa58da1
commit b5a8612b33
5 changed files with 116 additions and 11 deletions
+63
View File
@@ -0,0 +1,63 @@
package channel
import (
"context"
"errors"
"net"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/net/proxy"
)
func newHTTPClient(proxyAddress string, timeout time.Duration) (*http.Client, error) {
transport, err := newHTTPTransport(proxyAddress)
if err != nil {
return nil, err
}
return &http.Client{
Timeout: timeout,
Transport: transport,
}, nil
}
func newHTTPTransport(proxyAddress string) (*http.Transport, error) {
base, ok := http.DefaultTransport.(*http.Transport)
if !ok {
return &http.Transport{}, nil
}
transport := base.Clone()
proxyAddress = strings.TrimSpace(proxyAddress)
if proxyAddress == "" {
transport.Proxy = nil
return transport, nil
}
if !strings.Contains(proxyAddress, "://") {
proxyAddress = "http://" + proxyAddress
}
u, err := url.Parse(proxyAddress)
if err != nil {
return nil, err
}
switch strings.ToLower(u.Scheme) {
case "http", "https":
transport.Proxy = http.ProxyURL(u)
return transport, nil
case "socks5", "socks5h":
normalized := *u
normalized.Scheme = "socks5"
dialer, err := proxy.FromURL(&normalized, proxy.Direct)
if err != nil {
return nil, err
}
transport.Proxy = nil
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
}
return transport, nil
default:
return nil, errors.New("unsupported proxy scheme: " + strings.TrimSpace(u.Scheme))
}
}
+7 -2
View File
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"message-pusher/model"
"net/http"
"time"
"unicode/utf8"
)
@@ -25,6 +25,10 @@ type telegramMessageResponse struct {
func SendTelegramMessage(message *model.Message, user *model.User, channel_ *model.Channel) error {
// https://core.telegram.org/bots/api#sendmessage
client, err := newHTTPClient(channel_.URL, 10*time.Second)
if err != nil {
return err
}
messageRequest := telegramMessageRequest{
ChatId: channel_.AccountId,
}
@@ -53,13 +57,14 @@ func SendTelegramMessage(message *model.Message, user *model.User, channel_ *mod
if err != nil {
return err
}
resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", channel_.Secret), "application/json",
resp, err := client.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", channel_.Secret), "application/json",
bytes.NewBuffer(jsonData))
if err != nil {
return err
}
var res telegramMessageResponse
err = json.NewDecoder(resp.Body).Decode(&res)
_ = resp.Body.Close()
if err != nil {
return err
}
+6 -1
View File
@@ -41,6 +41,7 @@ func channel2item(channel_ *model.Channel) TokenStoreItem {
CorpId: corpId,
AgentSecret: channel_.Secret,
AgentId: agentId,
Proxy: channel_.URL,
}
return item
case model.TypeLarkApp:
@@ -227,12 +228,13 @@ func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channe
CorpId: corpId,
AgentSecret: oldChannel.Secret,
AgentId: agentId,
Proxy: oldChannel.URL,
}
// Yeah, it's a deep copy.
newItem := oldItem
// This means the user updated those fields.
if newChannel.AppId != "" {
corpId, agentId, err := parseWechatCorpAccountAppId(oldChannel.AppId)
corpId, agentId, err := parseWechatCorpAccountAppId(newChannel.AppId)
if err != nil {
common.SysError(err.Error())
return
@@ -243,6 +245,9 @@ func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channe
if newChannel.Secret != "" {
newItem.AgentSecret = newChannel.Secret
}
if newChannel.URL != oldChannel.URL {
newItem.Proxy = newChannel.URL
}
if !oldItem.IsShared() {
TokenStoreRemoveItem(&oldItem)
}
+22 -8
View File
@@ -23,18 +23,19 @@ type WeChatCorpAccountTokenStoreItem struct {
CorpId string
AgentSecret string
AgentId string
Proxy string
AccessToken string
}
func (i *WeChatCorpAccountTokenStoreItem) Key() string {
return i.CorpId + i.AgentId + i.AgentSecret
return i.CorpId + i.AgentId + i.AgentSecret + strings.TrimSpace(i.Proxy)
}
func (i *WeChatCorpAccountTokenStoreItem) IsShared() bool {
appId := fmt.Sprintf("%s|%s", i.CorpId, i.AgentId)
var count int64 = 0
model.DB.Model(&model.Channel{}).Where("secret = ? and app_id = ? and type = ?",
i.AgentSecret, appId, model.TypeWeChatCorpAccount).Count(&count)
model.DB.Model(&model.Channel{}).Where("secret = ? and app_id = ? and type = ? and url = ?",
i.AgentSecret, appId, model.TypeWeChatCorpAccount, strings.TrimSpace(i.Proxy)).Count(&count)
return count > 1
}
@@ -48,8 +49,10 @@ func (i *WeChatCorpAccountTokenStoreItem) Token() string {
func (i *WeChatCorpAccountTokenStoreItem) Refresh() {
// https://work.weixin.qq.com/api/doc/90000/90135/91039
client := http.Client{
Timeout: 5 * time.Second,
client, err := newHTTPClient(i.Proxy, 5*time.Second)
if err != nil {
common.SysError("failed to create http client: " + err.Error())
return
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",
i.CorpId, i.AgentSecret), nil)
@@ -148,13 +151,24 @@ func SendWeChatCorpMessage(message *model.Message, user *model.User, channel_ *m
if err != nil {
return err
}
key := fmt.Sprintf("%s%s%s", corpId, agentId, agentSecret)
accessToken := TokenStoreGetToken(key)
resp, err := http.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", accessToken), "application/json",
proxyAddress := channel_.URL
tokenItem := WeChatCorpAccountTokenStoreItem{
CorpId: corpId,
AgentSecret: agentSecret,
AgentId: agentId,
Proxy: proxyAddress,
}
accessToken := TokenStoreGetToken(tokenItem.Key())
client, err := newHTTPClient(proxyAddress, 10*time.Second)
if err != nil {
return err
}
resp, err := client.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", accessToken), "application/json",
bytes.NewBuffer(jsonData))
if err != nil {
return err
}
defer resp.Body.Close()
var res wechatCorpMessageResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
+18
View File
@@ -337,6 +337,14 @@ const EditChannel = () => {
value={inputs.other}
onChange={handleInputChange}
/>
<Form.Input
label='代理地址(可选)'
name='url'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.url}
placeholder='http://127.0.0.1:7890 或 socks5://127.0.0.1:7890'
/>
</Form.Group>
</>
);
@@ -520,6 +528,16 @@ const EditChannel = () => {
placeholder='在此设置 Telegram 会话 ID'
/>
</Form.Group>
<Form.Group widths={1}>
<Form.Input
label='代理地址(可选)'
name='url'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.url}
placeholder='http://127.0.0.1:7890 或 socks5://127.0.0.1:7890'
/>
</Form.Group>
<Button onClick={getTelegramChatId} loading={loading}>
获取会话 ID
</Button>