Merge branch 'main' into version

This commit is contained in:
Cytown
2026-03-22 19:58:33 +08:00
42 changed files with 3797 additions and 839 deletions
+53 -19
View File
@@ -259,6 +259,7 @@ type AgentDefaults struct {
MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
Routing *RoutingConfig `json:"routing,omitempty"`
ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"`
LogLevel string `json:"log_level,omitempty" env:"PICOCLAW_LOG_LEVEL"`
}
const (
@@ -307,6 +308,7 @@ type ChannelsConfig struct {
WeCom WeComConfig `json:"wecom"`
WeComApp WeComAppConfig `json:"wecom_app"`
WeComAIBot WeComAIBotConfig `json:"wecom_aibot"`
Weixin WeixinConfig `json:"weixin"`
Pico PicoConfig `json:"pico"`
PicoClient PicoClientConfig `json:"pico_client"`
IRC IRCConfig `json:"irc"`
@@ -751,6 +753,27 @@ func (c *WeComAIBotConfig) SetSecret(secret string) {
c.secDirty = true
}
type WeixinConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"`
token string
BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"`
CDNBaseURL string `json:"cdn_base_url" env:"PICOCLAW_CHANNELS_WEIXIN_CDN_BASE_URL"`
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"`
secDirty bool
}
func (c *WeixinConfig) Token() string {
return c.token
}
func (c *WeixinConfig) SetToken(token string) *WeixinConfig {
c.token = token
c.secDirty = true
return c
}
type PicoConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_PICO_ENABLED"`
token string
@@ -1391,82 +1414,87 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
// Handle Telegram token
if sec.Channels.Telegram != nil && sec.Channels.Telegram.Token != "" {
cfg.Channels.Telegram.SetToken(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.SetAppSecret(sec.Channels.Feishu.AppSecret)
cfg.Channels.Feishu.appSecret = sec.Channels.Feishu.AppSecret
}
if sec.Channels.Feishu.EncryptKey != "" {
cfg.Channels.Feishu.SetEncryptKey(sec.Channels.Feishu.EncryptKey)
cfg.Channels.Feishu.encryptKey = sec.Channels.Feishu.EncryptKey
}
if sec.Channels.Feishu.VerificationToken != "" {
cfg.Channels.Feishu.SetVerificationToken(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.SetToken(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.Discord.token = sec.Channels.Discord.Token
}
// Handle DingTalk client secret
if sec.Channels.DingTalk != nil && sec.Channels.DingTalk.ClientSecret != "" {
cfg.Channels.DingTalk.SetClientSecret(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.SetBotToken(sec.Channels.Slack.BotToken)
cfg.Channels.Slack.botToken = sec.Channels.Slack.BotToken
}
if sec.Channels.Slack.AppToken != "" {
cfg.Channels.Slack.SetAppToken(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.SetAccessToken(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.SetChannelSecret(sec.Channels.LINE.ChannelSecret)
cfg.Channels.LINE.channelSecret = sec.Channels.LINE.ChannelSecret
}
if sec.Channels.LINE.ChannelAccessToken != "" {
cfg.Channels.LINE.SetChannelAccessToken(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.SetAccessToken(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.SetToken(sec.Channels.WeCom.Token)
cfg.Channels.WeCom.token = sec.Channels.WeCom.Token
}
if sec.Channels.WeCom.EncodingAESKey != "" {
cfg.Channels.WeCom.SetEncodingAESKey(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.SetCorpSecret(sec.Channels.WeComApp.CorpSecret)
cfg.Channels.WeComApp.corpSecret = sec.Channels.WeComApp.CorpSecret
}
if sec.Channels.WeComApp.Token != "" {
cfg.Channels.WeComApp.SetToken(sec.Channels.WeComApp.Token)
cfg.Channels.WeComApp.token = sec.Channels.WeComApp.Token
}
if sec.Channels.WeComApp.EncodingAESKey != "" {
cfg.Channels.WeComApp.SetEncodingAESKey(sec.Channels.WeComApp.EncodingAESKey)
cfg.Channels.WeComApp.encodingAESKey = sec.Channels.WeComApp.EncodingAESKey
}
}
@@ -1485,7 +1513,7 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
// Handle Pico channel token
if sec.Channels.Pico != nil && sec.Channels.Pico.Token != "" {
cfg.Channels.Pico.SetToken(sec.Channels.Pico.Token)
cfg.Channels.Pico.token = sec.Channels.Pico.Token
}
// Handle IRC passwords
@@ -1503,7 +1531,7 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
// Handle QQ app secret
if sec.Channels.QQ != nil && sec.Channels.QQ.AppSecret != "" {
cfg.Channels.QQ.SetAppSecret(sec.Channels.QQ.AppSecret)
cfg.Channels.QQ.appSecret = sec.Channels.QQ.AppSecret
}
cfg.security = sec
@@ -1649,6 +1677,12 @@ func SaveConfig(path string, cfg *Config) error {
}
cfg.Channels.Discord.secDirty = false
}
if cfg.Channels.Weixin.secDirty {
cfg.security.Channels.Weixin = &WeixinSecurity{
Token: cfg.Channels.Weixin.Token(),
}
cfg.Channels.Discord.secDirty = false
}
if cfg.Channels.QQ.secDirty {
cfg.security.Channels.QQ = &QQSecurity{
AppSecret: cfg.Channels.QQ.AppSecret(),
+28
View File
@@ -88,6 +88,7 @@ type channelsConfigV0 struct {
Feishu feishuConfigV0 `json:"feishu"`
Discord discordConfigV0 `json:"discord"`
MaixCam maixcamConfigV0 `json:"maixcam"`
Weixin weixinConfigV0 `json:"weixin"`
QQ qqConfigV0 `json:"qq"`
DingTalk dingtalkConfigV0 `json:"dingtalk"`
Slack slackConfigV0 `json:"slack"`
@@ -107,6 +108,7 @@ func (v *channelsConfigV0) ToChannelsConfig() (ChannelsConfig, ChannelsSecurity)
discord, discordSecurity := v.Discord.ToDiscordConfig()
maixcam := v.MaixCam.ToMaixCamConfig()
qq, qqSecurity := v.QQ.ToQQConfig()
weixin, weixinSecurity := v.Weixin.ToWeiXinConfig()
dingtalk, dingtalkSecurity := v.DingTalk.ToDingTalkConfig()
slack, slackSecurity := v.Slack.ToSlackConfig()
matrix, matrixSecurity := v.Matrix.ToMatrixConfig()
@@ -125,6 +127,7 @@ func (v *channelsConfigV0) ToChannelsConfig() (ChannelsConfig, ChannelsSecurity)
Discord: discord,
MaixCam: maixcam,
QQ: qq,
Weixin: weixin,
DingTalk: dingtalk,
Slack: slack,
Matrix: matrix,
@@ -140,6 +143,7 @@ func (v *channelsConfigV0) ToChannelsConfig() (ChannelsConfig, ChannelsSecurity)
Feishu: &feishuSecurity,
Discord: &discordSecurity,
QQ: &qqSecurity,
Weixin: &weixinSecurity,
DingTalk: &dingtalkSecurity,
Slack: &slackSecurity,
Matrix: &matrixSecurity,
@@ -463,6 +467,30 @@ func (v *wecomConfigV0) ToWeComConfig() (WeComConfig, WeComSecurity) {
}
}
type weixinConfigV0 struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"`
Token string `json:"token" env:"PICOCLAW_CHANNELS_WEIXIN_TOKEN"`
BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"`
CDNBaseURL string `json:"cdn_base_url" env:"PICOCLAW_CHANNELS_WEIXIN_CDN_BASE_URL"`
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"`
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{
Token: v.Token,
}
}
type wecomappConfigV0 struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"`
CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"`
+42
View File
@@ -443,6 +443,13 @@ func TestDefaultConfig_CronAllowCommandEnabled(t *testing.T) {
}
}
func TestDefaultConfig_LogLevel(t *testing.T) {
cfg := DefaultConfig()
if cfg.Agents.Defaults.LogLevel != "fatal" {
t.Errorf("LogLevel = %q, want \"fatal\"", cfg.Agents.Defaults.LogLevel)
}
}
func TestLoadConfig_ExecAllowRemoteDefaultsTrueWhenUnset(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
@@ -1052,3 +1059,38 @@ func TestLoadConfig_UsesPassphraseProvider(t *testing.T) {
t.Errorf("api_key = %q, want %q", cfg.ModelList[0].APIKey(), plainKey)
}
}
func TestConfigParsesLogLevel(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json")
data := `{"version":1,"agents":{"defaults":{"log_level":"debug"}}}`
if err := os.WriteFile(cfgPath, []byte(data), 0o600); err != nil {
t.Fatalf("setup: %v", err)
}
cfg, err := LoadConfig(cfgPath)
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
if cfg.Agents.Defaults.LogLevel != "debug" {
t.Errorf("LogLevel = %q, want \"debug\"", cfg.Agents.Defaults.LogLevel)
}
}
func TestConfigLogLevelEmpty(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json")
data := `{}`
if err := os.WriteFile(cfgPath, []byte(data), 0o600); err != nil {
t.Fatalf("setup: %v", err)
}
cfg, err := LoadConfig(cfgPath)
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
// When config omits log_level, the DefaultConfig value ("fatal") is preserved.
if cfg.Agents.Defaults.LogLevel != "fatal" {
t.Errorf("LogLevel = %q, want \"fatal\"", cfg.Agents.Defaults.LogLevel)
}
}
+8
View File
@@ -29,6 +29,7 @@ func DefaultConfig() *Config {
Version: CurrentVersion,
Agents: AgentsConfig{
Defaults: AgentDefaults{
LogLevel: "fatal",
Workspace: workspacePath,
RestrictToWorkspace: true,
Provider: "",
@@ -155,6 +156,13 @@ func DefaultConfig() *Config {
WelcomeMessage: "Hello! I'm your AI assistant. How can I help you today?",
ProcessingMessage: DefaultWeComAIBotProcessingMessage,
},
Weixin: WeixinConfig{
Enabled: false,
BaseURL: "https://ilinkai.weixin.qq.com/",
CDNBaseURL: "https://novac2c.cdn.weixin.qq.com/c2c",
AllowFrom: FlexibleStringSlice{},
Proxy: "",
},
Pico: PicoConfig{
Enabled: false,
PingInterval: 30,
+5
View File
@@ -47,6 +47,7 @@ type ChannelsSecurity struct {
Telegram *TelegramSecurity `yaml:"telegram,omitempty"`
Feishu *FeishuSecurity `yaml:"feishu,omitempty"`
Discord *DiscordSecurity `yaml:"discord,omitempty"`
Weixin *WeixinSecurity `yaml:"weixin,omitempty"`
QQ *QQSecurity `yaml:"qq,omitempty"`
DingTalk *DingTalkSecurity `yaml:"dingtalk,omitempty"`
Slack *SlackSecurity `yaml:"slack,omitempty"`
@@ -74,6 +75,10 @@ type DiscordSecurity struct {
Token string `yaml:"token,omitempty" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
}
type WeixinSecurity struct {
Token string `yaml:"token,omitempty" env:"PICOCLAW_CHANNELS_WEIXIN_TOKEN"`
}
type QQSecurity struct {
AppSecret string `yaml:"app_secret,omitempty" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
}