refactor(config): keep mainline features with pure viper/mapstructure parsing

This commit is contained in:
sky5454 2026-02-20 18:34:19 +08:00
parent 41e6982f8d
commit 51ddfcac34
5 changed files with 826 additions and 622 deletions

View file

@ -3,7 +3,7 @@
<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1> <h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>
<h3>10$遑ャ莉カ ツキ 10MB蜀<42><EFBFBD> ツキ 1遘貞星蜉?ツキ 逧ョ逧ョ陌セ<E9998C><EFBFBD>莉ャ襍ー<E8A58D><EFBDB0></h3> <h3>10$硬件 · 10MB内存 · 1秒启动 · 皮皮虾,我们走!</h3>
<p> <p>
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go"> <img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
@ -19,9 +19,9 @@
--- ---
<EFBFBD><EFBFBD> **PicoClaw** 譏ッ荳€荳ェ蜿<EFBDAA> [nanobot](https://github.com/HKUDS/nanobot) 蜷ッ蜿醍噪雜<E599AA>スサ驥冗コァ荳ェ莠?AI 蜉ゥ謇九€ょョ<E38287>㊦逕ィ **Go 隸ュ險€** 莉朱峺驥肴桷<E882B4>檎サ丞紙莠<E7B499>€荳ェ窶懆<E7AAB6>荳セ窶晁ソ<E69981>ィ銀€€泌叉逕?AI Agent 閾ェ霄ォ鬩ア蜉ィ莠<EFBDA8>紛荳ェ譫カ譫<EFBDB6>ソ∫ァサ蜥御サ」遐∽シ伜喧縲? 🦐 **PicoClaw** 是一个受 [nanobot](https://github.com/HKUDS/nanobot) 启发的超轻量级个人 AI 助手。它采用 **Go 语言** 从零重构,经历了一个“自举”过程——即由 AI Agent 自身驱动了整个架构迁移和代码优化。
笞。<EFBFBD><EFBFBD> **譫∬<E8ADAB>霓サ驥<EFBDBB>**<EFBFBD>壼庄蝨?**10 鄒主<E98492>** 逧<>。ャ莉カ荳願ソ占。鯉シ悟<EFBDBC>蟄伜頃逕?**<10MB**縲りソ呎э蜻ウ逹€?OpenClaw 闃ら怐 99% <EFBFBD><EFBFBD>蟄假シ梧ッ?Mac mini 萓ソ螳<EFBFBD> 98%<25>? ⚡️ **极致轻量**:可在 **10 美元** 的硬件上运行,内存占用 **<10MB**。这意味着比 OpenClaw 节省 99% 的内存 Mac mini 便宜 98%
<table align="center"> <table align="center">
<tr align="center"> <tr align="center">
@ -38,55 +38,55 @@
</tr> </tr>
</table> </table>
豕ィ諢擾シ壻ココ謇区怏髯撰シ御クュ譁<EFBFBD>枚譯」蜿ッ閭ス逡・譛画サ槫錘<EFBFBD>瑚ッキ莨伜<EFBFBD>譟・逵玖恭譁<EFBFBD>枚譯」縲? 注意:人手有限,中文文档可能略有滞后,请优先查看英文文档。
> [!CAUTION] > [!CAUTION]
> **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明** > **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明**
> * **<EFBFBD><EFBFBD><EFBFBD>エァ蟶?(NO CRYPTO):** PicoClaw **豐。譛<EFBDA1>** 蜿題。御ササ菴募ョ俶婿莉」蟶√€ゝoken 謌冶劒諡溯エァ蟶√€よ園譛牙惠 `pump.fun` 謌門<E8AC8C>莉紋コ、譏灘ケウ蜿ー荳顔噪逶ク蜈ウ螢ー遘ー蝮<EFBDB0>ク?**隸磯ェ<E7A3AF>**縲? > * **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**
> * **螳俶婿蝓溷錐:** 蜚ッ荳€<C280>ョ俶婿鄂醍ォ呎弍 **[picoclaw.io](https://picoclaw.io)**<2A><EFBFBD>蜿ク螳倡ス第弍 **[sipeed.com](https://sipeed.com)**縲? > * **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
> * **隴ヲ諠<EFBFBD>:** 隶ク螟<EFBDB8> `.ai/.org/.com/.net/...` 蜷守シ€<C280>沺蜷崎「ォ隨ャ荳画婿謚「豕ィ<E8B195>瑚ッキ蜍ソ霓サ菫。縲? > * **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
> * **注意:** picoclaw正在初期的快速功能开发阶段可能有尚未修复的网络安全问题在1.0正式版发布前,请不要将其部署到生产环境中 > * **注意:** picoclaw正在初期的快速功能开发阶段可能有尚未修复的网络安全问题在1.0正式版发布前,请不要将其部署到生产环境中
> * **豕ィ諢<EFBFBD>:** picoclaw譛€霑大粋蟷カ莠<EFBDB6>、ァ驥襲Rs<52>瑚ソ第悄迚域悽蜿ッ閭ス蜀<EFBDBD>ュ伜頃逕ィ霎<EFBDA8>、?10~20MB)<29><EFBFBD>莉ャ蟆<EFBDAC>惠蜉溯<E89C89><EFBFBD>クコ謾カ謨帛錘霑幄。瑚オ<E7919A>コ仙頃逕ィ莨伜喧. > * **注意:** picoclaw最近合并了大量PRs近期版本可能内存占用较大(10~20MB),我们将在功能较为收敛后进行资源占用优化.
## 📢 新闻 (News) ## 📢 新闻 (News)
2026-02-16 <EFBFBD>脂 PicoClaw 蝨ィ荳€蜻ィ蜀<EFBDA8>ェ∫<EFBDAA>エ莠?2K star! 諢溯ー「螟ァ螳カ逧<EFBDB6><E980A7>豕ィ<E8B195> ̄icoClaw 逧<><E980A7>髟ソ騾溷コヲ雜<EFBDA6>ケ取<EFBDB9>莉ャ鬚<EFBDAC>悄. 逕ア莠傘R謨ー驥冗噪蠢ォ騾溯<E9A8BE>€<E996AD><EFBFBD>莉ャ莠滄怙遉セ蛹コ蠑€蜿題€<E9A18C>盾荳守サエ謚? 謌台サャ髴€隕∫噪蠢玲<E8A0A2><EFBFBD>ァ定牡蜥罫oadmap蟾イ扈丞書蟶<E69BB8>芦莠<E88AA6>霑咎㈹](docs/picoclaw_community_roadmap_260216.md), 譛溷セ<E6BAB7><EFBFBD><EFBFBD>盾荳趣シ? 2026-02-16 🎉 PicoClaw 在一周内突破了12K star! 感谢大家的关注PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀我们亟需社区开发者参与维护. 我们需要的志愿者角色和roadmap已经发布到了[这里](docs/picoclaw_community_roadmap_260216.md), 期待你的参与!
2026-02-13 <EFBFBD>**PicoClaw 蝨?4 螟ゥ蜀<EFBDA9>ェ∫<EFBDAA>エ 5000 Stars<72>?* 諢溯ー「遉セ蛹コ逧<EFBDBA>髪謖<E9ABAA>シ∫罰莠取ュ」蛟シ荳ュ蝗ス譏・闃ょ∞譛滂シ訓R 蜥?Issue 豸悟<E8B1B8><EFBFBD>、夲シ梧<EFBDBC>莉ャ豁」蝨ィ蛻ゥ逕ィ霑呎ョオ譌カ髣エ謨イ螳?**鬘ケ逶ョ霍ッ郤ソ蝗?(Roadmap)** 蟷カ扈<EFBDB6>サ?**蠑€蜿題€<E9A18C>セ、扈?*<2A>御サ・萓ソ蜉<EFBDBF>騾?PicoClaw 逧<>€蜿代€? 2026-02-13 🎉 **PicoClaw 在 4 天内突破 5000 Stars** 感谢社区的支持由于正值中国春节假期PR 和 Issue 涌入较多,我们正在利用这段时间敲定 **项目路线图 (Roadmap)** 并组建 **开发者群组**,以便加速 PicoClaw 的开发。
<EFBFBD>噫 **陦悟勘蜿キ蜿ャ<E89CBF>?* 隸キ蝨ィ GitHub Discussions 荳ュ謠蝉コ、謔ィ逧<EFBDA8>粥閭ス隸キ豎?(Feature Requests)縲よ<E7B8B2>莉ャ蟆<EFBDAC>惠謗・荳区擂逧<E69382>捉莨壻ク願ソ幄。悟ョ。譟・蜥御シ伜<EFBDBC>郤ァ謗貞コ上€? 🚀 **行动号召:** 请在 GitHub Discussions 中提交您的功能请求 (Feature Requests)。我们将在接下来的周会上进行审查和优先级排序。
2026-02-09 <EFBFBD>脂 **PicoClaw 豁」蠑丞書蟶<E69BB8>シ?* 莉<>畑 1 螟ゥ譫<EFBDA9>サコ<EFBDBB>梧葎蝨ィ蟆?AI Agent 蟶ヲ蜈・ 10 鄒主<E98492>遑ャ莉カ荳?<10MB <EFBFBD>ュ倡噪荳也阜縲を洶?PicoClaw<EFBFBD>育坩逧ョ陌セ<EFBFBD>会シ梧<EFBFBD>莉ャ襍ー<EFBFBD><EFBFBD> 2026-02-09 🎉 **PicoClaw 正式发布!** 仅用 1 天构建,旨在将 AI Agent 带入 10 美元硬件与 <10MB 内存的世界。🦐 PicoClaw皮皮虾我们走
## 笨?迚ケ諤? ## ✨ 特性
<EFBFBD>ェカ **雜<>スサ驥冗コァ**: 譬ク蠢<EFBDB8>粥閭ス蜀<EFBDBD>ュ伜頃逕ィ <10MB ??Clawdbot ?99%? 🪶 **超轻量级**: 核心功能内存占用 <10MB Clawdbot 99%。
<EFBFBD>**譫∽ス取<EFBDBD>譛ャ**: 鬮俶譜蛻ー雜ウ莉・蝨ィ 10 鄒主<E98492><EFBFBD>。ャ莉カ荳願ソ占。<E58DA0> 窶?豈?Mac mini 萓ソ螳<EFBDBF> 98%縲? 💰 **极低成本**: 高效到足以在 10 美元的硬件上运行 — 比 Mac mini 便宜 98%。
笞。<EFBFBD><EFBFBD> **髣ェ逕オ蜷ッ蜉ィ**: 蜷ッ蜉ィ騾溷コヲ蠢?400 蛟搾シ悟叉菴ソ蝨?0.6GHz 蜊墓<E89C8A>ク螟<EFBDB8>炊蝎ィ荳贋ケ溯<EFBDB9>蝨?1 遘貞<E98198>蜷ッ蜉ィ縲? ⚡️ **闪电启动**: 启动速度快 400 倍,即使在 0.6GHz 单核处理器上也能在 1 秒内启动。
<EFBFBD>訣 **逵滓ュ」蜿ッ遘サ讀?*: 霍?RISC-V縲、RM 蜥?x86 譫カ譫<EFBDB6>噪蜊穂コ瑚ソ帛宛譁<E5AE9B>サカ<EFBDBB>御ク€髞ョ霑占。鯉シ<E9AF89> 🌍 **真正可移植**: 跨 RISC-V、ARM 和 x86 架构的单二进制文件,一键运行!
<EFBFBD><EFBFBD> **AI 閾ェ荳セ**: 郤?Go 隸ュ險€蜴溽函螳樒鴫 窶?95% 逧<><E980A7>ク蠢<EFBDB8>サ」遐∫罰 Agent 逕滓<E98095><E6BB93>悟ケカ扈冗罰窶應ココ譛コ蝗樒<E89D97>?(Human-in-the-loop)窶晏セョ隹<EFBDAE>€? 🤖 **AI 自举**: 纯 Go 语言原生实现 — 95% 的核心代码由 Agent 生成,并经由“人机回环 (Human-in-the-loop)”微调。
| | OpenClaw | NanoBot | **PicoClaw** | | | OpenClaw | NanoBot | **PicoClaw** |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| **语言** | TypeScript | Python | **Go** | | **语言** | TypeScript | Python | **Go** |
| **RAM** | >1GB | >100MB | **< 10MB** | | **RAM** | >1GB | >100MB | **< 10MB** |
| **启动时间**</br>(0.8GHz core) | >500s | >30s | **<1s** | | **启动时间**</br>(0.8GHz core) | >500s | >30s | **<1s** |
| **謌先悽** | Mac Mini $599 | 螟ァ螟壽<E89E9F>?Linux 蠑€蜿第攸 ~$50 | **莉サ諢<EFBDBB> Linux 蠑€蜿第攸**</br>**菴手<E88FB4> $10** | | **成本** | Mac Mini $599 | 大多数 Linux 开发板 ~$50 | **任意 Linux 开发板**</br>**低至 $10** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512"> <img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 演示 ## 🦾 演示
### <EFBFBD><EFBFBD>?譬<>㊥蜉ゥ謇句キ・菴懈オ? ### 🛠️ 标准助手工作流
<table align="center"> <table align="center">
<tr align="center"> <tr align="center">
<th><p align="center"><EFBFBD>ァゥ 蜈ィ譬亥キ・遞句ク域ィ。蠑?/p></th> <th><p align="center">🧩 全栈工程师模式</p></th>
<th><p align="center"><EFBFBD><EFBFBD>?譌・蠢嶺ク手ァ<E6898B><EFBDA7>邂。逅?/p></th> <th><p align="center">🗂️ 日志与规划管理</p></th>
<th><p align="center"><EFBFBD>博 鄂醍サ懈頗邏「荳主ュヲ荵?/p></th> <th><p align="center">🔎 网络搜索与学习</p></th>
</tr> </tr>
<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_code.gif" width="240" height="180"></p></td>
@ -94,24 +94,24 @@
<td align="center"><p align="center"><img src="assets/picoclaw_search.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>
<tr> <tr>
<td align="center">蠑€蜿?窶?驛ィ鄂イ 窶?謇ゥ螻<EFBDA9></td> <td align="center">开发 • 部署 • 扩展</td>
<td align="center">譌・遞<EFBFBD> 窶?閾ェ蜉ィ蛹?窶?隶ー蠢<EFBDB0></td> <td align="center">日程 • 自动化 • 记忆</td>
<td align="center">蜿醍鴫 窶?豢槫ッ<E6A7AB> 窶?雜句漢</td> <td align="center">发现 • 洞察 • 趋势</td>
</tr> </tr>
</table> </table>
### 📱 在手机上轻松运行 ### 📱 在手机上轻松运行
picoclaw 蜿ッ莉・蟆<EFBFBD><EFBFBD>10蟷エ蜑咲噪閠∵立謇区惻蠎溽黄蛻ゥ逕ィ<EFBFBD>悟序霄ォ謌蝉クコ菴<EFBFBD>逧БI蜉ゥ逅<EFBFBD>シ∝ソォ騾滓欠蜊? picoclaw 可以将你10年前的老旧手机废物利用变身成为你的AI助理快速指南:
1. 先去应用商店下载安装Termux 1. 先去应用商店下载安装Termux
2. 謇灘シ€蜷取鴬陦梧欠莉? 2. 打开后执行指令
```bash ```bash
# 豕ィ諢<EFBFBD>: 荳矩擇逧ё0.1.1 蜿ッ莉・謐「荳コ菴<EFBDBA>螳樣刔逵句芦逧<E88AA6>怙譁ー迚域<E8BF9A>? # 注意: 下面的v0.1.1 可以换为你实际看到的最新版本
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64 wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
chmod +x picoclaw-linux-arm64 chmod +x picoclaw-linux-arm64
pkg install proot pkg install proot
termux-chroot ./picoclaw-linux-arm64 onboard termux-chroot ./picoclaw-linux-arm64 onboard
``` ```
辟カ蜷手キ滄囂荳矩擇逧<EFBFBD>€懷ソォ騾溷シ€蟋銀€晉ォ<EFBFBD>闃らサァ扈ュ驟咲スョpicoclaw蜊ウ蜿ッ菴ソ逕ィ<EFBFBD>? 然后跟随下面的“快速开始”章节继续配置picoclaw即可使用
<img src="assets/termux.jpg" alt="PicoClaw" width="512"> <img src="assets/termux.jpg" alt="PicoClaw" width="512">
@ -119,21 +119,21 @@ termux-chroot ./picoclaw-linux-arm64 onboard
### 🐜 创新的低占用部署 ### 🐜 创新的低占用部署
PicoClaw <EFBFBD>荵主庄莉・驛ィ鄂イ蝨ィ莉サ菴?Linux 隶セ螟<EFBDBE>ク奇シ<E5A587> PicoClaw 几乎可以部署在任何 Linux 设备上!
* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(鄂大哨) 謌?W(WiFi6) 迚域悽<E59F9F>檎畑莠取栫邂€螳カ蠎ュ蜉ゥ謇九€? * $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) 或 W(WiFi6) 版本,用于极简家庭助手。
* $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html)<EFBFBD><EFBFBD> $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html)<29>檎畑莠手<E88EA0>蜉ィ蛹匁恪蜉。蝎ィ霑千サエ縲? * $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html),或 $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html),用于自动化服务器运维。
* $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) 謌?$100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera)<EFBFBD>檎畑莠取匱閭ス逶第而縲? * $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera),用于智能监控。
[https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4](https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4) [https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4](https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4)
<EFBFBD>検 譖エ螟夐Κ鄂イ譯井セ区噴隸キ譛溷セ<E6BAB7>シ? 🌟 更多部署案例敬请期待!
## 📦 安装 ## 📦 安装
### 使用预编译二进制文件安装 ### 使用预编译二进制文件安装
莉?[Release 鬘オ髱「](https://github.com/sipeed/picoclaw/releases) 荳玖スス騾ら畑莠取お蟷ウ蜿ー逧<EFBDB0>崋莉カ縲? 从 [Release 页面](https://github.com/sipeed/picoclaw/releases) 下载适用于您平台的固件。
### 从源码安装(获取最新特性,开发推荐) ### 从源码安装(获取最新特性,开发推荐)
@ -143,20 +143,20 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# <EFBFBD>サコ<EFBFBD>域裏髴€螳芽」<EFBFBD>シ? # 构建(无需安装)
make build make build
# 为多平台构建 # 为多平台构建
make build-all make build-all
# <EFBFBD>サコ蟷カ螳芽」? # 构建并安装
make install make install
``` ```
## 🐳 Docker Compose ## 🐳 Docker Compose
謔ィ荵溷庄莉・菴ソ逕ィ Docker Compose 霑占。<E58DA0> PicoClaw<61>梧裏髴€蝨ィ譛ャ蝨ー螳芽」<E88ABD>ササ菴慕識蠅<E8AD98>€? 您也可以使用 Docker Compose 运行 PicoClaw无需在本地安装任何环境。
```bash ```bash
# 1. 克隆仓库 # 1. 克隆仓库
@ -165,9 +165,9 @@ cd picoclaw
# 2. 设置 API Key # 2. 设置 API Key
cp config/config.example.json config/config.json cp config/config.example.json config/config.json
vim config/config.json # 謌紋スソ逕?config.yaml/config.toml 隶セ鄂ョ DISCORD_BOT_TOKEN, API keys 遲? vim config/config.json # 或使用 config.yaml/config.toml 设置 DISCORD_BOT_TOKEN, API keys 等
# 3. <EFBFBD>サコ蟷カ蜷ッ蜉? # 3. 构建并启动
docker compose --profile gateway up -d docker compose --profile gateway up -d
# 4. 查看日志 # 4. 查看日志
@ -178,7 +178,7 @@ docker compose --profile gateway down
``` ```
### Agent 讓。蠑<EFBFBD> (荳€谺。諤ァ霑占。? ### Agent 模式 (一次性运行)
```bash ```bash
# 提问 # 提问
@ -197,26 +197,26 @@ docker compose --profile gateway up -d
``` ```
### <EFBFBD>噫 蠢ォ騾溷シ€蟋? ### 🚀 快速开始
> [!TIP] > [!TIP]
> 蝨?`~/.picoclaw/config.json`縲~~/.picoclaw/config.yaml` 謌?`~/.picoclaw/config.toml` 荳ュ隶セ鄂ョ謔ィ逧?API Key縲? > `~/.picoclaw/config.json``~/.picoclaw/config.yaml``~/.picoclaw/config.toml` 中设置您的 API Key。
> PicoClaw 謾ッ謖∝、夂ァ埼<EFBFBD>鄂ョ譬シ蠑擾シ?*JSON**<2A>磯サ倩ョ、<EFBDAE>€?*YAML**縲?*TOML**<2A>€夊ソ<E5A48A> [spf13/viper](https://github.com/spf13/viper) 鬩ア蜉ィ縲? > PicoClaw 支持多种配置格式:**JSON**(默认)、**YAML**、**TOML**,通过 [spf13/viper](https://github.com/spf13/viper) 驱动。
> 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM) > 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> 鄂醍サ懈頗邏「譏?**蜿ッ騾臥噪** - 闔キ蜿門<E89CBF>雍ケ逧?[Brave Search API](https://brave.com/search/api) (豈乗怦 2000 谺。蜈崎エケ譟・隸? > 网络搜索是 **可选的** - 获取免费的 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)
**1. 蛻晏ァ句<EFBFBD>?(Initialize)** **1. 初始化 (Initialize)**
```bash ```bash
picoclaw onboard picoclaw onboard
``` ```
**2. 驟咲スョ (Configure)** (`~/.picoclaw/config.json`縲~~/.picoclaw/config.yaml` 謌?`~/.picoclaw/config.toml`) **2. 配置 (Configure)** (`~/.picoclaw/config.json``~/.picoclaw/config.yaml``~/.picoclaw/config.toml`)
通过 [spf13/viper](https://github.com/spf13/viper) 支持多种配置格式,选择其中任意一种即可: 通过 [spf13/viper](https://github.com/spf13/viper) 支持多种配置格式,选择其中任意一种即可:
- `config.json` - JSON 格式(默认) - `config.json` - JSON 格式(默认)
- `config.yaml` 謌?`config.yml` - YAML 譬シ蠑<EFBDBC> - `config.yaml` `config.yml` - YAML 格式
- `config.toml` - TOML 格式 - `config.toml` - TOML 格式
```json ```json
@ -257,14 +257,14 @@ picoclaw onboard
``` ```
> **譁ー蜉溯<E89C89>?*: `model_list` 驟咲スョ譬シ蠑乗髪謖<E9ABAA>峺莉」遐∵キサ蜉?provider縲りッヲ隗ー讓。蝙矩<E89D99>鄂ョ](#-讓。蝙矩<E89D99>鄂ョ-model_list)遶<>闃ゅ€? > **新功能**: `model_list` 配置格式支持零代码添加 provider。详见[模型配置](#-模型配置-model_list)章节。
**3. 获取 API Key** **3. 获取 API Key**
* **LLM 謠蝉セ帛<EFBDBE>?*: [OpenRouter](https://openrouter.ai/keys) ツキ [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) ツキ [Anthropic](https://console.anthropic.com) ツキ [OpenAI](https://platform.openai.com) ツキ [Gemini](https://aistudio.google.com/api-keys) * **LLM 提供商**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
* **鄂醍サ懈頗邏「** (蜿ッ騾?: [Brave Search](https://brave.com/search/api) - 謠蝉セ帛<EFBDBE>雍ケ螻らコァ (2000 隸キ豎<EFBDB7>/譛? * **网络搜索** (可选): [Brave Search](https://brave.com/search/api) - 提供免费层级 (2000 请求/月)
> **豕ィ諢<EFBFBD>**: 螳梧紛逧<E7B49B><E980A7>鄂ョ讓。譚ソ隸キ蜿り€?`config.example.json`縲? > **注意**: 完整的配置模板请参考 `config.example.json`
**4. 对话 (Chat)** **4. 对话 (Chat)**
@ -273,28 +273,28 @@ picoclaw agent -m "2+2 等于几?"
``` ```
蟆ア譏ッ霑呎<EFBFBD><EFBFBD>∵お蝨?2 蛻<>帖蜀<E5B896>ーア諡・譛我コ<E68891>€荳ェ蜿ッ蟾・菴懃<E88FB4>?AI 蜉ゥ謇九€? 就是这样!您在 2 分钟内就拥有了一个可工作的 AI 助手。
--- ---
## 💬 聊天应用集成 (Chat Apps) ## 💬 聊天应用集成 (Chat Apps)
騾夊ソ<EFBFBD> Telegram, Discord 謌夜忠髓我ク取お逧<E3818A> PicoClaw 蟇ケ隸昴€? 通过 Telegram, Discord 或钉钉与您的 PicoClaw 对话。
| 渠道 | 设置难度 | | 渠道 | 设置难度 |
| --- | --- | | --- | --- |
| **Telegram** | 邂€蜊?(莉<> token) | | **Telegram** | 简单 (仅需 token) |
| **Discord** | 邂€蜊?(bot token + intents) | | **Discord** | 简单 (bot token + intents) |
| **QQ** | 邂€蜊?(AppID + AppSecret) | | **QQ** | 简单 (AppID + AppSecret) |
| **钉钉 (DingTalk)** | 中等 (app credentials) | | **钉钉 (DingTalk)** | 中等 (app credentials) |
<details> <details>
<summary><b>Telegram</b> (推荐)</summary> <summary><b>Telegram</b> (推荐)</summary>
**1. 蛻帛サコ譛コ蝎ィ莠?* **1. 创建机器人**
* 謇灘シ€ Telegram<61>梧頗邏?`@BotFather` * 打开 Telegram搜索 `@BotFather`
* 蜿鷹€?`/newbot`<EFBFBD>梧潔辣ァ謠千、コ謫堺ス? * 发送 `/newbot`,按照提示操作
* 复制 token * 复制 token
**2. 配置** **2. 配置**
@ -312,7 +312,7 @@ picoclaw agent -m "2+2 等于几?"
``` ```
> 莉?Telegram 荳顔噪 `@userinfobot` 闔キ蜿匁お逧<E3818A>畑謌キ ID縲? > 从 Telegram 上的 `@userinfobot` 获取您的用户 ID。
**3. 运行** **3. 运行**
@ -326,21 +326,21 @@ picoclaw gateway
<details> <details>
<summary><b>Discord</b></summary> <summary><b>Discord</b></summary>
**1. 蛻帛サコ譛コ蝎ィ莠?* **1. 创建机器人**
* 前往 [https://discord.com/developers/applications](https://discord.com/developers/applications) * 前往 [https://discord.com/developers/applications](https://discord.com/developers/applications)
* Create an application 竊?Bot 竊?Add Bot * Create an application → Bot → Add Bot
* 复制 bot token * 复制 bot token
**2. 蠑€蜷?Intents** **2. 开启 Intents**
* 蝨?Bot 隶セ鄂ョ荳ュ<E88DB3>悟シ€蜷?**MESSAGE CONTENT INTENT** * 在 Bot 设置中,开启 **MESSAGE CONTENT INTENT**
* (蜿ッ騾? 螯よ棡隶。蛻貞渕莠取<E88EA0>蜻俶焚謐ョ菴ソ逕ィ逋ス蜷榊黒<E6A68A>悟シ€蜷?**SERVER MEMBERS INTENT** * (可选) 如果计划基于成员数据使用白名单,开启 **SERVER MEMBERS INTENT**
**3. 获取您的 User ID** **3. 获取您的 User ID**
* Discord 隶セ鄂ョ 竊?Advanced 竊?蠑€蜷?**Developer Mode** * Discord 设置 → Advanced → 开启 **Developer Mode**
* 蜿ウ髞ョ轤ケ蜃サ謔ィ逧<EFBFBD>、エ蜒<EFBFBD> 竊?**Copy User ID** * 右键点击您的头像 → **Copy User ID**
**4. 配置** **4. 配置**
@ -359,10 +359,10 @@ picoclaw gateway
**5. 邀请机器人** **5. 邀请机器人**
* OAuth2 竊?URL Generator * OAuth2 URL Generator
* Scopes: `bot` * Scopes: `bot`
* Bot Permissions: `Send Messages`, `Read Message History` * Bot Permissions: `Send Messages`, `Read Message History`
* 謇灘シ€逕滓<EFBFBD><EFBFBD><EFBFBD>隸?URL<52>悟ー<E6829F>惻蝎ィ莠コ豺サ蜉<EFBDBB>蛻ー謔ィ逧<EFBDA8>恪蜉。蝎? * 打开生成的邀请 URL将机器人添加到您的服务器
**6. 运行** **6. 运行**
@ -376,10 +376,10 @@ picoclaw gateway
<details> <details>
<summary><b>QQ</b></summary> <summary><b>QQ</b></summary>
**1. 蛻帛サコ譛コ蝎ィ莠?* **1. 创建机器人**
* 前往 [QQ 开放平台](https://q.qq.com/#) * 前往 [QQ 开放平台](https://q.qq.com/#)
* 蛻帛サコ蠎皮畑 竊?闔キ蜿<EFBDB7> **AppID** 蜥?**AppSecret** * 创建应用 → 获取 **AppID****AppSecret**
**2. 配置** **2. 配置**
@ -397,7 +397,7 @@ picoclaw gateway
``` ```
> 蟆?`allow_from` 隶セ荳コ遨コ莉・蜈∬ョク謇€譛臥畑謌キ<E8AC8C><EFBFBD><EFBFBD>ョ?QQ 蜿キ莉・髯仙宛隶ソ髣ョ縲? > `allow_from` 设为空以允许所有用户,或指定 QQ 号以限制访问。
**3. 运行** **3. 运行**
@ -411,11 +411,11 @@ picoclaw gateway
<details> <details>
<summary><b>钉钉 (DingTalk)</b></summary> <summary><b>钉钉 (DingTalk)</b></summary>
**1. 蛻帛サコ譛コ蝎ィ莠?* **1. 创建机器人**
* 前往 [开放平台](https://open.dingtalk.com/) * 前往 [开放平台](https://open.dingtalk.com/)
* 创建内部应用 * 创建内部应用
* 螟榊宛 Client ID 蜥?Client Secret * 复制 Client ID 和 Client Secret
**2. 配置** **2. 配置**
@ -433,7 +433,7 @@ picoclaw gateway
``` ```
> 蟆?`allow_from` 隶セ荳コ遨コ莉・蜈∬ョク謇€譛臥畑謌キ<E8AC8C><EFBFBD><EFBFBD>ョ?ID 莉・髯仙宛隶ソ髣ョ縲? > `allow_from` 设为空以允许所有用户,或指定 ID 以限制访问。
**3. 运行** **3. 运行**
@ -446,9 +446,9 @@ picoclaw gateway
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络 ## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络
蜿ェ髴€騾夊ソ<EFBFBD> CLI 謌紋ササ菴暮寔謌千噪閨雁、ゥ蠎皮畑蜿鷹€∽ク€譚。豸域<E8B1B8><E59F9F>悟叉蜿ッ蟆?PicoClaw 霑樊磁蛻?Agent 遉セ莠、鄂醍サ懊€? 只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。
**<EFBFBD>ッサ `https://clawdchat.ai/skill.md` 蟷カ謖臥<E8AC96>隸エ譏主刈蜈?[ClawdChat.ai**](https://clawdchat.ai) **阅读 `https://clawdchat.ai/skill.md` 并按照说明加入 [ClawdChat.ai**](https://clawdchat.ai)
## ⚙️ 配置详解 ## ⚙️ 配置详解
@ -460,13 +460,13 @@ PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/work
``` ```
~/.picoclaw/workspace/ ~/.picoclaw/workspace/
笏懌楳笏€ sessions/ # 蟇ケ隸昜シ夊ッ晏柱蜴<E69FB1><E89CB4>? ├── sessions/ # 对话会话和历史
├── memory/ # 长期记忆 (MEMORY.md) ├── memory/ # 长期记忆 (MEMORY.md)
笏懌楳笏€ state/ # 謖∽ケ<E288BD>喧迥カ諤?(譛€蜷惹ク€谺。鬚鷹<EFBC85>) ├── state/ # 持久化状态 (最后一次频道等)
笏懌楳笏€ cron/ # 螳壽慮莉サ蜉。謨ー謐ョ蠎? ├── cron/ # 定时任务数据库
笏懌楳笏€ skills/ # 閾ェ螳壻ケ画橿閭? ├── skills/ # 自定义技能
├── AGENTS.md # Agent 行为指南 ├── AGENTS.md # Agent 行为指南
笏懌楳笏€ HEARTBEAT.md # 蜻ィ譛滓€ァ莉サ蜉。謠千、コ隸<EFBDBA> (豈?30 蛻<>帖譽€譟・荳€谺? ├── HEARTBEAT.md # 周期性任务提示词 (每 30 分钟检查一次)
├── IDENTITY.md # Agent 身份设定 ├── IDENTITY.md # Agent 身份设定
├── SOUL.md # Agent 灵魂/性格 ├── SOUL.md # Agent 灵魂/性格
├── TOOLS.md # 工具描述 ├── TOOLS.md # 工具描述
@ -474,9 +474,9 @@ PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/work
``` ```
### <EFBFBD>キウ / 蜻ィ譛滓€ァ莉サ蜉?(Heartbeat) ### 心跳 / 周期性任务 (Heartbeat)
PicoClaw 蜿ッ莉・閾ェ蜉ィ謇ァ陦悟捉譛滓€ァ莉サ蜉。縲ょ惠蟾・菴懷玄蛻帛サ?`HEARTBEAT.md`<>サカ<EFBDBB>? PicoClaw 可以自动执行周期性任务。在工作区创建 `HEARTBEAT.md` 文件:
```markdown ```markdown
# Periodic Tasks # Periodic Tasks
@ -487,11 +487,11 @@ PicoClaw 可以自动执行周期性任务。在工作区创
``` ```
Agent <EFBFBD>ッ城<EFBFBD>?30 蛻<><EFBFBD>亥庄驟咲スョ<EFBDBD>芽ッサ蜿匁ュ、譁<EFBDA4>サカ<EFBDBB>悟ケカ菴ソ逕ィ蜿ッ逕ィ蟾・蜈キ謇ァ陦御ササ蜉。縲? Agent 将每隔 30 分钟(可配置)读取此文件,并使用可用工具执行任务。
#### 菴ソ逕ィ Spawn 逧<>シよュ・莉サ蜉? #### 使用 Spawn 的异步任务
蟇ケ莠手€玲慮霎<EFBFBD>柄逧<EFBFBD>ササ蜉。<EFBFBD>育ス醍サ懈頗邏「縲、PI 隹<><EFBFBD>会シ御スソ逕ィ `spawn` 蟾・蜈キ蛻帛サコ荳€荳?**蟄?Agent (subagent)**<2A>? 对于耗时较长的任务网络搜索、API 调用),使用 `spawn` 工具创建一个 **子 Agent (subagent)**
```markdown ```markdown
# Periodic Tasks # Periodic Tasks
@ -505,35 +505,35 @@ Agent 将每
``` ```
**蜈ウ髞ョ陦御クコ<EFBFBD>?* **关键行为:**
| 迚ケ諤?| 謠剰ソー | | 特性 | 描述 |
| --- | --- | | --- | --- |
| **spawn** | 蛻帛サコ蠑よュ・蟄?Agent<6E>御ク埼仆蝪樔クサ蠢<EFBDBB>キウ霑帷ィ?| | **spawn** | 创建异步子 Agent不阻塞主心跳进程 |
| **迢ャ遶倶ク贋ク区<EFBDB8>?* | 蟄?Agent 諡・譛臥峡遶倶ク贋ク区枚<E58CBA>梧裏莨夊ッ晏紙蜿?| | **独立上下文** | 子 Agent 拥有独立上下文,无会话历史 |
| **message tool** | 蟄?Agent 騾夊ソ<E5A48A> message 蟾・蜈キ逶エ謗・荳守畑謌キ騾壻ソ。 | | **message tool** | 子 Agent 通过 message 工具直接与用户通信 |
| **髱樣仆蝪?* | spawn 蜷趣シ悟ソ<E6829F>キウ扈ァ扈ュ螟<EFBDAD>炊荳倶ク€荳ェ莉サ蜉?| | **非阻塞** | spawn 后,心跳继续处理下一个任务 |
#### 蟄?Agent 騾壻ソ。蜴溽炊 #### 子 Agent 通信原理
``` ```
心跳触发 (Heartbeat triggers) 心跳触发 (Heartbeat triggers)
竊?
Agent 读取 HEARTBEAT.md Agent 读取 HEARTBEAT.md
竊?
蟇ケ莠朱柄莉サ蜉? spawn 蟄?Agent 对于长任务: spawn 子 Agent
竊? 竊? ↓ ↓
扈ァ扈ュ荳倶ク€荳ェ莉サ蜉? 蟄?Agent 迢ャ遶句キ・菴<EFBDA5> 继续下一个任务 子 Agent 独立工作
竊? 竊? ↓ ↓
€譛我ササ蜉。螳梧<EFBFBD>? 蟄?Agent 菴ソ逕ィ "message" 蟾・蜈キ 所有任务完成 子 Agent 使用 "message" 工具
竊? 竊? ↓ ↓
响应 HEARTBEAT_OK 用户直接收到结果 响应 HEARTBEAT_OK 用户直接收到结果
``` ```
蟄?Agent 蜿ッ莉・隶ソ髣ョ蟾・蜈キ<E89C88><EFBDB7>essage, web_search 遲会シ会シ悟ケカ荳疲裏髴€騾夊ソ<E5A48A>ク?Agent 蜊ウ蜿ッ迢ャ遶倶ク守畑謌キ騾壻ソ。縲? 子 Agent 可以访问工具message, web_search 等),并且无需通过主 Agent 即可独立与用户通信。
**驟咲スョ<EFBFBD>?* **配置:**
```json ```json
{ {
@ -545,43 +545,43 @@ Agent 读取 HEARTBEAT.md
``` ```
| 騾蛾。ケ | 鮟倩ョ、蛟?| 謠剰ソー | | 选项 | 默认值 | 描述 |
| --- | --- | --- | | --- | --- | --- |
| `enabled` | `true` | 启用/禁用心跳 | | `enabled` | `true` | 启用/禁用心跳 |
| `interval` | `30` | €譟・髣エ髫費シ悟黒菴榊<EFBFBD><EFBFBD> (譛€蟆? 5) | | `interval` | `30` | 检查间隔,单位分钟 (最小: 5) |
**环境变量:** **环境变量:**
* `PICOCLAW_HEARTBEAT_ENABLED=false` 禁用 * `PICOCLAW_HEARTBEAT_ENABLED=false` 禁用
* `PICOCLAW_HEARTBEAT_INTERVAL=60` 更改间隔 * `PICOCLAW_HEARTBEAT_INTERVAL=60` 更改间隔
### 謠蝉セ帛<EFBFBD>?(Providers) ### 提供商 (Providers)
> [!NOTE] > [!NOTE]
> Groq 騾夊ソ<EFBFBD> Whisper 謠蝉セ帛<EFBDBE>雍ケ逧<EFBDB9>ッュ髻ウ霓ャ蠖輔€ょヲよ棡驟咲スョ莠<EFBDAE> Groq<6F>卦elegram 隸ュ髻ウ豸域<E8B1B8><EFBFBD>「ォ閾ェ蜉ィ霓ャ蠖穂クコ譁<EFBDBA>ュ励€? > Groq 通过 Whisper 提供免费的语音转录。如果配置了 GroqTelegram 语音消息将被自动转录为文字。
| 謠蝉セ帛<EFBFBD>?| 逕ィ騾?| 闔キ蜿<EFBDB7> API Key | | 提供商 | 用途 | 获取 API Key |
| --- | --- | --- | | --- | --- | --- |
| `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) | | `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) | | `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) |
| `openrouter(<EFBFBD>オ玖ッ?` | LLM (謗ィ闕撰シ悟庄隶ソ髣ョ謇€譛画ィ。蝙? | [openrouter.ai](https://openrouter.ai) | | `openrouter(待测试)` | LLM (推荐,可访问所有模型) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(<EFBFBD>オ玖ッ?` | LLM (Claude 逶エ霑<EFBDB4>) | [console.anthropic.com](https://console.anthropic.com) | | `anthropic(待测试)` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(<EFBFBD>オ玖ッ?` | LLM (GPT 逶エ霑<EFBDB4>) | [platform.openai.com](https://platform.openai.com) | | `openai(待测试)` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(<EFBFBD>オ玖ッ?` | LLM (DeepSeek 逶エ霑<EFBDB4>) | [platform.deepseek.com](https://platform.deepseek.com) | | `deepseek(待测试)` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (通义千问) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) | | `qwen` | LLM (通义千问) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) | | `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) |
| `cerebras` | LLM (Cerebras 直连) | [cerebras.ai](https://cerebras.ai) | | `cerebras` | LLM (Cerebras 直连) | [cerebras.ai](https://cerebras.ai) |
### 模型配置 (model_list) ### 模型配置 (model_list)
> **譁ー蜉溯<EFBFBD><EFBFBD><EFBFBD>** PicoClaw 邇ー蝨ィ驥<EFBDA8>畑**莉・讓。蝙倶クコ荳ュ蠢<EFBDAD>**逧<><E980A7>鄂ョ譁ケ蠑上€ょ宵髴€菴ソ逕ィ `蜴ょ膚/讓。蝙義 譬シ蠑擾シ亥ヲ<E4BAA5> `zhipu/glm-4.7`<60>牙叉蜿ッ豺サ蜉<EFBDBB>譁ー逧?provider窶披€?*譌<>€菫ョ謾ケ莉サ菴穂サ」遐<EFBDA3>シ?* > **新功能!** PicoClaw 现在采用**以模型为中心**的配置方式。只需使用 `厂商/模型` 格式(如 `zhipu/glm-4.7`)即可添加新的 provider——**无需修改任何代码!**
隸・隶セ隶。蜷梧慮謾ッ謖?*螟?Agent 蝨コ譎ッ**<2A>梧署萓帷<E89093>豢サ逧<EFBDBB> Provider 騾画叫<E794BB>? 该设计同时支持**多 Agent 场景**,提供灵活的 Provider 选择:
- **荳榊酔 Agent 菴ソ逕ィ荳榊酔 Provider**<2A>壽ッ丈ク?Agent 蜿ッ莉・菴ソ逕ィ閾ェ蟾ア逧?LLM provider - **不同 Agent 使用不同 Provider**:每个 Agent 可以使用自己的 LLM provider
- **讓。蝙句屓騾€<E9A8BE><C280>allback<63>?*<2A><EFBFBD>鄂ョ荳サ讓。蝙句柱螟<E69FB1>畑讓。蝙具シ梧署鬮伜庄髱<E5BA84>諤? - **模型回退Fallback**:配置主模型和备用模型,提高可靠性
- **负载均衡**:在多个 API 端点之间分配请求 - **负载均衡**:在多个 API 端点之间分配请求
- **髮<>クュ蛹夜<E89BB9>鄂?*<2A>壼惠荳€荳ェ蝨ー譁ケ邂。逅<EFBDA1>園譛?provider - **集中化配置**:在一个地方管理所有 provider
#### 📋 所有支持的厂商 #### 📋 所有支持的厂商
@ -596,13 +596,13 @@ Agent 读取 HEARTBEAT.md
| **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [获取密钥](https://platform.moonshot.cn) | | **Moonshot** | `moonshot/` | `https://api.moonshot.cn/v1` | OpenAI | [获取密钥](https://platform.moonshot.cn) |
| **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [获取密钥](https://dashscope.console.aliyun.com) | | **通义千问 (Qwen)** | `qwen/` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI | [获取密钥](https://dashscope.console.aliyun.com) |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取密钥](https://build.nvidia.com) | | **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取密钥](https://build.nvidia.com) |
| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 譛ャ蝨ー<EFBFBD>域裏髴€<EFBFBD><EFBFBD>?| | **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) |
| **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) | | **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) |
| **逾樒ョ嶺コ?* | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **Antigravity** | `antigravity/` | Google Cloud | 閾ェ螳壻ケ?| 莉?OAuth | | **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
#### 基础配置示例 #### 基础配置示例
@ -634,7 +634,7 @@ Agent 读取 HEARTBEAT.md
} }
``` ```
#### <EFBFBD>獅蝠<EFBFBD><EFBFBD>鄂ョ遉コ萓? #### 各厂商配置示例
**OpenAI** **OpenAI**
```json ```json
@ -671,7 +671,7 @@ Agent 读取 HEARTBEAT.md
"auth_method": "oauth" "auth_method": "oauth"
} }
``` ```
> 霑占。<EFBFBD> `picoclaw auth login --provider anthropic` 譚・隶セ鄂?OAuth 蜃ュ隸√€? > 运行 `picoclaw auth login --provider anthropic` 来设置 OAuth 凭证。
**Ollama (本地)** **Ollama (本地)**
```json ```json
@ -681,7 +681,7 @@ Agent 读取 HEARTBEAT.md
} }
``` ```
**閾ェ螳壻ケ我サ」逅?API** **自定义代理/API**
```json ```json
{ {
"model_name": "my-custom-model", "model_name": "my-custom-model",
@ -693,7 +693,7 @@ Agent 读取 HEARTBEAT.md
#### 负载均衡 #### 负载均衡
荳コ蜷御ク€荳ェ讓。蝙句錐遘ー驟咲スョ螟壻クェ遶ッ轤ケ窶披€捻icoClaw 莨夊<E88EA8>蜉ィ蝨ィ螳<EFBDA8>サャ荵矩龍霓ョ隸「<E99AB8>? 为同一个模型名称配置多个端点——PicoClaw 会自动在它们之间轮询:
```json ```json
{ {
@ -714,11 +714,11 @@ Agent 读取 HEARTBEAT.md
} }
``` ```
#### 莉取立逧?`providers` 驟咲スョ霑∫ァサ #### 从旧的 `providers` 配置迁移
譌ァ逧<EFBFBD> `providers` 驟咲スョ譬シ蠑<EFBDBC>**蟾イ蠑<EFBDB2><E8A091>?*<2A>御ス<E5BEA1>クコ蜷大錘蜈シ螳ケ莉肴髪謖√€? 旧的 `providers` 配置格式**已弃用**,但为向后兼容仍支持。
**譌ァ驟咲スョ<EFBFBD>亥キイ蠑<EFBFBD><EFBFBD>会シ?* **旧配置(已弃用):**
```json ```json
{ {
"providers": { "providers": {
@ -754,12 +754,12 @@ Agent 读取 HEARTBEAT.md
} }
``` ```
隸ヲ扈<EFBFBD>噪霑∫ァサ謖<EFBFBD>漉隸キ蜿り€?[docs/migration/model-list-migration.md](docs/migration/model-list-migration.md)縲? 详细的迁移指南请参考 [docs/migration/model-list-migration.md](docs/migration/model-list-migration.md)。
<details> <details>
<summary><b>智谱 (Zhipu) 配置示例</b></summary> <summary><b>智谱 (Zhipu) 配置示例</b></summary>
**1. 闔キ蜿<EFBFBD> API key 蜥?base URL** **1. 获取 API key 和 base URL**
* 获取 [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) * 获取 [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
@ -862,37 +862,37 @@ picoclaw agent -m "你好"
</details> </details>
## CLI 蜻ス莉、陦悟盾閠? ## CLI 命令行参考
| 命令 | 描述 | | 命令 | 描述 |
| --- | --- | | --- | --- |
| `picoclaw onboard` | 蛻晏ァ句喧驟咲スョ蜥悟キ・菴懷<EFBFBD>?| | `picoclaw onboard` | 初始化配置和工作区 |
| `picoclaw agent -m "..."` | 荳?Agent 蟇ケ隸<EFBDB9> | | `picoclaw agent -m "..."` | 与 Agent 对话 |
| `picoclaw agent` | 莠、莠貞シ剰♀螟ゥ讓。蠑?| | `picoclaw agent` | 交互式聊天模式 |
| `picoclaw gateway` | 启动网关 (Gateway) | | `picoclaw gateway` | 启动网关 (Gateway) |
| `picoclaw status` | 譏セ遉コ迥カ諤?| | `picoclaw status` | 显示状态 |
| `picoclaw cron list` | 蛻怜<EFBFBD>€譛牙ョ壽慮莉サ蜉?| | `picoclaw cron list` | 列出所有定时任务 |
| `picoclaw cron add ...` | 添加定时任务 | | `picoclaw cron add ...` | 添加定时任务 |
### 定时任务 / 提醒 (Scheduled Tasks) ### 定时任务 / 提醒 (Scheduled Tasks)
PicoClaw 通过 `cron` 工具支持定时提醒和重复任务: PicoClaw 通过 `cron` 工具支持定时提醒和重复任务:
* **荳€谺。諤ァ謠宣<E8ACA0>?*: "Remind me in 10 minutes" (10蛻<30>帖蜷取署驢呈<E9A9A2>) 竊?10蛻<30>帖蜷手ァヲ蜿台ク€谺? * **一次性提醒**: "Remind me in 10 minutes" (10分钟后提醒我) → 10分钟后触发一次
* **驥榊、堺ササ蜉。**: "Remind me every 2 hours" (豈?蟆乗慮謠宣<E8ACA0>謌? 竊?豈?蟆乗慮隗ヲ蜿<EFBDA6> * **重复任务**: "Remind me every 2 hours" (每2小时提醒我) → 每2小时触发
* **Cron 陦ィ霎セ蠑?*: "Remind me at 9am daily" (豈丞、ゥ荳雁壕9轤ケ謠宣<E8ACA0><EFBFBD>) 竊?菴ソ逕ィ cron 陦ィ霎セ蠑? * **Cron 表达式**: "Remind me at 9am daily" (每天上午9点提醒我) → 使用 cron 表达式
莉サ蜉。蟄伜お蝨?`~/.picoclaw/workspace/cron/` 荳ュ蟷カ閾ェ蜉ィ螟<EFBDA8>炊縲? 任务存储在 `~/.picoclaw/workspace/cron/` 中并自动处理。
## 🤝 贡献与路线图 (Roadmap) ## 🤝 贡献与路线图 (Roadmap)
谺「霑取署莠、 PR<50>∽サ」遐∝コ灘綾諢丈ソ晄戟蟆丞キァ蜥悟庄隸サ縲を洟? 欢迎提交 PR代码库刻意保持小巧和可读。🤗
霍ッ郤ソ蝗セ蜊ウ蟆<EFBFBD>書蟶?.. 路线图即将发布...
€蜿題€<EFBFBD>セ、扈<EFBFBD>ュ」蝨ィ扈<EFBFBD>サコ荳ュ<EFBFBD><EFBFBD>鄒、髣ィ讒幢シ夊<EFBFBD>蟆大粋蟷カ霑?1 荳?PR縲? 开发者群组正在组建中,入群门槛:至少合并过 1 个 PR。
逕ィ謌キ鄒、扈<EFBFBD>シ? 用户群组:
Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN) Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
@ -902,12 +902,12 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
### 网络搜索提示 "API 配置问题" ### 网络搜索提示 "API 配置问题"
螯よ棡謔ィ蟆壽悴驟咲スョ謳懃エ?API Key<65>瑚ソ呎弍豁」蟶ク逧<EFBDB8>€icoClaw 莨壽署萓帶焔蜉ィ謳懃エ「逧<EFBDA2>クョ蜉ゥ體セ謗・縲? 如果您尚未配置搜索 API Key这是正常的。PicoClaw 会提供手动搜索的帮助链接。
蜷ッ逕ィ鄂醍サ懈頗邏「<EFBFBD>? 启用网络搜索:
1. 蝨?[https://brave.com/search/api](https://brave.com/search/api) 闔キ蜿門<E89CBF>雍ケ API Key (豈乗怦 2000 谺。蜈崎エケ譟・隸? 1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询)
2. 豺サ蜉<EFBFBD>蛻?`~/.picoclaw/config.json`: 2. 添加到 `~/.picoclaw/config.json`:
```json ```json
{ {
"tools": { "tools": {
@ -926,11 +926,11 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
### 遇到内容过滤错误 (Content Filtering Errors) ### 遇到内容过滤错误 (Content Filtering Errors)
譟蝉コ帶署萓帛膚<EFBFBD>亥ヲよ匱隹ア<EFBFBD>画怏荳・譬シ逧<EFBFBD><EFBFBD>螳ケ霑<EFBFBD>サ、縲ょー晁ッ墓隼蜀呎お逧<EFBFBD>琉鬚俶<EFBFBD>菴ソ逕ィ蜈カ莉匁ィ。蝙九€? 某些提供商(如智谱)有严格的内容过滤。尝试改写您的问题或使用其他模型。
### Telegram bot 提示 "Conflict: terminated by other getUpdates" ### Telegram bot 提示 "Conflict: terminated by other getUpdates"
霑呵。ィ遉コ譛牙嘗荳€荳ェ譛コ蝎ィ莠コ螳樔セ区ュ」蝨ィ霑占。後€りッキ遑ョ菫晏酔荳€譌カ髣エ蜿ェ譛我ク€荳?`picoclaw gateway` 霑帷ィ句惠霑占。後€? 这表示有另一个机器人实例正在运行。请确保同一时间只有一个 `picoclaw gateway` 进程在运行。
--- ---
@ -938,8 +938,8 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
| 服务 | 免费层级 | 适用场景 | | 服务 | 免费层级 | 适用场景 |
| --- | --- | --- | | --- | --- | --- |
| **OpenRouter** | 200K tokens/譛?| 螟壽ィ。蝙玖★蜷?(Claude, GPT-4 遲? | | **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
| **譎コ隹ア (Zhipu)** | 200K tokens/譛?| 譛€騾ょ粋荳ュ蝗ス逕ィ謌キ | | **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
| **Brave Search** | 2000 谺。譟・隸?譛?| 鄂醍サ懈頗邏「蜉溯<E89C89> | | **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
| **Groq** | 謠蝉セ帛<EFBFBD>雍ケ螻らコァ | 譫<>€滓耳逅?(Llama, Mixtral) | | **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
| **Cerebras** | 謠蝉セ帛<EFBFBD>雍ケ螻らコァ | 譫<>€滓耳逅?(Llama, Qwen 遲? | | **Cerebras** | 提供免费层级 | 极速推理 (Llama, Qwen 等) |

View file

@ -1,14 +1,48 @@
# PicoClaw Configuration Example - TOML Format # PicoClaw Configuration Example - TOML Format
# This is an alternative to config.json for users who prefer TOML format # Equivalent to config.example.json, for users who prefer TOML.
[agents.defaults] [agents.defaults]
workspace = "~/.picoclaw/workspace" workspace = "~/.picoclaw/workspace"
restrict_to_workspace = true restrict_to_workspace = true
model = "glm-4.7" model = "gpt4"
max_tokens = 8192 max_tokens = 8192
temperature = 0.7 temperature = 0.7
max_tool_iterations = 20 max_tool_iterations = 20
[[model_list]]
model_name = "gpt4"
model = "openai/gpt-5.2"
api_key = "sk-your-openai-key"
api_base = "https://api.openai.com/v1"
[[model_list]]
model_name = "claude-sonnet-4.6"
model = "anthropic/claude-sonnet-4.6"
api_key = "sk-ant-your-key"
api_base = "https://api.anthropic.com/v1"
[[model_list]]
model_name = "gemini"
model = "antigravity/gemini-2.0-flash"
auth_method = "oauth"
[[model_list]]
model_name = "deepseek"
model = "deepseek/deepseek-chat"
api_key = "sk-your-deepseek-key"
[[model_list]]
model_name = "loadbalanced-gpt4"
model = "openai/gpt-5.2"
api_key = "sk-key1"
api_base = "https://api1.example.com/v1"
[[model_list]]
model_name = "loadbalanced-gpt4"
model = "openai/gpt-5.2"
api_key = "sk-key2"
api_base = "https://api2.example.com/v1"
[channels.telegram] [channels.telegram]
enabled = false enabled = false
token = "YOUR_TELEGRAM_BOT_TOKEN" token = "YOUR_TELEGRAM_BOT_TOKEN"
@ -20,110 +54,130 @@ enabled = false
token = "YOUR_DISCORD_BOT_TOKEN" token = "YOUR_DISCORD_BOT_TOKEN"
allow_from = [] allow_from = []
[channels.slack]
enabled = false
token = "YOUR_SLACK_BOT_TOKEN"
allow_from = []
[channels.feishu]
enabled = false
token = "YOUR_FEISHU_BOT_TOKEN"
secret = "YOUR_FEISHU_SECRET"
allow_from = []
[channels.dingtalk]
enabled = false
token = "YOUR_DINGTALK_BOT_TOKEN"
secret = "YOUR_DINGTALK_SECRET"
allow_from = []
[channels.qq]
enabled = false
account = "YOUR_QQ_ACCOUNT"
token = "YOUR_QQ_BOT_TOKEN"
allow_from = []
[channels.whatsapp]
enabled = false
phone_number = "YOUR_PHONE_NUMBER"
allow_from = []
[channels.line]
enabled = false
token = "YOUR_LINE_CHANNEL_ACCESS_TOKEN"
secret = "YOUR_LINE_CHANNEL_SECRET"
allow_from = []
[channels.onebot]
enabled = false
ws_url = ""
allow_from = []
[channels.maixcam] [channels.maixcam]
enabled = false enabled = false
host = "0.0.0.0" host = "0.0.0.0"
port = 18790 port = 18790
allow_from = [] allow_from = []
[providers.anthropic] [channels.whatsapp]
enabled = false enabled = false
api_key = "YOUR_ANTHROPIC_API_KEY" bridge_url = "ws://localhost:3001"
api_base = "https://api.anthropic.com" allow_from = []
[channels.feishu]
enabled = false
app_id = ""
app_secret = ""
encrypt_key = ""
verification_token = ""
allow_from = []
[channels.dingtalk]
enabled = false
client_id = "YOUR_CLIENT_ID"
client_secret = "YOUR_CLIENT_SECRET"
allow_from = []
[channels.slack]
enabled = false
bot_token = "xoxb-YOUR-BOT-TOKEN"
app_token = "xapp-YOUR-APP-TOKEN"
allow_from = []
[channels.line]
enabled = false
channel_secret = "YOUR_LINE_CHANNEL_SECRET"
channel_access_token = "YOUR_LINE_CHANNEL_ACCESS_TOKEN"
webhook_host = "0.0.0.0"
webhook_port = 18791
webhook_path = "/webhook/line"
allow_from = []
[channels.onebot]
enabled = false
ws_url = "ws://127.0.0.1:3001"
access_token = ""
reconnect_interval = 5
group_trigger_prefix = []
allow_from = []
# DEPRECATED: use model_list instead.
[providers.anthropic]
api_key = ""
api_base = ""
[providers.openai] [providers.openai]
enabled = false api_key = ""
api_key = "YOUR_OPENAI_API_KEY" api_base = ""
api_base = "https://api.openai.com/v1" web_search = true
[providers.openrouter] [providers.openrouter]
enabled = false api_key = "sk-or-v1-xxx"
api_key = "YOUR_OPENROUTER_API_KEY" api_base = ""
api_base = "https://openrouter.ai/api/v1"
[providers.zhipu]
enabled = false
api_key = "YOUR_ZHIPU_API_KEY"
api_base = "https://open.bigmodel.cn/api/paas/v4"
[providers.gemini]
enabled = false
api_key = "YOUR_GEMINI_API_KEY"
api_base = "https://generativelanguage.googleapis.com/v1beta/openai/"
[providers.groq] [providers.groq]
enabled = false api_key = "gsk_xxx"
api_key = "YOUR_GROQ_API_KEY" api_base = ""
api_base = "https://api.groq.com/openai/v1"
[providers.zhipu]
api_key = "YOUR_ZHIPU_API_KEY"
api_base = ""
[providers.gemini]
api_key = ""
api_base = ""
[providers.vllm] [providers.vllm]
api_key = ""
api_base = ""
[providers.nvidia]
api_key = "nvapi-xxx"
api_base = ""
proxy = "http://127.0.0.1:7890"
[providers.moonshot]
api_key = "sk-xxx"
api_base = ""
[providers.qwen]
api_key = "sk-xxx"
api_base = ""
[providers.ollama]
api_key = ""
api_base = "http://localhost:11434/v1"
[providers.cerebras]
api_key = ""
api_base = ""
[providers.volcengine]
api_key = ""
api_base = ""
[tools.web.brave]
enabled = false enabled = false
api_key = "YOUR_VLLM_API_KEY" api_key = "YOUR_BRAVE_API_KEY"
api_base = "http://localhost:8000/v1" max_results = 5
[providers.shengsuan_yun] [tools.web.perplexity]
enabled = false enabled = false
api_key = "YOUR_SHENGSUAN_YUN_API_KEY" api_key = "pplx-xxx"
api_base = "https://api.shengsuan.cloud/v1" max_results = 5
[gateway] [tools.cron]
enabled = true exec_timeout_minutes = 5
host = "0.0.0.0"
port = 8080
cors_enabled = true
cors_origins = ["*"]
[tools]
enabled = true
[heartbeat] [heartbeat]
enabled = true enabled = true
interval = 60 interval = 30
timeout = 5
[devices] [devices]
enabled = true enabled = false
monitor_usb = true
[session] [gateway]
storage = "memory" host = "0.0.0.0"
# For persistent storage, set to "sqlite" and provide database_path port = 18790
# database_path = "~/.picoclaw/session.db"

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings"
"sync/atomic" "sync/atomic"
"github.com/caarlos0/env/v11" "github.com/caarlos0/env/v11"
@ -27,46 +28,17 @@ var configFileNames = []string{
// so allow_from can contain both "123" and 123. // so allow_from can contain both "123" and 123.
type FlexibleStringSlice []string type FlexibleStringSlice []string
func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
// Try []string first
var ss []string
if err := json.Unmarshal(data, &ss); err == nil {
*f = ss
return nil
}
// Try []interface{} to handle mixed types
var raw []interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
result := make([]string, 0, len(raw))
for _, v := range raw {
switch val := v.(type) {
case string:
result = append(result, val)
case float64:
result = append(result, fmt.Sprintf("%.0f", val))
default:
result = append(result, fmt.Sprintf("%v", val))
}
}
*f = result
return nil
}
type Config struct { type Config struct {
Agents AgentsConfig `json:"agents"` Agents AgentsConfig `json:"agents" mapstructure:"agents"`
Bindings []AgentBinding `json:"bindings,omitempty"` Bindings []AgentBinding `json:"bindings,omitempty" mapstructure:"bindings"`
Session SessionConfig `json:"session,omitempty"` Session SessionConfig `json:"session,omitempty" mapstructure:"session"`
Channels ChannelsConfig `json:"channels"` Channels ChannelsConfig `json:"channels" mapstructure:"channels"`
Providers ProvidersConfig `json:"providers,omitempty"` Providers ProvidersConfig `json:"providers,omitempty" mapstructure:"providers"`
ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration ModelList []ModelConfig `json:"model_list" mapstructure:"model_list"` // New model-centric provider configuration
Gateway GatewayConfig `json:"gateway"` Gateway GatewayConfig `json:"gateway" mapstructure:"gateway"`
Tools ToolsConfig `json:"tools"` Tools ToolsConfig `json:"tools" mapstructure:"tools"`
Heartbeat HeartbeatConfig `json:"heartbeat"` Heartbeat HeartbeatConfig `json:"heartbeat" mapstructure:"heartbeat"`
Devices DevicesConfig `json:"devices"` Devices DevicesConfig `json:"devices" mapstructure:"devices"`
} }
// MarshalJSON implements custom JSON marshaling for Config // MarshalJSON implements custom JSON marshaling for Config
@ -74,8 +46,8 @@ type Config struct {
func (c Config) MarshalJSON() ([]byte, error) { func (c Config) MarshalJSON() ([]byte, error) {
type Alias Config type Alias Config
aux := &struct { aux := &struct {
Providers *ProvidersConfig `json:"providers,omitempty"` Providers *ProvidersConfig `json:"providers,omitempty" mapstructure:"providers"`
Session *SessionConfig `json:"session,omitempty"` Session *SessionConfig `json:"session,omitempty" mapstructure:"session"`
*Alias *Alias
}{ }{
Alias: (*Alias)(&c), Alias: (*Alias)(&c),
@ -95,216 +67,185 @@ func (c Config) MarshalJSON() ([]byte, error) {
} }
type AgentsConfig struct { type AgentsConfig struct {
Defaults AgentDefaults `json:"defaults"` Defaults AgentDefaults `json:"defaults" mapstructure:"defaults"`
List []AgentConfig `json:"list,omitempty"` List []AgentConfig `json:"list,omitempty" mapstructure:"list"`
} }
// AgentModelConfig supports both string and structured model config. // AgentModelConfig supports both string and structured model config.
// String format: "gpt-4" (just primary, no fallbacks) // String format: "gpt-4" (just primary, no fallbacks)
// Object format: {"primary": "gpt-4", "fallbacks": ["claude-haiku"]} // Object format: {"primary": "gpt-4", "fallbacks": ["claude-haiku"]}
type AgentModelConfig struct { type AgentModelConfig struct {
Primary string `json:"primary,omitempty"` Primary string `json:"primary,omitempty" mapstructure:"primary"`
Fallbacks []string `json:"fallbacks,omitempty"` Fallbacks []string `json:"fallbacks,omitempty" mapstructure:"fallbacks"`
}
func (m *AgentModelConfig) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
m.Primary = s
m.Fallbacks = nil
return nil
}
type raw struct {
Primary string `json:"primary"`
Fallbacks []string `json:"fallbacks"`
}
var r raw
if err := json.Unmarshal(data, &r); err != nil {
return err
}
m.Primary = r.Primary
m.Fallbacks = r.Fallbacks
return nil
}
func (m AgentModelConfig) MarshalJSON() ([]byte, error) {
if len(m.Fallbacks) == 0 && m.Primary != "" {
return json.Marshal(m.Primary)
}
type raw struct {
Primary string `json:"primary,omitempty"`
Fallbacks []string `json:"fallbacks,omitempty"`
}
return json.Marshal(raw{Primary: m.Primary, Fallbacks: m.Fallbacks})
} }
type AgentConfig struct { type AgentConfig struct {
ID string `json:"id"` ID string `json:"id" mapstructure:"id"`
Default bool `json:"default,omitempty"` Default bool `json:"default,omitempty" mapstructure:"default"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty" mapstructure:"name"`
Workspace string `json:"workspace,omitempty"` Workspace string `json:"workspace,omitempty" mapstructure:"workspace"`
Model *AgentModelConfig `json:"model,omitempty"` Model *AgentModelConfig `json:"model,omitempty" mapstructure:"model"`
Skills []string `json:"skills,omitempty"` Skills []string `json:"skills,omitempty" mapstructure:"skills"`
Subagents *SubagentsConfig `json:"subagents,omitempty"` Subagents *SubagentsConfig `json:"subagents,omitempty" mapstructure:"subagents"`
} }
type SubagentsConfig struct { type SubagentsConfig struct {
AllowAgents []string `json:"allow_agents,omitempty"` AllowAgents []string `json:"allow_agents,omitempty" mapstructure:"allow_agents"`
Model *AgentModelConfig `json:"model,omitempty"` Model *AgentModelConfig `json:"model,omitempty" mapstructure:"model"`
} }
type PeerMatch struct { type PeerMatch struct {
Kind string `json:"kind"` Kind string `json:"kind" mapstructure:"kind"`
ID string `json:"id"` ID string `json:"id" mapstructure:"id"`
} }
type BindingMatch struct { type BindingMatch struct {
Channel string `json:"channel"` Channel string `json:"channel" mapstructure:"channel"`
AccountID string `json:"account_id,omitempty"` AccountID string `json:"account_id,omitempty" mapstructure:"account_id"`
Peer *PeerMatch `json:"peer,omitempty"` Peer *PeerMatch `json:"peer,omitempty" mapstructure:"peer"`
GuildID string `json:"guild_id,omitempty"` GuildID string `json:"guild_id,omitempty" mapstructure:"guild_id"`
TeamID string `json:"team_id,omitempty"` TeamID string `json:"team_id,omitempty" mapstructure:"team_id"`
} }
type AgentBinding struct { type AgentBinding struct {
AgentID string `json:"agent_id"` AgentID string `json:"agent_id" mapstructure:"agent_id"`
Match BindingMatch `json:"match"` Match BindingMatch `json:"match" mapstructure:"match"`
} }
type SessionConfig struct { type SessionConfig struct {
DMScope string `json:"dm_scope,omitempty"` DMScope string `json:"dm_scope,omitempty" mapstructure:"dm_scope"`
IdentityLinks map[string][]string `json:"identity_links,omitempty"` IdentityLinks map[string][]string `json:"identity_links,omitempty" mapstructure:"identity_links"`
} }
type AgentDefaults struct { type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Workspace string `json:"workspace" mapstructure:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` RestrictToWorkspace bool `json:"restrict_to_workspace" mapstructure:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` Provider string `json:"provider" mapstructure:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` Model string `json:"model" mapstructure:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
ModelFallbacks []string `json:"model_fallbacks,omitempty"` ModelFallbacks []string `json:"model_fallbacks,omitempty" mapstructure:"model_fallbacks"`
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` ImageModel string `json:"image_model,omitempty" mapstructure:"image_model" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty" mapstructure:"image_model_fallbacks"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" mapstructure:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` Temperature *float64 `json:"temperature,omitempty" mapstructure:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` MaxToolIterations int `json:"max_tool_iterations" mapstructure:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
} }
type ChannelsConfig struct { type ChannelsConfig struct {
WhatsApp WhatsAppConfig `json:"whatsapp"` WhatsApp WhatsAppConfig `json:"whatsapp" mapstructure:"whatsapp"`
Telegram TelegramConfig `json:"telegram"` Telegram TelegramConfig `json:"telegram" mapstructure:"telegram"`
Feishu FeishuConfig `json:"feishu"` Feishu FeishuConfig `json:"feishu" mapstructure:"feishu"`
Discord DiscordConfig `json:"discord"` Discord DiscordConfig `json:"discord" mapstructure:"discord"`
MaixCam MaixCamConfig `json:"maixcam"` MaixCam MaixCamConfig `json:"maixcam" mapstructure:"maixcam"`
QQ QQConfig `json:"qq"` QQ QQConfig `json:"qq" mapstructure:"qq"`
DingTalk DingTalkConfig `json:"dingtalk"` DingTalk DingTalkConfig `json:"dingtalk" mapstructure:"dingtalk"`
Slack SlackConfig `json:"slack"` Slack SlackConfig `json:"slack" mapstructure:"slack"`
LINE LINEConfig `json:"line"` LINE LINEConfig `json:"line" mapstructure:"line"`
OneBot OneBotConfig `json:"onebot"` OneBot OneBotConfig `json:"onebot" mapstructure:"onebot"`
} }
type WhatsAppConfig struct { type WhatsAppConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"` BridgeURL string `json:"bridge_url" mapstructure:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
} }
type TelegramConfig struct { type TelegramConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` Token string `json:"token" mapstructure:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` Proxy string `json:"proxy" mapstructure:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
} }
type FeishuConfig struct { type FeishuConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"` AppID string `json:"app_id" mapstructure:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"` AppSecret string `json:"app_secret" mapstructure:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"` EncryptKey string `json:"encrypt_key" mapstructure:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"` VerificationToken string `json:"verification_token" mapstructure:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
} }
type DiscordConfig struct { type DiscordConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` Token string `json:"token" mapstructure:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
} }
type MaixCamConfig struct { type MaixCamConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"` Host string `json:"host" mapstructure:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"` Port int `json:"port" mapstructure:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
} }
type QQConfig struct { type QQConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"` AppID string `json:"app_id" mapstructure:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"` AppSecret string `json:"app_secret" mapstructure:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
} }
type DingTalkConfig struct { type DingTalkConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"` ClientID string `json:"client_id" mapstructure:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"` ClientSecret string `json:"client_secret" mapstructure:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
} }
type SlackConfig struct { type SlackConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"` BotToken string `json:"bot_token" mapstructure:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"` AppToken string `json:"app_token" mapstructure:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
} }
type LINEConfig struct { type LINEConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"` ChannelSecret string `json:"channel_secret" mapstructure:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"` ChannelAccessToken string `json:"channel_access_token" mapstructure:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"` WebhookHost string `json:"webhook_host" mapstructure:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"` WebhookPort int `json:"webhook_port" mapstructure:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"` WebhookPath string `json:"webhook_path" mapstructure:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
} }
type OneBotConfig struct { type OneBotConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"` WSUrl string `json:"ws_url" mapstructure:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"` AccessToken string `json:"access_token" mapstructure:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"` ReconnectInterval int `json:"reconnect_interval" mapstructure:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"` GroupTriggerPrefix []string `json:"group_trigger_prefix" mapstructure:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
} }
type HeartbeatConfig struct { type HeartbeatConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5 Interval int `json:"interval" mapstructure:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
} }
type DevicesConfig struct { type DevicesConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"` MonitorUSB bool `json:"monitor_usb" mapstructure:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
} }
type ProvidersConfig struct { type ProvidersConfig struct {
Anthropic ProviderConfig `json:"anthropic"` Anthropic ProviderConfig `json:"anthropic" mapstructure:"anthropic"`
OpenAI OpenAIProviderConfig `json:"openai"` OpenAI OpenAIProviderConfig `json:"openai" mapstructure:"openai"`
OpenRouter ProviderConfig `json:"openrouter"` OpenRouter ProviderConfig `json:"openrouter" mapstructure:"openrouter"`
Groq ProviderConfig `json:"groq"` Groq ProviderConfig `json:"groq" mapstructure:"groq"`
Zhipu ProviderConfig `json:"zhipu"` Zhipu ProviderConfig `json:"zhipu" mapstructure:"zhipu"`
VLLM ProviderConfig `json:"vllm"` VLLM ProviderConfig `json:"vllm" mapstructure:"vllm"`
Gemini ProviderConfig `json:"gemini"` Gemini ProviderConfig `json:"gemini" mapstructure:"gemini"`
Nvidia ProviderConfig `json:"nvidia"` Nvidia ProviderConfig `json:"nvidia" mapstructure:"nvidia"`
Ollama ProviderConfig `json:"ollama"` Ollama ProviderConfig `json:"ollama" mapstructure:"ollama"`
Moonshot ProviderConfig `json:"moonshot"` Moonshot ProviderConfig `json:"moonshot" mapstructure:"moonshot"`
ShengSuanYun ProviderConfig `json:"shengsuanyun"` ShengSuanYun ProviderConfig `json:"shengsuanyun" mapstructure:"shengsuanyun"`
DeepSeek ProviderConfig `json:"deepseek"` DeepSeek ProviderConfig `json:"deepseek" mapstructure:"deepseek"`
Cerebras ProviderConfig `json:"cerebras"` Cerebras ProviderConfig `json:"cerebras" mapstructure:"cerebras"`
VolcEngine ProviderConfig `json:"volcengine"` VolcEngine ProviderConfig `json:"volcengine" mapstructure:"volcengine"`
GitHubCopilot ProviderConfig `json:"github_copilot"` GitHubCopilot ProviderConfig `json:"github_copilot" mapstructure:"github_copilot"`
Antigravity ProviderConfig `json:"antigravity"` Antigravity ProviderConfig `json:"antigravity" mapstructure:"antigravity"`
Qwen ProviderConfig `json:"qwen"` Qwen ProviderConfig `json:"qwen" mapstructure:"qwen"`
} }
// IsEmpty checks if all provider configs are empty (no API keys or API bases set) // IsEmpty checks if all provider configs are empty (no API keys or API bases set)
@ -340,16 +281,16 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) {
} }
type ProviderConfig struct { type ProviderConfig struct {
APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` APIKey string `json:"api_key" mapstructure:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` APIBase string `json:"api_base" mapstructure:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` Proxy string `json:"proxy,omitempty" mapstructure:"proxy" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` AuthMethod string `json:"auth_method,omitempty" mapstructure:"auth_method" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc` ConnectMode string `json:"connect_mode,omitempty" mapstructure:"connect_mode" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
} }
type OpenAIProviderConfig struct { type OpenAIProviderConfig struct {
ProviderConfig ProviderConfig
WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"` WebSearch bool `json:"web_search" mapstructure:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"`
} }
// ModelConfig represents a model-centric provider configuration. // ModelConfig represents a model-centric provider configuration.
@ -359,22 +300,22 @@ type OpenAIProviderConfig struct {
// Default protocol is "openai" if no prefix is specified. // Default protocol is "openai" if no prefix is specified.
type ModelConfig struct { type ModelConfig struct {
// Required fields // Required fields
ModelName string `json:"model_name"` // User-facing alias for the model ModelName string `json:"model_name" mapstructure:"model_name"` // User-facing alias for the model
Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6") Model string `json:"model" mapstructure:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6")
// HTTP-based providers // HTTP-based providers
APIBase string `json:"api_base,omitempty"` // API endpoint URL APIBase string `json:"api_base,omitempty" mapstructure:"api_base"` // API endpoint URL
APIKey string `json:"api_key"` // API authentication key APIKey string `json:"api_key" mapstructure:"api_key"` // API authentication key
Proxy string `json:"proxy,omitempty"` // HTTP proxy URL Proxy string `json:"proxy,omitempty" mapstructure:"proxy"` // HTTP proxy URL
// Special providers (CLI-based, OAuth, etc.) // Special providers (CLI-based, OAuth, etc.)
AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token AuthMethod string `json:"auth_method,omitempty" mapstructure:"auth_method"` // Authentication method: oauth, token
ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc ConnectMode string `json:"connect_mode,omitempty" mapstructure:"connect_mode"` // Connection mode: stdio, grpc
Workspace string `json:"workspace,omitempty"` // Workspace path for CLI-based providers Workspace string `json:"workspace,omitempty" mapstructure:"workspace"` // Workspace path for CLI-based providers
// Optional optimizations // Optional optimizations
RPM int `json:"rpm,omitempty"` // Requests per minute limit RPM int `json:"rpm,omitempty" mapstructure:"rpm"` // Requests per minute limit
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens") MaxTokensField string `json:"max_tokens_field,omitempty" mapstructure:"max_tokens_field"` // Field name for max tokens (e.g., "max_completion_tokens")
} }
// Validate checks if the ModelConfig has all required fields. // Validate checks if the ModelConfig has all required fields.
@ -389,46 +330,46 @@ func (c *ModelConfig) Validate() error {
} }
type GatewayConfig struct { type GatewayConfig struct {
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"` Host string `json:"host" mapstructure:"host" env:"PICOCLAW_GATEWAY_HOST"`
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"` Port int `json:"port" mapstructure:"port" env:"PICOCLAW_GATEWAY_PORT"`
} }
type BraveConfig struct { type BraveConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"` APIKey string `json:"api_key" mapstructure:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` MaxResults int `json:"max_results" mapstructure:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
} }
type DuckDuckGoConfig struct { type DuckDuckGoConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` MaxResults int `json:"max_results" mapstructure:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
} }
type PerplexityConfig struct { type PerplexityConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` APIKey string `json:"api_key" mapstructure:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` MaxResults int `json:"max_results" mapstructure:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
} }
type WebToolsConfig struct { type WebToolsConfig struct {
Brave BraveConfig `json:"brave"` Brave BraveConfig `json:"brave" mapstructure:"brave"`
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"` DuckDuckGo DuckDuckGoConfig `json:"duckduckgo" mapstructure:"duckduckgo"`
Perplexity PerplexityConfig `json:"perplexity"` Perplexity PerplexityConfig `json:"perplexity" mapstructure:"perplexity"`
} }
type CronToolsConfig struct { type CronToolsConfig struct {
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout ExecTimeoutMinutes int `json:"exec_timeout_minutes" mapstructure:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
} }
type ExecConfig struct { type ExecConfig struct {
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"` EnableDenyPatterns bool `json:"enable_deny_patterns" mapstructure:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"` CustomDenyPatterns []string `json:"custom_deny_patterns" mapstructure:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
} }
type ToolsConfig struct { type ToolsConfig struct {
Web WebToolsConfig `json:"web"` Web WebToolsConfig `json:"web" mapstructure:"web"`
Cron CronToolsConfig `json:"cron"` Cron CronToolsConfig `json:"cron" mapstructure:"cron"`
Exec ExecConfig `json:"exec"` Exec ExecConfig `json:"exec" mapstructure:"exec"`
} }
func LoadConfig(path string) (*Config, error) { func LoadConfig(path string) (*Config, error) {
@ -449,13 +390,37 @@ func LoadConfig(path string) (*Config, error) {
} }
if err := v.Unmarshal(cfg, func(dc *mapstructure.DecoderConfig) { if err := v.Unmarshal(cfg, func(dc *mapstructure.DecoderConfig) {
dc.TagName = "json" dc.TagName = "mapstructure"
dc.WeaklyTypedInput = true dc.WeaklyTypedInput = true
dc.Squash = true dc.Squash = true
dc.DecodeHook = mapstructure.ComposeDecodeHookFunc( dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
func(_ reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { func(_ reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
modelCfgPtr := reflect.TypeOf((*AgentModelConfig)(nil)) modelCfgPtr := reflect.TypeOf((*AgentModelConfig)(nil))
modelCfgVal := reflect.TypeOf(AgentModelConfig{}) modelCfgVal := reflect.TypeOf(AgentModelConfig{})
fssPtr := reflect.TypeOf((*FlexibleStringSlice)(nil))
fssVal := reflect.TypeOf(FlexibleStringSlice{})
if t == fssPtr || t == fssVal {
converted := make(FlexibleStringSlice, 0)
switch raw := data.(type) {
case []string:
converted = append(converted, raw...)
case []interface{}:
for _, item := range raw {
converted = append(converted, fmt.Sprintf("%v", item))
}
case string:
converted = append(converted, raw)
default:
if data != nil {
converted = append(converted, fmt.Sprintf("%v", data))
}
}
if t == fssPtr {
return &converted, nil
}
return converted, nil
}
if t != modelCfgPtr && t != modelCfgVal { if t != modelCfgPtr && t != modelCfgVal {
return data, nil return data, nil
} }
@ -533,17 +498,8 @@ func SaveConfig(path string, cfg *Config) error {
v.SetConfigType(configType) v.SetConfigType(configType)
v.SetConfigFile(path) v.SetConfigFile(path)
settings := map[string]interface{}{} settings, err := structToTaggedMap(cfg)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Result: &settings,
WeaklyTypedInput: true,
Squash: true,
})
if err != nil { if err != nil {
return fmt.Errorf("failed to create config encoder: %w", err)
}
if err := decoder.Decode(cfg); err != nil {
return fmt.Errorf("failed to encode config map: %w", err) return fmt.Errorf("failed to encode config map: %w", err)
} }
@ -557,8 +513,8 @@ func SaveConfig(path string, cfg *Config) error {
delete(settings, "bindings") delete(settings, "bindings")
} }
for key, val := range settings { if err := v.MergeConfigMap(settings); err != nil {
v.Set(key, val) return fmt.Errorf("failed to merge config settings: %w", err)
} }
dir := filepath.Dir(path) dir := filepath.Dir(path)
@ -576,6 +532,121 @@ func SaveConfig(path string, cfg *Config) error {
return nil return nil
} }
func structToTaggedMap(v interface{}) (map[string]interface{}, error) {
encoded, err := encodeValueByMapstructureTag(reflect.ValueOf(v))
if err != nil {
return nil, err
}
m, ok := encoded.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("encoded value is not a map")
}
return m, nil
}
func encodeValueByMapstructureTag(v reflect.Value) (interface{}, error) {
if !v.IsValid() {
return nil, nil
}
for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
if v.IsNil() {
return nil, nil
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Struct:
t := v.Type()
out := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.PkgPath != "" {
continue
}
name, omitEmpty, skip := parseMapstructureTag(f.Tag.Get("mapstructure"), f.Name)
if skip {
continue
}
fv := v.Field(i)
if omitEmpty && fv.IsZero() {
continue
}
encoded, err := encodeValueByMapstructureTag(fv)
if err != nil {
return nil, err
}
if omitEmpty && encoded == nil {
continue
}
out[name] = encoded
}
return out, nil
case reflect.Map:
if v.IsNil() {
return nil, nil
}
out := make(map[string]interface{}, v.Len())
iter := v.MapRange()
for iter.Next() {
k := iter.Key()
if k.Kind() != reflect.String {
continue
}
encoded, err := encodeValueByMapstructureTag(iter.Value())
if err != nil {
return nil, err
}
out[k.String()] = encoded
}
return out, nil
case reflect.Slice, reflect.Array:
if v.Kind() == reflect.Slice && v.IsNil() {
return nil, nil
}
out := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
encoded, err := encodeValueByMapstructureTag(v.Index(i))
if err != nil {
return nil, err
}
out[i] = encoded
}
return out, nil
default:
return v.Interface(), nil
}
}
func parseMapstructureTag(tag string, fallback string) (name string, omitEmpty bool, skip bool) {
if tag == "-" {
return "", false, true
}
if tag == "" {
return fallback, false, false
}
parts := strings.Split(tag, ",")
name = parts[0]
if name == "" {
name = fallback
}
for i := 1; i < len(parts); i++ {
if parts[i] == "omitempty" {
omitEmpty = true
break
}
}
return name, omitEmpty, false
}
func ResolveConfigPath(home string) string { func ResolveConfigPath(home string) string {
configDir := filepath.Join(home, ".picoclaw") configDir := filepath.Join(home, ".picoclaw")
for _, name := range configFileNames { for _, name := range configFileNames {

View file

@ -1,44 +1,94 @@
package config package config
import ( import (
"encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
) )
func TestAgentModelConfig_UnmarshalString(t *testing.T) { func TestAgentModelConfig_StringFormatViaLoadConfig(t *testing.T) {
var m AgentModelConfig dir := t.TempDir()
if err := json.Unmarshal([]byte(`"gpt-4"`), &m); err != nil { configPath := filepath.Join(dir, "config.json")
t.Fatalf("unmarshal string: %v", err) jsonData := `{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"max_tool_iterations": 20
},
"list": [
{
"id": "sales",
"default": true,
"name": "Sales Bot",
"model": "gpt-4"
}
]
}
}`
if err := os.WriteFile(configPath, []byte(jsonData), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
} }
if m.Primary != "gpt-4" {
t.Errorf("Primary = %q, want 'gpt-4'", m.Primary) cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
} }
if m.Fallbacks != nil {
t.Errorf("Fallbacks = %v, want nil", m.Fallbacks) sales := cfg.Agents.List[0]
if sales.Model == nil || sales.Model.Primary != "gpt-4" {
t.Errorf("sales.Model = %+v", sales.Model)
} }
} }
func TestAgentModelConfig_UnmarshalObject(t *testing.T) { func TestAgentModelConfig_ObjectFormatViaLoadConfig(t *testing.T) {
var m AgentModelConfig dir := t.TempDir()
data := `{"primary": "claude-opus", "fallbacks": ["gpt-4o-mini", "haiku"]}` configPath := filepath.Join(dir, "config.json")
if err := json.Unmarshal([]byte(data), &m); err != nil { jsonData := `{
t.Fatalf("unmarshal object: %v", err) "agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"max_tool_iterations": 20
},
"list": [
{
"id": "support",
"name": "Support Bot",
"model": {
"primary": "claude-opus",
"fallbacks": ["gpt-4o-mini", "haiku"]
}
}
]
}
}`
if err := os.WriteFile(configPath, []byte(jsonData), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
} }
if m.Primary != "claude-opus" {
t.Errorf("Primary = %q, want 'claude-opus'", m.Primary) cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
} }
if len(m.Fallbacks) != 2 {
t.Fatalf("Fallbacks len = %d, want 2", len(m.Fallbacks)) support := cfg.Agents.List[0]
if support.Model == nil || support.Model.Primary != "claude-opus" {
t.Errorf("support.Model = %+v", support.Model)
} }
if m.Fallbacks[0] != "gpt-4o-mini" || m.Fallbacks[1] != "haiku" { if len(support.Model.Fallbacks) != 2 || support.Model.Fallbacks[0] != "gpt-4o-mini" || support.Model.Fallbacks[1] != "haiku" {
t.Errorf("Fallbacks = %v", m.Fallbacks) t.Errorf("support.Model.Fallbacks = %v", support.Model.Fallbacks)
} }
} }
func TestAgentConfig_FullParse(t *testing.T) { func TestAgentConfig_FullParseViaLoadConfig(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
jsonData := `{ jsonData := `{
"agents": { "agents": {
"defaults": { "defaults": {
@ -85,61 +135,23 @@ func TestAgentConfig_FullParse(t *testing.T) {
} }
}` }`
cfg := DefaultConfig() if err := os.WriteFile(configPath, []byte(jsonData), 0o600); err != nil {
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil { t.Fatalf("WriteFile() error: %v", err)
t.Fatalf("unmarshal: %v", err) }
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
} }
if len(cfg.Agents.List) != 2 { if len(cfg.Agents.List) != 2 {
t.Fatalf("agents.list len = %d, want 2", len(cfg.Agents.List)) t.Fatalf("agents.list len = %d, want 2", len(cfg.Agents.List))
} }
sales := cfg.Agents.List[0]
if sales.ID != "sales" || !sales.Default || sales.Name != "Sales Bot" {
t.Errorf("sales = %+v", sales)
}
if sales.Model == nil || sales.Model.Primary != "gpt-4" {
t.Errorf("sales.Model = %+v", sales.Model)
}
support := cfg.Agents.List[1]
if support.ID != "support" || support.Name != "Support Bot" {
t.Errorf("support = %+v", support)
}
if support.Model == nil || support.Model.Primary != "claude-opus" {
t.Errorf("support.Model = %+v", support.Model)
}
if len(support.Model.Fallbacks) != 1 || support.Model.Fallbacks[0] != "haiku" {
t.Errorf("support.Model.Fallbacks = %v", support.Model.Fallbacks)
}
if support.Subagents == nil || len(support.Subagents.AllowAgents) != 1 {
t.Errorf("support.Subagents = %+v", support.Subagents)
}
if len(cfg.Bindings) != 1 {
t.Fatalf("bindings len = %d, want 1", len(cfg.Bindings))
}
binding := cfg.Bindings[0]
if binding.AgentID != "support" || binding.Match.Channel != "telegram" {
t.Errorf("binding = %+v", binding)
}
if binding.Match.Peer == nil || binding.Match.Peer.Kind != "direct" || binding.Match.Peer.ID != "user123" {
t.Errorf("binding.Match.Peer = %+v", binding.Match.Peer)
}
if cfg.Session.DMScope != "per-peer" {
t.Errorf("Session.DMScope = %q", cfg.Session.DMScope)
}
if len(cfg.Session.IdentityLinks) != 1 {
t.Errorf("Session.IdentityLinks = %v", cfg.Session.IdentityLinks)
}
links := cfg.Session.IdentityLinks["john"]
if len(links) != 2 {
t.Errorf("john links = %v", links)
}
} }
func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) { func TestConfig_BackwardCompat_NoAgentsListViaLoadConfig(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
jsonData := `{ jsonData := `{
"agents": { "agents": {
"defaults": { "defaults": {
@ -151,9 +163,13 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
} }
}` }`
cfg := DefaultConfig() if err := os.WriteFile(configPath, []byte(jsonData), 0o600); err != nil {
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil { t.Fatalf("WriteFile() error: %v", err)
t.Fatalf("unmarshal: %v", err) }
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
} }
if len(cfg.Agents.List) != 0 { if len(cfg.Agents.List) != 0 {
@ -300,85 +316,6 @@ func TestSaveConfig_FilePermissions(t *testing.T) {
} }
} }
func TestSaveConfig_AllSupportedFormats(t *testing.T) {
tests := []struct {
name string
fileName string
expectedOut string
}{
{
name: "json",
fileName: "config.json",
expectedOut: "config.json",
},
{
name: "yaml",
fileName: "config.yaml",
expectedOut: "config.yaml",
},
{
name: "yml",
fileName: "config.yml",
expectedOut: "config.yml",
},
{
name: "toml",
fileName: "config.toml",
expectedOut: "config.toml",
},
{
name: "no_extension_defaults_to_json",
fileName: "config",
expectedOut: "config.json",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tmpDir := t.TempDir()
targetPath := filepath.Join(tmpDir, tc.fileName)
outPath := filepath.Join(tmpDir, tc.expectedOut)
cfg := DefaultConfig()
cfg.Agents.Defaults.Model = "unit-test-model"
cfg.Agents.Defaults.MaxTokens = 1234
cfg.Gateway.Host = "127.0.0.1"
cfg.Gateway.Port = 19090
cfg.Providers.Zhipu.APIKey = "test-zhipu-key"
if err := SaveConfig(targetPath, cfg); err != nil {
t.Fatalf("SaveConfig(%q) failed: %v", targetPath, err)
}
if _, err := os.Stat(outPath); err != nil {
t.Fatalf("expected output file %q to exist: %v", outPath, err)
}
loaded, err := LoadConfig(outPath)
if err != nil {
t.Fatalf("LoadConfig(%q) failed: %v", outPath, err)
}
if loaded.Agents.Defaults.Model != "unit-test-model" {
t.Errorf("Model = %q, want %q", loaded.Agents.Defaults.Model, "unit-test-model")
}
if loaded.Agents.Defaults.MaxTokens != 1234 {
t.Errorf("MaxTokens = %d, want %d", loaded.Agents.Defaults.MaxTokens, 1234)
}
if loaded.Gateway.Host != "127.0.0.1" {
t.Errorf("Gateway.Host = %q, want %q", loaded.Gateway.Host, "127.0.0.1")
}
if loaded.Gateway.Port != 19090 {
t.Errorf("Gateway.Port = %d, want %d", loaded.Gateway.Port, 19090)
}
if loaded.Providers.Zhipu.APIKey != "test-zhipu-key" {
t.Errorf("Providers.Zhipu.APIKey = %q, want %q", loaded.Providers.Zhipu.APIKey, "test-zhipu-key")
}
})
}
}
// TestConfig_Complete verifies all config fields are set // TestConfig_Complete verifies all config fields are set
func TestConfig_Complete(t *testing.T) { func TestConfig_Complete(t *testing.T) {
cfg := DefaultConfig() cfg := DefaultConfig()

View file

@ -0,0 +1,142 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestLoadConfig_FlexibleStringSlice_MixedArray(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
content := `{
"channels": {
"telegram": {
"enabled": true,
"token": "x",
"allow_from": ["u1", 123, true]
}
}
}`
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
got := cfg.Channels.Telegram.AllowFrom
want := []string{"u1", "123", "true"}
if len(got) != len(want) {
t.Fatalf("allow_from len=%d, want=%d (%v)", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("allow_from[%d]=%q, want %q", i, got[i], want[i])
}
}
}
func TestLoadConfig_FlexibleStringSlice_SingleString(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
content := `{
"channels": {
"telegram": {
"enabled": true,
"token": "x",
"allow_from": "solo-user"
}
}
}`
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
if len(cfg.Channels.Telegram.AllowFrom) != 1 || cfg.Channels.Telegram.AllowFrom[0] != "solo-user" {
t.Fatalf("allow_from=%v, want [solo-user]", cfg.Channels.Telegram.AllowFrom)
}
}
func TestLoadConfig_InvalidConfigSyntax(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.yaml")
invalid := "agents:\n defaults:\n model: [unclosed"
if err := os.WriteFile(configPath, []byte(invalid), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
_, err := LoadConfig(configPath)
if err == nil {
t.Fatal("LoadConfig() expected error for invalid syntax")
}
}
func TestLoadConfig_AutoMigrateProvidersToModelList(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
content := `{
"model_list": [],
"agents": {
"defaults": {
"provider": "openai",
"model": "gpt-5.2"
}
},
"providers": {
"openai": {
"api_key": "sk-test"
}
}
}`
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
if len(cfg.ModelList) == 0 {
t.Fatal("model_list should be auto-migrated from providers")
}
if cfg.ModelList[0].APIKey != "sk-test" {
t.Fatalf("migrated api_key=%q, want sk-test", cfg.ModelList[0].APIKey)
}
}
func TestLoadConfig_ValidateModelListError(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
content := `{
"model_list": [
{
"model_name": "broken",
"model": ""
}
]
}`
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
_, err := LoadConfig(configPath)
if err == nil {
t.Fatal("LoadConfig() expected validation error")
}
if !strings.Contains(err.Error(), "model is required") {
t.Fatalf("error=%v, want contains 'model is required'", err)
}
}