mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor Config to add Version and migratable
This commit is contained in:
@@ -22,7 +22,8 @@ Add this to `config.json`:
|
||||
"enabled": true,
|
||||
"text": "Thinking..."
|
||||
},
|
||||
"reasoning_channel_id": ""
|
||||
"reasoning_channel_id": "",
|
||||
"message_format": "richtext"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,10 +43,12 @@ Add this to `config.json`:
|
||||
| group_trigger | object | No | Group trigger strategy (`mention_only` / `prefixes`) |
|
||||
| placeholder | object | No | Placeholder message config |
|
||||
| reasoning_channel_id | string | No | Target channel for reasoning output |
|
||||
| message_format | string | No | Output format: `"richtext"` (default) renders markdown as HTML; `"plain"` sends plain text only |
|
||||
|
||||
## 3. Currently Supported
|
||||
|
||||
- Text message send/receive
|
||||
- Text message send/receive with markdown rendering (bold, italic, headers, code blocks, etc.)
|
||||
- Configurable message format (`richtext` / `plain`)
|
||||
- Incoming image/audio/video/file download (MediaStore first, local path fallback)
|
||||
- Incoming audio normalization into existing transcription flow (`[audio: ...]`)
|
||||
- Outgoing image/audio/video/file upload and send
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# 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
|
||||
|
||||
## 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. The version number is updated automatically
|
||||
5. 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
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"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 CurrentConfigVersion = 2 // Increment this
|
||||
```
|
||||
|
||||
### Step 3: Add a Loader Function
|
||||
|
||||
```go
|
||||
// loadConfigV2 loads a version 2 config
|
||||
func loadConfigV2(data []byte) (*Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Parse to ConfigV2 struct
|
||||
var v2 ConfigV2
|
||||
if err := json.Unmarshal(data, &v2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to current Config
|
||||
cfg.Version = v2.Version
|
||||
cfg.Agents = v2.Agents
|
||||
// ... map other fields
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 = loadConfigV2(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 TestMigrateV1ToV2(t *testing.T) {
|
||||
// Create a version 1 config
|
||||
v1Config := Config{
|
||||
Version: 1,
|
||||
// ... set up test data
|
||||
}
|
||||
|
||||
// Apply migration
|
||||
migrated, err := applyMigration(&v1Config, 1, 2)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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. **Test Thoroughly**: Test with real user config files
|
||||
7. **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):
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"max_tokens": 32768,
|
||||
"new_feature_enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Config Not Upgrading
|
||||
- Check that `CurrentConfigVersion` is incremented
|
||||
- Verify migration logic in `applyMigration()` handles the target version
|
||||
- Ensure `migrateConfig()` 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
|
||||
|
||||
Reference in New Issue
Block a user