refactor(config): keep mainline features with pure viper/mapstructure parsing
This commit is contained in:
parent
41e6982f8d
commit
51ddfcac34
5 changed files with 826 additions and 622 deletions
314
README.zh.md
314
README.zh.md
|
|
@ -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 提供免费的语音转录。如果配置了 Groq,Telegram 语音消息将被自动转录为文字。
|
||||||
|
|
||||||
| 謠蝉セ帛<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>1icoClaw 莨壽署萓帶焔蜉ィ謳懃エ「逧<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 等) |
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
142
pkg/config/loadconfig_parser_coverage_test.go
Normal file
142
pkg/config/loadconfig_parser_coverage_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue