mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 22:09:37 +08:00
add channel-specific build variants
This commit is contained in:
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -10,6 +10,14 @@ on:
|
||||
description: 'Release tag (e.g. v1.2.3). If empty, uses current ref name.'
|
||||
required: false
|
||||
type: string
|
||||
build_targets:
|
||||
description: 'Space-separated GOOS/GOARCH targets. Leave empty to use Makefile defaults.'
|
||||
required: false
|
||||
type: string
|
||||
channel_variants:
|
||||
description: 'Space-separated package variants. Leave empty to use full none and all single-channel variants.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -17,6 +25,9 @@ permissions:
|
||||
jobs:
|
||||
build-and-package:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_TARGETS: ${{ inputs.build_targets }}
|
||||
CHANNEL_PACKAGE_VARIANTS: ${{ inputs.channel_variants }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -58,7 +69,15 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make clean
|
||||
make package-all VERSION="${{ steps.ver.outputs.version }}"
|
||||
if [ -n "${BUILD_TARGETS:-}" ] && [ -n "${CHANNEL_PACKAGE_VARIANTS:-}" ]; then
|
||||
make package-all VERSION="${{ steps.ver.outputs.version }}" BUILD_TARGETS="${BUILD_TARGETS}" CHANNEL_PACKAGE_VARIANTS="${CHANNEL_PACKAGE_VARIANTS}"
|
||||
elif [ -n "${BUILD_TARGETS:-}" ]; then
|
||||
make package-all VERSION="${{ steps.ver.outputs.version }}" BUILD_TARGETS="${BUILD_TARGETS}"
|
||||
elif [ -n "${CHANNEL_PACKAGE_VARIANTS:-}" ]; then
|
||||
make package-all VERSION="${{ steps.ver.outputs.version }}" CHANNEL_PACKAGE_VARIANTS="${CHANNEL_PACKAGE_VARIANTS}"
|
||||
else
|
||||
make package-all VERSION="${{ steps.ver.outputs.version }}"
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
110
Makefile
110
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: all build build-linux-slim build-all build-webui package-all install install-win uninstall clean help test test-docker install-bootstrap-docs sync-embed-workspace sync-embed-workspace-base sync-embed-webui cleanup-embed-workspace test-only clean-test-artifacts dev
|
||||
.PHONY: all build build-variants build-linux-slim build-all build-all-variants build-webui package-all install install-win uninstall clean help test test-docker install-bootstrap-docs sync-embed-workspace sync-embed-workspace-base sync-embed-webui cleanup-embed-workspace test-only clean-test-artifacts dev
|
||||
|
||||
# Build variables
|
||||
BINARY_NAME=clawgo
|
||||
@@ -33,6 +33,12 @@ LINUX_SLIM_PATH=$(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)-slim
|
||||
|
||||
# Cross-platform build matrix (space-separated GOOS/GOARCH pairs)
|
||||
BUILD_TARGETS?=linux/amd64 linux/arm64 linux/riscv64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64
|
||||
CHANNELS?=telegram discord feishu maixcam qq dingtalk whatsapp
|
||||
CHANNEL_PACKAGE_VARIANTS?=full none $(CHANNELS)
|
||||
empty:=
|
||||
space:=$(empty) $(empty)
|
||||
comma:=,
|
||||
ALL_CHANNEL_OMIT_TAGS=$(subst $(space),$(comma),$(addprefix omit_,$(CHANNELS)))
|
||||
|
||||
# Installation
|
||||
INSTALL_PREFIX?=/usr/local
|
||||
@@ -108,6 +114,38 @@ build: sync-embed-workspace
|
||||
@echo "Build complete: $(BINARY_PATH)"
|
||||
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
|
||||
|
||||
## build-variants: Build current-platform full, no-channel, and per-channel binaries
|
||||
build-variants: sync-embed-workspace
|
||||
@echo "Building channel variants for $(PLATFORM)/$(ARCH): $(CHANNEL_PACKAGE_VARIANTS)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@set -e; trap '$(MAKE) cleanup-embed-workspace' EXIT; \
|
||||
for variant in $(CHANNEL_PACKAGE_VARIANTS); do \
|
||||
tags=""; \
|
||||
suffix=""; \
|
||||
if [ "$$variant" = "none" ]; then \
|
||||
tags="$(ALL_CHANNEL_OMIT_TAGS)"; \
|
||||
suffix="-nochannels"; \
|
||||
elif [ "$$variant" != "full" ]; then \
|
||||
for ch in $(CHANNELS); do \
|
||||
if [ "$$ch" != "$$variant" ]; then \
|
||||
tags="$${tags:+$$tags,}omit_$$ch"; \
|
||||
fi; \
|
||||
done; \
|
||||
suffix="-$$variant"; \
|
||||
fi; \
|
||||
out="$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH)$$suffix"; \
|
||||
echo " -> $$variant"; \
|
||||
if [ -n "$$tags" ]; then \
|
||||
$(GO) build $(GOFLAGS) $(BUILD_FLAGS) -tags "$$tags" $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
|
||||
else \
|
||||
$(GO) build $(GOFLAGS) $(BUILD_FLAGS) $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
|
||||
fi; \
|
||||
if [ "$(COMPRESS_BINARY)" = "1" ] && command -v upx >/dev/null 2>&1; then \
|
||||
upx $(UPX_FLAGS) "$$out" >/dev/null; \
|
||||
fi; \
|
||||
done
|
||||
@echo "Variant builds complete: $(BUILD_DIR)"
|
||||
|
||||
## build-linux-slim: Build a Linux-only slim binary (no feature trimming, no channel disabling)
|
||||
build-linux-slim: sync-embed-workspace
|
||||
@echo "Building $(BINARY_NAME) slim profile for linux/$(ARCH)..."
|
||||
@@ -142,6 +180,43 @@ build-all: sync-embed-workspace
|
||||
done
|
||||
@echo "All builds complete"
|
||||
|
||||
## build-all-variants: Build full, no-channel, and per-channel binaries for all configured platforms
|
||||
build-all-variants: sync-embed-workspace
|
||||
@echo "Building all channel variants for multiple platforms: $(BUILD_TARGETS)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@set -e; trap '$(MAKE) cleanup-embed-workspace' EXIT; \
|
||||
for target in $(BUILD_TARGETS); do \
|
||||
goos="$${target%/*}"; \
|
||||
goarch="$${target#*/}"; \
|
||||
for variant in $(CHANNEL_PACKAGE_VARIANTS); do \
|
||||
tags=""; \
|
||||
suffix=""; \
|
||||
if [ "$$variant" = "none" ]; then \
|
||||
tags="$(ALL_CHANNEL_OMIT_TAGS)"; \
|
||||
suffix="-nochannels"; \
|
||||
elif [ "$$variant" != "full" ]; then \
|
||||
for ch in $(CHANNELS); do \
|
||||
if [ "$$ch" != "$$variant" ]; then \
|
||||
tags="$${tags:+$$tags,}omit_$$ch"; \
|
||||
fi; \
|
||||
done; \
|
||||
suffix="-$$variant"; \
|
||||
fi; \
|
||||
out="$(BUILD_DIR)/$(BINARY_NAME)-$$goos-$$goarch$$suffix"; \
|
||||
if [ "$$goos" = "windows" ]; then out="$$out.exe"; fi; \
|
||||
echo " -> $$goos/$$goarch [$$variant]"; \
|
||||
if [ -n "$$tags" ]; then \
|
||||
CGO_ENABLED=0 GOOS=$$goos GOARCH=$$goarch $(GO) build $(GOFLAGS) $(BUILD_FLAGS) -tags "$$tags" $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
|
||||
else \
|
||||
CGO_ENABLED=0 GOOS=$$goos GOARCH=$$goarch $(GO) build $(GOFLAGS) $(BUILD_FLAGS) $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
|
||||
fi; \
|
||||
if [ "$(COMPRESS_BINARY)" = "1" ] && command -v upx >/dev/null 2>&1; then \
|
||||
upx $(UPX_FLAGS) "$$out" >/dev/null; \
|
||||
fi; \
|
||||
done; \
|
||||
done
|
||||
@echo "All variant builds complete"
|
||||
|
||||
## build-webui: Install WebUI dependencies when needed and build dist assets
|
||||
build-webui:
|
||||
@echo "Building WebUI..."
|
||||
@@ -164,22 +239,33 @@ build-webui:
|
||||
fi; \
|
||||
(cd "$(DEV_WEBUI_DIR)" && "$(NPM)" run build)
|
||||
|
||||
## package-all: Create compressed archives and checksums for all build targets
|
||||
package-all: build-all
|
||||
## package-all: Create compressed archives and checksums for full, no-channel, and per-channel build variants
|
||||
package-all: build-all-variants
|
||||
@echo "Packaging build artifacts..."
|
||||
@set -e; cd $(BUILD_DIR); \
|
||||
for target in $(BUILD_TARGETS); do \
|
||||
goos="$${target%/*}"; \
|
||||
goarch="$${target#*/}"; \
|
||||
bin="$(BINARY_NAME)-$$goos-$$goarch"; \
|
||||
if [ "$$goos" = "windows" ]; then \
|
||||
bin="$$bin.exe"; \
|
||||
archive="$(BINARY_NAME)-$$goos-$$goarch.zip"; \
|
||||
zip -q -j "$$archive" "$$bin"; \
|
||||
else \
|
||||
archive="$(BINARY_NAME)-$$goos-$$goarch.tar.gz"; \
|
||||
tar -czf "$$archive" "$$bin"; \
|
||||
fi; \
|
||||
for variant in $(CHANNEL_PACKAGE_VARIANTS); do \
|
||||
suffix=""; \
|
||||
archive_suffix=""; \
|
||||
if [ "$$variant" = "none" ]; then \
|
||||
suffix="-nochannels"; \
|
||||
archive_suffix="-nochannels"; \
|
||||
elif [ "$$variant" != "full" ]; then \
|
||||
suffix="-$$variant"; \
|
||||
archive_suffix="-$$variant"; \
|
||||
fi; \
|
||||
bin="$(BINARY_NAME)-$$goos-$$goarch$$suffix"; \
|
||||
if [ "$$goos" = "windows" ]; then \
|
||||
bin="$$bin.exe"; \
|
||||
archive="$(BINARY_NAME)-$$goos-$$goarch$$archive_suffix.zip"; \
|
||||
zip -q -j "$$archive" "$$bin"; \
|
||||
else \
|
||||
archive="$(BINARY_NAME)-$$goos-$$goarch$$archive_suffix.tar.gz"; \
|
||||
tar -czf "$$archive" "$$bin"; \
|
||||
fi; \
|
||||
done; \
|
||||
done
|
||||
@set -e; cd $(BUILD_DIR); \
|
||||
if command -v sha256sum >/dev/null 2>&1; then \
|
||||
|
||||
122
install.sh
122
install.sh
@@ -5,6 +5,9 @@ OWNER="YspCoder"
|
||||
REPO="clawgo"
|
||||
BIN="clawgo"
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
VARIANT="${CLAWGO_CHANNEL_VARIANT:-full}"
|
||||
VARIANT_EXPLICIT=0
|
||||
CHANNEL_VARIANTS=(full none telegram discord feishu maixcam qq dingtalk whatsapp)
|
||||
CONFIG_DIR="$HOME/.clawgo"
|
||||
CONFIG_PATH="$CONFIG_DIR/config.json"
|
||||
WORKSPACE_DIR="$HOME/.clawgo/workspace"
|
||||
@@ -12,12 +15,13 @@ LEGACY_WORKSPACE_DIR="$HOME/.openclaw/workspace"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0
|
||||
Usage: $0 [--variant full|none|telegram|discord|feishu|maixcam|qq|dingtalk|whatsapp]
|
||||
|
||||
Install or upgrade ClawGo from the latest GitHub release.
|
||||
|
||||
Notes:
|
||||
- WebUI is embedded in the binary and initialized when you run 'clawgo onboard'.
|
||||
- Variant 'none' installs the no-channel build.
|
||||
- OpenClaw migration is offered only when a legacy workspace is detected.
|
||||
EOF
|
||||
}
|
||||
@@ -77,6 +81,101 @@ tty_read() {
|
||||
printf -v "$__var_name" '%s' "$__reply"
|
||||
}
|
||||
|
||||
is_valid_variant() {
|
||||
local candidate="$1"
|
||||
local item
|
||||
for item in "${CHANNEL_VARIANTS[@]}"; do
|
||||
if [[ "$item" == "$candidate" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--variant)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
warn "--variant requires a value"
|
||||
exit 1
|
||||
fi
|
||||
VARIANT="$2"
|
||||
VARIANT_EXPLICIT=1
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
warn "Unknown argument: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
choose_variant() {
|
||||
if ! is_valid_variant "$VARIANT"; then
|
||||
warn "Unsupported variant: $VARIANT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$VARIANT_EXPLICIT" == "1" ]]; then
|
||||
log "Selected variant: $VARIANT"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -r /dev/tty ]]; then
|
||||
log "Selected variant: $VARIANT"
|
||||
return
|
||||
fi
|
||||
|
||||
log "Choose install variant:"
|
||||
log " 1. full Full build with all channels"
|
||||
log " 2. none No-channel build"
|
||||
log " 3. telegram Telegram-only build"
|
||||
log " 4. discord Discord-only build"
|
||||
log " 5. feishu Feishu-only build"
|
||||
log " 6. maixcam MaixCam-only build"
|
||||
log " 7. qq QQ-only build"
|
||||
log " 8. dingtalk DingTalk-only build"
|
||||
log " 9. whatsapp WhatsApp-only build"
|
||||
|
||||
local choice default_choice
|
||||
case "$VARIANT" in
|
||||
full) default_choice="1" ;;
|
||||
none) default_choice="2" ;;
|
||||
telegram) default_choice="3" ;;
|
||||
discord) default_choice="4" ;;
|
||||
feishu) default_choice="5" ;;
|
||||
maixcam) default_choice="6" ;;
|
||||
qq) default_choice="7" ;;
|
||||
dingtalk) default_choice="8" ;;
|
||||
whatsapp) default_choice="9" ;;
|
||||
*) default_choice="1" ;;
|
||||
esac
|
||||
tty_read choice "Enter your choice (1-9, default $default_choice): " "$default_choice"
|
||||
case "$choice" in
|
||||
1) VARIANT="full" ;;
|
||||
2) VARIANT="none" ;;
|
||||
3) VARIANT="telegram" ;;
|
||||
4) VARIANT="discord" ;;
|
||||
5) VARIANT="feishu" ;;
|
||||
6) VARIANT="maixcam" ;;
|
||||
7) VARIANT="qq" ;;
|
||||
8) VARIANT="dingtalk" ;;
|
||||
9) VARIANT="whatsapp" ;;
|
||||
*)
|
||||
warn "Invalid variant selection: $choice"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
log "Selected variant: $VARIANT"
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
@@ -110,7 +209,14 @@ fetch_latest_tag() {
|
||||
}
|
||||
|
||||
install_binary() {
|
||||
local file="${BIN}-${OS}-${ARCH}.tar.gz"
|
||||
local suffix=""
|
||||
if [[ "$VARIANT" == "none" ]]; then
|
||||
suffix="-nochannels"
|
||||
elif [[ "$VARIANT" != "full" ]]; then
|
||||
suffix="-$VARIANT"
|
||||
fi
|
||||
|
||||
local file="${BIN}-${OS}-${ARCH}${suffix}.tar.gz"
|
||||
local url="https://github.com/$OWNER/$REPO/releases/download/$TAG/$file"
|
||||
local out="$TMPDIR/$file"
|
||||
|
||||
@@ -121,10 +227,10 @@ install_binary() {
|
||||
local extracted_bin=""
|
||||
if [[ -f "$TMPDIR/$BIN" ]]; then
|
||||
extracted_bin="$TMPDIR/$BIN"
|
||||
elif [[ -f "$TMPDIR/${BIN}-${OS}-${ARCH}" ]]; then
|
||||
extracted_bin="$TMPDIR/${BIN}-${OS}-${ARCH}"
|
||||
elif [[ -f "$TMPDIR/${BIN}-${OS}-${ARCH}${suffix}" ]]; then
|
||||
extracted_bin="$TMPDIR/${BIN}-${OS}-${ARCH}${suffix}"
|
||||
else
|
||||
extracted_bin="$(find "$TMPDIR" -maxdepth 3 -type f \( -name "$BIN" -o -name "${BIN}-${OS}-${ARCH}" -o -name "${BIN}-*" \) ! -name "*.tar.gz" ! -name "*.zip" | head -n1)"
|
||||
extracted_bin="$(find "$TMPDIR" -maxdepth 3 -type f \( -name "$BIN" -o -name "${BIN}-${OS}-${ARCH}${suffix}" -o -name "${BIN}-*" \) ! -name "*.tar.gz" ! -name "*.zip" | head -n1)"
|
||||
fi
|
||||
|
||||
if [[ -z "$extracted_bin" || ! -f "$extracted_bin" ]]; then
|
||||
@@ -290,13 +396,11 @@ offer_onboard() {
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
parse_args "$@"
|
||||
|
||||
detect_platform
|
||||
fetch_latest_tag
|
||||
choose_variant
|
||||
|
||||
TMPDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
44
pkg/channels/channel_disabled.go
Normal file
44
pkg/channels/channel_disabled.go
Normal 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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_dingtalk
|
||||
|
||||
// ClawGo - Ultra-lightweight personal AI agent
|
||||
// DingTalk channel implementation using Stream Mode
|
||||
|
||||
|
||||
14
pkg/channels/dingtalk_stub.go
Normal file
14
pkg/channels/dingtalk_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_discord
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
14
pkg/channels/discord_stub.go
Normal file
14
pkg/channels/discord_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_feishu
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
14
pkg/channels/feishu_stub.go
Normal file
14
pkg/channels/feishu_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_maixcam
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
14
pkg/channels/maixcam_stub.go
Normal file
14
pkg/channels/maixcam_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_qq
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
14
pkg/channels/qq_stub.go
Normal file
14
pkg/channels/qq_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_telegram
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
14
pkg/channels/telegram_stub.go
Normal file
14
pkg/channels/telegram_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_whatsapp
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
56
pkg/channels/whatsapp_bridge_stub.go
Normal file
56
pkg/channels/whatsapp_bridge_stub.go
Normal 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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_whatsapp
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
159
pkg/channels/whatsapp_common.go
Normal file
159
pkg/channels/whatsapp_common.go
Normal 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
|
||||
}
|
||||
14
pkg/channels/whatsapp_stub.go
Normal file
14
pkg/channels/whatsapp_stub.go
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !omit_whatsapp
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user