feat(config): support viper multi-format config loading and CLI config override
- replace json-only load/save with viper-based multi-format support (json/yaml/toml/ini) - add config path resolution for ~/.picoclaw with extension fallback and explicit --config/-c override - keep agent model backward compatibility (string/object forms) via decoder hooks - add regression tests for yaml/json loading, provider api_key decode, and config path resolution - add config examples and update docs across README translations
This commit is contained in:
parent
12f0c4a6cf
commit
ba5b95f008
18 changed files with 998 additions and 87 deletions
13
README.ja.md
13
README.ja.md
|
|
@ -128,7 +128,7 @@ cd picoclaw
|
|||
|
||||
# 2. API キーを設定
|
||||
cp config/config.example.json config/config.json
|
||||
vim config/config.json # DISCORD_BOT_TOKEN, プロバイダーの API キーを設定
|
||||
vim config/config.json # または config.yaml を使用(DISCORD_BOT_TOKEN、プロバイダーの API キーを設定)
|
||||
|
||||
# 3. ビルドと起動
|
||||
docker compose --profile gateway up -d
|
||||
|
|
@ -160,7 +160,8 @@ docker compose --profile gateway up -d
|
|||
### 🚀 クイックスタート(ネイティブ)
|
||||
|
||||
> [!TIP]
|
||||
> `~/.picoclaw/config.json` に API キーを設定してください。
|
||||
> `~/.picoclaw/config.json`、`~/.picoclaw/config.yaml`、`~/.picoclaw/config.toml`、または `~/.picoclaw/config.ini` に API キーを設定してください。
|
||||
> PicoClaw は複数の設定フォーマットをサポート:**JSON**(デフォルト)、**YAML**、**TOML**、**INI** ([spf13/viper](https://github.com/spf13/viper) 経由)。
|
||||
> API キーの取得先: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
||||
> Web 検索は **任意** です - 無料の [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)
|
||||
|
||||
|
|
@ -170,7 +171,13 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. 設定** (`~/.picoclaw/config.json`)
|
||||
**2. 設定** (`~/.picoclaw/config.json`、`~/.picoclaw/config.yaml`、`~/.picoclaw/config.toml`、または `~/.picoclaw/config.ini`)
|
||||
|
||||
[spf13/viper](https://github.com/spf13/viper) 経由で複数の設定フォーマットをサポート。以下のいずれかを選択:
|
||||
- `config.json` - JSON フォーマット(デフォルト)
|
||||
- `config.yaml` または `config.yml` - YAML フォーマット
|
||||
- `config.toml` - TOML フォーマット
|
||||
- `config.ini` - INI フォーマット
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -160,7 +160,7 @@ cd picoclaw
|
|||
|
||||
# 2. Set your API keys
|
||||
cp config/config.example.json config/config.json
|
||||
vim config/config.json # Set DISCORD_BOT_TOKEN, API keys, etc.
|
||||
vim config/config.json # Set DISCORD_BOT_TOKEN, API keys, etc. (or use config.yaml/config.toml/config.ini)
|
||||
|
||||
# 3. Build & Start
|
||||
docker compose --profile gateway up -d
|
||||
|
|
@ -192,7 +192,8 @@ docker compose --profile gateway up -d
|
|||
### 🚀 Quick Start
|
||||
|
||||
> [!TIP]
|
||||
> Set your API key in `~/.picoclaw/config.json`.
|
||||
> Set your API key in `~/.picoclaw/config.json`, `~/.picoclaw/config.yaml`, `~/.picoclaw/config.toml`, or `~/.picoclaw/config.ini`.
|
||||
> PicoClaw supports multiple configuration formats: **JSON** (default), **YAML**, **TOML**, **INI** via [spf13/viper](https://github.com/spf13/viper).
|
||||
> Get API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
||||
> Web search is **optional** - get free [Brave Search API](https://brave.com/search/api) (2000 free queries/month) or use built-in auto fallback.
|
||||
|
||||
|
|
@ -202,7 +203,13 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. Configure** (`~/.picoclaw/config.json`)
|
||||
# 2. Configure (`~/.picoclaw/config.json`, `~/.picoclaw/config.yaml`, `~/.picoclaw/config.toml`, or `~/.picoclaw/config.ini`)
|
||||
|
||||
PicoClaw supports multiple configuration formats via [spf13/viper](https://github.com/spf13/viper). Choose any one:
|
||||
- `config.json` - JSON format (default, most common)
|
||||
- `config.yaml` or `config.yml` - YAML format
|
||||
- `config.toml` - TOML format
|
||||
- `config.ini` - INI format
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ cd picoclaw
|
|||
|
||||
# 2. Configure suas API keys
|
||||
cp config/config.example.json config/config.json
|
||||
vim config/config.json # Configure DISCORD_BOT_TOKEN, API keys, etc.
|
||||
vim config/config.json # Configure DISCORD_BOT_TOKEN, API keys, etc. (ou use config.yaml/config.yaml/config.toml/config.ini)
|
||||
|
||||
# 3. Build & Iniciar
|
||||
docker compose --profile gateway up -d
|
||||
|
|
@ -199,7 +199,8 @@ docker compose --profile gateway up -d
|
|||
### 🚀 Início Rápido
|
||||
|
||||
> [!TIP]
|
||||
> Configure sua API key em `~/.picoclaw/config.json`.
|
||||
> Configure sua API key em `~/.picoclaw/config.json`, `~/.picoclaw/config.yaml`, `~/.picoclaw/config.toml` ou `~/.picoclaw/config.ini`.
|
||||
> PicoClaw suporta múltiplos formatos de configuração: **JSON** (padrão), **YAML**, **TOML**, **INI** via [spf13/viper](https://github.com/spf13/viper).
|
||||
> Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
||||
> Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas grátis/mês) ou use o fallback automático integrado.
|
||||
|
||||
|
|
@ -209,7 +210,13 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. Configurar** (`~/.picoclaw/config.json`)
|
||||
**2. Configurar** (`~/.picoclaw/config.json`, `~/.picoclaw/config.yaml`, `~/.picoclaw/config.toml` ou `~/.picoclaw/config.ini`)
|
||||
|
||||
PicoClaw suporta múltiplos formatos via [spf13/viper](https://github.com/spf13/viper). Escolha qualquer um:
|
||||
- `config.json` - Formato JSON (padrão)
|
||||
- `config.yaml` ou `config.yml` - Formato YAML
|
||||
- `config.toml` - Formato TOML
|
||||
- `config.ini` - Formato INI
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
19
README.vi.md
19
README.vi.md
|
|
@ -145,9 +145,9 @@ Bạn cũng có thể chạy PicoClaw bằng Docker Compose mà không cần cà
|
|||
git clone https://github.com/sipeed/picoclaw.git
|
||||
cd picoclaw
|
||||
|
||||
# 2. Thiết lập API Key
|
||||
# 2. Thiết lập API key
|
||||
cp config/config.example.json config/config.json
|
||||
vim config/config.json # Thiết lập DISCORD_BOT_TOKEN, API keys, v.v.
|
||||
vim config/config.json # Thiết lập DISCORD_BOT_TOKEN, API keys, v.v. (hoặc dùng config.yaml/config.toml/config.ini)
|
||||
|
||||
# 3. Build & Khởi động
|
||||
docker compose --profile gateway up -d
|
||||
|
|
@ -178,8 +178,13 @@ docker compose --profile gateway up -d
|
|||
|
||||
### 🚀 Bắt đầu nhanh
|
||||
|
||||
### Cấu hình
|
||||
|
||||
Bạn có thể cấu hình PicoClaw bằng cách chỉnh sửa file cấu hình ở các định dạng sau: `config.json`, `config.yaml`, `config.toml`, hoặc `config.ini`. Tất cả các định dạng này đều được hỗ trợ.
|
||||
|
||||
> [!TIP]
|
||||
> Thiết lập API key trong `~/.picoclaw/config.json`.
|
||||
> Thiết lập API key trong `~/.picoclaw/config.json`, `~/.picoclaw/config.yaml`, `~/.picoclaw/config.toml` hoặc `~/.picoclaw/config.ini`.
|
||||
> PicoClaw hỗ trợ nhiều định dạng cấu hình: **JSON** (mặc định), **YAML**, **TOML**, **INI** via [spf13/viper](https://github.com/spf13/viper).
|
||||
> Lấy API key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
||||
> Tìm kiếm web là **tùy chọn** — lấy [Brave Search API](https://brave.com/search/api) miễn phí (2000 truy vấn/tháng) hoặc dùng tính năng auto fallback tích hợp sẵn.
|
||||
|
||||
|
|
@ -189,7 +194,13 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. Cấu hình** (`~/.picoclaw/config.json`)
|
||||
**2. Cấu hình** (`~/.picoclaw/config.json`, `~/.picoclaw/config.yaml`, `~/.picoclaw/config.toml` hoặc `~/.picoclaw/config.ini`)
|
||||
|
||||
PicoClaw hỗ trợ nhiều định dạng via [spf13/viper](https://github.com/spf13/viper). Chọn bất kỳ một:
|
||||
- `config.json` - Định dạng JSON (mặc định)
|
||||
- `config.yaml` hoặc `config.yml` - Định dạng YAML
|
||||
- `config.toml` - Định dạng TOML
|
||||
- `config.ini` - Định dạng INI
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
13
README.zh.md
13
README.zh.md
|
|
@ -165,7 +165,7 @@ cd picoclaw
|
|||
|
||||
# 2. 设置 API Key
|
||||
cp config/config.example.json config/config.json
|
||||
vim config/config.json # 设置 DISCORD_BOT_TOKEN, API keys 等
|
||||
vim config/config.json # 或使用 config.yaml/config.toml/config.ini 设置 DISCORD_BOT_TOKEN, API keys 等
|
||||
|
||||
# 3. 构建并启动
|
||||
docker compose --profile gateway up -d
|
||||
|
|
@ -200,7 +200,8 @@ docker compose --profile gateway up -d
|
|||
### 🚀 快速开始
|
||||
|
||||
> [!TIP]
|
||||
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。
|
||||
> 在 `~/.picoclaw/config.json`、`~/.picoclaw/config.yaml`、`~/.picoclaw/config.toml` 或 `~/.picoclaw/config.ini` 中设置您的 API Key。
|
||||
> PicoClaw 支持多种配置格式:**JSON**(默认)、**YAML**、**TOML**、**INI**,通过 [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 次免费查询)
|
||||
|
||||
|
|
@ -211,7 +212,13 @@ picoclaw onboard
|
|||
|
||||
```
|
||||
|
||||
**2. 配置 (Configure)** (`~/.picoclaw/config.json`)
|
||||
**2. 配置 (Configure)** (`~/.picoclaw/config.json`、`~/.picoclaw/config.yaml`、`~/.picoclaw/config.toml` 或 `~/.picoclaw/config.ini`)
|
||||
|
||||
通过 [spf13/viper](https://github.com/spf13/viper) 支持多种配置格式,选择其中任意一种即可:
|
||||
- `config.json` - JSON 格式(默认)
|
||||
- `config.yaml` 或 `config.yml` - YAML 格式
|
||||
- `config.toml` - TOML 格式
|
||||
- `config.ini` - INI 格式
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
55
build.bat
Normal file
55
build.bat
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@echo off
|
||||
REM Build script for picoclaw on Windows
|
||||
REM Compiles the picoclaw binary for Windows x64
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Build variables
|
||||
set BINARY_NAME=picoclaw
|
||||
set BUILD_DIR=build
|
||||
set CMD_DIR=cmd/picoclaw
|
||||
set MAIN_GO=%CMD_DIR%/main.go
|
||||
|
||||
REM Get git information
|
||||
for /f "tokens=*" %%i in ('git describe --tags --always --dirty 2^>nul') do set VERSION=%%i
|
||||
if not defined VERSION set VERSION=dev
|
||||
|
||||
for /f "tokens=*" %%i in ('git rev-parse --short=8 HEAD 2^>nul') do set GIT_COMMIT=%%i
|
||||
if not defined GIT_COMMIT set GIT_COMMIT=dev
|
||||
|
||||
for /f "tokens=*" %%i in ('powershell -Command "Get-Date -Format 'o'"') do set BUILD_TIME=%%i
|
||||
|
||||
REM Get Go version
|
||||
for /f "tokens=3" %%i in ('go version') do set GO_VERSION=%%i
|
||||
|
||||
REM Set linker flags
|
||||
set LDFLAGS=-ldflags "-X main.version=%VERSION% -X main.gitCommit=%GIT_COMMIT% -X main.buildTime=%BUILD_TIME% -X main.goVersion=%GO_VERSION% -s -w"
|
||||
|
||||
REM Run generate
|
||||
echo Running go generate...
|
||||
go generate ./...
|
||||
if errorlevel 1 (
|
||||
echo Generate failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Create build directory
|
||||
if not exist %BUILD_DIR% mkdir %BUILD_DIR%
|
||||
|
||||
REM Build for Windows x64
|
||||
echo Building %BINARY_NAME% for Windows x64...
|
||||
set GOOS=windows
|
||||
set GOARCH=amd64
|
||||
go build -v -tags stdjson %LDFLAGS% -o %BUILD_DIR%\%BINARY_NAME%-windows-amd64.exe ./%CMD_DIR%
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build complete: %BUILD_DIR%\%BINARY_NAME%-windows-amd64.exe
|
||||
echo.
|
||||
echo Build output:
|
||||
dir %BUILD_DIR%\%BINARY_NAME%-windows-amd64.exe
|
||||
|
||||
endlocal
|
||||
|
|
@ -49,6 +49,7 @@ var (
|
|||
gitCommit string
|
||||
buildTime string
|
||||
goVersion string
|
||||
configPathOverride string
|
||||
)
|
||||
|
||||
const logo = "🦞"
|
||||
|
|
@ -125,6 +126,13 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
args, err := parseConfigPathArg(os.Args)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Args = args
|
||||
|
||||
command := os.Args[1]
|
||||
|
||||
switch command {
|
||||
|
|
@ -990,8 +998,32 @@ func authStatusCmd() {
|
|||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
if configPathOverride != "" {
|
||||
return configPathOverride
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".picoclaw", "config.json")
|
||||
return config.ResolveConfigPath(home)
|
||||
}
|
||||
|
||||
func parseConfigPathArg(args []string) ([]string, error) {
|
||||
for i := 2; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
if arg == "--config" || arg == "-c" {
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("%s requires a path", arg)
|
||||
}
|
||||
configPathOverride = args[i+1]
|
||||
args = append(args[:i], args[i+2:]...)
|
||||
i--
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(arg, "--config=") {
|
||||
configPathOverride = strings.TrimPrefix(arg, "--config=")
|
||||
args = append(args[:i], args[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService {
|
||||
|
|
|
|||
129
config/config.example.ini
Normal file
129
config/config.example.ini
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
; PicoClaw Configuration Example - INI Format
|
||||
; This is an alternative to config.json for users who prefer INI format
|
||||
|
||||
[agents.defaults]
|
||||
workspace=~/.picoclaw/workspace
|
||||
restrict_to_workspace=true
|
||||
model=glm-4.7
|
||||
max_tokens=8192
|
||||
temperature=0.7
|
||||
max_tool_iterations=20
|
||||
|
||||
[channels.telegram]
|
||||
enabled=false
|
||||
token=YOUR_TELEGRAM_BOT_TOKEN
|
||||
proxy=
|
||||
allow_from=YOUR_USER_ID
|
||||
|
||||
[channels.discord]
|
||||
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]
|
||||
enabled=false
|
||||
api_key=YOUR_ANTHROPIC_API_KEY
|
||||
api_base=https://api.anthropic.com
|
||||
|
||||
[providers.openai]
|
||||
enabled=false
|
||||
api_key=YOUR_OPENAI_API_KEY
|
||||
api_base=https://api.openai.com/v1
|
||||
|
||||
[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/
|
||||
|
||||
[providers.groq]
|
||||
enabled=false
|
||||
api_key=YOUR_GROQ_API_KEY
|
||||
api_base=https://api.groq.com/openai/v1
|
||||
|
||||
[providers.vllm]
|
||||
enabled=false
|
||||
api_key=YOUR_VLLM_API_KEY
|
||||
api_base=http://localhost:8000/v1
|
||||
|
||||
[providers.shengsuan_yun]
|
||||
enabled=false
|
||||
api_key=YOUR_SHENGSUAN_YUN_API_KEY
|
||||
api_base=https://api.shengsuan.cloud/v1
|
||||
|
||||
[gateway]
|
||||
enabled=true
|
||||
host=0.0.0.0
|
||||
port=8080
|
||||
cors_enabled=true
|
||||
cors_origins=*
|
||||
|
||||
[tools]
|
||||
enabled=true
|
||||
|
||||
[heartbeat]
|
||||
enabled=true
|
||||
interval=60
|
||||
timeout=5
|
||||
|
||||
[devices]
|
||||
enabled=true
|
||||
|
||||
[session]
|
||||
storage=memory
|
||||
; For persistent storage, set to "sqlite" and provide database_path
|
||||
; database_path=~/.picoclaw/session.db
|
||||
129
config/config.example.toml
Normal file
129
config/config.example.toml
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# PicoClaw Configuration Example - TOML Format
|
||||
# This is an alternative to config.json for users who prefer TOML format
|
||||
|
||||
[agents.defaults]
|
||||
workspace = "~/.picoclaw/workspace"
|
||||
restrict_to_workspace = true
|
||||
model = "glm-4.7"
|
||||
max_tokens = 8192
|
||||
temperature = 0.7
|
||||
max_tool_iterations = 20
|
||||
|
||||
[channels.telegram]
|
||||
enabled = false
|
||||
token = "YOUR_TELEGRAM_BOT_TOKEN"
|
||||
proxy = ""
|
||||
allow_from = ["YOUR_USER_ID"]
|
||||
|
||||
[channels.discord]
|
||||
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]
|
||||
enabled = false
|
||||
api_key = "YOUR_ANTHROPIC_API_KEY"
|
||||
api_base = "https://api.anthropic.com"
|
||||
|
||||
[providers.openai]
|
||||
enabled = false
|
||||
api_key = "YOUR_OPENAI_API_KEY"
|
||||
api_base = "https://api.openai.com/v1"
|
||||
|
||||
[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/"
|
||||
|
||||
[providers.groq]
|
||||
enabled = false
|
||||
api_key = "YOUR_GROQ_API_KEY"
|
||||
api_base = "https://api.groq.com/openai/v1"
|
||||
|
||||
[providers.vllm]
|
||||
enabled = false
|
||||
api_key = "YOUR_VLLM_API_KEY"
|
||||
api_base = "http://localhost:8000/v1"
|
||||
|
||||
[providers.shengsuan_yun]
|
||||
enabled = false
|
||||
api_key = "YOUR_SHENGSUAN_YUN_API_KEY"
|
||||
api_base = "https://api.shengsuan.cloud/v1"
|
||||
|
||||
[gateway]
|
||||
enabled = true
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
cors_enabled = true
|
||||
cors_origins = ["*"]
|
||||
|
||||
[tools]
|
||||
enabled = true
|
||||
|
||||
[heartbeat]
|
||||
enabled = true
|
||||
interval = 60
|
||||
timeout = 5
|
||||
|
||||
[devices]
|
||||
enabled = true
|
||||
|
||||
[session]
|
||||
storage = "memory"
|
||||
# For persistent storage, set to "sqlite" and provide database_path
|
||||
# database_path = "~/.picoclaw/session.db"
|
||||
134
config/config.example.yaml
Normal file
134
config/config.example.yaml
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# PicoClaw Configuration Example - YAML Format
|
||||
# This is an alternative to config.json for users who prefer YAML format
|
||||
|
||||
agents:
|
||||
defaults:
|
||||
workspace: ~/.picoclaw/workspace
|
||||
restrict_to_workspace: true
|
||||
model: glm-4.7
|
||||
max_tokens: 8192
|
||||
temperature: 0.7
|
||||
max_tool_iterations: 20
|
||||
|
||||
channels:
|
||||
telegram:
|
||||
enabled: false
|
||||
token: YOUR_TELEGRAM_BOT_TOKEN
|
||||
proxy: ""
|
||||
allow_from:
|
||||
- YOUR_USER_ID
|
||||
|
||||
discord:
|
||||
enabled: false
|
||||
token: YOUR_DISCORD_BOT_TOKEN
|
||||
allow_from: []
|
||||
|
||||
slack:
|
||||
enabled: false
|
||||
token: YOUR_SLACK_BOT_TOKEN
|
||||
allow_from: []
|
||||
|
||||
feishu:
|
||||
enabled: false
|
||||
token: YOUR_FEISHU_BOT_TOKEN
|
||||
secret: YOUR_FEISHU_SECRET
|
||||
allow_from: []
|
||||
|
||||
dingtalk:
|
||||
enabled: false
|
||||
token: YOUR_DINGTALK_BOT_TOKEN
|
||||
secret: YOUR_DINGTALK_SECRET
|
||||
allow_from: []
|
||||
|
||||
qq:
|
||||
enabled: false
|
||||
account: YOUR_QQ_ACCOUNT
|
||||
token: YOUR_QQ_BOT_TOKEN
|
||||
allow_from: []
|
||||
|
||||
whatsapp:
|
||||
enabled: false
|
||||
phone_number: YOUR_PHONE_NUMBER
|
||||
allow_from: []
|
||||
|
||||
line:
|
||||
enabled: false
|
||||
token: YOUR_LINE_CHANNEL_ACCESS_TOKEN
|
||||
secret: YOUR_LINE_CHANNEL_SECRET
|
||||
allow_from: []
|
||||
|
||||
onebot:
|
||||
enabled: false
|
||||
ws_url: ""
|
||||
allow_from: []
|
||||
|
||||
maixcam:
|
||||
enabled: false
|
||||
host: 0.0.0.0
|
||||
port: 18790
|
||||
allow_from: []
|
||||
|
||||
providers:
|
||||
anthropic:
|
||||
enabled: false
|
||||
api_key: YOUR_ANTHROPIC_API_KEY
|
||||
api_base: https://api.anthropic.com
|
||||
|
||||
openai:
|
||||
enabled: false
|
||||
api_key: YOUR_OPENAI_API_KEY
|
||||
api_base: https://api.openai.com/v1
|
||||
|
||||
openrouter:
|
||||
enabled: false
|
||||
api_key: YOUR_OPENROUTER_API_KEY
|
||||
api_base: https://openrouter.ai/api/v1
|
||||
|
||||
zhipu:
|
||||
enabled: false
|
||||
api_key: YOUR_ZHIPU_API_KEY
|
||||
api_base: https://open.bigmodel.cn/api/paas/v4
|
||||
|
||||
gemini:
|
||||
enabled: false
|
||||
api_key: YOUR_GEMINI_API_KEY
|
||||
api_base: https://generativelanguage.googleapis.com/v1beta/openai/
|
||||
|
||||
groq:
|
||||
enabled: false
|
||||
api_key: YOUR_GROQ_API_KEY
|
||||
api_base: https://api.groq.com/openai/v1
|
||||
|
||||
vllm:
|
||||
enabled: false
|
||||
api_key: YOUR_VLLM_API_KEY
|
||||
api_base: http://localhost:8000/v1
|
||||
|
||||
shengsuan_yun:
|
||||
enabled: false
|
||||
api_key: YOUR_SHENGSUAN_YUN_API_KEY
|
||||
api_base: https://api.shengsuan.cloud/v1
|
||||
|
||||
gateway:
|
||||
enabled: true
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
cors_enabled: true
|
||||
cors_origins:
|
||||
- "*"
|
||||
|
||||
tools:
|
||||
enabled: true
|
||||
|
||||
heartbeat:
|
||||
enabled: true
|
||||
interval: 60
|
||||
timeout: 5
|
||||
|
||||
devices:
|
||||
enabled: true
|
||||
|
||||
session:
|
||||
storage: memory
|
||||
# For persistent storage, set to "sqlite" and provide database_path
|
||||
# database_path: ~/.picoclaw/session.db
|
||||
12
go.mod
12
go.mod
|
|
@ -22,7 +22,19 @@ require (
|
|||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
|
|
|||
24
go.sum
24
go.sum
|
|
@ -32,6 +32,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQzFzurxw=
|
||||
github.com/github/copilot-sdk/go v0.1.23/go.mod h1:GdwwBfMbm9AABLEM3x5IZKw4ZfwCYxZ1BgyytmZenQ0=
|
||||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||
|
|
@ -41,6 +43,8 @@ github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2m
|
|||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
|
|
@ -102,14 +106,28 @@ github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv
|
|||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
|
||||
github.com/openai/openai-go/v3 v3.22.0 h1:6MEoNoV8sbjOVmXdvhmuX3BjVbVdcExbVyGixiyJ8ys=
|
||||
github.com/openai/openai-go/v3 v3.22.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -122,6 +140,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tencent-connect/botgo v0.2.1 h1:+BrTt9Zh+awL28GWC4g5Na3nQaGRWb0N5IctS8WqBCk=
|
||||
github.com/tencent-connect/botgo v0.2.1/go.mod h1:oO1sG9ybhXNickvt+CVym5khwQ+uKhTR+IhTqEfOVsI=
|
||||
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
|
|
@ -151,6 +171,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
|
@ -224,6 +246,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// FlexibleStringSlice is a []string that also accepts JSON numbers,
|
||||
|
|
@ -56,6 +59,14 @@ type Config struct {
|
|||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var configFileNames = []string{
|
||||
"config.json",
|
||||
"config.yaml",
|
||||
"config.yml",
|
||||
"config.toml",
|
||||
"config.ini",
|
||||
}
|
||||
|
||||
type AgentsConfig struct {
|
||||
Defaults AgentDefaults `json:"defaults"`
|
||||
List []AgentConfig `json:"list,omitempty"`
|
||||
|
|
@ -65,8 +76,8 @@ type AgentsConfig struct {
|
|||
// 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"`
|
||||
Primary string `json:"primary" mapstructure:"primary"`
|
||||
Fallbacks []string `json:"fallbacks,omitempty" mapstructure:"fallbacks"`
|
||||
}
|
||||
|
||||
func (m *AgentModelConfig) UnmarshalJSON(data []byte) error {
|
||||
|
|
@ -76,30 +87,20 @@ func (m *AgentModelConfig) UnmarshalJSON(data []byte) error {
|
|||
m.Fallbacks = nil
|
||||
return nil
|
||||
}
|
||||
type raw struct {
|
||||
|
||||
type Alias struct {
|
||||
Primary string `json:"primary"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
}
|
||||
var r raw
|
||||
if err := json.Unmarshal(data, &r); err != nil {
|
||||
aux := &Alias{}
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Primary = r.Primary
|
||||
m.Fallbacks = r.Fallbacks
|
||||
m.Primary = aux.Primary
|
||||
m.Fallbacks = aux.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 {
|
||||
ID string `json:"id"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
|
|
@ -139,16 +140,16 @@ type SessionConfig struct {
|
|||
}
|
||||
|
||||
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" 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,omitempty"`
|
||||
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,omitempty"`
|
||||
MaxTokens int `json:"max_tokens" mapstructure:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||
Temperature float64 `json:"temperature" 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 {
|
||||
|
|
@ -275,7 +276,7 @@ type ProviderConfig struct {
|
|||
|
||||
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"`
|
||||
}
|
||||
|
||||
type GatewayConfig struct {
|
||||
|
|
@ -454,20 +455,69 @@ func DefaultConfig() *Config {
|
|||
func LoadConfig(path string) (*Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := env.Parse(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, err
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigFile(path)
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
if err := v.Unmarshal(cfg, func(dc *mapstructure.DecoderConfig) {
|
||||
dc.TagName = "json"
|
||||
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{})
|
||||
if t != modelCfgPtr && t != modelCfgVal {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
switch raw := data.(type) {
|
||||
case string:
|
||||
m := AgentModelConfig{Primary: raw}
|
||||
if t == modelCfgPtr {
|
||||
return &m, nil
|
||||
}
|
||||
return m, nil
|
||||
case map[string]interface{}:
|
||||
m := AgentModelConfig{}
|
||||
if primary, ok := raw["primary"]; ok {
|
||||
m.Primary = fmt.Sprintf("%v", primary)
|
||||
}
|
||||
if fallbacks, ok := raw["fallbacks"]; ok {
|
||||
switch fb := fallbacks.(type) {
|
||||
case []interface{}:
|
||||
for _, item := range fb {
|
||||
m.Fallbacks = append(m.Fallbacks, fmt.Sprintf("%v", item))
|
||||
}
|
||||
case []string:
|
||||
m.Fallbacks = append(m.Fallbacks, fb...)
|
||||
}
|
||||
}
|
||||
if t == modelCfgPtr {
|
||||
return &m, nil
|
||||
}
|
||||
return m, nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
},
|
||||
)
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config data: %w", err)
|
||||
}
|
||||
|
||||
if err := env.Parse(cfg); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to parse env overrides: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
|
@ -477,17 +527,66 @@ func SaveConfig(path string, cfg *Config) error {
|
|||
cfg.mu.RLock()
|
||||
defer cfg.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
ext := filepath.Ext(path)
|
||||
var configType string
|
||||
switch ext {
|
||||
case ".yaml", ".yml":
|
||||
configType = "yaml"
|
||||
case ".toml":
|
||||
configType = "toml"
|
||||
case ".ini":
|
||||
configType = "ini"
|
||||
case ".json", "":
|
||||
configType = "json"
|
||||
if ext == "" {
|
||||
path = path + ".json"
|
||||
}
|
||||
default:
|
||||
configType = "json"
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigType(configType)
|
||||
v.SetConfigFile(path)
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
var dataMap map[string]interface{}
|
||||
if err := json.Unmarshal(data, &dataMap); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal config map: %w", err)
|
||||
}
|
||||
|
||||
for key, val := range dataMap {
|
||||
v.Set(key, val)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0600)
|
||||
if err := v.WriteConfigAs(path); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
if err := os.Chmod(path, 0600); err != nil {
|
||||
return fmt.Errorf("failed to set config file permission: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResolveConfigPath(home string) string {
|
||||
configDir := filepath.Join(home, ".picoclaw")
|
||||
for _, name := range configFileNames {
|
||||
path := filepath.Join(configDir, name)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return filepath.Join(configDir, "config.json")
|
||||
}
|
||||
|
||||
func (c *Config) WorkspacePath() string {
|
||||
|
|
|
|||
|
|
@ -38,30 +38,6 @@ func TestAgentModelConfig_UnmarshalObject(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAgentModelConfig_MarshalString(t *testing.T) {
|
||||
m := AgentModelConfig{Primary: "gpt-4"}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if string(data) != `"gpt-4"` {
|
||||
t.Errorf("marshal = %s, want '\"gpt-4\"'", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentModelConfig_MarshalObject(t *testing.T) {
|
||||
m := AgentModelConfig{Primary: "claude-opus", Fallbacks: []string{"haiku"}}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(data, &result)
|
||||
if result["primary"] != "claude-opus" {
|
||||
t.Errorf("primary = %v", result["primary"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentConfig_FullParse(t *testing.T) {
|
||||
jsonData := `{
|
||||
"agents": {
|
||||
|
|
|
|||
98
pkg/config/loadconfig_agentmodel_test.go
Normal file
98
pkg/config/loadconfig_agentmodel_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfig_AgentModelConfigStringFormat(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonFile := filepath.Join(tmpDir, "config.json")
|
||||
|
||||
jsonContent := `{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "test",
|
||||
"model": "gpt-4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"channels": {},
|
||||
"providers": {}
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(jsonFile, []byte(jsonContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test JSON file: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(jsonFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config with string model format: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Agents.List) == 0 {
|
||||
t.Fatal("Expected at least one agent")
|
||||
}
|
||||
|
||||
agent := cfg.Agents.List[0]
|
||||
if agent.Model == nil {
|
||||
t.Fatal("Agent model is nil")
|
||||
}
|
||||
|
||||
if agent.Model.Primary != "gpt-4" {
|
||||
t.Errorf("Expected model primary 'gpt-4', got '%s'", agent.Model.Primary)
|
||||
}
|
||||
|
||||
if agent.Model.Fallbacks != nil && len(agent.Model.Fallbacks) > 0 {
|
||||
t.Errorf("Expected no fallbacks, got %v", agent.Model.Fallbacks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_AgentModelConfigObjectFormat(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonFile := filepath.Join(tmpDir, "config.json")
|
||||
|
||||
jsonContent := `{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "test",
|
||||
"model": {
|
||||
"primary": "claude-opus",
|
||||
"fallbacks": ["gpt-4", "haiku"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"channels": {},
|
||||
"providers": {}
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(jsonFile, []byte(jsonContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test JSON file: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(jsonFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config with object model format: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Agents.List) == 0 {
|
||||
t.Fatal("Expected at least one agent")
|
||||
}
|
||||
|
||||
agent := cfg.Agents.List[0]
|
||||
if agent.Model == nil {
|
||||
t.Fatal("Agent model is nil")
|
||||
}
|
||||
|
||||
if agent.Model.Primary != "claude-opus" {
|
||||
t.Errorf("Expected model primary 'claude-opus', got '%s'", agent.Model.Primary)
|
||||
}
|
||||
|
||||
if len(agent.Model.Fallbacks) != 2 {
|
||||
t.Errorf("Expected 2 fallbacks, got %d", len(agent.Model.Fallbacks))
|
||||
}
|
||||
}
|
||||
54
pkg/config/path_resolution_test.go
Normal file
54
pkg/config/path_resolution_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveConfigPath_DefaultJSONWhenMissing(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
got := ResolveConfigPath(home)
|
||||
want := filepath.Join(home, ".picoclaw", "config.json")
|
||||
if got != want {
|
||||
t.Fatalf("ResolveConfigPath() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveConfigPath_PicksExistingNonJSON(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
configDir := filepath.Join(home, ".picoclaw")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error: %v", err)
|
||||
}
|
||||
yamlPath := filepath.Join(configDir, "config.yaml")
|
||||
if err := os.WriteFile(yamlPath, []byte("agents: {}"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
got := ResolveConfigPath(home)
|
||||
if got != yamlPath {
|
||||
t.Fatalf("ResolveConfigPath() = %q, want %q", got, yamlPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveConfigPath_PrefersJSONWhenBothExist(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
configDir := filepath.Join(home, ".picoclaw")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error: %v", err)
|
||||
}
|
||||
yamlPath := filepath.Join(configDir, "config.yaml")
|
||||
if err := os.WriteFile(yamlPath, []byte("agents: {}"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
jsonPath := filepath.Join(configDir, "config.json")
|
||||
if err := os.WriteFile(jsonPath, []byte("{}"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
got := ResolveConfigPath(home)
|
||||
if got != jsonPath {
|
||||
t.Fatalf("ResolveConfigPath() = %q, want %q", got, jsonPath)
|
||||
}
|
||||
}
|
||||
113
pkg/config/viper_config_test.go
Normal file
113
pkg/config/viper_config_test.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfig_YAML(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tmpDir, "config.yaml")
|
||||
|
||||
yamlContent := `
|
||||
agents:
|
||||
defaults:
|
||||
workspace: ~/.picoclaw/workspace
|
||||
model: gpt-4
|
||||
max_tokens: 8192
|
||||
temperature: 0.7
|
||||
max_tool_iterations: 20
|
||||
channels: {}
|
||||
providers: {}
|
||||
`
|
||||
|
||||
if err := os.WriteFile(yamlFile, []byte(yamlContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test YAML file: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(yamlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load YAML config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults.Model != "gpt-4" {
|
||||
t.Errorf("Expected model 'gpt-4', got '%s'", cfg.Agents.Defaults.Model)
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults.MaxTokens != 8192 {
|
||||
t.Errorf("Expected max_tokens 8192, got %d", cfg.Agents.Defaults.MaxTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_JSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonFile := filepath.Join(tmpDir, "config.json")
|
||||
|
||||
jsonContent := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "~/.picoclaw/workspace",
|
||||
"model": "claude-3",
|
||||
"max_tokens": 4096
|
||||
}
|
||||
},
|
||||
"channels": {},
|
||||
"providers": {}
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(jsonFile, []byte(jsonContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test JSON file: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(jsonFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load JSON config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults.Model != "claude-3" {
|
||||
t.Errorf("Expected model 'claude-3', got '%s'", cfg.Agents.Defaults.Model)
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults.MaxTokens != 4096 {
|
||||
t.Errorf("Expected max_tokens 4096, got %d", cfg.Agents.Defaults.MaxTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_NonExistent(t *testing.T) {
|
||||
cfg, err := LoadConfig("/nonexistent/path/config.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("Should not return error for non-existent file, got: %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Error("Should return default config for non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_YAMLProviderAPIKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tmpDir, "config.yaml")
|
||||
|
||||
yamlContent := `
|
||||
agents:
|
||||
defaults:
|
||||
model: glm-4.7
|
||||
providers:
|
||||
zhipu:
|
||||
api_key: test-zhipu-key
|
||||
`
|
||||
|
||||
if err := os.WriteFile(yamlFile, []byte(yamlContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test YAML file: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(yamlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load YAML config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Providers.Zhipu.APIKey != "test-zhipu-key" {
|
||||
t.Fatalf("Expected zhipu api_key to be loaded, got %q", cfg.Providers.Zhipu.APIKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,12 +56,29 @@ Default model is `google/gemini-3-flash-preview` if none is set.
|
|||
|
||||
## Config
|
||||
|
||||
Optional config file: `~/.summarize/config.json`
|
||||
Optional config file: `~/.summarize/config.json`, `~/.summarize/config.yaml`, `~/.summarize/config.toml`, or `~/.summarize/config.ini`
|
||||
|
||||
PicoClaw supports multiple configuration formats via [spf13/viper](https://github.com/spf13/viper):
|
||||
|
||||
```json
|
||||
{ "model": "openai/gpt-5.2" }
|
||||
```
|
||||
|
||||
Or in YAML format:
|
||||
```yaml
|
||||
model: openai/gpt-5.2
|
||||
```
|
||||
|
||||
Or in TOML format:
|
||||
```toml
|
||||
model = "openai/gpt-5.2"
|
||||
```
|
||||
|
||||
Or in INI format:
|
||||
```ini
|
||||
model = openai/gpt-5.2
|
||||
```
|
||||
|
||||
Optional services:
|
||||
- `FIRECRAWL_API_KEY` for blocked sites
|
||||
- `APIFY_API_TOKEN` for YouTube fallback
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue