mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
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
This commit is contained in:
@@ -136,10 +136,15 @@ func loadPicoclawGatewaySettings() (picoclawGatewaySettings, error) {
|
|||||||
port = defaultPicoclawGatewayPort
|
port = defaultPicoclawGatewayPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
picoSettings := picoclawPicoSettingsV3{}
|
||||||
|
if pico, ok := cfg.Channels["pico"]; ok {
|
||||||
|
picoSettings = pico.Settings
|
||||||
|
}
|
||||||
|
|
||||||
settings := picoclawGatewaySettings{
|
settings := picoclawGatewaySettings{
|
||||||
GatewayURL: fmt.Sprintf("ws://%s:%d%s", host, port, picoclawGatewayPath),
|
GatewayURL: fmt.Sprintf("ws://%s:%d%s", host, port, picoclawGatewayPath),
|
||||||
Token: doc.resolvedGatewayToken(),
|
Token: doc.resolvedGatewayToken(),
|
||||||
AllowTokenQuery: cfg.Channels.Pico.AllowTokenQuery,
|
AllowTokenQuery: picoSettings.AllowTokenQuery,
|
||||||
}
|
}
|
||||||
settings.TargetModelName = resolvePicoclawTargetModelName(cfg)
|
settings.TargetModelName = resolvePicoclawTargetModelName(cfg)
|
||||||
|
|
||||||
@@ -148,11 +153,11 @@ func loadPicoclawGatewaySettings() (picoclawGatewaySettings, error) {
|
|||||||
settings.ModelName = settings.TargetModelName
|
settings.ModelName = settings.TargetModelName
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Channels.Pico.PingInterval > 0 {
|
if picoSettings.PingInterval > 0 {
|
||||||
settings.PingIntervalMs = cfg.Channels.Pico.PingInterval * 1000
|
settings.PingIntervalMs = picoSettings.PingInterval * 1000
|
||||||
}
|
}
|
||||||
if cfg.Channels.Pico.ReadTimeout > 0 {
|
if picoSettings.ReadTimeout > 0 {
|
||||||
settings.ReadTimeoutMs = cfg.Channels.Pico.ReadTimeout * 1000
|
settings.ReadTimeoutMs = picoSettings.ReadTimeout * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
picoclawPIDFileName = ".picoclaw.pid"
|
|
||||||
picoclawGatewayTokenPrefix = "pico-"
|
|
||||||
)
|
|
||||||
|
|
||||||
type picoclawConfigFile struct {
|
type picoclawConfigFile struct {
|
||||||
Agents struct {
|
Agents struct {
|
||||||
Defaults struct {
|
Defaults struct {
|
||||||
@@ -36,15 +31,22 @@ type picoclawConfigFile struct {
|
|||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
APIKeys []string `json:"api_keys,omitempty"`
|
APIKeys []string `json:"api_keys,omitempty"`
|
||||||
} `json:"model_list"`
|
} `json:"model_list"`
|
||||||
Channels struct {
|
Channels map[string]picoclawChannelEntry `json:"channel_list"`
|
||||||
Pico struct {
|
}
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Token string `json:"token"`
|
type picoclawChannelEntry struct {
|
||||||
AllowTokenQuery bool `json:"allow_token_query"`
|
Type string `json:"type"`
|
||||||
PingInterval int `json:"ping_interval"`
|
Enabled bool `json:"enabled"`
|
||||||
ReadTimeout int `json:"read_timeout"`
|
Settings picoclawPicoSettingsV3 `json:"settings"`
|
||||||
} `json:"pico"`
|
}
|
||||||
} `json:"channels"`
|
|
||||||
|
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 {
|
type picoclawConfigDocument struct {
|
||||||
@@ -154,88 +156,45 @@ func (d *picoclawConfigDocument) saveSecurity() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type picoclawSecurityConfig struct {
|
type picoclawSecurityConfig struct {
|
||||||
ModelList map[string]picoclawModelSecurityEntry `yaml:"model_list,omitempty"`
|
ModelList map[string]picoclawModelSecurityEntry `yaml:"model_list,omitempty"`
|
||||||
Channels picoclawSecurityChannels `yaml:"channels,omitempty"`
|
ChannelList map[string]picoclawChannelSecurityEntry `yaml:"channel_list,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type picoclawModelSecurityEntry struct {
|
type picoclawModelSecurityEntry struct {
|
||||||
APIKeys []string `yaml:"api_keys,omitempty"`
|
APIKeys []string `yaml:"api_keys,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type picoclawSecurityChannels struct {
|
type picoclawChannelSecurityEntry struct {
|
||||||
Pico *picoclawPicoSecurity `yaml:"pico,omitempty"`
|
Token string `yaml:"token,omitempty"`
|
||||||
|
Settings *picoclawChannelSecuritySettings `yaml:"settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type picoclawPicoSecurity struct {
|
type picoclawChannelSecuritySettings struct {
|
||||||
Token string `yaml:"token,omitempty"`
|
Token string `yaml:"token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type picoclawPIDFile struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *picoclawConfigDocument) resolvedPicoToken() string {
|
func (d *picoclawConfigDocument) resolvedPicoToken() string {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if d.security.Channels.Pico != nil {
|
// Check security config (channel_list.pico with nested settings)
|
||||||
if token := d.security.Channels.Pico.Token; token != "" {
|
if entry, ok := d.security.ChannelList["pico"]; ok {
|
||||||
return token
|
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 {
|
func (d *picoclawConfigDocument) resolvedGatewayToken() string {
|
||||||
baseToken := strings.TrimSpace(d.resolvedPicoToken())
|
return 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPicoclawSecurityConfig(securityPath string) (picoclawSecurityConfig, error) {
|
func loadPicoclawSecurityConfig(securityPath string) (picoclawSecurityConfig, error) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
picoclawBinaryPath = "/usr/bin/picoclaw"
|
picoclawBinaryPath = "/usr/bin/picoclaw"
|
||||||
picoclawCacheDir = "/root/.picoclaw-cache"
|
picoclawCacheDir = "/root/.picoclaw-cache"
|
||||||
picoclawDownloadURL = "https://cdn.sipeed.com/nanokvm/resources/picoclaw/v0.2.6/picoclaw_Linux_riscv64.tar.gz"
|
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.6/sha512.txt"
|
picoclawChecksumURL = "https://cdn.sipeed.com/nanokvm/resources/picoclaw/v0.2.8/sha512.txt"
|
||||||
etcInitPicoclawScript = "/etc/init.d/S96picoclaw"
|
etcInitPicoclawScript = "/etc/init.d/S96picoclaw"
|
||||||
kvmappPicoclawScript = "/kvmapp/system/init.d/S96picoclaw"
|
kvmappPicoclawScript = "/kvmapp/system/init.d/S96picoclaw"
|
||||||
picoclawStartTimeout = 15 * time.Second
|
picoclawStartTimeout = 15 * time.Second
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ var picoclawNanoKVMDefaults = []picoclawConfigDefault{
|
|||||||
{path: []string{"tools", "cron", "allow_command"}, value: true},
|
{path: []string{"tools", "cron", "allow_command"}, value: true},
|
||||||
{path: []string{"tools", "exec", "allow_remote"}, value: true},
|
{path: []string{"tools", "exec", "allow_remote"}, value: true},
|
||||||
{path: []string{"tools", "exec", "enable_deny_patterns"}, value: false},
|
{path: []string{"tools", "exec", "enable_deny_patterns"}, value: false},
|
||||||
{path: []string{"channels", "pico", "allow_token_query"}, value: false},
|
{path: []string{"channel_list", "pico", "type"}, value: "pico"},
|
||||||
{path: []string{"channels", "pico", "ping_interval"}, value: defaultPicoclawPingSec},
|
{path: []string{"channel_list", "pico", "settings", "allow_token_query"}, value: false},
|
||||||
{path: []string{"channels", "pico", "read_timeout"}, value: defaultPicoclawReadSec},
|
{path: []string{"channel_list", "pico", "settings", "ping_interval"}, value: defaultPicoclawPingSec},
|
||||||
{path: []string{"channels", "pico", "write_timeout"}, value: defaultPicoclawWriteSec},
|
{path: []string{"channel_list", "pico", "settings", "read_timeout"}, value: defaultPicoclawReadSec},
|
||||||
{path: []string{"channels", "pico", "max_connections"}, value: defaultPicoclawMaxConns},
|
{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},
|
{path: []string{"tools", "mcp", "enabled"}, value: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +73,10 @@ func ensurePicoclawStartupDefaults() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ensurePicoclawPicoChannelEnabled(doc *picoclawConfigDocument) 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +102,20 @@ func applyPicoclawStartupDefaults(editor *picoclawConfigEditor) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forceEnablePicoclawPicoChannel(doc *picoclawConfigDocument, editor *picoclawConfigEditor) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.setValue(true, "channels", "pico", "enabled")
|
editor.setValue(true, "channel_list", "pico", "enabled")
|
||||||
doc.config.Channels.Pico.Enabled = true
|
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) {
|
func defaultPicoclawMCPServer() (map[string]any, error) {
|
||||||
@@ -197,14 +209,19 @@ func ensurePicoclawPicoToken(doc *picoclawConfigDocument) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.security.Channels.Pico == nil {
|
if doc.security.ChannelList == nil {
|
||||||
doc.security.Channels.Pico = &picoclawPicoSecurity{}
|
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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.security.Channels.Pico.Token = token
|
entry.Settings.Token = token
|
||||||
|
doc.security.ChannelList["pico"] = entry
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,14 +230,23 @@ func picoSecurityToken(doc *picoclawConfigDocument) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.security.Channels.Pico != nil {
|
// Check security channel_list.pico.settings.token
|
||||||
if token := strings.TrimSpace(doc.security.Channels.Pico.Token); 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
|
return token, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if token := strings.TrimSpace(doc.config.Channels.Pico.Token); token != "" {
|
// Fall back to config channel_list.pico.settings.token
|
||||||
return token, nil
|
if pico, ok := doc.config.Channels["pico"]; ok {
|
||||||
|
if token := strings.TrimSpace(pico.Settings.Token); token != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return generatePicoclawToken()
|
return generatePicoclawToken()
|
||||||
|
|||||||
Reference in New Issue
Block a user