Merge branch 'main' into version

This commit is contained in:
Cytown
2026-03-22 02:45:09 +08:00
213 changed files with 19651 additions and 2878 deletions
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
run: corepack enable && corepack prepare pnpm@latest --activate
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
@@ -79,7 +79,7 @@ jobs:
run: git tag "${{ steps.version.outputs.version }}"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: ~> v2
+2 -2
View File
@@ -74,7 +74,7 @@ jobs:
run: corepack enable && corepack prepare pnpm@latest --activate
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
@@ -94,7 +94,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: ~> v2
+13 -5
View File
@@ -18,7 +18,7 @@
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [English](README.md) | **Français**
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | **Français** | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)
</div>
@@ -105,6 +105,8 @@ _*Les versions récentes peuvent utiliser 1020 Mo en raison des fusions rapid
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
> 📋 **[Liste de Compatibilité Matérielle](docs/hardware-compatibility.md)** — Voir toutes les cartes testées, du RISC-V à $5 au Raspberry Pi en passant par les téléphones Android. Votre carte n'est pas listée ? Soumettez une PR !
## 🦾 Démonstration
### 🛠️ Flux de Travail Standard de l'Assistant
@@ -139,7 +141,7 @@ Donnez une seconde vie à votre téléphone d'il y a dix ans ! Transformez-le en
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
termux-chroot ./picoclaw onboard # chroot fournit une disposition standard du système de fichiers Linux
```
Puis suivez les instructions de la section « Démarrage Rapide » pour terminer la configuration !
@@ -160,11 +162,15 @@ PicoClaw peut être déployé sur pratiquement n'importe quel appareil Linux !
## 📦 Installation
### Installer avec un binaire précompilé
### Télécharger depuis picoclaw.io (Recommandé)
Téléchargez le binaire pour votre plateforme depuis la page des [Releases](https://github.com/sipeed/picoclaw/releases).
Visitez **[picoclaw.io](https://picoclaw.io)** — le site officiel détecte automatiquement votre plateforme et propose un téléchargement en un clic. Pas besoin de choisir manuellement une architecture.
### Installer depuis les sources (dernières fonctionnalités, recommandé pour le développement)
### Télécharger le binaire précompilé
Vous pouvez aussi télécharger le binaire pour votre plateforme depuis la page [GitHub Releases](https://github.com/sipeed/picoclaw/releases).
### Compiler depuis les sources (pour le développement)
```bash
git clone https://github.com/sipeed/picoclaw.git
@@ -200,6 +206,7 @@ Pour des guides détaillés, consultez la documentation ci-dessous. Ce README ne
| 🔄 [Spawn & Tâches Asynchrones](docs/fr/spawn-tasks.md) | Tâches rapides, tâches longues avec spawn, orchestration asynchrone de sous-agents |
| 🐛 [Dépannage](docs/fr/troubleshooting.md) | Problèmes courants et solutions |
| 🔧 [Configuration des Outils](docs/fr/tools_configuration.md) | Activation/désactivation par outil, politiques exec |
| 📋 [Compatibilité Matérielle](docs/hardware-compatibility.md) | Cartes testées, exigences minimales, comment ajouter votre carte |
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Rejoignez le Réseau Social d'Agents
@@ -225,6 +232,7 @@ Connectez PicoClaw au Réseau Social d'Agents simplement en envoyant un seul mes
| `picoclaw skills install` | Installer une compétence |
| `picoclaw migrate` | Migrer les données des anciennes versions |
| `picoclaw auth login` | S'authentifier auprès des fournisseurs |
| `picoclaw model` | Voir ou changer le modèle par défaut |
### Tâches Planifiées / Rappels
+249
View File
@@ -0,0 +1,249 @@
<div align="center">
<img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw: Asisten AI Super Ringan berbasis Go</h1>
<h3>Perangkat Keras $10 · RAM <10MB · Boot <1 Detik · Ayo, Berangkat!</h3>
<p>
<img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20MIPS%2C%20RISC--V%2C%20LoongArch-blue" alt="Hardware">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
<br>
<a href="https://picoclaw.io"><img src="https://img.shields.io/badge/Website-picoclaw.io-blue?style=flat&logo=google-chrome&logoColor=white" alt="Website"></a>
<a href="https://docs.picoclaw.io/"><img src="https://img.shields.io/badge/Docs-Official-007acc?style=flat&logo=read-the-docs&logoColor=white" alt="Docs"></a>
<a href="https://deepwiki.com/sipeed/picoclaw"><img src="https://img.shields.io/badge/Wiki-DeepWiki-FFA500?style=flat&logo=wikipedia&logoColor=white" alt="Wiki"></a>
<br>
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
<a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [English](README.md) | **Bahasa Indonesia**
</div>
---
> **PicoClaw** adalah proyek open-source independen yang diinisiasi oleh [Sipeed](https://sipeed.com). Ditulis sepenuhnya dalam **Go** — bukan fork dari OpenClaw, NanoBot, atau proyek lainnya.
🦐 PicoClaw adalah asisten AI pribadi yang super ringan, terinspirasi dari [NanoBot](https://github.com/HKUDS/nanobot), ditulis ulang sepenuhnya dalam Go melalui proses "self-bootstrapping" — di mana AI Agent itu sendiri yang memandu seluruh migrasi arsitektur dan optimasi kode.
⚡️ Berjalan di perangkat keras $10 dengan RAM <10MB: Hemat 99% memori dibanding OpenClaw dan 98% lebih murah dibanding Mac mini!
<table align="center">
<tr align="center">
<td align="center" valign="top">
<p align="center">
<img src="assets/picoclaw_mem.gif" width="360" height="240">
</p>
</td>
<td align="center" valign="top">
<p align="center">
<img src="assets/licheervnano.png" width="400" height="240">
</p>
</td>
</tr>
</table>
> [!CAUTION]
> **🚨 KEAMANAN & SALURAN RESMI**
>
> * **TANPA KRIPTO:** PicoClaw **TIDAK** memiliki token/koin resmi. Semua klaim di `pump.fun` atau platform trading lainnya adalah **PENIPUAN**.
>
> * **DOMAIN RESMI:** Satu-satunya website resmi adalah **[picoclaw.io](https://picoclaw.io)**, dan website perusahaan adalah **[sipeed.com](https://sipeed.com)**
> * **Peringatan:** Banyak domain `.ai/.org/.com/.net/...` yang didaftarkan oleh pihak ketiga.
> * **Peringatan:** PicoClaw masih dalam tahap pengembangan awal dan mungkin memiliki masalah keamanan jaringan yang belum teratasi. Jangan deploy ke lingkungan produksi sebelum rilis v1.0.
> * **Catatan:** PicoClaw baru-baru ini menggabungkan banyak PR, yang mungkin mengakibatkan penggunaan memori lebih besar (1020MB) pada versi terbaru. Kami berencana untuk memprioritaskan optimasi sumber daya segera setelah fitur saat ini mencapai kondisi stabil.
## 📢 Berita
2026-03-17 🚀 **v0.2.3 Dirilis!** UI system tray (Windows & Linux), pelacakan status sub-agent (`spawn_status`), eksperimental gateway hot-reload, gerbang keamanan cron, dan 2 perbaikan keamanan. PicoClaw kini di **25K ⭐**!
2026-03-09 🎉 **v0.2.1 — Update terbesar!** Dukungan protokol MCP, 4 channel baru (Matrix/IRC/WeCom/Discord Proxy), 3 provider baru (Kimi/Minimax/Avian), pipeline vision, penyimpanan memori JSONL, dan routing model.
2026-02-28 📦 **v0.2.0** dirilis dengan dukungan Docker Compose dan launcher Web UI.
2026-02-26 🎉 PicoClaw mencapai **20K bintang** hanya dalam 17 hari! Orkestrasi channel otomatis dan antarmuka kapabilitas diluncurkan.
<details>
<summary>Berita lama...</summary>
2026-02-16 🎉 PicoClaw mencapai 12K bintang dalam satu minggu! Peran maintainer komunitas dan [roadmap](ROADMAP.md) resmi diposting.
2026-02-13 🎉 PicoClaw mencapai 5000 bintang dalam 4 hari! Roadmap Proyek dan pengaturan Grup Pengembang sedang berjalan.
2026-02-09 🎉 **PicoClaw Diluncurkan!** Dibangun dalam 1 hari untuk menghadirkan AI Agent ke perangkat keras $10 dengan RAM <10MB. 🦐 PicoClaw, Ayo Berangkat!
</details>
## ✨ Fitur
🪶 **Super Ringan**: Penggunaan memori <10MB — 99% lebih kecil dari fungsionalitas inti OpenClaw.*
💰 **Biaya Minimal**: Cukup efisien untuk berjalan di perangkat keras $10 — 98% lebih murah dari Mac mini.
⚡️ **Secepat Kilat**: Waktu startup 400X lebih cepat, boot dalam <1 detik bahkan di prosesor single core 0,6GHz.
🌍 **Portabilitas Sejati**: Satu binary mandiri untuk RISC-V, ARM, MIPS, dan x86, Satu Klik Langsung Jalan!
🤖 **AI-Bootstrapped**: Implementasi Go-native secara otonom — 95% kode inti dihasilkan oleh Agent dengan penyempurnaan human-in-the-loop.
🔌 **Dukungan MCP**: Integrasi [Model Context Protocol](https://modelcontextprotocol.io/) native — hubungkan server MCP mana pun untuk memperluas kapabilitas agent.
👁️ **Pipeline Vision**: Kirim gambar dan file langsung ke agent — encoding base64 otomatis untuk LLM multimodal.
🧠 **Routing Cerdas**: Routing model berbasis aturan — kueri sederhana diarahkan ke model ringan, menghemat biaya API.
_*Versi terbaru mungkin menggunakan 1020MB karena penggabungan fitur yang cepat. Optimasi sumber daya direncanakan. Perbandingan startup berdasarkan benchmark prosesor single-core 0,8GHz (lihat tabel di bawah)._
| | OpenClaw | NanoBot | **PicoClaw** |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Bahasa** | TypeScript | Python | **Go** |
| **RAM** | >1GB | >100MB | **< 10MB*** |
| **Startup**</br>(0,8GHz core) | >500d | >30d | **<1d** |
| **Biaya** | Mac Mini $599 | Kebanyakan Linux SBC </br>~$50 | **Semua Board Linux**</br>**Mulai dari $10** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 Demonstrasi
### 🛠️ Alur Kerja Asisten Standar
<table align="center">
<tr align="center">
<th><p align="center">🧩 Full-Stack Engineer</p></th>
<th><p align="center">🗂️ Pencatatan & Manajemen Perencanaan</p></th>
<th><p align="center">🔎 Pencarian Web & Pembelajaran</p></th>
</tr>
<tr>
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
</tr>
<tr>
<td align="center">Develop • Deploy • Scale</td>
<td align="center">Jadwal • Otomasi • Memori</td>
<td align="center">Penemuan • Wawasan • Tren</td>
</tr>
</table>
### 📱 Jalankan di HP Android Lama
Berikan kehidupan kedua untuk HP lama Anda! Ubah menjadi Asisten AI pintar dengan PicoClaw. Panduan Cepat:
1. **Instal [Termux](https://github.com/termux/termux-app)** (Unduh dari [GitHub Releases](https://github.com/termux/termux-app/releases), atau cari di F-Droid / Google Play).
2. **Jalankan perintah**
```bash
# Unduh rilis terbaru dari https://github.com/sipeed/picoclaw/releases
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
```
Kemudian ikuti instruksi di bagian "Panduan Cepat" untuk menyelesaikan konfigurasi!
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
### 🐜 Deploy Inovatif dengan Footprint Rendah
PicoClaw dapat di-deploy di hampir semua perangkat Linux!
- $9,9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versi E(Ethernet) atau W(WiFi6), untuk Home Assistant Minimal
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), atau $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) untuk Pemeliharaan Server Otomatis
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) atau $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) untuk Pemantauan Cerdas
<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>
🌟 Lebih Banyak Kasus Deploy Menanti!
## 📦 Instalasi
### Instal dengan binary yang sudah dikompilasi
Unduh binary untuk platform Anda dari halaman [Releases](https://github.com/sipeed/picoclaw/releases).
### Instal dari source (fitur terbaru, disarankan untuk pengembangan)
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Build, tidak perlu instal
make build
# Build untuk berbagai platform
make build-all
# Build untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)
make build-pi-zero
# Build dan Instal
make install
```
**Raspberry Pi Zero 2 W:** Gunakan binary yang sesuai dengan OS Anda: Raspberry Pi OS 32-bit → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Atau jalankan `make build-pi-zero` untuk build keduanya.
## 📚 Dokumentasi
Untuk panduan lengkap, lihat dokumen di bawah. README ini hanya berisi panduan cepat.
| Topik | Deskripsi |
|-------|-----------|
| 🐳 [Docker & Panduan Cepat](docs/docker.md) | Pengaturan Docker Compose, mode Launcher/Agent, konfigurasi Panduan Cepat |
| 💬 [Aplikasi Chat](docs/chat-apps.md) | Telegram, Discord, WhatsApp, Matrix, QQ, Slack, IRC, DingTalk, LINE, Feishu, WeCom, dan lainnya |
| ⚙️ [Konfigurasi](docs/configuration.md) | Variabel environment, tata letak workspace, sumber skill, sandbox keamanan, heartbeat |
| 🔌 [Provider & Model](docs/providers.md) | 20+ provider LLM, routing model, konfigurasi model_list, arsitektur provider |
| 🔄 [Spawn & Tugas Async](docs/spawn-tasks.md) | Tugas cepat, tugas panjang dengan spawn, orkestrasi sub-agent async |
| 🐛 [Pemecahan Masalah](docs/troubleshooting.md) | Masalah umum dan solusinya |
| 🔧 [Konfigurasi Tools](docs/tools_configuration.md) | Aktifkan/nonaktifkan tool, kebijakan exec |
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Bergabung dengan Jaringan Sosial Agent
Hubungkan Picoclaw ke Jaringan Sosial Agent hanya dengan mengirim satu pesan melalui CLI atau Aplikasi Chat terintegrasi.
**Baca `https://clawdchat.ai/skill.md` dan ikuti instruksi untuk bergabung dengan [ClawdChat.ai](https://clawdchat.ai)**
## 🖥️ Referensi CLI
| Perintah | Deskripsi |
| ------------------------- | -------------------------------- |
| `picoclaw onboard` | Inisialisasi konfigurasi & workspace |
| `picoclaw agent -m "..."` | Chat dengan agent |
| `picoclaw agent` | Mode chat interaktif |
| `picoclaw gateway` | Mulai gateway |
| `picoclaw status` | Tampilkan status |
| `picoclaw version` | Tampilkan info versi |
| `picoclaw cron list` | Daftar semua tugas terjadwal |
| `picoclaw cron add ...` | Tambah tugas terjadwal |
| `picoclaw cron disable` | Nonaktifkan tugas terjadwal |
| `picoclaw cron remove` | Hapus tugas terjadwal |
| `picoclaw skills list` | Daftar skill yang terinstal |
| `picoclaw skills install` | Instal skill |
| `picoclaw migrate` | Migrasi data dari versi lama |
| `picoclaw auth login` | Autentikasi dengan provider |
### Tugas Terjadwal / Pengingat
PicoClaw mendukung pengingat terjadwal dan tugas berulang melalui tool `cron`:
* **Pengingat satu kali**: "Ingatkan saya dalam 10 menit" → terpicu sekali setelah 10 menit
* **Tugas berulang**: "Ingatkan saya setiap 2 jam" → terpicu setiap 2 jam
* **Ekspresi cron**: "Ingatkan saya jam 9 pagi setiap hari" → menggunakan ekspresi cron
## 🤝 Kontribusi & Roadmap
PR sangat diterima! Codebase sengaja dibuat kecil dan mudah dibaca. 🤗
Lihat [Roadmap Komunitas](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md) lengkap kami.
Grup pengembang sedang dibangun, bergabunglah setelah PR pertama Anda di-merge!
Grup Pengguna:
discord: <https://discord.gg/V4sAZ9XWpN>
<img src="assets/wechat.png" alt="PicoClaw" width="512">
+1 -1
View File
@@ -18,7 +18,7 @@
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [English](README.md) | **Italiano**
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | **Italiano** | [Bahasa Indonesia](README.id.md) | [English](README.md)
</div>
+13 -5
View File
@@ -18,7 +18,7 @@
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [English](README.md)
[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)
</div>
@@ -105,6 +105,8 @@ _*最近のバージョンでは急速な機能マージにより 10〜20MB に
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
> 📋 **[ハードウェア互換性リスト](docs/hardware-compatibility.md)** — テスト済みの全ボード一覧($5 RISC-V から Raspberry Pi、Android スマートフォンまで)。お使いのボードが未掲載?PR を送ってください!
## 🦾 デモンストレーション
### 🛠️ スタンダードアシスタントワークフロー
@@ -139,7 +141,7 @@ _*最近のバージョンでは急速な機能マージにより 10〜20MB に
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
termux-chroot ./picoclaw onboard # chroot で標準的な Linux ファイルシステムレイアウトを提供
```
その後「クイックスタート」セクションの手順に従って設定を完了してください!
@@ -160,11 +162,15 @@ PicoClaw はほぼすべての Linux デバイスにデプロイできます!
## 📦 インストール
### コンパイル済みバイナリでインストール
### picoclaw.io からダウンロード(推奨)
[リリースページ](https://github.com/sipeed/picoclaw/releases) からお使いのプラットフォーム用のバイナリをダウンロードしてください
**[picoclaw.io](https://picoclaw.io)** にアクセス — 公式サイトがプラットフォームを自動検出し、ワンクリックでダウンロードできます。アーキテクチャを手動で選ぶ必要はありません
### ソースからインストール(最新機能、開発向け推奨)
### プリコンパイル済みバイナリをダウンロード
または、[GitHub Releases](https://github.com/sipeed/picoclaw/releases) ページからプラットフォームに合ったバイナリをダウンロードしてください。
### ソースからビルド(開発用)
```bash
git clone https://github.com/sipeed/picoclaw.git
@@ -200,6 +206,7 @@ make install
| 🔄 [Spawn & 非同期タスク](docs/ja/spawn-tasks.md) | クイックタスク、spawn による長時間タスク、非同期サブエージェントオーケストレーション |
| 🐛 [トラブルシューティング](docs/ja/troubleshooting.md) | よくある問題と解決策 |
| 🔧 [ツール設定](docs/ja/tools_configuration.md) | ツールごとの有効/無効、exec ポリシー |
| 📋 [ハードウェア互換性](docs/hardware-compatibility.md) | テスト済みボード、最小要件、ボードの追加方法 |
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> エージェントソーシャルネットワークに参加
@@ -225,6 +232,7 @@ CLI または統合チャットアプリからメッセージを 1 つ送るだ
| `picoclaw skills install` | スキルをインストール |
| `picoclaw migrate` | 旧バージョンからデータを移行 |
| `picoclaw auth login` | プロバイダーへの認証 |
| `picoclaw model` | デフォルトモデルの表示・切替 |
### スケジュールタスク / リマインダー
+1304 -10
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -18,7 +18,7 @@
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
[中文](README.zh.md) | [日本語](README.ja.md) | **Português** | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [English](README.md)
[中文](README.zh.md) | [日本語](README.ja.md) | **Português** | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)
</div>
@@ -105,6 +105,8 @@ _*Versões recentes podem usar 1020MB devido a merges rápidos de funcionalid
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
> 📋 **[Lista de Compatibilidade de Hardware](docs/hardware-compatibility.md)** — Veja todas as placas testadas, de RISC-V de $5 a Raspberry Pi e telefones Android. Sua placa não está listada? Envie um PR!
## 🦾 Demonstração
### 🛠️ Fluxos de Trabalho Padrão do Assistente
@@ -139,7 +141,7 @@ Dê uma segunda vida ao seu celular de dez anos atrás! Transforme-o em um assis
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
termux-chroot ./picoclaw onboard # chroot fornece um layout padrão do sistema de arquivos Linux
```
Depois siga as instruções na seção "Início Rápido" para completar a configuração!
@@ -160,11 +162,15 @@ O PicoClaw pode ser implantado em praticamente qualquer dispositivo Linux!
## 📦 Instalação
### Instalar com binário pré-compilado
### Baixar de picoclaw.io (Recomendado)
Baixe o binário para sua plataforma na página de [Releases](https://github.com/sipeed/picoclaw/releases).
Visite **[picoclaw.io](https://picoclaw.io)** — o site oficial detecta automaticamente sua plataforma e oferece download com um clique. Sem necessidade de escolher manualmente a arquitetura.
### Instalar a partir do código-fonte (funcionalidades mais recentes, recomendado para desenvolvimento)
### Baixar binário pré-compilado
Alternativamente, baixe o binário para sua plataforma na página de [GitHub Releases](https://github.com/sipeed/picoclaw/releases).
### Compilar a partir do código-fonte (para desenvolvimento)
```bash
git clone https://github.com/sipeed/picoclaw.git
@@ -200,6 +206,7 @@ Para guias detalhados, consulte a documentação abaixo. Este README cobre apena
| 🔄 [Spawn & Tarefas Assíncronas](docs/pt-br/spawn-tasks.md) | Tarefas rápidas, tarefas longas com spawn, orquestração assíncrona de sub-agentes |
| 🐛 [Solução de Problemas](docs/pt-br/troubleshooting.md) | Problemas comuns e soluções |
| 🔧 [Configuração de Ferramentas](docs/pt-br/tools_configuration.md) | Habilitar/desabilitar por ferramenta, políticas de execução |
| 📋 [Compatibilidade de Hardware](docs/hardware-compatibility.md) | Placas testadas, requisitos mínimos, como adicionar sua placa |
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Junte-se à Rede Social de Agentes
@@ -225,6 +232,7 @@ Conecte o PicoClaw à Rede Social de Agentes simplesmente enviando uma única me
| `picoclaw skills install` | Instalar uma skill |
| `picoclaw migrate` | Migrar dados de versões anteriores |
| `picoclaw auth login` | Autenticar com provedores |
| `picoclaw model` | Ver ou trocar o modelo padrão |
### Tarefas Agendadas / Lembretes
+13 -5
View File
@@ -18,7 +18,7 @@
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | **Tiếng Việt** | [Français](README.fr.md) | [English](README.md)
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | **Tiếng Việt** | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)
</div>
@@ -105,6 +105,8 @@ _*Các phiên bản gần đây có thể sử dụng 1020MB do merge tính n
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
> 📋 **[Danh Sách Tương Thích Phần Cứng](docs/hardware-compatibility.md)** — Xem tất cả các board đã được kiểm tra, từ RISC-V $5 đến Raspberry Pi và điện thoại Android. Board của bạn chưa có? Gửi PR!
## 🦾 Demo
### 🛠️ Quy trình trợ lý tiêu chuẩn
@@ -139,7 +141,7 @@ Hãy cho chiếc điện thoại cũ một cuộc sống mới! Biến nó thàn
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
termux-chroot ./picoclaw onboard # chroot cung cấp bố cục hệ thống tệp Linux tiêu chuẩn
```
Sau đó làm theo hướng dẫn trong phần "Bắt đầu nhanh" để hoàn tất cấu hình!
@@ -160,11 +162,15 @@ PicoClaw có thể triển khai trên hầu hết mọi thiết bị Linux!
## 📦 Cài đặt
### Cài đặt bằng binary biên dịch sẵn
### Tải từ picoclaw.io (Khuyến nghị)
Tải file binary cho nền tảng của bạn từ [trang Releases](https://github.com/sipeed/picoclaw/releases).
Truy cập **[picoclaw.io](https://picoclaw.io)** — trang web chính thức tự động phát hiện nền tảng của bạn và cung cấp tải xuống một cú nhấp. Không cần chọn kiến trúc thủ công.
### Cài đặt từ mã nguồn (có tính năng mới nhất, khuyên dùng cho phát triển)
### Tải binary đã biên dịch sẵn
Hoặc tải binary cho nền tảng của bạn từ trang [GitHub Releases](https://github.com/sipeed/picoclaw/releases).
### Biên dịch từ mã nguồn (cho phát triển)
```bash
git clone https://github.com/sipeed/picoclaw.git
@@ -200,6 +206,7 @@ make install
| 🔄 [Spawn & Tác vụ bất đồng bộ](docs/vi/spawn-tasks.md) | Tác vụ nhanh, tác vụ dài với spawn, điều phối sub-agent bất đồng bộ |
| 🐛 [Xử lý sự cố](docs/vi/troubleshooting.md) | Các vấn đề thường gặp và giải pháp |
| 🔧 [Cấu hình Công cụ](docs/vi/tools_configuration.md) | Bật/tắt từng công cụ, chính sách thực thi |
| 📋 [Tương Thích Phần Cứng](docs/hardware-compatibility.md) | Các board đã kiểm tra, yêu cầu tối thiểu, cách thêm board |
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Tham gia Mạng xã hội Agent
@@ -225,6 +232,7 @@ Kết nối PicoClaw với Mạng xã hội Agent chỉ bằng cách gửi một
| `picoclaw skills install` | Cài đặt một skill |
| `picoclaw migrate` | Di chuyển dữ liệu từ phiên bản cũ |
| `picoclaw auth login` | Xác thực với nhà cung cấp |
| `picoclaw model` | Xem hoặc chuyển đổi model mặc định |
### Tác vụ định kỳ / Nhắc nhở
+13 -5
View File
@@ -18,7 +18,7 @@
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
</p>
**中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [English](README.md)
**中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [English](README.md)
</div>
@@ -104,6 +104,8 @@ _*近期版本因快速合并 PR 可能占用 10–20MB,资源优化已列入
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
> 📋 **[硬件兼容列表](docs/hardware-compatibility.md)** — 查看所有已测试的板卡,从 $5 RISC-V 到树莓派到安卓手机。你的板卡没在列表中?欢迎提交 PR!
## 🦾 演示
### 🛠️ 标准助手工作流
@@ -138,7 +140,7 @@ PicoClaw 可以将你 10 年前的老旧手机废物利用,变身成为你的
wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz
tar xzf picoclaw_Linux_arm64.tar.gz
pkg install proot
termux-chroot ./picoclaw onboard
termux-chroot ./picoclaw onboard # chroot 提供标准 Linux 文件系统布局
```
然后跟随下面的"快速开始"章节继续配置 PicoClaw 即可使用!
@@ -159,11 +161,15 @@ PicoClaw 几乎可以部署在任何 Linux 设备上!
## 📦 安装
### 使用预编译二进制文件安装
### 从 picoclaw.io 下载(推荐)
从 [Release 页面](https://github.com/sipeed/picoclaw/releases) 下载适用于您平台的二进制文件
访问 **[picoclaw.io](https://picoclaw.io)** — 官网自动检测你的平台,提供一键下载,无需手动选择架构
### 从源码安装(获取最新特性,开发推荐)
### 下载预编译二进制文件
也可以从 [GitHub Releases](https://github.com/sipeed/picoclaw/releases) 页面手动下载对应平台的二进制文件。
### 从源码构建(开发用)
```bash
git clone https://github.com/sipeed/picoclaw.git
@@ -199,6 +205,7 @@ make install
| 🔄 [异步任务与 Spawn](docs/zh/spawn-tasks.md) | 快速任务、长任务与 Spawn、异步子 Agent 编排 |
| 🐛 [疑难解答](docs/zh/troubleshooting.md) | 常见问题与解决方案 |
| 🔧 [工具配置](docs/zh/tools_configuration.md) | 工具启用/禁用、执行策略 |
| 📋 [硬件兼容列表](docs/hardware-compatibility.md) | 已测试板卡、最低要求、如何添加你的板卡 |
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络
@@ -224,6 +231,7 @@ make install
| `picoclaw skills install` | 安装技能 |
| `picoclaw migrate` | 从旧版本迁移数据 |
| `picoclaw auth login` | 认证提供商 |
| `picoclaw model` | 查看或切换默认模型 |
### 定时任务 / 提醒
+236
View File
@@ -0,0 +1,236 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
// Package config provides types and I/O for ~/.picoclaw/tui.toml.
package config
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/sipeed/picoclaw/pkg/fileutil"
)
// DefaultConfigPath returns the default path to the tui.toml config file.
func DefaultConfigPath() string {
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
return filepath.Join(home, ".picoclaw", "tui.toml")
}
// TUIConfig is the top-level structure of ~/.picoclaw/tui.toml.
type TUIConfig struct {
Version string `toml:"version"`
Model Model `toml:"model"`
Provider Provider `toml:"provider"`
}
type Model struct {
Type string `toml:"type"` // "provider" (default) | "manual"
}
type Provider struct {
Schemes []Scheme `toml:"schemes"`
Users []User `toml:"users"`
Current ProviderCurrent `toml:"current"`
}
type Scheme struct {
Name string `toml:"name"` // unique key
BaseURL string `toml:"baseURL"` // required
Type string `toml:"type"` // "openai-compatible" (default) | "anthropic"
}
type User struct {
Name string `toml:"name"`
Scheme string `toml:"scheme"` // references Scheme.Name; (Name+Scheme) is unique
Type string `toml:"type"` // "key" (default) | "OAuth"
Key string `toml:"key"`
}
type ProviderCurrent struct {
Scheme string `toml:"scheme"` // references Scheme.Name
User string `toml:"user"` // references User.Name where User.Scheme == Scheme
Model string `toml:"model"` // from GET <baseURL>/models
}
// DefaultConfig returns a minimal valid TUIConfig.
func DefaultConfig() *TUIConfig {
return &TUIConfig{
Version: "1.0",
Model: Model{Type: "provider"},
Provider: Provider{
Schemes: []Scheme{},
Users: []User{},
Current: ProviderCurrent{},
},
}
}
// Load reads the TUI config from path. Returns a default config if the file does not exist.
func Load(path string) (*TUIConfig, error) {
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return DefaultConfig(), nil
}
if err != nil {
return nil, fmt.Errorf("failed to read config file %q: %w", path, err)
}
cfg := DefaultConfig()
if _, err := toml.Decode(string(data), cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file %q: %w", path, err)
}
applyDefaults(cfg)
return cfg, nil
}
// Save writes cfg to path atomically (safe for flash / SD storage).
func Save(path string, cfg *TUIConfig) error {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
var buf bytes.Buffer
enc := toml.NewEncoder(&buf)
if err := enc.Encode(cfg); err != nil {
return fmt.Errorf("failed to encode config: %w", err)
}
if err := fileutil.WriteFileAtomic(path, buf.Bytes(), 0o600); err != nil {
return fmt.Errorf("failed to write config file %q: %w", path, err)
}
return nil
}
func applyDefaults(cfg *TUIConfig) {
if cfg.Version == "" {
cfg.Version = "1.0"
}
if cfg.Model.Type == "" {
cfg.Model.Type = "provider"
}
for i := range cfg.Provider.Schemes {
if cfg.Provider.Schemes[i].Type == "" {
cfg.Provider.Schemes[i].Type = "openai-compatible"
}
}
for i := range cfg.Provider.Users {
if cfg.Provider.Users[i].Type == "" {
cfg.Provider.Users[i].Type = "key"
}
}
}
// SchemeByName returns the first Scheme whose Name matches, or nil.
func (p *Provider) SchemeByName(name string) *Scheme {
for i := range p.Schemes {
if p.Schemes[i].Name == name {
return &p.Schemes[i]
}
}
return nil
}
// UsersForScheme returns all users whose Scheme field matches schemeName.
func (p *Provider) UsersForScheme(schemeName string) []User {
var out []User
for _, u := range p.Users {
if u.Scheme == schemeName {
out = append(out, u)
}
}
return out
}
// SyncSelectedModelToMainConfig syncs the currently selected model to ~/.picoclaw/config.json
// Adds/replaces a "tui-prefer" model entry and sets it as the default model.
// Preserves all other existing fields in the config file unchanged.
func SyncSelectedModelToMainConfig(scheme Scheme, user User, modelID string) error {
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
mainConfigPath := filepath.Join(home, ".picoclaw", "config.json")
var cfg map[string]any
if data, readErr := os.ReadFile(mainConfigPath); readErr == nil {
if unmarshalErr := json.Unmarshal(data, &cfg); unmarshalErr != nil {
cfg = make(map[string]any)
}
} else {
cfg = make(map[string]any)
}
if _, ok := cfg["agents"]; !ok {
cfg["agents"] = make(map[string]any)
}
agents, ok := cfg["agents"].(map[string]any)
if ok {
if _, ok := agents["defaults"]; !ok {
agents["defaults"] = make(map[string]any)
}
defaults, ok := agents["defaults"].(map[string]any)
if ok {
defaults["model"] = "tui-prefer"
}
}
tuiModel := map[string]any{
"model_name": "tui-prefer",
"model": modelID,
"api_key": user.Key,
"api_base": scheme.BaseURL,
}
modelList := []any{}
if ml, ok := cfg["model_list"].([]any); ok {
modelList = ml
}
found := false
for i, m := range modelList {
if entry, ok := m.(map[string]any); ok {
if name, ok := entry["model_name"].(string); ok && name == "tui-prefer" {
modelList[i] = tuiModel
found = true
break
}
}
}
if !found {
modelList = append(modelList, tuiModel)
}
cfg["model_list"] = modelList
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(mainConfigPath), 0o700); err != nil {
return err
}
return os.WriteFile(mainConfigPath, data, 0o600)
}
func (cfg *TUIConfig) CurrentModelLabel() string {
cur := cfg.Provider.Current
if cur.Model == "" {
return "(not configured)"
}
label := cur.Scheme
if label != "" {
label += " / "
}
return label + cur.Model
}
@@ -1,49 +0,0 @@
package configstore
import (
"errors"
"os"
"path/filepath"
picoclawconfig "github.com/sipeed/picoclaw/pkg/config"
)
const (
configDirName = ".picoclaw"
configFileName = "config.json"
)
func ConfigPath() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, configFileName), nil
}
func ConfigDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, configDirName), nil
}
func Load() (*picoclawconfig.Config, error) {
path, err := ConfigPath()
if err != nil {
return nil, err
}
return picoclawconfig.LoadConfig(path)
}
func Save(cfg *picoclawconfig.Config) error {
if cfg == nil {
return errors.New("config is nil")
}
path, err := ConfigPath()
if err != nil {
return err
}
return picoclawconfig.SaveConfig(path, cfg)
}
@@ -1,522 +0,0 @@
package ui
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
configstore "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/internal/config"
picoclawconfig "github.com/sipeed/picoclaw/pkg/config"
)
type appState struct {
app *tview.Application
pages *tview.Pages
stack []string
config *picoclawconfig.Config
configPath string
gatewayCmd *exec.Cmd
menus map[string]*Menu
original []byte
hasOriginal bool
backupPath string
dirty bool
logPath string
}
func Run() error {
applyStyles()
cfg, err := configstore.Load()
if err != nil {
return err
}
path, err := configstore.ConfigPath()
if err != nil {
return err
}
if cfg == nil {
cfg = picoclawconfig.DefaultConfig()
}
originalData, hasOriginal := loadOriginalConfig(path)
backupPath := path + ".bak"
if hasOriginal {
_ = writeBackupConfig(backupPath, originalData)
}
logPath := filepath.Join(filepath.Dir(path), "gateway.log")
state := &appState{
app: tview.NewApplication(),
pages: tview.NewPages(),
config: cfg,
configPath: path,
menus: map[string]*Menu{},
original: originalData,
hasOriginal: hasOriginal,
backupPath: backupPath,
logPath: logPath,
}
state.push("main", state.mainMenu())
root := tview.NewFlex().SetDirection(tview.FlexRow)
root.AddItem(bannerView(), 6, 0, false)
root.AddItem(state.pages, 0, 1, true)
root.AddItem(footerView(), 1, 0, false)
if err := state.app.SetRoot(root, true).EnableMouse(false).Run(); err != nil {
return err
}
return nil
}
func (s *appState) push(name string, primitive tview.Primitive) {
s.pages.AddPage(name, primitive, true, true)
s.stack = append(s.stack, name)
s.pages.SwitchToPage(name)
if menu, ok := primitive.(*Menu); ok {
s.menus[name] = menu
}
}
func (s *appState) pop() {
if len(s.stack) == 0 {
return
}
last := s.stack[len(s.stack)-1]
s.pages.RemovePage(last)
s.stack = s.stack[:len(s.stack)-1]
if len(s.stack) == 0 {
s.app.Stop()
return
}
current := s.stack[len(s.stack)-1]
s.pages.SwitchToPage(current)
if menu, ok := s.menus[current]; ok {
s.refreshMenu(current, menu)
}
}
func (s *appState) mainMenu() tview.Primitive {
menu := NewMenu("Menu", nil)
refreshMainMenu(menu, s)
menu.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyEsc:
s.requestExit()
return nil
}
return event
})
return menu
}
func (s *appState) refreshMenu(name string, menu *Menu) {
switch name {
case "main":
refreshMainMenu(menu, s)
case "model":
refreshModelMenuFromState(menu, s)
case "channel":
refreshChannelMenuFromState(menu, s)
}
}
func (s *appState) countChannels() (enabled int, total int) {
c := s.config.Channels
entries := []bool{
c.Telegram.Enabled,
c.Discord.Enabled,
c.QQ.Enabled,
c.MaixCam.Enabled,
c.WhatsApp.Enabled,
c.Feishu.Enabled,
c.DingTalk.Enabled,
c.Slack.Enabled,
c.Matrix.Enabled,
c.LINE.Enabled,
c.OneBot.Enabled,
c.WeCom.Enabled,
c.WeComApp.Enabled,
}
total = len(entries)
for _, v := range entries {
if v {
enabled++
}
}
return enabled, total
}
func refreshMainMenuIfPresent(s *appState) {
if menu, ok := s.menus["main"]; ok {
refreshMainMenu(menu, s)
}
}
func refreshMainMenu(menu *Menu, s *appState) {
selectedModel := s.selectedModelName()
modelReady := selectedModel != ""
channelReady := s.hasEnabledChannel()
enabledCount, totalChannels := s.countChannels()
gatewayRunning := s.gatewayCmd != nil || s.isGatewayRunning()
gatewayLabel := "Start Gateway"
gatewayDescription := "Launch gateway for channels"
if gatewayRunning {
gatewayLabel = "Stop Gateway"
gatewayDescription = "Gateway running"
}
items := []MenuItem{
{
Label: rootModelLabel(selectedModel),
Description: rootModelDescription(),
Action: func() {
s.push("model", s.modelMenu())
},
MainColor: func() *tcell.Color {
if modelReady {
return nil
}
color := tcell.ColorGray
return &color
}(),
},
{
Label: rootChannelLabel(channelReady),
Description: fmt.Sprintf("%d/%d enabled", enabledCount, totalChannels),
Action: func() {
s.push("channel", s.channelMenu())
},
MainColor: func() *tcell.Color {
if channelReady {
return nil
}
color := tcell.ColorGray
return &color
}(),
},
{
Label: "Start Talk",
Description: "Open picoclaw agent in terminal",
Action: func() {
s.requestStartTalk()
},
Disabled: !modelReady,
},
{
Label: gatewayLabel,
Description: gatewayDescription,
Action: func() {
if gatewayRunning {
s.stopGateway()
} else {
s.requestStartGateway()
}
refreshMainMenu(menu, s)
},
Disabled: !gatewayRunning && (!modelReady || !channelReady),
},
{
Label: "View Gateway Log",
Description: "Open gateway.log",
Action: func() {
s.viewGatewayLog()
},
},
{
Label: "Exit",
Description: "Exit the TUI",
Action: func() {
s.requestExit()
},
},
}
menu.applyItems(items)
}
func (s *appState) applyChangesValidated() bool {
if err := s.config.ValidateModelList(); err != nil {
s.showMessage("Validation failed", err.Error())
return false
}
if err := s.validateAgentModel(); err != nil {
s.showMessage("Validation failed", err.Error())
return false
}
if err := configstore.Save(s.config); err != nil {
s.showMessage("Save failed", err.Error())
return false
}
if data, err := os.ReadFile(s.configPath); err == nil {
s.original = data
s.hasOriginal = true
_ = writeBackupConfig(s.backupPath, data)
}
return true
}
func (s *appState) requestExit() {
if s.dirty {
s.confirmApplyOrDiscard(func() {
s.app.Stop()
}, func() {
s.discardChanges()
s.app.Stop()
})
return
}
s.app.Stop()
}
func (s *appState) requestStartTalk() {
if s.dirty {
s.confirmApplyOrDiscard(func() {
s.startTalk()
}, func() {
s.startTalk()
})
return
}
s.startTalk()
}
func (s *appState) requestStartGateway() {
if s.dirty {
s.confirmApplyOrDiscard(func() {
s.startGateway()
}, func() {
s.startGateway()
})
return
}
s.startGateway()
}
func (s *appState) viewGatewayLog() {
data, err := os.ReadFile(s.logPath)
if err != nil {
s.showMessage("Log not found", "gateway.log not found")
return
}
text := tview.NewTextView()
text.SetBorder(true).SetTitle("Gateway Log")
text.SetText(string(data))
text.SetDoneFunc(func(key tcell.Key) {
s.pages.RemovePage("log")
})
text.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEsc {
s.pages.RemovePage("log")
return nil
}
return event
})
s.pages.AddPage("log", text, true, true)
}
func (s *appState) selectedModelName() string {
modelName := strings.TrimSpace(s.config.Agents.Defaults.GetModelName())
if modelName == "" {
return ""
}
if !s.isActiveModelValid() {
return ""
}
return modelName
}
func rootModelLabel(selected string) string {
if selected == "" {
return "Model (None)"
}
return "Model (" + selected + ")"
}
func rootModelDescription() string {
return "Using SPACE to choose your model"
}
func rootChannelLabel(valid bool) string {
if !valid {
return "Channel (no channel enabled)"
}
return "Channel"
}
func (s *appState) startTalk() {
if !s.isActiveModelValid() {
s.showMessage("Model required", "Select a valid model before starting talk")
return
}
if !s.applyChangesValidated() {
return
}
s.app.Suspend(func() {
cmd := exec.Command("picoclaw", "agent")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
})
}
func (s *appState) startGateway() {
if !s.isActiveModelValid() {
s.showMessage("Model required", "Select a valid model before starting gateway")
return
}
if !s.hasEnabledChannel() {
s.showMessage("Channel required", "Enable at least one channel before starting gateway")
return
}
if !s.applyChangesValidated() {
return
}
_ = stopGatewayProcess()
cmd := exec.Command("picoclaw", "gateway")
logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
s.showMessage("Gateway failed", err.Error())
return
}
cmd.Stdout = logFile
cmd.Stderr = logFile
if err := cmd.Start(); err != nil {
s.showMessage("Gateway failed", err.Error())
_ = logFile.Close()
return
}
_ = logFile.Close()
s.gatewayCmd = cmd
}
func (s *appState) stopGateway() {
_ = stopGatewayProcess()
if s.gatewayCmd != nil && s.gatewayCmd.Process != nil {
_ = s.gatewayCmd.Process.Kill()
}
s.gatewayCmd = nil
}
func (s *appState) isGatewayRunning() bool {
return isGatewayProcessRunning()
}
func (s *appState) validateAgentModel() error {
modelName := strings.TrimSpace(s.config.Agents.Defaults.GetModelName())
if modelName == "" {
return nil
}
_, err := s.config.GetModelConfig(modelName)
return err
}
func (s *appState) isActiveModelValid() bool {
modelName := strings.TrimSpace(s.config.Agents.Defaults.GetModelName())
if modelName == "" {
return false
}
cfg, err := s.config.GetModelConfig(modelName)
if err != nil {
return false
}
hasKey := strings.TrimSpace(cfg.APIKey()) != "" || strings.TrimSpace(cfg.AuthMethod) == "oauth"
hasModel := strings.TrimSpace(cfg.Model) != ""
return hasKey && hasModel
}
func (s *appState) hasEnabledChannel() bool {
c := s.config.Channels
return c.Telegram.Enabled || c.Discord.Enabled || c.QQ.Enabled || c.MaixCam.Enabled ||
c.WhatsApp.Enabled || c.Feishu.Enabled || c.DingTalk.Enabled || c.Slack.Enabled ||
c.Matrix.Enabled || c.LINE.Enabled || c.OneBot.Enabled || c.WeCom.Enabled || c.WeComApp.Enabled
}
func (s *appState) confirmApplyOrDiscard(onApply func(), onDiscard func()) {
if s.pages.HasPage("apply") {
return
}
modal := tview.NewModal().
SetText("Apply changes or discard before continuing?").
AddButtons([]string{"Cancel", "Discard", "Apply"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
s.pages.RemovePage("apply")
switch buttonLabel {
case "Discard":
s.discardChanges()
if onDiscard != nil {
onDiscard()
}
case "Apply":
if s.applyChangesValidated() {
s.dirty = false
if onApply != nil {
onApply()
}
}
}
})
modal.SetBorder(true)
s.pages.AddPage("apply", modal, true, true)
}
func (s *appState) discardChanges() {
if s.hasOriginal {
_ = writeOriginalConfig(s.configPath, s.original)
} else {
_ = os.Remove(s.configPath)
}
_ = os.Remove(s.backupPath)
if cfg, err := configstore.Load(); err == nil && cfg != nil {
s.config = cfg
}
s.dirty = false
refreshMainMenuIfPresent(s)
}
func (s *appState) showMessage(title, message string) {
if s.pages.HasPage("message") {
return
}
modal := tview.NewModal().
SetText(strings.TrimSpace(message)).
AddButtons([]string{"OK"}).
SetDoneFunc(func(_ int, _ string) {
s.pages.RemovePage("message")
})
modal.SetTitle(title).SetBorder(true)
modal.SetBackgroundColor(tview.Styles.ContrastBackgroundColor)
modal.SetTextColor(tview.Styles.PrimaryTextColor)
modal.SetButtonBackgroundColor(tcell.NewRGBColor(112, 102, 255))
modal.SetButtonTextColor(tview.Styles.PrimaryTextColor)
s.pages.AddPage("message", modal, true, true)
}
func loadOriginalConfig(path string) ([]byte, bool) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, false
}
return nil, false
}
return data, true
}
func writeOriginalConfig(path string, data []byte) error {
return os.WriteFile(path, data, 0o600)
}
func writeBackupConfig(path string, data []byte) error {
return os.WriteFile(path, data, 0o600)
}
@@ -1,433 +0,0 @@
package ui
import (
"fmt"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
picoclawconfig "github.com/sipeed/picoclaw/pkg/config"
)
func (s *appState) buildChannelMenuItems() []MenuItem {
return []MenuItem{
channelItem(
"Telegram",
"Telegram bot settings",
s.config.Channels.Telegram.Enabled,
func() { s.push("channel-telegram", s.telegramForm()) },
),
channelItem(
"Discord",
"Discord bot settings",
s.config.Channels.Discord.Enabled,
func() { s.push("channel-discord", s.discordForm()) },
),
channelItem(
"QQ",
"QQ bot settings",
s.config.Channels.QQ.Enabled,
func() { s.push("channel-qq", s.qqForm()) },
),
channelItem(
"MaixCam",
"MaixCam gateway",
s.config.Channels.MaixCam.Enabled,
func() { s.push("channel-maixcam", s.maixcamForm()) },
),
channelItem(
"WhatsApp",
"WhatsApp bridge",
s.config.Channels.WhatsApp.Enabled,
func() { s.push("channel-whatsapp", s.whatsappForm()) },
),
channelItem(
"Feishu",
"Feishu bot settings",
s.config.Channels.Feishu.Enabled,
func() { s.push("channel-feishu", s.feishuForm()) },
),
channelItem(
"DingTalk",
"DingTalk bot settings",
s.config.Channels.DingTalk.Enabled,
func() { s.push("channel-dingtalk", s.dingtalkForm()) },
),
channelItem(
"Slack",
"Slack bot settings",
s.config.Channels.Slack.Enabled,
func() { s.push("channel-slack", s.slackForm()) },
),
channelItem(
"Matrix",
"Matrix bot settings",
s.config.Channels.Matrix.Enabled,
func() { s.push("channel-matrix", s.matrixForm()) },
),
channelItem(
"LINE",
"LINE bot settings",
s.config.Channels.LINE.Enabled,
func() { s.push("channel-line", s.lineForm()) },
),
channelItem(
"OneBot",
"OneBot settings",
s.config.Channels.OneBot.Enabled,
func() { s.push("channel-onebot", s.onebotForm()) },
),
channelItem(
"WeCom",
"WeCom bot settings",
s.config.Channels.WeCom.Enabled,
func() { s.push("channel-wecom", s.wecomForm()) },
),
channelItem(
"WeCom App",
"WeCom App settings",
s.config.Channels.WeComApp.Enabled,
func() { s.push("channel-wecomapp", s.wecomAppForm()) },
),
}
}
func (s *appState) channelMenu() tview.Primitive {
menu := NewMenu("Channels", s.buildChannelMenuItems())
menu.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEsc {
s.pop()
return nil
}
return event
})
return menu
}
func refreshChannelMenuFromState(menu *Menu, s *appState) {
menu.applyItems(s.buildChannelMenuItems())
}
func (s *appState) telegramForm() tview.Primitive {
cfg := &s.config.Channels.Telegram
form := baseChannelForm("Telegram", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Token", cfg.Token(), 128, nil, func(text string) {
cfg.SetToken(strings.TrimSpace(text))
})
form.AddInputField("Proxy", cfg.Proxy, 128, nil, func(text string) {
cfg.Proxy = strings.TrimSpace(text)
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) discordForm() tview.Primitive {
cfg := &s.config.Channels.Discord
form := baseChannelForm("Discord", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Token", cfg.Token(), 128, nil, func(text string) {
cfg.SetToken(strings.TrimSpace(text))
})
form.AddCheckbox("Mention Only", cfg.MentionOnly, func(checked bool) {
cfg.MentionOnly = checked
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) qqForm() tview.Primitive {
cfg := &s.config.Channels.QQ
form := baseChannelForm("QQ", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("App ID", cfg.AppID, 64, nil, func(text string) {
cfg.AppID = strings.TrimSpace(text)
})
form.AddInputField("App Secret", cfg.AppSecret(), 128, nil, func(text string) {
cfg.SetAppSecret(strings.TrimSpace(text))
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) maixcamForm() tview.Primitive {
cfg := &s.config.Channels.MaixCam
form := baseChannelForm("MaixCam", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Host", cfg.Host, 64, nil, func(text string) {
cfg.Host = strings.TrimSpace(text)
})
addIntField(form, "Port", cfg.Port, func(value int) { cfg.Port = value })
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) whatsappForm() tview.Primitive {
cfg := &s.config.Channels.WhatsApp
form := baseChannelForm("WhatsApp", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Bridge URL", cfg.BridgeURL, 128, nil, func(text string) {
cfg.BridgeURL = strings.TrimSpace(text)
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) feishuForm() tview.Primitive {
cfg := &s.config.Channels.Feishu
form := baseChannelForm("Feishu", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("App ID", cfg.AppID, 64, nil, func(text string) {
cfg.AppID = strings.TrimSpace(text)
})
form.AddInputField("App Secret", cfg.AppSecret(), 128, nil, func(text string) {
cfg.SetAppSecret(strings.TrimSpace(text))
})
form.AddInputField("Encrypt Key", cfg.EncryptKey(), 128, nil, func(text string) {
cfg.SetEncryptKey(strings.TrimSpace(text))
})
form.AddInputField("Verification Token", cfg.VerificationToken(), 128, nil, func(text string) {
cfg.SetVerificationToken(strings.TrimSpace(text))
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) dingtalkForm() tview.Primitive {
cfg := &s.config.Channels.DingTalk
form := baseChannelForm("DingTalk", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Client ID", cfg.ClientID, 64, nil, func(text string) {
cfg.ClientID = strings.TrimSpace(text)
})
form.AddInputField("Client Secret", cfg.ClientSecret(), 128, nil, func(text string) {
cfg.SetClientSecret(strings.TrimSpace(text))
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) slackForm() tview.Primitive {
cfg := &s.config.Channels.Slack
form := baseChannelForm("Slack", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Bot Token", cfg.BotToken(), 128, nil, func(text string) {
cfg.SetBotToken(strings.TrimSpace(text))
})
form.AddInputField("App Token", cfg.AppToken(), 128, nil, func(text string) {
cfg.SetAppToken(strings.TrimSpace(text))
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) lineForm() tview.Primitive {
cfg := &s.config.Channels.LINE
form := baseChannelForm("LINE", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Channel Secret", cfg.ChannelSecret(), 128, nil, func(text string) {
cfg.SetChannelSecret(strings.TrimSpace(text))
})
form.AddInputField("Channel Access Token", cfg.ChannelAccessToken(), 128, nil, func(text string) {
cfg.SetChannelAccessToken(strings.TrimSpace(text))
})
form.AddInputField("Webhook Host", cfg.WebhookHost, 64, nil, func(text string) {
cfg.WebhookHost = strings.TrimSpace(text)
})
addIntField(form, "Webhook Port", cfg.WebhookPort, func(value int) { cfg.WebhookPort = value })
form.AddInputField("Webhook Path", cfg.WebhookPath, 64, nil, func(text string) {
cfg.WebhookPath = strings.TrimSpace(text)
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) matrixForm() tview.Primitive {
cfg := &s.config.Channels.Matrix
form := baseChannelForm("Matrix", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Homeserver", cfg.Homeserver, 128, nil, func(text string) {
cfg.Homeserver = strings.TrimSpace(text)
})
form.AddInputField("User ID", cfg.UserID, 128, nil, func(text string) {
cfg.UserID = strings.TrimSpace(text)
})
form.AddInputField("Access Token", cfg.AccessToken(), 128, nil, func(text string) {
cfg.SetAccessToken(strings.TrimSpace(text))
})
form.AddInputField("Device ID", cfg.DeviceID, 128, nil, func(text string) {
cfg.DeviceID = strings.TrimSpace(text)
})
form.AddCheckbox("Join On Invite", cfg.JoinOnInvite, func(checked bool) {
cfg.JoinOnInvite = checked
})
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) onebotForm() tview.Primitive {
cfg := &s.config.Channels.OneBot
form := baseChannelForm("OneBot", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("WS URL", cfg.WSUrl, 128, nil, func(text string) {
cfg.WSUrl = strings.TrimSpace(text)
})
form.AddInputField("Access Token", cfg.AccessToken(), 128, nil, func(text string) {
cfg.SetAccessToken(strings.TrimSpace(text))
})
addIntField(
form,
"Reconnect Interval",
cfg.ReconnectInterval,
func(value int) { cfg.ReconnectInterval = value },
)
form.AddInputField(
"Group Trigger Prefix",
strings.Join(cfg.GroupTriggerPrefix, ","),
128,
nil,
func(text string) {
cfg.GroupTriggerPrefix = splitCSV(text)
},
)
addAllowFromField(form, &cfg.AllowFrom)
return wrapWithBack(form, s)
}
func (s *appState) wecomForm() tview.Primitive {
cfg := &s.config.Channels.WeCom
form := baseChannelForm("WeCom", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Token", cfg.Token(), 128, nil, func(text string) {
cfg.SetToken(strings.TrimSpace(text))
})
form.AddInputField("Encoding AES Key", cfg.EncodingAESKey(), 128, nil, func(text string) {
cfg.SetEncodingAESKey(strings.TrimSpace(text))
})
form.AddInputField("Webhook URL", cfg.WebhookURL, 128, nil, func(text string) {
cfg.WebhookURL = strings.TrimSpace(text)
})
form.AddInputField("Webhook Host", cfg.WebhookHost, 64, nil, func(text string) {
cfg.WebhookHost = strings.TrimSpace(text)
})
addIntField(form, "Webhook Port", cfg.WebhookPort, func(value int) { cfg.WebhookPort = value })
form.AddInputField("Webhook Path", cfg.WebhookPath, 64, nil, func(text string) {
cfg.WebhookPath = strings.TrimSpace(text)
})
addAllowFromField(form, &cfg.AllowFrom)
addIntField(
form,
"Reply Timeout",
cfg.ReplyTimeout,
func(value int) { cfg.ReplyTimeout = value },
)
return wrapWithBack(form, s)
}
func (s *appState) wecomAppForm() tview.Primitive {
cfg := &s.config.Channels.WeComApp
form := baseChannelForm("WeCom App", cfg.Enabled, s.makeChannelOnEnabled(&cfg.Enabled))
form.AddInputField("Corp ID", cfg.CorpID, 64, nil, func(text string) {
cfg.CorpID = strings.TrimSpace(text)
})
form.AddInputField("Corp Secret", cfg.CorpSecret(), 128, nil, func(text string) {
cfg.SetCorpSecret(strings.TrimSpace(text))
})
addInt64Field(form, "Agent ID", cfg.AgentID, func(value int64) { cfg.AgentID = value })
form.AddInputField("Token", cfg.Token(), 128, nil, func(text string) {
cfg.SetToken(strings.TrimSpace(text))
})
form.AddInputField("Encoding AES Key", cfg.EncodingAESKey(), 128, nil, func(text string) {
cfg.SetEncodingAESKey(strings.TrimSpace(text))
})
form.AddInputField("Webhook Host", cfg.WebhookHost, 64, nil, func(text string) {
cfg.WebhookHost = strings.TrimSpace(text)
})
addIntField(form, "Webhook Port", cfg.WebhookPort, func(value int) { cfg.WebhookPort = value })
form.AddInputField("Webhook Path", cfg.WebhookPath, 64, nil, func(text string) {
cfg.WebhookPath = strings.TrimSpace(text)
})
addAllowFromField(form, &cfg.AllowFrom)
addIntField(
form,
"Reply Timeout",
cfg.ReplyTimeout,
func(value int) { cfg.ReplyTimeout = value },
)
return wrapWithBack(form, s)
}
func (s *appState) makeChannelOnEnabled(enabledPtr *bool) func(bool) {
return func(v bool) {
*enabledPtr = v
s.dirty = true
refreshMainMenuIfPresent(s)
if menu, ok := s.menus["channel"]; ok {
refreshChannelMenuFromState(menu, s)
}
}
}
func addAllowFromField(form *tview.Form, allowFrom *picoclawconfig.FlexibleStringSlice) {
form.AddInputField("Allow From", strings.Join(*allowFrom, ","), 128, nil, func(text string) {
*allowFrom = splitCSV(text)
})
}
func baseChannelForm(title string, enabled bool, onEnabled func(bool)) *tview.Form {
form := tview.NewForm()
form.SetBorder(true).SetTitle(fmt.Sprintf("Channel: %s", title))
form.SetButtonBackgroundColor(tcell.NewRGBColor(80, 250, 123))
form.SetButtonTextColor(tcell.NewRGBColor(12, 13, 22))
form.AddCheckbox("Enabled", enabled, func(checked bool) {
onEnabled(checked)
})
return form
}
func wrapWithBack(form *tview.Form, s *appState) tview.Primitive {
form.AddButton("Back", func() {
s.pop()
})
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEsc {
s.pop()
return nil
}
return event
})
return form
}
func splitCSV(input string) picoclawconfig.FlexibleStringSlice {
parts := strings.Split(strings.TrimSpace(input), ",")
cleaned := make([]string, 0, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
cleaned = append(cleaned, value)
}
return cleaned
}
func addIntField(form *tview.Form, label string, value int, onChange func(int)) {
form.AddInputField(label, fmt.Sprintf("%d", value), 16, nil, func(text string) {
var parsed int
if _, err := fmt.Sscanf(strings.TrimSpace(text), "%d", &parsed); err == nil {
onChange(parsed)
}
})
}
func addInt64Field(form *tview.Form, label string, value int64, onChange func(int64)) {
form.AddInputField(label, fmt.Sprintf("%d", value), 16, nil, func(text string) {
var parsed int64
if _, err := fmt.Sscanf(strings.TrimSpace(text), "%d", &parsed); err == nil {
onChange(parsed)
}
})
}
func channelItem(label, description string, enabled bool, action MenuAction) MenuItem {
item := MenuItem{
Label: label,
Description: description,
Action: action,
}
if !enabled {
color := tcell.ColorGray
item.MainColor = &color
}
return item
}
@@ -1,16 +0,0 @@
//go:build !windows
// +build !windows
package ui
import "os/exec"
func isGatewayProcessRunning() bool {
cmd := exec.Command("sh", "-c", "pgrep -f 'picoclaw\\s+gateway' >/dev/null 2>&1")
return cmd.Run() == nil
}
func stopGatewayProcess() error {
cmd := exec.Command("sh", "-c", "pkill -f 'picoclaw\\s+gateway' >/dev/null 2>&1")
return cmd.Run()
}
@@ -1,16 +0,0 @@
//go:build windows
// +build windows
package ui
import "os/exec"
func isGatewayProcessRunning() bool {
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq picoclaw.exe")
return cmd.Run() == nil
}
func stopGatewayProcess() error {
cmd := exec.Command("taskkill", "/F", "/IM", "picoclaw.exe")
return cmd.Run()
}
@@ -1,72 +0,0 @@
package ui
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type MenuAction func()
type MenuItem struct {
Label string
Description string
Action MenuAction
Disabled bool
MainColor *tcell.Color
DescColor *tcell.Color
}
type Menu struct {
*tview.Table
items []MenuItem
}
func NewMenu(title string, items []MenuItem) *Menu {
table := tview.NewTable().SetSelectable(true, false)
table.SetBorder(true).SetTitle(title)
table.SetBorders(false)
menu := &Menu{Table: table, items: items}
menu.applyItems(items)
menu.SetSelectedFunc(func(row, _ int) {
if row < 0 || row >= len(menu.items) {
return
}
item := menu.items[row]
if item.Disabled || item.Action == nil {
return
}
item.Action()
})
menu.SetSelectedStyle(
tcell.StyleDefault.Foreground(tview.Styles.InverseTextColor).
Background(tcell.NewRGBColor(189, 147, 249)),
)
return menu
}
func (m *Menu) applyItems(items []MenuItem) {
m.items = items
m.Clear()
for row, item := range items {
label := item.Label
if item.Disabled && label != "" {
label = label + " (disabled)"
}
left := tview.NewTableCell(label)
right := tview.NewTableCell(item.Description).SetAlign(tview.AlignRight)
if item.MainColor != nil {
left.SetTextColor(*item.MainColor)
}
if item.DescColor != nil {
right.SetTextColor(*item.DescColor)
} else {
right.SetTextColor(tview.Styles.TertiaryTextColor)
}
if item.Disabled {
left.SetTextColor(tcell.ColorGray)
right.SetTextColor(tcell.ColorGray)
}
m.SetCell(row, 0, left)
m.SetCell(row, 1, right)
}
}
@@ -1,399 +0,0 @@
package ui
import (
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
picoclawconfig "github.com/sipeed/picoclaw/pkg/config"
)
func (s *appState) modelMenu() tview.Primitive {
items := make([]MenuItem, 0, 1+len(s.config.ModelList))
currentModel := strings.TrimSpace(s.config.Agents.Defaults.ModelName)
for i := range s.config.ModelList {
index := i
model := s.config.ModelList[i]
isValid := isModelValid(model)
desc := model.APIBase
if desc == "" {
desc = model.AuthMethod
}
if desc == "" {
desc = "api_key required"
}
label := fmt.Sprintf("%s (%s)", model.ModelName, model.Model)
if model.ModelName == currentModel && currentModel != "" {
label = "* " + label
}
isSelected := model.ModelName == currentModel && currentModel != ""
items = append(items, MenuItem{
Label: label,
Description: desc,
MainColor: modelStatusColor(isValid, isSelected),
Action: func() {
s.push(fmt.Sprintf("model-%d", index), s.modelForm(index))
},
})
}
// Add model entry appended at the end so the models map to rows 1..N
items = append(items,
MenuItem{
Label: "**Add model**",
Description: "Append a new model entry",
Action: func() {
newName := s.nextAvailableModelName("new-model")
s.addModel(
&picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.4"},
)
s.push(
fmt.Sprintf("model-%d", len(s.config.ModelList)-1),
s.modelForm(len(s.config.ModelList)-1),
)
},
},
)
menu := NewMenu("Models", items)
menu.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEsc {
s.pop()
return nil
}
if event.Rune() == ' ' {
row, _ := menu.GetSelection()
if row >= 0 && row < len(s.config.ModelList) {
model := s.config.ModelList[row]
if !isModelValid(model) {
s.showMessage(
"Invalid model",
"Select a model with api_key or oauth auth_method",
)
return nil
}
s.config.Agents.Defaults.ModelName = model.ModelName
s.dirty = true
refreshModelMenu(menu, s.config.Agents.Defaults.GetModelName(), s.config.ModelList)
refreshMainMenuIfPresent(s)
}
return nil
}
return event
})
return menu
}
func (s *appState) modelForm(index int) tview.Primitive {
model := s.config.ModelList[index]
form := tview.NewForm()
form.SetBorder(true).SetTitle(fmt.Sprintf("Model: %s", model.ModelName))
addInput(form, "Model Name", model.ModelName, func(value string) {
if value == "" {
s.showMessage("Invalid model name", "Model Name cannot be empty")
return
}
if s.modelNameExists(value, index) {
s.showMessage("Duplicate model name", fmt.Sprintf("Model Name '%s' already exists", value))
return
}
oldName := model.ModelName
model.ModelName = value
if s.config.Agents.Defaults.ModelName == oldName {
s.config.Agents.Defaults.ModelName = value
}
s.dirty = true
form.SetTitle(fmt.Sprintf("Model: %s", model.ModelName))
refreshMainMenuIfPresent(s)
if menu, ok := s.menus["model"]; ok {
refreshModelMenuFromState(menu, s)
}
})
addInput(form, "Model", model.Model, func(value string) {
model.Model = value
s.dirty = true
refreshMainMenuIfPresent(s)
if menu, ok := s.menus["model"]; ok {
refreshModelMenuFromState(menu, s)
}
})
addInput(form, "API Base", model.APIBase, func(value string) {
model.APIBase = value
s.dirty = true
refreshMainMenuIfPresent(s)
if menu, ok := s.menus["model"]; ok {
refreshModelMenuFromState(menu, s)
}
})
addInput(form, "API Key", model.APIKey(), func(value string) {
model.SetAPIKey(value)
s.dirty = true
refreshMainMenuIfPresent(s)
if menu, ok := s.menus["model"]; ok {
refreshModelMenuFromState(menu, s)
}
})
addInput(form, "Proxy", model.Proxy, func(value string) {
model.Proxy = value
})
addInput(form, "Auth Method", model.AuthMethod, func(value string) {
model.AuthMethod = value
s.dirty = true
refreshMainMenuIfPresent(s)
if menu, ok := s.menus["model"]; ok {
refreshModelMenuFromState(menu, s)
}
})
addInput(form, "Connect Mode", model.ConnectMode, func(value string) {
model.ConnectMode = value
})
addInput(form, "Workspace", model.Workspace, func(value string) {
model.Workspace = value
})
addInput(form, "Max Tokens Field", model.MaxTokensField, func(value string) {
model.MaxTokensField = value
})
addIntInput(form, "RPM", model.RPM, func(value int) {
model.RPM = value
})
addIntInput(form, "Request Timeout", model.RequestTimeout, func(value int) {
model.RequestTimeout = value
})
form.AddButton("Delete", func() {
pageName := "confirm-delete-model"
if s.pages.HasPage(pageName) {
return
}
modal := tview.NewModal().
SetText("Are you sure you want to delete this model?").
AddButtons([]string{"Cancel", "Delete"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
s.pages.RemovePage(pageName)
if buttonLabel == "Delete" {
s.deleteModel(index)
}
})
modal.SetTitle("Confirm Delete").SetBorder(true)
s.pages.AddPage(pageName, modal, true, true)
})
form.AddButton("Test", func() {
s.testModel(model)
})
form.AddButton("Back", func() {
s.pop()
})
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEsc {
s.pop()
return nil
}
return event
})
return form
}
func addInput(form *tview.Form, label, value string, onChange func(string)) {
form.AddInputField(label, value, 128, nil, func(text string) {
onChange(strings.TrimSpace(text))
})
}
func addIntInput(form *tview.Form, label string, value int, onChange func(int)) {
form.AddInputField(label, fmt.Sprintf("%d", value), 16, nil, func(text string) {
var parsed int
if _, err := fmt.Sscanf(strings.TrimSpace(text), "%d", &parsed); err == nil {
onChange(parsed)
}
})
}
func (s *appState) addModel(model *picoclawconfig.ModelConfig) {
s.config.ModelList = append(s.config.ModelList, model)
}
func (s *appState) deleteModel(index int) {
if index < 0 || index >= len(s.config.ModelList) {
return
}
s.config.ModelList = append(s.config.ModelList[:index], s.config.ModelList[index+1:]...)
s.pop()
}
func modelStatusColor(valid bool, selected bool) *tcell.Color {
if valid {
color := tview.Styles.PrimaryTextColor
return &color
}
color := tcell.ColorGray
return &color
}
func refreshModelMenu(menu *Menu, currentModel string, models []*picoclawconfig.ModelConfig) {
for i, model := range models {
row := i
label := fmt.Sprintf("%s (%s)", model.ModelName, model.Model)
isValid := isModelValid(model)
if model.ModelName == currentModel && currentModel != "" {
label = "* " + label
}
cell := menu.GetCell(row, 0)
if cell != nil {
cell.SetText(label)
isSelected := model.ModelName == currentModel && currentModel != ""
color := modelStatusColor(isValid, isSelected)
if color != nil {
cell.SetTextColor(*color)
}
}
}
}
func refreshModelMenuFromState(menu *Menu, s *appState) {
items := make([]MenuItem, 0, 1+len(s.config.ModelList))
currentModel := strings.TrimSpace(s.config.Agents.Defaults.ModelName)
for i := range s.config.ModelList {
index := i
model := s.config.ModelList[i]
isValid := isModelValid(model)
desc := model.APIBase
if desc == "" {
desc = model.AuthMethod
}
if desc == "" {
desc = "api_key required"
}
label := fmt.Sprintf("%s (%s)", model.ModelName, model.Model)
if model.ModelName == currentModel && currentModel != "" {
label = "* " + label
}
isSelected := model.ModelName == currentModel && currentModel != ""
items = append(items, MenuItem{
Label: label,
Description: desc,
MainColor: modelStatusColor(isValid, isSelected),
Action: func() {
s.push(fmt.Sprintf("model-%d", index), s.modelForm(index))
},
})
}
items = append(items,
MenuItem{
Label: "**Add Model**",
Description: "Append a new model entry",
Action: func() {
newName := s.nextAvailableModelName("new-model")
s.addModel(
&picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.4"},
)
s.push(fmt.Sprintf("model-%d", len(s.config.ModelList)-1), s.modelForm(len(s.config.ModelList)-1))
},
},
)
menu.applyItems(items)
}
func isModelValid(model *picoclawconfig.ModelConfig) bool {
hasKey := strings.TrimSpace(model.APIKey()) != "" ||
strings.TrimSpace(model.AuthMethod) == "oauth"
hasModel := strings.TrimSpace(model.Model) != ""
return hasKey && hasModel
}
func (s *appState) modelNameExists(name string, excludeIndex int) bool {
target := strings.TrimSpace(name)
if target == "" {
return false
}
for i := range s.config.ModelList {
if i == excludeIndex {
continue
}
if strings.TrimSpace(s.config.ModelList[i].ModelName) == target {
return true
}
}
return false
}
func (s *appState) nextAvailableModelName(base string) string {
name := strings.TrimSpace(base)
if name == "" {
name = "new-model"
}
if !s.modelNameExists(name, -1) {
return name
}
for i := 2; ; i++ {
candidate := fmt.Sprintf("%s-%d", name, i)
if !s.modelNameExists(candidate, -1) {
return candidate
}
}
}
func (s *appState) testModel(model *picoclawconfig.ModelConfig) {
if model == nil {
return
}
if strings.TrimSpace(model.APIKey()) == "" {
s.showMessage("Missing API Key", "Set api_key before testing")
return
}
base := strings.TrimSpace(model.APIBase)
if base == "" {
s.showMessage("Missing API Base", "Set api_base before testing")
return
}
modelID := strings.TrimSpace(model.Model)
if modelID == "" {
s.showMessage("Missing Model", "Set model before testing")
return
}
if !strings.HasPrefix(modelID, "openai/") {
s.showMessage("Unsupported model", "Only openai/* models are supported for test")
return
}
modelName := strings.TrimPrefix(modelID, "openai/")
endpoint := strings.TrimRight(base, "/") + "/chat/completions"
payload := fmt.Sprintf(
`{"model":"%s","messages":[{"role":"user","content":"ping"}],"max_tokens":1}`,
modelName,
)
client := &http.Client{Timeout: 10 * time.Second}
request, err := http.NewRequest("POST", endpoint, strings.NewReader(payload))
if err != nil {
s.showMessage("Test failed", err.Error())
return
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer "+strings.TrimSpace(model.APIKey()))
resp, err := client.Do(request)
if err != nil {
s.showMessage("Test failed", err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
s.showMessage("Test OK", resp.Status)
return
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 2048))
if err != nil {
s.showMessage("Test failed", fmt.Sprintf("failed to read response: %v", err))
return
}
s.showMessage(
"Test failed",
fmt.Sprintf("%s: %s", resp.Status, strings.TrimSpace(string(body))),
)
}
@@ -1,55 +0,0 @@
package ui
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
const (
colorBlue = "[#3e5db9]"
colorRed = "[#d54646]"
banner = "\r\n[::b]" +
colorBlue + "██████╗ ██╗ ██████╗ ██████╗ " + colorRed + " ██████╗██╗ █████╗ ██╗ ██╗\n" +
colorBlue + "██╔══██╗██║██╔════╝██╔═══██╗" + colorRed + "██╔════╝██║ ██╔══██╗██║ ██║\n" +
colorBlue + "██████╔╝██║██║ ██║ ██║" + colorRed + "██║ ██║ ███████║██║ █╗ ██║\n" +
colorBlue + "██╔═══╝ ██║██║ ██║ ██║" + colorRed + "██║ ██║ ██╔══██║██║███╗██║\n" +
colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
"[:]"
)
func applyStyles() {
tview.Styles.PrimitiveBackgroundColor = tcell.NewRGBColor(12, 13, 22)
tview.Styles.ContrastBackgroundColor = tcell.NewRGBColor(34, 19, 53)
tview.Styles.MoreContrastBackgroundColor = tcell.NewRGBColor(18, 18, 32)
tview.Styles.BorderColor = tcell.NewRGBColor(112, 102, 255)
tview.Styles.TitleColor = tcell.NewRGBColor(255, 121, 198)
tview.Styles.GraphicsColor = tcell.NewRGBColor(139, 233, 253)
tview.Styles.PrimaryTextColor = tcell.NewRGBColor(241, 250, 255)
tview.Styles.SecondaryTextColor = tcell.NewRGBColor(80, 250, 123)
tview.Styles.TertiaryTextColor = tcell.NewRGBColor(139, 233, 253)
tview.Styles.InverseTextColor = tcell.NewRGBColor(12, 13, 22)
tview.Styles.ContrastSecondaryTextColor = tcell.NewRGBColor(189, 147, 249)
}
func bannerView() *tview.TextView {
text := tview.NewTextView()
text.SetDynamicColors(true)
text.SetTextAlign(tview.AlignCenter)
text.SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
text.SetText(banner)
text.SetBorder(false)
return text
}
const footerText = "Esc: Back/Exit | Enter: Enter | ←↓↑→ : Move | Space: Select | Tab/Shift+Tab: Switch"
func footerView() *tview.TextView {
text := tview.NewTextView()
text.SetTextAlign(tview.AlignCenter)
text.SetText(footerText)
text.SetBackgroundColor(tview.Styles.MoreContrastBackgroundColor)
text.SetTextColor(tview.Styles.PrimaryTextColor)
text.SetBorder(false)
return text
}
+36 -3
View File
@@ -1,15 +1,48 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/internal/ui"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
"github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/ui"
)
func main() {
if err := ui.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
configPath := tuicfg.DefaultConfigPath()
if len(os.Args) > 1 {
configPath = os.Args[1]
}
configDir := filepath.Dir(configPath)
if _, err := os.Stat(configDir); os.IsNotExist(err) {
cmd := exec.Command("picoclaw", "onboard")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
}
cfg, err := tuicfg.Load(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "picoclaw-launcher-tui: %v\n", err)
os.Exit(1)
}
app := ui.New(cfg, configPath)
// Bind model selection hook to sync to main config
app.OnModelSelected = func(scheme tuicfg.Scheme, user tuicfg.User, modelID string) {
_ = tuicfg.SyncSelectedModelToMainConfig(scheme, user, modelID)
}
if err := app.Run(); err != nil {
fmt.Fprintf(os.Stderr, "picoclaw-launcher-tui: %v\n", err)
os.Exit(1)
}
}
+325
View File
@@ -0,0 +1,325 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"fmt"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
)
// App is the root TUI application.
type App struct {
tapp *tview.Application
pages *tview.Pages
pageStack []string
cfg *tuicfg.TUIConfig
configPath string
pageRefreshFns map[string]func()
headerModelTV *tview.TextView
modalOpen map[string]bool
// OnModelSelected is called when a model is selected in the UI.
// Can be nil to disable.
OnModelSelected func(scheme tuicfg.Scheme, user tuicfg.User, modelID string)
modelCache map[string][]modelEntry
modelCacheMu sync.RWMutex
refreshMu sync.Mutex
}
// cacheKey returns the map key for a (scheme, user) pair.
func cacheKey(schemeName, userName string) string {
return fmt.Sprintf("%s/%s", schemeName, userName)
}
// cachedModels returns a defensive copy of the cached model list for a user (may be nil).
func (a *App) cachedModels(schemeName, userName string) []modelEntry {
a.modelCacheMu.RLock()
defer a.modelCacheMu.RUnlock()
entries := a.modelCache[cacheKey(schemeName, userName)]
return append([]modelEntry(nil), entries...)
}
// refreshModelCache fetches models for every user in the config concurrently.
// Serialized by refreshMu so concurrent calls don't race on the cache map.
// When all fetches complete it calls onDone via QueueUpdateDraw.
func (a *App) refreshModelCache(onDone func()) {
go func() {
a.refreshMu.Lock()
defer a.refreshMu.Unlock()
users := a.cfg.Provider.Users
schemes := a.cfg.Provider.Schemes
schemeURL := make(map[string]string, len(schemes))
for _, s := range schemes {
schemeURL[s.Name] = s.BaseURL
}
var wg sync.WaitGroup
for _, u := range users {
baseURL, ok := schemeURL[u.Scheme]
if !ok || baseURL == "" {
continue
}
if u.Key == "" {
a.modelCacheMu.Lock()
if a.modelCache == nil {
a.modelCache = make(map[string][]modelEntry)
}
a.modelCache[cacheKey(u.Scheme, u.Name)] = nil
a.modelCacheMu.Unlock()
continue
}
wg.Add(1)
bURL := baseURL
go func() {
defer wg.Done()
entries, err := fetchModels(bURL, u.Key)
a.modelCacheMu.Lock()
if a.modelCache == nil {
a.modelCache = make(map[string][]modelEntry)
}
if err != nil || len(entries) == 0 {
a.modelCache[cacheKey(u.Scheme, u.Name)] = nil
} else {
a.modelCache[cacheKey(u.Scheme, u.Name)] = entries
}
a.modelCacheMu.Unlock()
}()
}
wg.Wait()
if onDone != nil {
a.tapp.QueueUpdateDraw(onDone)
}
}()
}
// New creates and wires up the TUI application.
func New(cfg *tuicfg.TUIConfig, configPath string) *App {
// Cyberpunk Theme Colors
// Dark background
tview.Styles.PrimitiveBackgroundColor = tcell.NewHexColor(0x050510) // Deep Void
tview.Styles.ContrastBackgroundColor = tcell.NewHexColor(0x1a1a2e) // Dark Indigo
tview.Styles.MoreContrastBackgroundColor = tcell.NewHexColor(0x2a2a40)
// Borders and Titles
tview.Styles.BorderColor = tcell.NewHexColor(0x00f0ff) // Neon Cyan
tview.Styles.TitleColor = tcell.NewHexColor(0x00f0ff) // Neon Cyan
tview.Styles.GraphicsColor = tcell.NewHexColor(0xff00ff) // Neon Magenta
// Text
tview.Styles.PrimaryTextColor = tcell.NewHexColor(0xe0e0e0) // Off-white
tview.Styles.SecondaryTextColor = tcell.NewHexColor(0x00f0ff) // Neon Cyan
tview.Styles.TertiaryTextColor = tcell.NewHexColor(0x39ff14) // Neon Lime
tview.Styles.InverseTextColor = tcell.NewHexColor(0x000000) // Black
tview.Styles.ContrastSecondaryTextColor = tcell.NewHexColor(0xff00ff) // Neon Magenta
a := &App{
tapp: tview.NewApplication(),
pages: tview.NewPages(),
pageStack: []string{},
cfg: cfg,
configPath: configPath,
pageRefreshFns: make(map[string]func()),
modalOpen: make(map[string]bool),
}
a.tapp.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
if len(a.modalOpen) > 0 {
return event
}
return a.goBack()
}
return event
})
a.buildPages()
return a
}
// Run starts the TUI event loop.
func (a *App) Run() error {
return a.tapp.SetRoot(a.pages, true).EnableMouse(true).Run()
}
func (a *App) buildPages() {
a.pages.AddPage("home", a.newHomePage(), true, true)
a.pageStack = []string{"home"}
}
func (a *App) navigateTo(name string, page tview.Primitive) {
a.pages.RemovePage(name)
a.pages.AddPage(name, page, true, false)
a.pageStack = append(a.pageStack, name)
a.pages.SwitchToPage(name)
}
func (a *App) goBack() *tcell.EventKey {
if len(a.pageStack) <= 1 {
return nil
}
popped := a.pageStack[len(a.pageStack)-1]
a.pageStack = a.pageStack[:len(a.pageStack)-1]
a.pages.RemovePage(popped)
prev := a.pageStack[len(a.pageStack)-1]
if fn, ok := a.pageRefreshFns[prev]; ok {
fn()
}
if prev == "home" && a.headerModelTV != nil {
a.headerModelTV.SetText(a.cfg.CurrentModelLabel() + " ")
}
a.pages.SwitchToPage(prev)
return nil
}
func (a *App) showModal(name string, primitive tview.Primitive) {
a.modalOpen[name] = true
a.pages.AddPage(name, primitive, true, true)
}
func (a *App) hideModal(name string) {
delete(a.modalOpen, name)
a.pages.HidePage(name)
a.pages.RemovePage(name)
}
func (a *App) save() {
if err := tuicfg.Save(a.configPath, a.cfg); err != nil {
a.showError("save failed: " + err.Error())
}
}
func (a *App) showError(msg string) {
modal := tview.NewModal().
SetText(" [red::b]ERROR[-::-]\n\n" + msg).
AddButtons([]string{"OK"}).
SetDoneFunc(func(_ int, _ string) {
a.hideModal("error")
})
// Cyberpunk Modal Style
modal.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e)) // Deep Indigo
modal.SetTextColor(tcell.NewHexColor(0xffffff)) // White
modal.SetButtonBackgroundColor(tcell.NewHexColor(0xff2a2a)) // Neon Red
modal.SetButtonTextColor(tcell.NewHexColor(0xffffff)) // White
a.showModal("error", modal)
}
func (a *App) confirmDelete(label string, onConfirm func()) {
modal := tview.NewModal().
SetText(" [red::b]DELETE WARNING[-::-]\n\nDelete " + label + "?\n[gray]This action cannot be undone.[-]").
AddButtons([]string{"Delete", "Cancel"}).
SetDoneFunc(func(_ int, buttonLabel string) {
a.hideModal("confirm-delete")
if buttonLabel == "Delete" {
onConfirm()
}
})
// Cyberpunk Modal Style
modal.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e)) // Deep Indigo
modal.SetTextColor(tcell.NewHexColor(0xffffff)) // White
modal.SetButtonBackgroundColor(tcell.NewHexColor(0xff2a2a)) // Neon Red for danger
modal.SetButtonTextColor(tcell.NewHexColor(0xffffff)) // White
a.showModal("confirm-delete", modal)
}
func centeredForm(form *tview.Form, widthPct, height int) tview.Primitive {
return tview.NewFlex().
AddItem(tview.NewBox(), 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(tview.NewBox(), 0, 1, false).
AddItem(form, height, 1, true).
AddItem(tview.NewBox(), 0, 1, false), 0, widthPct, true).
AddItem(tview.NewBox(), 0, 1, false)
}
func hintBar(text string) *tview.TextView {
tv := tview.NewTextView().
SetText(text).
SetDynamicColors(true).
SetTextAlign(tview.AlignCenter).
SetTextColor(tcell.NewHexColor(0x00f0ff)) // Neon Cyan
tv.SetBackgroundColor(tcell.NewHexColor(0x2a2a40)) // Darker Indigo
return tv
}
func (a *App) buildShell(pageID string, content tview.Primitive, hint string) tview.Primitive {
var modelTV *tview.TextView
if pageID == "home" {
if a.headerModelTV == nil {
a.headerModelTV = tview.NewTextView()
a.headerModelTV.SetTextAlign(tview.AlignRight).
SetTextColor(tcell.NewHexColor(0x39ff14)). // Neon Lime
SetDynamicColors(true).
SetBackgroundColor(tcell.NewHexColor(0x050510))
}
modelTV = a.headerModelTV
modelTV.SetText("MODEL: " + a.cfg.CurrentModelLabel() + " ")
} else {
modelTV = tview.NewTextView()
modelTV.SetBackgroundColor(tcell.NewHexColor(0x050510))
}
headerLeft := tview.NewTextView().
SetText(" [#ff00ff::b]///[#00f0ff] PICOCLAW LAUNCHER [#ff00ff]///").
SetDynamicColors(true).
SetBackgroundColor(tcell.NewHexColor(0x050510))
header := tview.NewFlex().
AddItem(headerLeft, 0, 1, false).
AddItem(modelTV, 0, 1, false)
sidebar := tview.NewTextView().
SetDynamicColors(true).
SetWrap(false)
sidebar.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e)) // Deep Indigo
// Cyberpunk Sidebar Styling
activePrefix := "[#39ff14::b]>> " // Neon Lime arrow
activeSuffix := "[-]"
inactivePrefix := "[#808080] "
inactiveSuffix := "[-]"
sbText := "\n\n" // Top padding
menuItem := func(id, label string) string {
if pageID == id {
return activePrefix + label + activeSuffix + "\n\n"
}
return inactivePrefix + label + inactiveSuffix + "\n\n"
}
sbText += menuItem("home", "HOME")
sbText += menuItem("schemes", "SCHEMES")
sbText += menuItem("users", "USERS")
sbText += menuItem("models", "MODELS")
sbText += menuItem("channels", "CHANNELS")
sbText += menuItem("gateway", "GATEWAY")
sidebar.SetText(sbText)
footer := hintBar(hint)
grid := tview.NewGrid().
SetRows(1, 0, 1).
SetColumns(20, 0). // Slightly wider sidebar
AddItem(header, 0, 0, 1, 2, 0, 0, false).
AddItem(sidebar, 1, 0, 1, 1, 0, 0, false).
AddItem(content, 1, 1, 1, 1, 0, 0, true).
AddItem(footer, 2, 0, 1, 2, 0, 0, false)
// Add a border around the content area if possible, or ensure content has its own border
// grid.SetBorders(false) // Grid borders usually look bad, handled by components
return grid
}
+202
View File
@@ -0,0 +1,202 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strconv"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func (a *App) newChannelsPage() tview.Primitive {
list := tview.NewList()
list.SetBorder(true).
SetTitle(" [#00f0ff::b] COMMUNICATION CHANNELS ").
SetTitleColor(tcell.NewHexColor(0x00f0ff)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
list.SetMainTextColor(tcell.NewHexColor(0xe0e0e0))
list.SetSecondaryTextColor(tcell.NewHexColor(0x808080))
list.SetSelectedStyle(
tcell.StyleDefault.Background(tcell.NewHexColor(0xff00ff)).Foreground(tcell.NewHexColor(0x050510)),
)
list.SetHighlightFullLine(true)
list.SetBackgroundColor(tcell.NewHexColor(0x050510))
rebuild := func() {
sel := list.GetCurrentItem()
list.Clear()
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
configPath := filepath.Join(home, ".picoclaw", "config.json")
var cfg map[string]any
if data, err := os.ReadFile(configPath); err == nil {
_ = json.Unmarshal(data, &cfg)
}
if chRaw, ok := cfg["channels"].(map[string]any); ok {
for name, ch := range chRaw {
chMap, ok := ch.(map[string]any)
enabled := "disabled"
if ok {
if e, ok := chMap["enabled"].(bool); ok && e {
enabled = "enabled"
}
}
list.AddItem(name, fmt.Sprintf("Status: %s", enabled), 0, func() {
a.showChannelEditForm(configPath, name, chMap)
})
}
}
if sel >= 0 && sel < list.GetItemCount() {
list.SetCurrentItem(sel)
}
}
rebuild()
a.pageRefreshFns["channels"] = rebuild
list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
return a.goBack()
}
return event
})
return a.buildShell("channels", list, " [#ff00ff]Enter:[-] edit [#ff2a2a]ESC:[-] back ")
}
func (a *App) showChannelEditForm(configPath, channelName string, existing map[string]any) {
form := tview.NewForm()
form.SetBorder(true).
SetTitle(" [::b]EDIT CHANNEL ").
SetTitleColor(tcell.NewHexColor(0x39ff14)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
form.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e))
form.SetFieldBackgroundColor(tcell.NewHexColor(0x050510))
form.SetFieldTextColor(tcell.NewHexColor(0x00f0ff))
form.SetLabelColor(tcell.NewHexColor(0xe0e0e0))
form.SetButtonBackgroundColor(tcell.NewHexColor(0xff00ff))
form.SetButtonTextColor(tcell.NewHexColor(0xffffff))
fields := make(map[string]*tview.InputField)
var nameField *tview.InputField
if channelName == "" {
nameField = tview.NewInputField().
SetLabel("Channel Name").
SetText("").
SetFieldWidth(28)
form.AddFormItem(nameField)
}
for k, v := range existing {
if reflect.ValueOf(v).Kind() == reflect.Map || reflect.ValueOf(v).Kind() == reflect.Slice {
continue
}
valStr := fmt.Sprintf("%v", v)
field := tview.NewInputField().
SetLabel(k).
SetText(valStr).
SetFieldWidth(28)
form.AddFormItem(field)
fields[k] = field
}
form.AddButton("SAVE", func() {
var cfg map[string]any
if data, err := os.ReadFile(configPath); err == nil {
if err := json.Unmarshal(data, &cfg); err != nil {
cfg = make(map[string]any)
}
} else {
cfg = make(map[string]any)
}
if _, ok := cfg["channels"]; !ok {
cfg["channels"] = make(map[string]any)
}
channels, ok := cfg["channels"].(map[string]any)
if !ok {
channels = make(map[string]any)
cfg["channels"] = channels
}
finalName := channelName
if channelName == "" {
if nameField == nil || nameField.GetText() == "" {
a.showError("Channel name is required")
return
}
finalName = nameField.GetText()
}
updated := make(map[string]any)
if existing != nil {
for k, v := range existing {
updated[k] = v
}
}
for k, field := range fields {
val := field.GetText()
if val == "true" {
updated[k] = true
} else if val == "false" {
updated[k] = false
} else if num, err := strconv.Atoi(val); err == nil {
updated[k] = num
} else {
updated[k] = val
}
}
if channelName != "" && finalName != channelName {
delete(channels, channelName)
}
channels[finalName] = updated
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
a.showError(fmt.Sprintf("Failed to save config: %v", err))
return
}
if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil {
a.showError(fmt.Sprintf("Failed to create config directory: %v", err))
return
}
if err := os.WriteFile(configPath, data, 0o600); err != nil {
a.showError(fmt.Sprintf("Failed to write config: %v", err))
return
}
a.hideModal("channel-edit")
a.goBack()
})
form.AddButton("CANCEL", func() {
a.hideModal("channel-edit")
})
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
a.hideModal("channel-edit")
return nil
}
return event
})
a.showModal("channel-edit", centeredForm(form, 4, 20))
}
+261
View File
@@ -0,0 +1,261 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
const pidFileName = "gateway.pid"
type gatewayStatus struct {
running bool
pid int
}
func getPidPath() string {
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
return filepath.Join(home, ".picoclaw", pidFileName)
}
func isProcessRunning(pid int) bool {
if runtime.GOOS == "windows" {
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid))
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), strconv.Itoa(pid))
} else if runtime.GOOS == "darwin" {
cmd := exec.Command("ps", "aux")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), fmt.Sprintf(" %d ", pid))
}
// Linux
_, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
return err == nil
}
func getGatewayStatus() gatewayStatus {
pidPath := getPidPath()
data, err := os.ReadFile(pidPath)
if err != nil {
return gatewayStatus{running: false}
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return gatewayStatus{running: false}
}
if !isProcessRunning(pid) {
os.Remove(pidPath)
return gatewayStatus{running: false}
}
return gatewayStatus{
running: true,
pid: pid,
}
}
func startGateway() error {
status := getGatewayStatus()
if status.running {
return fmt.Errorf("gateway is already running (PID: %d)", status.pid)
}
pidPath := getPidPath()
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", "start /B picoclaw gateway > NUL 2>&1")
} else {
cmd = exec.Command("sh", "-c", "nohup picoclaw gateway > /dev/null 2>&1 & echo $! > "+pidPath)
}
err := cmd.Start()
if err != nil {
return err
}
time.Sleep(1 * time.Second)
if runtime.GOOS == "windows" {
cmd := exec.Command(
"wmic",
"process",
"where",
"name='picoclaw.exe' and commandline like '%gateway%'",
"get",
"processid",
)
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to get gateway PID: %w", err)
}
lines := strings.Split(string(output), "\n")
for _, line := range lines[1:] {
line = strings.TrimSpace(line)
if line == "" {
continue
}
pid, err := strconv.Atoi(line)
if err == nil {
os.WriteFile(pidPath, []byte(strconv.Itoa(pid)), 0o600)
break
}
}
}
status = getGatewayStatus()
if !status.running {
return fmt.Errorf("failed to start gateway")
}
return nil
}
func stopGateway() error {
status := getGatewayStatus()
if !status.running {
return fmt.Errorf("gateway is not running")
}
var err error
if runtime.GOOS == "windows" {
err = exec.Command("taskkill", "/F", "/PID", strconv.Itoa(status.pid)).Run()
} else {
err = exec.Command("kill", "-9", strconv.Itoa(status.pid)).Run()
}
if err != nil {
return err
}
// 多次尝试确认进程已停止
for i := 0; i < 5; i++ {
if !isProcessRunning(status.pid) {
break
}
time.Sleep(200 * time.Millisecond)
}
os.Remove(getPidPath())
return nil
}
func (a *App) newGatewayPage() tview.Primitive {
flex := tview.NewFlex().SetDirection(tview.FlexRow)
flex.SetBorder(true).
SetTitle(" [#00f0ff::b] GATEWAY MANAGEMENT ").
SetTitleColor(tcell.NewHexColor(0x00f0ff)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
flex.SetBackgroundColor(tcell.NewHexColor(0x050510))
statusTV := tview.NewTextView().
SetDynamicColors(true).
SetTextAlign(tview.AlignCenter).
SetText("Checking status...")
statusTV.SetBackgroundColor(tcell.NewHexColor(0x050510))
var updateStatus func()
// 使用List作为按钮,保证显示和交互正常
buttons := tview.NewList()
buttons.SetBackgroundColor(tcell.NewHexColor(0x050510))
buttons.SetMainTextColor(tcell.ColorWhite)
buttons.SetSelectedBackgroundColor(tcell.NewHexColor(0xff00ff))
buttons.SetSelectedTextColor(tcell.ColorBlack)
buttons.AddItem(" [lime]START[white] ", "", 0, func() {
if !getGatewayStatus().running {
err := startGateway()
if err != nil {
a.showError(err.Error())
}
updateStatus()
}
})
buttons.AddItem(" [red]STOP[white] ", "", 0, func() {
if getGatewayStatus().running {
err := stopGateway()
if err != nil {
a.showError(err.Error())
}
updateStatus()
}
})
buttonFlex := tview.NewFlex().SetDirection(tview.FlexColumn)
buttonFlex.
AddItem(tview.NewBox(), 0, 1, false).
AddItem(buttons, 20, 1, true).
AddItem(tview.NewBox(), 0, 1, false)
flex.
AddItem(tview.NewBox(), 0, 1, false).
AddItem(statusTV, 3, 1, false).
AddItem(tview.NewBox(), 0, 1, false).
AddItem(buttonFlex, 4, 1, true).
AddItem(tview.NewBox(), 0, 1, false)
updateStatus = func() {
status := getGatewayStatus()
if status.running {
statusTV.SetText(fmt.Sprintf("[#39ff14::b]GATEWAY RUNNING[-]\n\nPID: %d", status.pid))
buttons.SetItemText(0, " [gray]START[white] ", "")
buttons.SetItemText(1, " [red]STOP[white] ", "")
} else {
statusTV.SetText("[#ff2a2a::b]GATEWAY STOPPED[-]\n\nPID: N/A")
buttons.SetItemText(0, " [lime]START[white] ", "")
buttons.SetItemText(1, " [gray]STOP[white] ", "")
}
}
updateStatus()
done := make(chan struct{})
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
a.tapp.QueueUpdateDraw(updateStatus)
case <-done:
return
}
}
}()
originalInputCapture := flex.GetInputCapture()
flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
close(done)
return a.goBack()
}
if originalInputCapture != nil {
return originalInputCapture(event)
}
return event
})
a.pageRefreshFns["gateway"] = updateStatus
return a.buildShell("gateway", flex, " [#39ff14]Enter:[-] select [#ff2a2a]ESC:[-] back ")
}
+70
View File
@@ -0,0 +1,70 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"os"
"os/exec"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func (a *App) newHomePage() tview.Primitive {
list := tview.NewList()
list.SetBorder(true).
SetTitle(" [#00f0ff::b] ACTIVE CONFIGURATION ").
SetTitleColor(tcell.NewHexColor(0x00f0ff)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
list.SetMainTextColor(tcell.NewHexColor(0xe0e0e0))
list.SetSecondaryTextColor(tcell.NewHexColor(0x808080))
list.SetSelectedStyle(
tcell.StyleDefault.Background(tcell.NewHexColor(0x39ff14)).Foreground(tcell.NewHexColor(0x050510)),
)
list.SetHighlightFullLine(true)
list.SetBackgroundColor(tcell.NewHexColor(0x050510))
rebuildList := func() {
sel := list.GetCurrentItem()
list.Clear()
list.AddItem("MODEL: "+a.cfg.CurrentModelLabel(), "Select to configure AI model", 'm', func() {
a.navigateTo("schemes", a.newSchemesPage())
})
list.AddItem(
"CHANNELS: Configure communication channels",
"Manage Telegram/Discord/WeChat channels",
'n',
func() {
a.navigateTo("channels", a.newChannelsPage())
},
)
list.AddItem("GATEWAY MANAGEMENT", "Manage PicoClaw gateway daemon", 'g', func() {
a.navigateTo("gateway", a.newGatewayPage())
})
list.AddItem("CHAT: Start AI agent chat", "Launch interactive chat session", 'c', func() {
a.tapp.Suspend(func() {
cmd := exec.Command("picoclaw", "agent")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
})
})
list.AddItem("QUIT SYSTEM", "Exit PicoClaw Launcher", 'q', func() { a.tapp.Stop() })
if sel >= 0 && sel < list.GetItemCount() {
list.SetCurrentItem(sel)
}
}
rebuildList()
a.pageRefreshFns["home"] = rebuildList
return a.buildShell(
"home",
list,
" [#00f0ff]m:[-] model [#00f0ff]n:[-] channels [#00f0ff]g:[-] gateway [#00f0ff]c:[-] chat [#ff2a2a]q:[-] quit ",
)
}
+200
View File
@@ -0,0 +1,200 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
)
type modelsAPIResponse struct {
Data []modelEntry `json:"data"`
}
type modelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
func (a *App) newModelsPage(schemeName, userName, baseURL string) tview.Primitive {
table := tview.NewTable().
SetBorders(false).
SetSelectable(true, false).
SetFixed(0, 0)
table.SetBorder(true).
SetTitle(fmt.Sprintf(" [#00f0ff::b] MODELS · %s / %s ", schemeName, userName)).
SetTitleColor(tcell.NewHexColor(0x00f0ff)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
table.SetSelectedStyle(
tcell.StyleDefault.Background(tcell.NewHexColor(0xff00ff)).Foreground(tcell.NewHexColor(0xffffff)),
)
table.SetBackgroundColor(tcell.NewHexColor(0x050510))
var modelIDs []string
status := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetDynamicColors(true).
SetText("[#ffff00]FETCHING MODELS...[-]")
status.SetBackgroundColor(tcell.NewHexColor(0x050510))
flex := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(status, 1, 0, false).
AddItem(table, 0, 1, false)
apiKey := a.resolveKey(schemeName, userName)
go func() {
var entries []modelEntry
var err error
if apiKey == "" {
err = fmt.Errorf("key is required")
} else {
entries, err = fetchModels(baseURL, apiKey)
}
a.modelCacheMu.Lock()
if a.modelCache == nil {
a.modelCache = make(map[string][]modelEntry)
}
if err == nil && len(entries) > 0 {
a.modelCache[cacheKey(schemeName, userName)] = entries
} else {
a.modelCache[cacheKey(schemeName, userName)] = nil
}
a.modelCacheMu.Unlock()
a.tapp.QueueUpdateDraw(func() {
if err != nil {
status.SetText(fmt.Sprintf("[#ff2a2a]ERROR: %s[-]", err.Error()))
table.SetCell(0, 0, tview.NewTableCell(" (failed to load models)"))
a.tapp.SetFocus(table)
return
}
if len(entries) == 0 {
status.SetText("[#ff2a2a]NO MODELS RETURNED[-]")
table.SetCell(0, 0, tview.NewTableCell(" (no models available)"))
a.tapp.SetFocus(table)
return
}
status.SetText(fmt.Sprintf("[#39ff14]%d MODEL(S) LOADED[-]", len(entries)))
for i, m := range entries {
modelIDs = append(modelIDs, m.ID)
table.SetCell(i, 0,
tview.NewTableCell(fmt.Sprintf("%3d", i+1)).
SetAlign(tview.AlignRight).
SetTextColor(tcell.NewHexColor(0x808080)).
SetSelectable(false),
)
table.SetCell(i, 1,
tview.NewTableCell(" "+m.ID).
SetAlign(tview.AlignLeft).
SetExpansion(1).
SetTextColor(tcell.NewHexColor(0xe0e0e0)),
)
}
a.tapp.SetFocus(table)
})
}()
table.SetSelectedFunc(func(row, _ int) {
if row < 0 || row >= len(modelIDs) {
return
}
a.cfg.Provider.Current = tuicfg.ProviderCurrent{
Scheme: schemeName,
User: userName,
Model: modelIDs[row],
}
a.save()
// Trigger model selected callback if set
if a.OnModelSelected != nil && a.cfg.Model.Type == "provider" {
scheme := a.cfg.Provider.SchemeByName(schemeName)
if scheme == nil {
a.goBack()
return
}
var user tuicfg.User
for _, u := range a.cfg.Provider.Users {
if u.Scheme == schemeName && u.Name == userName {
user = u
break
}
}
a.OnModelSelected(*scheme, user, modelIDs[row])
}
a.goBack()
})
return a.buildShell("models", flex, " [#39ff14]Enter:[-] select [#ff00ff]ESC:[-] back ")
}
func (a *App) resolveKey(schemeName, userName string) string {
for _, u := range a.cfg.Provider.Users {
if u.Scheme == schemeName && u.Name == userName {
return u.Key
}
}
return ""
}
func fetchModels(baseURL, apiKey string) ([]modelEntry, error) {
url := strings.TrimRight(baseURL, "/") + "/models"
client := &http.Client{Timeout: 15 * time.Second}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
if apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
var result modelsAPIResponse
if err := json.Unmarshal(body, &result); err == nil && len(result.Data) > 0 {
return result.Data, nil
}
var arr []modelEntry
if err := json.Unmarshal(body, &arr); err == nil {
return arr, nil
}
return nil, fmt.Errorf(
"decode response: unrecognized shape: %s",
strings.TrimSpace(string(body[:min(len(body), 256)])),
)
}
+252
View File
@@ -0,0 +1,252 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
)
func (a *App) newSchemesPage() tview.Primitive {
table := tview.NewTable().
SetBorders(false).
SetSelectable(true, false)
table.SetBorder(true).
SetTitle(" [#00f0ff::b] PROVIDER SCHEMES ").
SetTitleColor(tcell.NewHexColor(0x00f0ff)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
table.SetSelectedStyle(
tcell.StyleDefault.Background(tcell.NewHexColor(0xff00ff)).Foreground(tcell.NewHexColor(0xffffff)),
)
table.SetBackgroundColor(tcell.NewHexColor(0x050510))
rowToIdx := func(row int) int { return row / 2 }
selectedSchemeName := func() string {
row, _ := table.GetSelection()
idx := rowToIdx(row)
schemes := a.cfg.Provider.Schemes
if idx >= 0 && idx < len(schemes) {
return schemes[idx].Name
}
return ""
}
rebuild := func() {
selName := selectedSchemeName()
table.Clear()
schemes := a.cfg.Provider.Schemes
for i, s := range schemes {
nameRow := i * 2
detailRow := nameRow + 1
table.SetCell(nameRow, 0,
tview.NewTableCell(" "+s.Name).
SetTextColor(tcell.NewHexColor(0xe0e0e0)).
SetExpansion(1).
SetSelectable(true),
)
users := a.cfg.Provider.UsersForScheme(s.Name)
n := len(users)
m := 0
for _, u := range users {
if models := a.cachedModels(s.Name, u.Name); len(models) > 0 {
m++
}
}
table.SetCell(detailRow, 0,
tview.NewTableCell(fmt.Sprintf(" [#808080](%d/%d) %s", m, n, s.BaseURL)).
SetTextColor(tcell.NewHexColor(0x808080)).
SetExpansion(1).
SetSelectable(false),
)
table.SetCell(detailRow, 1,
tview.NewTableCell("[#00f0ff]"+s.Type+" ").
SetAlign(tview.AlignRight).
SetSelectable(false),
)
}
if selName != "" {
for i, s := range schemes {
if s.Name == selName {
table.Select(i*2, 0)
return
}
}
}
if table.GetRowCount() > 0 {
table.Select(0, 0)
}
}
rebuild()
a.refreshModelCache(rebuild)
a.pageRefreshFns["schemes"] = func() { a.refreshModelCache(rebuild) }
table.SetSelectedFunc(func(row, _ int) {
idx := rowToIdx(row)
schemes := a.cfg.Provider.Schemes
if idx < 0 || idx >= len(schemes) {
return
}
name := schemes[idx].Name
a.navigateTo("users", a.newUsersPage(name))
})
table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
row, _ := table.GetSelection()
idx := rowToIdx(row)
schemes := a.cfg.Provider.Schemes
switch event.Rune() {
case 'a':
a.showSchemeForm(nil, func(s tuicfg.Scheme) {
a.cfg.Provider.Schemes = append(a.cfg.Provider.Schemes, s)
a.save()
a.refreshModelCache(rebuild)
})
return nil
case 'e':
if idx < 0 || idx >= len(schemes) {
return nil
}
origName := schemes[idx].Name
orig := schemes[idx]
a.showSchemeForm(&orig, func(s tuicfg.Scheme) {
current := a.cfg.Provider.Schemes
for i, sc := range current {
if sc.Name == origName {
a.cfg.Provider.Schemes[i] = s
break
}
}
a.save()
a.refreshModelCache(func() {
rebuild()
for i, sc := range a.cfg.Provider.Schemes {
if sc.Name == s.Name {
table.Select(i*2, 0)
break
}
}
})
})
return nil
case 'd':
if idx < 0 || idx >= len(schemes) {
return nil
}
name := schemes[idx].Name
a.confirmDelete(fmt.Sprintf("scheme %q", name), func() {
current := a.cfg.Provider.Schemes
newSchemes := make([]tuicfg.Scheme, 0, len(current))
for _, sc := range current {
if sc.Name != name {
newSchemes = append(newSchemes, sc)
}
}
a.cfg.Provider.Schemes = newSchemes
existing := a.cfg.Provider.Users
filtered := make([]tuicfg.User, 0, len(existing))
for _, u := range existing {
if u.Scheme != name {
filtered = append(filtered, u)
}
}
a.cfg.Provider.Users = filtered
a.save()
a.refreshModelCache(rebuild)
})
return nil
}
return event
})
return a.buildShell(
"schemes",
table,
" [#00f0ff]a:[-] add [#00f0ff]e:[-] edit [#ff2a2a]d:[-] delete [#39ff14]Enter:[-] open [#ff00ff]ESC:[-] back ",
)
}
func (a *App) showSchemeForm(existing *tuicfg.Scheme, onSave func(tuicfg.Scheme)) {
name := ""
baseURL := ""
schemeType := "openai-compatible"
title := " ADD SCHEME "
if existing != nil {
name = existing.Name
baseURL = existing.BaseURL
schemeType = existing.Type
title = " EDIT SCHEME "
}
typeOptions := []string{"openai-compatible", "anthropic"}
typeIdx := 0
for i, t := range typeOptions {
if t == schemeType {
typeIdx = i
break
}
}
form := tview.NewForm()
form.
AddInputField("Name", name, 20, nil, func(text string) { name = text }).
AddInputField("Base URL", baseURL, 28, nil, func(text string) { baseURL = text }).
AddDropDown("Type", typeOptions, typeIdx, func(option string, _ int) { schemeType = option }).
AddButton("SAVE", func() {
if name == "" {
a.showError("Name is required")
return
}
if baseURL == "" {
a.showError("Base URL is required")
return
}
if existing == nil {
for _, s := range a.cfg.Provider.Schemes {
if s.Name == name {
a.showError(fmt.Sprintf("Scheme name %q already exists", name))
return
}
}
}
a.hideModal("scheme-form")
onSave(tuicfg.Scheme{Name: name, BaseURL: baseURL, Type: schemeType})
}).
AddButton("CANCEL", func() {
a.hideModal("scheme-form")
})
form.SetBorder(true).
SetTitle(" [::b]" + title + " ").
SetTitleColor(tcell.NewHexColor(0x39ff14)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
form.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e))
form.SetFieldBackgroundColor(tcell.NewHexColor(0x050510))
form.SetFieldTextColor(tcell.NewHexColor(0x00f0ff))
form.SetLabelColor(tcell.NewHexColor(0xe0e0e0))
form.SetButtonBackgroundColor(tcell.NewHexColor(0xff00ff))
form.SetButtonTextColor(tcell.NewHexColor(0xffffff))
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
a.hideModal("scheme-form")
return nil
}
return event
})
a.showModal("scheme-form", centeredForm(form, 4, 12))
}
+261
View File
@@ -0,0 +1,261 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
)
func (a *App) newUsersPage(schemeName string) tview.Primitive {
table := tview.NewTable().
SetBorders(false).
SetSelectable(true, false)
table.SetBorder(true).
SetTitle(fmt.Sprintf(" [#00f0ff::b] USERS · %s ", schemeName)).
SetTitleColor(tcell.NewHexColor(0x00f0ff)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
table.SetSelectedStyle(
tcell.StyleDefault.Background(tcell.NewHexColor(0xff00ff)).Foreground(tcell.NewHexColor(0xffffff)),
)
table.SetBackgroundColor(tcell.NewHexColor(0x050510))
visibleUsers := func() []tuicfg.User {
var out []tuicfg.User
for _, u := range a.cfg.Provider.Users {
if u.Scheme == schemeName {
out = append(out, u)
}
}
return out
}
findUserGlobalIdx := func(userName string) int {
for i, u := range a.cfg.Provider.Users {
if u.Scheme == schemeName && u.Name == userName {
return i
}
}
return -1
}
rowToVisIdx := func(row int) int { return row / 2 }
selectedUserName := func() string {
row, _ := table.GetSelection()
users := visibleUsers()
visIdx := rowToVisIdx(row)
if visIdx >= 0 && visIdx < len(users) {
return users[visIdx].Name
}
return ""
}
rebuild := func() {
selName := selectedUserName()
table.Clear()
users := visibleUsers()
for i, u := range users {
nameRow := i * 2
detailRow := nameRow + 1
table.SetCell(nameRow, 0,
tview.NewTableCell(" "+u.Name).
SetTextColor(tcell.NewHexColor(0xe0e0e0)).
SetExpansion(1).
SetSelectable(true),
)
table.SetCell(nameRow, 1,
tview.NewTableCell("").
SetSelectable(false),
)
models := a.cachedModels(schemeName, u.Name)
var detailText string
if len(models) > 0 {
detailText = fmt.Sprintf(" [#39ff14]%d models available[-]", len(models))
} else {
detailText = " [#ff2a2a]Inactive / No Access[-]"
}
table.SetCell(detailRow, 0,
tview.NewTableCell(detailText).
SetTextColor(tcell.NewHexColor(0x808080)).
SetExpansion(1).
SetSelectable(false),
)
table.SetCell(detailRow, 1,
tview.NewTableCell("[#00f0ff]"+u.Type+" ").
SetAlign(tview.AlignRight).
SetSelectable(false),
)
}
if selName != "" {
for i, u := range users {
if u.Name == selName {
table.Select(i*2, 0)
return
}
}
}
if table.GetRowCount() > 0 {
table.Select(0, 0)
}
}
rebuild()
a.refreshModelCache(rebuild)
a.pageRefreshFns["users"] = func() { a.refreshModelCache(rebuild) }
table.SetSelectedFunc(func(row, _ int) {
visIdx := rowToVisIdx(row)
users := visibleUsers()
if visIdx < 0 || visIdx >= len(users) {
return
}
uName := users[visIdx].Name
scheme := a.cfg.Provider.SchemeByName(schemeName)
if scheme == nil {
a.showError(fmt.Sprintf("Scheme %q not found", schemeName))
return
}
a.navigateTo("models", a.newModelsPage(schemeName, uName, scheme.BaseURL))
})
table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
row, _ := table.GetSelection()
visIdx := rowToVisIdx(row)
users := visibleUsers()
switch event.Rune() {
case 'a':
a.showUserForm(schemeName, nil, func(u tuicfg.User) {
a.cfg.Provider.Users = append(a.cfg.Provider.Users, u)
a.save()
a.refreshModelCache(rebuild)
})
return nil
case 'e':
if visIdx < 0 || visIdx >= len(users) {
return nil
}
origName := users[visIdx].Name
orig := a.cfg.Provider.Users[findUserGlobalIdx(origName)]
a.showUserForm(schemeName, &orig, func(u tuicfg.User) {
cfgIdx := findUserGlobalIdx(origName)
if cfgIdx < 0 {
a.showError(fmt.Sprintf("User %q no longer exists", origName))
return
}
a.cfg.Provider.Users[cfgIdx] = u
a.save()
a.refreshModelCache(func() {
rebuild()
for i, usr := range visibleUsers() {
if usr.Name == u.Name {
table.Select(i*2, 0)
break
}
}
})
})
return nil
case 'd':
if visIdx < 0 || visIdx >= len(users) {
return nil
}
uName := users[visIdx].Name
a.confirmDelete(fmt.Sprintf("user %q", uName), func() {
cfgIdx := findUserGlobalIdx(uName)
if cfgIdx < 0 {
return
}
all := a.cfg.Provider.Users
a.cfg.Provider.Users = append(all[:cfgIdx], all[cfgIdx+1:]...)
a.save()
a.refreshModelCache(rebuild)
})
return nil
}
return event
})
return a.buildShell(
"users",
table,
" [#00f0ff]a:[-] add [#00f0ff]e:[-] edit [#ff2a2a]d:[-] delete [#39ff14]Enter:[-] models [#ff00ff]ESC:[-] back ",
)
}
func (a *App) showUserForm(schemeName string, existing *tuicfg.User, onSave func(tuicfg.User)) {
name := ""
userType := "key"
key := ""
title := " ADD USER "
if existing != nil {
name = existing.Name
userType = existing.Type
key = existing.Key
title = " EDIT USER "
}
typeOptions := []string{"key", "OAuth"}
typeIdx := 0
for i, t := range typeOptions {
if t == userType {
typeIdx = i
break
}
}
form := tview.NewForm()
form.
AddInputField("Name", name, 20, nil, func(text string) { name = text }).
AddDropDown("Type", typeOptions, typeIdx, func(option string, _ int) { userType = option }).
AddPasswordField("Key", key, 28, '*', func(text string) { key = text }).
AddButton("SAVE", func() {
if name == "" {
a.showError("Name is required")
return
}
if existing == nil {
for _, u := range a.cfg.Provider.Users {
if u.Scheme == schemeName && u.Name == name {
a.showError(fmt.Sprintf("User name %q already exists for this scheme", name))
return
}
}
}
a.hideModal("user-form")
onSave(tuicfg.User{Name: name, Scheme: schemeName, Type: userType, Key: key})
}).
AddButton("CANCEL", func() {
a.hideModal("user-form")
})
form.SetBorder(true).
SetTitle(" [::b]" + title + " ").
SetTitleColor(tcell.NewHexColor(0x39ff14)).
SetBorderColor(tcell.NewHexColor(0x00f0ff))
form.SetBackgroundColor(tcell.NewHexColor(0x1a1a2e))
form.SetFieldBackgroundColor(tcell.NewHexColor(0x050510))
form.SetFieldTextColor(tcell.NewHexColor(0x00f0ff))
form.SetLabelColor(tcell.NewHexColor(0xe0e0e0))
form.SetButtonBackgroundColor(tcell.NewHexColor(0xff00ff))
form.SetButtonTextColor(tcell.NewHexColor(0xffffff))
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
a.hideModal("user-form")
return nil
}
return event
})
a.showModal("user-form", centeredForm(form, 4, 13))
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"context"
"net"
"net/http"
"os"
"strings"
"sync/atomic"
"time"
)
func init() {
// 仅在 /etc/resolv.conf 不存在时才覆盖(即 Android 环境)
if _, err := os.Stat("/etc/resolv.conf"); err == nil {
return
}
// 从环境变量获取 DNS server 列表,多个用 ; 隔开
// 例如: PICOCLAW_DNS_SERVER="8.8.8.8:53;1.1.1.1:53;223.5.5.5:53"
dnsEnv := os.Getenv("PICOCLAW_DNS_SERVER")
if dnsEnv == "" {
dnsEnv = "8.8.8.8:53;1.1.1.1:53"
}
var dnsServers []string
for _, s := range strings.Split(dnsEnv, ";") {
s = strings.TrimSpace(s)
if s != "" {
// 如果没有带端口号,自动补上 :53
if _, _, err := net.SplitHostPort(s); err != nil {
s = s + ":53"
}
dnsServers = append(dnsServers, s)
}
}
// 轮询索引,在多个 DNS 服务器之间轮转
var idx uint64
customResolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 5 * time.Second}
// Round-robin: 依次尝试不同的 DNS 服务器
server := dnsServers[atomic.AddUint64(&idx, 1)%uint64(len(dnsServers))]
return d.DialContext(ctx, "udp", server)
},
}
// 覆盖全局 DefaultResolver
net.DefaultResolver = customResolver
// 覆盖 http.DefaultTransport 使用自定义 DNS 解析的 DialContext
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Resolver: customResolver,
}
if tr, ok := http.DefaultTransport.(*http.Transport); ok {
tr.DialContext = dialer.DialContext
}
}
+30 -2
View File
@@ -8,7 +8,11 @@
"temperature": 0.7,
"max_tool_iterations": 20,
"summarize_message_threshold": 20,
"summarize_token_percent": 75
"summarize_token_percent": 75,
"tool_feedback": {
"enabled": false,
"max_args_length": 300
}
}
},
"model_list": [
@@ -80,7 +84,10 @@
"proxy": "",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": false,
"reasoning_channel_id": ""
"reasoning_channel_id": "",
"streaming": {
"enabled": true
}
},
"discord": {
"enabled": false,
@@ -200,6 +207,8 @@
"wecom_aibot": {
"_comment": "WeCom AI Bot (智能机器人) - Official WeCom AI Bot integration, supports proactive messaging and private chats.",
"enabled": false,
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-aibot",
@@ -207,6 +216,25 @@
"welcome_message": "Hello! I'm your AI assistant. How can I help you today?",
"reasoning_channel_id": ""
},
"pico": {
"enabled": false,
"token": "YOUR_PICO_TOKEN",
"allow_token_query": false,
"allow_origins": [],
"ping_interval": 30,
"read_timeout": 60,
"max_connections": 100,
"allow_from": []
},
"pico_client": {
"enabled": false,
"url": "wss://remote-pico-server/pico/ws",
"token": "YOUR_PICO_TOKEN",
"session_id": "",
"ping_interval": 30,
"read_timeout": 60,
"allow_from": []
},
"irc": {
"enabled": false,
"server": "irc.libera.chat:6697",
+4 -4
View File
@@ -438,7 +438,7 @@ type ProviderAuthResult = {
### 1. Required Environment/Dependencies
- Go ≥ 1.21
- Go ≥ 1.25
- PicoClaw codebase (`pkg/providers/` and `pkg/auth/`)
- `crypto` and `net/http` standard library packages
@@ -584,7 +584,7 @@ Each SSE message (`data: {...}`) is wrapped in a `response` field:
],
"agents": {
"defaults": {
"model": "gemini-flash"
"model_name": "gemini-flash"
}
}
}
@@ -674,7 +674,7 @@ Add a default entry in `pkg/config/defaults.go`:
#### 5. Add Auth Support (Optional)
If your provider requires OAuth or special authentication, add a case to `cmd/picoclaw/cmd_auth.go`:
If your provider requires OAuth or special authentication, add a case to `cmd/picoclaw/internal/auth/helpers.go`:
```go
case "your-provider":
@@ -736,7 +736,7 @@ export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/m
- `pkg/auth/store.go` - Auth credential storage (`~/.picoclaw/auth.json`)
- `pkg/providers/factory.go` - Provider factory and protocol routing
- `pkg/providers/types.go` - Provider interface definitions
- `cmd/picoclaw/cmd_auth.go` - Auth CLI commands
- `cmd/picoclaw/internal/auth/helpers.go` - Auth CLI commands
- **Documentation:**
- `docs/ANTIGRAVITY_USAGE.md` - Antigravity usage guide
+35
View File
@@ -0,0 +1,35 @@
> Retour au [README](../../../README.fr.md)
# DingTalk
DingTalk est la plateforme de communication d'entreprise d'Alibaba, très populaire dans les milieux professionnels chinois. Elle utilise un SDK de streaming pour maintenir des connexions persistantes.
## Configuration
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| ------------- | ------ | ------ | ---------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal DingTalk |
| client_id | string | Oui | Client ID de l'application DingTalk |
| client_secret | string | Oui | Client Secret de l'application DingTalk |
| allow_from | array | Non | Liste blanche d'ID utilisateurs ; vide signifie tous les utilisateurs |
## Procédure de configuration
1. Rendez-vous sur la [plateforme ouverte DingTalk](https://open.dingtalk.com/)
2. Créez une application interne d'entreprise
3. Obtenez le Client ID et le Client Secret depuis les paramètres de l'application
4. Configurez OAuth et les abonnements aux événements (si nécessaire)
5. Renseignez le Client ID et le Client Secret dans le fichier de configuration
+35
View File
@@ -0,0 +1,35 @@
> [README](../../../README.ja.md) に戻る
# DingTalk
DingTalkはアリババの企業向けコミュニケーションプラットフォームで、中国のビジネス環境で広く利用されています。ストリーミング SDK を使用して持続的な接続を維持します。
## 設定
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ------------- | ------ | ---- | -------------------------------------------- |
| enabled | bool | はい | DingTalk チャンネルを有効にするかどうか |
| client_id | string | はい | DingTalk アプリケーションの Client ID |
| client_secret | string | はい | DingTalk アプリケーションの Client Secret |
| allow_from | array | いいえ | ユーザーIDのホワイトリスト。空の場合は全ユーザーを許可 |
## セットアップ手順
1. [DingTalk オープンプラットフォーム](https://open.dingtalk.com/) にアクセスする
2. 企業内部アプリケーションを作成する
3. アプリケーション設定から Client ID と Client Secret を取得する
4. OAuth とイベントサブスクリプションを設定する(必要な場合)
5. Client ID と Client Secret を設定ファイルに入力する
+35
View File
@@ -0,0 +1,35 @@
> Back to [README](../../../README.md)
# DingTalk
DingTalk is Alibaba's enterprise communication platform, widely used in Chinese workplaces. It uses a streaming SDK to maintain persistent connections.
## Configuration
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------- |
| enabled | bool | Yes | Whether to enable the DingTalk channel |
| client_id | string | Yes | Client ID of the DingTalk application |
| client_secret | string | Yes | Client Secret of the DingTalk application |
| allow_from | array | No | User ID whitelist; empty means all users are allowed |
## Setup
1. Go to the [DingTalk Open Platform](https://open.dingtalk.com/)
2. Create an internal enterprise application
3. Obtain the Client ID and Client Secret from the application settings
4. Configure OAuth and event subscriptions (if needed)
5. Fill in the Client ID and Client Secret in the configuration file
+35
View File
@@ -0,0 +1,35 @@
> Voltar ao [README](../../../README.pt-br.md)
# DingTalk
DingTalk é a plataforma de comunicação empresarial da Alibaba, amplamente utilizada no ambiente corporativo chinês. Ela usa um SDK de streaming para manter conexões persistentes.
## Configuração
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ------------- | ------ | ----------- | ---------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal DingTalk deve ser habilitado |
| client_id | string | Sim | Client ID do aplicativo DingTalk |
| client_secret | string | Sim | Client Secret do aplicativo DingTalk |
| allow_from | array | Não | Lista de permissão de IDs de usuário; vazio permite todos |
## Configuração passo a passo
1. Acesse a [Plataforma Aberta DingTalk](https://open.dingtalk.com/)
2. Crie um aplicativo interno corporativo
3. Obtenha o Client ID e o Client Secret nas configurações do aplicativo
4. Configure OAuth e assinaturas de eventos (se necessário)
5. Preencha o Client ID e o Client Secret no arquivo de configuração
+35
View File
@@ -0,0 +1,35 @@
> Quay lại [README](../../../README.vi.md)
# DingTalk
DingTalk là nền tảng giao tiếp doanh nghiệp của Alibaba, được sử dụng rộng rãi trong môi trường làm việc tại Trung Quốc. Nền tảng này sử dụng SDK streaming để duy trì kết nối liên tục.
## Cấu hình
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ------------- | ------ | -------- | ---------------------------------------------------------------- |
| enabled | bool | Có | Có bật kênh DingTalk hay không |
| client_id | string | Có | Client ID của ứng dụng DingTalk |
| client_secret | string | Có | Client Secret của ứng dụng DingTalk |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống cho phép tất cả |
## Quy trình thiết lập
1. Truy cập [Nền tảng mở DingTalk](https://open.dingtalk.com/)
2. Tạo một ứng dụng nội bộ doanh nghiệp
3. Lấy Client ID và Client Secret từ cài đặt ứng dụng
4. Cấu hình OAuth và đăng ký sự kiện (nếu cần)
5. Điền Client ID và Client Secret vào file cấu hình
+2
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# 钉钉
钉钉是阿里巴巴的企业通讯平台,在中国职场中广受欢迎。它采用流式 SDK 来维持持久连接。
+39
View File
@@ -0,0 +1,39 @@
> Retour au [README](../../../README.fr.md)
# Discord
Discord est une application gratuite de chat vocal, vidéo et textuel conçue pour les communautés. PicoClaw se connecte aux serveurs Discord via l'API Bot Discord, avec prise en charge de la réception et de l'envoi de messages.
## Configuration
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {
"mention_only": false
}
}
}
}
```
| Champ | Type | Requis | Description |
| ------------- | ------ | ------ | --------------------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal Discord |
| token | string | Oui | Token du bot Discord |
| allow_from | array | Non | Liste blanche d'identifiants utilisateur ; vide signifie tous les utilisateurs |
| group_trigger | object | Non | Paramètres de déclenchement de groupe (exemple : { "mention_only": false }) |
## Configuration initiale
1. Accéder au [Portail des développeurs Discord](https://discord.com/developers/applications) et créer une nouvelle application
2. Activer les Intents :
- Message Content Intent
- Server Members Intent
3. Obtenir le Token du bot
4. Renseigner le Token du bot dans le fichier de configuration
5. Inviter le bot sur le serveur et lui accorder les permissions nécessaires (ex. envoyer des messages, lire l'historique des messages)
+39
View File
@@ -0,0 +1,39 @@
> [README](../../../README.ja.md) に戻る
# Discord
Discord はコミュニティ向けに設計された無料の音声・ビデオ・テキストチャットアプリケーションです。PicoClaw は Discord Bot API を通じて Discord サーバーに接続し、メッセージの受信と送信をサポートします。
## 設定
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {
"mention_only": false
}
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ------------- | ------ | ------ | ----------------------------------------------------------------- |
| enabled | bool | はい | Discord チャンネルを有効にするかどうか |
| token | string | はい | Discord ボットトークン |
| allow_from | array | いいえ | 許可するユーザーIDのリスト。空の場合はすべてのユーザーを許可 |
| group_trigger | object | いいえ | グループトリガー設定(例: { "mention_only": false } |
## セットアップ手順
1. [Discord 開発者ポータル](https://discord.com/developers/applications) にアクセスして新しいアプリケーションを作成する
2. Intents を有効にする:
- Message Content Intent
- Server Members Intent
3. Bot トークンを取得する
4. 設定ファイルに Bot トークンを入力する
5. ボットをサーバーに招待し、必要な権限を付与する(例: メッセージの送信、メッセージ履歴の読み取りなど)
+39
View File
@@ -0,0 +1,39 @@
> Back to [README](../../../README.md)
# Discord
Discord is a free voice, video, and text chat application designed for communities. PicoClaw connects to Discord servers via the Discord Bot API, supporting both receiving and sending messages.
## Configuration
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {
"mention_only": false
}
}
}
}
```
| Field | Type | Required | Description |
| ------------- | ------ | -------- | --------------------------------------------------------------------------- |
| enabled | bool | Yes | Whether to enable the Discord channel |
| token | string | Yes | Discord Bot Token |
| allow_from | array | No | Allowlist of user IDs; empty means all users are allowed |
| group_trigger | object | No | Group trigger settings (example: { "mention_only": false }) |
## Setup
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) and create a new application
2. Enable Intents:
- Message Content Intent
- Server Members Intent
3. Obtain the Bot Token
4. Fill in the Bot Token in the configuration file
5. Invite the bot to your server and grant the necessary permissions (e.g. Send Messages, Read Message History)
+39
View File
@@ -0,0 +1,39 @@
> Voltar ao [README](../../../README.pt-br.md)
# Discord
Discord é um aplicativo gratuito de chat de voz, vídeo e texto projetado para comunidades. O PicoClaw se conecta a servidores Discord via Discord Bot API, com suporte para receber e enviar mensagens.
## Configuração
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {
"mention_only": false
}
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ------------- | ------ | ----------- | --------------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal Discord deve ser habilitado |
| token | string | Sim | Token do Bot Discord |
| allow_from | array | Não | Lista de IDs de usuários permitidos; vazio significa todos os usuários |
| group_trigger | object | Não | Configurações de gatilho de grupo (exemplo: { "mention_only": false }) |
## Configuração inicial
1. Acesse o [Portal de Desenvolvedores do Discord](https://discord.com/developers/applications) e crie uma nova aplicação
2. Habilite os Intents:
- Message Content Intent
- Server Members Intent
3. Obtenha o Token do Bot
4. Preencha o Token do Bot no arquivo de configuração
5. Convide o bot para o servidor e conceda as permissões necessárias (ex. enviar mensagens, ler histórico de mensagens)
+39
View File
@@ -0,0 +1,39 @@
> Quay lại [README](../../../README.vi.md)
# Discord
Discord là ứng dụng chat thoại, video và văn bản miễn phí được thiết kế cho cộng đồng. PicoClaw kết nối với máy chủ Discord qua Discord Bot API, hỗ trợ nhận và gửi tin nhắn.
## Cấu hình
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {
"mention_only": false
}
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ------------- | ------ | -------- | --------------------------------------------------------------------------- |
| enabled | bool | Có | Có bật kênh Discord hay không |
| token | string | Có | Token Bot Discord |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống nghĩa là cho phép tất cả |
| group_trigger | object | Không | Cài đặt kích hoạt nhóm (ví dụ: { "mention_only": false }) |
## Hướng dẫn thiết lập
1. Truy cập [Discord Developer Portal](https://discord.com/developers/applications) và tạo ứng dụng mới
2. Bật các Intents:
- Message Content Intent
- Server Members Intent
3. Lấy Bot Token
4. Điền Bot Token vào file cấu hình
5. Mời bot vào máy chủ và cấp các quyền cần thiết (ví dụ: gửi tin nhắn, đọc lịch sử tin nhắn)
+2
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# Discord
Discord 是一个专为社区设计的免费语音、视频和文本聊天应用。PicoClaw 通过 Discord Bot API 连接到 Discord 服务器,支持接收和发送消息。
+52
View File
@@ -0,0 +1,52 @@
> Retour au [README](../../../README.fr.md)
# Feishu
Feishu (nom international : Lark) est une plateforme de collaboration d'entreprise de ByteDance. Elle prend en charge les marchés chinois et mondiaux via des connexions WebSocket pilotées par événements.
## Configuration
```json
{
"channels": {
"feishu": {
"enabled": true,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| --------------------- | ------ | ------ | --------------------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal Feishu |
| app_id | string | Oui | App ID de l'application Feishu (commence par `cli_`) |
| app_secret | string | Oui | App Secret de l'application Feishu |
| encrypt_key | string | Non | Clé de chiffrement pour les callbacks d'événements |
| verification_token | string | Non | Token utilisé pour la vérification des événements Webhook |
| allow_from | array | Non | Liste blanche d'identifiants utilisateur ; vide signifie tous les utilisateurs |
| random_reaction_emoji | array | Non | Liste d'emojis de réaction aléatoires ; vide utilise le "Pin" par défaut |
## Configuration initiale
1. Accéder à la [plateforme ouverte Feishu](https://open.feishu.cn/) et créer une application
2. Activer la capacité **Bot** dans les paramètres de l'application
3. Créer une version et publier l'application (la configuration prend effet après la publication)
4. Obtenir l'**App ID** (commence par `cli_`) et l'**App Secret**
5. Renseigner l'App ID et l'App Secret dans le fichier de configuration PicoClaw
6. Exécuter `picoclaw gateway` pour démarrer le service
7. Rechercher le nom du bot dans Feishu et commencer une conversation
> PicoClaw se connecte à Feishu en mode WebSocket/SDK — aucune adresse de callback publique ni URL Webhook n'est requise.
>
> `encrypt_key` et `verification_token` sont optionnels ; l'activation du chiffrement des événements est recommandée pour les environnements de production.
>
> Pour les références d'emojis personnalisés, voir : [Liste des emojis Feishu](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)
## Limitations de plateforme
> ⚠️ **Le canal Feishu ne prend pas en charge les appareils 32 bits.** Le SDK Feishu ne fournit que des builds 64 bits. Les architectures 32 bits (armv6, armv7, mipsle, etc.) ne peuvent pas utiliser le canal Feishu. Pour la messagerie sur des appareils 32 bits, utilisez Telegram, Discord ou OneBot.
+52
View File
@@ -0,0 +1,52 @@
> [README](../../../README.ja.md) に戻る
# 飛書(Feishu
飛書(国際名:Lark)は ByteDance が提供するエンタープライズコラボレーションプラットフォームです。イベント駆動型の WebSocket 接続を通じて、中国および世界市場の両方をサポートします。
## 設定
```json
{
"channels": {
"feishu": {
"enabled": true,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| --------------------- | ------ | ------ | ----------------------------------------------------------------- |
| enabled | bool | はい | 飛書チャンネルを有効にするかどうか |
| app_id | string | はい | 飛書アプリケーションの App ID(`cli_` で始まる) |
| app_secret | string | はい | 飛書アプリケーションの App Secret |
| encrypt_key | string | いいえ | イベントコールバックの暗号化キー |
| verification_token | string | いいえ | Webhook イベント検証に使用するトークン |
| allow_from | array | いいえ | 許可するユーザーIDのリスト。空の場合はすべてのユーザーを許可 |
| random_reaction_emoji | array | いいえ | ランダムに追加する絵文字のリスト。空の場合はデフォルトの "Pin" を使用 |
## セットアップ手順
1. [飛書オープンプラットフォーム](https://open.feishu.cn/) にアクセスしてアプリケーションを作成する
2. アプリケーション設定で**ボット**機能を有効にする
3. バージョンを作成してアプリケーションを公開する(公開後に設定が有効になる)
4. **App ID**`cli_` で始まる)と **App Secret** を取得する
5. PicoClaw 設定ファイルに App ID と App Secret を入力する
6. `picoclaw gateway` を実行してサービスを起動する
7. 飛書でボット名を検索して会話を始める
> PicoClaw は WebSocket/SDK モードで飛書に接続するため、公開コールバックアドレスや Webhook URL の設定は不要です。
>
> `encrypt_key``verification_token` はオプションですが、本番環境ではイベント暗号化を有効にすることを推奨します。
>
> カスタム絵文字の参考:[飛書絵文字リスト](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)
## プラットフォーム制限
> ⚠️ **飛書チャネルは 32 ビットデバイスをサポートしていません。** 飛書 SDK は 64 ビットビルドのみ提供しています。armv6 / armv7 / mipsle などの 32 ビットアーキテクチャでは飛書チャネルを使用できません。32 ビットデバイスでのメッセージングには、Telegram、Discord、または OneBot をご利用ください。
+52
View File
@@ -0,0 +1,52 @@
> Back to [README](../../../README.md)
# Feishu
Feishu (international name: Lark) is an enterprise collaboration platform by ByteDance. It supports both Chinese and global markets through event-driven WebSocket connections.
## Configuration
```json
{
"channels": {
"feishu": {
"enabled": true,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------ |
| enabled | bool | Yes | Whether to enable the Feishu channel |
| app_id | string | Yes | App ID of the Feishu application (starts with `cli_`) |
| app_secret | string | Yes | App Secret of the Feishu application |
| encrypt_key | string | No | Encryption key for event callbacks |
| verification_token | string | No | Token used for Webhook event verification |
| allow_from | array | No | Allowlist of user IDs; empty means all users are allowed |
| random_reaction_emoji | array | No | List of random reaction emojis; empty uses the default "Pin" |
## Setup
1. Go to the [Feishu Open Platform](https://open.feishu.cn/) and create an application
2. Enable the **Bot** capability in the application settings
3. Create a version and publish the application (configuration takes effect only after publishing)
4. Obtain the **App ID** (starts with `cli_`) and **App Secret**
5. Fill in the App ID and App Secret in the PicoClaw configuration file
6. Run `picoclaw gateway` to start the service
7. Search for the bot name in Feishu and start a conversation
> PicoClaw connects to Feishu using WebSocket/SDK mode — no public callback address or Webhook URL is required.
>
> `encrypt_key` and `verification_token` are optional; enabling event encryption is recommended for production environments.
>
> For custom emoji references, see: [Feishu Emoji List](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)
## Platform Limitations
> ⚠️ **Feishu channel does not support 32-bit devices.** The Feishu SDK only provides 64-bit builds. Devices running armv6, armv7, mipsle, or other 32-bit architectures cannot use the Feishu channel. For messaging on 32-bit devices, use Telegram, Discord, or OneBot instead.
+52
View File
@@ -0,0 +1,52 @@
> Voltar ao [README](../../../README.pt-br.md)
# Feishu
Feishu (nome internacional: Lark) é uma plataforma de colaboração empresarial da ByteDance. Suporta os mercados chinês e global por meio de conexões WebSocket orientadas a eventos.
## Configuração
```json
{
"channels": {
"feishu": {
"enabled": true,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| --------------------- | ------ | ----------- | -------------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal Feishu deve ser habilitado |
| app_id | string | Sim | App ID da aplicação Feishu (começa com `cli_`) |
| app_secret | string | Sim | App Secret da aplicação Feishu |
| encrypt_key | string | Não | Chave de criptografia para callbacks de eventos |
| verification_token | string | Não | Token usado para verificação de eventos Webhook |
| allow_from | array | Não | Lista de IDs de usuários permitidos; vazio significa todos os usuários |
| random_reaction_emoji | array | Não | Lista de emojis de reação aleatórios; vazio usa o "Pin" padrão |
## Configuração inicial
1. Acesse a [Plataforma Aberta Feishu](https://open.feishu.cn/) e crie uma aplicação
2. Habilite a capacidade de **Bot** nas configurações da aplicação
3. Crie uma versão e publique a aplicação (a configuração entra em vigor após a publicação)
4. Obtenha o **App ID** (começa com `cli_`) e o **App Secret**
5. Preencha o App ID e o App Secret no arquivo de configuração do PicoClaw
6. Execute `picoclaw gateway` para iniciar o serviço
7. Pesquise o nome do bot no Feishu e inicie uma conversa
> O PicoClaw se conecta ao Feishu usando o modo WebSocket/SDK — nenhum endereço de callback público ou URL de Webhook é necessário.
>
> `encrypt_key` e `verification_token` são opcionais; recomenda-se habilitar a criptografia de eventos em ambientes de produção.
>
> Para referências de emojis personalizados, consulte: [Lista de Emojis do Feishu](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)
## Limitações de Plataforma
> ⚠️ **O canal Feishu não suporta dispositivos 32 bits.** O SDK do Feishu fornece apenas builds 64 bits. Arquiteturas 32 bits (armv6, armv7, mipsle, etc.) não podem usar o canal Feishu. Para mensagens em dispositivos 32 bits, use Telegram, Discord ou OneBot.
+52
View File
@@ -0,0 +1,52 @@
> Quay lại [README](../../../README.vi.md)
# Feishu
Feishu (tên quốc tế: Lark) là nền tảng cộng tác doanh nghiệp của ByteDance. Hỗ trợ cả thị trường Trung Quốc và toàn cầu thông qua kết nối WebSocket theo hướng sự kiện.
## Cấu hình
```json
{
"channels": {
"feishu": {
"enabled": true,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| --------------------- | ------ | -------- | ------------------------------------------------------------------------ |
| enabled | bool | Có | Có bật kênh Feishu hay không |
| app_id | string | Có | App ID của ứng dụng Feishu (bắt đầu bằng `cli_`) |
| app_secret | string | Có | App Secret của ứng dụng Feishu |
| encrypt_key | string | Không | Khóa mã hóa cho callback sự kiện |
| verification_token | string | Không | Token dùng để xác minh sự kiện Webhook |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống nghĩa là cho phép tất cả |
| random_reaction_emoji | array | Không | Danh sách emoji phản ứng ngẫu nhiên; để trống dùng "Pin" mặc định |
## Hướng dẫn thiết lập
1. Truy cập [Nền tảng Mở Feishu](https://open.feishu.cn/) và tạo ứng dụng
2. Bật khả năng **Bot** trong cài đặt ứng dụng
3. Tạo phiên bản và xuất bản ứng dụng (cấu hình có hiệu lực sau khi xuất bản)
4. Lấy **App ID** (bắt đầu bằng `cli_`) và **App Secret**
5. Điền App ID và App Secret vào file cấu hình PicoClaw
6. Chạy `picoclaw gateway` để khởi động dịch vụ
7. Tìm kiếm tên bot trong Feishu và bắt đầu trò chuyện
> PicoClaw kết nối với Feishu bằng chế độ WebSocket/SDK — không cần cấu hình địa chỉ callback công khai hay Webhook URL.
>
> `encrypt_key``verification_token` là tùy chọn; nên bật mã hóa sự kiện trong môi trường sản xuất.
>
> Tham khảo emoji tùy chỉnh: [Danh sách Emoji Feishu](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)
## Giới hạn nền tảng
> ⚠️ **Kênh Feishu không hỗ trợ thiết bị 32 bit.** SDK Feishu chỉ cung cấp bản build 64 bit. Các kiến trúc 32 bit (armv6, armv7, mipsle, v.v.) không thể sử dụng kênh Feishu. Để nhắn tin trên thiết bị 32 bit, hãy dùng Telegram, Discord hoặc OneBot.
+6
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# 飞书
飞书(国际版名称:Lark)是字节跳动旗下的企业协作平台。它通过事件驱动的 Webhook 同时支持中国和全球市场。
@@ -39,3 +41,7 @@
4. 设置加密(可选,生产环境建议启用)
5. 将 App ID、App Secret、Encrypt Key 和 Verification Token(如果启用加密) 填入配置文件中
6. 自定义你希望 PicoClaw react 你消息时的表情(可选, Reference URL: [Feishu Emoji List](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce))
## 平台限制
> ⚠️ **飞书通道不支持 32 位设备。** 飞书官方 SDK 仅提供 64 位构建,armv6 / armv7 / mipsle 等 32 位架构无法使用飞书通道。如需在 32 位设备上接入即时通讯,请改用 Telegram、Discord 或 OneBot 等通道。
+40
View File
@@ -0,0 +1,40 @@
> Retour au [README](../../../README.fr.md)
# Line
PicoClaw prend en charge LINE via l'API LINE Messaging avec des callbacks webhook.
## Configuration
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| -------------------- | ------ | ------ | ------------------------------------------------------------------------ |
| enabled | bool | Oui | Activer ou non le canal LINE |
| channel_secret | string | Oui | Channel Secret de l'API LINE Messaging |
| channel_access_token | string | Oui | Channel Access Token de l'API LINE Messaging |
| webhook_path | string | Non | Chemin du webhook (par défaut : /webhook/line) |
| allow_from | array | Non | Liste blanche d'ID utilisateurs ; vide signifie tous les utilisateurs |
## Procédure de configuration
1. Rendez-vous sur la [LINE Developers Console](https://developers.line.biz/console/) et créez un fournisseur de services ainsi qu'un canal Messaging API
2. Obtenez le Channel Secret et le Channel Access Token
3. Configurez le webhook :
- LINE exige que les webhooks utilisent HTTPS. Vous devez donc déployer un serveur compatible HTTPS ou utiliser un outil de proxy inverse comme ngrok pour exposer votre serveur local sur Internet
- PicoClaw utilise un serveur HTTP Gateway partagé pour recevoir les callbacks webhook de tous les canaux, écoutant par défaut sur 127.0.0.1:18790
- Définissez l'URL du webhook sur `https://your-domain.com/webhook/line`, puis configurez un proxy inverse de votre domaine externe vers le Gateway local (port par défaut 18790)
- Activez le webhook et vérifiez l'URL
4. Renseignez le Channel Secret et le Channel Access Token dans le fichier de configuration
+40
View File
@@ -0,0 +1,40 @@
> [README](../../../README.ja.md) に戻る
# Line
PicoClaw は LINE Messaging API と Webhook コールバックを通じて LINE をサポートします。
## 設定
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| -------------------- | ------ | ------ | ------------------------------------------------------------------ |
| enabled | bool | はい | LINE チャンネルを有効にするかどうか |
| channel_secret | string | はい | LINE Messaging API の Channel Secret |
| channel_access_token | string | はい | LINE Messaging API の Channel Access Token |
| webhook_path | string | いいえ | Webhook のパス(デフォルト: /webhook/line |
| allow_from | array | いいえ | ユーザーIDのホワイトリスト。空の場合は全ユーザーを許可 |
## セットアップ手順
1. [LINE Developers Console](https://developers.line.biz/console/) にアクセスし、サービスプロバイダーと Messaging API チャンネルを作成する
2. Channel Secret と Channel Access Token を取得する
3. Webhook を設定する:
- LINE は Webhook に HTTPS が必要なため、HTTPS 対応サーバーをデプロイするか、ngrok などのリバースプロキシツールを使用してローカルサーバーをインターネットに公開する必要があります
- PicoClaw は共有の Gateway HTTP サーバーを使用してすべてのチャンネルの Webhook コールバックを受信します。デフォルトのリッスンアドレスは 127.0.0.1:18790 です
- Webhook URL を `https://your-domain.com/webhook/line` に設定し、外部ドメインをローカルの Gateway(デフォルトポート 18790)にリバースプロキシする
- Webhook を有効にして URL を検証する
4. Channel Secret と Channel Access Token を設定ファイルに入力する
+40
View File
@@ -0,0 +1,40 @@
> Back to [README](../../../README.md)
# Line
PicoClaw supports LINE through the LINE Messaging API with webhook callbacks.
## Configuration
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| -------------------- | ------ | -------- | ------------------------------------------------------------------ |
| enabled | bool | Yes | Whether to enable the LINE channel |
| channel_secret | string | Yes | Channel Secret for the LINE Messaging API |
| channel_access_token | string | Yes | Channel Access Token for the LINE Messaging API |
| webhook_path | string | No | Webhook path (default: /webhook/line) |
| allow_from | array | No | User ID whitelist; empty means all users are allowed |
## Setup
1. Go to the [LINE Developers Console](https://developers.line.biz/console/) and create a provider and a Messaging API channel
2. Obtain the Channel Secret and Channel Access Token
3. Configure the webhook:
- LINE requires webhooks to use HTTPS, so you need to deploy a server with HTTPS support, or use a reverse proxy tool like ngrok to expose your local server to the internet
- PicoClaw uses a shared Gateway HTTP server to receive webhook callbacks for all channels, listening on 127.0.0.1:18790 by default
- Set the Webhook URL to `https://your-domain.com/webhook/line`, then reverse-proxy your external domain to the local Gateway (default port 18790)
- Enable the webhook and verify the URL
4. Fill in the Channel Secret and Channel Access Token in the configuration file
+40
View File
@@ -0,0 +1,40 @@
> Voltar ao [README](../../../README.pt-br.md)
# Line
O PicoClaw suporta o LINE por meio da LINE Messaging API com callbacks de webhook.
## Configuração
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| -------------------- | ------ | ----------- | ---------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal LINE deve ser habilitado |
| channel_secret | string | Sim | Channel Secret da LINE Messaging API |
| channel_access_token | string | Sim | Channel Access Token da LINE Messaging API |
| webhook_path | string | Não | Caminho do webhook (padrão: /webhook/line) |
| allow_from | array | Não | Lista de permissão de IDs de usuário; vazio permite todos |
## Configuração passo a passo
1. Acesse o [LINE Developers Console](https://developers.line.biz/console/) e crie um provedor de serviços e um canal Messaging API
2. Obtenha o Channel Secret e o Channel Access Token
3. Configure o webhook:
- O LINE exige que os webhooks usem HTTPS, portanto é necessário implantar um servidor com suporte a HTTPS ou usar uma ferramenta de proxy reverso como o ngrok para expor seu servidor local à internet
- O PicoClaw usa um servidor HTTP Gateway compartilhado para receber callbacks de webhook de todos os canais, escutando em 127.0.0.1:18790 por padrão
- Defina a URL do webhook como `https://your-domain.com/webhook/line` e configure um proxy reverso do seu domínio externo para o Gateway local (porta padrão 18790)
- Ative o webhook e verifique a URL
4. Preencha o Channel Secret e o Channel Access Token no arquivo de configuração
+40
View File
@@ -0,0 +1,40 @@
> Quay lại [README](../../../README.vi.md)
# Line
PicoClaw hỗ trợ LINE thông qua LINE Messaging API kết hợp với webhook callback.
## Cấu hình
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| -------------------- | ------ | -------- | ---------------------------------------------------------------------- |
| enabled | bool | Có | Có bật kênh LINE hay không |
| channel_secret | string | Có | Channel Secret của LINE Messaging API |
| channel_access_token | string | Có | Channel Access Token của LINE Messaging API |
| webhook_path | string | Không | Đường dẫn webhook (mặc định: /webhook/line) |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống cho phép tất cả |
## Quy trình thiết lập
1. Truy cập [LINE Developers Console](https://developers.line.biz/console/) và tạo một nhà cung cấp dịch vụ cùng một kênh Messaging API
2. Lấy Channel Secret và Channel Access Token
3. Cấu hình webhook:
- LINE yêu cầu webhook phải sử dụng HTTPS, vì vậy bạn cần triển khai máy chủ hỗ trợ HTTPS hoặc dùng công cụ reverse proxy như ngrok để expose máy chủ cục bộ ra internet
- PicoClaw sử dụng máy chủ HTTP Gateway dùng chung để nhận webhook callback cho tất cả các kênh, mặc định lắng nghe tại 127.0.0.1:18790
- Đặt Webhook URL thành `https://your-domain.com/webhook/line`, sau đó reverse proxy tên miền bên ngoài về Gateway cục bộ (cổng mặc định 18790)
- Bật webhook và xác minh URL
4. Điền Channel Secret và Channel Access Token vào file cấu hình
+2
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# Line
PicoClaw 通过 LINE Messaging API 配合 Webhook 回调功能实现对 LINE 的支持。
+35
View File
@@ -0,0 +1,35 @@
> Retour au [README](../../../README.fr.md)
# MaixCam
MaixCam est un canal dédié à la connexion aux caméras AI Sipeed MaixCAM et MaixCAM2. Il utilise des sockets TCP pour une communication bidirectionnelle et prend en charge les scénarios de déploiement d'IA en périphérie.
## Configuration
```json
{
"channels": {
"maixcam": {
"enabled": true,
"host": "0.0.0.0",
"port": 18790,
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| ---------- | ------ | ------ | --------------------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal MaixCam |
| host | string | Oui | Adresse d'écoute du serveur TCP |
| port | int | Oui | Port d'écoute du serveur TCP |
| allow_from | array | Non | Liste blanche d'identifiants d'appareils ; vide signifie tous les appareils |
## Cas d'utilisation
Le canal MaixCam permet à PicoClaw de fonctionner comme backend IA pour les appareils en périphérie :
- **Surveillance intelligente** : MaixCAM envoie des images ; PicoClaw les analyse via des modèles de vision
- **Contrôle IoT** : Les appareils envoient des données de capteurs ; PicoClaw coordonne les réponses
- **IA hors ligne** : Déployer PicoClaw sur un réseau local pour une inférence à faible latence
+35
View File
@@ -0,0 +1,35 @@
> [README](../../../README.ja.md) に戻る
# MaixCam
MaixCam は、Sipeed MaixCAM および MaixCAM2 AI カメラデバイスへの接続専用チャンネルです。TCP ソケットを使用した双方向通信を実装し、エッジ AI デプロイメントシナリオをサポートします。
## 設定
```json
{
"channels": {
"maixcam": {
"enabled": true,
"host": "0.0.0.0",
"port": 18790,
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------- | ------ | ------ | ------------------------------------------------------------- |
| enabled | bool | はい | MaixCam チャンネルを有効にするかどうか |
| host | string | はい | TCP サーバーのリッスンアドレス |
| port | int | はい | TCP サーバーのリッスンポート |
| allow_from | array | いいえ | 許可するデバイスIDのリスト。空の場合はすべてのデバイスを許可 |
## ユースケース
MaixCam チャンネルにより、PicoClaw はエッジデバイスの AI バックエンドとして機能できます:
- **スマート監視**:MaixCAM が画像フレームを送信し、PicoClaw がビジョンモデルで分析する
- **IoT 制御**:デバイスがセンサーデータを送信し、PicoClaw がレスポンスを調整する
- **オフライン AI**:ローカルネットワークに PicoClaw をデプロイして低遅延推論を実現する
+35
View File
@@ -0,0 +1,35 @@
> Back to [README](../../../README.md)
# MaixCam
MaixCam is a dedicated channel for connecting to Sipeed MaixCAM and MaixCAM2 AI camera devices. It uses TCP sockets for bidirectional communication and supports edge AI deployment scenarios.
## Configuration
```json
{
"channels": {
"maixcam": {
"enabled": true,
"host": "0.0.0.0",
"port": 18790,
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ---------------------------------------------------------------- |
| enabled | bool | Yes | Whether to enable the MaixCam channel |
| host | string | Yes | TCP server listening address |
| port | int | Yes | TCP server listening port |
| allow_from | array | No | Allowlist of device IDs; empty means all devices are allowed |
## Use Cases
The MaixCam channel enables PicoClaw to act as an AI backend for edge devices:
- **Smart Surveillance**: MaixCAM sends image frames; PicoClaw analyzes them using vision models
- **IoT Control**: Devices send sensor data; PicoClaw coordinates responses
- **Offline AI**: Deploy PicoClaw on a local network for low-latency inference
+35
View File
@@ -0,0 +1,35 @@
> Voltar ao [README](../../../README.pt-br.md)
# MaixCam
MaixCam é um canal dedicado para conectar dispositivos de câmera AI Sipeed MaixCAM e MaixCAM2. Utiliza sockets TCP para comunicação bidirecional e suporta cenários de implantação de IA na borda.
## Configuração
```json
{
"channels": {
"maixcam": {
"enabled": true,
"host": "0.0.0.0",
"port": 18790,
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ---------- | ------ | ----------- | -------------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal MaixCam deve ser habilitado |
| host | string | Sim | Endereço de escuta do servidor TCP |
| port | int | Sim | Porta de escuta do servidor TCP |
| allow_from | array | Não | Lista de IDs de dispositivos permitidos; vazio significa todos os dispositivos |
## Casos de uso
O canal MaixCam permite que o PicoClaw atue como backend de IA para dispositivos de borda:
- **Vigilância inteligente**: MaixCAM envia quadros de imagem; PicoClaw os analisa usando modelos de visão
- **Controle IoT**: Dispositivos enviam dados de sensores; PicoClaw coordena as respostas
- **IA offline**: Implante o PicoClaw em uma rede local para inferência de baixa latência
+35
View File
@@ -0,0 +1,35 @@
> Quay lại [README](../../../README.vi.md)
# MaixCam
MaixCam là kênh chuyên dụng để kết nối với các thiết bị camera AI Sipeed MaixCAM và MaixCAM2. Sử dụng TCP socket để giao tiếp hai chiều và hỗ trợ các kịch bản triển khai AI tại biên.
## Cấu hình
```json
{
"channels": {
"maixcam": {
"enabled": true,
"host": "0.0.0.0",
"port": 18790,
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ---------- | ------ | -------- | ------------------------------------------------------------------------ |
| enabled | bool | Có | Có bật kênh MaixCam hay không |
| host | string | Có | Địa chỉ lắng nghe của máy chủ TCP |
| port | int | Có | Cổng lắng nghe của máy chủ TCP |
| allow_from | array | Không | Danh sách trắng ID thiết bị; để trống nghĩa là cho phép tất cả thiết bị |
## Trường hợp sử dụng
Kênh MaixCam cho phép PicoClaw hoạt động như backend AI cho các thiết bị biên:
- **Giám sát thông minh**: MaixCAM gửi khung hình ảnh; PicoClaw phân tích bằng mô hình thị giác
- **Điều khiển IoT**: Thiết bị gửi dữ liệu cảm biến; PicoClaw điều phối phản hồi
- **AI ngoại tuyến**: Triển khai PicoClaw trên mạng nội bộ để suy luận độ trễ thấp
+10 -6
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# MaixCam
MaixCam 是专用于连接矽速科技 MaixCAM 与 MaixCAM2 AI 摄像设备的通道。它采用 TCP 套接字实现双向通信,支持边缘 AI 部署场景。
@@ -9,18 +11,20 @@ MaixCam 是专用于连接矽速科技 MaixCAM 与 MaixCAM2 AI 摄像设备的
"channels": {
"maixcam": {
"enabled": true,
"server_address": "0.0.0.0:8899",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []
}
}
}
```
| 字段 | 类型 | 必填 | 描述 |
| -------------- | ------ | ---- | -------------------------------- |
| enabled | bool | 是 | 是否启用 MaixCam 频道 |
| server_address | string | 是 | TCP 服务器监听地址和端口 |
| allow_from | array | | 设备ID白名单,空表示允许所有设备 |
| 字段 | 类型 | 必填 | 描述 |
| ---------- | ------ | ---- | -------------------------------- |
| enabled | bool | 是 | 是否启用 MaixCam 频道 |
| host | string | 是 | TCP 服务器监听地址 |
| port | int | | TCP 服务器监听端口 |
| allow_from | array | 否 | 设备ID白名单,空表示允许所有设备 |
## 使用场景
+1
View File
@@ -42,6 +42,7 @@
| group_trigger | object | 否 | 群聊触发策略(支持 `mention_only` / `prefixes` |
| placeholder | object | 否 | 占位消息配置 |
| reasoning_channel_id | string | 否 | 思维链输出目标通道 |
| message_format | string | 否 | 消息格式:`richtext`(富文本)或 `plain`(纯文本) |
## 3. 当前支持
+33
View File
@@ -0,0 +1,33 @@
> Retour au [README](../../../README.fr.md)
# OneBot
OneBot est un standard de protocole ouvert pour les bots QQ, fournissant une interface unifiée pour diverses implémentations de bots QQ (par exemple go-cqhttp, Mirai). Il utilise WebSocket pour la communication.
## Configuration
```json
{
"channels": {
"onebot": {
"enabled": true,
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| ------------ | ------ | ------ | -------------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal OneBot |
| ws_url | string | Oui | URL WebSocket du serveur OneBot |
| access_token | string | Non | Jeton d'accès pour la connexion au serveur OneBot |
| allow_from | array | Non | Liste blanche d'ID utilisateurs ; vide signifie tous les utilisateurs |
## Procédure de configuration
1. Déployez une implémentation compatible OneBot (par exemple napcat)
2. Configurez l'implémentation OneBot pour activer le service WebSocket et définir un jeton d'accès (si nécessaire)
3. Renseignez l'URL WebSocket et le jeton d'accès dans le fichier de configuration
+33
View File
@@ -0,0 +1,33 @@
> [README](../../../README.ja.md) に戻る
# OneBot
OneBot は QQ ボット向けのオープンプロトコル標準で、複数の QQ ボット実装(例: go-cqhttp、Mirai)に統一されたインターフェースを提供します。通信には WebSocket を使用します。
## 設定
```json
{
"channels": {
"onebot": {
"enabled": true,
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ------------ | ------ | ------ | ---------------------------------------------------------------- |
| enabled | bool | はい | OneBot チャンネルを有効にするかどうか |
| ws_url | string | はい | OneBot サーバーの WebSocket URL |
| access_token | string | いいえ | OneBot サーバーへの接続に使用するアクセストークン |
| allow_from | array | いいえ | ユーザーIDのホワイトリスト。空の場合は全ユーザーを許可 |
## セットアップ手順
1. OneBot 互換の実装(例: napcat)をデプロイする
2. OneBot 実装で WebSocket サービスを有効にし、アクセストークンを設定する(必要な場合)
3. WebSocket URL とアクセストークンを設定ファイルに入力する
+33
View File
@@ -0,0 +1,33 @@
> Back to [README](../../../README.md)
# OneBot
OneBot is an open protocol standard for QQ bots, providing a unified interface for various QQ bot implementations (e.g. go-cqhttp, Mirai). It uses WebSocket for communication.
## Configuration
```json
{
"channels": {
"onebot": {
"enabled": true,
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| ------------ | ------ | -------- | ---------------------------------------------------------------- |
| enabled | bool | Yes | Whether to enable the OneBot channel |
| ws_url | string | Yes | WebSocket URL of the OneBot server |
| access_token | string | No | Access token for connecting to the OneBot server |
| allow_from | array | No | User ID whitelist; empty means all users are allowed |
## Setup
1. Deploy a OneBot-compatible implementation (e.g. napcat)
2. Configure the OneBot implementation to enable the WebSocket service and set an access token (if needed)
3. Fill in the WebSocket URL and access token in the configuration file
+33
View File
@@ -0,0 +1,33 @@
> Voltar ao [README](../../../README.pt-br.md)
# OneBot
OneBot é um padrão de protocolo aberto para bots QQ, fornecendo uma interface unificada para diversas implementações de bots QQ (ex.: go-cqhttp, Mirai). Utiliza WebSocket para comunicação.
## Configuração
```json
{
"channels": {
"onebot": {
"enabled": true,
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ------------ | ------ | ----------- | -------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal OneBot deve ser habilitado |
| ws_url | string | Sim | URL WebSocket do servidor OneBot |
| access_token | string | Não | Token de acesso para conexão ao servidor OneBot |
| allow_from | array | Não | Lista de permissão de IDs de usuário; vazio permite todos |
## Configuração passo a passo
1. Implante uma implementação compatível com OneBot (ex.: napcat)
2. Configure a implementação OneBot para habilitar o serviço WebSocket e definir um token de acesso (se necessário)
3. Preencha a URL WebSocket e o token de acesso no arquivo de configuração
+33
View File
@@ -0,0 +1,33 @@
> Quay lại [README](../../../README.vi.md)
# OneBot
OneBot là tiêu chuẩn giao thức mở dành cho bot QQ, cung cấp giao diện thống nhất cho nhiều triển khai bot QQ khác nhau (ví dụ: go-cqhttp, Mirai). Nó sử dụng WebSocket để giao tiếp.
## Cấu hình
```json
{
"channels": {
"onebot": {
"enabled": true,
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ------------ | ------ | -------- | -------------------------------------------------------------------- |
| enabled | bool | Có | Có bật kênh OneBot hay không |
| ws_url | string | Có | URL WebSocket của máy chủ OneBot |
| access_token | string | Không | Token truy cập để kết nối với máy chủ OneBot |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống cho phép tất cả |
## Quy trình thiết lập
1. Triển khai một bản triển khai tương thích OneBot (ví dụ: napcat)
2. Cấu hình bản triển khai OneBot để bật dịch vụ WebSocket và đặt token truy cập (nếu cần)
3. Điền URL WebSocket và token truy cập vào file cấu hình
+2
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# OneBot
OneBot 是一个面向 QQ 机器人的开放协议标准,为多种 QQ 机器人实现(例如 go-cqhttp、Mirai)提供了统一的接口。它使用 WebSocket 进行通信。
+54
View File
@@ -0,0 +1,54 @@
> Retour au [README](../../../README.fr.md)
# QQ
PicoClaw prend en charge QQ via l'API Bot officielle de la plateforme ouverte QQ.
## Configuration
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| ---------- | ------ | ------ | --------------------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal QQ |
| app_id | string | Oui | App ID de l'application bot QQ |
| app_secret | string | Oui | App Secret de l'application bot QQ |
| allow_from | array | Non | Liste blanche d'identifiants utilisateur ; vide signifie tous les utilisateurs |
## Configuration initiale
### Configuration rapide (recommandée)
La plateforme ouverte QQ propose une entrée de création en un clic :
1. Ouvrir [QQ Bot Quick Create](https://q.qq.com/qqbot/openclaw/index.html) et se connecter en scannant le QR code
2. Le système crée automatiquement un bot — copier l'**App ID** et l'**App Secret**
3. Renseigner les identifiants dans le fichier de configuration PicoClaw
4. Exécuter `picoclaw gateway` pour démarrer le service
5. Ouvrir QQ et commencer à discuter avec le bot
> L'App Secret n'est affiché qu'une seule fois — sauvegardez-le immédiatement. Le consulter à nouveau forcera une réinitialisation.
>
> Les bots créés via l'entrée rapide sont réservés à l'usage personnel du créateur et ne prennent pas en charge les discussions de groupe. Pour la prise en charge des groupes, configurez le mode sandbox sur la [plateforme ouverte QQ](https://q.qq.com/).
### Configuration manuelle
1. Se connecter à la [plateforme ouverte QQ](https://q.qq.com/) avec son compte QQ et s'inscrire en tant que développeur
2. Créer un bot QQ et personnaliser son avatar et son nom
3. Obtenir l'**App ID** et l'**App Secret** dans les paramètres du bot
4. Renseigner les identifiants dans le fichier de configuration PicoClaw
5. Exécuter `picoclaw gateway` pour démarrer le service
6. Rechercher votre bot dans QQ et commencer à discuter
> Pendant le développement, il est recommandé d'activer le mode sandbox et d'y ajouter les utilisateurs et groupes de test pour le débogage.
+54
View File
@@ -0,0 +1,54 @@
> [README](../../../README.ja.md) に戻る
# QQ
PicoClaw は QQ オープンプラットフォームの公式 Bot API を通じて QQ をサポートします。
## 設定
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------- | ------ | ------ | ------------------------------------------------------------- |
| enabled | bool | はい | QQ チャンネルを有効にするかどうか |
| app_id | string | はい | QQ ボットアプリケーションの App ID |
| app_secret | string | はい | QQ ボットアプリケーションの App Secret |
| allow_from | array | いいえ | 許可するユーザーIDのリスト。空の場合はすべてのユーザーを許可 |
## セットアップ手順
### クイックセットアップ(推奨)
QQ オープンプラットフォームにはワンクリック作成エントリーが用意されています:
1. [QQ ボットクイック作成](https://q.qq.com/qqbot/openclaw/index.html) を開き、QR コードをスキャンしてログインする
2. システムが自動的にボットを作成するので、**App ID** と **App Secret** をコピーする
3. PicoClaw 設定ファイルに認証情報を入力する
4. `picoclaw gateway` を実行してサービスを起動する
5. QQ を開いてボットとの会話を始める
> App Secret は一度しか表示されません。すぐに保存してください。再度表示しようとすると強制的にリセットされます。
>
> クイックエントリーで作成したボットは作成者本人のみが使用でき、グループチャットには対応していません。グループチャット機能が必要な場合は、[QQ オープンプラットフォーム](https://q.qq.com/) でサンドボックスモードを設定してください。
### 手動セットアップ
1. QQ アカウントで [QQ オープンプラットフォーム](https://q.qq.com/) にログインし、開発者アカウントを登録する
2. QQ ボットを作成し、アバターと名前をカスタマイズする
3. ボット設定から **App ID****App Secret** を取得する
4. PicoClaw 設定ファイルに認証情報を入力する
5. `picoclaw gateway` を実行してサービスを起動する
6. QQ でボットを検索して会話を始める
> 開発段階ではサンドボックスモードを有効にし、テストユーザーとグループをサンドボックスに追加してデバッグすることを推奨します。
+54
View File
@@ -0,0 +1,54 @@
> Back to [README](../../../README.md)
# QQ
PicoClaw provides QQ support via the official Bot API from the QQ Open Platform.
## Configuration
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| ---------- | ------ | -------- | -------------------------------------------------------- |
| enabled | bool | Yes | Whether to enable the QQ channel |
| app_id | string | Yes | App ID of the QQ bot application |
| app_secret | string | Yes | App Secret of the QQ bot application |
| allow_from | array | No | Allowlist of user IDs; empty means all users are allowed |
## Setup
### Quick Setup (Recommended)
The QQ Open Platform provides a one-click creation entry:
1. Open [QQ Bot Quick Create](https://q.qq.com/qqbot/openclaw/index.html) and log in by scanning the QR code
2. The system automatically creates a bot — copy the **App ID** and **App Secret**
3. Fill in the credentials in the PicoClaw configuration file
4. Run `picoclaw gateway` to start the service
5. Open QQ and start chatting with the bot
> The App Secret is only shown once — save it immediately. Viewing it again will force a reset.
>
> Bots created via the quick entry are for the creator's personal use only and do not support group chats. For group chat support, configure sandbox mode on the [QQ Open Platform](https://q.qq.com/).
### Manual Setup
1. Log in to the [QQ Open Platform](https://q.qq.com/) with your QQ account and register as a developer
2. Create a QQ bot and customize its avatar and name
3. Obtain the **App ID** and **App Secret** from the bot settings
4. Fill in the credentials in the PicoClaw configuration file
5. Run `picoclaw gateway` to start the service
6. Search for your bot in QQ and start chatting
> During development, it is recommended to enable sandbox mode and add test users and groups to the sandbox for debugging.
+54
View File
@@ -0,0 +1,54 @@
> Voltar ao [README](../../../README.pt-br.md)
# QQ
O PicoClaw oferece suporte ao QQ via API Bot oficial da Plataforma Aberta QQ.
## Configuração
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ---------- | ------ | ----------- | -------------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal QQ deve ser habilitado |
| app_id | string | Sim | App ID da aplicação bot QQ |
| app_secret | string | Sim | App Secret da aplicação bot QQ |
| allow_from | array | Não | Lista de IDs de usuários permitidos; vazio significa todos os usuários |
## Configuração inicial
### Configuração rápida (recomendada)
A Plataforma Aberta QQ oferece uma entrada de criação com um clique:
1. Abra o [QQ Bot Quick Create](https://q.qq.com/qqbot/openclaw/index.html) e faça login escaneando o QR code
2. O sistema cria o bot automaticamente — copie o **App ID** e o **App Secret**
3. Preencha as credenciais no arquivo de configuração do PicoClaw
4. Execute `picoclaw gateway` para iniciar o serviço
5. Abra o QQ e comece a conversar com o bot
> O App Secret é exibido apenas uma vez — salve-o imediatamente. Visualizá-lo novamente forçará uma redefinição.
>
> Bots criados pela entrada rápida são apenas para uso pessoal do criador e não suportam chats em grupo. Para suporte a grupos, configure o modo sandbox na [Plataforma Aberta QQ](https://q.qq.com/).
### Configuração manual
1. Faça login na [Plataforma Aberta QQ](https://q.qq.com/) com sua conta QQ e registre-se como desenvolvedor
2. Crie um bot QQ e personalize seu avatar e nome
3. Obtenha o **App ID** e o **App Secret** nas configurações do bot
4. Preencha as credenciais no arquivo de configuração do PicoClaw
5. Execute `picoclaw gateway` para iniciar o serviço
6. Pesquise seu bot no QQ e comece a conversar
> Durante o desenvolvimento, recomenda-se habilitar o modo sandbox e adicionar usuários e grupos de teste ao sandbox para depuração.
+54
View File
@@ -0,0 +1,54 @@
> Quay lại [README](../../../README.vi.md)
# QQ
PicoClaw hỗ trợ QQ thông qua API Bot chính thức của Nền tảng Mở QQ.
## Cấu hình
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ---------- | ------ | -------- | ------------------------------------------------------------------------ |
| enabled | bool | Có | Có bật kênh QQ hay không |
| app_id | string | Có | App ID của ứng dụng bot QQ |
| app_secret | string | Có | App Secret của ứng dụng bot QQ |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống nghĩa là cho phép tất cả |
## Hướng dẫn thiết lập
### Thiết lập nhanh (Khuyến nghị)
Nền tảng Mở QQ cung cấp lối vào tạo bot một chạm:
1. Mở [QQ Bot Quick Create](https://q.qq.com/qqbot/openclaw/index.html) và đăng nhập bằng cách quét mã QR
2. Hệ thống tự động tạo bot — sao chép **App ID** và **App Secret**
3. Điền thông tin xác thực vào file cấu hình PicoClaw
4. Chạy `picoclaw gateway` để khởi động dịch vụ
5. Mở QQ và bắt đầu trò chuyện với bot
> App Secret chỉ hiển thị một lần — hãy lưu lại ngay. Xem lại sẽ buộc phải đặt lại.
>
> Bot được tạo qua lối vào nhanh chỉ dành cho người tạo sử dụng cá nhân và chưa hỗ trợ chat nhóm. Để hỗ trợ chat nhóm, hãy cấu hình chế độ sandbox trên [Nền tảng Mở QQ](https://q.qq.com/).
### Tạo thủ công
1. Đăng nhập vào [Nền tảng Mở QQ](https://q.qq.com/) bằng tài khoản QQ và đăng ký tài khoản nhà phát triển
2. Tạo bot QQ, tùy chỉnh ảnh đại diện và tên
3. Lấy **App ID****App Secret** trong cài đặt bot
4. Điền thông tin xác thực vào file cấu hình PicoClaw
5. Chạy `picoclaw gateway` để khởi động dịch vụ
6. Tìm kiếm bot của bạn trong QQ và bắt đầu trò chuyện
> Trong giai đoạn phát triển, nên bật chế độ sandbox và thêm người dùng, nhóm thử nghiệm vào sandbox để gỡ lỗi.
+26 -4
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# QQ
PicoClaw 通过 QQ 开放平台的官方机器人 API 提供对 QQ 的支持。
@@ -28,7 +30,27 @@ PicoClaw 通过 QQ 开放平台的官方机器人 API 提供对 QQ 的支持。
## 设置流程
1. 前往 [QQ 开放平台](https://q.qq.com/) 创建一个机器人
2. 通过仪表盘获取 App ID 和 App Secret
3. 开启机器人沙箱模式, 将用户和群添加到沙箱中
4. 将 App ID 和 App Secret 填入配置文件中
### 快捷方式(推荐)
QQ 开放平台提供了一键创建入口:
1. 打开 [QQ 机器人快速创建](https://q.qq.com/qqbot/openclaw/index.html),扫码登录
2. 系统自动创建机器人,复制 **App ID** 和 **App Secret**
3. 将凭证填入 PicoClaw 配置文件
4. 运行 `picoclaw gateway` 启动服务
5. 打开 QQ,与机器人开始对话
> App Secret 仅显示一次,请立即保存。再次查看将强制重置。
>
> 通过快捷入口创建的机器人仅供创建人使用,暂不支持群聊。如需群聊功能,请在 [QQ 开放平台](https://q.qq.com/) 配置沙箱模式。
### 手动创建
1. 使用 QQ 账号登录 [QQ 开放平台](https://q.qq.com/),注册开发者账号
2. 创建 QQ 机器人,自定义头像和名称
3. 在机器人设置中获取 **App ID** 和 **App Secret**
4. 将凭证填入 PicoClaw 配置文件
5. 运行 `picoclaw gateway` 启动服务
6. 在 QQ 中搜索你的机器人,开始对话
> 开发阶段建议开启沙箱模式,将测试用户和群添加到沙箱中进行调试。
+35
View File
@@ -0,0 +1,35 @@
> Retour au [README](../../../README.fr.md)
# Slack
Slack est l'une des principales plateformes de messagerie instantanée pour les entreprises. PicoClaw utilise le Socket Mode de Slack pour une communication bidirectionnelle en temps réel, sans nécessiter la configuration d'un endpoint webhook public.
## Configuration
```json
{
"channels": {
"slack": {
"enabled": true,
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []
}
}
}
```
| Champ | Type | Requis | Description |
| ---------- | ------ | ------ | ---------------------------------------------------------------------------- |
| enabled | bool | Oui | Activer ou non le canal Slack |
| bot_token | string | Oui | Bot User OAuth Token du bot Slack (commence par xoxb-) |
| app_token | string | Oui | App Level Token Socket Mode de l'application Slack (commence par xapp-) |
| allow_from | array | Non | Liste blanche d'ID utilisateurs ; vide signifie tous les utilisateurs |
## Procédure de configuration
1. Rendez-vous sur [Slack API](https://api.slack.com/) et créez une nouvelle application Slack
2. Activez le Socket Mode et obtenez l'App Level Token
3. Ajoutez des Bot Token Scopes (par exemple `chat:write`, `im:history`, etc.)
4. Installez l'application dans votre espace de travail et obtenez le Bot User OAuth Token
5. Renseignez le Bot Token et l'App Token dans le fichier de configuration
+35
View File
@@ -0,0 +1,35 @@
> [README](../../../README.ja.md) に戻る
# Slack
Slack は世界をリードする企業向けインスタントメッセージングプラットフォームです。PicoClaw は Slack の Socket Mode を使用してリアルタイムの双方向通信を実現しており、公開 Webhook エンドポイントの設定は不要です。
## 設定
```json
{
"channels": {
"slack": {
"enabled": true,
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------- | ------ | ------ | ------------------------------------------------------------------------ |
| enabled | bool | はい | Slack チャンネルを有効にするかどうか |
| bot_token | string | はい | Slack ボットの Bot User OAuth Tokenxoxb- で始まる) |
| app_token | string | はい | Slack アプリの Socket Mode App Level Tokenxapp- で始まる) |
| allow_from | array | いいえ | ユーザーIDのホワイトリスト。空の場合は全ユーザーを許可 |
## セットアップ手順
1. [Slack API](https://api.slack.com/) にアクセスして新しい Slack アプリを作成する
2. Socket Mode を有効にして App Level Token を取得する
3. Bot Token Scopes を追加する(例: `chat:write``im:history` など)
4. アプリをワークスペースにインストールして Bot User OAuth Token を取得する
5. Bot Token と App Token を設定ファイルに入力する
+35
View File
@@ -0,0 +1,35 @@
> Back to [README](../../../README.md)
# Slack
Slack is a leading enterprise instant messaging platform. PicoClaw uses Slack's Socket Mode for real-time bidirectional communication, with no need to configure a public webhook endpoint.
## Configuration
```json
{
"channels": {
"slack": {
"enabled": true,
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []
}
}
}
```
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------------------------------------------------ |
| enabled | bool | Yes | Whether to enable the Slack channel |
| bot_token | string | Yes | Bot User OAuth Token for the Slack bot (starts with xoxb-) |
| app_token | string | Yes | Socket Mode App Level Token for the Slack app (starts with xapp-) |
| allow_from | array | No | User ID whitelist; empty means all users are allowed |
## Setup
1. Go to [Slack API](https://api.slack.com/) and create a new Slack app
2. Enable Socket Mode and obtain the App Level Token
3. Add Bot Token Scopes (e.g. `chat:write`, `im:history`, etc.)
4. Install the app to your workspace and obtain the Bot User OAuth Token
5. Fill in the Bot Token and App Token in the configuration file
+35
View File
@@ -0,0 +1,35 @@
> Voltar ao [README](../../../README.pt-br.md)
# Slack
O Slack é uma das principais plataformas de mensagens instantâneas para empresas. O PicoClaw usa o Socket Mode do Slack para comunicação bidirecional em tempo real, sem necessidade de configurar um endpoint de webhook público.
## Configuração
```json
{
"channels": {
"slack": {
"enabled": true,
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ---------- | ------ | ----------- | ---------------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal Slack deve ser habilitado |
| bot_token | string | Sim | Bot User OAuth Token do bot Slack (começa com xoxb-) |
| app_token | string | Sim | App Level Token do Socket Mode do aplicativo Slack (começa com xapp-) |
| allow_from | array | Não | Lista de permissão de IDs de usuário; vazio permite todos |
## Configuração passo a passo
1. Acesse o [Slack API](https://api.slack.com/) e crie um novo aplicativo Slack
2. Ative o Socket Mode e obtenha o App Level Token
3. Adicione Bot Token Scopes (ex.: `chat:write`, `im:history`, etc.)
4. Instale o aplicativo no seu workspace e obtenha o Bot User OAuth Token
5. Preencha o Bot Token e o App Token no arquivo de configuração
+35
View File
@@ -0,0 +1,35 @@
> Quay lại [README](../../../README.vi.md)
# Slack
Slack là nền tảng nhắn tin tức thì hàng đầu dành cho doanh nghiệp. PicoClaw sử dụng Socket Mode của Slack để giao tiếp hai chiều theo thời gian thực, không cần cấu hình endpoint webhook công khai.
## Cấu hình
```json
{
"channels": {
"slack": {
"enabled": true,
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ---------- | ------ | -------- | ---------------------------------------------------------------------------- |
| enabled | bool | Có | Có bật kênh Slack hay không |
| bot_token | string | Có | Bot User OAuth Token của Slack bot (bắt đầu bằng xoxb-) |
| app_token | string | Có | App Level Token Socket Mode của ứng dụng Slack (bắt đầu bằng xapp-) |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống cho phép tất cả |
## Quy trình thiết lập
1. Truy cập [Slack API](https://api.slack.com/) và tạo một ứng dụng Slack mới
2. Bật Socket Mode và lấy App Level Token
3. Thêm Bot Token Scopes (ví dụ: `chat:write`, `im:history`, v.v.)
4. Cài đặt ứng dụng vào workspace và lấy Bot User OAuth Token
5. Điền Bot Token và App Token vào file cấu hình
+2
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# Slack
Slack 是全球领先的企业级即时通讯平台。PicoClaw 采用 Slack 的 Socket Mode 实现实时双向通信,无需配置公开的 Webhook 端点。
+35
View File
@@ -0,0 +1,35 @@
> Retour au [README](../../../README.fr.md)
# Telegram
Le canal Telegram utilise le long polling via l'API Bot Telegram pour une communication basée sur les bots. Il prend en charge les messages texte, les pièces jointes multimédias (photos, messages vocaux, audio, documents), la transcription vocale via Groq Whisper et la gestion des commandes intégrée.
## Configuration
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": ""
}
}
}
```
| Champ | Type | Requis | Description |
| ---------- | ------ | ------ | ------------------------------------------------------------------------ |
| enabled | bool | Oui | Activer ou non le canal Telegram |
| token | string | Oui | Token de l'API Bot Telegram |
| allow_from | array | Non | Liste blanche d'identifiants utilisateur ; vide signifie tous les utilisateurs |
| proxy | string | Non | URL du proxy pour se connecter à l'API Telegram (ex. http://127.0.0.1:7890) |
## Configuration initiale
1. Rechercher `@BotFather` dans Telegram
2. Envoyer la commande `/newbot` et suivre les instructions pour créer un nouveau bot
3. Obtenir le Token de l'API HTTP
4. Renseigner le Token dans le fichier de configuration
5. (Optionnel) Configurer `allow_from` pour restreindre les identifiants utilisateur autorisés à interagir (les IDs peuvent être obtenus via `@userinfobot`)
+35
View File
@@ -0,0 +1,35 @@
> [README](../../../README.ja.md) に戻る
# Telegram
Telegram チャンネルは、Telegram Bot API を使用したロングポーリングによるボットベースの通信を実装しています。テキストメッセージ、メディア添付ファイル(写真、音声、オーディオ、ドキュメント)、Groq Whisper による音声文字起こし、および組み込みコマンドハンドラーをサポートしています。
## 設定
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": ""
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------- | ------ | ---- | ----------------------------------------------------------------- |
| enabled | bool | はい | Telegram チャンネルを有効にするかどうか |
| token | string | はい | Telegram Bot API トークン |
| allow_from | array | いいえ | 許可するユーザーIDのリスト。空の場合はすべてのユーザーを許可 |
| proxy | string | いいえ | Telegram API への接続に使用するプロキシ URL (例: http://127.0.0.1:7890) |
## セットアップ手順
1. Telegram で `@BotFather` を検索する
2. `/newbot` コマンドを送信し、指示に従って新しいボットを作成する
3. HTTP API トークンを取得する
4. 設定ファイルにトークンを入力する
5. (任意) `allow_from` を設定して、対話を許可するユーザー ID を制限する(ID は `@userinfobot` で取得可能)
+35
View File
@@ -0,0 +1,35 @@
> Back to [README](../../../README.md)
# Telegram
The Telegram channel uses long polling via the Telegram Bot API for bot-based communication. It supports text messages, media attachments (photos, voice, audio, documents), voice transcription via Groq Whisper, and built-in command handling.
## Configuration
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": ""
}
}
}
```
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------------------------------------------ |
| enabled | bool | Yes | Whether to enable the Telegram channel |
| token | string | Yes | Telegram Bot API Token |
| allow_from | array | No | Allowlist of user IDs; empty means all users are allowed |
| proxy | string | No | Proxy URL for connecting to the Telegram API (e.g. http://127.0.0.1:7890) |
## Setup
1. Search for `@BotFather` in Telegram
2. Send the `/newbot` command and follow the prompts to create a new bot
3. Obtain the HTTP API Token
4. Fill in the Token in the configuration file
5. (Optional) Configure `allow_from` to restrict which user IDs can interact (you can get IDs via `@userinfobot`)
+35
View File
@@ -0,0 +1,35 @@
> Voltar ao [README](../../../README.pt-br.md)
# Telegram
O canal Telegram utiliza long polling via a API de Bot do Telegram para comunicação baseada em bots. Suporta mensagens de texto, anexos de mídia (fotos, voz, áudio, documentos), transcrição de voz via Groq Whisper e tratamento de comandos integrado.
## Configuração
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": ""
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ---------- | ------ | ----------- | -------------------------------------------------------------------------- |
| enabled | bool | Sim | Se o canal Telegram deve ser habilitado |
| token | string | Sim | Token da API de Bot do Telegram |
| allow_from | array | Não | Lista de IDs de usuários permitidos; vazio significa todos os usuários |
| proxy | string | Não | URL do proxy para conexão com a API do Telegram (ex. http://127.0.0.1:7890) |
## Configuração inicial
1. Pesquise por `@BotFather` no Telegram
2. Envie o comando `/newbot` e siga as instruções para criar um novo bot
3. Obtenha o Token da API HTTP
4. Preencha o Token no arquivo de configuração
5. (Opcional) Configure `allow_from` para restringir quais IDs de usuário podem interagir (os IDs podem ser obtidos via `@userinfobot`)
+35
View File
@@ -0,0 +1,35 @@
> Quay lại [README](../../../README.vi.md)
# Telegram
Kênh Telegram sử dụng long polling qua Telegram Bot API để giao tiếp dựa trên bot. Hỗ trợ tin nhắn văn bản, tệp đính kèm đa phương tiện (ảnh, giọng nói, âm thanh, tài liệu), chuyển giọng nói thành văn bản qua Groq Whisper và xử lý lệnh tích hợp sẵn.
## Cấu hình
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": ""
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ---------- | ------ | -------- | ------------------------------------------------------------------------ |
| enabled | bool | Có | Có bật kênh Telegram hay không |
| token | string | Có | Token API Bot Telegram |
| allow_from | array | Không | Danh sách trắng ID người dùng; để trống nghĩa là cho phép tất cả |
| proxy | string | Không | URL proxy để kết nối với Telegram API (ví dụ: http://127.0.0.1:7890) |
## Hướng dẫn thiết lập
1. Tìm kiếm `@BotFather` trong Telegram
2. Gửi lệnh `/newbot` và làm theo hướng dẫn để tạo bot mới
3. Lấy Token API HTTP
4. Điền Token vào file cấu hình
5. (Tùy chọn) Cấu hình `allow_from` để giới hạn ID người dùng được phép tương tác (có thể lấy ID qua `@userinfobot`)
+2
View File
@@ -1,3 +1,5 @@
> 返回 [README](../../../README.zh.md)
# Telegram
Telegram Channel 通过 Telegram 机器人 API 使用长轮询实现基于机器人的通信。它支持文本消息、媒体附件(照片、语音、音频、文档)、通过 Groq Whisper 进行语音转录以及内置命令处理器。
@@ -0,0 +1,118 @@
> Retour au [README](../../../../README.fr.md)
# WeCom AI Bot
Le WeCom AI Bot est une méthode d'intégration de conversation IA officiellement fournie par WeCom. Il prend en charge les conversations privées et de groupe, intègre un protocole de réponse en streaming et supporte l'envoi proactif de la réponse finale via `response_url` en cas de dépassement de délai.
## Comparaison avec les autres canaux WeCom
| Fonctionnalité | WeCom Bot | WeCom App | **WeCom AI Bot** |
|----------------|-----------|-----------|-----------------|
| Chat privé | ✅ | ✅ | ✅ |
| Chat de groupe | ✅ | ❌ | ✅ |
| Sortie en streaming | ❌ | ❌ | ✅ |
| Push proactif en cas de timeout | ❌ | ✅ | ✅ |
| Complexité de configuration | Faible | Élevée | Moyenne |
## Configuration
```json
{
"channels": {
"wecom_aibot": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-aibot",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"max_steps": 10
}
}
}
```
| Champ | Type | Requis | Description |
| ---------------- | ------ | ------ | -------------------------------------------------- |
| token | string | Oui | Jeton de vérification du callback, configuré sur la page de gestion de l'AI Bot |
| encoding_aes_key | string | Oui | Clé AES de 43 caractères, générée aléatoirement sur la page de gestion de l'AI Bot |
| webhook_path | string | Non | Chemin du webhook (par défaut : /webhook/wecom-aibot) |
| allow_from | array | Non | Liste blanche d'ID utilisateurs ; un tableau vide autorise tous les utilisateurs |
| welcome_message | string | Non | Message de bienvenue envoyé à l'ouverture du chat ; laisser vide pour désactiver |
| reply_timeout | int | Non | Délai de réponse en secondes (par défaut : 5) |
| max_steps | int | Non | Nombre maximum d'étapes d'exécution de l'agent (par défaut : 10) |
## Procédure de configuration
1. Connectez-vous à la [console d'administration WeCom](https://work.weixin.qq.com/wework_admin)
2. Accédez à « Gestion des applications » → « AI Bot », puis créez ou sélectionnez un AI Bot
3. Sur la page de configuration de l'AI Bot, renseignez les informations de « Réception des messages » :
- **URL** : `http://<your-server-ip>:18790/webhook/wecom-aibot`
- **Token** : Généré aléatoirement ou personnalisé
- **EncodingAESKey** : Cliquez sur « Générer aléatoirement » pour obtenir une clé de 43 caractères
4. Saisissez le Token et l'EncodingAESKey dans le fichier de configuration PicoClaw, démarrez le service, puis revenez à la console d'administration pour enregistrer (WeCom enverra une requête de vérification)
> [!TIP]
> Le serveur doit être accessible par les serveurs WeCom. Si vous êtes sur un intranet ou en développement local, utilisez [ngrok](https://ngrok.com) ou frp pour le tunneling.
## Protocole de réponse en streaming
Le WeCom AI Bot utilise un protocole de « pull en streaming », différent de la réponse unique d'un webhook standard :
```
L'utilisateur envoie un message
PicoClaw retourne immédiatement {finish: false} (l'agent commence le traitement)
WeCom effectue un pull environ toutes les 1 seconde avec {msgtype: "stream", stream: {id: "..."}}
├─ Agent non terminé → retourne {finish: false} (continuer à attendre)
└─ Agent terminé → retourne {finish: true, content: "contenu de la réponse"}
```
**Gestion du timeout** (tâche dépassant 30 secondes) :
Si le traitement de l'agent dépasse environ 30 secondes (la fenêtre de polling maximale de WeCom est de 6 minutes), PicoClaw va :
1. Fermer immédiatement le stream et afficher à l'utilisateur : « ⏳ 正在处理中,请稍候,结果将稍后发送。 »
2. L'agent continue de s'exécuter en arrière-plan
3. Une fois l'agent terminé, la réponse finale est envoyée proactivement à l'utilisateur via le `response_url` inclus dans le message
> `response_url` est émis par WeCom, valable 1 heure, utilisable une seule fois, sans chiffrement requis — il suffit de POSTer directement le corps du message markdown.
## Message de bienvenue
Lorsque `welcome_message` est configuré, PicoClaw répond automatiquement avec ce message lorsqu'un utilisateur ouvre la fenêtre de chat avec l'AI Bot (événement `enter_chat`). Laisser vide pour ignorer silencieusement.
```json
"welcome_message": "你好!我是 PicoClaw AI 助手,有什么可以帮你?"
```
## FAQ
### Échec de la vérification de l'URL de callback
- Vérifiez que le pare-feu du serveur autorise le port concerné (par défaut 18790)
- Vérifiez que `token` et `encoding_aes_key` sont correctement renseignés
- Consultez les logs PicoClaw pour voir si une requête GET de WeCom a été reçue
### Les messages ne reçoivent pas de réponse
- Vérifiez que `allow_from` ne restreint pas accidentellement l'expéditeur
- Recherchez `context canceled` ou des erreurs d'agent dans les logs
- Vérifiez que la configuration de l'agent (ex. `model_name`) est correcte
### Pas de push final reçu pour les tâches longues
- Vérifiez que le callback du message inclut `response_url` (uniquement supporté par la nouvelle version du WeCom AI Bot)
- Vérifiez que le serveur peut effectuer des requêtes sortantes (nécessite un POST vers `response_url`)
- Consultez les logs pour les mots-clés `response_url mode` et `Sending reply via response_url`
## Références
- [Documentation d'intégration WeCom AI Bot](https://developer.work.weixin.qq.com/document/path/100719)
- [Description du protocole de réponse en streaming](https://developer.work.weixin.qq.com/document/path/100719)
- [Réponse proactive via response_url](https://developer.work.weixin.qq.com/document/path/101138)
@@ -0,0 +1,118 @@
> [README](../../../../README.ja.md) に戻る
# 企業WeChat AIボット
企業WeChat AIボット(AI Bot)は、企業WeChatが公式に提供するAI会話連携方式です。プライベートチャットとグループチャットの両方をサポートし、ストリーミングレスポンスプロトコルを内蔵しており、タイムアウト後に `response_url` を通じて最終返信をプッシュする機能もサポートしています。
## 他のWeCom チャンネルとの比較
| 機能 | WeCom Bot | WeCom App | **WeCom AI Bot** |
|------|-----------|-----------|-----------------|
| プライベートチャット | ✅ | ✅ | ✅ |
| グループチャット | ✅ | ❌ | ✅ |
| ストリーミング出力 | ❌ | ❌ | ✅ |
| タイムアウト時のプッシュ | ❌ | ✅ | ✅ |
| 設定の複雑さ | 低 | 高 | 中 |
## 設定
```json
{
"channels": {
"wecom_aibot": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-aibot",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"max_steps": 10
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------------- | ------ | ---- | -------------------------------------------------- |
| token | string | はい | コールバック検証トークン。AIボット管理ページで設定 |
| encoding_aes_key | string | はい | 43文字のAESキー。AIボット管理ページでランダム生成 |
| webhook_path | string | いいえ | Webhookパス(デフォルト:/webhook/wecom-aibot |
| allow_from | array | いいえ | ユーザーIDの許可リスト。空配列は全ユーザーを許可 |
| welcome_message | string | いいえ | ユーザーがチャットを開いたときに送信するウェルカムメッセージ。空白の場合は送信しない |
| reply_timeout | int | いいえ | 返信タイムアウト(秒、デフォルト:5) |
| max_steps | int | いいえ | エージェントの最大実行ステップ数(デフォルト:10) |
## セットアップ手順
1. [企業WeChat管理コンソール](https://work.weixin.qq.com/wework_admin) にログイン
2. 「アプリ管理」→「AIボット」に進み、AIボットを作成または選択
3. AIボット設定ページで「メッセージ受信」情報を入力:
- **URL**`http://<your-server-ip>:18790/webhook/wecom-aibot`
- **Token**:ランダム生成またはカスタム
- **EncodingAESKey**:「ランダム生成」をクリックして43文字のキーを取得
4. TokenとEncodingAESKeyをPicoClawの設定ファイルに入力し、サービスを起動してから管理コンソールに戻って保存(企業WeChatが検証リクエストを送信します)
> [!TIP]
> サーバーは企業WeChatのサーバーからアクセス可能である必要があります。イントラネットやローカル開発環境の場合は、[ngrok](https://ngrok.com) またはfrpを使用してトンネリングしてください。
## ストリーミングレスポンスプロトコル
WeCom AIボットは「ストリーミングプル」プロトコルを使用しており、通常のWebhookの一回限りの返信とは異なります:
```
ユーザーがメッセージを送信
PicoClawが即座に {finish: false} を返す(エージェントが処理開始)
企業WeChatが約1秒ごとに {msgtype: "stream", stream: {id: "..."}} でプル
├─ エージェント未完了 → {finish: false} を返す(待機継続)
└─ エージェント完了 → {finish: true, content: "返信内容"} を返す
```
**タイムアウト処理**(タスクが30秒を超える場合):
エージェントの処理時間が約30秒を超えた場合(企業WeChatの最大ポーリングウィンドウは6分)、PicoClawは:
1. 即座にストリームを閉じ、ユーザーに「⏳ 正在处理中,请稍候,结果将稍后发送。」と表示
2. エージェントはバックグラウンドで処理を継続
3. エージェント完了後、メッセージに含まれる `response_url` を通じて最終返信をユーザーにプッシュ
> `response_url` は企業WeChatが発行し、有効期限は1時間、使用は1回限りで、暗号化不要。マークダウンメッセージ本文をそのままPOSTするだけです。
## ウェルカムメッセージ
`welcome_message` を設定すると、ユーザーがAIボットとのチャットウィンドウを開いたとき(`enter_chat` イベント)に、PicoClawが自動的にそのメッセージを返信します。空白の場合は無視されます。
```json
"welcome_message": "你好!我是 PicoClaw AI 助手,有什么可以帮你?"
```
## よくある質問
### コールバックURL検証の失敗
- サーバーのファイアウォールで該当ポートが開放されているか確認(デフォルト18790)
- `token``encoding_aes_key` が正しく入力されているか確認
- PicoClawのログに企業WeChatからのGETリクエストが届いているか確認
### メッセージに返信がない
- `allow_from` が誤って送信者を制限していないか確認
- ログに `context canceled` またはエージェントエラーが出ていないか確認
- エージェント設定(`model_name` など)が正しいか確認
### 長時間タスクで最終プッシュが届かない
- メッセージコールバックに `response_url` が含まれているか確認(新バージョンの企業WeChat AIボットのみ対応)
- サーバーが外部ネットワークへのアウトバウンドリクエストを送信できるか確認(`response_url` へのPOSTが必要)
- ログのキーワード `response_url mode``Sending reply via response_url` を確認
## 参考ドキュメント
- [企業WeChat AIボット連携ドキュメント](https://developer.work.weixin.qq.com/document/path/100719)
- [ストリーミングレスポンスプロトコルの説明](https://developer.work.weixin.qq.com/document/path/100719)
- [response_url によるプロアクティブ返信](https://developer.work.weixin.qq.com/document/path/101138)
+118
View File
@@ -0,0 +1,118 @@
> Back to [README](../../../../README.md)
# WeCom AI Bot
The WeCom AI Bot is an official AI conversation integration provided by WeCom. It supports both private and group chats, has a built-in streaming response protocol, and supports proactively pushing the final reply via `response_url` after a timeout.
## Comparison with Other WeCom Channels
| Feature | WeCom Bot | WeCom App | **WeCom AI Bot** |
|---------|-----------|-----------|-----------------|
| Private Chat | ✅ | ✅ | ✅ |
| Group Chat | ✅ | ❌ | ✅ |
| Streaming Output | ❌ | ❌ | ✅ |
| Proactive Push on Timeout | ❌ | ✅ | ✅ |
| Configuration Complexity | Low | High | Medium |
## Configuration
```json
{
"channels": {
"wecom_aibot": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-aibot",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"max_steps": 10
}
}
}
```
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | -------------------------------------------------- |
| token | string | Yes | Callback verification token, configured on the AI Bot management page |
| encoding_aes_key | string | Yes | 43-character AES key, randomly generated on the AI Bot management page |
| webhook_path | string | No | Webhook path (default: /webhook/wecom-aibot) |
| allow_from | array | No | User ID allowlist; empty array allows all users |
| welcome_message | string | No | Welcome message sent when a user opens the chat; leave empty to disable |
| reply_timeout | int | No | Reply timeout in seconds (default: 5) |
| max_steps | int | No | Maximum agent execution steps (default: 10) |
## Setup
1. Log in to the [WeCom Admin Console](https://work.weixin.qq.com/wework_admin)
2. Go to "App Management" → "AI Bot", then create or select an AI Bot
3. On the AI Bot configuration page, fill in the "Message Reception" details:
- **URL**: `http://<your-server-ip>:18790/webhook/wecom-aibot`
- **Token**: Randomly generated or custom
- **EncodingAESKey**: Click "Random Generate" to get a 43-character key
4. Enter the Token and EncodingAESKey into the PicoClaw config file, start the service, then return to the admin console to save (WeCom will send a verification request)
> [!TIP]
> The server must be accessible by WeCom's servers. If you are on an intranet or developing locally, use [ngrok](https://ngrok.com) or frp for tunneling.
## Streaming Response Protocol
WeCom AI Bot uses a "streaming pull" protocol, which differs from the one-shot reply of a standard webhook:
```
User sends a message
PicoClaw immediately returns {finish: false} (Agent starts processing)
WeCom pulls approximately every 1 second with {msgtype: "stream", stream: {id: "..."}}
├─ Agent not done → returns {finish: false} (keep waiting)
└─ Agent done → returns {finish: true, content: "reply content"}
```
**Timeout Handling** (task exceeds 30 seconds):
If the Agent takes longer than approximately 30 seconds (WeCom's maximum polling window is 6 minutes), PicoClaw will:
1. Immediately close the stream and show the user: "⏳ 正在处理中,请稍候,结果将稍后发送。"
2. The Agent continues running in the background
3. Once the Agent finishes, the final reply is proactively pushed to the user via the `response_url` included in the message
> `response_url` is issued by WeCom, valid for 1 hour, can only be used once, requires no encryption — just POST the markdown message body directly.
## Welcome Message
When `welcome_message` is configured, PicoClaw will automatically reply with it when a user opens the chat window with the AI Bot (`enter_chat` event). Leave it empty to silently ignore the event.
```json
"welcome_message": "你好!我是 PicoClaw AI 助手,有什么可以帮你?"
```
## FAQ
### Callback URL Verification Failed
- Confirm the server firewall has the relevant port open (default 18790)
- Confirm `token` and `encoding_aes_key` are entered correctly
- Check PicoClaw logs to see if a GET request from WeCom was received
### Messages Not Getting a Reply
- Check whether `allow_from` is accidentally restricting the sender
- Look for `context canceled` or Agent errors in the logs
- Confirm the Agent configuration (e.g., `model_name`) is correct
### No Final Push Received for Long-Running Tasks
- Confirm the message callback includes `response_url` (only supported by the newer WeCom AI Bot)
- Confirm the server can make outbound requests (needs to POST to `response_url`)
- Check logs for keywords `response_url mode` and `Sending reply via response_url`
## Reference
- [WeCom AI Bot Integration Docs](https://developer.work.weixin.qq.com/document/path/100719)
- [Streaming Response Protocol](https://developer.work.weixin.qq.com/document/path/100719)
- [Proactive Reply via response_url](https://developer.work.weixin.qq.com/document/path/101138)
@@ -0,0 +1,118 @@
> Voltar ao [README](../../../../README.pt-br.md)
# WeCom AI Bot
O WeCom AI Bot é uma forma oficial de integração de conversas com IA fornecida pelo WeCom. Suporta conversas privadas e em grupo, possui um protocolo de resposta em streaming integrado e suporta o envio proativo da resposta final via `response_url` após um timeout.
## Comparação com outros canais WeCom
| Recurso | WeCom Bot | WeCom App | **WeCom AI Bot** |
|---------|-----------|-----------|-----------------|
| Chat privado | ✅ | ✅ | ✅ |
| Chat em grupo | ✅ | ❌ | ✅ |
| Saída em streaming | ❌ | ❌ | ✅ |
| Push proativo em timeout | ❌ | ✅ | ✅ |
| Complexidade de configuração | Baixa | Alta | Média |
## Configuração
```json
{
"channels": {
"wecom_aibot": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-aibot",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"max_steps": 10
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ---------------- | ------ | ----------- | -------------------------------------------------- |
| token | string | Sim | Token de verificação de callback, configurado na página de gerenciamento do AI Bot |
| encoding_aes_key | string | Sim | Chave AES de 43 caracteres, gerada aleatoriamente na página de gerenciamento do AI Bot |
| webhook_path | string | Não | Caminho do webhook (padrão: /webhook/wecom-aibot) |
| allow_from | array | Não | Lista de permissão de IDs de usuários; array vazio permite todos os usuários |
| welcome_message | string | Não | Mensagem de boas-vindas enviada quando o usuário abre o chat; deixe vazio para desativar |
| reply_timeout | int | Não | Timeout de resposta em segundos (padrão: 5) |
| max_steps | int | Não | Número máximo de etapas de execução do agente (padrão: 10) |
## Configuração passo a passo
1. Faça login no [Console de Administração do WeCom](https://work.weixin.qq.com/wework_admin)
2. Acesse "Gerenciamento de Apps" → "AI Bot", depois crie ou selecione um AI Bot
3. Na página de configuração do AI Bot, preencha as informações de "Recebimento de Mensagens":
- **URL**: `http://<your-server-ip>:18790/webhook/wecom-aibot`
- **Token**: Gerado aleatoriamente ou personalizado
- **EncodingAESKey**: Clique em "Gerar Aleatoriamente" para obter uma chave de 43 caracteres
4. Insira o Token e o EncodingAESKey no arquivo de configuração do PicoClaw, inicie o serviço e volte ao console de administração para salvar (o WeCom enviará uma requisição de verificação)
> [!TIP]
> O servidor precisa ser acessível pelos servidores do WeCom. Se estiver em uma intranet ou desenvolvendo localmente, use [ngrok](https://ngrok.com) ou frp para tunelamento.
## Protocolo de resposta em streaming
O WeCom AI Bot usa um protocolo de "pull em streaming", diferente da resposta única de um webhook padrão:
```
Usuário envia uma mensagem
PicoClaw retorna imediatamente {finish: false} (Agente começa a processar)
WeCom faz pull aproximadamente a cada 1 segundo com {msgtype: "stream", stream: {id: "..."}}
├─ Agente não concluído → retorna {finish: false} (continuar aguardando)
└─ Agente concluído → retorna {finish: true, content: "conteúdo da resposta"}
```
**Tratamento de timeout** (tarefa excede 30 segundos):
Se o processamento do agente demorar mais de aproximadamente 30 segundos (a janela máxima de polling do WeCom é de 6 minutos), o PicoClaw irá:
1. Fechar imediatamente o stream e exibir ao usuário: "⏳ 正在处理中,请稍候,结果将稍后发送。"
2. O agente continua executando em segundo plano
3. Após a conclusão do agente, a resposta final é enviada proativamente ao usuário via `response_url` incluído na mensagem
> `response_url` é emitido pelo WeCom, válido por 1 hora, pode ser usado apenas uma vez, sem necessidade de criptografia — basta fazer um POST com o corpo da mensagem em markdown diretamente.
## Mensagem de boas-vindas
Quando `welcome_message` está configurado, o PicoClaw responde automaticamente com essa mensagem quando um usuário abre a janela de chat com o AI Bot (evento `enter_chat`). Deixe vazio para ignorar silenciosamente.
```json
"welcome_message": "你好!我是 PicoClaw AI 助手,有什么可以帮你?"
```
## Perguntas frequentes
### Falha na verificação da URL de callback
- Confirme que o firewall do servidor tem a porta correspondente aberta (padrão 18790)
- Confirme que `token` e `encoding_aes_key` estão preenchidos corretamente
- Verifique os logs do PicoClaw para ver se uma requisição GET do WeCom foi recebida
### Mensagens sem resposta
- Verifique se `allow_from` está restringindo acidentalmente o remetente
- Procure por `context canceled` ou erros do agente nos logs
- Confirme que a configuração do agente (ex.: `model_name`) está correta
### Nenhum push final recebido para tarefas longas
- Confirme que o callback da mensagem inclui `response_url` (suportado apenas pelo novo WeCom AI Bot)
- Confirme que o servidor consegue fazer requisições de saída (precisa fazer POST para `response_url`)
- Verifique nos logs as palavras-chave `response_url mode` e `Sending reply via response_url`
## Referências
- [Documentação de integração do WeCom AI Bot](https://developer.work.weixin.qq.com/document/path/100719)
- [Descrição do protocolo de resposta em streaming](https://developer.work.weixin.qq.com/document/path/100719)
- [Resposta proativa via response_url](https://developer.work.weixin.qq.com/document/path/101138)
@@ -0,0 +1,118 @@
> Quay lại [README](../../../../README.vi.md)
# WeCom AI Bot
WeCom AI Bot là phương thức tích hợp hội thoại AI chính thức do WeCom cung cấp. Hỗ trợ cả chat riêng tư và chat nhóm, tích hợp giao thức phản hồi streaming, và hỗ trợ chủ động đẩy phản hồi cuối cùng qua `response_url` sau khi hết thời gian chờ.
## So sánh với các kênh WeCom khác
| Tính năng | WeCom Bot | WeCom App | **WeCom AI Bot** |
|-----------|-----------|-----------|-----------------|
| Chat riêng tư | ✅ | ✅ | ✅ |
| Chat nhóm | ✅ | ❌ | ✅ |
| Đầu ra streaming | ❌ | ❌ | ✅ |
| Đẩy chủ động khi timeout | ❌ | ✅ | ✅ |
| Độ phức tạp cấu hình | Thấp | Cao | Trung bình |
## Cấu hình
```json
{
"channels": {
"wecom_aibot": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-aibot",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"max_steps": 10
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ---------------- | ------ | --------- | -------------------------------------------------- |
| token | string | Có | Token xác minh callback, cấu hình trên trang quản lý AI Bot |
| encoding_aes_key | string | Có | Khóa AES 43 ký tự, được tạo ngẫu nhiên trên trang quản lý AI Bot |
| webhook_path | string | Không | Đường dẫn webhook (mặc định: /webhook/wecom-aibot) |
| allow_from | array | Không | Danh sách cho phép ID người dùng; mảng rỗng cho phép tất cả người dùng |
| welcome_message | string | Không | Tin nhắn chào mừng gửi khi người dùng mở chat; để trống để tắt |
| reply_timeout | int | Không | Thời gian chờ phản hồi tính bằng giây (mặc định: 5) |
| max_steps | int | Không | Số bước thực thi tối đa của agent (mặc định: 10) |
## Hướng dẫn thiết lập
1. Đăng nhập vào [Bảng điều khiển quản trị WeCom](https://work.weixin.qq.com/wework_admin)
2. Vào "Quản lý ứng dụng" → "AI Bot", sau đó tạo hoặc chọn một AI Bot
3. Trên trang cấu hình AI Bot, điền thông tin "Nhận tin nhắn":
- **URL**: `http://<your-server-ip>:18790/webhook/wecom-aibot`
- **Token**: Tạo ngẫu nhiên hoặc tùy chỉnh
- **EncodingAESKey**: Nhấp "Tạo ngẫu nhiên" để lấy khóa 43 ký tự
4. Nhập Token và EncodingAESKey vào file cấu hình PicoClaw, khởi động dịch vụ rồi quay lại bảng điều khiển quản trị để lưu (WeCom sẽ gửi yêu cầu xác minh)
> [!TIP]
> Máy chủ cần có thể truy cập được từ các máy chủ WeCom. Nếu bạn đang ở mạng nội bộ hoặc phát triển cục bộ, hãy sử dụng [ngrok](https://ngrok.com) hoặc frp để tạo tunnel.
## Giao thức phản hồi streaming
WeCom AI Bot sử dụng giao thức "pull streaming", khác với phản hồi một lần của webhook thông thường:
```
Người dùng gửi tin nhắn
PicoClaw trả về ngay {finish: false} (Agent bắt đầu xử lý)
WeCom pull khoảng mỗi 1 giây với {msgtype: "stream", stream: {id: "..."}}
├─ Agent chưa xong → trả về {finish: false} (tiếp tục chờ)
└─ Agent xong → trả về {finish: true, content: "nội dung phản hồi"}
```
**Xử lý timeout** (tác vụ vượt quá 30 giây):
Nếu thời gian xử lý của agent vượt quá khoảng 30 giây (cửa sổ polling tối đa của WeCom là 6 phút), PicoClaw sẽ:
1. Đóng stream ngay lập tức và hiển thị cho người dùng: "⏳ 正在处理中,请稍候,结果将稍后发送。"
2. Agent tiếp tục chạy ở nền
3. Sau khi agent hoàn thành, phản hồi cuối cùng được chủ động đẩy đến người dùng qua `response_url` có trong tin nhắn
> `response_url` do WeCom cấp, có hiệu lực 1 giờ, chỉ dùng được một lần, không cần mã hóa — chỉ cần POST trực tiếp nội dung tin nhắn markdown.
## Tin nhắn chào mừng
Khi `welcome_message` được cấu hình, PicoClaw sẽ tự động phản hồi bằng tin nhắn đó khi người dùng mở cửa sổ chat với AI Bot (sự kiện `enter_chat`). Để trống để bỏ qua im lặng.
```json
"welcome_message": "你好!我是 PicoClaw AI 助手,有什么可以帮你?"
```
## Câu hỏi thường gặp
### Xác minh URL callback thất bại
- Xác nhận tường lửa máy chủ đã mở cổng tương ứng (mặc định 18790)
- Xác nhận `token``encoding_aes_key` được điền đúng
- Kiểm tra log PicoClaw xem có nhận được yêu cầu GET từ WeCom không
### Tin nhắn không nhận được phản hồi
- Kiểm tra xem `allow_from` có vô tình hạn chế người gửi không
- Tìm `context canceled` hoặc lỗi agent trong log
- Xác nhận cấu hình agent (ví dụ: `model_name`) là đúng
### Không nhận được push cuối cùng cho tác vụ dài
- Xác nhận callback tin nhắn có chứa `response_url` (chỉ hỗ trợ bởi WeCom AI Bot phiên bản mới)
- Xác nhận máy chủ có thể thực hiện yêu cầu ra ngoài (cần POST đến `response_url`)
- Kiểm tra log với từ khóa `response_url mode``Sending reply via response_url`
## Tài liệu tham khảo
- [Tài liệu tích hợp WeCom AI Bot](https://developer.work.weixin.qq.com/document/path/100719)
- [Mô tả giao thức phản hồi streaming](https://developer.work.weixin.qq.com/document/path/100719)
- [Phản hồi chủ động qua response_url](https://developer.work.weixin.qq.com/document/path/101138)
+92 -23
View File
@@ -1,6 +1,11 @@
> 返回 [README](../../../../README.zh.md)
# 企业微信智能机器人 (AI Bot)
企业微信智能机器人(AI Bot)是企业微信官方提供的 AI 对话接入方式,支持私聊与群聊,内置流式响应协议,并支持超时后通过 `response_url` 主动推送最终回复。
企业微信智能机器人(AI Bot)是企业微信官方提供的 AI 对话接入方式,支持私聊与群聊,内置流式响应协议。PicoClaw 当前同时支持两种接入模式:
- WebSocket 长连接模式:使用 `bot_id` + `secret`,优先级更高,推荐使用
- Webhook 短连接模式:使用 `token` + `encoding_aes_key`,兼容传统回调,并支持超时后通过 `response_url` 主动推送最终回复
## 与其他 WeCom 通道的对比
@@ -14,6 +19,25 @@
## 配置
### WebSocket 长连接模式(推荐)
```json
{
"channels": {
"wecom_aibot": {
"enabled": true,
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"max_steps": 10
}
}
}
```
### Webhook 短连接模式
```json
{
"channels": {
@@ -24,38 +48,68 @@
"webhook_path": "/webhook/wecom-aibot",
"allow_from": [],
"welcome_message": "你好!有什么可以帮助你的吗?",
"processing_message": "⏳ Processing, please wait. The results will be sent shortly.",
"max_steps": 10
}
}
}
```
| 字段 | 类型 | 必填 | 描述 |
| ---------------- | ------ | ---- | -------------------------------------------------- |
| token | string | 是 | 回调验证令牌,在 AI Bot 管理页面配置 |
| encoding_aes_key | string | 是 | 43 字符 AES 密钥,在 AI Bot 管理页面随机生成 |
| webhook_path | string | | Webhook 路径(默认:/webhook/wecom-aibot |
| allow_from | array | | 用户 ID 白名单,空数组表示允许所有用户 |
| welcome_message | string | 否 | 用户进入聊天时发送的欢迎语,留空则不发送 |
| reply_timeout | int | 否 | 回复超时时间(秒,默认:5) |
| max_steps | int | 否 | Agent 最大执行步骤数(默认:10) |
### WebSocket 模式字段
| 字段 | 类型 | 必填 | 描述 |
|--------|--------|------|--------------------------------------------|
| bot_id | string | | AI Bot 的唯一标识,在 AI Bot 管理页面配置 |
| secret | string | | AI Bot 的密钥,在 AI Bot 管理页面配置 |
### Webhook 模式字段
| 字段 | 类型 | 必填 | 描述 |
|------------------|--------|------|----------------------------------------------|
| token | string | 是 | 回调验证令牌,在 AI Bot 管理页面配置 |
| encoding_aes_key | string | 是 | 43 字符 AES 密钥,在 AI Bot 管理页面随机生成 |
| webhook_path | string | 否 | Webhook 路径,默认 `/webhook/wecom-aibot` |
| processing_message | string | 否 | 流式超时后返回给用户的提示语 |
### 通用字段
| 字段 | 类型 | 必填 | 描述 |
|-----------------|--------|------|------------------------------------------|
| allow_from | array | 否 | 用户 ID 白名单,空数组表示允许所有用户 |
| welcome_message | string | 否 | 用户进入聊天时发送的欢迎语,留空则不发送 |
| reply_timeout | int | 否 | 回复超时时间(秒,默认:5) |
| max_steps | int | 否 | Agent 最大执行步骤数(默认:10) |
## 模式选择
- 当 `bot_id``secret` 同时存在时,PicoClaw 会优先使用 WebSocket 长连接模式
- 否则,当 `token``encoding_aes_key` 同时存在时,PicoClaw 会使用 Webhook 短连接模式
## 设置流程
### WebSocket 长连接模式
1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin)
2. 进入"应用管理" → "智能机器人",创建或选择一个 AI Bot
3. 在 AI Bot 配置页面,配置 Bot 的名称、头像等信息,获取 `Bot ID``Secret`
4. 在 PicoClaw 配置文件中添加上述配置,重启 PicoClaw
### Webhook 短连接模式
1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin)
2. 进入"应用管理" → "智能机器人",创建或选择一个 AI Bot
3. 在 AI Bot 配置页面,填写"消息接收"信息:
- **URL**`http://<your-server-ip>:18791/webhook/wecom-aibot`
- **URL**`http://<your-server-ip>:18790/webhook/wecom-aibot`
- **Token**:随机生成或自定义
- **EncodingAESKey**:点击"随机生成",得到 43 字符密钥
4. 将 Token 和 EncodingAESKey 填入 PicoClaw 配置文件,启动服务后回到管理后台保存(企业微信会发送验证请求)
4. 将 Token 和 EncodingAESKey 填入 PicoClaw 配置文件,启动服务后回到管理后台保存
> [!TIP]
> 服务器需要能被企业微信服务器访问。如在内网/本地开发,可使用 [ngrok](https://ngrok.com) 或 frp 做内网穿透。
> 服务器需要能被企业微信服务器访问。如在内网本地开发,可使用 [ngrok](https://ngrok.com) 或 frp 做内网穿透。
## 流式响应协议
## Webhook 模式的流式响应协议
WeCom AI Bot 使用"流式拉取"协议,区别于普通 Webhook 的一次性回复:
Webhook 模式使用"流式拉取"协议,区别于普通 Webhook 的一次性回复:
```
用户发消息
@@ -71,16 +125,24 @@ PicoClaw 立即返回 {finish: false}Agent 开始处理)
└─ Agent 完成 → 返回 {finish: true, content: "回答内容"}
```
**超时处理**(任务超过 30 秒):
**超时处理**(任务超过 30 秒):
若 Agent 处理时间超过约 30 秒(企业微信最大轮询窗口为 6 分钟)PicoClaw 会:
若 Agent 处理时间超过轮询窗口PicoClaw 会:
1. 立即关闭流,向用户显示「⏳ 正在处理中,请稍候,结果将稍后发送。」
1. 立即关闭流,向用户显示 `processing_message` 提示语
2. Agent 继续在后台运行
3. Agent 完成后,通过消息中携带的 `response_url` 将最终回复主动推送给用户
> `response_url` 由企业微信颁发,有效期 1 小时,只可使用一次,无需加密,直接 POST markdown 消息体即可。
## 超时提示语
配置 `processing_message` 后,当 Webhook 模式的流式轮询超时并切换到 `response_url` 主动推送模式时,PicoClaw 会先返回这段提示语来结束当前流。
```json
"processing_message": "⏳ Processing, please wait. The results will be sent shortly."
```
## 欢迎语
配置 `welcome_message` 后,当用户打开与 AI Bot 的聊天窗口时(`enter_chat` 事件),PicoClaw 会自动回复该欢迎语。留空则静默忽略。
@@ -91,11 +153,18 @@ PicoClaw 立即返回 {finish: false}Agent 开始处理)
## 常见问题
### WebSocket 模式无法连接
- 检查 `bot_id``secret` 是否填写正确
- 查看日志中是否有 WebSocket 连接或鉴权失败信息
- 确认服务器可以访问企业微信长连接接口
### 回调 URL 验证失败
- 确认服务器防火墙已开放对应端口(默认 18791)
- 确认 `token``encoding_aes_key` 填写正确
- 检查 PicoClaw 日志是否收到了来自企业微信的 GET 请求
- 确认服务器防火墙已开放对应端口
- 检查 PicoClaw 日志是否收到了来自企业微信的验证请求
### 消息没有回复
@@ -105,12 +174,12 @@ PicoClaw 立即返回 {finish: false}Agent 开始处理)
### 超长任务没有收到最终推送
- 确认消息回调中携带了 `response_url`(仅企业微信新版 AI Bot 支持)
- 确认服务器能主动访问外网(需向 `response_url` POST 请求)
- 确认消息回调中携带了 `response_url`
- 确认服务器能主动访问外网
- 查看日志关键词 `response_url mode``Sending reply via response_url`
## 参考文档
- [企业微信 AI Bot 接入文档](https://developer.work.weixin.qq.com/document/path/100719)
- [企业微信 AI Bot 接入文档](https://developer.work.weixin.qq.com/document/path/101463)
- [流式响应协议说明](https://developer.work.weixin.qq.com/document/path/100719)
- [response_url 主动回复](https://developer.work.weixin.qq.com/document/path/101138)
@@ -0,0 +1,47 @@
> Retour au [README](../../../../README.fr.md)
# Application interne WeCom
Une application interne WeCom est une application créée par une entreprise au sein de WeCom, principalement destinée à un usage interne. Grâce aux applications internes WeCom, les entreprises peuvent assurer une communication et une collaboration efficaces avec leurs employés, améliorant ainsi la productivité.
## Configuration
```json
{
"channels": {
"wecom_app": {
"enabled": true,
"corp_id": "wwxxxxxxxxxxxxxxxx",
"corp_secret": "YOUR_CORP_SECRET",
"agent_id": 1000002,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-app",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| Champ | Type | Requis | Description |
| ---------------- | ------ | ------ | ---------------------------------------- |
| corp_id | string | Oui | ID de l'entreprise |
| corp_secret | string | Oui | Secret de l'application |
| agent_id | int | Oui | ID de l'agent de l'application |
| token | string | Oui | Jeton de vérification du callback |
| encoding_aes_key | string | Oui | Clé AES de 43 caractères |
| webhook_path | string | Non | Chemin du webhook (par défaut : /webhook/wecom-app) |
| allow_from | array | Non | Liste blanche d'ID utilisateurs |
| reply_timeout | int | Non | Délai de réponse en secondes |
## Procédure de configuration
1. Connectez-vous à la [console d'administration WeCom](https://work.weixin.qq.com/)
2. Accédez à « Gestion des applications » -> « Créer une application »
3. Obtenez l'ID d'entreprise (CorpID) et le Secret de l'application
4. Configurez « Réception des messages » dans les paramètres de l'application pour obtenir le Token et l'EncodingAESKey
5. Définissez l'URL de callback sur `http://<your-server-ip>:<port>/webhook/wecom-app`
6. Saisissez le CorpID, le Secret, l'AgentID et les autres informations dans le fichier de configuration
Remarque : PicoClaw utilise désormais un serveur HTTP Gateway partagé pour recevoir les callbacks webhook de tous les canaux. L'adresse d'écoute par défaut est 127.0.0.1:18790. Pour recevoir des callbacks depuis l'internet public, configurez un reverse proxy de votre domaine externe vers le Gateway (port par défaut 18790).
@@ -0,0 +1,47 @@
> [README](../../../../README.ja.md) に戻る
# 企業WeChat 自社開発アプリ
企業WeChat 自社開発アプリとは、企業が企業WeChat内で作成するアプリケーションで、主に社内利用を目的としています。企業WeChat 自社開発アプリを通じて、企業は従業員との効率的なコミュニケーションと協業を実現し、業務効率を向上させることができます。
## 設定
```json
{
"channels": {
"wecom_app": {
"enabled": true,
"corp_id": "wwxxxxxxxxxxxxxxxx",
"corp_secret": "YOUR_CORP_SECRET",
"agent_id": 1000002,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-app",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------------- | ------ | ---- | ---------------------------------------- |
| corp_id | string | はい | 企業ID |
| corp_secret | string | はい | アプリケーションシークレット |
| agent_id | int | はい | アプリケーションエージェントID |
| token | string | はい | コールバック検証トークン |
| encoding_aes_key | string | はい | 43文字のAESキー |
| webhook_path | string | いいえ | Webhookパス(デフォルト:/webhook/wecom-app |
| allow_from | array | いいえ | ユーザーIDの許可リスト |
| reply_timeout | int | いいえ | 返信タイムアウト(秒) |
## セットアップ手順
1. [企業WeChat管理コンソール](https://work.weixin.qq.com/) にログイン
2. 「アプリ管理」→「アプリを作成」に進む
3. 企業IDCorpID)とアプリのSecretを取得
4. アプリ設定で「メッセージ受信」を設定し、TokenとEncodingAESKeyを取得
5. コールバックURLを `http://<your-server-ip>:<port>/webhook/wecom-app` に設定
6. CorpID、Secret、AgentIDなどの情報を設定ファイルに入力
注意:PicoClawは現在、すべてのチャンネルのwebhookコールバックを受信するために共有のGateway HTTPサーバーを使用しています。デフォルトのリスニングアドレスは127.0.0.1:18790です。公共インターネットからコールバックを受信するには、外部ドメインをGateway(デフォルトポート18790)にリバースプロキシしてください。
+47
View File
@@ -0,0 +1,47 @@
> Back to [README](../../../../README.md)
# WeCom Internal App
A WeCom Internal App is an application created by an enterprise within WeCom, primarily intended for internal use. Through WeCom Internal Apps, enterprises can achieve efficient communication and collaboration with employees, improving productivity.
## Configuration
```json
{
"channels": {
"wecom_app": {
"enabled": true,
"corp_id": "wwxxxxxxxxxxxxxxxx",
"corp_secret": "YOUR_CORP_SECRET",
"agent_id": 1000002,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-app",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | ---------------------------------------- |
| corp_id | string | Yes | Enterprise ID |
| corp_secret | string | Yes | Application secret |
| agent_id | int | Yes | Application agent ID |
| token | string | Yes | Callback verification token |
| encoding_aes_key | string | Yes | 43-character AES key |
| webhook_path | string | No | Webhook path (default: /webhook/wecom-app) |
| allow_from | array | No | User ID allowlist |
| reply_timeout | int | No | Reply timeout in seconds |
## Setup
1. Log in to the [WeCom Admin Console](https://work.weixin.qq.com/)
2. Go to "App Management" -> "Create App"
3. Obtain the Enterprise ID (CorpID) and App Secret
4. Configure "Receive Messages" in the app settings to get the Token and EncodingAESKey
5. Set the callback URL to `http://<your-server-ip>:<port>/webhook/wecom-app`
6. Enter the CorpID, Secret, AgentID, and other details into the config file
Note: PicoClaw now uses a shared Gateway HTTP server to receive webhook callbacks for all channels. The default listening address is 127.0.0.1:18790. To receive callbacks from the public internet, reverse-proxy your external domain to the Gateway (default port 18790).
@@ -0,0 +1,47 @@
> Voltar ao [README](../../../../README.pt-br.md)
# App Interno WeCom
Um App Interno WeCom é um aplicativo criado por uma empresa dentro do WeCom, destinado principalmente ao uso interno. Por meio dos Apps Internos WeCom, as empresas podem alcançar comunicação e colaboração eficientes com os funcionários, melhorando a produtividade.
## Configuração
```json
{
"channels": {
"wecom_app": {
"enabled": true,
"corp_id": "wwxxxxxxxxxxxxxxxx",
"corp_secret": "YOUR_CORP_SECRET",
"agent_id": 1000002,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-app",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
| ---------------- | ------ | ----------- | ---------------------------------------- |
| corp_id | string | Sim | ID da empresa |
| corp_secret | string | Sim | Segredo da aplicação |
| agent_id | int | Sim | ID do agente da aplicação |
| token | string | Sim | Token de verificação de callback |
| encoding_aes_key | string | Sim | Chave AES de 43 caracteres |
| webhook_path | string | Não | Caminho do webhook (padrão: /webhook/wecom-app) |
| allow_from | array | Não | Lista de permissão de IDs de usuários |
| reply_timeout | int | Não | Timeout de resposta em segundos |
## Configuração passo a passo
1. Faça login no [Console de Administração do WeCom](https://work.weixin.qq.com/)
2. Acesse "Gerenciamento de Apps" -> "Criar App"
3. Obtenha o ID da Empresa (CorpID) e o Secret do App
4. Configure "Receber Mensagens" nas configurações do app para obter o Token e o EncodingAESKey
5. Defina a URL de callback como `http://<your-server-ip>:<port>/webhook/wecom-app`
6. Insira o CorpID, Secret, AgentID e outras informações no arquivo de configuração
Nota: O PicoClaw agora usa um servidor HTTP Gateway compartilhado para receber callbacks de webhook de todos os canais. O endereço de escuta padrão é 127.0.0.1:18790. Para receber callbacks da internet pública, configure um reverse proxy do seu domínio externo para o Gateway (porta padrão 18790).
@@ -0,0 +1,47 @@
> Quay lại [README](../../../../README.vi.md)
# Ứng dụng nội bộ WeCom
Ứng dụng nội bộ WeCom là ứng dụng được doanh nghiệp tạo ra trong WeCom, chủ yếu dùng cho mục đích nội bộ. Thông qua ứng dụng nội bộ WeCom, doanh nghiệp có thể thực hiện giao tiếp và cộng tác hiệu quả với nhân viên, nâng cao hiệu suất làm việc.
## Cấu hình
```json
{
"channels": {
"wecom_app": {
"enabled": true,
"corp_id": "wwxxxxxxxxxxxxxxxx",
"corp_secret": "YOUR_CORP_SECRET",
"agent_id": 1000002,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_path": "/webhook/wecom-app",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| Trường | Kiểu | Bắt buộc | Mô tả |
| ---------------- | ------ | --------- | ---------------------------------------- |
| corp_id | string | Có | ID doanh nghiệp |
| corp_secret | string | Có | Secret của ứng dụng |
| agent_id | int | Có | ID agent của ứng dụng |
| token | string | Có | Token xác minh callback |
| encoding_aes_key | string | Có | Khóa AES 43 ký tự |
| webhook_path | string | Không | Đường dẫn webhook (mặc định: /webhook/wecom-app) |
| allow_from | array | Không | Danh sách cho phép ID người dùng |
| reply_timeout | int | Không | Thời gian chờ phản hồi tính bằng giây |
## Hướng dẫn thiết lập
1. Đăng nhập vào [Bảng điều khiển quản trị WeCom](https://work.weixin.qq.com/)
2. Vào "Quản lý ứng dụng" -> "Tạo ứng dụng"
3. Lấy ID doanh nghiệp (CorpID) và Secret của ứng dụng
4. Cấu hình "Nhận tin nhắn" trong cài đặt ứng dụng để lấy Token và EncodingAESKey
5. Đặt URL callback thành `http://<your-server-ip>:<port>/webhook/wecom-app`
6. Nhập CorpID, Secret, AgentID và các thông tin khác vào file cấu hình
Lưu ý: PicoClaw hiện sử dụng máy chủ HTTP Gateway dùng chung để nhận callback webhook cho tất cả các kênh. Địa chỉ lắng nghe mặc định là 127.0.0.1:18790. Để nhận callback từ internet công cộng, hãy cấu hình reverse proxy từ tên miền bên ngoài của bạn đến Gateway (cổng mặc định 18790).
@@ -1,3 +1,5 @@
> 返回 [README](../../../../README.zh.md)
# 企业微信自建应用
企业微信自建应用是指企业在企业微信中创建的应用,主要用于企业内部使用。通过企业微信自建应用,企业可以实现与员工的高效沟通和协作,提高工作效率。
@@ -0,0 +1,41 @@
> Retour au [README](../../../../README.fr.md)
# WeCom Bot
Le WeCom Bot est une méthode d'intégration rapide fournie par WeCom, permettant de recevoir des messages via une URL Webhook.
## Configuration
```json
{
"channels": {
"wecom": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
"webhook_path": "/webhook/wecom",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| Champ | Type | Requis | Description |
| ---------------- | ------ | ------ | -------------------------------------------- |
| token | string | Oui | Jeton de vérification de signature |
| encoding_aes_key | string | Oui | Clé AES de 43 caractères utilisée pour le déchiffrement |
| webhook_url | string | Oui | URL Webhook du bot de groupe WeCom utilisée pour envoyer les réponses |
| webhook_path | string | Non | Chemin de l'endpoint webhook (par défaut : /webhook/wecom) |
| allow_from | array | Non | Liste blanche d'ID utilisateurs (vide = autoriser tous les utilisateurs) |
| reply_timeout | int | Non | Délai de réponse en secondes (par défaut : 5) |
## Procédure de configuration
1. Ajouter un bot à un groupe WeCom
2. Obtenir l'URL Webhook
3. (Pour recevoir des messages) Configurer l'adresse API de réception des messages (URL de callback), le Token et l'EncodingAESKey sur la page de configuration du bot
4. Saisir les informations pertinentes dans le fichier de configuration
Remarque : PicoClaw utilise désormais un serveur HTTP Gateway partagé pour recevoir les callbacks webhook de tous les canaux. L'adresse d'écoute par défaut est 127.0.0.1:18790. Pour recevoir des callbacks depuis l'internet public, configurez un reverse proxy de votre domaine externe vers le Gateway (port par défaut 18790).
@@ -0,0 +1,41 @@
> [README](../../../../README.ja.md) に戻る
# 企業WeChat ボット
企業WeChat ボットは、企業WeChatが提供するWebhook URLを通じてメッセージを受信できる迅速な連携方式です。
## 設定
```json
{
"channels": {
"wecom": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
"webhook_path": "/webhook/wecom",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| フィールド | 型 | 必須 | 説明 |
| ---------------- | ------ | ---- | -------------------------------------------- |
| token | string | はい | 署名検証トークン |
| encoding_aes_key | string | はい | 復号化に使用する43文字のAESキー |
| webhook_url | string | はい | 返信送信に使用する企業WeChatグループボットのWebhook URL |
| webhook_path | string | いいえ | Webhookエンドポイントパス(デフォルト:/webhook/wecom |
| allow_from | array | いいえ | ユーザーIDの許可リスト(空 = 全ユーザーを許可) |
| reply_timeout | int | いいえ | 返信タイムアウト(秒、デフォルト:5) |
## セットアップ手順
1. 企業WeChatグループにボットを追加
2. Webhook URLを取得
3. (メッセージを受信する場合)ボット設定ページでメッセージ受信APIアドレス(コールバックURL)、Token、EncodingAESKeyを設定
4. 関連情報を設定ファイルに入力
注意:PicoClawは現在、すべてのチャンネルのwebhookコールバックを受信するために共有のGateway HTTPサーバーを使用しています。デフォルトのリスニングアドレスは127.0.0.1:18790です。公共インターネットからコールバックを受信するには、外部ドメインをGateway(デフォルトポート18790)にリバースプロキシしてください。
+41
View File
@@ -0,0 +1,41 @@
> Back to [README](../../../../README.md)
# WeCom Bot
WeCom Bot is a quick integration method provided by WeCom that can receive messages via a Webhook URL.
## Configuration
```json
{
"channels": {
"wecom": {
"enabled": true,
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
"webhook_path": "/webhook/wecom",
"allow_from": [],
"reply_timeout": 5
}
}
}
```
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | -------------------------------------------- |
| token | string | Yes | Signature verification token |
| encoding_aes_key | string | Yes | 43-character AES key used for decryption |
| webhook_url | string | Yes | WeCom group bot webhook URL used to send replies |
| webhook_path | string | No | Webhook endpoint path (default: /webhook/wecom) |
| allow_from | array | No | User ID allowlist (empty = allow all users) |
| reply_timeout | int | No | Reply timeout in seconds (default: 5) |
## Setup
1. Add a bot to a WeCom group
2. Obtain the Webhook URL
3. (To receive messages) Configure the message receiving API address (callback URL), Token, and EncodingAESKey on the bot configuration page
4. Enter the relevant information into the config file
Note: PicoClaw now uses a shared Gateway HTTP server to receive webhook callbacks for all channels. The default listening address is 127.0.0.1:18790. To receive callbacks from the public internet, reverse-proxy your external domain to the Gateway (default port 18790).

Some files were not shown because too many files have changed in this diff Show More