mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
230 lines
6.1 KiB
Markdown
230 lines
6.1 KiB
Markdown
# Config Schema Versioning Guide
|
|
|
|
## Overview
|
|
|
|
PicoClaw uses a schema versioning system for `config.json` to ensure smooth upgrades as the configuration format evolves.
|
|
|
|
## Version History
|
|
|
|
### Version 1
|
|
- **Introduction**: Initial version with version field support
|
|
- **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.)
|
|
3. If the loaded version is less than the latest, migrations are applied incrementally
|
|
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`: Previous version (will be auto-migrated to V2 on load)
|
|
- `2`: Current version
|
|
|
|
```json
|
|
{
|
|
"version": 2,
|
|
"agents": {...},
|
|
...
|
|
}
|
|
```
|
|
|
|
## Adding a New Migration
|
|
|
|
When making breaking changes to the config schema:
|
|
|
|
### Step 1: Define the New Version Struct
|
|
|
|
Create a new struct for the new version if the structure changes significantly:
|
|
|
|
```go
|
|
// ConfigV2 represents version 2 config structure
|
|
type ConfigV2 struct {
|
|
Version int `json:"version"`
|
|
Agents AgentsConfig `json:"agents"`
|
|
// ... other fields with new structure
|
|
}
|
|
```
|
|
|
|
### Step 2: Update Current Config Version
|
|
|
|
```go
|
|
const CurrentVersion = 2 // Increment this
|
|
```
|
|
|
|
### Step 3: Add a Loader Function
|
|
|
|
```go
|
|
// loadConfigV3 loads a version 3 config
|
|
func loadConfigV3(data []byte) (*Config, error) {
|
|
cfg := DefaultConfig()
|
|
|
|
// Parse to ConfigV3 struct
|
|
var v3 ConfigV3
|
|
if err := json.Unmarshal(data, &v3); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to current Config
|
|
cfg.Version = v3.Version
|
|
cfg.Agents = v3.Agents
|
|
// ... map other fields
|
|
|
|
return cfg, nil
|
|
}
|
|
```
|
|
|
|
### Step 4: Add Migration Logic
|
|
|
|
```go
|
|
func (c *configV2) Migrate() (*Config, error) {
|
|
// Apply V2→V3 structural changes here
|
|
migrated := &c.Config
|
|
migrated.Version = 3
|
|
// Apply structural changes
|
|
return migrated, nil
|
|
}
|
|
```
|
|
|
|
### Step 5: Update LoadConfig Switch
|
|
|
|
```go
|
|
func LoadConfig(path string) (*Config, error) {
|
|
// ... read file ...
|
|
|
|
switch versionInfo.Version {
|
|
case 0:
|
|
cfg, err = loadConfigV0(data)
|
|
case 1:
|
|
cfg, err = loadConfigV1(data)
|
|
case 2:
|
|
cfg, err = loadConfig(data)
|
|
case 3:
|
|
cfg, err = loadConfigV3(data)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version)
|
|
}
|
|
|
|
// ... migrate and validate ...
|
|
}
|
|
```
|
|
|
|
### Step 6: Test Your Migration
|
|
|
|
Create a test in `config_migration_test.go`:
|
|
|
|
```go
|
|
func TestMigrateV2ToV3(t *testing.T) {
|
|
// Create a version 2 config
|
|
v2Config := Config{
|
|
Version: 2,
|
|
// ... set up test data
|
|
}
|
|
|
|
// Apply migration
|
|
migrated, err := v2Config.Migrate()
|
|
if err != nil {
|
|
t.Fatalf("Migration failed: %v", err)
|
|
}
|
|
|
|
// Verify version is updated
|
|
if migrated.Version != 3 {
|
|
t.Errorf("Expected version 3, got %d", migrated.Version)
|
|
}
|
|
|
|
// Verify data is preserved/transformed correctly
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## Migration Best Practices
|
|
|
|
1. **Version-Specific Structs**: Define a separate struct for each version that has structural changes
|
|
2. **Backward Compatibility**: Ensure old configs can still be loaded with their specific structs
|
|
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. **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 2):
|
|
```json
|
|
{
|
|
"version": 2,
|
|
"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 `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
|
|
- Review migration logic for edge cases
|
|
- Ensure all required fields are properly initialized
|
|
- Verify the loader function for the source version
|
|
|
|
### Data Loss After Migration
|
|
- 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
|
|
|