mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
fix(review): address Copilot PR feedback
- Add compile-time interface assertion for sessionWindowMockRepo - Fix flaky fallback test by capturing time.Now() before calling UpdateSessionWindow - Replace stale hardcoded timestamps with dynamic future values - Add millisecond detection and bounds validation for reset header timestamp - Use pause/resume pattern for interval in UsageProgressBar to avoid idle timers on large lists - Fix gofmt comment alignment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1054,14 +1054,26 @@ func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *Acc
|
|||||||
// 优先使用响应头中的真实重置时间(比预测更准确)
|
// 优先使用响应头中的真实重置时间(比预测更准确)
|
||||||
if resetStr := headers.Get("anthropic-ratelimit-unified-5h-reset"); resetStr != "" {
|
if resetStr := headers.Get("anthropic-ratelimit-unified-5h-reset"); resetStr != "" {
|
||||||
if ts, err := strconv.ParseInt(resetStr, 10, 64); err == nil {
|
if ts, err := strconv.ParseInt(resetStr, 10, 64); err == nil {
|
||||||
|
// 检测可能的毫秒时间戳(秒级约为 1e9,毫秒约为 1e12)
|
||||||
|
if ts > 1e11 {
|
||||||
|
slog.Warn("account_session_window_header_millis_detected", "account_id", account.ID, "raw_reset", resetStr)
|
||||||
|
ts = ts / 1000
|
||||||
|
}
|
||||||
end := time.Unix(ts, 0)
|
end := time.Unix(ts, 0)
|
||||||
// 窗口需要初始化,或者真实重置时间与已存储的不同,则更新
|
// 校验时间戳是否在合理范围内(不早于 5h 前,不晚于 7 天后)
|
||||||
if needInitWindow || account.SessionWindowEnd == nil || !end.Equal(*account.SessionWindowEnd) {
|
minAllowed := time.Now().Add(-5 * time.Hour)
|
||||||
|
maxAllowed := time.Now().Add(7 * 24 * time.Hour)
|
||||||
|
if end.Before(minAllowed) || end.After(maxAllowed) {
|
||||||
|
slog.Warn("account_session_window_header_out_of_range", "account_id", account.ID, "raw_reset", resetStr, "parsed_end", end)
|
||||||
|
} else if needInitWindow || account.SessionWindowEnd == nil || !end.Equal(*account.SessionWindowEnd) {
|
||||||
|
// 窗口需要初始化,或者真实重置时间与已存储的不同,则更新
|
||||||
start := end.Add(-5 * time.Hour)
|
start := end.Add(-5 * time.Hour)
|
||||||
windowStart = &start
|
windowStart = &start
|
||||||
windowEnd = &end
|
windowEnd = &end
|
||||||
slog.Info("account_session_window_from_header", "account_id", account.ID, "window_start", start, "window_end", end, "status", status)
|
slog.Info("account_session_window_from_header", "account_id", account.ID, "window_start", start, "window_end", end, "status", status)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
slog.Warn("account_session_window_header_parse_failed", "account_id", account.ID, "raw_reset", resetStr, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type sessionWindowMockRepo struct {
|
|||||||
clearRateLimitIDs []int64
|
clearRateLimitIDs []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ AccountRepository = (*sessionWindowMockRepo)(nil)
|
||||||
|
|
||||||
type swCall struct {
|
type swCall struct {
|
||||||
ID int64
|
ID int64
|
||||||
Start *time.Time
|
Start *time.Time
|
||||||
@@ -160,7 +162,7 @@ func newRateLimitServiceForTest(repo AccountRepository) *RateLimitService {
|
|||||||
func TestUpdateSessionWindow_UsesResetHeader(t *testing.T) {
|
func TestUpdateSessionWindow_UsesResetHeader(t *testing.T) {
|
||||||
// The reset header provides the real window end as a Unix timestamp.
|
// The reset header provides the real window end as a Unix timestamp.
|
||||||
// UpdateSessionWindow should use it instead of the hour-truncated prediction.
|
// UpdateSessionWindow should use it instead of the hour-truncated prediction.
|
||||||
resetUnix := int64(1771020000) // 2026-02-14T10:00:00Z
|
resetUnix := time.Now().Add(3 * time.Hour).Unix()
|
||||||
wantEnd := time.Unix(resetUnix, 0)
|
wantEnd := time.Unix(resetUnix, 0)
|
||||||
wantStart := wantEnd.Add(-5 * time.Hour)
|
wantStart := wantEnd.Add(-5 * time.Hour)
|
||||||
|
|
||||||
@@ -203,6 +205,11 @@ func TestUpdateSessionWindow_FallbackPredictionWhenNoResetHeader(t *testing.T) {
|
|||||||
headers.Set("anthropic-ratelimit-unified-5h-status", "allowed_warning")
|
headers.Set("anthropic-ratelimit-unified-5h-status", "allowed_warning")
|
||||||
// No anthropic-ratelimit-unified-5h-reset header
|
// No anthropic-ratelimit-unified-5h-reset header
|
||||||
|
|
||||||
|
// Capture now before the call to avoid hour-boundary races
|
||||||
|
now := time.Now()
|
||||||
|
expectedStart := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||||
|
expectedEnd := expectedStart.Add(5 * time.Hour)
|
||||||
|
|
||||||
svc.UpdateSessionWindow(context.Background(), account, headers)
|
svc.UpdateSessionWindow(context.Background(), account, headers)
|
||||||
|
|
||||||
if len(repo.sessionWindowCalls) != 1 {
|
if len(repo.sessionWindowCalls) != 1 {
|
||||||
@@ -214,9 +221,6 @@ func TestUpdateSessionWindow_FallbackPredictionWhenNoResetHeader(t *testing.T) {
|
|||||||
t.Fatal("expected window end to be set (fallback prediction)")
|
t.Fatal("expected window end to be set (fallback prediction)")
|
||||||
}
|
}
|
||||||
// Fallback: start = current hour truncated, end = start + 5h
|
// Fallback: start = current hour truncated, end = start + 5h
|
||||||
now := time.Now()
|
|
||||||
expectedStart := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
|
||||||
expectedEnd := expectedStart.Add(5 * time.Hour)
|
|
||||||
|
|
||||||
if !call.End.Equal(expectedEnd) {
|
if !call.End.Equal(expectedEnd) {
|
||||||
t.Errorf("expected fallback end %v, got %v", expectedEnd, *call.End)
|
t.Errorf("expected fallback end %v, got %v", expectedEnd, *call.End)
|
||||||
@@ -229,7 +233,7 @@ func TestUpdateSessionWindow_FallbackPredictionWhenNoResetHeader(t *testing.T) {
|
|||||||
func TestUpdateSessionWindow_CorrectsStalePrediction(t *testing.T) {
|
func TestUpdateSessionWindow_CorrectsStalePrediction(t *testing.T) {
|
||||||
// When the stored SessionWindowEnd is wrong (from a previous prediction),
|
// When the stored SessionWindowEnd is wrong (from a previous prediction),
|
||||||
// and the reset header provides the real time, it should update the window.
|
// and the reset header provides the real time, it should update the window.
|
||||||
staleEnd := time.Now().Add(2 * time.Hour) // existing prediction: 2h from now
|
staleEnd := time.Now().Add(2 * time.Hour) // existing prediction: 2h from now
|
||||||
realResetUnix := time.Now().Add(4 * time.Hour).Unix() // real reset: 4h from now
|
realResetUnix := time.Now().Add(4 * time.Hour).Unix() // real reset: 4h from now
|
||||||
wantEnd := time.Unix(realResetUnix, 0)
|
wantEnd := time.Unix(realResetUnix, 0)
|
||||||
|
|
||||||
@@ -291,7 +295,7 @@ func TestUpdateSessionWindow_NoUpdateWhenHeaderMatchesStored(t *testing.T) {
|
|||||||
|
|
||||||
func TestUpdateSessionWindow_ClearsUtilizationOnWindowReset(t *testing.T) {
|
func TestUpdateSessionWindow_ClearsUtilizationOnWindowReset(t *testing.T) {
|
||||||
// When needInitWindow=true and window is set, utilization should be cleared.
|
// When needInitWindow=true and window is set, utilization should be cleared.
|
||||||
resetUnix := int64(1771020000)
|
resetUnix := time.Now().Add(3 * time.Hour).Unix()
|
||||||
|
|
||||||
repo := &sessionWindowMockRepo{}
|
repo := &sessionWindowMockRepo{}
|
||||||
svc := newRateLimitServiceForTest(repo)
|
svc := newRateLimitServiceForTest(repo)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useIntervalFn } from '@vueuse/core'
|
import { useIntervalFn } from '@vueuse/core'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { WindowStats } from '@/types'
|
import type { WindowStats } from '@/types'
|
||||||
@@ -72,11 +72,28 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Reactive clock for countdown (updates every 60s)
|
// Reactive clock for countdown — only runs when a reset time is shown,
|
||||||
|
// to avoid creating many idle timers across large account lists.
|
||||||
const now = ref(new Date())
|
const now = ref(new Date())
|
||||||
useIntervalFn(() => {
|
const { pause: pauseClock, resume: resumeClock } = useIntervalFn(
|
||||||
now.value = new Date()
|
() => {
|
||||||
}, 60_000)
|
now.value = new Date()
|
||||||
|
},
|
||||||
|
60_000,
|
||||||
|
{ immediate: false },
|
||||||
|
)
|
||||||
|
if (props.resetsAt) resumeClock()
|
||||||
|
watch(
|
||||||
|
() => props.resetsAt,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
now.value = new Date()
|
||||||
|
resumeClock()
|
||||||
|
} else {
|
||||||
|
pauseClock()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Label background colors
|
// Label background colors
|
||||||
const labelClass = computed(() => {
|
const labelClass = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user