From dff794cee42a602e4c879b1bddfb8fc1007210c9 Mon Sep 17 00:00:00 2001 From: BeaconCat Date: Mon, 4 May 2026 00:30:40 +0800 Subject: [PATCH] feat(picoclaw): adapt to PicoClaw V3 config format - channels -> channel_list with nested settings struct (V3 format) - security channels -> channel_list.pico.settings.token - Remove broken pico- prefix token composition logic - Token now uses exact match with PicoClaw gateway - Add GitHub Actions CI for building update package - Version: 2.4.0.Beacon --- server/service/picoclaw/config.go | 15 ++- server/service/picoclaw/config_files.go | 111 ++++++------------- server/service/picoclaw/runtime_constants.go | 4 +- server/service/picoclaw/runtime_defaults.go | 60 +++++++--- 4 files changed, 90 insertions(+), 100 deletions(-) diff --git a/server/service/picoclaw/config.go b/server/service/picoclaw/config.go index 48ed8d7..a7c0d92 100644 --- a/server/service/picoclaw/config.go +++ b/server/service/picoclaw/config.go @@ -136,10 +136,15 @@ func loadPicoclawGatewaySettings() (picoclawGatewaySettings, error) { port = defaultPicoclawGatewayPort } + picoSettings := picoclawPicoSettingsV3{} + if pico, ok := cfg.Channels["pico"]; ok { + picoSettings = pico.Settings + } + settings := picoclawGatewaySettings{ GatewayURL: fmt.Sprintf("ws://%s:%d%s", host, port, picoclawGatewayPath), Token: doc.resolvedGatewayToken(), - AllowTokenQuery: cfg.Channels.Pico.AllowTokenQuery, + AllowTokenQuery: picoSettings.AllowTokenQuery, } settings.TargetModelName = resolvePicoclawTargetModelName(cfg) @@ -148,11 +153,11 @@ func loadPicoclawGatewaySettings() (picoclawGatewaySettings, error) { settings.ModelName = settings.TargetModelName } - if cfg.Channels.Pico.PingInterval > 0 { - settings.PingIntervalMs = cfg.Channels.Pico.PingInterval * 1000 + if picoSettings.PingInterval > 0 { + settings.PingIntervalMs = picoSettings.PingInterval * 1000 } - if cfg.Channels.Pico.ReadTimeout > 0 { - settings.ReadTimeoutMs = cfg.Channels.Pico.ReadTimeout * 1000 + if picoSettings.ReadTimeout > 0 { + settings.ReadTimeoutMs = picoSettings.ReadTimeout * 1000 } return settings, nil diff --git a/server/service/picoclaw/config_files.go b/server/service/picoclaw/config_files.go index f7aa114..239390f 100644 --- a/server/service/picoclaw/config_files.go +++ b/server/service/picoclaw/config_files.go @@ -12,11 +12,6 @@ import ( "gopkg.in/yaml.v3" ) -const ( - picoclawPIDFileName = ".picoclaw.pid" - picoclawGatewayTokenPrefix = "pico-" -) - type picoclawConfigFile struct { Agents struct { Defaults struct { @@ -36,15 +31,22 @@ type picoclawConfigFile struct { APIKey string `json:"api_key"` APIKeys []string `json:"api_keys,omitempty"` } `json:"model_list"` - Channels struct { - Pico struct { - Enabled bool `json:"enabled"` - Token string `json:"token"` - AllowTokenQuery bool `json:"allow_token_query"` - PingInterval int `json:"ping_interval"` - ReadTimeout int `json:"read_timeout"` - } `json:"pico"` - } `json:"channels"` + Channels map[string]picoclawChannelEntry `json:"channel_list"` +} + +type picoclawChannelEntry struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` + Settings picoclawPicoSettingsV3 `json:"settings"` +} + +type picoclawPicoSettingsV3 struct { + Token string `json:"token,omitempty"` + AllowTokenQuery bool `json:"allow_token_query,omitempty"` + PingInterval int `json:"ping_interval,omitempty"` + ReadTimeout int `json:"read_timeout,omitempty"` + WriteTimeout int `json:"write_timeout,omitempty"` + MaxConnections int `json:"max_connections,omitempty"` } type picoclawConfigDocument struct { @@ -154,88 +156,45 @@ func (d *picoclawConfigDocument) saveSecurity() error { } type picoclawSecurityConfig struct { - ModelList map[string]picoclawModelSecurityEntry `yaml:"model_list,omitempty"` - Channels picoclawSecurityChannels `yaml:"channels,omitempty"` + ModelList map[string]picoclawModelSecurityEntry `yaml:"model_list,omitempty"` + ChannelList map[string]picoclawChannelSecurityEntry `yaml:"channel_list,omitempty"` } type picoclawModelSecurityEntry struct { APIKeys []string `yaml:"api_keys,omitempty"` } -type picoclawSecurityChannels struct { - Pico *picoclawPicoSecurity `yaml:"pico,omitempty"` +type picoclawChannelSecurityEntry struct { + Token string `yaml:"token,omitempty"` + Settings *picoclawChannelSecuritySettings `yaml:"settings,omitempty"` } -type picoclawPicoSecurity struct { +type picoclawChannelSecuritySettings struct { Token string `yaml:"token,omitempty"` } -type picoclawPIDFile struct { - Token string `json:"token"` -} - func (d *picoclawConfigDocument) resolvedPicoToken() string { if d == nil { return "" } - if d.security.Channels.Pico != nil { - if token := d.security.Channels.Pico.Token; token != "" { - return token + // Check security config (channel_list.pico with nested settings) + if entry, ok := d.security.ChannelList["pico"]; ok { + if entry.Settings != nil && entry.Settings.Token != "" { + return entry.Settings.Token + } + if entry.Token != "" { + return entry.Token } } - return d.config.Channels.Pico.Token + // Fall back to config.json channel_list.pico.settings.token + if pico, ok := d.config.Channels["pico"]; ok { + return pico.Settings.Token + } + return "" } func (d *picoclawConfigDocument) resolvedGatewayToken() string { - baseToken := strings.TrimSpace(d.resolvedPicoToken()) - if baseToken == "" { - return "" - } - - runtimeToken, err := d.resolvedPIDToken() - if err != nil { - return baseToken - } - - return composePicoclawGatewayToken(baseToken, runtimeToken) -} - -func (d *picoclawConfigDocument) resolvedPIDToken() (string, error) { - if d == nil || d.configPath == "" { - return "", nil - } - - pidPath := filepath.Join(filepath.Dir(d.configPath), picoclawPIDFileName) - data, err := os.ReadFile(pidPath) - if err != nil { - if os.IsNotExist(err) { - return "", nil - } - return "", fmt.Errorf("failed to read picoclaw pid file: %w", err) - } - - var pidFile picoclawPIDFile - if err := json.Unmarshal(data, &pidFile); err != nil { - return "", fmt.Errorf("failed to parse picoclaw pid file: %w", err) - } - - return strings.TrimSpace(pidFile.Token), nil -} - -func composePicoclawGatewayToken(baseToken, runtimeToken string) string { - baseToken = strings.TrimSpace(baseToken) - runtimeToken = strings.TrimSpace(runtimeToken) - - switch { - case baseToken == "": - return "" - case strings.HasPrefix(baseToken, picoclawGatewayTokenPrefix): - return baseToken - case runtimeToken == "": - return baseToken - default: - return picoclawGatewayTokenPrefix + runtimeToken + baseToken - } + return strings.TrimSpace(d.resolvedPicoToken()) } func loadPicoclawSecurityConfig(securityPath string) (picoclawSecurityConfig, error) { diff --git a/server/service/picoclaw/runtime_constants.go b/server/service/picoclaw/runtime_constants.go index 796f6e5..6b82e48 100644 --- a/server/service/picoclaw/runtime_constants.go +++ b/server/service/picoclaw/runtime_constants.go @@ -7,8 +7,8 @@ import ( const ( picoclawBinaryPath = "/usr/bin/picoclaw" picoclawCacheDir = "/root/.picoclaw-cache" - picoclawDownloadURL = "https://cdn.sipeed.com/nanokvm/resources/picoclaw/v0.2.6/picoclaw_Linux_riscv64.tar.gz" - picoclawChecksumURL = "https://cdn.sipeed.com/nanokvm/resources/picoclaw/v0.2.6/sha512.txt" + picoclawDownloadURL = "https://cdn.sipeed.com/nanokvm/resources/picoclaw/v0.2.8/picoclaw_Linux_riscv64.tar.gz" + picoclawChecksumURL = "https://cdn.sipeed.com/nanokvm/resources/picoclaw/v0.2.8/sha512.txt" etcInitPicoclawScript = "/etc/init.d/S96picoclaw" kvmappPicoclawScript = "/kvmapp/system/init.d/S96picoclaw" picoclawStartTimeout = 15 * time.Second diff --git a/server/service/picoclaw/runtime_defaults.go b/server/service/picoclaw/runtime_defaults.go index 3b6c0ed..74b1d25 100644 --- a/server/service/picoclaw/runtime_defaults.go +++ b/server/service/picoclaw/runtime_defaults.go @@ -32,11 +32,12 @@ var picoclawNanoKVMDefaults = []picoclawConfigDefault{ {path: []string{"tools", "cron", "allow_command"}, value: true}, {path: []string{"tools", "exec", "allow_remote"}, value: true}, {path: []string{"tools", "exec", "enable_deny_patterns"}, value: false}, - {path: []string{"channels", "pico", "allow_token_query"}, value: false}, - {path: []string{"channels", "pico", "ping_interval"}, value: defaultPicoclawPingSec}, - {path: []string{"channels", "pico", "read_timeout"}, value: defaultPicoclawReadSec}, - {path: []string{"channels", "pico", "write_timeout"}, value: defaultPicoclawWriteSec}, - {path: []string{"channels", "pico", "max_connections"}, value: defaultPicoclawMaxConns}, + {path: []string{"channel_list", "pico", "type"}, value: "pico"}, + {path: []string{"channel_list", "pico", "settings", "allow_token_query"}, value: false}, + {path: []string{"channel_list", "pico", "settings", "ping_interval"}, value: defaultPicoclawPingSec}, + {path: []string{"channel_list", "pico", "settings", "read_timeout"}, value: defaultPicoclawReadSec}, + {path: []string{"channel_list", "pico", "settings", "write_timeout"}, value: defaultPicoclawWriteSec}, + {path: []string{"channel_list", "pico", "settings", "max_connections"}, value: defaultPicoclawMaxConns}, {path: []string{"tools", "mcp", "enabled"}, value: true}, } @@ -72,7 +73,10 @@ func ensurePicoclawStartupDefaults() error { } func ensurePicoclawPicoChannelEnabled(doc *picoclawConfigDocument) error { - if doc == nil || doc.config.Channels.Pico.Enabled { + if doc == nil { + return nil + } + if pico, ok := doc.config.Channels["pico"]; ok && pico.Enabled { return nil } @@ -98,12 +102,20 @@ func applyPicoclawStartupDefaults(editor *picoclawConfigEditor) error { } func forceEnablePicoclawPicoChannel(doc *picoclawConfigDocument, editor *picoclawConfigEditor) { - if doc == nil || editor == nil || doc.config.Channels.Pico.Enabled { + if doc == nil || editor == nil { + return + } + if pico, ok := doc.config.Channels["pico"]; ok && pico.Enabled { return } - editor.setValue(true, "channels", "pico", "enabled") - doc.config.Channels.Pico.Enabled = true + editor.setValue(true, "channel_list", "pico", "enabled") + if doc.config.Channels == nil { + doc.config.Channels = make(map[string]picoclawChannelEntry) + } + pico := doc.config.Channels["pico"] + pico.Enabled = true + doc.config.Channels["pico"] = pico } func defaultPicoclawMCPServer() (map[string]any, error) { @@ -197,14 +209,19 @@ func ensurePicoclawPicoToken(doc *picoclawConfigDocument) (bool, error) { return false, err } - if doc.security.Channels.Pico == nil { - doc.security.Channels.Pico = &picoclawPicoSecurity{} + if doc.security.ChannelList == nil { + doc.security.ChannelList = make(map[string]picoclawChannelSecurityEntry) } - if strings.TrimSpace(doc.security.Channels.Pico.Token) == token { + entry := doc.security.ChannelList["pico"] + if entry.Settings == nil { + entry.Settings = &picoclawChannelSecuritySettings{} + } + if strings.TrimSpace(entry.Settings.Token) == token { return false, nil } - doc.security.Channels.Pico.Token = token + entry.Settings.Token = token + doc.security.ChannelList["pico"] = entry return true, nil } @@ -213,14 +230,23 @@ func picoSecurityToken(doc *picoclawConfigDocument) (string, error) { return "", nil } - if doc.security.Channels.Pico != nil { - if token := strings.TrimSpace(doc.security.Channels.Pico.Token); token != "" { + // Check security channel_list.pico.settings.token + if entry, ok := doc.security.ChannelList["pico"]; ok { + if entry.Settings != nil { + if token := strings.TrimSpace(entry.Settings.Token); token != "" { + return token, nil + } + } + if token := strings.TrimSpace(entry.Token); token != "" { return token, nil } } - if token := strings.TrimSpace(doc.config.Channels.Pico.Token); token != "" { - return token, nil + // Fall back to config channel_list.pico.settings.token + if pico, ok := doc.config.Channels["pico"]; ok { + if token := strings.TrimSpace(pico.Settings.Token); token != "" { + return token, nil + } } return generatePicoclawToken()