From cf9e0496f7ec303c638f78b84dc03b59f3af5ecc Mon Sep 17 00:00:00 2001 From: Cytown Date: Tue, 24 Mar 2026 10:26:11 +0800 Subject: [PATCH] fix launcher can't save model api_key issue (#1928) * fix launcher can't save model api_key issue * add backup for old data before migrate config and fix migrate to empty security issue --- pkg/config/config.go | 304 ++++++++++-------- pkg/config/config_old.go | 616 +++++++++++++++++++++--------------- pkg/config/config_test.go | 6 +- pkg/config/defaults.go | 5 +- pkg/config/security.go | 6 +- pkg/config/security_test.go | 4 +- pkg/fileutil/file.go | 8 + web/backend/api/models.go | 18 +- 8 files changed, 552 insertions(+), 415 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 051670437..f0d9aa580 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1350,11 +1350,14 @@ type MCPConfig struct { } func LoadConfig(path string) (*Config, error) { + logger.Debugf("loading config from %s", path) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { + logger.WarnF("config file not found, using default config", map[string]any{"path": path}) return DefaultConfig(), nil } + logger.Errorf("failed to read config file: %v", err) return nil, err } @@ -1366,6 +1369,7 @@ func LoadConfig(path string) (*Config, error) { return nil, fmt.Errorf("failed to detect config version: %w", e) } if len(data) <= 10 { + logger.Warn(fmt.Sprintf("content is [%s]", string(data))) return DefaultConfig().WithSecurity(&SecurityConfig{}), nil } @@ -1381,36 +1385,39 @@ func LoadConfig(path string) (*Config, error) { } cfg, e = v.Migrate() if e != nil { - logger.DebugF("config migrate fail", map[string]any{"from": versionInfo.Version, "to": CurrentVersion}) + logger.ErrorF("config migrate fail", map[string]any{"from": versionInfo.Version, "to": CurrentVersion}) return nil, e } - logger.DebugF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion}) - defer func() { + logger.InfoF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion}) + err = makeBackup(path) + if err != nil { + return nil, err + } + defer func(cfg *Config) { _ = SaveConfig(path, cfg) - }() + }(cfg) case CurrentVersion: // Current version cfg, err = loadConfig(data) if err != nil { return nil, err } + // Load security configuration + securityPath := securityPath(path) + sec, err := loadSecurityConfig(securityPath) + if err != nil { + return nil, fmt.Errorf("failed to load security config: %w", err) + } + + // Apply security references from .security.yml BEFORE resolveAPIKeys + // This resolves ref: references to actual values + if err := applySecurityConfig(cfg, sec); err != nil { + return nil, fmt.Errorf("failed to apply security config: %w", err) + } default: return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version) } - // Load security configuration - securityPath := securityPath(path) - sec, err := loadSecurityConfig(securityPath) - if err != nil { - return nil, fmt.Errorf("failed to load security config: %w", err) - } - - // Apply security references from .security.yml BEFORE resolveAPIKeys - // This resolves ref: references to actual values - if err := applySecurityConfig(cfg, sec); err != nil { - return nil, fmt.Errorf("failed to apply security config: %w", err) - } - if passphrase := credential.PassphraseProvider(); passphrase != "" { for _, m := range cfg.ModelList { for _, k := range m.apiKeys { @@ -1462,6 +1469,19 @@ func LoadConfig(path string) (*Config, error) { return cfg, nil } +func makeBackup(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil + } + // Create backup of the config file before migration + bakPath := path + ".bak" + if err := fileutil.CopyFile(path, bakPath, 0o600); err != nil { + logger.ErrorF("failed to create config backup", map[string]any{"error": err}) + return fmt.Errorf("failed to create config backup: %w", err) + } + return nil +} + func copyArray[T any](dst, src *[]T) { *dst = make([]T, len(*src)) copy(*dst, *src) @@ -1474,32 +1494,36 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error { return nil } - if sec.Web.Brave != nil && len(sec.Web.Brave.APIKeys) > 0 { - copyArray(&cfg.Tools.Web.Brave.apiKeys, &sec.Web.Brave.APIKeys) + if sec.Web != nil { + if sec.Web.Brave != nil && len(sec.Web.Brave.APIKeys) > 0 { + copyArray(&cfg.Tools.Web.Brave.apiKeys, &sec.Web.Brave.APIKeys) + } + + if sec.Web.Tavily != nil && len(sec.Web.Tavily.APIKeys) > 0 { + copyArray(&cfg.Tools.Web.Tavily.apiKeys, &sec.Web.Tavily.APIKeys) + } + + if sec.Web.Perplexity != nil && len(sec.Web.Perplexity.APIKeys) > 0 { + copyArray(&cfg.Tools.Web.Perplexity.apiKeys, &sec.Web.Perplexity.APIKeys) + } + + if sec.Web.GLMSearch != nil && sec.Web.GLMSearch.APIKey != "" { + cfg.Tools.Web.GLMSearch.apiKey = sec.Web.GLMSearch.APIKey + } + + if sec.Web.BaiduSearch != nil && sec.Web.BaiduSearch.APIKey != "" { + cfg.Tools.Web.BaiduSearch.apiKey = sec.Web.BaiduSearch.APIKey + } } - if sec.Web.Tavily != nil && len(sec.Web.Tavily.APIKeys) > 0 { - copyArray(&cfg.Tools.Web.Tavily.apiKeys, &sec.Web.Tavily.APIKeys) - } + if sec.Skills != nil { + if sec.Skills.Github != nil && sec.Skills.Github.Token != "" { + cfg.Tools.Skills.Github.token = sec.Skills.Github.Token + } - if sec.Web.Perplexity != nil && len(sec.Web.Perplexity.APIKeys) > 0 { - copyArray(&cfg.Tools.Web.Perplexity.apiKeys, &sec.Web.Perplexity.APIKeys) - } - - if sec.Web.GLMSearch != nil && sec.Web.GLMSearch.APIKey != "" { - cfg.Tools.Web.GLMSearch.apiKey = sec.Web.GLMSearch.APIKey - } - - if sec.Web.BaiduSearch != nil && sec.Web.BaiduSearch.APIKey != "" { - cfg.Tools.Web.BaiduSearch.apiKey = sec.Web.BaiduSearch.APIKey - } - - if sec.Skills.Github != nil && sec.Skills.Github.Token != "" { - cfg.Tools.Skills.Github.token = sec.Skills.Github.Token - } - - if sec.Skills.ClawHub != nil && sec.Skills.ClawHub.AuthToken != "" { - cfg.Tools.Skills.Registries.ClawHub.authToken = sec.Skills.ClawHub.AuthToken + if sec.Skills.ClawHub != nil && sec.Skills.ClawHub.AuthToken != "" { + cfg.Tools.Skills.Registries.ClawHub.authToken = sec.Skills.ClawHub.AuthToken + } } names := toNameIndex(cfg.ModelList) @@ -1521,126 +1545,128 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error { } } - // Handle Telegram token - if sec.Channels.Telegram != nil && sec.Channels.Telegram.Token != "" { - cfg.Channels.Telegram.token = sec.Channels.Telegram.Token - } + if sec.Channels != nil { + // Handle Telegram token + if sec.Channels.Telegram != nil && sec.Channels.Telegram.Token != "" { + cfg.Channels.Telegram.token = sec.Channels.Telegram.Token + } - // Handle Feishu credentials - if sec.Channels.Feishu != nil { - if sec.Channels.Feishu.AppSecret != "" { - cfg.Channels.Feishu.appSecret = sec.Channels.Feishu.AppSecret + // Handle Feishu credentials + if sec.Channels.Feishu != nil { + if sec.Channels.Feishu.AppSecret != "" { + cfg.Channels.Feishu.appSecret = sec.Channels.Feishu.AppSecret + } + if sec.Channels.Feishu.EncryptKey != "" { + cfg.Channels.Feishu.encryptKey = sec.Channels.Feishu.EncryptKey + } + if sec.Channels.Feishu.VerificationToken != "" { + cfg.Channels.Feishu.verificationToken = sec.Channels.Feishu.VerificationToken + } } - if sec.Channels.Feishu.EncryptKey != "" { - cfg.Channels.Feishu.encryptKey = sec.Channels.Feishu.EncryptKey - } - if sec.Channels.Feishu.VerificationToken != "" { - cfg.Channels.Feishu.verificationToken = sec.Channels.Feishu.VerificationToken - } - } - // Handle Discord token - if sec.Channels.Discord != nil && sec.Channels.Discord.Token != "" { - cfg.Channels.Discord.token = sec.Channels.Discord.Token - } + // Handle Discord token + if sec.Channels.Discord != nil && sec.Channels.Discord.Token != "" { + cfg.Channels.Discord.token = sec.Channels.Discord.Token + } - // Handle Weixin token - if sec.Channels.Weixin != nil && sec.Channels.Weixin.Token != "" { - cfg.Channels.Weixin.token = sec.Channels.Weixin.Token - } + // Handle Weixin token + if sec.Channels.Weixin != nil && sec.Channels.Weixin.Token != "" { + cfg.Channels.Weixin.token = sec.Channels.Weixin.Token + } - // Handle DingTalk client secret - if sec.Channels.DingTalk != nil && sec.Channels.DingTalk.ClientSecret != "" { - cfg.Channels.DingTalk.clientSecret = sec.Channels.DingTalk.ClientSecret - } + // Handle DingTalk client secret + if sec.Channels.DingTalk != nil && sec.Channels.DingTalk.ClientSecret != "" { + cfg.Channels.DingTalk.clientSecret = sec.Channels.DingTalk.ClientSecret + } - // Handle Slack tokens - if sec.Channels.Slack != nil { - if sec.Channels.Slack.BotToken != "" { - cfg.Channels.Slack.botToken = sec.Channels.Slack.BotToken + // Handle Slack tokens + if sec.Channels.Slack != nil { + if sec.Channels.Slack.BotToken != "" { + cfg.Channels.Slack.botToken = sec.Channels.Slack.BotToken + } + if sec.Channels.Slack.AppToken != "" { + cfg.Channels.Slack.appToken = sec.Channels.Slack.AppToken + } } - if sec.Channels.Slack.AppToken != "" { - cfg.Channels.Slack.appToken = sec.Channels.Slack.AppToken - } - } - // Handle Matrix access token - if sec.Channels.Matrix != nil && sec.Channels.Matrix.AccessToken != "" { - cfg.Channels.Matrix.accessToken = sec.Channels.Matrix.AccessToken - } + // Handle Matrix access token + if sec.Channels.Matrix != nil && sec.Channels.Matrix.AccessToken != "" { + cfg.Channels.Matrix.accessToken = sec.Channels.Matrix.AccessToken + } - // Handle LINE credentials - if sec.Channels.LINE != nil { - if sec.Channels.LINE.ChannelSecret != "" { - cfg.Channels.LINE.channelSecret = sec.Channels.LINE.ChannelSecret + // Handle LINE credentials + if sec.Channels.LINE != nil { + if sec.Channels.LINE.ChannelSecret != "" { + cfg.Channels.LINE.channelSecret = sec.Channels.LINE.ChannelSecret + } + if sec.Channels.LINE.ChannelAccessToken != "" { + cfg.Channels.LINE.channelAccessToken = sec.Channels.LINE.ChannelAccessToken + } } - if sec.Channels.LINE.ChannelAccessToken != "" { - cfg.Channels.LINE.channelAccessToken = sec.Channels.LINE.ChannelAccessToken - } - } - // Handle OneBot access token - if sec.Channels.OneBot != nil && sec.Channels.OneBot.AccessToken != "" { - cfg.Channels.OneBot.accessToken = sec.Channels.OneBot.AccessToken - } + // Handle OneBot access token + if sec.Channels.OneBot != nil && sec.Channels.OneBot.AccessToken != "" { + cfg.Channels.OneBot.accessToken = sec.Channels.OneBot.AccessToken + } - // Handle WeCom token and encoding key - if sec.Channels.WeCom != nil { - if sec.Channels.WeCom.Token != "" { - cfg.Channels.WeCom.token = sec.Channels.WeCom.Token + // Handle WeCom token and encoding key + if sec.Channels.WeCom != nil { + if sec.Channels.WeCom.Token != "" { + cfg.Channels.WeCom.token = sec.Channels.WeCom.Token + } + if sec.Channels.WeCom.EncodingAESKey != "" { + cfg.Channels.WeCom.encodingAESKey = sec.Channels.WeCom.EncodingAESKey + } } - if sec.Channels.WeCom.EncodingAESKey != "" { - cfg.Channels.WeCom.encodingAESKey = sec.Channels.WeCom.EncodingAESKey - } - } - // Handle WeCom App credentials - if sec.Channels.WeComApp != nil { - if sec.Channels.WeComApp.CorpSecret != "" { - cfg.Channels.WeComApp.corpSecret = sec.Channels.WeComApp.CorpSecret + // Handle WeCom App credentials + if sec.Channels.WeComApp != nil { + if sec.Channels.WeComApp.CorpSecret != "" { + cfg.Channels.WeComApp.corpSecret = sec.Channels.WeComApp.CorpSecret + } + if sec.Channels.WeComApp.Token != "" { + cfg.Channels.WeComApp.token = sec.Channels.WeComApp.Token + } + if sec.Channels.WeComApp.EncodingAESKey != "" { + cfg.Channels.WeComApp.encodingAESKey = sec.Channels.WeComApp.EncodingAESKey + } } - if sec.Channels.WeComApp.Token != "" { - cfg.Channels.WeComApp.token = sec.Channels.WeComApp.Token - } - if sec.Channels.WeComApp.EncodingAESKey != "" { - cfg.Channels.WeComApp.encodingAESKey = sec.Channels.WeComApp.EncodingAESKey - } - } - // Handle WeCom AI Bot credentials - if sec.Channels.WeComAIBot != nil { - if sec.Channels.WeComAIBot.Token != "" { - cfg.Channels.WeComAIBot.token = sec.Channels.WeComAIBot.Token + // Handle WeCom AI Bot credentials + if sec.Channels.WeComAIBot != nil { + if sec.Channels.WeComAIBot.Token != "" { + cfg.Channels.WeComAIBot.token = sec.Channels.WeComAIBot.Token + } + if sec.Channels.WeComAIBot.EncodingAESKey != "" { + cfg.Channels.WeComAIBot.encodingAESKey = sec.Channels.WeComAIBot.EncodingAESKey + } + if sec.Channels.WeComAIBot.Secret != "" { + cfg.Channels.WeComAIBot.secret = sec.Channels.WeComAIBot.Secret + } } - if sec.Channels.WeComAIBot.EncodingAESKey != "" { - cfg.Channels.WeComAIBot.encodingAESKey = sec.Channels.WeComAIBot.EncodingAESKey - } - if sec.Channels.WeComAIBot.Secret != "" { - cfg.Channels.WeComAIBot.secret = sec.Channels.WeComAIBot.Secret - } - } - // Handle Pico channel token - if sec.Channels.Pico != nil && sec.Channels.Pico.Token != "" { - cfg.Channels.Pico.token = sec.Channels.Pico.Token - } + // Handle Pico channel token + if sec.Channels.Pico != nil && sec.Channels.Pico.Token != "" { + cfg.Channels.Pico.token = sec.Channels.Pico.Token + } - // Handle IRC passwords - if sec.Channels.IRC != nil { - if sec.Channels.IRC.Password != "" { - cfg.Channels.IRC.password = sec.Channels.IRC.Password + // Handle IRC passwords + if sec.Channels.IRC != nil { + if sec.Channels.IRC.Password != "" { + cfg.Channels.IRC.password = sec.Channels.IRC.Password + } + if sec.Channels.IRC.NickServPassword != "" { + cfg.Channels.IRC.nickServPassword = sec.Channels.IRC.NickServPassword + } + if sec.Channels.IRC.SASLPassword != "" { + cfg.Channels.IRC.saslPassword = sec.Channels.IRC.SASLPassword + } } - if sec.Channels.IRC.NickServPassword != "" { - cfg.Channels.IRC.nickServPassword = sec.Channels.IRC.NickServPassword - } - if sec.Channels.IRC.SASLPassword != "" { - cfg.Channels.IRC.saslPassword = sec.Channels.IRC.SASLPassword - } - } - // Handle QQ app secret - if sec.Channels.QQ != nil && sec.Channels.QQ.AppSecret != "" { - cfg.Channels.QQ.appSecret = sec.Channels.QQ.AppSecret + // Handle QQ app secret + if sec.Channels.QQ != nil && sec.Channels.QQ.AppSecret != "" { + cfg.Channels.QQ.appSecret = sec.Channels.QQ.AppSecret + } } cfg.security = sec diff --git a/pkg/config/config_old.go b/pkg/config/config_old.go index c7c7f0028..01909f5a9 100644 --- a/pkg/config/config_old.go +++ b/pkg/config/config_old.go @@ -5,7 +5,9 @@ package config -import "encoding/json" +import ( + "encoding/json" +) type agentDefaultsV0 struct { Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` @@ -139,21 +141,21 @@ func (v *channelsConfigV0) ToChannelsConfig() (ChannelsConfig, ChannelsSecurity) Pico: pico, IRC: irc, }, ChannelsSecurity{ - Telegram: &telegramSecurity, - Feishu: &feishuSecurity, - Discord: &discordSecurity, - QQ: &qqSecurity, - Weixin: &weixinSecurity, - DingTalk: &dingtalkSecurity, - Slack: &slackSecurity, - Matrix: &matrixSecurity, - LINE: &lineSecurity, - OneBot: &onebotSecurity, - WeCom: &wecomSecurity, - WeComApp: &wecomappSecurity, - WeComAIBot: &wecomaibotSecurity, - Pico: &picoSecurity, - IRC: &ircSecurity, + Telegram: telegramSecurity, + Feishu: feishuSecurity, + Discord: discordSecurity, + QQ: qqSecurity, + Weixin: weixinSecurity, + DingTalk: dingtalkSecurity, + Slack: slackSecurity, + Matrix: matrixSecurity, + LINE: lineSecurity, + OneBot: onebotSecurity, + WeCom: wecomSecurity, + WeComApp: wecomappSecurity, + WeComAIBot: wecomaibotSecurity, + Pico: picoSecurity, + IRC: ircSecurity, } } @@ -169,19 +171,23 @@ type qqConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"` } -func (v *qqConfigV0) ToQQConfig() (QQConfig, QQSecurity) { - return QQConfig{ - Enabled: v.Enabled, - AppID: v.AppID, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - MaxMessageLength: v.MaxMessageLength, - MaxBase64FileSizeMiB: v.MaxBase64FileSizeMiB, - SendMarkdown: v.SendMarkdown, - ReasoningChannelID: v.ReasoningChannelID, - }, QQSecurity{ +func (v *qqConfigV0) ToQQConfig() (QQConfig, *QQSecurity) { + var sec *QQSecurity + if v.AppSecret != "" { + sec = &QQSecurity{ AppSecret: v.AppSecret, } + } + return QQConfig{ + Enabled: v.Enabled, + AppID: v.AppID, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + MaxMessageLength: v.MaxMessageLength, + MaxBase64FileSizeMiB: v.MaxBase64FileSizeMiB, + SendMarkdown: v.SendMarkdown, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type telegramConfigV0 struct { @@ -197,21 +203,25 @@ type telegramConfigV0 struct { UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"` } -func (v *telegramConfigV0) ToTelegramConfig() (TelegramConfig, TelegramSecurity) { - return TelegramConfig{ - Enabled: v.Enabled, - token: v.Token, - BaseURL: v.BaseURL, - Proxy: v.Proxy, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Typing: v.Typing, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - UseMarkdownV2: v.UseMarkdownV2, - }, TelegramSecurity{ +func (v *telegramConfigV0) ToTelegramConfig() (TelegramConfig, *TelegramSecurity) { + var sec *TelegramSecurity + if v.Token != "" { + sec = &TelegramSecurity{ Token: v.Token, } + } + return TelegramConfig{ + Enabled: v.Enabled, + token: v.Token, + BaseURL: v.BaseURL, + Proxy: v.Proxy, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Typing: v.Typing, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + UseMarkdownV2: v.UseMarkdownV2, + }, sec } type feishuConfigV0 struct { @@ -228,20 +238,24 @@ type feishuConfigV0 struct { IsLark bool `json:"is_lark" env:"PICOCLAW_CHANNELS_FEISHU_IS_LARK"` } -func (v *feishuConfigV0) ToFeishuConfig() (FeishuConfig, FeishuSecurity) { - return FeishuConfig{ - Enabled: v.Enabled, - AppID: v.AppID, - appSecret: v.AppSecret, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - }, FeishuSecurity{ +func (v *feishuConfigV0) ToFeishuConfig() (FeishuConfig, *FeishuSecurity) { + var sec *FeishuSecurity + if v.AppSecret != "" || v.EncryptKey != "" || v.VerificationToken != "" { + sec = &FeishuSecurity{ AppSecret: v.AppSecret, EncryptKey: v.EncryptKey, VerificationToken: v.VerificationToken, } + } + return FeishuConfig{ + Enabled: v.Enabled, + AppID: v.AppID, + appSecret: v.AppSecret, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type discordConfigV0 struct { @@ -256,20 +270,24 @@ type discordConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"` } -func (v *discordConfigV0) ToDiscordConfig() (DiscordConfig, DiscordSecurity) { - return DiscordConfig{ - Enabled: v.Enabled, - token: v.Token, - Proxy: v.Proxy, - AllowFrom: v.AllowFrom, - MentionOnly: v.MentionOnly, - GroupTrigger: v.GroupTrigger, - Typing: v.Typing, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - }, DiscordSecurity{ +func (v *discordConfigV0) ToDiscordConfig() (DiscordConfig, *DiscordSecurity) { + var sec *DiscordSecurity + if v.Token != "" { + sec = &DiscordSecurity{ Token: v.Token, } + } + return DiscordConfig{ + Enabled: v.Enabled, + token: v.Token, + Proxy: v.Proxy, + AllowFrom: v.AllowFrom, + MentionOnly: v.MentionOnly, + GroupTrigger: v.GroupTrigger, + Typing: v.Typing, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type maixcamConfigV0 struct { @@ -299,17 +317,21 @@ type dingtalkConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"` } -func (v *dingtalkConfigV0) ToDingTalkConfig() (DingTalkConfig, DingTalkSecurity) { - return DingTalkConfig{ - Enabled: v.Enabled, - ClientID: v.ClientID, - clientSecret: v.ClientSecret, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - ReasoningChannelID: v.ReasoningChannelID, - }, DingTalkSecurity{ +func (v *dingtalkConfigV0) ToDingTalkConfig() (DingTalkConfig, *DingTalkSecurity) { + var sec *DingTalkSecurity + if v.ClientSecret != "" { + sec = &DingTalkSecurity{ ClientSecret: v.ClientSecret, } + } + return DingTalkConfig{ + Enabled: v.Enabled, + ClientID: v.ClientID, + clientSecret: v.ClientSecret, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type slackConfigV0 struct { @@ -323,20 +345,24 @@ type slackConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"` } -func (v *slackConfigV0) ToSlackConfig() (SlackConfig, SlackSecurity) { - return SlackConfig{ - Enabled: v.Enabled, - botToken: v.BotToken, - appToken: v.AppToken, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Typing: v.Typing, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - }, SlackSecurity{ +func (v *slackConfigV0) ToSlackConfig() (SlackConfig, *SlackSecurity) { + var sec *SlackSecurity + if v.BotToken != "" || v.AppToken != "" { + sec = &SlackSecurity{ BotToken: v.BotToken, AppToken: v.AppToken, } + } + return SlackConfig{ + Enabled: v.Enabled, + botToken: v.BotToken, + appToken: v.AppToken, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Typing: v.Typing, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type matrixConfigV0 struct { @@ -353,22 +379,26 @@ type matrixConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` } -func (v *matrixConfigV0) ToMatrixConfig() (MatrixConfig, MatrixSecurity) { - return MatrixConfig{ - Enabled: v.Enabled, - Homeserver: v.Homeserver, - UserID: v.UserID, - accessToken: v.AccessToken, - DeviceID: v.DeviceID, - JoinOnInvite: v.JoinOnInvite, - MessageFormat: v.MessageFormat, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - }, MatrixSecurity{ +func (v *matrixConfigV0) ToMatrixConfig() (MatrixConfig, *MatrixSecurity) { + var sec *MatrixSecurity + if v.AccessToken != "" { + sec = &MatrixSecurity{ AccessToken: v.AccessToken, } + } + return MatrixConfig{ + Enabled: v.Enabled, + Homeserver: v.Homeserver, + UserID: v.UserID, + accessToken: v.AccessToken, + DeviceID: v.DeviceID, + JoinOnInvite: v.JoinOnInvite, + MessageFormat: v.MessageFormat, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type lineConfigV0 struct { @@ -385,23 +415,27 @@ type lineConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"` } -func (v *lineConfigV0) ToLINEConfig() (LINEConfig, LINESecurity) { - return LINEConfig{ - Enabled: v.Enabled, - channelSecret: v.ChannelSecret, - channelAccessToken: v.ChannelAccessToken, - WebhookHost: v.WebhookHost, - WebhookPort: v.WebhookPort, - WebhookPath: v.WebhookPath, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Typing: v.Typing, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - }, LINESecurity{ +func (v *lineConfigV0) ToLINEConfig() (LINEConfig, *LINESecurity) { + var sec *LINESecurity + if v.ChannelSecret != "" || v.ChannelAccessToken != "" { + sec = &LINESecurity{ ChannelSecret: v.ChannelSecret, ChannelAccessToken: v.ChannelAccessToken, } + } + return LINEConfig{ + Enabled: v.Enabled, + channelSecret: v.ChannelSecret, + channelAccessToken: v.ChannelAccessToken, + WebhookHost: v.WebhookHost, + WebhookPort: v.WebhookPort, + WebhookPath: v.WebhookPath, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Typing: v.Typing, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type onebotConfigV0 struct { @@ -417,21 +451,25 @@ type onebotConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"` } -func (v *onebotConfigV0) ToOneBotConfig() (OneBotConfig, OneBotSecurity) { - return OneBotConfig{ - Enabled: v.Enabled, - WSUrl: v.WSUrl, - accessToken: v.AccessToken, - ReconnectInterval: v.ReconnectInterval, - GroupTriggerPrefix: v.GroupTriggerPrefix, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Typing: v.Typing, - Placeholder: v.Placeholder, - ReasoningChannelID: v.ReasoningChannelID, - }, OneBotSecurity{ +func (v *onebotConfigV0) ToOneBotConfig() (OneBotConfig, *OneBotSecurity) { + var sec *OneBotSecurity + if v.AccessToken != "" { + sec = &OneBotSecurity{ AccessToken: v.AccessToken, } + } + return OneBotConfig{ + Enabled: v.Enabled, + WSUrl: v.WSUrl, + accessToken: v.AccessToken, + ReconnectInterval: v.ReconnectInterval, + GroupTriggerPrefix: v.GroupTriggerPrefix, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Typing: v.Typing, + Placeholder: v.Placeholder, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type wecomConfigV0 struct { @@ -448,23 +486,27 @@ type wecomConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_REASONING_CHANNEL_ID"` } -func (v *wecomConfigV0) ToWeComConfig() (WeComConfig, WeComSecurity) { - return WeComConfig{ - Enabled: v.Enabled, - token: v.Token, - encodingAESKey: v.EncodingAESKey, - WebhookURL: v.WebhookURL, - WebhookHost: v.WebhookHost, - WebhookPort: v.WebhookPort, - WebhookPath: v.WebhookPath, - AllowFrom: v.AllowFrom, - ReplyTimeout: v.ReplyTimeout, - GroupTrigger: v.GroupTrigger, - ReasoningChannelID: v.ReasoningChannelID, - }, WeComSecurity{ +func (v *wecomConfigV0) ToWeComConfig() (WeComConfig, *WeComSecurity) { + var sec *WeComSecurity + if v.Token != "" || v.EncodingAESKey != "" { + sec = &WeComSecurity{ Token: v.Token, EncodingAESKey: v.EncodingAESKey, } + } + return WeComConfig{ + Enabled: v.Enabled, + token: v.Token, + encodingAESKey: v.EncodingAESKey, + WebhookURL: v.WebhookURL, + WebhookHost: v.WebhookHost, + WebhookPort: v.WebhookPort, + WebhookPath: v.WebhookPath, + AllowFrom: v.AllowFrom, + ReplyTimeout: v.ReplyTimeout, + GroupTrigger: v.GroupTrigger, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type weixinConfigV0 struct { @@ -477,18 +519,22 @@ type weixinConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"` } -func (v *weixinConfigV0) ToWeiXinConfig() (WeixinConfig, WeixinSecurity) { - return WeixinConfig{ - Enabled: v.Enabled, - token: v.Token, - BaseURL: v.BaseURL, - CDNBaseURL: v.CDNBaseURL, - Proxy: v.Proxy, - AllowFrom: v.AllowFrom, - ReasoningChannelID: v.ReasoningChannelID, - }, WeixinSecurity{ +func (v *weixinConfigV0) ToWeiXinConfig() (WeixinConfig, *WeixinSecurity) { + var sec *WeixinSecurity + if v.Token != "" { + sec = &WeixinSecurity{ Token: v.Token, } + } + return WeixinConfig{ + Enabled: v.Enabled, + token: v.Token, + BaseURL: v.BaseURL, + CDNBaseURL: v.CDNBaseURL, + Proxy: v.Proxy, + AllowFrom: v.AllowFrom, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type wecomappConfigV0 struct { @@ -507,26 +553,30 @@ type wecomappConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_APP_REASONING_CHANNEL_ID"` } -func (v *wecomappConfigV0) ToWeComAppConfig() (WeComAppConfig, WeComAppSecurity) { - return WeComAppConfig{ - Enabled: v.Enabled, - CorpID: v.CorpID, - corpSecret: v.CorpSecret, - AgentID: v.AgentID, - token: v.Token, - encodingAESKey: v.EncodingAESKey, - WebhookHost: v.WebhookHost, - WebhookPort: v.WebhookPort, - WebhookPath: v.WebhookPath, - AllowFrom: v.AllowFrom, - ReplyTimeout: v.ReplyTimeout, - GroupTrigger: v.GroupTrigger, - ReasoningChannelID: v.ReasoningChannelID, - }, WeComAppSecurity{ +func (v *wecomappConfigV0) ToWeComAppConfig() (WeComAppConfig, *WeComAppSecurity) { + var sec *WeComAppSecurity + if v.CorpSecret != "" || v.Token != "" || v.EncodingAESKey != "" { + sec = &WeComAppSecurity{ CorpSecret: v.CorpSecret, Token: v.Token, EncodingAESKey: v.EncodingAESKey, } + } + return WeComAppConfig{ + Enabled: v.Enabled, + CorpID: v.CorpID, + corpSecret: v.CorpSecret, + AgentID: v.AgentID, + token: v.Token, + encodingAESKey: v.EncodingAESKey, + WebhookHost: v.WebhookHost, + WebhookPort: v.WebhookPort, + WebhookPath: v.WebhookPath, + AllowFrom: v.AllowFrom, + ReplyTimeout: v.ReplyTimeout, + GroupTrigger: v.GroupTrigger, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type wecomaibotConfigV0 struct { @@ -542,20 +592,24 @@ type wecomaibotConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REASONING_CHANNEL_ID"` } -func (v *wecomaibotConfigV0) ToWeComAIBotConfig() (WeComAIBotConfig, WeComAIBotSecurity) { - return WeComAIBotConfig{ - Enabled: v.Enabled, - WebhookPath: v.WebhookPath, - AllowFrom: v.AllowFrom, - ReplyTimeout: v.ReplyTimeout, - MaxSteps: v.MaxSteps, - WelcomeMessage: v.WelcomeMessage, - ReasoningChannelID: v.ReasoningChannelID, - }, WeComAIBotSecurity{ +func (v *wecomaibotConfigV0) ToWeComAIBotConfig() (WeComAIBotConfig, *WeComAIBotSecurity) { + var sec *WeComAIBotSecurity + if v.Token != "" || v.Secret != "" || v.EncodingAESKey != "" { + sec = &WeComAIBotSecurity{ Token: v.Token, Secret: v.Secret, EncodingAESKey: v.EncodingAESKey, } + } + return WeComAIBotConfig{ + Enabled: v.Enabled, + WebhookPath: v.WebhookPath, + AllowFrom: v.AllowFrom, + ReplyTimeout: v.ReplyTimeout, + MaxSteps: v.MaxSteps, + WelcomeMessage: v.WelcomeMessage, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type picoConfigV0 struct { @@ -571,21 +625,25 @@ type picoConfigV0 struct { Placeholder PlaceholderConfig `json:"placeholder,omitempty"` } -func (v *picoConfigV0) ToPicoConfig() (PicoConfig, PicoSecurity) { - return PicoConfig{ - Enabled: v.Enabled, - token: v.Token, - AllowTokenQuery: v.AllowTokenQuery, - AllowOrigins: v.AllowOrigins, - PingInterval: v.PingInterval, - ReadTimeout: v.ReadTimeout, - WriteTimeout: v.WriteTimeout, - MaxConnections: v.MaxConnections, - AllowFrom: v.AllowFrom, - Placeholder: v.Placeholder, - }, PicoSecurity{ +func (v *picoConfigV0) ToPicoConfig() (PicoConfig, *PicoSecurity) { + var sec *PicoSecurity + if v.Token != "" { + sec = &PicoSecurity{ Token: v.Token, } + } + return PicoConfig{ + Enabled: v.Enabled, + token: v.Token, + AllowTokenQuery: v.AllowTokenQuery, + AllowOrigins: v.AllowOrigins, + PingInterval: v.PingInterval, + ReadTimeout: v.ReadTimeout, + WriteTimeout: v.WriteTimeout, + MaxConnections: v.MaxConnections, + AllowFrom: v.AllowFrom, + Placeholder: v.Placeholder, + }, sec } type ircConfigV0 struct { @@ -607,29 +665,33 @@ type ircConfigV0 struct { ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_IRC_REASONING_CHANNEL_ID"` } -func (v *ircConfigV0) ToIRCConfig() (IRCConfig, IRCSecurity) { - return IRCConfig{ - Enabled: v.Enabled, - Server: v.Server, - TLS: v.TLS, - Nick: v.Nick, - User: v.User, - RealName: v.RealName, - password: v.Password, - nickServPassword: v.NickServPassword, - SASLUser: v.SASLUser, - saslPassword: v.SASLPassword, - Channels: v.Channels, - RequestCaps: v.RequestCaps, - AllowFrom: v.AllowFrom, - GroupTrigger: v.GroupTrigger, - Typing: v.Typing, - ReasoningChannelID: v.ReasoningChannelID, - }, IRCSecurity{ +func (v *ircConfigV0) ToIRCConfig() (IRCConfig, *IRCSecurity) { + var sec *IRCSecurity + if v.Password != "" || v.NickServPassword != "" || v.SASLPassword != "" { + sec = &IRCSecurity{ Password: v.Password, NickServPassword: v.NickServPassword, SASLPassword: v.SASLPassword, } + } + return IRCConfig{ + Enabled: v.Enabled, + Server: v.Server, + TLS: v.TLS, + Nick: v.Nick, + User: v.User, + RealName: v.RealName, + password: v.Password, + nickServPassword: v.NickServPassword, + SASLUser: v.SASLUser, + saslPassword: v.SASLPassword, + Channels: v.Channels, + RequestCaps: v.RequestCaps, + AllowFrom: v.AllowFrom, + GroupTrigger: v.GroupTrigger, + Typing: v.Typing, + ReasoningChannelID: v.ReasoningChannelID, + }, sec } type providersConfigV0 struct { @@ -783,7 +845,7 @@ func (c *configV0) Migrate() (*Config, error) { cfg.Tools.Web, secWeb = c.Tools.Web.ToWebToolsConfig() cfg.Tools.Cron = c.Tools.Cron cfg.Tools.Exec = c.Tools.Exec - var secSkills SkillsSecurity + var secSkills *SkillsSecurity cfg.Tools.Skills, secSkills = c.Tools.Skills.ToSkillsToolsConfig() cfg.Tools.MediaCleanup = c.Tools.MediaCleanup cfg.Tools.MCP = c.Tools.MCP @@ -835,16 +897,18 @@ func (c *configV0) Migrate() (*Config, error) { for i, m := range c.ModelList { // Merge APIKey and APIKeys, deduplicating mergedKeys := MergeAPIKeys(m.APIKey, m.APIKeys) - secModels[names[i]] = ModelSecurityEntry{ - APIKeys: mergedKeys, + if len(mergedKeys) > 0 { + secModels[names[i]] = ModelSecurityEntry{ + APIKeys: mergedKeys, + } } } } cfg.WithSecurity(&SecurityConfig{ ModelList: secModels, - Channels: secChannels, - Web: secWeb, + Channels: &secChannels, + Web: &secWeb, Skills: secSkills, }) cfg.Version = CurrentVersion @@ -873,13 +937,17 @@ type braveConfigV0 struct { MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` } -func (v *braveConfigV0) ToBraveConfig() (BraveConfig, BraveSecurity) { - return BraveConfig{ - Enabled: v.Enabled, - MaxResults: v.MaxResults, - }, BraveSecurity{ +func (v *braveConfigV0) ToBraveConfig() (BraveConfig, *BraveSecurity) { + var sec *BraveSecurity + if k := MergeAPIKeys(v.APIKey, v.APIKeys); len(k) > 0 { + sec = &BraveSecurity{ APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys), } + } + return BraveConfig{ + Enabled: v.Enabled, + MaxResults: v.MaxResults, + }, sec } type tavilyConfigV0 struct { @@ -890,14 +958,18 @@ type tavilyConfigV0 struct { MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_MAX_RESULTS"` } -func (v *tavilyConfigV0) ToTavilyConfig() (TavilyConfig, TavilySecurity) { - return TavilyConfig{ - Enabled: v.Enabled, - BaseURL: v.BaseURL, - MaxResults: v.MaxResults, - }, TavilySecurity{ - APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys), +func (v *tavilyConfigV0) ToTavilyConfig() (TavilyConfig, *TavilySecurity) { + var sec *TavilySecurity + if k := MergeAPIKeys(v.APIKey, v.APIKeys); len(k) > 0 { + sec = &TavilySecurity{ + APIKeys: k, } + } + return TavilyConfig{ + Enabled: v.Enabled, + BaseURL: v.BaseURL, + MaxResults: v.MaxResults, + }, sec } type perplexityConfigV0 struct { @@ -907,13 +979,17 @@ type perplexityConfigV0 struct { MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` } -func (v *perplexityConfigV0) ToPerplexityConfig() (PerplexityConfig, PerplexitySecurity) { - return PerplexityConfig{ - Enabled: v.Enabled, - MaxResults: v.MaxResults, - }, PerplexitySecurity{ - APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys), +func (v *perplexityConfigV0) ToPerplexityConfig() (PerplexityConfig, *PerplexitySecurity) { + var sec *PerplexitySecurity + if k := MergeAPIKeys(v.APIKey, v.APIKeys); len(k) > 0 { + sec = &PerplexitySecurity{ + APIKeys: k, } + } + return PerplexityConfig{ + Enabled: v.Enabled, + MaxResults: v.MaxResults, + }, sec } type glmSearchConfigV0 struct { @@ -923,15 +999,19 @@ type glmSearchConfigV0 struct { SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` } -func (v *glmSearchConfigV0) ToGLMSearchConfig() (GLMSearchConfig, GLMSearchSecurity) { - return GLMSearchConfig{ - Enabled: v.Enabled, - apiKey: v.APIKey, - BaseURL: v.BaseURL, - SearchEngine: v.SearchEngine, - }, GLMSearchSecurity{ +func (v *glmSearchConfigV0) ToGLMSearchConfig() (GLMSearchConfig, *GLMSearchSecurity) { + var sec *GLMSearchSecurity + if v.APIKey != "" { + sec = &GLMSearchSecurity{ APIKey: v.APIKey, } + } + return GLMSearchConfig{ + Enabled: v.Enabled, + apiKey: v.APIKey, + BaseURL: v.BaseURL, + SearchEngine: v.SearchEngine, + }, sec } func (v *webToolsConfigV0) ToWebToolsConfig() (WebToolsConfig, WebToolsSecurity) { @@ -954,10 +1034,10 @@ func (v *webToolsConfigV0) ToWebToolsConfig() (WebToolsConfig, WebToolsSecurity) Format: v.Format, PrivateHostWhitelist: v.PrivateHostWhitelist, }, WebToolsSecurity{ - Brave: &braveSecurity, - Tavily: &tavilySecurity, - Perplexity: &perplexitySecurity, - GLMSearch: &glmSearchSecurity, + Brave: braveSecurity, + Tavily: tavilySecurity, + Perplexity: perplexitySecurity, + GLMSearch: glmSearchSecurity, } } @@ -981,16 +1061,20 @@ type clawHubRegistryConfigV0 struct { SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` } -func (v *clawHubRegistryConfigV0) ToClawHubRegistryConfig() (ClawHubRegistryConfig, ClawHubSecurity) { - return ClawHubRegistryConfig{ - Enabled: v.Enabled, - BaseURL: v.BaseURL, - authToken: v.AuthToken, - SearchPath: v.SearchPath, - SkillsPath: v.SkillsPath, - }, ClawHubSecurity{ +func (v *clawHubRegistryConfigV0) ToClawHubRegistryConfig() (ClawHubRegistryConfig, *ClawHubSecurity) { + var sec *ClawHubSecurity + if v.AuthToken != "" { + sec = &ClawHubSecurity{ AuthToken: v.AuthToken, } + } + return ClawHubRegistryConfig{ + Enabled: v.Enabled, + BaseURL: v.BaseURL, + authToken: v.AuthToken, + SearchPath: v.SearchPath, + SkillsPath: v.SkillsPath, + }, sec } type skillsGithubConfigV0 struct { @@ -998,13 +1082,17 @@ type skillsGithubConfigV0 struct { Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_PROXY"` } -func (v *skillsGithubConfigV0) ToSkillsGithubConfig() (SkillsGithubConfig, GithubSecurity) { - return SkillsGithubConfig{ - token: v.Token, - Proxy: v.Proxy, - }, GithubSecurity{ +func (v *skillsGithubConfigV0) ToSkillsGithubConfig() (SkillsGithubConfig, *GithubSecurity) { + var sec *GithubSecurity + if v.Token != "" { + sec = &GithubSecurity{ Token: v.Token, } + } + return SkillsGithubConfig{ + token: v.Token, + Proxy: v.Proxy, + }, sec } func (v *skillsRegistriesConfigV0) ToSkillsRegistriesConfig() (SkillsRegistriesConfig, *ClawHubSecurity) { @@ -1012,21 +1100,25 @@ func (v *skillsRegistriesConfigV0) ToSkillsRegistriesConfig() (SkillsRegistriesC return SkillsRegistriesConfig{ ClawHub: clawHub, - }, &clawHubSecurity + }, clawHubSecurity } -func (v *skillsToolsConfigV0) ToSkillsToolsConfig() (SkillsToolsConfig, SkillsSecurity) { +func (v *skillsToolsConfigV0) ToSkillsToolsConfig() (SkillsToolsConfig, *SkillsSecurity) { registries, registriesSecurity := v.Registries.ToSkillsRegistriesConfig() github, githubSecurity := v.Github.ToSkillsGithubConfig() - return SkillsToolsConfig{ - ToolConfig: v.ToolConfig, - Registries: registries, - Github: github, - MaxConcurrentSearches: v.MaxConcurrentSearches, - SearchCache: v.SearchCache, - }, SkillsSecurity{ - Github: &githubSecurity, + var sec *SkillsSecurity + if githubSecurity != nil || registriesSecurity != nil { + sec = &SkillsSecurity{ + Github: githubSecurity, ClawHub: registriesSecurity, } + } + return SkillsToolsConfig{ + ToolConfig: v.ToolConfig, + Registries: registries, + Github: github, + MaxConcurrentSearches: v.MaxConcurrentSearches, + SearchCache: v.SearchCache, + }, sec } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 7d0e3657a..b356d474f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1364,7 +1364,7 @@ func TestFilterSensitiveData_AllTokenTypes(t *testing.T) { "test-model": {APIKeys: []string{"sk-model-key-12345"}}, }, // Channel tokens - Channels: ChannelsSecurity{ + Channels: &ChannelsSecurity{ Telegram: &TelegramSecurity{Token: "telegram-bot-token-abcdef"}, Discord: &DiscordSecurity{Token: "discord-bot-token-xyz789"}, Slack: &SlackSecurity{BotToken: "xoxb-slack-bot-token", AppToken: "xapp-slack-app-token"}, @@ -1382,7 +1382,7 @@ func TestFilterSensitiveData_AllTokenTypes(t *testing.T) { }, }, // Web tool API keys - Web: WebToolsSecurity{ + Web: &WebToolsSecurity{ Brave: &BraveSecurity{APIKeys: []string{"brave-api-key"}}, Tavily: &TavilySecurity{APIKeys: []string{"tavily-api-key"}}, Perplexity: &PerplexitySecurity{APIKeys: []string{"perplexity-api-key"}}, @@ -1390,7 +1390,7 @@ func TestFilterSensitiveData_AllTokenTypes(t *testing.T) { BaiduSearch: &BaiduSearchSecurity{APIKey: "baidu-search-key"}, }, // Skills tokens - Skills: SkillsSecurity{ + Skills: &SkillsSecurity{ Github: &GithubSecurity{Token: "github-token-xyz"}, ClawHub: &ClawHubSecurity{AuthToken: "clawhub-auth-token"}, }, diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 48c03f988..c1d0ea0f6 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -539,8 +539,9 @@ func DefaultConfig() *Config { }, security: &SecurityConfig{ ModelList: map[string]ModelSecurityEntry{}, - Channels: ChannelsSecurity{}, - Web: WebToolsSecurity{}, + Channels: &ChannelsSecurity{}, + Web: &WebToolsSecurity{}, + Skills: &SkillsSecurity{}, }, } } diff --git a/pkg/config/security.go b/pkg/config/security.go index c6641f099..816d465c7 100644 --- a/pkg/config/security.go +++ b/pkg/config/security.go @@ -34,10 +34,10 @@ type SecurityConfig struct { ModelList map[string]ModelSecurityEntry `yaml:"model_list,omitempty"` // Channel tokens/secrets - Channels ChannelsSecurity `yaml:"channels,omitempty"` + Channels *ChannelsSecurity `yaml:"channels,omitempty"` - Web WebToolsSecurity `yaml:"web,omitempty"` - Skills SkillsSecurity `yaml:"skills,omitempty"` + Web *WebToolsSecurity `yaml:"web,omitempty"` + Skills *SkillsSecurity `yaml:"skills,omitempty"` // cache for sensitive values and compiled regex (computed once) sensitiveCache *SensitiveDataCache diff --git a/pkg/config/security_test.go b/pkg/config/security_test.go index 74e765f6b..af08a67db 100644 --- a/pkg/config/security_test.go +++ b/pkg/config/security_test.go @@ -59,12 +59,12 @@ func TestSaveAndLoadSecurityConfig(t *testing.T) { APIKeys: []string{"key1", "key2"}, }, }, - Channels: ChannelsSecurity{ + Channels: &ChannelsSecurity{ Telegram: &TelegramSecurity{ Token: "telegram-token", }, }, - Web: WebToolsSecurity{ + Web: &WebToolsSecurity{ Brave: &BraveSecurity{ APIKeys: []string{"brave-api-key"}, }, diff --git a/pkg/fileutil/file.go b/pkg/fileutil/file.go index 7ca872374..22374ac3d 100644 --- a/pkg/fileutil/file.go +++ b/pkg/fileutil/file.go @@ -117,3 +117,11 @@ func WriteFileAtomic(path string, data []byte, perm os.FileMode) error { cleanup = false return nil } + +func CopyFile(src, dst string, perm os.FileMode) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return WriteFileAtomic(dst, data, perm) +} diff --git a/web/backend/api/models.go b/web/backend/api/models.go index 802b28526..1e3b5f90a 100644 --- a/web/backend/api/models.go +++ b/web/backend/api/models.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/logger" ) // registerModelRoutes binds model list management endpoints to the ServeMux. @@ -158,7 +159,12 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - var mc config.ModelConfig + type custom struct { + config.ModelConfig + APIKey string `json:"api_key"` + } + + var mc custom if err = json.Unmarshal(body, &mc); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return @@ -182,14 +188,18 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) { // Preserve the existing API key when the caller omits it (empty string). // This lets the UI update api_base / proxy without clearing the stored secret. - if mc.APIKey() == "" { - mc.SetAPIKey(cfg.ModelList[idx].APIKey()) + if mc.APIKey == "" { + mc.ModelConfig.SetAPIKey(cfg.ModelList[idx].APIKey()) + } else { + mc.ModelConfig.SetAPIKey(mc.APIKey) } if mc.ExtraBody == nil { mc.ExtraBody = cfg.ModelList[idx].ExtraBody } - cfg.ModelList[idx] = &mc + cfg.ModelList[idx] = &mc.ModelConfig + + logger.Debugf("update model config: %#v", mc.ModelConfig) if err := config.SaveConfig(h.configPath, cfg); err != nil { http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)