diff --git a/README.zh.md b/README.zh.md
index 4d8ee25bb..f68fa2c77 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -3,7 +3,7 @@
PicoClaw: 基于Go语言的超高效 AI 助手
-10$硬件 · 10MB内存 · 1秒启?· 皮皮虾,我们走!
+10$硬件 · 10MB内存 · 1秒启动 · 皮皮虾,我们走!
@@ -19,9 +19,9 @@
---
-🦐 **PicoClaw** 是一个受 [nanobot](https://github.com/HKUDS/nanobot) 启发的超轻量级个?AI 助手。它采用 **Go 语言** 从零重构,经历了一个“自举”过程——即?AI Agent 自身驱动了整个架构迁移和代码优化?
+🦐 **PicoClaw** 是一个受 [nanobot](https://github.com/HKUDS/nanobot) 启发的超轻量级个人 AI 助手。它采用 **Go 语言** 从零重构,经历了一个“自举”过程——即由 AI Agent 自身驱动了整个架构迁移和代码优化。
-⚡️ **极致轻量**:可?**10 美元** 的硬件上运行,内存占?**<10MB**。这意味着?OpenClaw 节省 99% 的内存,?Mac mini 便宜 98%?
+⚡️ **极致轻量**:可在 **10 美元** 的硬件上运行,内存占用 **<10MB**。这意味着比 OpenClaw 节省 99% 的内存,比 Mac mini 便宜 98%!
-注意:人手有限,中文文档可能略有滞后,请优先查看英文文档?
+注意:人手有限,中文文档可能略有滞后,请优先查看英文文档。
> [!CAUTION]
> **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明**
-> * **无加密货?(NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均?**诈骗**?
-> * **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**?
-> * **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信?
+> * **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**。
+> * **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
+> * **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
> * **注意:** picoclaw正在初期的快速功能开发阶段,可能有尚未修复的网络安全问题,在1.0正式版发布前,请不要将其部署到生产环境中
-> * **注意:** picoclaw最近合并了大量PRs,近期版本可能内存占用较?10~20MB),我们将在功能较为收敛后进行资源占用优化.
+> * **注意:** picoclaw最近合并了大量PRs,近期版本可能内存占用较大(10~20MB),我们将在功能较为收敛后进行资源占用优化.
## 📢 新闻 (News)
-2026-02-16 🎉 PicoClaw 在一周内突破?2K star! 感谢大家的关注!PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀,我们亟需社区开发者参与维? 我们需要的志愿者角色和roadmap已经发布到了[这里](docs/picoclaw_community_roadmap_260216.md), 期待你的参与?
+2026-02-16 🎉 PicoClaw 在一周内突破了12K star! 感谢大家的关注!PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀,我们亟需社区开发者参与维护. 我们需要的志愿者角色和roadmap已经发布到了[这里](docs/picoclaw_community_roadmap_260216.md), 期待你的参与!
-2026-02-13 🎉 **PicoClaw ?4 天内突破 5000 Stars?* 感谢社区的支持!由于正值中国春节假期,PR ?Issue 涌入较多,我们正在利用这段时间敲?**项目路线?(Roadmap)** 并组?**开发者群?*,以便加?PicoClaw 的开发?
-🚀 **行动号召?* 请在 GitHub Discussions 中提交您的功能请?(Feature Requests)。我们将在接下来的周会上进行审查和优先级排序?
+2026-02-13 🎉 **PicoClaw 在 4 天内突破 5000 Stars!** 感谢社区的支持!由于正值中国春节假期,PR 和 Issue 涌入较多,我们正在利用这段时间敲定 **项目路线图 (Roadmap)** 并组建 **开发者群组**,以便加速 PicoClaw 的开发。
+🚀 **行动号召:** 请在 GitHub Discussions 中提交您的功能请求 (Feature Requests)。我们将在接下来的周会上进行审查和优先级排序。
-2026-02-09 🎉 **PicoClaw 正式发布?* 仅用 1 天构建,旨在?AI Agent 带入 10 美元硬件?<10MB 内存的世界。?PicoClaw(皮皮虾),我们走!
+2026-02-09 🎉 **PicoClaw 正式发布!** 仅用 1 天构建,旨在将 AI Agent 带入 10 美元硬件与 <10MB 内存的世界。🦐 PicoClaw(皮皮虾),我们走!
-## ?特?
+## ✨ 特性
-🪶 **超轻量级**: 核心功能内存占用 <10MB ??Clawdbot ?99%?
+🪶 **超轻量级**: 核心功能内存占用 <10MB — 比 Clawdbot 小 99%。
-💰 **极低成本**: 高效到足以在 10 美元的硬件上运行 ??Mac mini 便宜 98%?
+💰 **极低成本**: 高效到足以在 10 美元的硬件上运行 — 比 Mac mini 便宜 98%。
-⚡️ **闪电启动**: 启动速度?400 倍,即使?0.6GHz 单核处理器上也能?1 秒内启动?
+⚡️ **闪电启动**: 启动速度快 400 倍,即使在 0.6GHz 单核处理器上也能在 1 秒内启动。
-🌍 **真正可移?*: ?RISC-V、ARM ?x86 架构的单二进制文件,一键运行!
+🌍 **真正可移植**: 跨 RISC-V、ARM 和 x86 架构的单二进制文件,一键运行!
-🤖 **AI 自举**: ?Go 语言原生实现 ?95% 的核心代码由 Agent 生成,并经由“人机回?(Human-in-the-loop)”微调?
+🤖 **AI 自举**: 纯 Go 语言原生实现 — 95% 的核心代码由 Agent 生成,并经由“人机回环 (Human-in-the-loop)”微调。
| | OpenClaw | NanoBot | **PicoClaw** |
| --- | --- | --- | --- |
| **语言** | TypeScript | Python | **Go** |
| **RAM** | >1GB | >100MB | **< 10MB** |
| **启动时间**(0.8GHz core) | >500s | >30s | **<1s** |
-| **成本** | Mac Mini $599 | 大多?Linux 开发板 ~$50 | **任意 Linux 开发板****低至 $10** |
+| **成本** | Mac Mini $599 | 大多数 Linux 开发板 ~$50 | **任意 Linux 开发板****低至 $10** |
## 🦾 演示
-### 🛠?标准助手工作?
+### 🛠️ 标准助手工作流
-🧩 全栈工程师模?/p> |
-🗂?日志与规划管?/p> |
-🔎 网络搜索与学?/p> |
+🧩 全栈工程师模式 |
+🗂️ 日志与规划管理 |
+🔎 网络搜索与学习 |

|
@@ -94,24 +94,24 @@

|
-| 开??部署 ?扩展 |
-日程 ?自动??记忆 |
-发现 ?洞察 ?趋势 |
+开发 • 部署 • 扩展 |
+日程 • 自动化 • 记忆 |
+发现 • 洞察 • 趋势 |
### 📱 在手机上轻松运行
-picoclaw 可以将你10年前的老旧手机废物利用,变身成为你的AI助理!快速指?
+picoclaw 可以将你10年前的老旧手机废物利用,变身成为你的AI助理!快速指南:
1. 先去应用商店下载安装Termux
-2. 打开后执行指?
+2. 打开后执行指令
```bash
-# 注意: 下面的v0.1.1 可以换为你实际看到的最新版?
+# 注意: 下面的v0.1.1 可以换为你实际看到的最新版本
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
chmod +x picoclaw-linux-arm64
pkg install proot
termux-chroot ./picoclaw-linux-arm64 onboard
```
-然后跟随下面的“快速开始”章节继续配置picoclaw即可使用?
+然后跟随下面的“快速开始”章节继续配置picoclaw即可使用!
@@ -119,21 +119,21 @@ termux-chroot ./picoclaw-linux-arm64 onboard
### 🐜 创新的低占用部署
-PicoClaw 几乎可以部署在任?Linux 设备上!
+PicoClaw 几乎可以部署在任何 Linux 设备上!
-* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) ?W(WiFi6) 版本,用于极简家庭助手?
-* $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),用于智能监控?
+* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) 或 W(WiFi6) 版本,用于极简家庭助手。
+* $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),用于智能监控。
[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)
-🌟 更多部署案例敬请期待?
+🌟 更多部署案例敬请期待!
## 📦 安装
### 使用预编译二进制文件安装
-?[Release 页面](https://github.com/sipeed/picoclaw/releases) 下载适用于您平台的固件?
+从 [Release 页面](https://github.com/sipeed/picoclaw/releases) 下载适用于您平台的固件。
### 从源码安装(获取最新特性,开发推荐)
@@ -143,20 +143,20 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
-# 构建(无需安装?
+# 构建(无需安装)
make build
# 为多平台构建
make build-all
-# 构建并安?
+# 构建并安装
make install
```
## 🐳 Docker Compose
-您也可以使用 Docker Compose 运行 PicoClaw,无需在本地安装任何环境?
+您也可以使用 Docker Compose 运行 PicoClaw,无需在本地安装任何环境。
```bash
# 1. 克隆仓库
@@ -165,9 +165,9 @@ cd picoclaw
# 2. 设置 API Key
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. 构建并启?
+# 3. 构建并启动
docker compose --profile gateway up -d
# 4. 查看日志
@@ -178,7 +178,7 @@ docker compose --profile gateway down
```
-### Agent 模式 (一次性运?
+### Agent 模式 (一次性运行)
```bash
# 提问
@@ -197,26 +197,26 @@ docker compose --profile gateway up -d
```
-### 🚀 快速开?
+### 🚀 快速开始
> [!TIP]
-> ?`~/.picoclaw/config.json`、`~/.picoclaw/config.yaml` ?`~/.picoclaw/config.toml` 中设置您?API Key?
-> PicoClaw 支持多种配置格式?*JSON**(默认)?*YAML**?*TOML**,通过 [spf13/viper](https://github.com/spf13/viper) 驱动?
+> 在 `~/.picoclaw/config.json`、`~/.picoclaw/config.yaml` 或 `~/.picoclaw/config.toml` 中设置您的 API Key。
+> 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)
-> 网络搜索?**可选的** - 获取免费?[Brave Search API](https://brave.com/search/api) (每月 2000 次免费查?
+> 网络搜索是 **可选的** - 获取免费的 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)
-**1. 初始?(Initialize)**
+**1. 初始化 (Initialize)**
```bash
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) 支持多种配置格式,选择其中任意一种即可:
- `config.json` - JSON 格式(默认)
-- `config.yaml` ?`config.yml` - YAML 格式
+- `config.yaml` 或 `config.yml` - YAML 格式
- `config.toml` - TOML 格式
```json
@@ -257,14 +257,14 @@ picoclaw onboard
```
-> **新功?*: `model_list` 配置格式支持零代码添?provider。详见[模型配置](#-模型配置-model_list)章节?
+> **新功能**: `model_list` 配置格式支持零代码添加 provider。详见[模型配置](#-模型配置-model_list)章节。
**3. 获取 API Key**
-* **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) - 提供免费层级 (2000 请求/?
+* **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) - 提供免费层级 (2000 请求/月)
-> **注意**: 完整的配置模板请参?`config.example.json`?
+> **注意**: 完整的配置模板请参考 `config.example.json`。
**4. 对话 (Chat)**
@@ -273,28 +273,28 @@ picoclaw agent -m "2+2 等于几?"
```
-就是这样!您?2 分钟内就拥有了一个可工作?AI 助手?
+就是这样!您在 2 分钟内就拥有了一个可工作的 AI 助手。
---
## 💬 聊天应用集成 (Chat Apps)
-通过 Telegram, Discord 或钉钉与您的 PicoClaw 对话?
+通过 Telegram, Discord 或钉钉与您的 PicoClaw 对话。
| 渠道 | 设置难度 |
| --- | --- |
-| **Telegram** | 简?(仅需 token) |
-| **Discord** | 简?(bot token + intents) |
-| **QQ** | 简?(AppID + AppSecret) |
+| **Telegram** | 简单 (仅需 token) |
+| **Discord** | 简单 (bot token + intents) |
+| **QQ** | 简单 (AppID + AppSecret) |
| **钉钉 (DingTalk)** | 中等 (app credentials) |
Telegram (推荐)
-**1. 创建机器?*
+**1. 创建机器人**
-* 打开 Telegram,搜?`@BotFather`
-* 发?`/newbot`,按照提示操?
+* 打开 Telegram,搜索 `@BotFather`
+* 发送 `/newbot`,按照提示操作
* 复制 token
**2. 配置**
@@ -312,7 +312,7 @@ picoclaw agent -m "2+2 等于几?"
```
-> ?Telegram 上的 `@userinfobot` 获取您的用户 ID?
+> 从 Telegram 上的 `@userinfobot` 获取您的用户 ID。
**3. 运行**
@@ -326,21 +326,21 @@ picoclaw gateway
Discord
-**1. 创建机器?*
+**1. 创建机器人**
* 前往 [https://discord.com/developers/applications](https://discord.com/developers/applications)
-* Create an application ?Bot ?Add Bot
+* Create an application → Bot → Add Bot
* 复制 bot token
-**2. 开?Intents**
+**2. 开启 Intents**
-* ?Bot 设置中,开?**MESSAGE CONTENT INTENT**
-* (可? 如果计划基于成员数据使用白名单,开?**SERVER MEMBERS INTENT**
+* 在 Bot 设置中,开启 **MESSAGE CONTENT INTENT**
+* (可选) 如果计划基于成员数据使用白名单,开启 **SERVER MEMBERS INTENT**
**3. 获取您的 User ID**
-* Discord 设置 ?Advanced ?开?**Developer Mode**
-* 右键点击您的头像 ?**Copy User ID**
+* Discord 设置 → Advanced → 开启 **Developer Mode**
+* 右键点击您的头像 → **Copy User ID**
**4. 配置**
@@ -359,10 +359,10 @@ picoclaw gateway
**5. 邀请机器人**
-* OAuth2 ?URL Generator
+* OAuth2 → URL Generator
* Scopes: `bot`
* Bot Permissions: `Send Messages`, `Read Message History`
-* 打开生成的邀?URL,将机器人添加到您的服务?
+* 打开生成的邀请 URL,将机器人添加到您的服务器
**6. 运行**
@@ -376,10 +376,10 @@ picoclaw gateway
QQ
-**1. 创建机器?*
+**1. 创建机器人**
* 前往 [QQ 开放平台](https://q.qq.com/#)
-* 创建应用 ?获取 **AppID** ?**AppSecret**
+* 创建应用 → 获取 **AppID** 和 **AppSecret**
**2. 配置**
@@ -397,7 +397,7 @@ picoclaw gateway
```
-> ?`allow_from` 设为空以允许所有用户,或指?QQ 号以限制访问?
+> 将 `allow_from` 设为空以允许所有用户,或指定 QQ 号以限制访问。
**3. 运行**
@@ -411,11 +411,11 @@ picoclaw gateway
钉钉 (DingTalk)
-**1. 创建机器?*
+**1. 创建机器人**
* 前往 [开放平台](https://open.dingtalk.com/)
* 创建内部应用
-* 复制 Client ID ?Client Secret
+* 复制 Client ID 和 Client Secret
**2. 配置**
@@ -433,7 +433,7 @@ picoclaw gateway
```
-> ?`allow_from` 设为空以允许所有用户,或指?ID 以限制访问?
+> 将 `allow_from` 设为空以允许所有用户,或指定 ID 以限制访问。
**3. 运行**
@@ -446,9 +446,9 @@ picoclaw gateway
##
加入 Agent 社交网络
-只需通过 CLI 或任何集成的聊天应用发送一条消息,即可?PicoClaw 连接?Agent 社交网络?
+只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。
-**阅读 `https://clawdchat.ai/skill.md` 并按照说明加?[ClawdChat.ai**](https://clawdchat.ai)
+**阅读 `https://clawdchat.ai/skill.md` 并按照说明加入 [ClawdChat.ai**](https://clawdchat.ai)
## ⚙️ 配置详解
@@ -460,13 +460,13 @@ PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/work
```
~/.picoclaw/workspace/
-├── sessions/ # 对话会话和历?
+├── sessions/ # 对话会话和历史
├── memory/ # 长期记忆 (MEMORY.md)
-├── state/ # 持久化状?(最后一次频道等)
-├── cron/ # 定时任务数据?
-├── skills/ # 自定义技?
+├── state/ # 持久化状态 (最后一次频道等)
+├── cron/ # 定时任务数据库
+├── skills/ # 自定义技能
├── AGENTS.md # Agent 行为指南
-├── HEARTBEAT.md # 周期性任务提示词 (?30 分钟检查一?
+├── HEARTBEAT.md # 周期性任务提示词 (每 30 分钟检查一次)
├── IDENTITY.md # Agent 身份设定
├── SOUL.md # Agent 灵魂/性格
├── TOOLS.md # 工具描述
@@ -474,9 +474,9 @@ PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/work
```
-### 心跳 / 周期性任?(Heartbeat)
+### 心跳 / 周期性任务 (Heartbeat)
-PicoClaw 可以自动执行周期性任务。在工作区创?`HEARTBEAT.md` 文件?
+PicoClaw 可以自动执行周期性任务。在工作区创建 `HEARTBEAT.md` 文件:
```markdown
# Periodic Tasks
@@ -487,11 +487,11 @@ PicoClaw 可以自动执行周期性任务。在工作区创
```
-Agent 将每?30 分钟(可配置)读取此文件,并使用可用工具执行任务?
+Agent 将每隔 30 分钟(可配置)读取此文件,并使用可用工具执行任务。
-#### 使用 Spawn 的异步任?
+#### 使用 Spawn 的异步任务
-对于耗时较长的任务(网络搜索、API 调用),使用 `spawn` 工具创建一?**?Agent (subagent)**?
+对于耗时较长的任务(网络搜索、API 调用),使用 `spawn` 工具创建一个 **子 Agent (subagent)**:
```markdown
# Periodic Tasks
@@ -505,35 +505,35 @@ Agent 将每
```
-**关键行为?*
+**关键行为:**
-| 特?| 描述 |
+| 特性 | 描述 |
| --- | --- |
-| **spawn** | 创建异步?Agent,不阻塞主心跳进?|
-| **独立上下?* | ?Agent 拥有独立上下文,无会话历?|
-| **message tool** | ?Agent 通过 message 工具直接与用户通信 |
-| **非阻?* | spawn 后,心跳继续处理下一个任?|
+| **spawn** | 创建异步子 Agent,不阻塞主心跳进程 |
+| **独立上下文** | 子 Agent 拥有独立上下文,无会话历史 |
+| **message tool** | 子 Agent 通过 message 工具直接与用户通信 |
+| **非阻塞** | spawn 后,心跳继续处理下一个任务 |
-#### ?Agent 通信原理
+#### 子 Agent 通信原理
```
心跳触发 (Heartbeat triggers)
- ?
+ ↓
Agent 读取 HEARTBEAT.md
- ?
-对于长任? spawn ?Agent
- ? ?
-继续下一个任? ?Agent 独立工作
- ? ?
-所有任务完? ?Agent 使用 "message" 工具
- ? ?
+ ↓
+对于长任务: spawn 子 Agent
+ ↓ ↓
+继续下一个任务 子 Agent 独立工作
+ ↓ ↓
+所有任务完成 子 Agent 使用 "message" 工具
+ ↓ ↓
响应 HEARTBEAT_OK 用户直接收到结果
```
-?Agent 可以访问工具(message, web_search 等),并且无需通过?Agent 即可独立与用户通信?
+子 Agent 可以访问工具(message, web_search 等),并且无需通过主 Agent 即可独立与用户通信。
-**配置?*
+**配置:**
```json
{
@@ -545,43 +545,43 @@ Agent 读取 HEARTBEAT.md
```
-| 选项 | 默认?| 描述 |
+| 选项 | 默认值 | 描述 |
| --- | --- | --- |
| `enabled` | `true` | 启用/禁用心跳 |
-| `interval` | `30` | 检查间隔,单位分钟 (最? 5) |
+| `interval` | `30` | 检查间隔,单位分钟 (最小: 5) |
**环境变量:**
* `PICOCLAW_HEARTBEAT_ENABLED=false` 禁用
* `PICOCLAW_HEARTBEAT_INTERVAL=60` 更改间隔
-### 提供?(Providers)
+### 提供商 (Providers)
> [!NOTE]
-> Groq 通过 Whisper 提供免费的语音转录。如果配置了 Groq,Telegram 语音消息将被自动转录为文字?
+> Groq 通过 Whisper 提供免费的语音转录。如果配置了 Groq,Telegram 语音消息将被自动转录为文字。
-| 提供?| 用?| 获取 API Key |
+| 提供商 | 用途 | 获取 API Key |
| --- | --- | --- |
| `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) |
-| `openrouter(待测?` | LLM (推荐,可访问所有模? | [openrouter.ai](https://openrouter.ai) |
-| `anthropic(待测?` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
-| `openai(待测?` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
-| `deepseek(待测?` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
+| `openrouter(待测试)` | LLM (推荐,可访问所有模型) | [openrouter.ai](https://openrouter.ai) |
+| `anthropic(待测试)` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
+| `openai(待测试)` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
+| `deepseek(待测试)` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
| `qwen` | LLM (通义千问) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
| `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) |
| `cerebras` | LLM (Cerebras 直连) | [cerebras.ai](https://cerebras.ai) |
### 模型配置 (model_list)
-> **新功能!** PicoClaw 现在采用**以模型为中心**的配置方式。只需使用 `厂商/模型` 格式(如 `zhipu/glm-4.7`)即可添加新?provider—?*无需修改任何代码?*
+> **新功能!** PicoClaw 现在采用**以模型为中心**的配置方式。只需使用 `厂商/模型` 格式(如 `zhipu/glm-4.7`)即可添加新的 provider——**无需修改任何代码!**
-该设计同时支?*?Agent 场景**,提供灵活的 Provider 选择?
+该设计同时支持**多 Agent 场景**,提供灵活的 Provider 选择:
-- **不同 Agent 使用不同 Provider**:每?Agent 可以使用自己?LLM provider
-- **模型回退(Fallback?*:配置主模型和备用模型,提高可靠?
+- **不同 Agent 使用不同 Provider**:每个 Agent 可以使用自己的 LLM provider
+- **模型回退(Fallback)**:配置主模型和备用模型,提高可靠性
- **负载均衡**:在多个 API 端点之间分配请求
-- **集中化配?*:在一个地方管理所?provider
+- **集中化配置**:在一个地方管理所有 provider
#### 📋 所有支持的厂商
@@ -596,13 +596,13 @@ Agent 读取 HEARTBEAT.md
| **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) |
| **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [获取密钥](https://build.nvidia.com) |
-| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥?|
+| **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | 本地(无需密钥) |
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
| **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) |
-| **神算?* | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
-| **Antigravity** | `antigravity/` | Google Cloud | 自定?| ?OAuth |
+| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
+| **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
#### 基础配置示例
@@ -634,7 +634,7 @@ Agent 读取 HEARTBEAT.md
}
```
-#### 各厂商配置示?
+#### 各厂商配置示例
**OpenAI**
```json
@@ -671,7 +671,7 @@ Agent 读取 HEARTBEAT.md
"auth_method": "oauth"
}
```
-> 运行 `picoclaw auth login --provider anthropic` 来设?OAuth 凭证?
+> 运行 `picoclaw auth login --provider anthropic` 来设置 OAuth 凭证。
**Ollama (本地)**
```json
@@ -681,7 +681,7 @@ Agent 读取 HEARTBEAT.md
}
```
-**自定义代?API**
+**自定义代理/API**
```json
{
"model_name": "my-custom-model",
@@ -693,7 +693,7 @@ Agent 读取 HEARTBEAT.md
#### 负载均衡
-为同一个模型名称配置多个端点——PicoClaw 会自动在它们之间轮询?
+为同一个模型名称配置多个端点——PicoClaw 会自动在它们之间轮询:
```json
{
@@ -714,11 +714,11 @@ Agent 读取 HEARTBEAT.md
}
```
-#### 从旧?`providers` 配置迁移
+#### 从旧的 `providers` 配置迁移
-旧的 `providers` 配置格式**已弃?*,但为向后兼容仍支持?
+旧的 `providers` 配置格式**已弃用**,但为向后兼容仍支持。
-**旧配置(已弃用)?*
+**旧配置(已弃用):**
```json
{
"providers": {
@@ -754,12 +754,12 @@ Agent 读取 HEARTBEAT.md
}
```
-详细的迁移指南请参?[docs/migration/model-list-migration.md](docs/migration/model-list-migration.md)?
+详细的迁移指南请参考 [docs/migration/model-list-migration.md](docs/migration/model-list-migration.md)。
智谱 (Zhipu) 配置示例
-**1. 获取 API key ?base URL**
+**1. 获取 API key 和 base URL**
* 获取 [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
@@ -862,37 +862,37 @@ picoclaw agent -m "你好"
-## CLI 命令行参?
+## CLI 命令行参考
| 命令 | 描述 |
| --- | --- |
-| `picoclaw onboard` | 初始化配置和工作?|
-| `picoclaw agent -m "..."` | ?Agent 对话 |
-| `picoclaw agent` | 交互式聊天模?|
+| `picoclaw onboard` | 初始化配置和工作区 |
+| `picoclaw agent -m "..."` | 与 Agent 对话 |
+| `picoclaw agent` | 交互式聊天模式 |
| `picoclaw gateway` | 启动网关 (Gateway) |
-| `picoclaw status` | 显示状?|
-| `picoclaw cron list` | 列出所有定时任?|
+| `picoclaw status` | 显示状态 |
+| `picoclaw cron list` | 列出所有定时任务 |
| `picoclaw cron add ...` | 添加定时任务 |
### 定时任务 / 提醒 (Scheduled Tasks)
PicoClaw 通过 `cron` 工具支持定时提醒和重复任务:
-* **一次性提?*: "Remind me in 10 minutes" (10分钟后提醒我) ?10分钟后触发一?
-* **重复任务**: "Remind me every 2 hours" (?小时提醒? ??小时触发
-* **Cron 表达?*: "Remind me at 9am daily" (每天上午9点提醒我) ?使用 cron 表达?
+* **一次性提醒**: "Remind me in 10 minutes" (10分钟后提醒我) → 10分钟后触发一次
+* **重复任务**: "Remind me every 2 hours" (每2小时提醒我) → 每2小时触发
+* **Cron 表达式**: "Remind me at 9am daily" (每天上午9点提醒我) → 使用 cron 表达式
-任务存储?`~/.picoclaw/workspace/cron/` 中并自动处理?
+任务存储在 `~/.picoclaw/workspace/cron/` 中并自动处理。
## 🤝 贡献与路线图 (Roadmap)
-欢迎提交 PR!代码库刻意保持小巧和可读。?
+欢迎提交 PR!代码库刻意保持小巧和可读。🤗
-路线图即将发?..
+路线图即将发布...
-开发者群组正在组建中,入群门槛:至少合并?1 ?PR?
+开发者群组正在组建中,入群门槛:至少合并过 1 个 PR。
-用户群组?
+用户群组:
Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
@@ -902,12 +902,12 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
### 网络搜索提示 "API 配置问题"
-如果您尚未配置搜?API Key,这是正常的。PicoClaw 会提供手动搜索的帮助链接?
+如果您尚未配置搜索 API Key,这是正常的。PicoClaw 会提供手动搜索的帮助链接。
-启用网络搜索?
+启用网络搜索:
-1. ?[https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查?
-2. 添加?`~/.picoclaw/config.json`:
+1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询)
+2. 添加到 `~/.picoclaw/config.json`:
```json
{
"tools": {
@@ -926,11 +926,11 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
### 遇到内容过滤错误 (Content Filtering Errors)
-某些提供商(如智谱)有严格的内容过滤。尝试改写您的问题或使用其他模型?
+某些提供商(如智谱)有严格的内容过滤。尝试改写您的问题或使用其他模型。
### 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 ? |
-| **智谱 (Zhipu)** | 200K tokens/?| 最适合中国用户 |
-| **Brave Search** | 2000 次查??| 网络搜索功能 |
-| **Groq** | 提供免费层级 | 极速推?(Llama, Mixtral) |
-| **Cerebras** | 提供免费层级 | 极速推?(Llama, Qwen ? |
+| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
+| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
+| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
+| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
+| **Cerebras** | 提供免费层级 | 极速推理 (Llama, Qwen 等) |
diff --git a/config/config.example.toml b/config/config.example.toml
index 09213d664..7cf848971 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -1,14 +1,48 @@
# 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]
workspace = "~/.picoclaw/workspace"
restrict_to_workspace = true
-model = "glm-4.7"
+model = "gpt4"
max_tokens = 8192
temperature = 0.7
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]
enabled = false
token = "YOUR_TELEGRAM_BOT_TOKEN"
@@ -20,110 +54,130 @@ enabled = false
token = "YOUR_DISCORD_BOT_TOKEN"
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]
enabled = false
host = "0.0.0.0"
port = 18790
allow_from = []
-[providers.anthropic]
+[channels.whatsapp]
enabled = false
-api_key = "YOUR_ANTHROPIC_API_KEY"
-api_base = "https://api.anthropic.com"
+bridge_url = "ws://localhost:3001"
+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]
-enabled = false
-api_key = "YOUR_OPENAI_API_KEY"
-api_base = "https://api.openai.com/v1"
+api_key = ""
+api_base = ""
+web_search = true
[providers.openrouter]
-enabled = false
-api_key = "YOUR_OPENROUTER_API_KEY"
-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/"
+api_key = "sk-or-v1-xxx"
+api_base = ""
[providers.groq]
-enabled = false
-api_key = "YOUR_GROQ_API_KEY"
-api_base = "https://api.groq.com/openai/v1"
+api_key = "gsk_xxx"
+api_base = ""
+
+[providers.zhipu]
+api_key = "YOUR_ZHIPU_API_KEY"
+api_base = ""
+
+[providers.gemini]
+api_key = ""
+api_base = ""
[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
-api_key = "YOUR_VLLM_API_KEY"
-api_base = "http://localhost:8000/v1"
+api_key = "YOUR_BRAVE_API_KEY"
+max_results = 5
-[providers.shengsuan_yun]
+[tools.web.perplexity]
enabled = false
-api_key = "YOUR_SHENGSUAN_YUN_API_KEY"
-api_base = "https://api.shengsuan.cloud/v1"
+api_key = "pplx-xxx"
+max_results = 5
-[gateway]
-enabled = true
-host = "0.0.0.0"
-port = 8080
-cors_enabled = true
-cors_origins = ["*"]
-
-[tools]
-enabled = true
+[tools.cron]
+exec_timeout_minutes = 5
[heartbeat]
enabled = true
-interval = 60
-timeout = 5
+interval = 30
[devices]
-enabled = true
+enabled = false
+monitor_usb = true
-[session]
-storage = "memory"
-# For persistent storage, set to "sqlite" and provide database_path
-# database_path = "~/.picoclaw/session.db"
+[gateway]
+host = "0.0.0.0"
+port = 18790
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 5df4121eb..a4a57e779 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"reflect"
+ "strings"
"sync/atomic"
"github.com/caarlos0/env/v11"
@@ -27,46 +28,17 @@ var configFileNames = []string{
// so allow_from can contain both "123" and 123.
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 {
- Agents AgentsConfig `json:"agents"`
- Bindings []AgentBinding `json:"bindings,omitempty"`
- Session SessionConfig `json:"session,omitempty"`
- Channels ChannelsConfig `json:"channels"`
- Providers ProvidersConfig `json:"providers,omitempty"`
- ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration
- Gateway GatewayConfig `json:"gateway"`
- Tools ToolsConfig `json:"tools"`
- Heartbeat HeartbeatConfig `json:"heartbeat"`
- Devices DevicesConfig `json:"devices"`
+ Agents AgentsConfig `json:"agents" mapstructure:"agents"`
+ Bindings []AgentBinding `json:"bindings,omitempty" mapstructure:"bindings"`
+ Session SessionConfig `json:"session,omitempty" mapstructure:"session"`
+ Channels ChannelsConfig `json:"channels" mapstructure:"channels"`
+ Providers ProvidersConfig `json:"providers,omitempty" mapstructure:"providers"`
+ ModelList []ModelConfig `json:"model_list" mapstructure:"model_list"` // New model-centric provider configuration
+ Gateway GatewayConfig `json:"gateway" mapstructure:"gateway"`
+ Tools ToolsConfig `json:"tools" mapstructure:"tools"`
+ Heartbeat HeartbeatConfig `json:"heartbeat" mapstructure:"heartbeat"`
+ Devices DevicesConfig `json:"devices" mapstructure:"devices"`
}
// MarshalJSON implements custom JSON marshaling for Config
@@ -74,8 +46,8 @@ type Config struct {
func (c Config) MarshalJSON() ([]byte, error) {
type Alias Config
aux := &struct {
- Providers *ProvidersConfig `json:"providers,omitempty"`
- Session *SessionConfig `json:"session,omitempty"`
+ Providers *ProvidersConfig `json:"providers,omitempty" mapstructure:"providers"`
+ Session *SessionConfig `json:"session,omitempty" mapstructure:"session"`
*Alias
}{
Alias: (*Alias)(&c),
@@ -95,216 +67,185 @@ func (c Config) MarshalJSON() ([]byte, error) {
}
type AgentsConfig struct {
- Defaults AgentDefaults `json:"defaults"`
- List []AgentConfig `json:"list,omitempty"`
+ Defaults AgentDefaults `json:"defaults" mapstructure:"defaults"`
+ List []AgentConfig `json:"list,omitempty" mapstructure:"list"`
}
// AgentModelConfig supports both string and structured model config.
// String format: "gpt-4" (just primary, no fallbacks)
// Object format: {"primary": "gpt-4", "fallbacks": ["claude-haiku"]}
type AgentModelConfig struct {
- Primary string `json:"primary,omitempty"`
- Fallbacks []string `json:"fallbacks,omitempty"`
-}
-
-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})
+ Primary string `json:"primary,omitempty" mapstructure:"primary"`
+ Fallbacks []string `json:"fallbacks,omitempty" mapstructure:"fallbacks"`
}
type AgentConfig struct {
- ID string `json:"id"`
- Default bool `json:"default,omitempty"`
- Name string `json:"name,omitempty"`
- Workspace string `json:"workspace,omitempty"`
- Model *AgentModelConfig `json:"model,omitempty"`
- Skills []string `json:"skills,omitempty"`
- Subagents *SubagentsConfig `json:"subagents,omitempty"`
+ ID string `json:"id" mapstructure:"id"`
+ Default bool `json:"default,omitempty" mapstructure:"default"`
+ Name string `json:"name,omitempty" mapstructure:"name"`
+ Workspace string `json:"workspace,omitempty" mapstructure:"workspace"`
+ Model *AgentModelConfig `json:"model,omitempty" mapstructure:"model"`
+ Skills []string `json:"skills,omitempty" mapstructure:"skills"`
+ Subagents *SubagentsConfig `json:"subagents,omitempty" mapstructure:"subagents"`
}
type SubagentsConfig struct {
- AllowAgents []string `json:"allow_agents,omitempty"`
- Model *AgentModelConfig `json:"model,omitempty"`
+ AllowAgents []string `json:"allow_agents,omitempty" mapstructure:"allow_agents"`
+ Model *AgentModelConfig `json:"model,omitempty" mapstructure:"model"`
}
type PeerMatch struct {
- Kind string `json:"kind"`
- ID string `json:"id"`
+ Kind string `json:"kind" mapstructure:"kind"`
+ ID string `json:"id" mapstructure:"id"`
}
type BindingMatch struct {
- Channel string `json:"channel"`
- AccountID string `json:"account_id,omitempty"`
- Peer *PeerMatch `json:"peer,omitempty"`
- GuildID string `json:"guild_id,omitempty"`
- TeamID string `json:"team_id,omitempty"`
+ Channel string `json:"channel" mapstructure:"channel"`
+ AccountID string `json:"account_id,omitempty" mapstructure:"account_id"`
+ Peer *PeerMatch `json:"peer,omitempty" mapstructure:"peer"`
+ GuildID string `json:"guild_id,omitempty" mapstructure:"guild_id"`
+ TeamID string `json:"team_id,omitempty" mapstructure:"team_id"`
}
type AgentBinding struct {
- AgentID string `json:"agent_id"`
- Match BindingMatch `json:"match"`
+ AgentID string `json:"agent_id" mapstructure:"agent_id"`
+ Match BindingMatch `json:"match" mapstructure:"match"`
}
type SessionConfig struct {
- DMScope string `json:"dm_scope,omitempty"`
- IdentityLinks map[string][]string `json:"identity_links,omitempty"`
+ DMScope string `json:"dm_scope,omitempty" mapstructure:"dm_scope"`
+ IdentityLinks map[string][]string `json:"identity_links,omitempty" mapstructure:"identity_links"`
}
type AgentDefaults struct {
- Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
- RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
- Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
- Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
- ModelFallbacks []string `json:"model_fallbacks,omitempty"`
- ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
- ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
- MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
- Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
- MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
+ Workspace string `json:"workspace" mapstructure:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
+ RestrictToWorkspace bool `json:"restrict_to_workspace" mapstructure:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
+ Provider string `json:"provider" mapstructure:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
+ Model string `json:"model" mapstructure:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
+ ModelFallbacks []string `json:"model_fallbacks,omitempty" mapstructure:"model_fallbacks"`
+ ImageModel string `json:"image_model,omitempty" mapstructure:"image_model" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
+ ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty" mapstructure:"image_model_fallbacks"`
+ MaxTokens int `json:"max_tokens" mapstructure:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
+ Temperature *float64 `json:"temperature,omitempty" mapstructure:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
+ MaxToolIterations int `json:"max_tool_iterations" mapstructure:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
}
type ChannelsConfig struct {
- WhatsApp WhatsAppConfig `json:"whatsapp"`
- Telegram TelegramConfig `json:"telegram"`
- Feishu FeishuConfig `json:"feishu"`
- Discord DiscordConfig `json:"discord"`
- MaixCam MaixCamConfig `json:"maixcam"`
- QQ QQConfig `json:"qq"`
- DingTalk DingTalkConfig `json:"dingtalk"`
- Slack SlackConfig `json:"slack"`
- LINE LINEConfig `json:"line"`
- OneBot OneBotConfig `json:"onebot"`
+ WhatsApp WhatsAppConfig `json:"whatsapp" mapstructure:"whatsapp"`
+ Telegram TelegramConfig `json:"telegram" mapstructure:"telegram"`
+ Feishu FeishuConfig `json:"feishu" mapstructure:"feishu"`
+ Discord DiscordConfig `json:"discord" mapstructure:"discord"`
+ MaixCam MaixCamConfig `json:"maixcam" mapstructure:"maixcam"`
+ QQ QQConfig `json:"qq" mapstructure:"qq"`
+ DingTalk DingTalkConfig `json:"dingtalk" mapstructure:"dingtalk"`
+ Slack SlackConfig `json:"slack" mapstructure:"slack"`
+ LINE LINEConfig `json:"line" mapstructure:"line"`
+ OneBot OneBotConfig `json:"onebot" mapstructure:"onebot"`
}
type WhatsAppConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
- BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
+ BridgeURL string `json:"bridge_url" mapstructure:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
}
type TelegramConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
- Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
- Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
+ Token string `json:"token" mapstructure:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
+ Proxy string `json:"proxy" mapstructure:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
}
type FeishuConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
- AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
- AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
- EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
- VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
+ AppID string `json:"app_id" mapstructure:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
+ AppSecret string `json:"app_secret" mapstructure:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
+ EncryptKey string `json:"encrypt_key" mapstructure:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
+ VerificationToken string `json:"verification_token" mapstructure:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
}
type DiscordConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
- Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
+ Token string `json:"token" mapstructure:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
}
type MaixCamConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
- Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
- Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
+ Host string `json:"host" mapstructure:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
+ Port int `json:"port" mapstructure:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
}
type QQConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
- AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
- AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
+ AppID string `json:"app_id" mapstructure:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
+ AppSecret string `json:"app_secret" mapstructure:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
}
type DingTalkConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
- ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
- ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
+ ClientID string `json:"client_id" mapstructure:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
+ ClientSecret string `json:"client_secret" mapstructure:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
}
type SlackConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
- BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
- AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
+ BotToken string `json:"bot_token" mapstructure:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
+ AppToken string `json:"app_token" mapstructure:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
}
type LINEConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
- ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
- ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
- WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
- WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
- WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
+ ChannelSecret string `json:"channel_secret" mapstructure:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
+ ChannelAccessToken string `json:"channel_access_token" mapstructure:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
+ WebhookHost string `json:"webhook_host" mapstructure:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
+ WebhookPort int `json:"webhook_port" mapstructure:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
+ WebhookPath string `json:"webhook_path" mapstructure:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
}
type OneBotConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
- WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
- AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
- ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
- GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
+ WSUrl string `json:"ws_url" mapstructure:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
+ AccessToken string `json:"access_token" mapstructure:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
+ ReconnectInterval int `json:"reconnect_interval" mapstructure:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
+ GroupTriggerPrefix []string `json:"group_trigger_prefix" mapstructure:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" mapstructure:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
}
type HeartbeatConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
- Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
+ Interval int `json:"interval" mapstructure:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
}
type DevicesConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
- MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
+ MonitorUSB bool `json:"monitor_usb" mapstructure:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
}
type ProvidersConfig struct {
- Anthropic ProviderConfig `json:"anthropic"`
- OpenAI OpenAIProviderConfig `json:"openai"`
- OpenRouter ProviderConfig `json:"openrouter"`
- Groq ProviderConfig `json:"groq"`
- Zhipu ProviderConfig `json:"zhipu"`
- VLLM ProviderConfig `json:"vllm"`
- Gemini ProviderConfig `json:"gemini"`
- Nvidia ProviderConfig `json:"nvidia"`
- Ollama ProviderConfig `json:"ollama"`
- Moonshot ProviderConfig `json:"moonshot"`
- ShengSuanYun ProviderConfig `json:"shengsuanyun"`
- DeepSeek ProviderConfig `json:"deepseek"`
- Cerebras ProviderConfig `json:"cerebras"`
- VolcEngine ProviderConfig `json:"volcengine"`
- GitHubCopilot ProviderConfig `json:"github_copilot"`
- Antigravity ProviderConfig `json:"antigravity"`
- Qwen ProviderConfig `json:"qwen"`
+ Anthropic ProviderConfig `json:"anthropic" mapstructure:"anthropic"`
+ OpenAI OpenAIProviderConfig `json:"openai" mapstructure:"openai"`
+ OpenRouter ProviderConfig `json:"openrouter" mapstructure:"openrouter"`
+ Groq ProviderConfig `json:"groq" mapstructure:"groq"`
+ Zhipu ProviderConfig `json:"zhipu" mapstructure:"zhipu"`
+ VLLM ProviderConfig `json:"vllm" mapstructure:"vllm"`
+ Gemini ProviderConfig `json:"gemini" mapstructure:"gemini"`
+ Nvidia ProviderConfig `json:"nvidia" mapstructure:"nvidia"`
+ Ollama ProviderConfig `json:"ollama" mapstructure:"ollama"`
+ Moonshot ProviderConfig `json:"moonshot" mapstructure:"moonshot"`
+ ShengSuanYun ProviderConfig `json:"shengsuanyun" mapstructure:"shengsuanyun"`
+ DeepSeek ProviderConfig `json:"deepseek" mapstructure:"deepseek"`
+ Cerebras ProviderConfig `json:"cerebras" mapstructure:"cerebras"`
+ VolcEngine ProviderConfig `json:"volcengine" mapstructure:"volcengine"`
+ GitHubCopilot ProviderConfig `json:"github_copilot" mapstructure:"github_copilot"`
+ Antigravity ProviderConfig `json:"antigravity" mapstructure:"antigravity"`
+ Qwen ProviderConfig `json:"qwen" mapstructure:"qwen"`
}
// 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 {
- APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
- APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
- Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
- AuthMethod string `json:"auth_method,omitempty" 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`
+ APIKey string `json:"api_key" mapstructure:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
+ APIBase string `json:"api_base" mapstructure:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
+ Proxy string `json:"proxy,omitempty" mapstructure:"proxy" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
+ AuthMethod string `json:"auth_method,omitempty" mapstructure:"auth_method" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
+ 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 {
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.
@@ -359,22 +300,22 @@ type OpenAIProviderConfig struct {
// Default protocol is "openai" if no prefix is specified.
type ModelConfig struct {
// Required fields
- ModelName string `json:"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")
+ ModelName string `json:"model_name" mapstructure:"model_name"` // User-facing alias for the model
+ Model string `json:"model" mapstructure:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6")
// HTTP-based providers
- APIBase string `json:"api_base,omitempty"` // API endpoint URL
- APIKey string `json:"api_key"` // API authentication key
- Proxy string `json:"proxy,omitempty"` // HTTP proxy URL
+ APIBase string `json:"api_base,omitempty" mapstructure:"api_base"` // API endpoint URL
+ APIKey string `json:"api_key" mapstructure:"api_key"` // API authentication key
+ Proxy string `json:"proxy,omitempty" mapstructure:"proxy"` // HTTP proxy URL
// Special providers (CLI-based, OAuth, etc.)
- AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token
- ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc
- Workspace string `json:"workspace,omitempty"` // Workspace path for CLI-based providers
+ AuthMethod string `json:"auth_method,omitempty" mapstructure:"auth_method"` // Authentication method: oauth, token
+ ConnectMode string `json:"connect_mode,omitempty" mapstructure:"connect_mode"` // Connection mode: stdio, grpc
+ Workspace string `json:"workspace,omitempty" mapstructure:"workspace"` // Workspace path for CLI-based providers
// Optional optimizations
- RPM int `json:"rpm,omitempty"` // Requests per minute limit
- MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
+ RPM int `json:"rpm,omitempty" mapstructure:"rpm"` // Requests per minute limit
+ 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.
@@ -389,46 +330,46 @@ func (c *ModelConfig) Validate() error {
}
type GatewayConfig struct {
- Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
- Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
+ Host string `json:"host" mapstructure:"host" env:"PICOCLAW_GATEWAY_HOST"`
+ Port int `json:"port" mapstructure:"port" env:"PICOCLAW_GATEWAY_PORT"`
}
type BraveConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
- APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
- MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
+ APIKey string `json:"api_key" mapstructure:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
+ MaxResults int `json:"max_results" mapstructure:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
}
type DuckDuckGoConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
- MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
+ MaxResults int `json:"max_results" mapstructure:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
}
type PerplexityConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
- APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
- MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
+ Enabled bool `json:"enabled" mapstructure:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
+ APIKey string `json:"api_key" mapstructure:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
+ MaxResults int `json:"max_results" mapstructure:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
}
type WebToolsConfig struct {
- Brave BraveConfig `json:"brave"`
- DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
- Perplexity PerplexityConfig `json:"perplexity"`
+ Brave BraveConfig `json:"brave" mapstructure:"brave"`
+ DuckDuckGo DuckDuckGoConfig `json:"duckduckgo" mapstructure:"duckduckgo"`
+ Perplexity PerplexityConfig `json:"perplexity" mapstructure:"perplexity"`
}
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 {
- EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
- CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_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" mapstructure:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
}
type ToolsConfig struct {
- Web WebToolsConfig `json:"web"`
- Cron CronToolsConfig `json:"cron"`
- Exec ExecConfig `json:"exec"`
+ Web WebToolsConfig `json:"web" mapstructure:"web"`
+ Cron CronToolsConfig `json:"cron" mapstructure:"cron"`
+ Exec ExecConfig `json:"exec" mapstructure:"exec"`
}
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) {
- dc.TagName = "json"
+ dc.TagName = "mapstructure"
dc.WeaklyTypedInput = true
dc.Squash = true
dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
func(_ reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
modelCfgPtr := reflect.TypeOf((*AgentModelConfig)(nil))
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 {
return data, nil
}
@@ -533,17 +498,8 @@ func SaveConfig(path string, cfg *Config) error {
v.SetConfigType(configType)
v.SetConfigFile(path)
- settings := map[string]interface{}{}
- decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
- TagName: "json",
- Result: &settings,
- WeaklyTypedInput: true,
- Squash: true,
- })
+ settings, err := structToTaggedMap(cfg)
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)
}
@@ -557,8 +513,8 @@ func SaveConfig(path string, cfg *Config) error {
delete(settings, "bindings")
}
- for key, val := range settings {
- v.Set(key, val)
+ if err := v.MergeConfigMap(settings); err != nil {
+ return fmt.Errorf("failed to merge config settings: %w", err)
}
dir := filepath.Dir(path)
@@ -576,6 +532,121 @@ func SaveConfig(path string, cfg *Config) error {
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 {
configDir := filepath.Join(home, ".picoclaw")
for _, name := range configFileNames {
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index bf8e16b59..310d10350 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -1,44 +1,94 @@
package config
import (
- "encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
)
-func TestAgentModelConfig_UnmarshalString(t *testing.T) {
- var m AgentModelConfig
- if err := json.Unmarshal([]byte(`"gpt-4"`), &m); err != nil {
- t.Fatalf("unmarshal string: %v", err)
+func TestAgentModelConfig_StringFormatViaLoadConfig(t *testing.T) {
+ dir := t.TempDir()
+ configPath := filepath.Join(dir, "config.json")
+ 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) {
- var m AgentModelConfig
- data := `{"primary": "claude-opus", "fallbacks": ["gpt-4o-mini", "haiku"]}`
- if err := json.Unmarshal([]byte(data), &m); err != nil {
- t.Fatalf("unmarshal object: %v", err)
+func TestAgentModelConfig_ObjectFormatViaLoadConfig(t *testing.T) {
+ dir := t.TempDir()
+ configPath := filepath.Join(dir, "config.json")
+ jsonData := `{
+ "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" {
- t.Errorf("Fallbacks = %v", m.Fallbacks)
+ if len(support.Model.Fallbacks) != 2 || support.Model.Fallbacks[0] != "gpt-4o-mini" || support.Model.Fallbacks[1] != "haiku" {
+ 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 := `{
"agents": {
"defaults": {
@@ -85,61 +135,23 @@ func TestAgentConfig_FullParse(t *testing.T) {
}
}`
- cfg := DefaultConfig()
- if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
- t.Fatalf("unmarshal: %v", err)
+ if err := os.WriteFile(configPath, []byte(jsonData), 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.Agents.List) != 2 {
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 := `{
"agents": {
"defaults": {
@@ -151,9 +163,13 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
}
}`
- cfg := DefaultConfig()
- if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
- t.Fatalf("unmarshal: %v", err)
+ if err := os.WriteFile(configPath, []byte(jsonData), 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.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
func TestConfig_Complete(t *testing.T) {
cfg := DefaultConfig()
diff --git a/pkg/config/loadconfig_parser_coverage_test.go b/pkg/config/loadconfig_parser_coverage_test.go
new file mode 100644
index 000000000..d5cb8cd9e
--- /dev/null
+++ b/pkg/config/loadconfig_parser_coverage_test.go
@@ -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)
+ }
+}