add channel-specific build variants

This commit is contained in:
lpf
2026-03-10 14:18:29 +08:00
parent c7b159d2ed
commit 4a1b5f27e4
23 changed files with 608 additions and 173 deletions

View File

@@ -0,0 +1,44 @@
package channels
import (
"context"
"fmt"
"github.com/YspCoder/clawgo/pkg/bus"
)
func errChannelDisabled(name string) error {
return fmt.Errorf("%s channel is disabled at build time", name)
}
type disabledChannel struct {
name string
}
func (c disabledChannel) Name() string {
return c.name
}
func (c disabledChannel) Start(ctx context.Context) error {
return errChannelDisabled(c.name)
}
func (c disabledChannel) Stop(ctx context.Context) error {
return nil
}
func (c disabledChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
return errChannelDisabled(c.name)
}
func (c disabledChannel) IsRunning() bool {
return false
}
func (c disabledChannel) IsAllowed(senderID string) bool {
return false
}
func (c disabledChannel) HealthCheck(ctx context.Context) error {
return errChannelDisabled(c.name)
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_dingtalk
// ClawGo - Ultra-lightweight personal AI agent
// DingTalk channel implementation using Stream Mode

View File

@@ -0,0 +1,14 @@
//go:build omit_dingtalk
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type DingTalkChannel struct{ disabledChannel }
func NewDingTalkChannel(cfg config.DingTalkConfig, bus *bus.MessageBus) (*DingTalkChannel, error) {
return nil, errChannelDisabled("dingtalk")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_discord
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_discord
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type DiscordChannel struct{ disabledChannel }
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
return nil, errChannelDisabled("discord")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_feishu
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_feishu
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type FeishuChannel struct{ disabledChannel }
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
return nil, errChannelDisabled("feishu")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_maixcam
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_maixcam
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type MaixCamChannel struct{ disabledChannel }
func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamChannel, error) {
return nil, errChannelDisabled("maixcam")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_qq
package channels
import (

14
pkg/channels/qq_stub.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build omit_qq
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type QQChannel struct{ disabledChannel }
func NewQQChannel(cfg config.QQConfig, bus *bus.MessageBus) (*QQChannel, error) {
return nil, errChannelDisabled("qq")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_telegram
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_telegram
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type TelegramChannel struct{ disabledChannel }
func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*TelegramChannel, error) {
return nil, errChannelDisabled("telegram")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (
@@ -9,7 +11,6 @@ import (
"mime"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -27,31 +28,6 @@ import (
_ "modernc.org/sqlite"
)
type WhatsAppBridgeStatus struct {
State string `json:"state"`
Connected bool `json:"connected"`
LoggedIn bool `json:"logged_in"`
BridgeAddr string `json:"bridge_addr"`
UserJID string `json:"user_jid,omitempty"`
PushName string `json:"push_name,omitempty"`
Platform string `json:"platform,omitempty"`
QRCode string `json:"qr_code,omitempty"`
QRAvailable bool `json:"qr_available"`
LastEvent string `json:"last_event,omitempty"`
LastError string `json:"last_error,omitempty"`
UpdatedAt string `json:"updated_at"`
InboundCount int `json:"inbound_count"`
OutboundCount int `json:"outbound_count"`
ReadReceiptCount int `json:"read_receipt_count"`
LastInboundAt string `json:"last_inbound_at,omitempty"`
LastOutboundAt string `json:"last_outbound_at,omitempty"`
LastReadAt string `json:"last_read_at,omitempty"`
LastInboundFrom string `json:"last_inbound_from,omitempty"`
LastOutboundTo string `json:"last_outbound_to,omitempty"`
LastInboundText string `json:"last_inbound_text,omitempty"`
LastOutboundText string `json:"last_outbound_text,omitempty"`
}
type WhatsAppBridgeService struct {
addr string
stateDir string
@@ -818,36 +794,10 @@ func (s *WhatsAppBridgeService) closeWSClients() {
}
}
func ParseWhatsAppBridgeListenAddr(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if strings.Contains(raw, "://") {
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
if strings.TrimSpace(u.Host) == "" {
return "", fmt.Errorf("bridge url host is required")
}
return u.Host, nil
}
return raw, nil
}
func (s *WhatsAppBridgeService) ServeLogout(w http.ResponseWriter, r *http.Request) {
s.wrapHandler(s.handleLogout)(w, r)
}
func BridgeStatusURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "status")
}
func BridgeLogoutURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "logout")
}
func normalizeWhatsAppRecipientJID(raw string) (types.JID, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
@@ -895,102 +845,3 @@ func extractWhatsAppMessageText(msg *waProto.Message) string {
return ""
}
}
func bridgeEndpointURL(raw, endpoint string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if !strings.Contains(raw, "://") {
raw = "ws://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
switch u.Scheme {
case "wss":
u.Scheme = "https"
default:
u.Scheme = "http"
}
u.Path = bridgeSiblingPath(u.Path, endpoint)
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
}
func bridgeSiblingPath(pathValue, endpoint string) string {
pathValue = strings.TrimSpace(pathValue)
if endpoint == "" {
endpoint = "status"
}
if pathValue == "" || pathValue == "/" {
return "/" + endpoint
}
trimmed := strings.TrimSuffix(pathValue, "/")
if strings.HasSuffix(trimmed, "/ws") {
return strings.TrimSuffix(trimmed, "/ws") + "/" + endpoint
}
return trimmed + "/" + endpoint
}
func normalizeBridgeBasePath(basePath string) string {
basePath = strings.TrimSpace(basePath)
if basePath == "" || basePath == "/" {
return "/"
}
if !strings.HasPrefix(basePath, "/") {
basePath = "/" + basePath
}
return strings.TrimSuffix(basePath, "/")
}
func joinBridgeRoute(basePath, endpoint string) string {
basePath = normalizeBridgeBasePath(basePath)
if basePath == "/" {
return "/" + strings.TrimPrefix(endpoint, "/")
}
return basePath + "/" + strings.TrimPrefix(endpoint, "/")
}
func isLocalRequest(r *http.Request) bool {
if r == nil {
return false
}
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
return isLocalRemoteAddr(strings.TrimSpace(r.RemoteAddr), addrs)
}
func isLocalRemoteAddr(remoteAddr string, localAddrs []net.Addr) bool {
host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr))
if err != nil {
host = strings.TrimSpace(remoteAddr)
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() {
return true
}
for _, addr := range localAddrs {
if addr == nil {
continue
}
switch v := addr.(type) {
case *net.IPNet:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
case *net.IPAddr:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,56 @@
//go:build omit_whatsapp
package channels
import (
"context"
"net/http"
"strings"
)
type WhatsAppBridgeService struct {
addr string
stateDir string
printQR bool
status WhatsAppBridgeStatus
}
func NewWhatsAppBridgeService(addr, stateDir string, printQR bool) *WhatsAppBridgeService {
return &WhatsAppBridgeService{
addr: strings.TrimSpace(addr),
stateDir: strings.TrimSpace(stateDir),
printQR: printQR,
status: WhatsAppBridgeStatus{
State: "disabled",
BridgeAddr: strings.TrimSpace(addr),
},
}
}
func (s *WhatsAppBridgeService) Start(ctx context.Context) error {
return errChannelDisabled("whatsapp")
}
func (s *WhatsAppBridgeService) StartEmbedded(ctx context.Context) error {
return errChannelDisabled("whatsapp")
}
func (s *WhatsAppBridgeService) Stop() {}
func (s *WhatsAppBridgeService) RegisterRoutes(mux *http.ServeMux, basePath string) {}
func (s *WhatsAppBridgeService) StatusSnapshot() WhatsAppBridgeStatus {
return s.status
}
func (s *WhatsAppBridgeService) ServeWS(w http.ResponseWriter, r *http.Request) {
http.Error(w, errChannelDisabled("whatsapp").Error(), http.StatusNotImplemented)
}
func (s *WhatsAppBridgeService) ServeStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, errChannelDisabled("whatsapp").Error(), http.StatusNotImplemented)
}
func (s *WhatsAppBridgeService) ServeLogout(w http.ResponseWriter, r *http.Request) {
http.Error(w, errChannelDisabled("whatsapp").Error(), http.StatusNotImplemented)
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (

View File

@@ -0,0 +1,159 @@
package channels
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
)
type WhatsAppBridgeStatus struct {
State string `json:"state"`
Connected bool `json:"connected"`
LoggedIn bool `json:"logged_in"`
BridgeAddr string `json:"bridge_addr"`
UserJID string `json:"user_jid,omitempty"`
PushName string `json:"push_name,omitempty"`
Platform string `json:"platform,omitempty"`
QRCode string `json:"qr_code,omitempty"`
QRAvailable bool `json:"qr_available"`
LastEvent string `json:"last_event,omitempty"`
LastError string `json:"last_error,omitempty"`
UpdatedAt string `json:"updated_at"`
InboundCount int `json:"inbound_count"`
OutboundCount int `json:"outbound_count"`
ReadReceiptCount int `json:"read_receipt_count"`
LastInboundAt string `json:"last_inbound_at,omitempty"`
LastOutboundAt string `json:"last_outbound_at,omitempty"`
LastReadAt string `json:"last_read_at,omitempty"`
LastInboundFrom string `json:"last_inbound_from,omitempty"`
LastOutboundTo string `json:"last_outbound_to,omitempty"`
LastInboundText string `json:"last_inbound_text,omitempty"`
LastOutboundText string `json:"last_outbound_text,omitempty"`
}
func ParseWhatsAppBridgeListenAddr(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if strings.Contains(raw, "://") {
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
if strings.TrimSpace(u.Host) == "" {
return "", fmt.Errorf("bridge url host is required")
}
return u.Host, nil
}
return raw, nil
}
func BridgeStatusURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "status")
}
func BridgeLogoutURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "logout")
}
func bridgeEndpointURL(raw, endpoint string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if !strings.Contains(raw, "://") {
raw = "ws://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
switch u.Scheme {
case "wss":
u.Scheme = "https"
default:
u.Scheme = "http"
}
u.Path = bridgeSiblingPath(u.Path, endpoint)
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
}
func bridgeSiblingPath(pathValue, endpoint string) string {
pathValue = strings.TrimSpace(pathValue)
if endpoint == "" {
endpoint = "status"
}
if pathValue == "" || pathValue == "/" {
return "/" + endpoint
}
trimmed := strings.TrimSuffix(pathValue, "/")
if strings.HasSuffix(trimmed, "/ws") {
return strings.TrimSuffix(trimmed, "/ws") + "/" + endpoint
}
return trimmed + "/" + endpoint
}
func normalizeBridgeBasePath(basePath string) string {
basePath = strings.TrimSpace(basePath)
if basePath == "" || basePath == "/" {
return "/"
}
if !strings.HasPrefix(basePath, "/") {
basePath = "/" + basePath
}
return strings.TrimSuffix(basePath, "/")
}
func joinBridgeRoute(basePath, endpoint string) string {
basePath = normalizeBridgeBasePath(basePath)
if basePath == "/" {
return "/" + strings.TrimPrefix(endpoint, "/")
}
return basePath + "/" + strings.TrimPrefix(endpoint, "/")
}
func isLocalRequest(r *http.Request) bool {
if r == nil {
return false
}
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
return isLocalRemoteAddr(strings.TrimSpace(r.RemoteAddr), addrs)
}
func isLocalRemoteAddr(remoteAddr string, localAddrs []net.Addr) bool {
host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr))
if err != nil {
host = strings.TrimSpace(remoteAddr)
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() {
return true
}
for _, addr := range localAddrs {
if addr == nil {
continue
}
switch v := addr.(type) {
case *net.IPNet:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
case *net.IPAddr:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,14 @@
//go:build omit_whatsapp
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type WhatsAppChannel struct{ disabledChannel }
func NewWhatsAppChannel(cfg config.WhatsAppConfig, bus *bus.MessageBus) (*WhatsAppChannel, error) {
return nil, errChannelDisabled("whatsapp")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (