fix(qq): resolve golangci-lint formatting issues
- Fix interface{} → any in sync.Map Range callbacks
- Wrap long method signatures in mockQQAPI
- Remove blank lines between }) and following statements per gofumpt
This commit is contained in:
parent
d25abb59a4
commit
d3de49f3a3
5 changed files with 1153 additions and 46 deletions
1
.worktrees/qq-stability
Submodule
1
.worktrees/qq-stability
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 75ba9e83883d143fff60d511885e2b14b9cab597
|
||||||
1104
docs/superpowers/plans/2026-03-19-qq-stability-improvements.md
Normal file
1104
docs/superpowers/plans/2026-03-19-qq-stability-improvements.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -43,9 +43,9 @@ const (
|
||||||
bytesPerMiB = 1024 * 1024
|
bytesPerMiB = 1024 * 1024
|
||||||
|
|
||||||
// Reconnection constants (used as defaults when config values are 0)
|
// Reconnection constants (used as defaults when config values are 0)
|
||||||
reconnectInitialDefault = 5 * time.Second
|
reconnectInitialDefault = 5 * time.Second
|
||||||
reconnectMaxDefault = 5 * time.Minute
|
reconnectMaxDefault = 5 * time.Minute
|
||||||
reconnectMultiplier = 2.0
|
reconnectMultiplier = 2.0
|
||||||
|
|
||||||
// Retry constants (used as defaults when config values are 0)
|
// Retry constants (used as defaults when config values are 0)
|
||||||
maxRetriesDefault = 3
|
maxRetriesDefault = 3
|
||||||
|
|
@ -53,8 +53,8 @@ const (
|
||||||
retryMaxDelayDefault = 10 * time.Second
|
retryMaxDelayDefault = 10 * time.Second
|
||||||
|
|
||||||
// Rate limit defaults
|
// Rate limit defaults
|
||||||
rateLimitGroupDefault = 500 * time.Millisecond
|
rateLimitGroupDefault = 500 * time.Millisecond
|
||||||
rateLimitDirectDefault = 200 * time.Millisecond
|
rateLimitDirectDefault = 200 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
type qqAPI interface {
|
type qqAPI interface {
|
||||||
|
|
@ -96,9 +96,9 @@ type QQChannel struct {
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
|
|
||||||
// Reconnection state
|
// Reconnection state
|
||||||
reconnectMu sync.Mutex
|
reconnectMu sync.Mutex
|
||||||
reconnecting bool
|
reconnecting bool
|
||||||
stopReconnect chan struct{}
|
stopReconnect chan struct{}
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
groupRateLimiter *rateLimiter
|
groupRateLimiter *rateLimiter
|
||||||
|
|
@ -270,9 +270,9 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
backoff := c.calculateBackoff(attempt - 1)
|
backoff := c.calculateBackoff(attempt - 1)
|
||||||
logger.InfoCF("qq", "Retrying send", map[string]any{
|
logger.InfoCF("qq", "Retrying send", map[string]any{
|
||||||
"attempt": attempt,
|
"attempt": attempt,
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"backoff": backoff.String(),
|
"backoff": backoff.String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
@ -1093,15 +1093,15 @@ func (c *QQChannel) reconnect() {
|
||||||
// resetSessionState clears transient state that may be invalid after reconnection.
|
// resetSessionState clears transient state that may be invalid after reconnection.
|
||||||
func (c *QQChannel) resetSessionState() {
|
func (c *QQChannel) resetSessionState() {
|
||||||
// Clear transient state that may be invalid after reconnection
|
// Clear transient state that may be invalid after reconnection
|
||||||
c.chatType.Range(func(key, value interface{}) bool {
|
c.chatType.Range(func(key, value any) bool {
|
||||||
c.chatType.Delete(key)
|
c.chatType.Delete(key)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
c.lastMsgID.Range(func(key, value interface{}) bool {
|
c.lastMsgID.Range(func(key, value any) bool {
|
||||||
c.lastMsgID.Delete(key)
|
c.lastMsgID.Delete(key)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
c.msgSeqCounters.Range(func(key, value interface{}) bool {
|
c.msgSeqCounters.Range(func(key, value any) bool {
|
||||||
c.msgSeqCounters.Delete(key)
|
c.msgSeqCounters.Delete(key)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,14 @@ import (
|
||||||
|
|
||||||
// mockQQAPI implements qqAPI for testing
|
// mockQQAPI implements qqAPI for testing
|
||||||
type mockQQAPI struct {
|
type mockQQAPI struct {
|
||||||
callCount atomic.Int32
|
callCount atomic.Int32
|
||||||
wsCalled atomic.Bool
|
wsCalled atomic.Bool
|
||||||
postCalled atomic.Bool
|
postCalled atomic.Bool
|
||||||
lastChatID string
|
lastChatID string
|
||||||
lastChatKind string
|
lastChatKind string
|
||||||
shouldFail bool
|
shouldFail bool
|
||||||
failErr error
|
failErr error
|
||||||
failRemaining atomic.Int32
|
failRemaining atomic.Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockQQAPI) WS(ctx context.Context, params map[string]string, body string) (*dto.WebsocketAP, error) {
|
func (m *mockQQAPI) WS(ctx context.Context, params map[string]string, body string) (*dto.WebsocketAP, error) {
|
||||||
|
|
@ -35,7 +35,9 @@ func (m *mockQQAPI) WS(ctx context.Context, params map[string]string, body strin
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockQQAPI) PostGroupMessage(ctx context.Context, groupID string, msg dto.APIMessage, opt ...options.Option) (*dto.Message, error) {
|
func (m *mockQQAPI) PostGroupMessage(
|
||||||
|
ctx context.Context, groupID string, msg dto.APIMessage, opt ...options.Option,
|
||||||
|
) (*dto.Message, error) {
|
||||||
m.postCalled.Store(true)
|
m.postCalled.Store(true)
|
||||||
m.lastChatID = groupID
|
m.lastChatID = groupID
|
||||||
m.lastChatKind = "group"
|
m.lastChatKind = "group"
|
||||||
|
|
@ -51,7 +53,9 @@ func (m *mockQQAPI) PostGroupMessage(ctx context.Context, groupID string, msg dt
|
||||||
return &dto.Message{ID: "msg-123"}, nil
|
return &dto.Message{ID: "msg-123"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockQQAPI) PostC2CMessage(ctx context.Context, userID string, msg dto.APIMessage, opt ...options.Option) (*dto.Message, error) {
|
func (m *mockQQAPI) PostC2CMessage(
|
||||||
|
ctx context.Context, userID string, msg dto.APIMessage, opt ...options.Option,
|
||||||
|
) (*dto.Message, error) {
|
||||||
m.postCalled.Store(true)
|
m.postCalled.Store(true)
|
||||||
m.lastChatID = userID
|
m.lastChatID = userID
|
||||||
m.lastChatKind = "direct"
|
m.lastChatKind = "direct"
|
||||||
|
|
@ -78,12 +82,12 @@ func newTestQQChannel(t *testing.T, api *mockQQAPI) *QQChannel {
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := &QQChannel{
|
ch := &QQChannel{
|
||||||
BaseChannel: channels.NewBaseChannel("qq", cfg, nil, config.FlexibleStringSlice{"*"}),
|
BaseChannel: channels.NewBaseChannel("qq", cfg, nil, config.FlexibleStringSlice{"*"}),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
api: api,
|
api: api,
|
||||||
dedup: make(map[string]time.Time),
|
dedup: make(map[string]time.Time),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
groupRateLimiter: newRateLimiter(500 * time.Millisecond),
|
groupRateLimiter: newRateLimiter(500 * time.Millisecond),
|
||||||
directRateLimiter: newRateLimiter(200 * time.Millisecond),
|
directRateLimiter: newRateLimiter(200 * time.Millisecond),
|
||||||
}
|
}
|
||||||
ch.SetRunning(true)
|
ch.SetRunning(true)
|
||||||
|
|
@ -102,7 +106,6 @@ func TestSend_Success(t *testing.T) {
|
||||||
ChatID: "group-1",
|
ChatID: "group-1",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error, got %v", err)
|
t.Errorf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +117,7 @@ func TestSend_Success(t *testing.T) {
|
||||||
// TestSend_WithRetry tests that retries happen on transient errors
|
// TestSend_WithRetry tests that retries happen on transient errors
|
||||||
func TestSend_WithRetry(t *testing.T) {
|
func TestSend_WithRetry(t *testing.T) {
|
||||||
api := &mockQQAPI{
|
api := &mockQQAPI{
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
failErr: errors.New("connection reset"),
|
failErr: errors.New("connection reset"),
|
||||||
failRemaining: atomic.Int32{},
|
failRemaining: atomic.Int32{},
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +130,6 @@ func TestSend_WithRetry(t *testing.T) {
|
||||||
ChatID: "group-1",
|
ChatID: "group-1",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Should succeed after retries
|
// Should succeed after retries
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected success after retry, got %v", err)
|
t.Errorf("expected success after retry, got %v", err)
|
||||||
|
|
@ -140,7 +142,7 @@ func TestSend_WithRetry(t *testing.T) {
|
||||||
// TestSend_NonRetryableError tests that non-retryable errors don't retry
|
// TestSend_NonRetryableError tests that non-retryable errors don't retry
|
||||||
func TestSend_NonRetryableError(t *testing.T) {
|
func TestSend_NonRetryableError(t *testing.T) {
|
||||||
api := &mockQQAPI{
|
api := &mockQQAPI{
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
failErr: errors.New("unauthorized"),
|
failErr: errors.New("unauthorized"),
|
||||||
failRemaining: atomic.Int32{},
|
failRemaining: atomic.Int32{},
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +168,7 @@ func TestSend_NonRetryableError(t *testing.T) {
|
||||||
// TestSend_ContextCancellation tests that retries stop on context cancellation
|
// TestSend_ContextCancellation tests that retries stop on context cancellation
|
||||||
func TestSend_ContextCancellation(t *testing.T) {
|
func TestSend_ContextCancellation(t *testing.T) {
|
||||||
api := &mockQQAPI{
|
api := &mockQQAPI{
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
failErr: errors.New("timeout"),
|
failErr: errors.New("timeout"),
|
||||||
failRemaining: atomic.Int32{},
|
failRemaining: atomic.Int32{},
|
||||||
}
|
}
|
||||||
|
|
@ -414,9 +416,9 @@ func TestQQChannel_ResetSessionState(t *testing.T) {
|
||||||
// TestQQChannel_ResetSessionState_ReasoningChannel tests that reasoning channel is re-registered
|
// TestQQChannel_ResetSessionState_ReasoningChannel tests that reasoning channel is re-registered
|
||||||
func TestQQChannel_ResetSessionState_ReasoningChannel(t *testing.T) {
|
func TestQQChannel_ResetSessionState_ReasoningChannel(t *testing.T) {
|
||||||
ch := &QQChannel{
|
ch := &QQChannel{
|
||||||
BaseChannel: channels.NewBaseChannel("qq", config.QQConfig{}, nil, config.FlexibleStringSlice{"*"}),
|
BaseChannel: channels.NewBaseChannel("qq", config.QQConfig{}, nil, config.FlexibleStringSlice{"*"}),
|
||||||
config: config.QQConfig{ReasoningChannelID: "reasoning-group"},
|
config: config.QQConfig{ReasoningChannelID: "reasoning-group"},
|
||||||
dedup: make(map[string]time.Time),
|
dedup: make(map[string]time.Time),
|
||||||
groupRateLimiter: newRateLimiter(500 * time.Millisecond),
|
groupRateLimiter: newRateLimiter(500 * time.Millisecond),
|
||||||
directRateLimiter: newRateLimiter(200 * time.Millisecond),
|
directRateLimiter: newRateLimiter(200 * time.Millisecond),
|
||||||
}
|
}
|
||||||
|
|
@ -431,9 +433,9 @@ func TestQQChannel_ResetSessionState_ReasoningChannel(t *testing.T) {
|
||||||
// TestQQChannel_ConfigDrivenParams tests that config values override defaults.
|
// TestQQChannel_ConfigDrivenParams tests that config values override defaults.
|
||||||
func TestQQChannel_ConfigDrivenParams(t *testing.T) {
|
func TestQQChannel_ConfigDrivenParams(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
setup func(*config.QQConfig)
|
setup func(*config.QQConfig)
|
||||||
check func(*testing.T, *QQChannel)
|
check func(*testing.T, *QQChannel)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "reconnect initial uses config when set",
|
name: "reconnect initial uses config when set",
|
||||||
|
|
@ -490,7 +492,7 @@ func TestQQChannel_ConfigDrivenParams(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rate limiters use config values",
|
name: "rate limiters use config values",
|
||||||
setup: func(cfg *config.QQConfig) {
|
setup: func(cfg *config.QQConfig) {
|
||||||
cfg.RateLimitGroupMs = 1000
|
cfg.RateLimitGroupMs = 1000
|
||||||
cfg.RateLimitDirectMs = 500
|
cfg.RateLimitDirectMs = 500
|
||||||
|
|
|
||||||
|
|
@ -360,11 +360,11 @@ type QQConfig 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"`
|
||||||
|
|
||||||
// Stability config (all optional with sensible defaults)
|
// Stability config (all optional with sensible defaults)
|
||||||
ReconnectInitialMs int `json:"reconnect_initial_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RECONNECT_INITIAL_MS"`
|
ReconnectInitialMs int `json:"reconnect_initial_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RECONNECT_INITIAL_MS"`
|
||||||
ReconnectMaxMs int `json:"reconnect_max_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RECONNECT_MAX_MS"`
|
ReconnectMaxMs int `json:"reconnect_max_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RECONNECT_MAX_MS"`
|
||||||
MaxRetries int `json:"max_retries,omitempty" env:"PICOCLAW_CHANNELS_QQ_MAX_RETRIES"`
|
MaxRetries int `json:"max_retries,omitempty" env:"PICOCLAW_CHANNELS_QQ_MAX_RETRIES"`
|
||||||
RetryInitialDelayMs int `json:"retry_initial_delay_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RETRY_INITIAL_DELAY_MS"`
|
RetryInitialDelayMs int `json:"retry_initial_delay_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RETRY_INITIAL_DELAY_MS"`
|
||||||
RetryMaxDelayMs int `json:"retry_max_delay_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RETRY_MAX_DELAY_MS"`
|
RetryMaxDelayMs int `json:"retry_max_delay_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RETRY_MAX_DELAY_MS"`
|
||||||
RateLimitGroupMs int `json:"rate_limit_group_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RATE_LIMIT_GROUP_MS"`
|
RateLimitGroupMs int `json:"rate_limit_group_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RATE_LIMIT_GROUP_MS"`
|
||||||
RateLimitDirectMs int `json:"rate_limit_direct_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RATE_LIMIT_DIRECT_MS"`
|
RateLimitDirectMs int `json:"rate_limit_direct_ms,omitempty" env:"PICOCLAW_CHANNELS_QQ_RATE_LIMIT_DIRECT_MS"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue