feat(telegram): support custom api_base_url for bot api
This commit is contained in:
parent
1265655ef0
commit
12f352420c
7 changed files with 161 additions and 5 deletions
|
|
@ -324,6 +324,7 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We
|
|||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_BOT_TOKEN",
|
||||
"base_url": "",
|
||||
"allow_from": ["YOUR_USER_ID"]
|
||||
}
|
||||
}
|
||||
|
|
@ -331,6 +332,8 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We
|
|||
```
|
||||
|
||||
> Get your user ID from `@userinfobot` on Telegram.
|
||||
>
|
||||
> Set `base_url` when you need a custom Telegram Bot API endpoint (for example, local Bot API server or reverse proxy).
|
||||
|
||||
**3. Run**
|
||||
|
||||
|
|
@ -1188,6 +1191,7 @@ picoclaw agent -m "Hello"
|
|||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "123456:ABC...",
|
||||
"base_url": "",
|
||||
"allow_from": ["123456789"]
|
||||
},
|
||||
"discord": {
|
||||
|
|
|
|||
|
|
@ -713,6 +713,7 @@ picoclaw agent -m "你好"
|
|||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "123456:ABC...",
|
||||
"base_url": "",
|
||||
"allow_from": ["123456789"]
|
||||
},
|
||||
"discord": {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
|||
var opts []telego.BotOption
|
||||
telegramCfg := cfg.Channels.Telegram
|
||||
|
||||
baseURL, err := normalizeTelegramBaseURL(telegramCfg.BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid telegram base_url %q: %w", telegramCfg.BaseURL, err)
|
||||
}
|
||||
if baseURL != "" {
|
||||
opts = append(opts, telego.WithAPIServer(baseURL))
|
||||
}
|
||||
|
||||
if telegramCfg.Proxy != "" {
|
||||
proxyURL, parseErr := url.Parse(telegramCfg.Proxy)
|
||||
if parseErr != nil {
|
||||
|
|
@ -72,10 +80,6 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
|||
}))
|
||||
}
|
||||
|
||||
if baseURL := strings.TrimRight(strings.TrimSpace(telegramCfg.BaseURL), "/"); baseURL != "" {
|
||||
opts = append(opts, telego.WithAPIServer(baseURL))
|
||||
}
|
||||
|
||||
bot, err := telego.NewBot(telegramCfg.Token, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
|
||||
|
|
@ -100,6 +104,30 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
|||
}, nil
|
||||
}
|
||||
|
||||
func normalizeTelegramBaseURL(raw string) (string, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
normalized := strings.TrimRight(trimmed, "/")
|
||||
parsed, err := url.Parse(normalized)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", fmt.Errorf("scheme must be http or https")
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return "", fmt.Errorf("host is required")
|
||||
}
|
||||
if parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return "", fmt.Errorf("query and fragment are not supported")
|
||||
}
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (c *TelegramChannel) Start(ctx context.Context) error {
|
||||
logger.InfoC("telegram", "Starting Telegram bot (polling mode)...")
|
||||
|
||||
|
|
|
|||
93
pkg/channels/telegram/telegram_baseurl_test.go
Normal file
93
pkg/channels/telegram/telegram_baseurl_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestNormalizeTelegramBaseURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty value is allowed",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "trim spaces and trailing slash",
|
||||
input: " https://telegram-proxy.example.com/custom/ ",
|
||||
want: "https://telegram-proxy.example.com/custom",
|
||||
},
|
||||
{
|
||||
name: "missing scheme",
|
||||
input: "telegram-proxy.example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported scheme",
|
||||
input: "ftp://telegram-proxy.example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "query not allowed",
|
||||
input: "https://telegram-proxy.example.com/api?x=1",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := normalizeTelegramBaseURL(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeTelegramBaseURL error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("normalizeTelegramBaseURL() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTelegramChannel_UsesCustomBaseURL(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channels.Telegram.Token = "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
|
||||
cfg.Channels.Telegram.BaseURL = "https://telegram-proxy.example.com/custom/"
|
||||
|
||||
channel, err := NewTelegramChannel(cfg, bus.NewMessageBus())
|
||||
if err != nil {
|
||||
t.Fatalf("NewTelegramChannel error: %v", err)
|
||||
}
|
||||
|
||||
got := channel.bot.FileDownloadURL("photos/abc.jpg")
|
||||
wantPrefix := "https://telegram-proxy.example.com/custom/file/bot123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi/"
|
||||
if !strings.HasPrefix(got, wantPrefix) {
|
||||
t.Fatalf("FileDownloadURL prefix = %q, want prefix %q", got, wantPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTelegramChannel_InvalidCustomBaseURLErrors(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channels.Telegram.Token = "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
|
||||
cfg.Channels.Telegram.BaseURL = "telegram-proxy.example.com"
|
||||
|
||||
_, err := NewTelegramChannel(cfg, bus.NewMessageBus())
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid telegram base_url") {
|
||||
t.Fatalf("error = %q, expected invalid base_url message", err.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -189,6 +189,31 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTelegramConfig_BaseURLParse(t *testing.T) {
|
||||
jsonData := `{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "123456:ABC",
|
||||
"base_url": "https://telegram-proxy.example.com/custom"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
cfg := DefaultConfig()
|
||||
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Channels.Telegram.BaseURL != "https://telegram-proxy.example.com/custom" {
|
||||
t.Errorf(
|
||||
"Telegram.BaseURL = %q, want %q",
|
||||
cfg.Channels.Telegram.BaseURL,
|
||||
"https://telegram-proxy.example.com/custom",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default
|
||||
func TestDefaultConfig_HeartbeatEnabled(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
|
@ -277,6 +302,9 @@ func TestDefaultConfig_Channels(t *testing.T) {
|
|||
if cfg.Channels.Telegram.Enabled {
|
||||
t.Error("Telegram should be disabled by default")
|
||||
}
|
||||
if cfg.Channels.Telegram.BaseURL != "" {
|
||||
t.Errorf("Telegram BaseURL should be empty by default, got %q", cfg.Channels.Telegram.BaseURL)
|
||||
}
|
||||
if cfg.Channels.Discord.Enabled {
|
||||
t.Error("Discord should be disabled by default")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ func DefaultConfig() *Config {
|
|||
Telegram: TelegramConfig{
|
||||
Enabled: false,
|
||||
Token: "",
|
||||
BaseURL: "",
|
||||
Proxy: "",
|
||||
AllowFrom: FlexibleStringSlice{},
|
||||
Typing: TypingConfig{Enabled: true},
|
||||
Placeholder: PlaceholderConfig{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue