mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat: add VK channel support (#2276)
* feat: add VK channel support - Add VK channel implementation using vksdk - Support text messages and media attachments - Implement Long Poll API for real-time messaging - Add group chat support with trigger prefixes - Add user whitelist (allow_from) configuration - Add VK channel documentation Files: - pkg/channels/vk/: VK channel implementation - pkg/config/config.go: Add VKConfig structure - pkg/channels/manager.go: Register VK channel - pkg/gateway/gateway.go: Import VK channel package - docs/channels/vk/: Usage documentation * test: add unit tests for VK channel - Test channel initialization with various configurations - Test allow_from whitelist functionality - Test group trigger configuration - Test max message length (4000 chars) - Test message splitting logic - Test attachment processing All tests passing ✓ * fix: resolve linting issues in VK channel - Format VKConfig struct tags to comply with golines - Remove unused mu sync.Mutex field - Remove unused stripPrefix method All tests passing ✓ * style: format VKConfig with golines - Align struct tags to match project style - Match formatting with other channel configs (Telegram, etc.) - Fix golines linting error * style: fix struct tag formatting in config.go * docs: update VK channel docs to use secure token storage * feat(vk): add voice capabilities support - Implement VoiceCapabilities() method for VK channel - Add audio_message attachment handling in processAttachments - Add comprehensive tests for voice capabilities - Support both ASR (speech-to-text) and TTS (text-to-speech) * docs: add VK channel to documentation and update voice support - Add VK channel to README.md and README.zh.md channel lists - Update VK channel documentation with voice message support - Document ASR and TTS capabilities for VK channel - Add voice transcription configuration reference
This commit is contained in:
+53
-13
@@ -296,6 +296,7 @@ type ChannelsConfig struct {
|
||||
Pico PicoConfig `json:"pico" yaml:"pico,omitempty"`
|
||||
PicoClient PicoClientConfig `json:"pico_client" yaml:"pico_client,omitempty"`
|
||||
IRC IRCConfig `json:"irc" yaml:"irc,omitempty"`
|
||||
VK VKConfig `json:"vk" yaml:"vk,omitempty"`
|
||||
}
|
||||
|
||||
// GroupTriggerConfig controls when the bot responds in group chats.
|
||||
@@ -550,6 +551,21 @@ type IRCConfig struct {
|
||||
ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-"`
|
||||
}
|
||||
|
||||
type VKConfig struct {
|
||||
Enabled bool `json:"enabled" yaml:"-" env:"PICOCLAW_CHANNELS_VK_ENABLED"`
|
||||
Token SecureString `json:"token,omitzero" yaml:"token,omitempty" env:"PICOCLAW_CHANNELS_VK_TOKEN"`
|
||||
GroupID int `json:"group_id" yaml:"-" env:"PICOCLAW_CHANNELS_VK_GROUP_ID"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" yaml:"-" env:"PICOCLAW_CHANNELS_VK_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty" yaml:"-"`
|
||||
Typing TypingConfig `json:"typing,omitempty" yaml:"-"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty" yaml:"-"`
|
||||
ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-" env:"PICOCLAW_CHANNELS_VK_REASONING_CHANNEL_ID"`
|
||||
}
|
||||
|
||||
func (c *VKConfig) SetToken(token string) {
|
||||
c.Token = *NewSecureString(token)
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
|
||||
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
||||
@@ -765,13 +781,13 @@ type WebToolsConfig struct {
|
||||
// the client-side web_search tool is hidden to avoid duplicate search surfaces,
|
||||
// and the provider's built-in search is used instead. Falls back to client-side
|
||||
// search when the provider does not support native search.
|
||||
PreferNative bool `json:"prefer_native" yaml:"-" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"`
|
||||
PreferNative bool `yaml:"-" json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"`
|
||||
// Proxy is an optional proxy URL for web tools (http/https/socks5/socks5h).
|
||||
// For authenticated proxies, prefer HTTP_PROXY/HTTPS_PROXY env vars instead of embedding credentials in config.
|
||||
Proxy string `json:"proxy,omitempty" yaml:"-" env:"PICOCLAW_TOOLS_WEB_PROXY"`
|
||||
FetchLimitBytes int64 `json:"fetch_limit_bytes,omitempty" yaml:"-" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"`
|
||||
Format string `json:"format,omitempty" yaml:"-" env:"PICOCLAW_TOOLS_WEB_FORMAT"`
|
||||
PrivateHostWhitelist FlexibleStringSlice `json:"private_host_whitelist,omitempty" yaml:"-" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"`
|
||||
Proxy string `yaml:"-" json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"`
|
||||
FetchLimitBytes int64 `yaml:"-" json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"`
|
||||
Format string `yaml:"-" json:"format,omitempty" env:"PICOCLAW_TOOLS_WEB_FORMAT"`
|
||||
PrivateHostWhitelist FlexibleStringSlice `yaml:"-" json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"`
|
||||
}
|
||||
|
||||
type CronToolsConfig struct {
|
||||
@@ -939,7 +955,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
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})
|
||||
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)
|
||||
@@ -962,7 +981,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
var cfg *Config
|
||||
switch versionInfo.Version {
|
||||
case 0:
|
||||
logger.InfoF("config migrate start", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
||||
logger.InfoF(
|
||||
"config migrate start",
|
||||
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||
)
|
||||
// Legacy config (no version field)
|
||||
v, e := loadConfigV0(data)
|
||||
if e != nil {
|
||||
@@ -970,10 +992,16 @@ func LoadConfig(path string) (*Config, error) {
|
||||
}
|
||||
cfg, e = v.Migrate()
|
||||
if e != nil {
|
||||
logger.ErrorF("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.InfoF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
||||
logger.InfoF(
|
||||
"config migrate success",
|
||||
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||
)
|
||||
err = makeBackup(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -981,7 +1009,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
// Load existing security config and merge with migrated one to prevent data loss
|
||||
secErr := loadSecurityConfig(cfg, securityPath(path))
|
||||
if secErr != nil && !os.IsNotExist(secErr) {
|
||||
logger.WarnF("failed to load existing security config during migration", map[string]any{"error": secErr})
|
||||
logger.WarnF(
|
||||
"failed to load existing security config during migration",
|
||||
map[string]any{"error": secErr},
|
||||
)
|
||||
return nil, fmt.Errorf("failed to load existing security config: %w", secErr)
|
||||
}
|
||||
defer func(cfg *Config) {
|
||||
@@ -989,7 +1020,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
}(cfg)
|
||||
case 1:
|
||||
// V1→V2 migration: infer Enabled and migrate channel config fields
|
||||
logger.InfoF("config migrate start", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
||||
logger.InfoF(
|
||||
"config migrate start",
|
||||
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||
)
|
||||
cfg, err = loadConfig(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1003,7 +1037,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
oldCfg := &configV1{Config: *cfg}
|
||||
cfg, err = oldCfg.Migrate()
|
||||
if err != nil {
|
||||
logger.ErrorF("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, err
|
||||
}
|
||||
|
||||
@@ -1015,7 +1052,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
defer func(cfg *Config) {
|
||||
_ = SaveConfig(path, cfg)
|
||||
}(cfg)
|
||||
logger.InfoF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
||||
logger.InfoF(
|
||||
"config migrate success",
|
||||
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||
)
|
||||
case CurrentVersion:
|
||||
// Current version
|
||||
cfg, err = loadConfig(data)
|
||||
|
||||
Reference in New Issue
Block a user