From 4c4ff363802c8236c361273ff17595b9614b8e74 Mon Sep 17 00:00:00 2001 From: zhiyu Date: Mon, 3 Aug 2026 20:55:37 +0800 Subject: [PATCH] =?UTF-8?q?perf(codex):=20=E7=89=88=E6=9C=AC=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=97=B4=E9=9A=94=E6=94=B9=E4=B8=BA=206=20=E5=B0=8F?= =?UTF-8?q?=E6=97=B6=E5=B9=B6=E5=8A=A0=E5=90=AF=E5=8A=A8=E9=98=B2=E6=8A=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同步间隔 3h → 6h:客户端版本是天级变化,6 小时足够跟上, 对 GitHub 的调用降到每天 4 次。 - 新增启动防抖:借同步设置行自身的 UpdatedAt 判断,若同步值仍在一个周期内 则跳过启动同步。频繁重启、滚动发布或崩溃重启原本会把「启动即同步」 放大成对 GitHub 的连续请求;首次部署尚无同步值时不受影响。 - 补测试:启动防抖两个方向,以及版本比较按段取数字的回归 (字典序会把 0.99.0 判为大于 0.146.0,同时让「取最大值」与 「只向前推进」两处判错)。 --- .../openai_codex_version_sync_service.go | 36 +++++++- .../openai_codex_version_sync_service_test.go | 87 +++++++++++++++++-- .../src/i18n/locales/en/admin/settings.ts | 2 +- .../src/i18n/locales/zh/admin/settings.ts | 2 +- 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/backend/internal/service/openai_codex_version_sync_service.go b/backend/internal/service/openai_codex_version_sync_service.go index cc3c37ffe5..a4e5cf8659 100644 --- a/backend/internal/service/openai_codex_version_sync_service.go +++ b/backend/internal/service/openai_codex_version_sync_service.go @@ -10,8 +10,8 @@ import ( const ( // openAICodexVersionSyncInterval 自动同步间隔。上游客户端发版频率是天级, - // 3 小时足够及时跟上,同时把对 GitHub API 的调用压到每天 8 次。 - openAICodexVersionSyncInterval = 3 * time.Hour + // 6 小时足够及时跟上,同时把对 GitHub API 的调用压到每天 4 次。 + openAICodexVersionSyncInterval = 6 * time.Hour // openAICodexVersionSyncTimeout 单次同步的整体超时。 openAICodexVersionSyncTimeout = 30 * time.Second // openAICodexVersionSyncRepo 官方 Codex 客户端仓库。 @@ -64,7 +64,7 @@ func (s *OpenAICodexVersionSyncService) Start() { ticker := time.NewTicker(s.interval) defer ticker.Stop() - s.runOnce() + s.runInitial() for { select { case <-ticker.C: @@ -86,6 +86,36 @@ func (s *OpenAICodexVersionSyncService) Stop() { s.wg.Wait() } +// runInitial 执行启动时的首次同步。若同步值在一个同步周期内已被刷新过则跳过: +// 频繁重启、滚动发布或崩溃重启会让「启动即同步」放大成对 GitHub 的连续请求, +// 而版本号是天级变化的,重启后没有立刻重新拉取的必要。 +func (s *OpenAICodexVersionSyncService) runInitial() { + if s.syncedWithinInterval() { + return + } + s.runOnce() +} + +// syncedWithinInterval 判断已同步值是否仍在一个同步周期内。 +// 借设置行自身的 UpdatedAt 判断,无需额外记录时间戳的设置项。 +// 读取失败或尚无有效同步值时返回 false,让启动同步照常执行。 +func (s *OpenAICodexVersionSyncService) syncedWithinInterval() bool { + if s.interval <= 0 { + return false + } + ctx, cancel := context.WithTimeout(context.Background(), openAICodexVersionSyncTimeout) + defer cancel() + + setting, err := s.settingRepo.Get(ctx, SettingKeyOpenAICodexClientVersionSynced) + if err != nil || setting == nil || setting.UpdatedAt.IsZero() { + return false + } + if NormalizeCodexClientVersion(setting.Value) == "" { + return false + } + return time.Since(setting.UpdatedAt) < s.interval +} + func (s *OpenAICodexVersionSyncService) runOnce() { ctx, cancel := context.WithTimeout(context.Background(), openAICodexVersionSyncTimeout) defer cancel() diff --git a/backend/internal/service/openai_codex_version_sync_service_test.go b/backend/internal/service/openai_codex_version_sync_service_test.go index b448f07a4f..586f447f2b 100644 --- a/backend/internal/service/openai_codex_version_sync_service_test.go +++ b/backend/internal/service/openai_codex_version_sync_service_test.go @@ -5,6 +5,7 @@ import ( "errors" "sync" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -14,11 +15,12 @@ import ( type codexVersionSyncSettingRepoStub struct { SettingRepository // 嵌入接口,未实现的方法会 panic(不应被调用) - mu sync.Mutex - values map[string]string - getErr error - setErr error - writes []string + mu sync.Mutex + values map[string]string + getErr error + setErr error + updatedAt time.Time + writes []string } func newCodexVersionSyncSettingRepoStub(values map[string]string) *codexVersionSyncSettingRepoStub { @@ -244,3 +246,78 @@ func TestGetOpenAICodexCanonicalUserAgentBuildsFromVersion(t *testing.T) { svc.GetOpenAICodexCanonicalUserAgent(context.Background()), ) } + +func (r *codexVersionSyncSettingRepoStub) Get(_ context.Context, key string) (*Setting, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.getErr != nil { + return nil, r.getErr + } + value, ok := r.values[key] + if !ok { + return nil, ErrSettingNotFound + } + return &Setting{Key: key, Value: value, UpdatedAt: r.updatedAt}, nil +} + +// 启动同步防抖:同步值仍在一个周期内时跳过,避免频繁重启/滚动发布把启动同步 +// 放大成对 GitHub 的连续请求。 +func TestOpenAICodexVersionSyncInitialSkipsWhenRecentlySynced(t *testing.T) { + repo := newCodexVersionSyncSettingRepoStub(map[string]string{ + SettingKeyOpenAICodexClientVersionSynced: "0.146.0", + }) + repo.updatedAt = time.Now().Add(-time.Hour) + github := &codexVersionSyncGitHubStub{releases: []*GitHubRelease{{TagName: "rust-v0.147.0"}}} + + newCodexVersionSyncService(repo, github).runInitial() + + require.Zero(t, github.calls, "同步值仍在周期内时不应请求上游") + require.Empty(t, repo.syncedWrites()) +} + +func TestOpenAICodexVersionSyncInitialRunsWhenStaleOrMissing(t *testing.T) { + t.Run("同步值已过期", func(t *testing.T) { + repo := newCodexVersionSyncSettingRepoStub(map[string]string{ + SettingKeyOpenAICodexClientVersionSynced: "0.146.0", + }) + repo.updatedAt = time.Now().Add(-7 * time.Hour) + github := &codexVersionSyncGitHubStub{releases: []*GitHubRelease{{TagName: "rust-v0.147.0"}}} + + newCodexVersionSyncService(repo, github).runInitial() + + require.Equal(t, 1, github.calls) + require.Equal(t, []string{"0.147.0"}, repo.syncedWrites()) + }) + + // 首次部署尚无同步值:必须立刻同步,不能被防抖挡住。 + t.Run("尚无同步值", func(t *testing.T) { + repo := newCodexVersionSyncSettingRepoStub(nil) + repo.updatedAt = time.Now() + github := &codexVersionSyncGitHubStub{releases: []*GitHubRelease{{TagName: "rust-v0.146.0"}}} + + newCodexVersionSyncService(repo, github).runInitial() + + require.Equal(t, 1, github.calls) + require.Equal(t, []string{"0.146.0"}, repo.syncedWrites()) + }) +} + +// 版本比较必须按段取数字:字典序会把 0.99.0 判为大于 0.146.0, +// 从而让「取最大值」和「只向前推进」两处逻辑同时判错。 +func TestCodexVersionComparisonIsNumericNotLexical(t *testing.T) { + require.Greater(t, CompareVersions("0.146.0", "0.99.0"), 0) + + require.Equal(t, "0.146.0", latestCodexStableReleaseVersion([]*GitHubRelease{ + {TagName: "rust-v0.99.0"}, + {TagName: "rust-v0.146.0"}, + })) + + repo := newCodexVersionSyncSettingRepoStub(map[string]string{ + SettingKeyOpenAICodexClientVersionSynced: "0.146.0", + }) + github := &codexVersionSyncGitHubStub{releases: []*GitHubRelease{{TagName: "rust-v0.99.0"}}} + + newCodexVersionSyncService(repo, github).runOnce() + + require.Empty(t, repo.syncedWrites(), "0.99.0 低于已同步的 0.146.0,不得写入") +} diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts index ea0298f352..a67075aa4f 100644 --- a/frontend/src/i18n/locales/en/admin/settings.ts +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -429,7 +429,7 @@ export default { openaiCodexClientVersionPlaceholder: 'Leave empty to follow auto-sync', openaiCodexClientVersionHint: 'The Codex client version this gateway declares upstream, shared by the User-Agent and the version header. Leave empty to use the auto-synced latest stable release; setting a value pins it and stops following auto-sync.', openaiCodexVersionAutoSync: 'Auto-sync Codex version', - openaiCodexVersionAutoSyncHint: 'Fetches the latest stable client version from the official repository every 3 hours, so you never need to upgrade this service just to keep the version current. When disabled, only the version above or the built-in default is used.', + openaiCodexVersionAutoSyncHint: 'Fetches the latest stable client version from the official repository every 6 hours, so you never need to upgrade this service just to keep the version current. When disabled, only the version above or the built-in default is used.', openaiCodexVersionSyncedValue: 'Currently synced: {version}', codexHardeningTitle: "Codex Settings", codexClientRestrictionTitle: "Codex client restriction", diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts index ed3c48aefd..68531375c3 100644 --- a/frontend/src/i18n/locales/zh/admin/settings.ts +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -422,7 +422,7 @@ export default { openaiCodexClientVersionPlaceholder: '留空则跟随自动同步', openaiCodexClientVersionHint: '网关对上游声明的 Codex 客户端版本号,User-Agent 与 version 头同源使用。留空表示使用自动同步到的官方最新稳定版;填写后固定为该版本,不再跟随同步。', openaiCodexVersionAutoSync: '自动同步 Codex 版本号', - openaiCodexVersionAutoSyncHint: '每 3 小时从官方仓库获取最新稳定版客户端版本号,无需为了跟版本而升级本服务。关闭后仅使用上方手填版本或内置版本。', + openaiCodexVersionAutoSyncHint: '每 6 小时从官方仓库获取最新稳定版客户端版本号,无需为了跟版本而升级本服务。关闭后仅使用上方手填版本或内置版本。', openaiCodexVersionSyncedValue: '当前同步到:{version}', codexHardeningTitle: 'Codex 设置', codexClientRestrictionTitle: 'Codex 客户端限制',