diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 50e1cfda8..000000000 --- a/.dockerignore +++ /dev/null @@ -1,10 +0,0 @@ -.git -.gitignore -build/ -.clawdroid/ -config/ -.env -.env.example -*.md -LICENSE -assets/ diff --git a/.env.example b/.env.example deleted file mode 100644 index 66539b634..000000000 --- a/.env.example +++ /dev/null @@ -1,19 +0,0 @@ -# ── LLM Provider ────────────────────────── -# Uncomment and set the API key for your provider -# OPENROUTER_API_KEY=sk-or-v1-xxx -# ZHIPU_API_KEY=xxx -# ANTHROPIC_API_KEY=sk-ant-xxx -# OPENAI_API_KEY=sk-xxx -# GEMINI_API_KEY=xxx - -# ── Chat Channel ────────────────────────── -# TELEGRAM_BOT_TOKEN=123456:ABC... -# DISCORD_BOT_TOKEN=xxx -# LINE_CHANNEL_SECRET=xxx -# LINE_CHANNEL_ACCESS_TOKEN=xxx - -# ── Web Search (optional) ──────────────── -# BRAVE_SEARCH_API_KEY=BSA... - -# ── Timezone ────────────────────────────── -TZ=Asia/Tokyo diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7cc3b0ea6..d40cc44b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ assignees: '' - **Go Version:** (e.g., go 1.22) - **AI Model & Provider:** (e.g., GPT-4o via OpenAI / DeepSeek via SiliconFlow) - **Operating System:** (e.g., Ubuntu 22.04 / macOS / Android Termux) -- **Channels:** (e.g., Discord, Telegram, Feishu, ...) +- **Channels:** (e.g., Telegram, Discord, Slack, LINE, ...) ## 📸 Steps to Reproduce 1. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d2773e27d..75bae88ee 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,7 +16,7 @@ - **Hardware:** [e.g. Raspberry Pi 5, Orange Pi, PC] - **OS:** [e.g. Debian 12, Ubuntu 22.04] - **Model/Provider:** [e.g. OpenAI GPT-4o, Kimi k2, DeepSeek-V3] -- **Channels:** [e.g. Discord, Telegram, Feishu, ...] +- **Channels:** [e.g. Telegram, Discord, Slack, LINE, ...] ## 📸 Proof of Work (Optional for Docs) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index daea2654b..000000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: 🐳 Build & Push Docker Image - -on: - workflow_call: - inputs: - tag: - description: "Release tag" - required: true - type: string - -env: - GHCR_REGISTRY: ghcr.io - GHCR_IMAGE_NAME: ${{ github.repository_owner }}/clawdroid - DOCKERHUB_REGISTRY: docker.io - DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} - -jobs: - build: - name: 🏗️ Build Docker Image - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - # ── Checkout ────────────────────────────── - - name: 📥 Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ inputs.tag }} - - # ── Docker Buildx ───────────────────────── - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - # ── Login to GHCR ───────────────────────── - - name: 🔑 Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.GHCR_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # ── Login to Docker Hub ──────────────────── - - name: 🔑 Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKERHUB_REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # ── Metadata (tags & labels) ────────────── - - name: 🏷️ Prepare image tags - id: tags - shell: bash - run: | - tag="${{ inputs.tag }}" - echo "ghcr_tag=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT" - echo "ghcr_latest=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT" - echo "dockerhub_tag=${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT" - echo "dockerhub_latest=${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT" - - # ── Build & Push ────────────────────────── - - name: 🚀 Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: | - ${{ steps.tags.outputs.ghcr_tag }} - ${{ steps.tags.outputs.ghcr_latest }} - ${{ steps.tags.outputs.dockerhub_tag }} - ${{ steps.tags.outputs.dockerhub_latest }} - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d68ae416..4caec1b20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,26 +59,6 @@ jobs: with: go-version-file: go.mod - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: @@ -87,8 +67,6 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} - DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} - name: Apply release flags shell: bash diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8d8812204..f85f8f2ed 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,36 +13,11 @@ builds: - CGO_ENABLED=0 goos: - linux - - windows - - darwin - - freebsd goarch: - amd64 - arm64 - - riscv64 - - s390x - - mips64 - arm main: ./cmd/clawdroid - ignore: - - goos: windows - goarch: arm - -dockers_v2: - - id: clawdroid - dockerfile: Dockerfile.goreleaser - ids: - - clawdroid - images: - - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/clawdroid" - - "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}" - tags: - - "{{ .Tag }}" - - "latest" - platforms: - - linux/amd64 - - linux/arm64 - - linux/riscv64 archives: - formats: [tar.gz] @@ -54,11 +29,6 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} - # use zip for windows archives - format_overrides: - - goos: windows - formats: [zip] - changelog: sort: asc filters: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bb1f55454..000000000 --- a/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# ============================================================ -# Stage 1: Build the clawdroid binary -# ============================================================ -FROM golang:1.26.0-alpine AS builder - -RUN apk add --no-cache git make - -WORKDIR /src - -# Cache dependencies -COPY go.mod go.sum ./ -RUN go mod download - -# Copy source and build -COPY . . -RUN make build - -# ============================================================ -# Stage 2: Minimal runtime image -# ============================================================ -FROM alpine:3.23 - -RUN apk add --no-cache ca-certificates tzdata curl - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q --spider http://localhost:18790/health || exit 1 - -# Copy binary -COPY --from=builder /src/build/clawdroid /usr/local/bin/clawdroid - -# Create clawdroid home directory -RUN /usr/local/bin/clawdroid onboard - -ENTRYPOINT ["clawdroid"] -CMD ["gateway"] diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser deleted file mode 100644 index bcc1c4e82..000000000 --- a/Dockerfile.goreleaser +++ /dev/null @@ -1,10 +0,0 @@ -FROM alpine:3.21 - -ARG TARGETPLATFORM - -RUN apk add --no-cache ca-certificates tzdata - -COPY $TARGETPLATFORM/clawdroid /usr/local/bin/clawdroid - -ENTRYPOINT ["clawdroid"] -CMD ["gateway"] diff --git a/Makefile b/Makefile index 09e0823d4..f9cc31ffe 100644 --- a/Makefile +++ b/Makefile @@ -33,29 +33,13 @@ BUILTIN_SKILLS_DIR=$(CURDIR)/skills UNAME_S:=$(shell uname -s) UNAME_M:=$(shell uname -m) -# Platform-specific settings -ifeq ($(UNAME_S),Linux) - PLATFORM=linux - ifeq ($(UNAME_M),x86_64) - ARCH=amd64 - else ifeq ($(UNAME_M),aarch64) - ARCH=arm64 - else ifeq ($(UNAME_M),riscv64) - ARCH=riscv64 - else - ARCH=$(UNAME_M) - endif -else ifeq ($(UNAME_S),Darwin) - PLATFORM=darwin - ifeq ($(UNAME_M),x86_64) - ARCH=amd64 - else ifeq ($(UNAME_M),arm64) - ARCH=arm64 - else - ARCH=$(UNAME_M) - endif +# Platform-specific settings (Linux only) +PLATFORM=linux +ifeq ($(UNAME_M),x86_64) + ARCH=amd64 +else ifeq ($(UNAME_M),aarch64) + ARCH=arm64 else - PLATFORM=$(UNAME_S) ARCH=$(UNAME_M) endif @@ -85,9 +69,7 @@ build-all: generate @mkdir -p $(BUILD_DIR) GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR) GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR) - GOOS=linux GOARCH=riscv64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR) - GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR) - GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR) + GOOS=linux GOARCH=arm $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm ./$(CMD_DIR) @echo "All builds complete" ## install: Install clawdroid to system and copy builtin skills diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 4765374c9..000000000 --- a/ROADMAP.md +++ /dev/null @@ -1,116 +0,0 @@ - -# 🦐 ClawDroid Roadmap - -> **Vision**: To build the ultimate lightweight, secure, and fully autonomous AI Agent infrastructure.automate the mundane, unleash your creativity - ---- - -## 🚀 1. Core Optimization: Extreme Lightweight - -*Our defining characteristic. We fight software bloat to ensure ClawDroid runs smoothly on the smallest embedded devices.* - -* [**Memory Footprint Reduction**](https://github.com/KarakuriAgent/clawdroid/issues/346) - * **Goal**: Run smoothly on 64MB RAM embedded boards (e.g., low-end RISC-V SBCs) with the core process consuming < 20MB. - * **Context**: RAM is expensive and scarce on edge devices. Memory optimization takes precedence over storage size. - * **Action**: Analyze memory growth between releases, remove redundant dependencies, and optimize data structures. - - -## 🛡️ 2. Security Hardening: Defense in Depth - -*Paying off early technical debt. We invite security experts to help build a "Secure-by-Default" agent.* - -* **Input Defense & Permission Control** - * **Prompt Injection Defense**: Harden JSON extraction logic to prevent LLM manipulation. - * **Tool Abuse Prevention**: Strict parameter validation to ensure generated commands stay within safe boundaries. - * **SSRF Protection**: Built-in blocklists for network tools to prevent accessing internal IPs (LAN/Metadata services). - - -* **Sandboxing & Isolation** - * **Filesystem Sandbox**: Restrict file R/W operations to specific directories only. - * **Context Isolation**: Prevent data leakage between different user sessions or channels. - * **Privacy Redaction**: Auto-redact sensitive info (API Keys, PII) from logs and standard outputs. - - -* **Authentication & Secrets** - * **Crypto Upgrade**: Adopt modern algorithms like `ChaCha20-Poly1305` for secret storage. - * **OAuth 2.0 Flow**: Deprecate hardcoded API keys in the CLI; move to secure OAuth flows. - - - -## 🔌 3. Connectivity: Protocol-First Architecture - -*Connect every model, reach every platform.* - -* **Provider** - * [**Architecture Upgrade**](https://github.com/KarakuriAgent/clawdroid/issues/283): Refactor from "Vendor-based" to "Protocol-based" classification (e.g., OpenAI-compatible, Ollama-compatible). *(Status: In progress by @Daming, ETA 5 days)* - * **Local Models**: Deep integration with **Ollama**, **vLLM**, **LM Studio**, and **Mistral** (local inference). - * **Online Models**: Continued support for frontier closed-source models. - - -* **Channel** - * **IM Matrix**: QQ, WeChat (Work), DingTalk, Feishu (Lark), Telegram, Discord, WhatsApp, LINE, Slack, Email, KOOK, Signal, ... - * **Standards**: Support for the **OneBot** protocol. - * [**attachment**](https://github.com/KarakuriAgent/clawdroid/issues/348): Native handling of images, audio, and video attachments. - - -* **Skill Marketplace** - * [**Discovery skills**](https://github.com/KarakuriAgent/clawdroid/issues/287): Implement `find_skill` to automatically discover and install skills from the [GitHub Skills Repo] or other registries. - - - -## 🧠 4. Advanced Capabilities: From Chatbot to Agentic AI - -*Beyond conversation—focusing on action and collaboration.* - -* **Operations** - * [**MCP Support**](https://github.com/KarakuriAgent/clawdroid/issues/290): Native support for the **Model Context Protocol (MCP)**. - * [**Browser Automation**](https://github.com/KarakuriAgent/clawdroid/issues/293): Headless browser control via CDP (Chrome DevTools Protocol) or ActionBook. - * [**Mobile Operation**](https://github.com/KarakuriAgent/clawdroid/issues/292): Android device control (similar to BotDrop). - - -* **Multi-Agent Collaboration** - * [**Basic Multi-Agent**](https://github.com/KarakuriAgent/clawdroid/issues/294) implement - * [**Model Routing**](https://github.com/KarakuriAgent/clawdroid/issues/295): "Smart Routing" — dispatch simple tasks to small/local models (fast/cheap) and complex tasks to SOTA models (smart). - * [**Swarm Mode**](https://github.com/KarakuriAgent/clawdroid/issues/284): Collaboration between multiple ClawDroid instances on the same network. - * [**AIEOS**](https://github.com/KarakuriAgent/clawdroid/issues/296): Exploring AI-Native Operating System interaction paradigms. - - - -## 📚 5. Developer Experience (DevEx) & Documentation - -*Lowering the barrier to entry so anyone can deploy in minutes.* - -* [**QuickGuide (Zero-Config Start)**](https://github.com/KarakuriAgent/clawdroid/issues/350) - * Interactive CLI Wizard: If launched without config, automatically detect the environment and guide the user through Token/Network setup step-by-step. - - -* **Comprehensive Documentation** - * **Platform Guides**: Dedicated guides for Windows, macOS, Linux, and Android. - * **Step-by-Step Tutorials**: "Babysitter-level" guides for configuring Providers and Channels. - * **AI-Assisted Docs**: Using AI to auto-generate API references and code comments (with human verification to prevent hallucinations). - - - -## 🤖 6. Engineering: AI-Powered Open Source - -*Born from Vibe Coding, we continue to use AI to accelerate development.* - -* **AI-Enhanced CI/CD** - * Integrate AI for automated Code Review, Linting, and PR Labeling. - * **Bot Noise Reduction**: Optimize bot interactions to keep PR timelines clean. - * **Issue Triage**: AI agents to analyze incoming issues and suggest preliminary fixes. - - - -## 🎨 7. Brand & Community - -* [**Logo Design**](https://github.com/KarakuriAgent/clawdroid/issues/297): We are looking for a **Mantis Shrimp (Stomatopoda)** logo design! - * *Concept*: Needs to reflect "Small but Mighty" and "Lightning Fast Strikes." - - - ---- - -### 🤝 Call for Contributions - -We welcome community contributions to any item on this roadmap! Please comment on the relevant Issue or submit a PR. Let's build the best Edge AI Agent together! \ No newline at end of file diff --git a/cmd/clawdroid/main.go b/cmd/clawdroid/main.go index 3037dc2f0..db97bf537 100644 --- a/cmd/clawdroid/main.go +++ b/cmd/clawdroid/main.go @@ -13,7 +13,6 @@ import ( "fmt" "io" "io/fs" - "net/http" "os" "os/signal" "path/filepath" @@ -27,7 +26,6 @@ import ( "github.com/KarakuriAgent/clawdroid/pkg/channels" "github.com/KarakuriAgent/clawdroid/pkg/config" "github.com/KarakuriAgent/clawdroid/pkg/cron" - "github.com/KarakuriAgent/clawdroid/pkg/health" "github.com/KarakuriAgent/clawdroid/pkg/heartbeat" "github.com/KarakuriAgent/clawdroid/pkg/logger" "github.com/KarakuriAgent/clawdroid/pkg/providers" @@ -542,14 +540,6 @@ func gatewayCmd() { fmt.Printf("Error starting channels: %v\n", err) } - healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port) - go func() { - if err := healthServer.Start(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()}) - } - }() - fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port) - go agentLoop.Run(ctx) sigChan := make(chan os.Signal, 1) @@ -558,7 +548,6 @@ func gatewayCmd() { fmt.Println("\nShutting down...") cancel() - healthServer.Stop(context.Background()) heartbeatService.Stop() cronService.Stop() agentLoop.Stop() diff --git a/config/config.example.json b/config/config.example.json index ba52592a8..2c6e9f489 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -27,37 +27,11 @@ "token": "YOUR_DISCORD_BOT_TOKEN", "allow_from": [] }, - "maixcam": { - "enabled": false, - "host": "127.0.0.1", - "port": 18792, - "allow_from": [] - }, "whatsapp": { "enabled": false, "bridge_url": "ws://localhost:3001", "allow_from": [] }, - "feishu": { - "enabled": false, - "app_id": "", - "app_secret": "", - "encrypt_key": "", - "verification_token": "", - "allow_from": [] - }, - "qq": { - "enabled": false, - "app_id": "", - "app_secret": "", - "allow_from": [] - }, - "dingtalk": { - "enabled": false, - "client_id": "YOUR_CLIENT_ID", - "client_secret": "YOUR_CLIENT_SECRET", - "allow_from": [] - }, "slack": { "enabled": false, "bot_token": "xoxb-YOUR-BOT-TOKEN", @@ -73,14 +47,6 @@ "webhook_path": "/webhook/line", "allow_from": [] }, - "onebot": { - "enabled": false, - "ws_url": "ws://127.0.0.1:3001", - "access_token": "", - "reconnect_interval": 5, - "group_trigger_prefix": [], - "allow_from": [] - }, "websocket": { "enabled": true, "host": "127.0.0.1", diff --git a/doc/android-apk-plan.md b/doc/android-apk-plan.md deleted file mode 100644 index 8fe34597a..000000000 --- a/doc/android-apk-plan.md +++ /dev/null @@ -1,577 +0,0 @@ -# ClawDroid Android APK - チャットUI設計プラン - -## Context - -ClawDroidはTermux上で動作するGo製AIエージェント。WebSocketサーバーチャネル(`pkg/channels/websocket.go`)を通じて外部クライアントと通信可能。このWSサーバーに接続するAndroid APKを作成する。初期リリースはテキスト+画像チャットUI、将来的にボイスUI(Googleアシスタント風)を追加予定。 - -## WSプロトコル(確認済み) - -``` -Endpoint: ws://127.0.0.1:18793/ws - -Client → Server: {"content":"text", "sender_id":"optional", "images":["raw_base64_no_prefix"]} -Server → Client: {"content":"text"} -``` - -- imagesはraw base64(`data:`プレフィックスなし)。サーバー側で`data:image/png;base64,`を付与(websocket.go:237) -- sender_id未指定時、サーバーがUUIDを割り当て(websocket.go:227-230) - ---- - -## 技術スタック - -| ツール | バージョン | 備考 | -|--------|-----------|------| -| AGP | **9.0.1** | Kotlin組み込み、`kotlin-android`プラグイン不要 | -| Gradle | **9.1+** | AGP 9.0要件 | -| Kotlin | **2.3.0** (AGP組み込み) | | -| Compose BOM | **2026.01.01** | | -| Ktor Client | **3.4.0** | OkHttpエンジン + WebSocketプラグイン | -| kotlinx.serialization | **1.10.0** | コンパイラプラグイン | -| Koin | **4.1.1** | DSLベースDI | -| Room | **2.8.4** | SQLite永続化(KSP必要) | -| KSP | **2.3.6** | Room用 | -| compileSdk / targetSdk | **36** | | -| minSdk | **28** | | - ---- - -## アーキテクチャ — マルチモジュール構成 - -**Jetpack Compose + MVVM + Clean Architecture + Feature Module分離** - -``` -:app → アプリのエントリーポイント、DI、ナビゲーション -:feature:chat → チャットUI(Compose画面、ViewModel、UIコンポーネント) -:core:domain → ドメインモデル、リポジトリinterface、UseCases -:core:data → リポジトリ実装、Room DB、WebSocketClient、DTO -:core:ui → 共有テーマ、共有コンポーネント -``` - -**依存関係**: -``` -:app → :feature:chat, :core:domain, :core:data, :core:ui -:feature:chat → :core:domain, :core:ui -:core:data → :core:domain -:core:ui → (compose dependencies only) -``` - -**利点**: ボイスUI追加時は `:feature:voice` を新設するだけ。`:core:domain`と`:core:data`はそのまま共有。 - ---- - -## ディレクトリ構成 - -``` -android/ -├── .gitignore -├── build.gradle.kts # ルートビルド -├── settings.gradle.kts # 全モジュール include -├── gradle.properties -├── gradle/ -│ └── libs.versions.toml -│ -├── app/ # === :app モジュール === -│ ├── build.gradle.kts -│ ├── proguard-rules.pro -│ └── src/main/ -│ ├── AndroidManifest.xml -│ ├── res/ -│ │ ├── xml/network_security_config.xml -│ │ ├── values/strings.xml -│ │ ├── values/colors.xml -│ │ └── values/themes.xml -│ └── java/io/clawdroid/android/ -│ ├── ClawDroidApp.kt # Application(Koin初期化) -│ ├── MainActivity.kt # Single Activity + Navigation -│ └── di/ -│ └── AppModule.kt # Koin全体module定義 -│ -├── feature/ -│ └── chat/ # === :feature:chat モジュール === -│ ├── build.gradle.kts -│ └── src/main/java/io/clawdroid/android/feature/chat/ -│ ├── ChatViewModel.kt -│ ├── ChatUiState.kt -│ ├── ChatEvent.kt -│ ├── screen/ -│ │ └── ChatScreen.kt -│ └── component/ -│ ├── MessageBubble.kt -│ ├── MessageInput.kt -│ ├── ConnectionBanner.kt -│ ├── ImagePreview.kt -│ └── MessageList.kt -│ -├── core/ -│ ├── domain/ # === :core:domain モジュール === -│ │ ├── build.gradle.kts -│ │ └── src/main/java/io/clawdroid/android/core/domain/ -│ │ ├── model/ -│ │ │ ├── ChatMessage.kt -│ │ │ ├── ConnectionState.kt -│ │ │ └── ImageAttachment.kt -│ │ ├── repository/ -│ │ │ └── ChatRepository.kt # interface -│ │ └── usecase/ -│ │ ├── SendMessageUseCase.kt -│ │ ├── ObserveMessagesUseCase.kt -│ │ ├── ObserveConnectionUseCase.kt -│ │ └── LoadMoreMessagesUseCase.kt -│ │ -│ ├── data/ # === :core:data モジュール === -│ │ ├── build.gradle.kts -│ │ └── src/main/java/io/clawdroid/android/core/data/ -│ │ ├── remote/ -│ │ │ ├── WebSocketClient.kt # Ktor WS + auto-reconnect -│ │ │ └── dto/ -│ │ │ ├── WsIncoming.kt # @Serializable -│ │ │ └── WsOutgoing.kt # @Serializable -│ │ ├── local/ -│ │ │ ├── AppDatabase.kt # Room Database -│ │ │ ├── entity/ -│ │ │ │ └── MessageEntity.kt # Room Entity -│ │ │ ├── dao/ -│ │ │ │ └── MessageDao.kt # Room DAO -│ │ │ └── converter/ -│ │ │ └── Converters.kt # TypeConverter -│ │ ├── mapper/ -│ │ │ └── MessageMapper.kt # Entity ↔ Domain, DTO ↔ Entity -│ │ └── repository/ -│ │ └── ChatRepositoryImpl.kt -│ │ -│ └── ui/ # === :core:ui モジュール === -│ ├── build.gradle.kts -│ └── src/main/java/io/clawdroid/android/core/ui/ -│ └── theme/ -│ ├── Theme.kt -│ ├── Color.kt -│ └── Type.kt -``` - ---- - -## 各層の設計 - -### :core:data — Data層 - -#### Room DB(メッセージ永続化) - -**MessageEntity**: -```kotlin -@Entity(tableName = "messages") -data class MessageEntity( - @PrimaryKey val id: String, // UUID - val content: String, - val sender: String, // "USER" or "AGENT" - val imageBase64List: String?, // JSON文字列 ["base64_1","base64_2"] - val timestamp: Long, // epoch millis - val status: String // "SENDING","SENT","FAILED","RECEIVED" -) -``` - -**MessageDao**: -```kotlin -@Dao -interface MessageDao { - @Query("SELECT * FROM messages ORDER BY timestamp DESC LIMIT :limit") - fun getRecentMessages(limit: Int): Flow> - - @Query("SELECT * FROM messages WHERE timestamp < :beforeTimestamp ORDER BY timestamp DESC LIMIT :limit") - suspend fun getMessagesBefore(beforeTimestamp: Long, limit: Int): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(message: MessageEntity) - - @Update - suspend fun update(message: MessageEntity) -} -``` - -**AppDatabase**: -```kotlin -@Database(entities = [MessageEntity::class], version = 1) -@TypeConverters(Converters::class) -abstract class AppDatabase : RoomDatabase() { - abstract fun messageDao(): MessageDao -} -``` - -#### ページネーション戦略 - -``` -起動時: - → DAO.getRecentMessages(limit=50) をFlowでobserve - → 最新50件が表示される → 新メッセージはinsert → Flowが自動更新 - -スクロール遡り: - → LazyColumnの先頭付近に到達を検知 - → Repository.loadMore() → _displayLimitを+30 - → RoomのFlowが自動的に再クエリ → UIに反映 -``` - -#### WebSocketClient — Ktor Client (OkHttpエンジン) ベース -- `connectionState: StateFlow` で接続状態を公開 -- `incomingMessages: SharedFlow` でサーバー応答を公開 -- `connect()` / `disconnect()` / `send(dto)` メソッド -- exponential backoff自動再接続(1s→2s→4s→8s→max 30s) - -```kotlin -val client = HttpClient(OkHttp) { - install(WebSockets) - engine { - preconfigured = OkHttpClient.Builder() - .pingInterval(30, TimeUnit.SECONDS) - .build() - } -} -``` - -#### ChatRepositoryImpl — Room + WS統合 - -```kotlin -class ChatRepositoryImpl( - private val webSocketClient: WebSocketClient, - private val messageDao: MessageDao, - private val scope: CoroutineScope -) : ChatRepository { - - private val _displayLimit = MutableStateFlow(INITIAL_LOAD_COUNT) // 50 - - override val messages: StateFlow> = - _displayLimit.flatMapLatest { limit -> - messageDao.getRecentMessages(limit) - }.map { entities -> - entities.map { MessageMapper.toDomain(it) }.reversed() - }.stateIn(scope, SharingStarted.Lazily, emptyList()) - - override val connectionState = webSocketClient.connectionState - - init { - scope.launch { - webSocketClient.incomingMessages.collect { dto -> - val entity = MessageMapper.toEntity(dto) - messageDao.insert(entity) - } - } - } - - override suspend fun sendMessage(text: String, images: List) { - val entity = MessageMapper.toEntity(text, images, MessageStatus.SENDING) - messageDao.insert(entity) - val wsDto = MessageMapper.toWsIncoming(text, images) - val success = webSocketClient.send(wsDto) - messageDao.update(entity.copy(status = if (success) "SENT" else "FAILED")) - } - - override fun loadMore() { - _displayLimit.update { it + PAGE_SIZE } - } - - companion object { - const val INITIAL_LOAD_COUNT = 50 - const val PAGE_SIZE = 30 - } -} -``` - -#### DTO — `@Serializable` - -```kotlin -@Serializable -data class WsIncoming( - val content: String, - @SerialName("sender_id") val senderId: String? = null, - val images: List? = null -) - -@Serializable -data class WsOutgoing(val content: String) -``` - -### :core:domain — Domain層 - -**ChatMessage** — `id, content, sender(USER/AGENT), images: List, timestamp, status` - -**ConnectionState** — `DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING` - -**ImageAttachment** — `uri, base64, mimeType` - -**ChatRepository interface**: -```kotlin -interface ChatRepository { - val messages: StateFlow> - val connectionState: StateFlow - suspend fun sendMessage(text: String, images: List = emptyList()) - fun loadMore() - fun connect() - fun disconnect() -} -``` - -**UseCases** — `SendMessageUseCase`, `ObserveMessagesUseCase`, `ObserveConnectionUseCase`, `LoadMoreMessagesUseCase` - -### :core:ui — 共有UI - -テーマ定義(Color, Type, Theme)。将来的に共有コンポーネントもここに配置。 - -### :feature:chat — チャットUI - -**ChatUiState**: -```kotlin -data class ChatUiState( - val messages: List = emptyList(), - val connectionState: ConnectionState = ConnectionState.DISCONNECTED, - val inputText: String = "", - val pendingImages: List = emptyList(), - val isLoadingMore: Boolean = false, - val canLoadMore: Boolean = true, - val error: String? = null -) -``` - -**ChatViewModel** — Koin `koinViewModel()`で注入 - -**Compose UI階層**: -``` -ChatScreen -├── ConnectionBanner # 接続状態(赤/黄/非表示) -├── MessageList (weight 1f) # LazyColumn (reverseLayout=true) -│ ├── Loading indicator # isLoadingMore時 -│ └── MessageBubble # 右=ユーザー、左=エージェント -└── Bottom Column - ├── ImagePreviewRow # 送信前サムネイル - └── MessageInput # [カメラ][ギャラリー][TextField][送信] -``` - -**スクロール検知でloadMore**: -```kotlin -val listState = rememberLazyListState() -val shouldLoadMore by remember { - derivedStateOf { - val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull() - lastVisibleItem != null && - lastVisibleItem.index >= listState.layoutInfo.totalItemsCount - 5 - } -} -LaunchedEffect(shouldLoadMore) { - if (shouldLoadMore) viewModel.onLoadMore() -} -``` - ---- - -## DI(Koin) - -```kotlin -// app/di/AppModule.kt -val appModule = module { - // Room - single { - Room.databaseBuilder(androidContext(), AppDatabase::class.java, "clawdroid.db").build() - } - single { get().messageDao() } - - // Ktor HttpClient - single { - HttpClient(OkHttp) { - install(WebSockets) - engine { - preconfigured = OkHttpClient.Builder() - .pingInterval(30, TimeUnit.SECONDS) - .build() - } - } - } - - // WebSocketClient - single { WebSocketClient(get()) } - - // Repository - single { ChatRepositoryImpl(get(), get(), get()) } - - // UseCases - factory { SendMessageUseCase(get()) } - factory { ObserveMessagesUseCase(get()) } - factory { ObserveConnectionUseCase(get()) } - factory { LoadMoreMessagesUseCase(get()) } - - // ViewModel - viewModel { ChatViewModel(get(), get(), get(), get(), get()) } -} -``` - ---- - -## Gradle設定 - -### settings.gradle.kts -```kotlin -pluginManagement { - repositories { google(); mavenCentral(); gradlePluginPortal() } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { google(); mavenCentral() } -} -rootProject.name = "ClawDroid" -include(":app") -include(":feature:chat") -include(":core:domain") -include(":core:data") -include(":core:ui") -``` - -### gradle/libs.versions.toml -```toml -[versions] -agp = "9.0.1" -kotlin = "2.3.0" -ksp = "2.3.6" -compose-bom = "2026.01.01" -ktor = "3.4.0" -koin = "4.1.1" -serialization = "1.10.0" -room = "2.8.4" -lifecycle = "2.10.0" -activity-compose = "1.12.4" -core-ktx = "1.17.0" -coroutines = "1.10.2" - -[libraries] -# Compose -compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } -compose-ui = { group = "androidx.compose.ui", name = "ui" } -compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -compose-material3 = { group = "androidx.compose.material3", name = "material3" } -compose-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } - -# AndroidX -activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } -core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } -lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } -lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } - -# Ktor -ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } -ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" } -ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } -ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } - -# Serialization -serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } - -# Room -room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } -room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } -room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } - -# Koin -koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } -koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } - -# Coroutines -coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } - -[plugins] -android-application = { id = "com.android.application", version.ref = "agp" } -android-library = { id = "com.android.library", version.ref = "agp" } -serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -``` - -### 各モジュールのbuild.gradle.kts概要 - -**:app** — `android-application`, `serialization` → depends on all modules -**:feature:chat** — `android-library` → depends on `:core:domain`, `:core:ui` -**:core:domain** — `android-library` → depends on nothing (pure Kotlin + coroutines) -**:core:data** — `android-library`, `serialization`, `ksp` → depends on `:core:domain` (Room, Ktor, DTO) -**:core:ui** — `android-library` → depends on nothing (Compose theme only) - -KSPは`:core:data`モジュールのみで使用(Room compiler)。 - ---- - -## 注意点 - -- **Cleartext通信**: `ws://127.0.0.1`への通信に`network_security_config.xml`でlocalhost/127.0.0.1のみ許可 -- **画像base64**: サーバーが`data:image/png;base64,`を自動付与するため、クライアントはraw base64のみ送信 -- **画像のDB保存**: 画像base64はMessageEntityにJSON配列文字列として保存。TypeConverterで変換 -- **Ktor WS ping**: OkHttpエンジン使用時はOkHttpBuilder側の`pingInterval`で設定 -- **AGP 9.0**: `kotlin-android`プラグイン不適用、`kotlin.compilerOptions`を使用 -- **マルチモジュール**: 各library moduleは`android.namespace`を個別設定 - ---- - -## 実装順序 - -### Phase 1: プロジェクトスキャフォールド + マルチモジュール -1. `android/` ディレクトリ構造(5モジュール分) -2. ルート `build.gradle.kts`、`settings.gradle.kts`(全module include)、`libs.versions.toml` -3. `gradle.properties` -4. 各モジュールの `build.gradle.kts` -5. `app/` の `AndroidManifest.xml` + `network_security_config.xml` + リソース -6. Gradleラッパー生成 - -### Phase 2: :core:domain -7. `ConnectionState.kt`、`ChatMessage.kt`、`ImageAttachment.kt` -8. `ChatRepository.kt` (interface) -9. 4つのUseCase - -### Phase 3: :core:ui -10. Theme(Color.kt, Type.kt, Theme.kt) - -### Phase 4: :core:data — Room -11. `MessageEntity.kt` + `Converters.kt` -12. `MessageDao.kt` -13. `AppDatabase.kt` - -### Phase 5: :core:data — WebSocket + Repository -14. `WsIncoming.kt`、`WsOutgoing.kt` (@Serializable) -15. `MessageMapper.kt` -16. `WebSocketClient.kt`(Ktor + auto-reconnect) -17. `ChatRepositoryImpl.kt`(Room + WS統合、ページネーション) - -### Phase 6: :feature:chat -18. `ChatUiState.kt`、`ChatEvent.kt` -19. `ChatViewModel.kt`(loadMore対応) -20. UIコンポーネント(ConnectionBanner, MessageBubble, MessageList, ImagePreview, MessageInput) -21. `ChatScreen.kt`(スクロール検知+ページネーション) - -### Phase 7: :app -22. `AppModule.kt`(Koin全体module) -23. `ClawDroidApp.kt` -24. `MainActivity.kt` - ---- - -## 検証方法 - -1. Termuxでclawdroid起動(WSサーバーが`127.0.0.1:18793`でリスン) -2. APKをビルド・インストール(`./gradlew assembleDebug`) -3. アプリ起動 → ConnectionBannerが「Connected」表示を確認 -4. テキスト送信 → エージェントからの応答がチャットに表示されることを確認 -5. アプリを再起動 → 前回のチャット履歴がRoomから読み込まれて表示されることを確認 -6. 50件以上メッセージを蓄積 → 上にスクロール → 古いメッセージが動的にロードされることを確認 -7. カメラ/ギャラリーから画像添付+テキスト送信 → エージェントが画像を認識した応答を返すことを確認 -8. WSサーバーを停止 → 「Reconnecting」表示 → サーバー再起動後に自動再接続を確認 - ---- - -## 将来の拡張 - -- **:feature:voice**: `:core:domain`と`:core:data`をそのまま共有。SpeechRecognizer+TextToSpeechのUI層のみ新設 -- **複数サーバー**: `WebSocketClient`のurl引数を動的に -- **メッセージ検索**: Room DAOにFTS4クエリ追加 - ---- - -## 参照するサーバー側ファイル - -| ファイル | 用途 | -|---------|------| -| `pkg/channels/websocket.go` | WSプロトコル定義 | -| `pkg/bus/types.go` | InboundMessage/OutboundMessage構造体 | -| `pkg/channels/base.go` | Channelインターフェース | -| `pkg/config/config.go` | WebSocketConfig | diff --git a/doc/clawdroid_community_roadmap_260216.md b/doc/clawdroid_community_roadmap_260216.md deleted file mode 100644 index 53f4034d8..000000000 --- a/doc/clawdroid_community_roadmap_260216.md +++ /dev/null @@ -1,112 +0,0 @@ -## 🚀 Join the ClawDroid Journey: Call for Community Volunteers & Roadmap Reveal - -**Hello, ClawDroid Community!** - -First, a massive thank you to everyone for your enthusiasm and PR contributions. It is because of you that ClawDroid continues to iterate and evolve so rapidly. Thanks to the simplicity and accessibility of the **Go language**, we’ve seen a non-stop stream of high-quality PRs! - -ClawDroid is growing much faster than we anticipated. As we are currently in the midst of the **Chinese New Year holiday**, we are looking to recruit community volunteers to help us maintain this incredible momentum. - -This document outlines the specific volunteer roles we need right now and provides a look at our upcoming **Roadmap**. - -### 🎁 Community Perks - -To show our appreciation, developers who officially join our community operations will receive: - -* **Exclusive AI Hardware:** Our upcoming, unreleased AI device. -* **Token Discounts:** Potential discounts on LLM tokens (currently in negotiations with major providers). - -### 🎥 Calling All Content Creators! - -Not a developer? You can still help! We welcome users to post **ClawDroid reviews or tutorials**. - -* **Twitter:** Use the tag **#clawdroid** and mention **@SipeedIO**. -* **Bilibili:** Mention **@Sipeed矽速科技** or send us a DM. -We will be rewarding high-quality content creators with the same perks as our community developers! - ---- - -## 🛠️ Urgent Volunteer Roles - -We are looking for experts in the following areas: - -1. **Issue/PR Reviewers** -* **The Mission:** With PRs and Issues exploding in volume, we need help with initial triage, evaluation, and merging. -* **Focus:** Preliminary merging and community health. Efficiency optimization and security audits will be handled by specialized roles. - - -2. **Resource Optimization Experts** -* **The Mission:** Rapid growth has introduced dependencies that are making ClawDroid a bit "heavy." We want to keep it lean. -* **Focus:** Analyzing resource growth between releases and trimming redundancy. -* **Priority:** **RAM usage optimization** > Binary size reduction. - - -3. **Security Audit & Bug Fixes** -* **The Mission:** Due to the "vibe coding" nature of our early stages, we need a thorough review of network security and AI permission management. -* **Focus:** Auditing the codebase for vulnerabilities and implementing robust fixes. - - -4. **Documentation & DX (Developer Experience)** -* **The Mission:** Our current README is a bit outdated. We need "step-by-step" guides that even beginners can follow. -* **Focus:** Creating clear, user-friendly documentation for both setup and development. - - -5. **AI-Powered CI/CD Optimization** -* **The Mission:** ClawDroid started as a "vibe coding" experiment; now we want to use AI to manage it. -* **Focus:** Automating builds with AI and exploring AI-driven issue resolution. - -**How to Apply:** > If you are interested in any of the roles above, please send an email to support@sipeed.com with the subject line: [Apply: ClawDroid Expert Volunteer] + Your Desired Role. -Please include a brief introduction and any relevant experience or portfolio links. We will review all applications and grant project permissions to selected contributors! - ---- - -## 📍 The Roadmap - -Interested in a specific feature? You can "claim" these tasks and start building: - -### -* **Provider:** - * **Provider Refactor:** Currently being handled by **@Daming** (ETA: 5 days) - * You can still submit code; Daming will merge it into the new implementation. -* **Channels:** - * Support for OneBot, additional platforms - * attachments (images, audio, video, files). -* **Skills:** - * Implementing `find_skill` to discover tools via [openclaw/skills](https://github.com/openclaw/skills) and other platforms. -* **Operations:** * MCP Support. - * Android operations (e.g., botdrop). - * Browser automation via CDP or ActionBook. - - -* **Multi-Agent Ecosystem:** - * **Basic Model-Agnet** S - * **Model Routing:** Small models for easy tasks, large models for hard ones (to save tokens). - * **Swarm Mode.** - * **AIEOS Integration.** - - -* **Branding:** - * **Logo**: We need a cute logo! We’re leaning toward a **Mantis Shrimp**—small, but packs a legendary punch! - - -We have officially created these tasks as GitHub Issues, all marked with the roadmap tag. -This list will be updated continuously as we progress. -If you would like to claim a task, please feel free to start a conversation by commenting directly on the corresponding issue! - ---- - -## 🤝 How to Join - -**Everything is open to your creativity!** If you have a wild idea, just PR it. - -1. **The Fast Track:** Once you have at least **one merged PR**, you are eligible to join our **Developer Discord** to help plan the future of ClawDroid. -2. **The Application Track:** If you haven’t submitted a PR yet but want to dive in, email **support@sipeed.com** with the subject: -> `[Apply Join ClawDroid Dev Group] + Your GitHub Account` -> Include the role you're interested in and any evidence of your development experience. - - - -### Looking Ahead - -Powered by ClawDroid, we are crafting a Swarm AI Assistant to transform your environment into a seamless network of personal stewards. By automating the friction of daily life, we empower you to transcend the ordinary and freely explore your creative potential. - -**Finally, Happy Chinese New Year to everyone!** May ClawDroid gallop forward in this **Year of the Horse!** 🐎 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 68df56e7e..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,40 +0,0 @@ -services: - # ───────────────────────────────────────────── - # ClawDroid Agent (one-shot query) - # docker compose run --rm clawdroid-agent -m "Hello" - # ───────────────────────────────────────────── - clawdroid-agent: - build: - context: . - dockerfile: Dockerfile - container_name: clawdroid-agent - profiles: - - agent - volumes: - - ./config/config.json:/root/.clawdroid/config.json:ro - - clawdroid-workspace:/root/.clawdroid/workspace - entrypoint: ["clawdroid", "agent"] - stdin_open: true - tty: true - - # ───────────────────────────────────────────── - # ClawDroid Gateway (Long-running Bot) - # docker compose up clawdroid-gateway - # ───────────────────────────────────────────── - clawdroid-gateway: - build: - context: . - dockerfile: Dockerfile - container_name: clawdroid-gateway - restart: unless-stopped - profiles: - - gateway - volumes: - # Configuration file - - ./config/config.json:/root/.clawdroid/config.json:ro - # Persistent workspace (sessions, memory, logs) - - clawdroid-workspace:/root/.clawdroid/workspace - command: ["gateway"] - -volumes: - clawdroid-workspace: diff --git a/go.mod b/go.mod index a4ec08f1e..0ea584496 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,11 @@ require ( github.com/chzyer/readline v1.5.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/larksuite/oapi-sdk-go/v3 v3.5.3 github.com/modelcontextprotocol/go-sdk v1.3.1 github.com/mozilla-ai/any-llm-go v0.8.0 github.com/mymmrac/telego v1.6.0 - github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 github.com/slack-go/slack v0.17.3 github.com/stretchr/testify v1.11.1 - github.com/tencent-connect/botgo v0.2.1 - golang.org/x/oauth2 v0.35.0 ) require ( @@ -41,8 +37,6 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/go-resty/resty/v2 v2.17.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect @@ -69,7 +63,7 @@ require ( golang.org/x/arch v0.24.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.50.0 // indirect - golang.org/x/sync v0.19.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genai v1.45.0 // indirect diff --git a/go.sum b/go.sum index 70562477c..627def1f7 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -28,8 +27,6 @@ github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCc github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= @@ -44,22 +41,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= -github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -75,19 +62,13 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= @@ -95,36 +76,24 @@ github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9 github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk= -github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modelcontextprotocol/go-sdk v1.3.1 h1:TfqtNKOIWN4Z1oqmPAiWDC2Jq7K9OdJaooe0teoXASI= @@ -133,25 +102,14 @@ github.com/mozilla-ai/any-llm-go v0.8.0 h1:QNM2yeMaFp3TnIX7+pJ1oxakfA2bbQtyH7pch github.com/mozilla-ai/any-llm-go v0.8.0/go.mod h1:hfidShiFrygKCzyMTMJWAUv6S5q7ZP/1qWK3Azc6RLU= github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0= github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/ollama/ollama v0.15.4 h1:y841GH5lsi5j5BTFyX/E+UOC3Yiw+JBfdjBVRGw+I0M= github.com/ollama/ollama v0.15.4/go.mod h1:4Yn3jw2hZ4VqyJ1XciYawDRE8bzv4RT3JiVZR1kCfwE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8= -github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -165,7 +123,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -173,9 +130,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tencent-connect/botgo v0.2.1 h1:+BrTt9Zh+awL28GWC4g5Na3nQaGRWb0N5IctS8WqBCk= -github.com/tencent-connect/botgo v0.2.1/go.mod h1:oO1sG9ybhXNickvt+CVym5khwQ+uKhTR+IhTqEfOVsI= -github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -201,9 +155,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -211,111 +162,54 @@ go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genai v1.45.0 h1:s80ZpS42XW0zu/ogiOtenCio17nJ7reEFJjoCftukpA= @@ -341,21 +235,11 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/channels/dingtalk.go b/pkg/channels/dingtalk.go deleted file mode 100644 index 4c7c0fa62..000000000 --- a/pkg/channels/dingtalk.go +++ /dev/null @@ -1,193 +0,0 @@ -// ClawDroid - Ultra-lightweight personal AI agent -// DingTalk channel implementation using Stream Mode - -package channels - -import ( - "context" - "fmt" - "sync" - - "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" - "github.com/open-dingtalk/dingtalk-stream-sdk-go/client" - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/config" - "github.com/KarakuriAgent/clawdroid/pkg/logger" - "github.com/KarakuriAgent/clawdroid/pkg/utils" -) - -// DingTalkChannel implements the Channel interface for DingTalk (钉钉) -// It uses WebSocket for receiving messages via stream mode and API for sending -type DingTalkChannel struct { - *BaseChannel - config config.DingTalkConfig - clientID string - clientSecret string - streamClient *client.StreamClient - ctx context.Context - cancel context.CancelFunc - // Map to store session webhooks for each chat - sessionWebhooks sync.Map // chatID -> sessionWebhook -} - -// NewDingTalkChannel creates a new DingTalk channel instance -func NewDingTalkChannel(cfg config.DingTalkConfig, messageBus *bus.MessageBus) (*DingTalkChannel, error) { - if cfg.ClientID == "" || cfg.ClientSecret == "" { - return nil, fmt.Errorf("dingtalk client_id and client_secret are required") - } - - base := NewBaseChannel("dingtalk", cfg, messageBus, cfg.AllowFrom) - - return &DingTalkChannel{ - BaseChannel: base, - config: cfg, - clientID: cfg.ClientID, - clientSecret: cfg.ClientSecret, - }, nil -} - -// Start initializes the DingTalk channel with Stream Mode -func (c *DingTalkChannel) Start(ctx context.Context) error { - logger.InfoC("dingtalk", "Starting DingTalk channel (Stream Mode)...") - - c.ctx, c.cancel = context.WithCancel(ctx) - - // Create credential config - cred := client.NewAppCredentialConfig(c.clientID, c.clientSecret) - - // Create the stream client with options - c.streamClient = client.NewStreamClient( - client.WithAppCredential(cred), - client.WithAutoReconnect(true), - ) - - // Register chatbot callback handler (IChatBotMessageHandler is a function type) - c.streamClient.RegisterChatBotCallbackRouter(c.onChatBotMessageReceived) - - // Start the stream client - if err := c.streamClient.Start(c.ctx); err != nil { - return fmt.Errorf("failed to start stream client: %w", err) - } - - c.setRunning(true) - logger.InfoC("dingtalk", "DingTalk channel started (Stream Mode)") - return nil -} - -// Stop gracefully stops the DingTalk channel -func (c *DingTalkChannel) Stop(ctx context.Context) error { - logger.InfoC("dingtalk", "Stopping DingTalk channel...") - - if c.cancel != nil { - c.cancel() - } - - if c.streamClient != nil { - c.streamClient.Close() - } - - c.setRunning(false) - logger.InfoC("dingtalk", "DingTalk channel stopped") - return nil -} - -// Send sends a message to DingTalk via the chatbot reply API -func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { - if !c.IsRunning() { - return fmt.Errorf("dingtalk channel not running") - } - - // Get session webhook from storage - sessionWebhookRaw, ok := c.sessionWebhooks.Load(msg.ChatID) - if !ok { - return fmt.Errorf("no session_webhook found for chat %s, cannot send message", msg.ChatID) - } - - sessionWebhook, ok := sessionWebhookRaw.(string) - if !ok { - return fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID) - } - - logger.DebugCF("dingtalk", "Sending message", map[string]interface{}{ - "chat_id": msg.ChatID, - "preview": utils.Truncate(msg.Content, 100), - }) - - // Use the session webhook to send the reply - return c.SendDirectReply(ctx, sessionWebhook, msg.Content) -} - -// onChatBotMessageReceived implements the IChatBotMessageHandler function signature -// This is called by the Stream SDK when a new message arrives -// IChatBotMessageHandler is: func(c context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) -func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) { - // Extract message content from Text field - content := data.Text.Content - if content == "" { - // Try to extract from Content interface{} if Text is empty - if contentMap, ok := data.Content.(map[string]interface{}); ok { - if textContent, ok := contentMap["content"].(string); ok { - content = textContent - } - } - } - - if content == "" { - return nil, nil // Ignore empty messages - } - - senderID := data.SenderStaffId - senderNick := data.SenderNick - chatID := senderID - if data.ConversationType != "1" { - // For group chats - chatID = data.ConversationId - } - - // Store the session webhook for this chat so we can reply later - c.sessionWebhooks.Store(chatID, data.SessionWebhook) - - metadata := map[string]string{ - "sender_name": senderNick, - "conversation_id": data.ConversationId, - "conversation_type": data.ConversationType, - "platform": "dingtalk", - "session_webhook": data.SessionWebhook, - } - - logger.DebugCF("dingtalk", "Received message", map[string]interface{}{ - "sender_nick": senderNick, - "sender_id": senderID, - "preview": utils.Truncate(content, 50), - }) - - // Handle the message through the base channel - c.HandleMessage(senderID, chatID, content, nil, metadata) - - // Return nil to indicate we've handled the message asynchronously - // The response will be sent through the message bus - return nil, nil -} - -// SendDirectReply sends a direct reply using the session webhook -func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, content string) error { - replier := chatbot.NewChatbotReplier() - - // Convert string content to []byte for the API - contentBytes := []byte(content) - titleBytes := []byte("ClawDroid") - - // Send markdown formatted reply - err := replier.SimpleReplyMarkdown( - ctx, - sessionWebhook, - titleBytes, - contentBytes, - ) - - if err != nil { - return fmt.Errorf("failed to send reply: %w", err) - } - - return nil -} diff --git a/pkg/channels/feishu_32.go b/pkg/channels/feishu_32.go deleted file mode 100644 index e90b65269..000000000 --- a/pkg/channels/feishu_32.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build !amd64 && !arm64 && !riscv64 && !mips64 && !ppc64 - -package channels - -import ( - "context" - "errors" - - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/config" -) - -// FeishuChannel is a stub implementation for 32-bit architectures -type FeishuChannel struct { - *BaseChannel -} - -// NewFeishuChannel returns an error on 32-bit architectures where the Feishu SDK is not supported -func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) { - return nil, errors.New("feishu channel is not supported on 32-bit architectures (armv7l, 386, etc.). Please use a 64-bit system or disable feishu in your config") -} - -// Start is a stub method to satisfy the Channel interface -func (c *FeishuChannel) Start(ctx context.Context) error { - return nil -} - -// Stop is a stub method to satisfy the Channel interface -func (c *FeishuChannel) Stop(ctx context.Context) error { - return nil -} - -// Send is a stub method to satisfy the Channel interface -func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { - return errors.New("feishu channel is not supported on 32-bit architectures") -} diff --git a/pkg/channels/feishu_64.go b/pkg/channels/feishu_64.go deleted file mode 100644 index f9cb43fe6..000000000 --- a/pkg/channels/feishu_64.go +++ /dev/null @@ -1,218 +0,0 @@ -//go:build amd64 || arm64 || riscv64 || mips64 || ppc64 - -package channels - -import ( - "context" - "encoding/json" - "fmt" - "sync" - "time" - - lark "github.com/larksuite/oapi-sdk-go/v3" - larkdispatcher "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher" - larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" - larkws "github.com/larksuite/oapi-sdk-go/v3/ws" - - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/config" - "github.com/KarakuriAgent/clawdroid/pkg/logger" - "github.com/KarakuriAgent/clawdroid/pkg/utils" -) - -type FeishuChannel struct { - *BaseChannel - config config.FeishuConfig - client *lark.Client - wsClient *larkws.Client - - mu sync.Mutex - cancel context.CancelFunc -} - -func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) { - base := NewBaseChannel("feishu", cfg, bus, cfg.AllowFrom) - - return &FeishuChannel{ - BaseChannel: base, - config: cfg, - client: lark.NewClient(cfg.AppID, cfg.AppSecret), - }, nil -} - -func (c *FeishuChannel) Start(ctx context.Context) error { - if c.config.AppID == "" || c.config.AppSecret == "" { - return fmt.Errorf("feishu app_id or app_secret is empty") - } - - dispatcher := larkdispatcher.NewEventDispatcher(c.config.VerificationToken, c.config.EncryptKey). - OnP2MessageReceiveV1(c.handleMessageReceive) - - runCtx, cancel := context.WithCancel(ctx) - - c.mu.Lock() - c.cancel = cancel - c.wsClient = larkws.NewClient( - c.config.AppID, - c.config.AppSecret, - larkws.WithEventHandler(dispatcher), - ) - wsClient := c.wsClient - c.mu.Unlock() - - c.setRunning(true) - logger.InfoC("feishu", "Feishu channel started (websocket mode)") - - go func() { - if err := wsClient.Start(runCtx); err != nil { - logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]interface{}{ - "error": err.Error(), - }) - } - }() - - return nil -} - -func (c *FeishuChannel) Stop(ctx context.Context) error { - c.mu.Lock() - if c.cancel != nil { - c.cancel() - c.cancel = nil - } - c.wsClient = nil - c.mu.Unlock() - - c.setRunning(false) - logger.InfoC("feishu", "Feishu channel stopped") - return nil -} - -func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { - if !c.IsRunning() { - return fmt.Errorf("feishu channel not running") - } - - if msg.ChatID == "" { - return fmt.Errorf("chat ID is empty") - } - - payload, err := json.Marshal(map[string]string{"text": msg.Content}) - if err != nil { - return fmt.Errorf("failed to marshal feishu content: %w", err) - } - - req := larkim.NewCreateMessageReqBuilder(). - ReceiveIdType(larkim.ReceiveIdTypeChatId). - Body(larkim.NewCreateMessageReqBodyBuilder(). - ReceiveId(msg.ChatID). - MsgType(larkim.MsgTypeText). - Content(string(payload)). - Uuid(fmt.Sprintf("clawdroid-%d", time.Now().UnixNano())). - Build()). - Build() - - resp, err := c.client.Im.V1.Message.Create(ctx, req) - if err != nil { - return fmt.Errorf("failed to send feishu message: %w", err) - } - - if !resp.Success() { - return fmt.Errorf("feishu api error: code=%d msg=%s", resp.Code, resp.Msg) - } - - logger.DebugCF("feishu", "Feishu message sent", map[string]interface{}{ - "chat_id": msg.ChatID, - }) - - return nil -} - -func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2MessageReceiveV1) error { - if event == nil || event.Event == nil || event.Event.Message == nil { - return nil - } - - message := event.Event.Message - sender := event.Event.Sender - - chatID := stringValue(message.ChatId) - if chatID == "" { - return nil - } - - senderID := extractFeishuSenderID(sender) - if senderID == "" { - senderID = "unknown" - } - - content := extractFeishuMessageContent(message) - if content == "" { - content = "[empty message]" - } - - metadata := map[string]string{} - if messageID := stringValue(message.MessageId); messageID != "" { - metadata["message_id"] = messageID - } - if messageType := stringValue(message.MessageType); messageType != "" { - metadata["message_type"] = messageType - } - if chatType := stringValue(message.ChatType); chatType != "" { - metadata["chat_type"] = chatType - } - if sender != nil && sender.TenantKey != nil { - metadata["tenant_key"] = *sender.TenantKey - } - - logger.InfoCF("feishu", "Feishu message received", map[string]interface{}{ - "sender_id": senderID, - "chat_id": chatID, - "preview": utils.Truncate(content, 80), - }) - - c.HandleMessage(senderID, chatID, content, nil, metadata) - return nil -} - -func extractFeishuSenderID(sender *larkim.EventSender) string { - if sender == nil || sender.SenderId == nil { - return "" - } - - if sender.SenderId.UserId != nil && *sender.SenderId.UserId != "" { - return *sender.SenderId.UserId - } - if sender.SenderId.OpenId != nil && *sender.SenderId.OpenId != "" { - return *sender.SenderId.OpenId - } - if sender.SenderId.UnionId != nil && *sender.SenderId.UnionId != "" { - return *sender.SenderId.UnionId - } - - return "" -} - -func extractFeishuMessageContent(message *larkim.EventMessage) string { - if message == nil || message.Content == nil || *message.Content == "" { - return "" - } - - if message.MessageType != nil && *message.MessageType == larkim.MsgTypeText { - var textPayload struct { - Text string `json:"text"` - } - if err := json.Unmarshal([]byte(*message.Content), &textPayload); err == nil { - return textPayload.Text - } - } - - return *message.Content -} - -func stringValue(v *string) string { - if v == nil { - return "" - } - return *v -} diff --git a/pkg/channels/maixcam.go b/pkg/channels/maixcam.go deleted file mode 100644 index bf8a17e9d..000000000 --- a/pkg/channels/maixcam.go +++ /dev/null @@ -1,243 +0,0 @@ -package channels - -import ( - "context" - "encoding/json" - "fmt" - "net" - "sync" - - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/config" - "github.com/KarakuriAgent/clawdroid/pkg/logger" -) - -type MaixCamChannel struct { - *BaseChannel - config config.MaixCamConfig - listener net.Listener - clients map[net.Conn]bool - clientsMux sync.RWMutex - running bool -} - -type MaixCamMessage struct { - Type string `json:"type"` - Tips string `json:"tips"` - Timestamp float64 `json:"timestamp"` - Data map[string]interface{} `json:"data"` -} - -func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamChannel, error) { - base := NewBaseChannel("maixcam", cfg, bus, cfg.AllowFrom) - - return &MaixCamChannel{ - BaseChannel: base, - config: cfg, - clients: make(map[net.Conn]bool), - running: false, - }, nil -} - -func (c *MaixCamChannel) Start(ctx context.Context) error { - logger.InfoC("maixcam", "Starting MaixCam channel server") - - addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port) - listener, err := net.Listen("tcp", addr) - if err != nil { - return fmt.Errorf("failed to listen on %s: %w", addr, err) - } - - c.listener = listener - c.setRunning(true) - - logger.InfoCF("maixcam", "MaixCam server listening", map[string]interface{}{ - "host": c.config.Host, - "port": c.config.Port, - }) - - go c.acceptConnections(ctx) - - return nil -} - -func (c *MaixCamChannel) acceptConnections(ctx context.Context) { - logger.DebugC("maixcam", "Starting connection acceptor") - - for { - select { - case <-ctx.Done(): - logger.InfoC("maixcam", "Stopping connection acceptor") - return - default: - conn, err := c.listener.Accept() - if err != nil { - if c.running { - logger.ErrorCF("maixcam", "Failed to accept connection", map[string]interface{}{ - "error": err.Error(), - }) - } - return - } - - logger.InfoCF("maixcam", "New connection from MaixCam device", map[string]interface{}{ - "remote_addr": conn.RemoteAddr().String(), - }) - - c.clientsMux.Lock() - c.clients[conn] = true - c.clientsMux.Unlock() - - go c.handleConnection(conn, ctx) - } - } -} - -func (c *MaixCamChannel) handleConnection(conn net.Conn, ctx context.Context) { - logger.DebugC("maixcam", "Handling MaixCam connection") - - defer func() { - conn.Close() - c.clientsMux.Lock() - delete(c.clients, conn) - c.clientsMux.Unlock() - logger.DebugC("maixcam", "Connection closed") - }() - - decoder := json.NewDecoder(conn) - - for { - select { - case <-ctx.Done(): - return - default: - var msg MaixCamMessage - if err := decoder.Decode(&msg); err != nil { - if err.Error() != "EOF" { - logger.ErrorCF("maixcam", "Failed to decode message", map[string]interface{}{ - "error": err.Error(), - }) - } - return - } - - c.processMessage(msg, conn) - } - } -} - -func (c *MaixCamChannel) processMessage(msg MaixCamMessage, conn net.Conn) { - switch msg.Type { - case "person_detected": - c.handlePersonDetection(msg) - case "heartbeat": - logger.DebugC("maixcam", "Received heartbeat") - case "status": - c.handleStatusUpdate(msg) - default: - logger.WarnCF("maixcam", "Unknown message type", map[string]interface{}{ - "type": msg.Type, - }) - } -} - -func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) { - logger.InfoCF("maixcam", "", map[string]interface{}{ - "timestamp": msg.Timestamp, - "data": msg.Data, - }) - - senderID := "maixcam" - chatID := "default" - - classInfo, ok := msg.Data["class_name"].(string) - if !ok { - classInfo = "person" - } - - score, _ := msg.Data["score"].(float64) - x, _ := msg.Data["x"].(float64) - y, _ := msg.Data["y"].(float64) - w, _ := msg.Data["w"].(float64) - h, _ := msg.Data["h"].(float64) - - content := fmt.Sprintf("📷 Person detected!\nClass: %s\nConfidence: %.2f%%\nPosition: (%.0f, %.0f)\nSize: %.0fx%.0f", - classInfo, score*100, x, y, w, h) - - metadata := map[string]string{ - "timestamp": fmt.Sprintf("%.0f", msg.Timestamp), - "class_id": fmt.Sprintf("%.0f", msg.Data["class_id"]), - "score": fmt.Sprintf("%.2f", score), - "x": fmt.Sprintf("%.0f", x), - "y": fmt.Sprintf("%.0f", y), - "w": fmt.Sprintf("%.0f", w), - "h": fmt.Sprintf("%.0f", h), - } - - c.HandleMessage(senderID, chatID, content, []string{}, metadata) -} - -func (c *MaixCamChannel) handleStatusUpdate(msg MaixCamMessage) { - logger.InfoCF("maixcam", "Status update from MaixCam", map[string]interface{}{ - "status": msg.Data, - }) -} - -func (c *MaixCamChannel) Stop(ctx context.Context) error { - logger.InfoC("maixcam", "Stopping MaixCam channel") - c.setRunning(false) - - if c.listener != nil { - c.listener.Close() - } - - c.clientsMux.Lock() - defer c.clientsMux.Unlock() - - for conn := range c.clients { - conn.Close() - } - c.clients = make(map[net.Conn]bool) - - logger.InfoC("maixcam", "MaixCam channel stopped") - return nil -} - -func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { - if !c.IsRunning() { - return fmt.Errorf("maixcam channel not running") - } - - c.clientsMux.RLock() - defer c.clientsMux.RUnlock() - - if len(c.clients) == 0 { - logger.WarnC("maixcam", "No MaixCam devices connected") - return fmt.Errorf("no connected MaixCam devices") - } - - response := map[string]interface{}{ - "type": "command", - "timestamp": float64(0), - "message": msg.Content, - "chat_id": msg.ChatID, - } - - data, err := json.Marshal(response) - if err != nil { - return fmt.Errorf("failed to marshal response: %w", err) - } - - var sendErr error - for conn := range c.clients { - if _, err := conn.Write(data); err != nil { - logger.ErrorCF("maixcam", "Failed to send to client", map[string]interface{}{ - "client": conn.RemoteAddr().String(), - "error": err.Error(), - }) - sendErr = err - } - } - - return sendErr -} diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 57e45f4d4..a4f44ddfe 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -72,19 +72,6 @@ func (m *Manager) initChannels() error { } } - if m.config.Channels.Feishu.Enabled { - logger.DebugC("channels", "Attempting to initialize Feishu channel") - feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus) - if err != nil { - logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]interface{}{ - "error": err.Error(), - }) - } else { - m.channels["feishu"] = feishu - logger.InfoC("channels", "Feishu channel enabled successfully") - } - } - if m.config.Channels.Discord.Enabled && m.config.Channels.Discord.Token != "" { logger.DebugC("channels", "Attempting to initialize Discord channel") discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus) @@ -98,45 +85,6 @@ func (m *Manager) initChannels() error { } } - if m.config.Channels.MaixCam.Enabled { - logger.DebugC("channels", "Attempting to initialize MaixCam channel") - maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus) - if err != nil { - logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]interface{}{ - "error": err.Error(), - }) - } else { - m.channels["maixcam"] = maixcam - logger.InfoC("channels", "MaixCam channel enabled successfully") - } - } - - if m.config.Channels.QQ.Enabled { - logger.DebugC("channels", "Attempting to initialize QQ channel") - qq, err := NewQQChannel(m.config.Channels.QQ, m.bus) - if err != nil { - logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]interface{}{ - "error": err.Error(), - }) - } else { - m.channels["qq"] = qq - logger.InfoC("channels", "QQ channel enabled successfully") - } - } - - if m.config.Channels.DingTalk.Enabled && m.config.Channels.DingTalk.ClientID != "" { - logger.DebugC("channels", "Attempting to initialize DingTalk channel") - dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus) - if err != nil { - logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]interface{}{ - "error": err.Error(), - }) - } else { - m.channels["dingtalk"] = dingtalk - logger.InfoC("channels", "DingTalk channel enabled successfully") - } - } - if m.config.Channels.Slack.Enabled && m.config.Channels.Slack.BotToken != "" { logger.DebugC("channels", "Attempting to initialize Slack channel") slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus) @@ -163,19 +111,6 @@ func (m *Manager) initChannels() error { } } - if m.config.Channels.OneBot.Enabled && m.config.Channels.OneBot.WSUrl != "" { - logger.DebugC("channels", "Attempting to initialize OneBot channel") - onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus) - if err != nil { - logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]interface{}{ - "error": err.Error(), - }) - } else { - m.channels["onebot"] = onebot - logger.InfoC("channels", "OneBot channel enabled successfully") - } - } - if m.config.Channels.WebSocket.Enabled { logger.DebugC("channels", "Attempting to initialize WebSocket channel") ws, err := NewWebSocketChannel(m.config.Channels.WebSocket, m.bus) diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go deleted file mode 100644 index 5699a4f63..000000000 --- a/pkg/channels/onebot.go +++ /dev/null @@ -1,686 +0,0 @@ -package channels - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - "sync" - "time" - - "github.com/gorilla/websocket" - - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/config" - "github.com/KarakuriAgent/clawdroid/pkg/logger" -) - -type OneBotChannel struct { - *BaseChannel - config config.OneBotConfig - conn *websocket.Conn - ctx context.Context - cancel context.CancelFunc - dedup map[string]struct{} - dedupRing []string - dedupIdx int - mu sync.Mutex - writeMu sync.Mutex - echoCounter int64 -} - -type oneBotRawEvent struct { - PostType string `json:"post_type"` - MessageType string `json:"message_type"` - SubType string `json:"sub_type"` - MessageID json.RawMessage `json:"message_id"` - UserID json.RawMessage `json:"user_id"` - GroupID json.RawMessage `json:"group_id"` - RawMessage string `json:"raw_message"` - Message json.RawMessage `json:"message"` - Sender json.RawMessage `json:"sender"` - SelfID json.RawMessage `json:"self_id"` - Time json.RawMessage `json:"time"` - MetaEventType string `json:"meta_event_type"` - Echo string `json:"echo"` - RetCode json.RawMessage `json:"retcode"` - Status BotStatus `json:"status"` -} - -type BotStatus struct { - Online bool `json:"online"` - Good bool `json:"good"` -} - -type oneBotSender struct { - UserID json.RawMessage `json:"user_id"` - Nickname string `json:"nickname"` - Card string `json:"card"` -} - -type oneBotEvent struct { - PostType string - MessageType string - SubType string - MessageID string - UserID int64 - GroupID int64 - Content string - RawContent string - IsBotMentioned bool - Sender oneBotSender - SelfID int64 - Time int64 - MetaEventType string -} - -type oneBotAPIRequest struct { - Action string `json:"action"` - Params interface{} `json:"params"` - Echo string `json:"echo,omitempty"` -} - -type oneBotSendPrivateMsgParams struct { - UserID int64 `json:"user_id"` - Message string `json:"message"` -} - -type oneBotSendGroupMsgParams struct { - GroupID int64 `json:"group_id"` - Message string `json:"message"` -} - -func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) { - base := NewBaseChannel("onebot", cfg, messageBus, cfg.AllowFrom) - - const dedupSize = 1024 - return &OneBotChannel{ - BaseChannel: base, - config: cfg, - dedup: make(map[string]struct{}, dedupSize), - dedupRing: make([]string, dedupSize), - dedupIdx: 0, - }, nil -} - -func (c *OneBotChannel) Start(ctx context.Context) error { - if c.config.WSUrl == "" { - return fmt.Errorf("OneBot ws_url not configured") - } - - logger.InfoCF("onebot", "Starting OneBot channel", map[string]interface{}{ - "ws_url": c.config.WSUrl, - }) - - c.ctx, c.cancel = context.WithCancel(ctx) - - if err := c.connect(); err != nil { - logger.WarnCF("onebot", "Initial connection failed, will retry in background", map[string]interface{}{ - "error": err.Error(), - }) - } else { - go c.listen() - } - - if c.config.ReconnectInterval > 0 { - go c.reconnectLoop() - } else { - // If reconnect is disabled but initial connection failed, we cannot recover - if c.conn == nil { - return fmt.Errorf("failed to connect to OneBot and reconnect is disabled") - } - } - - c.setRunning(true) - logger.InfoC("onebot", "OneBot channel started successfully") - - return nil -} - -func (c *OneBotChannel) connect() error { - dialer := websocket.DefaultDialer - dialer.HandshakeTimeout = 10 * time.Second - - header := make(map[string][]string) - if c.config.AccessToken != "" { - header["Authorization"] = []string{"Bearer " + c.config.AccessToken} - } - - conn, _, err := dialer.Dial(c.config.WSUrl, header) - if err != nil { - return err - } - - c.mu.Lock() - c.conn = conn - c.mu.Unlock() - - logger.InfoC("onebot", "WebSocket connected") - return nil -} - -func (c *OneBotChannel) reconnectLoop() { - interval := time.Duration(c.config.ReconnectInterval) * time.Second - if interval < 5*time.Second { - interval = 5 * time.Second - } - - for { - select { - case <-c.ctx.Done(): - return - case <-time.After(interval): - c.mu.Lock() - conn := c.conn - c.mu.Unlock() - - if conn == nil { - logger.InfoC("onebot", "Attempting to reconnect...") - if err := c.connect(); err != nil { - logger.ErrorCF("onebot", "Reconnect failed", map[string]interface{}{ - "error": err.Error(), - }) - } else { - go c.listen() - } - } - } - } -} - -func (c *OneBotChannel) Stop(ctx context.Context) error { - logger.InfoC("onebot", "Stopping OneBot channel") - c.setRunning(false) - - if c.cancel != nil { - c.cancel() - } - - c.mu.Lock() - if c.conn != nil { - c.conn.Close() - c.conn = nil - } - c.mu.Unlock() - - return nil -} - -func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { - if !c.IsRunning() { - return fmt.Errorf("OneBot channel not running") - } - - c.mu.Lock() - conn := c.conn - c.mu.Unlock() - - if conn == nil { - return fmt.Errorf("OneBot WebSocket not connected") - } - - action, params, err := c.buildSendRequest(msg) - if err != nil { - return err - } - - c.writeMu.Lock() - c.echoCounter++ - echo := fmt.Sprintf("send_%d", c.echoCounter) - c.writeMu.Unlock() - - req := oneBotAPIRequest{ - Action: action, - Params: params, - Echo: echo, - } - - data, err := json.Marshal(req) - if err != nil { - return fmt.Errorf("failed to marshal OneBot request: %w", err) - } - - c.writeMu.Lock() - err = conn.WriteMessage(websocket.TextMessage, data) - c.writeMu.Unlock() - - if err != nil { - logger.ErrorCF("onebot", "Failed to send message", map[string]interface{}{ - "error": err.Error(), - }) - return err - } - - return nil -} - -func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, interface{}, error) { - chatID := msg.ChatID - - if len(chatID) > 6 && chatID[:6] == "group:" { - groupID, err := strconv.ParseInt(chatID[6:], 10, 64) - if err != nil { - return "", nil, fmt.Errorf("invalid group ID in chatID: %s", chatID) - } - return "send_group_msg", oneBotSendGroupMsgParams{ - GroupID: groupID, - Message: msg.Content, - }, nil - } - - if len(chatID) > 8 && chatID[:8] == "private:" { - userID, err := strconv.ParseInt(chatID[8:], 10, 64) - if err != nil { - return "", nil, fmt.Errorf("invalid user ID in chatID: %s", chatID) - } - return "send_private_msg", oneBotSendPrivateMsgParams{ - UserID: userID, - Message: msg.Content, - }, nil - } - - userID, err := strconv.ParseInt(chatID, 10, 64) - if err != nil { - return "", nil, fmt.Errorf("invalid chatID for OneBot: %s", chatID) - } - - return "send_private_msg", oneBotSendPrivateMsgParams{ - UserID: userID, - Message: msg.Content, - }, nil -} - -func (c *OneBotChannel) listen() { - for { - select { - case <-c.ctx.Done(): - return - default: - c.mu.Lock() - conn := c.conn - c.mu.Unlock() - - if conn == nil { - logger.WarnC("onebot", "WebSocket connection is nil, listener exiting") - return - } - - _, message, err := conn.ReadMessage() - if err != nil { - logger.ErrorCF("onebot", "WebSocket read error", map[string]interface{}{ - "error": err.Error(), - }) - c.mu.Lock() - if c.conn != nil { - c.conn.Close() - c.conn = nil - } - c.mu.Unlock() - return - } - - logger.DebugCF("onebot", "Raw WebSocket message received", map[string]interface{}{ - "length": len(message), - "payload": string(message), - }) - - var raw oneBotRawEvent - if err := json.Unmarshal(message, &raw); err != nil { - logger.WarnCF("onebot", "Failed to unmarshal raw event", map[string]interface{}{ - "error": err.Error(), - "payload": string(message), - }) - continue - } - - if raw.Echo != "" || raw.Status.Online || raw.Status.Good { - logger.DebugCF("onebot", "Received API response, skipping", map[string]interface{}{ - "echo": raw.Echo, - "status": raw.Status, - }) - continue - } - - logger.DebugCF("onebot", "Parsed raw event", map[string]interface{}{ - "post_type": raw.PostType, - "message_type": raw.MessageType, - "sub_type": raw.SubType, - "meta_event_type": raw.MetaEventType, - }) - - c.handleRawEvent(&raw) - } - } -} - -func parseJSONInt64(raw json.RawMessage) (int64, error) { - if len(raw) == 0 { - return 0, nil - } - - var n int64 - if err := json.Unmarshal(raw, &n); err == nil { - return n, nil - } - - var s string - if err := json.Unmarshal(raw, &s); err == nil { - return strconv.ParseInt(s, 10, 64) - } - return 0, fmt.Errorf("cannot parse as int64: %s", string(raw)) -} - -func parseJSONString(raw json.RawMessage) string { - if len(raw) == 0 { - return "" - } - var s string - if err := json.Unmarshal(raw, &s); err == nil { - return s - } - - return string(raw) -} - -type parseMessageResult struct { - Text string - IsBotMentioned bool -} - -func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult { - if len(raw) == 0 { - return parseMessageResult{} - } - - var s string - if err := json.Unmarshal(raw, &s); err == nil { - mentioned := false - if selfID > 0 { - cqAt := fmt.Sprintf("[CQ:at,qq=%d]", selfID) - if strings.Contains(s, cqAt) { - mentioned = true - s = strings.ReplaceAll(s, cqAt, "") - s = strings.TrimSpace(s) - } - } - return parseMessageResult{Text: s, IsBotMentioned: mentioned} - } - - var segments []map[string]interface{} - if err := json.Unmarshal(raw, &segments); err == nil { - var text string - mentioned := false - selfIDStr := strconv.FormatInt(selfID, 10) - for _, seg := range segments { - segType, _ := seg["type"].(string) - data, _ := seg["data"].(map[string]interface{}) - switch segType { - case "text": - if data != nil { - if t, ok := data["text"].(string); ok { - text += t - } - } - case "at": - if data != nil && selfID > 0 { - qqVal := fmt.Sprintf("%v", data["qq"]) - if qqVal == selfIDStr || qqVal == "all" { - mentioned = true - } - } - } - } - return parseMessageResult{Text: strings.TrimSpace(text), IsBotMentioned: mentioned} - } - return parseMessageResult{} -} - -func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) { - switch raw.PostType { - case "message": - evt, err := c.normalizeMessageEvent(raw) - if err != nil { - logger.WarnCF("onebot", "Failed to normalize message event", map[string]interface{}{ - "error": err.Error(), - }) - return - } - c.handleMessage(evt) - case "meta_event": - c.handleMetaEvent(raw) - case "notice": - logger.DebugCF("onebot", "Notice event received", map[string]interface{}{ - "sub_type": raw.SubType, - }) - case "request": - logger.DebugCF("onebot", "Request event received", map[string]interface{}{ - "sub_type": raw.SubType, - }) - case "": - logger.DebugCF("onebot", "Event with empty post_type (possibly API response)", map[string]interface{}{ - "echo": raw.Echo, - "status": raw.Status, - }) - default: - logger.DebugCF("onebot", "Unknown post_type", map[string]interface{}{ - "post_type": raw.PostType, - }) - } -} - -func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent, error) { - userID, err := parseJSONInt64(raw.UserID) - if err != nil { - return nil, fmt.Errorf("parse user_id: %w (raw: %s)", err, string(raw.UserID)) - } - - groupID, _ := parseJSONInt64(raw.GroupID) - selfID, _ := parseJSONInt64(raw.SelfID) - ts, _ := parseJSONInt64(raw.Time) - messageID := parseJSONString(raw.MessageID) - - parsed := parseMessageContentEx(raw.Message, selfID) - isBotMentioned := parsed.IsBotMentioned - - content := raw.RawMessage - if content == "" { - content = parsed.Text - } else if selfID > 0 { - cqAt := fmt.Sprintf("[CQ:at,qq=%d]", selfID) - if strings.Contains(content, cqAt) { - isBotMentioned = true - content = strings.ReplaceAll(content, cqAt, "") - content = strings.TrimSpace(content) - } - } - - var sender oneBotSender - if len(raw.Sender) > 0 { - if err := json.Unmarshal(raw.Sender, &sender); err != nil { - logger.WarnCF("onebot", "Failed to parse sender", map[string]interface{}{ - "error": err.Error(), - "sender": string(raw.Sender), - }) - } - } - - logger.DebugCF("onebot", "Normalized message event", map[string]interface{}{ - "message_type": raw.MessageType, - "user_id": userID, - "group_id": groupID, - "message_id": messageID, - "content_len": len(content), - "nickname": sender.Nickname, - }) - - return &oneBotEvent{ - PostType: raw.PostType, - MessageType: raw.MessageType, - SubType: raw.SubType, - MessageID: messageID, - UserID: userID, - GroupID: groupID, - Content: content, - RawContent: raw.RawMessage, - IsBotMentioned: isBotMentioned, - Sender: sender, - SelfID: selfID, - Time: ts, - MetaEventType: raw.MetaEventType, - }, nil -} - -func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) { - switch raw.MetaEventType { - case "lifecycle": - logger.InfoCF("onebot", "Lifecycle event", map[string]interface{}{ - "sub_type": raw.SubType, - }) - case "heartbeat": - logger.DebugC("onebot", "Heartbeat received") - default: - logger.DebugCF("onebot", "Unknown meta_event_type", map[string]interface{}{ - "meta_event_type": raw.MetaEventType, - }) - } -} - -func (c *OneBotChannel) handleMessage(evt *oneBotEvent) { - if c.isDuplicate(evt.MessageID) { - logger.DebugCF("onebot", "Duplicate message, skipping", map[string]interface{}{ - "message_id": evt.MessageID, - }) - return - } - - content := evt.Content - if content == "" { - logger.DebugCF("onebot", "Received empty message, ignoring", map[string]interface{}{ - "message_id": evt.MessageID, - }) - return - } - - senderID := strconv.FormatInt(evt.UserID, 10) - var chatID string - - metadata := map[string]string{ - "message_id": evt.MessageID, - } - - switch evt.MessageType { - case "private": - chatID = "private:" + senderID - logger.InfoCF("onebot", "Received private message", map[string]interface{}{ - "sender": senderID, - "message_id": evt.MessageID, - "length": len(content), - "content": truncate(content, 100), - }) - - case "group": - groupIDStr := strconv.FormatInt(evt.GroupID, 10) - chatID = "group:" + groupIDStr - metadata["group_id"] = groupIDStr - - senderUserID, _ := parseJSONInt64(evt.Sender.UserID) - if senderUserID > 0 { - metadata["sender_user_id"] = strconv.FormatInt(senderUserID, 10) - } - - if evt.Sender.Card != "" { - metadata["sender_name"] = evt.Sender.Card - } else if evt.Sender.Nickname != "" { - metadata["sender_name"] = evt.Sender.Nickname - } - - triggered, strippedContent := c.checkGroupTrigger(content, evt.IsBotMentioned) - if !triggered { - logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]interface{}{ - "sender": senderID, - "group": groupIDStr, - "is_mentioned": evt.IsBotMentioned, - "content": truncate(content, 100), - }) - return - } - content = strippedContent - - logger.InfoCF("onebot", "Received group message", map[string]interface{}{ - "sender": senderID, - "group": groupIDStr, - "message_id": evt.MessageID, - "is_mentioned": evt.IsBotMentioned, - "length": len(content), - "content": truncate(content, 100), - }) - - default: - logger.WarnCF("onebot", "Unknown message type, cannot route", map[string]interface{}{ - "type": evt.MessageType, - "message_id": evt.MessageID, - "user_id": evt.UserID, - }) - return - } - - if evt.Sender.Nickname != "" { - metadata["nickname"] = evt.Sender.Nickname - } - - logger.DebugCF("onebot", "Forwarding message to bus", map[string]interface{}{ - "sender_id": senderID, - "chat_id": chatID, - "content": truncate(content, 100), - }) - - c.HandleMessage(senderID, chatID, content, []string{}, metadata) -} - -func (c *OneBotChannel) isDuplicate(messageID string) bool { - if messageID == "" || messageID == "0" { - return false - } - - c.mu.Lock() - defer c.mu.Unlock() - - if _, exists := c.dedup[messageID]; exists { - return true - } - - if old := c.dedupRing[c.dedupIdx]; old != "" { - delete(c.dedup, old) - } - c.dedupRing[c.dedupIdx] = messageID - c.dedup[messageID] = struct{}{} - c.dedupIdx = (c.dedupIdx + 1) % len(c.dedupRing) - - return false -} - -func truncate(s string, n int) string { - runes := []rune(s) - if len(runes) <= n { - return s - } - return string(runes[:n]) + "..." -} - -func (c *OneBotChannel) checkGroupTrigger(content string, isBotMentioned bool) (triggered bool, strippedContent string) { - if isBotMentioned { - return true, strings.TrimSpace(content) - } - - for _, prefix := range c.config.GroupTriggerPrefix { - if prefix == "" { - continue - } - if strings.HasPrefix(content, prefix) { - return true, strings.TrimSpace(strings.TrimPrefix(content, prefix)) - } - } - - return false, content -} diff --git a/pkg/channels/qq.go b/pkg/channels/qq.go deleted file mode 100644 index 062b0a773..000000000 --- a/pkg/channels/qq.go +++ /dev/null @@ -1,243 +0,0 @@ -package channels - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/tencent-connect/botgo" - "github.com/tencent-connect/botgo/dto" - "github.com/tencent-connect/botgo/event" - "github.com/tencent-connect/botgo/openapi" - "github.com/tencent-connect/botgo/token" - "golang.org/x/oauth2" - - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/config" - "github.com/KarakuriAgent/clawdroid/pkg/logger" -) - -type QQChannel struct { - *BaseChannel - config config.QQConfig - api openapi.OpenAPI - tokenSource oauth2.TokenSource - ctx context.Context - cancel context.CancelFunc - sessionManager botgo.SessionManager - processedIDs map[string]bool - mu sync.RWMutex -} - -func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel, error) { - base := NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom) - - return &QQChannel{ - BaseChannel: base, - config: cfg, - processedIDs: make(map[string]bool), - }, nil -} - -func (c *QQChannel) Start(ctx context.Context) error { - if c.config.AppID == "" || c.config.AppSecret == "" { - return fmt.Errorf("QQ app_id and app_secret not configured") - } - - logger.InfoC("qq", "Starting QQ bot (WebSocket mode)") - - // 创建 token source - credentials := &token.QQBotCredentials{ - AppID: c.config.AppID, - AppSecret: c.config.AppSecret, - } - c.tokenSource = token.NewQQBotTokenSource(credentials) - - // 创建子 context - c.ctx, c.cancel = context.WithCancel(ctx) - - // 启动自动刷新 token 协程 - if err := token.StartRefreshAccessToken(c.ctx, c.tokenSource); err != nil { - return fmt.Errorf("failed to start token refresh: %w", err) - } - - // 初始化 OpenAPI 客户端 - c.api = botgo.NewOpenAPI(c.config.AppID, c.tokenSource).WithTimeout(5 * time.Second) - - // 注册事件处理器 - intent := event.RegisterHandlers( - c.handleC2CMessage(), - c.handleGroupATMessage(), - ) - - // 获取 WebSocket 接入点 - wsInfo, err := c.api.WS(c.ctx, nil, "") - if err != nil { - return fmt.Errorf("failed to get websocket info: %w", err) - } - - logger.InfoCF("qq", "Got WebSocket info", map[string]interface{}{ - "shards": wsInfo.Shards, - }) - - // 创建并保存 sessionManager - c.sessionManager = botgo.NewSessionManager() - - // 在 goroutine 中启动 WebSocket 连接,避免阻塞 - go func() { - if err := c.sessionManager.Start(wsInfo, c.tokenSource, &intent); err != nil { - logger.ErrorCF("qq", "WebSocket session error", map[string]interface{}{ - "error": err.Error(), - }) - c.setRunning(false) - } - }() - - c.setRunning(true) - logger.InfoC("qq", "QQ bot started successfully") - - return nil -} - -func (c *QQChannel) Stop(ctx context.Context) error { - logger.InfoC("qq", "Stopping QQ bot") - c.setRunning(false) - - if c.cancel != nil { - c.cancel() - } - - return nil -} - -func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { - if !c.IsRunning() { - return fmt.Errorf("QQ bot not running") - } - - // 构造消息 - msgToCreate := &dto.MessageToCreate{ - Content: msg.Content, - } - - // C2C 消息发送 - _, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate) - if err != nil { - logger.ErrorCF("qq", "Failed to send C2C message", map[string]interface{}{ - "error": err.Error(), - }) - return err - } - - return nil -} - -// handleC2CMessage 处理 QQ 私聊消息 -func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler { - return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error { - // 去重检查 - if c.isDuplicate(data.ID) { - return nil - } - - // 提取用户信息 - var senderID string - if data.Author != nil && data.Author.ID != "" { - senderID = data.Author.ID - } else { - logger.WarnC("qq", "Received message with no sender ID") - return nil - } - - // 提取消息内容 - content := data.Content - if content == "" { - logger.DebugC("qq", "Received empty message, ignoring") - return nil - } - - logger.InfoCF("qq", "Received C2C message", map[string]interface{}{ - "sender": senderID, - "length": len(content), - }) - - // 转发到消息总线 - metadata := map[string]string{ - "message_id": data.ID, - } - - c.HandleMessage(senderID, senderID, content, []string{}, metadata) - - return nil - } -} - -// handleGroupATMessage 处理群@消息 -func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler { - return func(event *dto.WSPayload, data *dto.WSGroupATMessageData) error { - // 去重检查 - if c.isDuplicate(data.ID) { - return nil - } - - // 提取用户信息 - var senderID string - if data.Author != nil && data.Author.ID != "" { - senderID = data.Author.ID - } else { - logger.WarnC("qq", "Received group message with no sender ID") - return nil - } - - // 提取消息内容(去掉 @ 机器人部分) - content := data.Content - if content == "" { - logger.DebugC("qq", "Received empty group message, ignoring") - return nil - } - - logger.InfoCF("qq", "Received group AT message", map[string]interface{}{ - "sender": senderID, - "group": data.GroupID, - "length": len(content), - }) - - // 转发到消息总线(使用 GroupID 作为 ChatID) - metadata := map[string]string{ - "message_id": data.ID, - "group_id": data.GroupID, - } - - c.HandleMessage(senderID, data.GroupID, content, []string{}, metadata) - - return nil - } -} - -// isDuplicate 检查消息是否重复 -func (c *QQChannel) isDuplicate(messageID string) bool { - c.mu.Lock() - defer c.mu.Unlock() - - if c.processedIDs[messageID] { - return true - } - - c.processedIDs[messageID] = true - - // 简单清理:限制 map 大小 - if len(c.processedIDs) > 10000 { - // 清空一半 - count := 0 - for id := range c.processedIDs { - if count >= 5000 { - break - } - delete(c.processedIDs, id) - count++ - } - } - - return false -} diff --git a/pkg/channels/telegram_commands.go b/pkg/channels/telegram_commands.go index 291176be7..cbe9544bc 100644 --- a/pkg/channels/telegram_commands.go +++ b/pkg/channels/telegram_commands.go @@ -122,15 +122,15 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error { if c.config.Channels.WhatsApp.Enabled { enabled = append(enabled, "whatsapp") } - if c.config.Channels.Feishu.Enabled { - enabled = append(enabled, "feishu") - } if c.config.Channels.Discord.Enabled { enabled = append(enabled, "discord") } if c.config.Channels.Slack.Enabled { enabled = append(enabled, "slack") } + if c.config.Channels.LINE.Enabled { + enabled = append(enabled, "line") + } response = fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- ")) default: diff --git a/pkg/config/config.go b/pkg/config/config.go index b0c2c5f43..5221a883e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -77,14 +77,9 @@ type AgentDefaults struct { type ChannelsConfig struct { WhatsApp WhatsAppConfig `json:"whatsapp"` Telegram TelegramConfig `json:"telegram"` - Feishu FeishuConfig `json:"feishu"` Discord DiscordConfig `json:"discord"` - MaixCam MaixCamConfig `json:"maixcam"` - QQ QQConfig `json:"qq"` - DingTalk DingTalkConfig `json:"dingtalk"` Slack SlackConfig `json:"slack"` LINE LINEConfig `json:"line"` - OneBot OneBotConfig `json:"onebot"` WebSocket WebSocketConfig `json:"websocket"` } @@ -101,42 +96,12 @@ type TelegramConfig struct { AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_TELEGRAM_ALLOW_FROM"` } -type FeishuConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_FEISHU_ENABLED"` - AppID string `json:"app_id" env:"CLAWDROID_CHANNELS_FEISHU_APP_ID"` - AppSecret string `json:"app_secret" env:"CLAWDROID_CHANNELS_FEISHU_APP_SECRET"` - EncryptKey string `json:"encrypt_key" env:"CLAWDROID_CHANNELS_FEISHU_ENCRYPT_KEY"` - VerificationToken string `json:"verification_token" env:"CLAWDROID_CHANNELS_FEISHU_VERIFICATION_TOKEN"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_FEISHU_ALLOW_FROM"` -} - type DiscordConfig struct { Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_DISCORD_ENABLED"` Token string `json:"token" env:"CLAWDROID_CHANNELS_DISCORD_TOKEN"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_DISCORD_ALLOW_FROM"` } -type MaixCamConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_MAIXCAM_ENABLED"` - Host string `json:"host" env:"CLAWDROID_CHANNELS_MAIXCAM_HOST"` - Port int `json:"port" env:"CLAWDROID_CHANNELS_MAIXCAM_PORT"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_MAIXCAM_ALLOW_FROM"` -} - -type QQConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_QQ_ENABLED"` - AppID string `json:"app_id" env:"CLAWDROID_CHANNELS_QQ_APP_ID"` - AppSecret string `json:"app_secret" env:"CLAWDROID_CHANNELS_QQ_APP_SECRET"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_QQ_ALLOW_FROM"` -} - -type DingTalkConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_DINGTALK_ENABLED"` - ClientID string `json:"client_id" env:"CLAWDROID_CHANNELS_DINGTALK_CLIENT_ID"` - ClientSecret string `json:"client_secret" env:"CLAWDROID_CHANNELS_DINGTALK_CLIENT_SECRET"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_DINGTALK_ALLOW_FROM"` -} - type SlackConfig struct { Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_SLACK_ENABLED"` BotToken string `json:"bot_token" env:"CLAWDROID_CHANNELS_SLACK_BOT_TOKEN"` @@ -154,15 +119,6 @@ type LINEConfig struct { AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_LINE_ALLOW_FROM"` } -type OneBotConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_ONEBOT_ENABLED"` - WSUrl string `json:"ws_url" env:"CLAWDROID_CHANNELS_ONEBOT_WS_URL"` - AccessToken string `json:"access_token" env:"CLAWDROID_CHANNELS_ONEBOT_ACCESS_TOKEN"` - ReconnectInterval int `json:"reconnect_interval" env:"CLAWDROID_CHANNELS_ONEBOT_RECONNECT_INTERVAL"` - GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"CLAWDROID_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"CLAWDROID_CHANNELS_ONEBOT_ALLOW_FROM"` -} - type WebSocketConfig struct { Enabled bool `json:"enabled" env:"CLAWDROID_CHANNELS_WEBSOCKET_ENABLED"` Host string `json:"host" env:"CLAWDROID_CHANNELS_WEBSOCKET_HOST"` @@ -263,37 +219,11 @@ func DefaultConfig() *Config { Token: "", AllowFrom: FlexibleStringSlice{}, }, - Feishu: FeishuConfig{ - Enabled: false, - AppID: "", - AppSecret: "", - EncryptKey: "", - VerificationToken: "", - AllowFrom: FlexibleStringSlice{}, - }, Discord: DiscordConfig{ Enabled: false, Token: "", AllowFrom: FlexibleStringSlice{}, }, - MaixCam: MaixCamConfig{ - Enabled: false, - Host: "127.0.0.1", - Port: 18792, - AllowFrom: FlexibleStringSlice{}, - }, - QQ: QQConfig{ - Enabled: false, - AppID: "", - AppSecret: "", - AllowFrom: FlexibleStringSlice{}, - }, - DingTalk: DingTalkConfig{ - Enabled: false, - ClientID: "", - ClientSecret: "", - AllowFrom: FlexibleStringSlice{}, - }, Slack: SlackConfig{ Enabled: false, BotToken: "", @@ -309,14 +239,6 @@ func DefaultConfig() *Config { WebhookPath: "/webhook/line", AllowFrom: FlexibleStringSlice{}, }, - OneBot: OneBotConfig{ - Enabled: false, - WSUrl: "ws://127.0.0.1:3001", - AccessToken: "", - ReconnectInterval: 5, - GroupTriggerPrefix: []string{}, - AllowFrom: FlexibleStringSlice{}, - }, WebSocket: WebSocketConfig{ Enabled: true, Host: "127.0.0.1", diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 3b0adcc49..d72a558eb 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -54,12 +54,12 @@ func TestDefaultConfig_MaxToolIterations(t *testing.T) { } } -// TestDefaultConfig_Temperature verifies temperature has default value +// TestDefaultConfig_Temperature verifies temperature has expected default value (0 = deterministic) func TestDefaultConfig_Temperature(t *testing.T) { cfg := DefaultConfig() - if cfg.Agents.Defaults.Temperature == 0 { - t.Error("Temperature should not be zero") + if cfg.Agents.Defaults.Temperature != 0 { + t.Errorf("Temperature should be 0 by default, got %v", cfg.Agents.Defaults.Temperature) } } @@ -101,21 +101,9 @@ func TestDefaultConfig_Channels(t *testing.T) { if cfg.Channels.Telegram.Enabled { t.Error("Telegram should be disabled by default") } - if cfg.Channels.Feishu.Enabled { - t.Error("Feishu should be disabled by default") - } if cfg.Channels.Discord.Enabled { t.Error("Discord should be disabled by default") } - if cfg.Channels.MaixCam.Enabled { - t.Error("MaixCam should be disabled by default") - } - if cfg.Channels.QQ.Enabled { - t.Error("QQ should be disabled by default") - } - if cfg.Channels.DingTalk.Enabled { - t.Error("DingTalk should be disabled by default") - } if cfg.Channels.Slack.Enabled { t.Error("Slack should be disabled by default") } @@ -208,9 +196,6 @@ func TestConfig_Complete(t *testing.T) { if cfg.LLM.Model == "" { t.Error("LLM.Model should not be empty") } - if cfg.Agents.Defaults.Temperature == 0 { - t.Error("Temperature should have default value") - } if cfg.Agents.Defaults.MaxTokens == 0 { t.Error("MaxTokens should not be zero") } diff --git a/pkg/health/server.go b/pkg/health/server.go deleted file mode 100644 index 77b36034d..000000000 --- a/pkg/health/server.go +++ /dev/null @@ -1,164 +0,0 @@ -package health - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "sync" - "time" -) - -type Server struct { - server *http.Server - mu sync.RWMutex - ready bool - checks map[string]Check - startTime time.Time -} - -type Check struct { - Name string `json:"name"` - Status string `json:"status"` - Message string `json:"message,omitempty"` - Timestamp time.Time `json:"timestamp"` -} - -type StatusResponse struct { - Status string `json:"status"` - Uptime string `json:"uptime"` - Checks map[string]Check `json:"checks,omitempty"` -} - -func NewServer(host string, port int) *Server { - mux := http.NewServeMux() - s := &Server{ - ready: false, - checks: make(map[string]Check), - startTime: time.Now(), - } - - mux.HandleFunc("/health", s.healthHandler) - mux.HandleFunc("/ready", s.readyHandler) - - addr := fmt.Sprintf("%s:%d", host, port) - s.server = &http.Server{ - Addr: addr, - Handler: mux, - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - } - - return s -} - -func (s *Server) Start() error { - s.mu.Lock() - s.ready = true - s.mu.Unlock() - return s.server.ListenAndServe() -} - -func (s *Server) StartContext(ctx context.Context) error { - s.mu.Lock() - s.ready = true - s.mu.Unlock() - - errCh := make(chan error, 1) - go func() { - errCh <- s.server.ListenAndServe() - }() - - select { - case err := <-errCh: - return err - case <-ctx.Done(): - return s.server.Shutdown(context.Background()) - } -} - -func (s *Server) Stop(ctx context.Context) error { - s.mu.Lock() - s.ready = false - s.mu.Unlock() - return s.server.Shutdown(ctx) -} - -func (s *Server) SetReady(ready bool) { - s.mu.Lock() - s.ready = ready - s.mu.Unlock() -} - -func (s *Server) RegisterCheck(name string, checkFn func() (bool, string)) { - s.mu.Lock() - defer s.mu.Unlock() - - status, msg := checkFn() - s.checks[name] = Check{ - Name: name, - Status: statusString(status), - Message: msg, - Timestamp: time.Now(), - } -} - -func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - uptime := time.Since(s.startTime) - resp := StatusResponse{ - Status: "ok", - Uptime: uptime.String(), - } - - json.NewEncoder(w).Encode(resp) -} - -func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - s.mu.RLock() - ready := s.ready - checks := make(map[string]Check) - for k, v := range s.checks { - checks[k] = v - } - s.mu.RUnlock() - - if !ready { - w.WriteHeader(http.StatusServiceUnavailable) - json.NewEncoder(w).Encode(StatusResponse{ - Status: "not ready", - Checks: checks, - }) - return - } - - for _, check := range checks { - if check.Status == "fail" { - w.WriteHeader(http.StatusServiceUnavailable) - json.NewEncoder(w).Encode(StatusResponse{ - Status: "not ready", - Checks: checks, - }) - return - } - } - - w.WriteHeader(http.StatusOK) - uptime := time.Since(s.startTime) - json.NewEncoder(w).Encode(StatusResponse{ - Status: "ready", - Uptime: uptime.String(), - Checks: checks, - }) -} - -func statusString(ok bool) string { - if ok { - return "ok" - } - return "fail" -} diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index 65a10c5d2..ae38371ce 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -271,7 +271,7 @@ This file contains tasks for the heartbeat service to check periodically. - Check for unread messages - Review upcoming calendar events -- Check device status (e.g., MaixCam) +- Check device status ## Instructions diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 22f66829f..dabebef16 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,7 +1,6 @@ package logger import ( - "encoding/json" "fmt" "log" "os" @@ -36,9 +35,7 @@ var ( mu sync.RWMutex ) -type Logger struct { - file *os.File -} +type Logger struct{} type LogEntry struct { Level string `json:"level"` @@ -61,41 +58,6 @@ func SetLevel(level LogLevel) { currentLevel = level } -func GetLevel() LogLevel { - mu.RLock() - defer mu.RUnlock() - return currentLevel -} - -func EnableFileLogging(filePath string) error { - mu.Lock() - defer mu.Unlock() - - file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("failed to open log file: %w", err) - } - - if logger.file != nil { - logger.file.Close() - } - - logger.file = file - log.Println("File logging enabled:", filePath) - return nil -} - -func DisableFileLogging() { - mu.Lock() - defer mu.Unlock() - - if logger.file != nil { - logger.file.Close() - logger.file = nil - log.Println("File logging disabled") - } -} - func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) { if level < currentLevel { return @@ -116,13 +78,6 @@ func logMessage(level LogLevel, component string, message string, fields map[str } } - if logger.file != nil { - jsonData, err := json.Marshal(entry) - if err == nil { - logger.file.WriteString(string(jsonData) + "\n") - } - } - var fieldStr string if len(fields) > 0 { fieldStr = " " + formatFields(fields) diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go index 9b9c96820..73036db41 100644 --- a/pkg/logger/logger_test.go +++ b/pkg/logger/logger_test.go @@ -5,7 +5,7 @@ import ( ) func TestLogLevelFiltering(t *testing.T) { - initialLevel := GetLevel() + initialLevel := currentLevel defer SetLevel(initialLevel) SetLevel(WARN) @@ -45,7 +45,7 @@ func TestLogLevelFiltering(t *testing.T) { } func TestLoggerWithComponent(t *testing.T) { - initialLevel := GetLevel() + initialLevel := currentLevel defer SetLevel(initialLevel) SetLevel(DEBUG) @@ -102,22 +102,8 @@ func TestLogLevels(t *testing.T) { } } -func TestSetGetLevel(t *testing.T) { - initialLevel := GetLevel() - defer SetLevel(initialLevel) - - tests := []LogLevel{DEBUG, INFO, WARN, ERROR, FATAL} - - for _, level := range tests { - SetLevel(level) - if GetLevel() != level { - t.Errorf("SetLevel(%v) -> GetLevel() = %v, want %v", level, GetLevel(), level) - } - } -} - func TestLoggerHelperFunctions(t *testing.T) { - initialLevel := GetLevel() + initialLevel := currentLevel defer SetLevel(initialLevel) SetLevel(INFO) diff --git a/workspace/IDENTITY.md b/workspace/IDENTITY.md index dc875dd66..cf7eb9173 100644 --- a/workspace/IDENTITY.md +++ b/workspace/IDENTITY.md @@ -20,7 +20,7 @@ Ultra-lightweight personal AI assistant written in Go, inspired by nanobot. - Web search and content fetching - File system operations (read, write, edit) - Shell command execution -- Multi-channel messaging (Telegram, WhatsApp, Feishu) +- Multi-channel messaging (Telegram, WhatsApp, Discord, Slack, LINE) - Skill-based extensibility - Memory and context management