mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
update docs according to newest config version 2 (#2186)
This commit is contained in:
+80
-81
@@ -11,24 +11,35 @@ PicoClaw uses a schema versioning system for `config.json` to ensure smooth upgr
|
||||
- **Changes**: Added `version` field to Config struct
|
||||
- **Migration**: No structural changes needed for existing configs
|
||||
|
||||
### Version 2
|
||||
- **Introduction**: Model enable/disable support and channel config unification
|
||||
- **Changes**:
|
||||
- Added `enabled` field to `ModelConfig` — allows disabling individual model entries without removing them
|
||||
- During V1→V2 migration, `enabled` is auto-inferred: models with API keys or the reserved `local-model` name are enabled; others default to disabled
|
||||
- Migrated legacy channel fields: Discord `mention_only` → `group_trigger.mention_only`, OneBot `group_trigger_prefix` → `group_trigger.prefixes`
|
||||
- V0 configs now migrate directly to CurrentVersion (V2) instead of going through V1
|
||||
- `makeBackup()` now uses date-only suffix (e.g., `config.json.20260330.bak`) and also backs up `.security.yml`
|
||||
|
||||
## How It Works
|
||||
|
||||
### Automatic Migration
|
||||
When you load a config file:
|
||||
1. The system first reads the `version` field from the JSON
|
||||
2. Based on the detected version, it loads the appropriate config struct (`ConfigV0`, `ConfigV1`, etc.)
|
||||
2. Based on the detected version, it loads the appropriate config struct (`configV0`, `configV1`, etc.)
|
||||
3. If the loaded version is less than the latest, migrations are applied incrementally
|
||||
4. The version number is updated automatically
|
||||
5. The migrated config is automatically saved back to disk
|
||||
4. Before saving, the system automatically creates a date-stamped backup of `config.json` and `.security.yml`
|
||||
5. The version number is updated automatically
|
||||
6. The migrated config is automatically saved back to disk
|
||||
|
||||
### Version Field
|
||||
The `version` field in `config.json` indicates the schema version:
|
||||
- `0` or missing: Legacy config (no version field)
|
||||
- `1`: Current version with versioning support
|
||||
- `1`: Previous version (will be auto-migrated to V2 on load)
|
||||
- `2`: Current version
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"agents": {...},
|
||||
...
|
||||
}
|
||||
@@ -54,25 +65,25 @@ type ConfigV2 struct {
|
||||
### Step 2: Update Current Config Version
|
||||
|
||||
```go
|
||||
const CurrentConfigVersion = 2 // Increment this
|
||||
const CurrentVersion = 2 // Increment this
|
||||
```
|
||||
|
||||
### Step 3: Add a Loader Function
|
||||
|
||||
```go
|
||||
// loadConfigV2 loads a version 2 config
|
||||
func loadConfigV2(data []byte) (*Config, error) {
|
||||
// loadConfigV3 loads a version 3 config
|
||||
func loadConfigV3(data []byte) (*Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Parse to ConfigV2 struct
|
||||
var v2 ConfigV2
|
||||
if err := json.Unmarshal(data, &v2); err != nil {
|
||||
// Parse to ConfigV3 struct
|
||||
var v3 ConfigV3
|
||||
if err := json.Unmarshal(data, &v3); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to current Config
|
||||
cfg.Version = v2.Version
|
||||
cfg.Agents = v2.Agents
|
||||
cfg.Version = v3.Version
|
||||
cfg.Agents = v3.Agents
|
||||
// ... map other fields
|
||||
|
||||
return cfg, nil
|
||||
@@ -82,29 +93,12 @@ func loadConfigV2(data []byte) (*Config, error) {
|
||||
### Step 4: Add Migration Logic
|
||||
|
||||
```go
|
||||
// applyMigration applies a single migration step from fromVersion to toVersion
|
||||
func applyMigration(cfg *Config, fromVersion, toVersion int) (*Config, error) {
|
||||
switch toVersion {
|
||||
case 1:
|
||||
// Migration from version 0 to 1
|
||||
return &Config{
|
||||
Version: 1,
|
||||
Agents: cfg.Agents,
|
||||
// ... copy all fields
|
||||
}, nil
|
||||
case 2:
|
||||
// Migration from version 1 to 2
|
||||
// Example: Move or rename fields
|
||||
migrated := *cfg
|
||||
migrated.Version = 2
|
||||
// Apply structural changes
|
||||
if cfg.SomeOldField != "" {
|
||||
migrated.SomeNewField = cfg.SomeOldField
|
||||
}
|
||||
return &migrated, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported migration target version: %d", toVersion)
|
||||
}
|
||||
func (c *configV2) Migrate() (*Config, error) {
|
||||
// Apply V2→V3 structural changes here
|
||||
migrated := &c.Config
|
||||
migrated.Version = 3
|
||||
// Apply structural changes
|
||||
return migrated, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -120,7 +114,9 @@ func LoadConfig(path string) (*Config, error) {
|
||||
case 1:
|
||||
cfg, err = loadConfigV1(data)
|
||||
case 2:
|
||||
cfg, err = loadConfigV2(data)
|
||||
cfg, err = loadConfig(data)
|
||||
case 3:
|
||||
cfg, err = loadConfigV3(data)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version)
|
||||
}
|
||||
@@ -134,22 +130,22 @@ func LoadConfig(path string) (*Config, error) {
|
||||
Create a test in `config_migration_test.go`:
|
||||
|
||||
```go
|
||||
func TestMigrateV1ToV2(t *testing.T) {
|
||||
// Create a version 1 config
|
||||
v1Config := Config{
|
||||
Version: 1,
|
||||
func TestMigrateV2ToV3(t *testing.T) {
|
||||
// Create a version 2 config
|
||||
v2Config := Config{
|
||||
Version: 2,
|
||||
// ... set up test data
|
||||
}
|
||||
|
||||
// Apply migration
|
||||
migrated, err := applyMigration(&v1Config, 1, 2)
|
||||
migrated, err := v2Config.Migrate()
|
||||
if err != nil {
|
||||
t.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify version is updated
|
||||
if migrated.Version != 2 {
|
||||
t.Errorf("Expected version 2, got %d", migrated.Version)
|
||||
if migrated.Version != 3 {
|
||||
t.Errorf("Expected version 3, got %d", migrated.Version)
|
||||
}
|
||||
|
||||
// Verify data is preserved/transformed correctly
|
||||
@@ -164,58 +160,60 @@ func TestMigrateV1ToV2(t *testing.T) {
|
||||
3. **No Data Loss**: Migrations should preserve all user settings
|
||||
4. **Idempotent**: Running the same migration multiple times should be safe
|
||||
5. **Auto-Save**: Migrated configs are automatically saved to update the user's file
|
||||
6. **Test Thoroughly**: Test with real user config files
|
||||
7. **Update Defaults**: Keep `defaults.go` in sync with the latest schema
|
||||
6. **Auto-Backup**: Before saving, the system creates a date-stamped backup of `config.json` and `.security.yml`
|
||||
7. **Test Thoroughly**: Test with real user config files
|
||||
8. **Update Defaults**: Keep `defaults.go` in sync with the latest schema
|
||||
|
||||
## Example Migration
|
||||
|
||||
### Scenario: Adding a new field with default value
|
||||
|
||||
Old config (version 1):
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"max_tokens": 32768
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Migration to version 2:
|
||||
```go
|
||||
case 2:
|
||||
migrated := *cfg
|
||||
migrated.Version = 2
|
||||
|
||||
// Add new field with default value if not set
|
||||
if migrated.Agents.Defaults.NewFeatureEnabled == false {
|
||||
// Use default value
|
||||
}
|
||||
|
||||
return &migrated, nil
|
||||
```
|
||||
|
||||
New config (version 2):
|
||||
Old config (version 2):
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"max_tokens": 32768,
|
||||
"new_feature_enabled": false
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-5.4",
|
||||
"model": "openai/gpt-5.4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Migration to version 3:
|
||||
```go
|
||||
func (c *configV2) Migrate() (*Config, error) {
|
||||
migrated := &c.Config
|
||||
migrated.Version = 3
|
||||
|
||||
// Add new field with default value if not set
|
||||
// ...
|
||||
|
||||
return migrated, nil
|
||||
}
|
||||
```
|
||||
|
||||
New config (version 3):
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt-5.4",
|
||||
"model": "openai/gpt-5.4",
|
||||
"new_option": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Config Not Upgrading
|
||||
- Check that `CurrentConfigVersion` is incremented
|
||||
- Verify migration logic in `applyMigration()` handles the target version
|
||||
- Ensure `migrateConfig()` is called in `LoadConfig()`
|
||||
- Check that `CurrentVersion` is incremented
|
||||
- Verify migration logic handles the target version
|
||||
- Ensure `Migrate()` is called in `LoadConfig()`
|
||||
|
||||
### Migration Errors
|
||||
- Check error messages for specific migration failures
|
||||
@@ -227,4 +225,5 @@ New config (version 2):
|
||||
- Ensure all fields are copied during migration
|
||||
- Check that the migration doesn't overwrite values with defaults unnecessarily
|
||||
- Review the conversion logic in the loader functions
|
||||
- Check the auto-backup files (e.g., `config.json.20260330.bak`) to recover original data
|
||||
|
||||
|
||||
Reference in New Issue
Block a user