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
This commit is contained in:
Cytown 2026-03-24 10:26:11 +08:00 committed by GitHub
parent aa3300c1bd
commit cf9e0496f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 552 additions and 415 deletions

View file

@ -1350,11 +1350,14 @@ type MCPConfig struct {
} }
func LoadConfig(path string) (*Config, error) { func LoadConfig(path string) (*Config, error) {
logger.Debugf("loading config from %s", path)
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
logger.WarnF("config file not found, using default config", map[string]any{"path": path})
return DefaultConfig(), nil return DefaultConfig(), nil
} }
logger.Errorf("failed to read config file: %v", err)
return nil, 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) return nil, fmt.Errorf("failed to detect config version: %w", e)
} }
if len(data) <= 10 { if len(data) <= 10 {
logger.Warn(fmt.Sprintf("content is [%s]", string(data)))
return DefaultConfig().WithSecurity(&SecurityConfig{}), nil return DefaultConfig().WithSecurity(&SecurityConfig{}), nil
} }
@ -1381,36 +1385,39 @@ func LoadConfig(path string) (*Config, error) {
} }
cfg, e = v.Migrate() cfg, e = v.Migrate()
if e != nil { 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 return nil, e
} }
logger.DebugF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion}) logger.InfoF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
defer func() { err = makeBackup(path)
if err != nil {
return nil, err
}
defer func(cfg *Config) {
_ = SaveConfig(path, cfg) _ = SaveConfig(path, cfg)
}() }(cfg)
case CurrentVersion: case CurrentVersion:
// Current version // Current version
cfg, err = loadConfig(data) cfg, err = loadConfig(data)
if err != nil { if err != nil {
return nil, err 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: default:
return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version) 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 != "" { if passphrase := credential.PassphraseProvider(); passphrase != "" {
for _, m := range cfg.ModelList { for _, m := range cfg.ModelList {
for _, k := range m.apiKeys { for _, k := range m.apiKeys {
@ -1462,6 +1469,19 @@ func LoadConfig(path string) (*Config, error) {
return cfg, nil 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) { func copyArray[T any](dst, src *[]T) {
*dst = make([]T, len(*src)) *dst = make([]T, len(*src))
copy(*dst, *src) copy(*dst, *src)
@ -1474,32 +1494,36 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
return nil return nil
} }
if sec.Web.Brave != nil && len(sec.Web.Brave.APIKeys) > 0 { if sec.Web != nil {
copyArray(&cfg.Tools.Web.Brave.apiKeys, &sec.Web.Brave.APIKeys) 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 { if sec.Skills != nil {
copyArray(&cfg.Tools.Web.Tavily.apiKeys, &sec.Web.Tavily.APIKeys) 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 { if sec.Skills.ClawHub != nil && sec.Skills.ClawHub.AuthToken != "" {
copyArray(&cfg.Tools.Web.Perplexity.apiKeys, &sec.Web.Perplexity.APIKeys) cfg.Tools.Skills.Registries.ClawHub.authToken = sec.Skills.ClawHub.AuthToken
} }
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
} }
names := toNameIndex(cfg.ModelList) names := toNameIndex(cfg.ModelList)
@ -1521,126 +1545,128 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
} }
} }
// Handle Telegram token if sec.Channels != nil {
if sec.Channels.Telegram != nil && sec.Channels.Telegram.Token != "" { // Handle Telegram token
cfg.Channels.Telegram.token = sec.Channels.Telegram.Token if sec.Channels.Telegram != nil && sec.Channels.Telegram.Token != "" {
} cfg.Channels.Telegram.token = sec.Channels.Telegram.Token
}
// Handle Feishu credentials // Handle Feishu credentials
if sec.Channels.Feishu != nil { if sec.Channels.Feishu != nil {
if sec.Channels.Feishu.AppSecret != "" { if sec.Channels.Feishu.AppSecret != "" {
cfg.Channels.Feishu.appSecret = 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 // Handle Discord token
if sec.Channels.Discord != nil && sec.Channels.Discord.Token != "" { if sec.Channels.Discord != nil && sec.Channels.Discord.Token != "" {
cfg.Channels.Discord.token = sec.Channels.Discord.Token cfg.Channels.Discord.token = sec.Channels.Discord.Token
} }
// Handle Weixin token // Handle Weixin token
if sec.Channels.Weixin != nil && sec.Channels.Weixin.Token != "" { if sec.Channels.Weixin != nil && sec.Channels.Weixin.Token != "" {
cfg.Channels.Weixin.token = sec.Channels.Weixin.Token cfg.Channels.Weixin.token = sec.Channels.Weixin.Token
} }
// Handle DingTalk client secret // Handle DingTalk client secret
if sec.Channels.DingTalk != nil && sec.Channels.DingTalk.ClientSecret != "" { if sec.Channels.DingTalk != nil && sec.Channels.DingTalk.ClientSecret != "" {
cfg.Channels.DingTalk.clientSecret = sec.Channels.DingTalk.ClientSecret cfg.Channels.DingTalk.clientSecret = sec.Channels.DingTalk.ClientSecret
} }
// Handle Slack tokens // Handle Slack tokens
if sec.Channels.Slack != nil { if sec.Channels.Slack != nil {
if sec.Channels.Slack.BotToken != "" { if sec.Channels.Slack.BotToken != "" {
cfg.Channels.Slack.botToken = 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 // Handle Matrix access token
if sec.Channels.Matrix != nil && sec.Channels.Matrix.AccessToken != "" { if sec.Channels.Matrix != nil && sec.Channels.Matrix.AccessToken != "" {
cfg.Channels.Matrix.accessToken = sec.Channels.Matrix.AccessToken cfg.Channels.Matrix.accessToken = sec.Channels.Matrix.AccessToken
} }
// Handle LINE credentials // Handle LINE credentials
if sec.Channels.LINE != nil { if sec.Channels.LINE != nil {
if sec.Channels.LINE.ChannelSecret != "" { if sec.Channels.LINE.ChannelSecret != "" {
cfg.Channels.LINE.channelSecret = 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 // Handle OneBot access token
if sec.Channels.OneBot != nil && sec.Channels.OneBot.AccessToken != "" { if sec.Channels.OneBot != nil && sec.Channels.OneBot.AccessToken != "" {
cfg.Channels.OneBot.accessToken = sec.Channels.OneBot.AccessToken cfg.Channels.OneBot.accessToken = sec.Channels.OneBot.AccessToken
} }
// Handle WeCom token and encoding key // Handle WeCom token and encoding key
if sec.Channels.WeCom != nil { if sec.Channels.WeCom != nil {
if sec.Channels.WeCom.Token != "" { if sec.Channels.WeCom.Token != "" {
cfg.Channels.WeCom.token = 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 // Handle WeCom App credentials
if sec.Channels.WeComApp != nil { if sec.Channels.WeComApp != nil {
if sec.Channels.WeComApp.CorpSecret != "" { if sec.Channels.WeComApp.CorpSecret != "" {
cfg.Channels.WeComApp.corpSecret = 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 // Handle WeCom AI Bot credentials
if sec.Channels.WeComAIBot != nil { if sec.Channels.WeComAIBot != nil {
if sec.Channels.WeComAIBot.Token != "" { if sec.Channels.WeComAIBot.Token != "" {
cfg.Channels.WeComAIBot.token = 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 // Handle Pico channel token
if sec.Channels.Pico != nil && sec.Channels.Pico.Token != "" { if sec.Channels.Pico != nil && sec.Channels.Pico.Token != "" {
cfg.Channels.Pico.token = sec.Channels.Pico.Token cfg.Channels.Pico.token = sec.Channels.Pico.Token
} }
// Handle IRC passwords // Handle IRC passwords
if sec.Channels.IRC != nil { if sec.Channels.IRC != nil {
if sec.Channels.IRC.Password != "" { if sec.Channels.IRC.Password != "" {
cfg.Channels.IRC.password = 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 // Handle QQ app secret
if sec.Channels.QQ != nil && sec.Channels.QQ.AppSecret != "" { if sec.Channels.QQ != nil && sec.Channels.QQ.AppSecret != "" {
cfg.Channels.QQ.appSecret = sec.Channels.QQ.AppSecret cfg.Channels.QQ.appSecret = sec.Channels.QQ.AppSecret
}
} }
cfg.security = sec cfg.security = sec

View file

@ -5,7 +5,9 @@
package config package config
import "encoding/json" import (
"encoding/json"
)
type agentDefaultsV0 struct { type agentDefaultsV0 struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
@ -139,21 +141,21 @@ func (v *channelsConfigV0) ToChannelsConfig() (ChannelsConfig, ChannelsSecurity)
Pico: pico, Pico: pico,
IRC: irc, IRC: irc,
}, ChannelsSecurity{ }, ChannelsSecurity{
Telegram: &telegramSecurity, Telegram: telegramSecurity,
Feishu: &feishuSecurity, Feishu: feishuSecurity,
Discord: &discordSecurity, Discord: discordSecurity,
QQ: &qqSecurity, QQ: qqSecurity,
Weixin: &weixinSecurity, Weixin: weixinSecurity,
DingTalk: &dingtalkSecurity, DingTalk: dingtalkSecurity,
Slack: &slackSecurity, Slack: slackSecurity,
Matrix: &matrixSecurity, Matrix: matrixSecurity,
LINE: &lineSecurity, LINE: lineSecurity,
OneBot: &onebotSecurity, OneBot: onebotSecurity,
WeCom: &wecomSecurity, WeCom: wecomSecurity,
WeComApp: &wecomappSecurity, WeComApp: wecomappSecurity,
WeComAIBot: &wecomaibotSecurity, WeComAIBot: wecomaibotSecurity,
Pico: &picoSecurity, Pico: picoSecurity,
IRC: &ircSecurity, IRC: ircSecurity,
} }
} }
@ -169,19 +171,23 @@ type qqConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
} }
func (v *qqConfigV0) ToQQConfig() (QQConfig, QQSecurity) { func (v *qqConfigV0) ToQQConfig() (QQConfig, *QQSecurity) {
return QQConfig{ var sec *QQSecurity
Enabled: v.Enabled, if v.AppSecret != "" {
AppID: v.AppID, sec = &QQSecurity{
AllowFrom: v.AllowFrom,
GroupTrigger: v.GroupTrigger,
MaxMessageLength: v.MaxMessageLength,
MaxBase64FileSizeMiB: v.MaxBase64FileSizeMiB,
SendMarkdown: v.SendMarkdown,
ReasoningChannelID: v.ReasoningChannelID,
}, QQSecurity{
AppSecret: v.AppSecret, 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 { type telegramConfigV0 struct {
@ -197,21 +203,25 @@ type telegramConfigV0 struct {
UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"` UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
} }
func (v *telegramConfigV0) ToTelegramConfig() (TelegramConfig, TelegramSecurity) { func (v *telegramConfigV0) ToTelegramConfig() (TelegramConfig, *TelegramSecurity) {
return TelegramConfig{ var sec *TelegramSecurity
Enabled: v.Enabled, if v.Token != "" {
token: v.Token, sec = &TelegramSecurity{
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{
Token: v.Token, 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 { type feishuConfigV0 struct {
@ -228,20 +238,24 @@ type feishuConfigV0 struct {
IsLark bool `json:"is_lark" env:"PICOCLAW_CHANNELS_FEISHU_IS_LARK"` IsLark bool `json:"is_lark" env:"PICOCLAW_CHANNELS_FEISHU_IS_LARK"`
} }
func (v *feishuConfigV0) ToFeishuConfig() (FeishuConfig, FeishuSecurity) { func (v *feishuConfigV0) ToFeishuConfig() (FeishuConfig, *FeishuSecurity) {
return FeishuConfig{ var sec *FeishuSecurity
Enabled: v.Enabled, if v.AppSecret != "" || v.EncryptKey != "" || v.VerificationToken != "" {
AppID: v.AppID, sec = &FeishuSecurity{
appSecret: v.AppSecret,
AllowFrom: v.AllowFrom,
GroupTrigger: v.GroupTrigger,
Placeholder: v.Placeholder,
ReasoningChannelID: v.ReasoningChannelID,
}, FeishuSecurity{
AppSecret: v.AppSecret, AppSecret: v.AppSecret,
EncryptKey: v.EncryptKey, EncryptKey: v.EncryptKey,
VerificationToken: v.VerificationToken, 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 { type discordConfigV0 struct {
@ -256,20 +270,24 @@ type discordConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"`
} }
func (v *discordConfigV0) ToDiscordConfig() (DiscordConfig, DiscordSecurity) { func (v *discordConfigV0) ToDiscordConfig() (DiscordConfig, *DiscordSecurity) {
return DiscordConfig{ var sec *DiscordSecurity
Enabled: v.Enabled, if v.Token != "" {
token: v.Token, sec = &DiscordSecurity{
Proxy: v.Proxy,
AllowFrom: v.AllowFrom,
MentionOnly: v.MentionOnly,
GroupTrigger: v.GroupTrigger,
Typing: v.Typing,
Placeholder: v.Placeholder,
ReasoningChannelID: v.ReasoningChannelID,
}, DiscordSecurity{
Token: v.Token, 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 { type maixcamConfigV0 struct {
@ -299,17 +317,21 @@ type dingtalkConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"`
} }
func (v *dingtalkConfigV0) ToDingTalkConfig() (DingTalkConfig, DingTalkSecurity) { func (v *dingtalkConfigV0) ToDingTalkConfig() (DingTalkConfig, *DingTalkSecurity) {
return DingTalkConfig{ var sec *DingTalkSecurity
Enabled: v.Enabled, if v.ClientSecret != "" {
ClientID: v.ClientID, sec = &DingTalkSecurity{
clientSecret: v.ClientSecret,
AllowFrom: v.AllowFrom,
GroupTrigger: v.GroupTrigger,
ReasoningChannelID: v.ReasoningChannelID,
}, DingTalkSecurity{
ClientSecret: v.ClientSecret, 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 { type slackConfigV0 struct {
@ -323,20 +345,24 @@ type slackConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"`
} }
func (v *slackConfigV0) ToSlackConfig() (SlackConfig, SlackSecurity) { func (v *slackConfigV0) ToSlackConfig() (SlackConfig, *SlackSecurity) {
return SlackConfig{ var sec *SlackSecurity
Enabled: v.Enabled, if v.BotToken != "" || v.AppToken != "" {
botToken: v.BotToken, sec = &SlackSecurity{
appToken: v.AppToken,
AllowFrom: v.AllowFrom,
GroupTrigger: v.GroupTrigger,
Typing: v.Typing,
Placeholder: v.Placeholder,
ReasoningChannelID: v.ReasoningChannelID,
}, SlackSecurity{
BotToken: v.BotToken, BotToken: v.BotToken,
AppToken: v.AppToken, 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 { type matrixConfigV0 struct {
@ -353,22 +379,26 @@ type matrixConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"`
} }
func (v *matrixConfigV0) ToMatrixConfig() (MatrixConfig, MatrixSecurity) { func (v *matrixConfigV0) ToMatrixConfig() (MatrixConfig, *MatrixSecurity) {
return MatrixConfig{ var sec *MatrixSecurity
Enabled: v.Enabled, if v.AccessToken != "" {
Homeserver: v.Homeserver, sec = &MatrixSecurity{
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{
AccessToken: v.AccessToken, 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 { type lineConfigV0 struct {
@ -385,23 +415,27 @@ type lineConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"`
} }
func (v *lineConfigV0) ToLINEConfig() (LINEConfig, LINESecurity) { func (v *lineConfigV0) ToLINEConfig() (LINEConfig, *LINESecurity) {
return LINEConfig{ var sec *LINESecurity
Enabled: v.Enabled, if v.ChannelSecret != "" || v.ChannelAccessToken != "" {
channelSecret: v.ChannelSecret, sec = &LINESecurity{
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{
ChannelSecret: v.ChannelSecret, ChannelSecret: v.ChannelSecret,
ChannelAccessToken: v.ChannelAccessToken, 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 { type onebotConfigV0 struct {
@ -417,21 +451,25 @@ type onebotConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"`
} }
func (v *onebotConfigV0) ToOneBotConfig() (OneBotConfig, OneBotSecurity) { func (v *onebotConfigV0) ToOneBotConfig() (OneBotConfig, *OneBotSecurity) {
return OneBotConfig{ var sec *OneBotSecurity
Enabled: v.Enabled, if v.AccessToken != "" {
WSUrl: v.WSUrl, sec = &OneBotSecurity{
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{
AccessToken: v.AccessToken, 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 { type wecomConfigV0 struct {
@ -448,23 +486,27 @@ type wecomConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_REASONING_CHANNEL_ID"`
} }
func (v *wecomConfigV0) ToWeComConfig() (WeComConfig, WeComSecurity) { func (v *wecomConfigV0) ToWeComConfig() (WeComConfig, *WeComSecurity) {
return WeComConfig{ var sec *WeComSecurity
Enabled: v.Enabled, if v.Token != "" || v.EncodingAESKey != "" {
token: v.Token, sec = &WeComSecurity{
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{
Token: v.Token, Token: v.Token,
EncodingAESKey: v.EncodingAESKey, 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 { type weixinConfigV0 struct {
@ -477,18 +519,22 @@ type weixinConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"`
} }
func (v *weixinConfigV0) ToWeiXinConfig() (WeixinConfig, WeixinSecurity) { func (v *weixinConfigV0) ToWeiXinConfig() (WeixinConfig, *WeixinSecurity) {
return WeixinConfig{ var sec *WeixinSecurity
Enabled: v.Enabled, if v.Token != "" {
token: v.Token, sec = &WeixinSecurity{
BaseURL: v.BaseURL,
CDNBaseURL: v.CDNBaseURL,
Proxy: v.Proxy,
AllowFrom: v.AllowFrom,
ReasoningChannelID: v.ReasoningChannelID,
}, WeixinSecurity{
Token: v.Token, 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 { type wecomappConfigV0 struct {
@ -507,26 +553,30 @@ type wecomappConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_APP_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_APP_REASONING_CHANNEL_ID"`
} }
func (v *wecomappConfigV0) ToWeComAppConfig() (WeComAppConfig, WeComAppSecurity) { func (v *wecomappConfigV0) ToWeComAppConfig() (WeComAppConfig, *WeComAppSecurity) {
return WeComAppConfig{ var sec *WeComAppSecurity
Enabled: v.Enabled, if v.CorpSecret != "" || v.Token != "" || v.EncodingAESKey != "" {
CorpID: v.CorpID, sec = &WeComAppSecurity{
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{
CorpSecret: v.CorpSecret, CorpSecret: v.CorpSecret,
Token: v.Token, Token: v.Token,
EncodingAESKey: v.EncodingAESKey, 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 { type wecomaibotConfigV0 struct {
@ -542,20 +592,24 @@ type wecomaibotConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REASONING_CHANNEL_ID"`
} }
func (v *wecomaibotConfigV0) ToWeComAIBotConfig() (WeComAIBotConfig, WeComAIBotSecurity) { func (v *wecomaibotConfigV0) ToWeComAIBotConfig() (WeComAIBotConfig, *WeComAIBotSecurity) {
return WeComAIBotConfig{ var sec *WeComAIBotSecurity
Enabled: v.Enabled, if v.Token != "" || v.Secret != "" || v.EncodingAESKey != "" {
WebhookPath: v.WebhookPath, sec = &WeComAIBotSecurity{
AllowFrom: v.AllowFrom,
ReplyTimeout: v.ReplyTimeout,
MaxSteps: v.MaxSteps,
WelcomeMessage: v.WelcomeMessage,
ReasoningChannelID: v.ReasoningChannelID,
}, WeComAIBotSecurity{
Token: v.Token, Token: v.Token,
Secret: v.Secret, Secret: v.Secret,
EncodingAESKey: v.EncodingAESKey, 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 { type picoConfigV0 struct {
@ -571,21 +625,25 @@ type picoConfigV0 struct {
Placeholder PlaceholderConfig `json:"placeholder,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
} }
func (v *picoConfigV0) ToPicoConfig() (PicoConfig, PicoSecurity) { func (v *picoConfigV0) ToPicoConfig() (PicoConfig, *PicoSecurity) {
return PicoConfig{ var sec *PicoSecurity
Enabled: v.Enabled, if v.Token != "" {
token: v.Token, sec = &PicoSecurity{
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{
Token: v.Token, 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 { type ircConfigV0 struct {
@ -607,29 +665,33 @@ type ircConfigV0 struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_IRC_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_IRC_REASONING_CHANNEL_ID"`
} }
func (v *ircConfigV0) ToIRCConfig() (IRCConfig, IRCSecurity) { func (v *ircConfigV0) ToIRCConfig() (IRCConfig, *IRCSecurity) {
return IRCConfig{ var sec *IRCSecurity
Enabled: v.Enabled, if v.Password != "" || v.NickServPassword != "" || v.SASLPassword != "" {
Server: v.Server, sec = &IRCSecurity{
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{
Password: v.Password, Password: v.Password,
NickServPassword: v.NickServPassword, NickServPassword: v.NickServPassword,
SASLPassword: v.SASLPassword, 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 { type providersConfigV0 struct {
@ -783,7 +845,7 @@ func (c *configV0) Migrate() (*Config, error) {
cfg.Tools.Web, secWeb = c.Tools.Web.ToWebToolsConfig() cfg.Tools.Web, secWeb = c.Tools.Web.ToWebToolsConfig()
cfg.Tools.Cron = c.Tools.Cron cfg.Tools.Cron = c.Tools.Cron
cfg.Tools.Exec = c.Tools.Exec cfg.Tools.Exec = c.Tools.Exec
var secSkills SkillsSecurity var secSkills *SkillsSecurity
cfg.Tools.Skills, secSkills = c.Tools.Skills.ToSkillsToolsConfig() cfg.Tools.Skills, secSkills = c.Tools.Skills.ToSkillsToolsConfig()
cfg.Tools.MediaCleanup = c.Tools.MediaCleanup cfg.Tools.MediaCleanup = c.Tools.MediaCleanup
cfg.Tools.MCP = c.Tools.MCP cfg.Tools.MCP = c.Tools.MCP
@ -835,16 +897,18 @@ func (c *configV0) Migrate() (*Config, error) {
for i, m := range c.ModelList { for i, m := range c.ModelList {
// Merge APIKey and APIKeys, deduplicating // Merge APIKey and APIKeys, deduplicating
mergedKeys := MergeAPIKeys(m.APIKey, m.APIKeys) mergedKeys := MergeAPIKeys(m.APIKey, m.APIKeys)
secModels[names[i]] = ModelSecurityEntry{ if len(mergedKeys) > 0 {
APIKeys: mergedKeys, secModels[names[i]] = ModelSecurityEntry{
APIKeys: mergedKeys,
}
} }
} }
} }
cfg.WithSecurity(&SecurityConfig{ cfg.WithSecurity(&SecurityConfig{
ModelList: secModels, ModelList: secModels,
Channels: secChannels, Channels: &secChannels,
Web: secWeb, Web: &secWeb,
Skills: secSkills, Skills: secSkills,
}) })
cfg.Version = CurrentVersion cfg.Version = CurrentVersion
@ -873,13 +937,17 @@ type braveConfigV0 struct {
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
} }
func (v *braveConfigV0) ToBraveConfig() (BraveConfig, BraveSecurity) { func (v *braveConfigV0) ToBraveConfig() (BraveConfig, *BraveSecurity) {
return BraveConfig{ var sec *BraveSecurity
Enabled: v.Enabled, if k := MergeAPIKeys(v.APIKey, v.APIKeys); len(k) > 0 {
MaxResults: v.MaxResults, sec = &BraveSecurity{
}, BraveSecurity{
APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys), APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys),
} }
}
return BraveConfig{
Enabled: v.Enabled,
MaxResults: v.MaxResults,
}, sec
} }
type tavilyConfigV0 struct { type tavilyConfigV0 struct {
@ -890,14 +958,18 @@ type tavilyConfigV0 struct {
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_MAX_RESULTS"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_MAX_RESULTS"`
} }
func (v *tavilyConfigV0) ToTavilyConfig() (TavilyConfig, TavilySecurity) { func (v *tavilyConfigV0) ToTavilyConfig() (TavilyConfig, *TavilySecurity) {
return TavilyConfig{ var sec *TavilySecurity
Enabled: v.Enabled, if k := MergeAPIKeys(v.APIKey, v.APIKeys); len(k) > 0 {
BaseURL: v.BaseURL, sec = &TavilySecurity{
MaxResults: v.MaxResults, APIKeys: k,
}, TavilySecurity{
APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys),
} }
}
return TavilyConfig{
Enabled: v.Enabled,
BaseURL: v.BaseURL,
MaxResults: v.MaxResults,
}, sec
} }
type perplexityConfigV0 struct { type perplexityConfigV0 struct {
@ -907,13 +979,17 @@ type perplexityConfigV0 struct {
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
} }
func (v *perplexityConfigV0) ToPerplexityConfig() (PerplexityConfig, PerplexitySecurity) { func (v *perplexityConfigV0) ToPerplexityConfig() (PerplexityConfig, *PerplexitySecurity) {
return PerplexityConfig{ var sec *PerplexitySecurity
Enabled: v.Enabled, if k := MergeAPIKeys(v.APIKey, v.APIKeys); len(k) > 0 {
MaxResults: v.MaxResults, sec = &PerplexitySecurity{
}, PerplexitySecurity{ APIKeys: k,
APIKeys: MergeAPIKeys(v.APIKey, v.APIKeys),
} }
}
return PerplexityConfig{
Enabled: v.Enabled,
MaxResults: v.MaxResults,
}, sec
} }
type glmSearchConfigV0 struct { type glmSearchConfigV0 struct {
@ -923,15 +999,19 @@ type glmSearchConfigV0 struct {
SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"`
} }
func (v *glmSearchConfigV0) ToGLMSearchConfig() (GLMSearchConfig, GLMSearchSecurity) { func (v *glmSearchConfigV0) ToGLMSearchConfig() (GLMSearchConfig, *GLMSearchSecurity) {
return GLMSearchConfig{ var sec *GLMSearchSecurity
Enabled: v.Enabled, if v.APIKey != "" {
apiKey: v.APIKey, sec = &GLMSearchSecurity{
BaseURL: v.BaseURL,
SearchEngine: v.SearchEngine,
}, GLMSearchSecurity{
APIKey: v.APIKey, APIKey: v.APIKey,
} }
}
return GLMSearchConfig{
Enabled: v.Enabled,
apiKey: v.APIKey,
BaseURL: v.BaseURL,
SearchEngine: v.SearchEngine,
}, sec
} }
func (v *webToolsConfigV0) ToWebToolsConfig() (WebToolsConfig, WebToolsSecurity) { func (v *webToolsConfigV0) ToWebToolsConfig() (WebToolsConfig, WebToolsSecurity) {
@ -954,10 +1034,10 @@ func (v *webToolsConfigV0) ToWebToolsConfig() (WebToolsConfig, WebToolsSecurity)
Format: v.Format, Format: v.Format,
PrivateHostWhitelist: v.PrivateHostWhitelist, PrivateHostWhitelist: v.PrivateHostWhitelist,
}, WebToolsSecurity{ }, WebToolsSecurity{
Brave: &braveSecurity, Brave: braveSecurity,
Tavily: &tavilySecurity, Tavily: tavilySecurity,
Perplexity: &perplexitySecurity, Perplexity: perplexitySecurity,
GLMSearch: &glmSearchSecurity, GLMSearch: glmSearchSecurity,
} }
} }
@ -981,16 +1061,20 @@ type clawHubRegistryConfigV0 struct {
SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"`
} }
func (v *clawHubRegistryConfigV0) ToClawHubRegistryConfig() (ClawHubRegistryConfig, ClawHubSecurity) { func (v *clawHubRegistryConfigV0) ToClawHubRegistryConfig() (ClawHubRegistryConfig, *ClawHubSecurity) {
return ClawHubRegistryConfig{ var sec *ClawHubSecurity
Enabled: v.Enabled, if v.AuthToken != "" {
BaseURL: v.BaseURL, sec = &ClawHubSecurity{
authToken: v.AuthToken,
SearchPath: v.SearchPath,
SkillsPath: v.SkillsPath,
}, ClawHubSecurity{
AuthToken: v.AuthToken, AuthToken: v.AuthToken,
} }
}
return ClawHubRegistryConfig{
Enabled: v.Enabled,
BaseURL: v.BaseURL,
authToken: v.AuthToken,
SearchPath: v.SearchPath,
SkillsPath: v.SkillsPath,
}, sec
} }
type skillsGithubConfigV0 struct { type skillsGithubConfigV0 struct {
@ -998,13 +1082,17 @@ type skillsGithubConfigV0 struct {
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_PROXY"` Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_PROXY"`
} }
func (v *skillsGithubConfigV0) ToSkillsGithubConfig() (SkillsGithubConfig, GithubSecurity) { func (v *skillsGithubConfigV0) ToSkillsGithubConfig() (SkillsGithubConfig, *GithubSecurity) {
return SkillsGithubConfig{ var sec *GithubSecurity
token: v.Token, if v.Token != "" {
Proxy: v.Proxy, sec = &GithubSecurity{
}, GithubSecurity{
Token: v.Token, Token: v.Token,
} }
}
return SkillsGithubConfig{
token: v.Token,
Proxy: v.Proxy,
}, sec
} }
func (v *skillsRegistriesConfigV0) ToSkillsRegistriesConfig() (SkillsRegistriesConfig, *ClawHubSecurity) { func (v *skillsRegistriesConfigV0) ToSkillsRegistriesConfig() (SkillsRegistriesConfig, *ClawHubSecurity) {
@ -1012,21 +1100,25 @@ func (v *skillsRegistriesConfigV0) ToSkillsRegistriesConfig() (SkillsRegistriesC
return SkillsRegistriesConfig{ return SkillsRegistriesConfig{
ClawHub: clawHub, ClawHub: clawHub,
}, &clawHubSecurity }, clawHubSecurity
} }
func (v *skillsToolsConfigV0) ToSkillsToolsConfig() (SkillsToolsConfig, SkillsSecurity) { func (v *skillsToolsConfigV0) ToSkillsToolsConfig() (SkillsToolsConfig, *SkillsSecurity) {
registries, registriesSecurity := v.Registries.ToSkillsRegistriesConfig() registries, registriesSecurity := v.Registries.ToSkillsRegistriesConfig()
github, githubSecurity := v.Github.ToSkillsGithubConfig() github, githubSecurity := v.Github.ToSkillsGithubConfig()
return SkillsToolsConfig{ var sec *SkillsSecurity
ToolConfig: v.ToolConfig, if githubSecurity != nil || registriesSecurity != nil {
Registries: registries, sec = &SkillsSecurity{
Github: github, Github: githubSecurity,
MaxConcurrentSearches: v.MaxConcurrentSearches,
SearchCache: v.SearchCache,
}, SkillsSecurity{
Github: &githubSecurity,
ClawHub: registriesSecurity, ClawHub: registriesSecurity,
} }
}
return SkillsToolsConfig{
ToolConfig: v.ToolConfig,
Registries: registries,
Github: github,
MaxConcurrentSearches: v.MaxConcurrentSearches,
SearchCache: v.SearchCache,
}, sec
} }

View file

@ -1364,7 +1364,7 @@ func TestFilterSensitiveData_AllTokenTypes(t *testing.T) {
"test-model": {APIKeys: []string{"sk-model-key-12345"}}, "test-model": {APIKeys: []string{"sk-model-key-12345"}},
}, },
// Channel tokens // Channel tokens
Channels: ChannelsSecurity{ Channels: &ChannelsSecurity{
Telegram: &TelegramSecurity{Token: "telegram-bot-token-abcdef"}, Telegram: &TelegramSecurity{Token: "telegram-bot-token-abcdef"},
Discord: &DiscordSecurity{Token: "discord-bot-token-xyz789"}, Discord: &DiscordSecurity{Token: "discord-bot-token-xyz789"},
Slack: &SlackSecurity{BotToken: "xoxb-slack-bot-token", AppToken: "xapp-slack-app-token"}, 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 tool API keys
Web: WebToolsSecurity{ Web: &WebToolsSecurity{
Brave: &BraveSecurity{APIKeys: []string{"brave-api-key"}}, Brave: &BraveSecurity{APIKeys: []string{"brave-api-key"}},
Tavily: &TavilySecurity{APIKeys: []string{"tavily-api-key"}}, Tavily: &TavilySecurity{APIKeys: []string{"tavily-api-key"}},
Perplexity: &PerplexitySecurity{APIKeys: []string{"perplexity-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"}, BaiduSearch: &BaiduSearchSecurity{APIKey: "baidu-search-key"},
}, },
// Skills tokens // Skills tokens
Skills: SkillsSecurity{ Skills: &SkillsSecurity{
Github: &GithubSecurity{Token: "github-token-xyz"}, Github: &GithubSecurity{Token: "github-token-xyz"},
ClawHub: &ClawHubSecurity{AuthToken: "clawhub-auth-token"}, ClawHub: &ClawHubSecurity{AuthToken: "clawhub-auth-token"},
}, },

View file

@ -539,8 +539,9 @@ func DefaultConfig() *Config {
}, },
security: &SecurityConfig{ security: &SecurityConfig{
ModelList: map[string]ModelSecurityEntry{}, ModelList: map[string]ModelSecurityEntry{},
Channels: ChannelsSecurity{}, Channels: &ChannelsSecurity{},
Web: WebToolsSecurity{}, Web: &WebToolsSecurity{},
Skills: &SkillsSecurity{},
}, },
} }
} }

View file

@ -34,10 +34,10 @@ type SecurityConfig struct {
ModelList map[string]ModelSecurityEntry `yaml:"model_list,omitempty"` ModelList map[string]ModelSecurityEntry `yaml:"model_list,omitempty"`
// Channel tokens/secrets // Channel tokens/secrets
Channels ChannelsSecurity `yaml:"channels,omitempty"` Channels *ChannelsSecurity `yaml:"channels,omitempty"`
Web WebToolsSecurity `yaml:"web,omitempty"` Web *WebToolsSecurity `yaml:"web,omitempty"`
Skills SkillsSecurity `yaml:"skills,omitempty"` Skills *SkillsSecurity `yaml:"skills,omitempty"`
// cache for sensitive values and compiled regex (computed once) // cache for sensitive values and compiled regex (computed once)
sensitiveCache *SensitiveDataCache sensitiveCache *SensitiveDataCache

View file

@ -59,12 +59,12 @@ func TestSaveAndLoadSecurityConfig(t *testing.T) {
APIKeys: []string{"key1", "key2"}, APIKeys: []string{"key1", "key2"},
}, },
}, },
Channels: ChannelsSecurity{ Channels: &ChannelsSecurity{
Telegram: &TelegramSecurity{ Telegram: &TelegramSecurity{
Token: "telegram-token", Token: "telegram-token",
}, },
}, },
Web: WebToolsSecurity{ Web: &WebToolsSecurity{
Brave: &BraveSecurity{ Brave: &BraveSecurity{
APIKeys: []string{"brave-api-key"}, APIKeys: []string{"brave-api-key"},
}, },

View file

@ -117,3 +117,11 @@ func WriteFileAtomic(path string, data []byte, perm os.FileMode) error {
cleanup = false cleanup = false
return nil 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)
}

View file

@ -9,6 +9,7 @@ import (
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
) )
// registerModelRoutes binds model list management endpoints to the ServeMux. // 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() 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 { if err = json.Unmarshal(body, &mc); err != nil {
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
return 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). // 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. // This lets the UI update api_base / proxy without clearing the stored secret.
if mc.APIKey() == "" { if mc.APIKey == "" {
mc.SetAPIKey(cfg.ModelList[idx].APIKey()) mc.ModelConfig.SetAPIKey(cfg.ModelList[idx].APIKey())
} else {
mc.ModelConfig.SetAPIKey(mc.APIKey)
} }
if mc.ExtraBody == nil { if mc.ExtraBody == nil {
mc.ExtraBody = cfg.ModelList[idx].ExtraBody 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 { if err := config.SaveConfig(h.configPath, cfg); err != nil {
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)