feat(config): add YAML configuration file support
Users can now use either config.json or config.yml for configuration. The system automatically detects which format to use based on the file extension. JSON remains the default if no config file exists yet. - Add YAML unmarshaling with proper type normalization - Update getConfigPath() to check for both formats - Update all documentation to reflect dual format support - Add tests for YAML load and save operations
This commit is contained in:
parent
eda6e37332
commit
4b99d49569
9 changed files with 184 additions and 22 deletions
|
|
@ -160,7 +160,7 @@ docker compose --profile gateway up -d
|
|||
### 🚀 クイックスタート(ネイティブ)
|
||||
|
||||
> [!TIP]
|
||||
> `~/.picoclaw/config.json` に API キーを設定してください。
|
||||
> API キーは `~/.picoclaw/config.json`(または `~/.picoclaw/config.yml`)に設定してください。
|
||||
> 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 +170,7 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. 設定** (`~/.picoclaw/config.json`)
|
||||
**2. 設定** (`~/.picoclaw/config.json` または `~/.picoclaw/config.yml`)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -432,7 +432,7 @@ picoclaw gateway
|
|||
|
||||
## ⚙️ 設定
|
||||
|
||||
設定ファイル: `~/.picoclaw/config.json`
|
||||
設定ファイル: `~/.picoclaw/config.json`(`~/.picoclaw/config.yml` も対応)
|
||||
|
||||
### ワークスペース構成
|
||||
|
||||
|
|
@ -741,7 +741,7 @@ Discord: https://discord.gg/V4sAZ9XWpN
|
|||
|
||||
Web 検索を有効にするには:
|
||||
1. [https://brave.com/search/api](https://brave.com/search/api) で無料の API キーを取得(月 2000 クエリ無料)
|
||||
2. `~/.picoclaw/config.json` に追加:
|
||||
2. 設定ファイル(`~/.picoclaw/config.json` または `~/.picoclaw/config.yml`)に追加:
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ 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` (or `~/.picoclaw/config.yml`).
|
||||
> 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 +202,7 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. Configure** (`~/.picoclaw/config.json`)
|
||||
**2. Configure** (`~/.picoclaw/config.json` or `~/.picoclaw/config.yml`)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -472,7 +472,7 @@ Connect Picoclaw to the Agent Social Network simply by sending a single message
|
|||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Config file: `~/.picoclaw/config.json`
|
||||
Config file: `~/.picoclaw/config.json` (also supports `~/.picoclaw/config.yml`)
|
||||
|
||||
### Workspace Layout
|
||||
|
||||
|
|
@ -835,7 +835,7 @@ To enable web search:
|
|||
1. **Option 1 (Recommended)**: Get a free API key at [https://brave.com/search/api](https://brave.com/search/api) (2000 free queries/month) for the best results.
|
||||
2. **Option 2 (No Credit Card)**: If you don't have a key, we automatically fall back to **DuckDuckGo** (no key required).
|
||||
|
||||
Add the key to `~/.picoclaw/config.json` if using Brave:
|
||||
Add the key to your config file (`~/.picoclaw/config.json` or `~/.picoclaw/config.yml`) if using Brave:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ docker compose --profile gateway up -d
|
|||
### 🚀 Inicio Rapido
|
||||
|
||||
> [!TIP]
|
||||
> Configure sua API key em `~/.picoclaw/config.json`.
|
||||
> Configure sua API key em `~/.picoclaw/config.json` (ou `~/.picoclaw/config.yml`).
|
||||
> 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 gratis/mes) ou use o fallback automatico integrado.
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ docker compose --profile gateway up -d
|
|||
picoclaw onboard
|
||||
```
|
||||
|
||||
**2. Configurar** (`~/.picoclaw/config.json`)
|
||||
**2. Configurar** (`~/.picoclaw/config.json` ou `~/.picoclaw/config.yml`)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -478,7 +478,7 @@ Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma unica mens
|
|||
|
||||
## ⚙️ Configuracao Detalhada
|
||||
|
||||
Arquivo de configuracao: `~/.picoclaw/config.json`
|
||||
Arquivo de configuracao: `~/.picoclaw/config.json` (tambem suporta `~/.picoclaw/config.yml`)
|
||||
|
||||
### Estrutura do Workspace
|
||||
|
||||
|
|
@ -841,7 +841,7 @@ Para habilitar a busca web:
|
|||
1. **Opcao 1 (Recomendado)**: Obtenha uma API key gratuita em [https://brave.com/search/api](https://brave.com/search/api) (2000 consultas gratis/mes) para os melhores resultados.
|
||||
2. **Opcao 2 (Sem Cartao de Credito)**: Se voce nao tem uma key, o sistema automaticamente usa o **DuckDuckGo** como fallback (sem necessidade de key).
|
||||
|
||||
Adicione a key em `~/.picoclaw/config.json` se usar o Brave:
|
||||
Adicione a key no arquivo de configuracao (`~/.picoclaw/config.json` ou `~/.picoclaw/config.yml`) se usar o Brave:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
10
README.zh.md
10
README.zh.md
|
|
@ -200,7 +200,7 @@ docker compose --profile gateway up -d
|
|||
### 🚀 快速开始
|
||||
|
||||
> [!TIP]
|
||||
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。
|
||||
> 在配置文件中设置您的 API Key:`~/.picoclaw/config.json`(或 `~/.picoclaw/config.yml`)。
|
||||
> 获取 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 +211,7 @@ picoclaw onboard
|
|||
|
||||
```
|
||||
|
||||
**2. 配置 (Configure)** (`~/.picoclaw/config.json`)
|
||||
**2. 配置 (Configure)** (`~/.picoclaw/config.json` 或 `~/.picoclaw/config.yml`)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -438,7 +438,7 @@ picoclaw gateway
|
|||
|
||||
## ⚙️ 配置详解
|
||||
|
||||
配置文件路径: `~/.picoclaw/config.json`
|
||||
配置文件路径: `~/.picoclaw/config.json`(也支持 `~/.picoclaw/config.yml`)
|
||||
|
||||
### 工作区布局 (Workspace Layout)
|
||||
|
||||
|
|
@ -707,7 +707,7 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
|
|||
启用网络搜索:
|
||||
|
||||
1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询)
|
||||
2. 添加到 `~/.picoclaw/config.json`:
|
||||
2. 添加到配置文件(`~/.picoclaw/config.json` 或 `~/.picoclaw/config.yml`):
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
|
|
@ -741,4 +741,4 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
|
|||
| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
|
||||
| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
|
||||
| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
|
||||
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
|
||||
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
|
||||
|
|
|
|||
|
|
@ -985,7 +985,19 @@ func authStatusCmd() {
|
|||
|
||||
func getConfigPath() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".picoclaw", "config.json")
|
||||
configDir := filepath.Join(home, ".picoclaw")
|
||||
|
||||
jsonPath := filepath.Join(configDir, "config.json")
|
||||
if _, err := os.Stat(jsonPath); err == nil {
|
||||
return jsonPath
|
||||
}
|
||||
|
||||
ymlPath := filepath.Join(configDir, "config.yml")
|
||||
if _, err := os.Stat(ymlPath); err == nil {
|
||||
return ymlPath
|
||||
}
|
||||
|
||||
return jsonPath
|
||||
}
|
||||
|
||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Tools Configuration
|
||||
|
||||
PicoClaw's tools configuration is located in the `tools` field of `config.json`.
|
||||
PicoClaw's tools configuration is located in the `tools` field of your config file (`config.json` or `config.yml`).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error {
|
|||
if provider == "" {
|
||||
provider = "configured default"
|
||||
}
|
||||
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.yaml",
|
||||
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.json/config.yml",
|
||||
c.config.Agents.Defaults.Model, provider)
|
||||
|
||||
case "channels":
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// FlexibleStringSlice is a []string that also accepts JSON numbers,
|
||||
|
|
@ -379,7 +381,7 @@ func LoadConfig(path string) (*Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
if err := unmarshalConfigData(path, data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +396,7 @@ func SaveConfig(path string, cfg *Config) error {
|
|||
cfg.mu.RLock()
|
||||
defer cfg.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
data, err := marshalConfigData(path, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -407,6 +409,75 @@ func SaveConfig(path string, cfg *Config) error {
|
|||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
func isYAMLPath(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return ext == ".yml"
|
||||
}
|
||||
|
||||
func unmarshalConfigData(path string, data []byte, cfg *Config) error {
|
||||
if isYAMLPath(path) {
|
||||
return unmarshalYAMLIntoConfig(data, cfg)
|
||||
}
|
||||
return json.Unmarshal(data, cfg)
|
||||
}
|
||||
|
||||
func unmarshalYAMLIntoConfig(data []byte, cfg *Config) error {
|
||||
var raw interface{}
|
||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
normalized := normalizeYAMLValue(raw)
|
||||
jsonData, err := json.Marshal(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonData, cfg)
|
||||
}
|
||||
|
||||
func normalizeYAMLValue(v interface{}) interface{} {
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
m := make(map[string]interface{}, len(t))
|
||||
for k, vv := range t {
|
||||
m[k] = normalizeYAMLValue(vv)
|
||||
}
|
||||
return m
|
||||
case map[interface{}]interface{}:
|
||||
m := make(map[string]interface{}, len(t))
|
||||
for k, vv := range t {
|
||||
m[fmt.Sprintf("%v", k)] = normalizeYAMLValue(vv)
|
||||
}
|
||||
return m
|
||||
case []interface{}:
|
||||
out := make([]interface{}, len(t))
|
||||
for i := range t {
|
||||
out[i] = normalizeYAMLValue(t[i])
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func marshalConfigData(path string, cfg *Config) ([]byte, error) {
|
||||
if !isYAMLPath(path) {
|
||||
return json.MarshalIndent(cfg, "", " ")
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(jsonData, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return yaml.Marshal(raw)
|
||||
}
|
||||
|
||||
func (c *Config) WorkspacePath() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -243,3 +244,81 @@ func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
|
|||
t.Fatal("OpenAI codex web search should be false when disabled in config file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_YAML(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.yml")
|
||||
data := `
|
||||
agents:
|
||||
defaults:
|
||||
model: gpt-4o-mini
|
||||
providers:
|
||||
openai:
|
||||
web_search: false
|
||||
channels:
|
||||
telegram:
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(configPath, []byte(data), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults.Model != "gpt-4o-mini" {
|
||||
t.Fatalf("Agents.Defaults.Model = %q, want %q", cfg.Agents.Defaults.Model, "gpt-4o-mini")
|
||||
}
|
||||
if cfg.Providers.OpenAI.WebSearch {
|
||||
t.Fatal("OpenAI web_search should be false from YAML")
|
||||
}
|
||||
if !cfg.Channels.Telegram.Enabled {
|
||||
t.Fatal("Channels.Telegram.Enabled should be true from YAML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveConfig_YAML(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.yml")
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Agents.Defaults.Model = "test-model"
|
||||
if err := SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error: %v", err)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile() error: %v", err)
|
||||
}
|
||||
|
||||
content := string(raw)
|
||||
if !strings.Contains(content, "agents:") {
|
||||
t.Fatalf("yaml output should contain root key 'agents', got: %s", content)
|
||||
}
|
||||
if !strings.Contains(content, "defaults:") {
|
||||
t.Fatalf("yaml output should contain key 'defaults', got: %s", content)
|
||||
}
|
||||
if !strings.Contains(content, "model: test-model") {
|
||||
t.Fatalf("yaml output should contain updated model, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_YAML_ExtensionYAMLNotSupported(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.yaml")
|
||||
data := `
|
||||
agents:
|
||||
defaults:
|
||||
model: gpt-4o-mini
|
||||
`
|
||||
if err := os.WriteFile(configPath, []byte(data), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
if _, err := LoadConfig(configPath); err == nil {
|
||||
t.Fatal("LoadConfig() should fail for .yaml; only .yml is supported")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue