From 6d3b5c0b1b9627d5f756911b79b3812f98f8d674 Mon Sep 17 00:00:00 2001 From: "bmiddle1.backend.zhangfucheng" <5286482+zhang_fucheng@user.noreply.gitee.com> Date: Sat, 22 Aug 2026 13:16:21 +0800 Subject: [PATCH] feat: add explicit account fingerprint option --- CODEX_OVERDRAFT_DEPLOYMENT_CN.md | 8 ++++++-- CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md | 3 ++- FORK_VERSION | 2 +- .../internal/service/openai_codex_fingerprint.go | 15 ++++++++++----- .../service/openai_codex_fingerprint_test.go | 13 +++++++++++++ backend/internal/service/openai_ws_pool.go | 2 +- .../components/account/BulkEditAccountModal.vue | 5 +++-- .../src/components/account/CreateAccountModal.vue | 5 +++-- .../src/components/account/EditAccountModal.vue | 7 ++++--- frontend/src/i18n/locales/en/admin/accounts.ts | 3 ++- frontend/src/i18n/locales/zh/admin/accounts.ts | 3 ++- 11 files changed, 47 insertions(+), 19 deletions(-) diff --git a/CODEX_OVERDRAFT_DEPLOYMENT_CN.md b/CODEX_OVERDRAFT_DEPLOYMENT_CN.md index 5bc63f4cc..9af079701 100644 --- a/CODEX_OVERDRAFT_DEPLOYMENT_CN.md +++ b/CODEX_OVERDRAFT_DEPLOYMENT_CN.md @@ -101,6 +101,10 @@ GATEWAY_OPENAI_ACCOUNT_UNIQUE_FINGERPRINT_ENABLED=true 它会按账号固定 Codex 设备指纹;单个账号可在 `extra` 中设置 `codex_fingerprint_mode: off` 关闭。该开关只影响 OpenAI OAuth/Codex 账号,不影响 API Key。 +账号编辑页的“Codex 指纹收敛”下拉框新增“账号唯一设备(新增,推荐)”选项。选择后会写入 +`codex_fingerprint_mode: account_device`,只固定该账号的设备指纹,不收敛会话和线程; +不选择账号级覆盖时,仍由全局开关提供相同的默认行为。 + ### 3. 创建数据目录 ```bash @@ -399,13 +403,13 @@ docker compose \ 如果服务器目录是 `/opt/sub2api`,只需替换第一条路径。升级完成后重新执行“判断功能是否生效”中的镜像、环境变量和日志检查。 -更新后可在管理后台点击刷新,版本应显示为类似 `v0.1.179-overdraft.2`,更新方式应提示源码构建使用 `git pull`。也可以直接检查容器内二进制版本: +更新后可在管理后台点击刷新,版本应显示为类似 `v0.1.179-overdraft.3`,更新方式应提示源码构建使用 `git pull`。也可以直接检查容器内二进制版本: ```bash docker exec sub2api /app/sub2api -version ``` -维护者发布新的源码版本时必须递增 `FORK_VERSION`,例如从 `0.1.177-overdraft.3` 改为 `0.1.177-overdraft.4`。同步到新的上游 Sub2API 版本时,使用 `0.1.179-overdraft.2` 这样的版本号。 +维护者发布新的源码版本时必须递增 `FORK_VERSION`,例如从 `0.1.177-overdraft.3` 改为 `0.1.177-overdraft.4`。同步到新的上游 Sub2API 版本时,使用 `0.1.179-overdraft.3` 这样的版本号。 ## 合并 Sub2API 官方更新 diff --git a/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md b/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md index 2da763d35..0f39fe2b1 100644 --- a/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md +++ b/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md @@ -11,6 +11,7 @@ - 代码默认值:关闭;`deploy/config.example.yaml` 部署示例默认开启 - OpenAI 账号固定指纹开关:`gateway.openai_account_unique_fingerprint_enabled` - 固定指纹开关默认开启;没有显式账号模式时按账号 ID 派生稳定设备指纹,账号 extra 中显式设置 `codex_fingerprint_mode: off` 可单独关闭 +- 账号编辑页新增 `账号唯一设备(新增,推荐)` 选项,对应 `codex_fingerprint_mode: account_device`;它只固定该账号的设备指纹,不改变会话/线程收敛 - Fork 版本文件:`FORK_VERSION` - 更新源:`DeanZFC/sub2api-overdraft` 的 `codex-overdraft` 分支 @@ -18,7 +19,7 @@ 源码 Docker 构建会读取根目录的 `FORK_VERSION`,并以 `BuildType=source` 写入二进制。后台更新服务通过 GitHub Contents API 读取 Fork 分支上的同名文件,使用语义化版本比较判断是否有新版本。Redis 缓存同时记录仓库和构建类型,因此旧的官方更新缓存不会继续生效。 -源码构建仅显示 `git pull` 更新提示,`PerformUpdate`、指定版本回退和在线回退列表均被禁用,防止官方二进制覆盖透支功能。维护者每次发布 Fork 更新都必须递增 `FORK_VERSION`;当前版本为 `0.1.179-overdraft.2`。 +源码构建仅显示 `git pull` 更新提示,`PerformUpdate`、指定版本回退和在线回退列表均被禁用,防止官方二进制覆盖透支功能。维护者每次发布 Fork 更新都必须递增 `FORK_VERSION`;当前版本为 `0.1.179-overdraft.3`。 Sub2API `v0.1.179` 继续保留原生 remote compaction v2 在 `/responses` 路径。本定制同时检查旧 Compact 路径和原生 v2 请求信号,二者都不会开启额度透支调度或注入透支请求形态。 diff --git a/FORK_VERSION b/FORK_VERSION index 540ffff00..b0c2c2171 100644 --- a/FORK_VERSION +++ b/FORK_VERSION @@ -1 +1 @@ -0.1.179-overdraft.2 +0.1.179-overdraft.3 diff --git a/backend/internal/service/openai_codex_fingerprint.go b/backend/internal/service/openai_codex_fingerprint.go index 9bf5dec1f..2f7176333 100644 --- a/backend/internal/service/openai_codex_fingerprint.go +++ b/backend/internal/service/openai_codex_fingerprint.go @@ -88,6 +88,10 @@ const ( // 账号 extra 未显式配置模式时,GetCodexFingerprintMode 返回此值; // 出站请求是否按全局开关提升到 device 模式由 resolveCodexFingerprintMode 决定。 codexFingerprintOff codexFingerprintMode = "off" + // codexFingerprintAccountDevice 使用账号 ID(已有系统种子优先)派生 + // 唯一且稳定的设备指纹,仅收敛 installation_id。这是全局默认行为的 + // 显式账号级选项,不要求把系统种子写入账号 extra。 + codexFingerprintAccountDevice codexFingerprintMode = "account_device" // codexFingerprintDevice 仅收敛 installation_id 为账号级恒定值。 // 上游看到 1 台设备 + 多会话(每用户各自的 session)。 codexFingerprintDevice codexFingerprintMode = "device" @@ -137,7 +141,7 @@ func codexFingerprintModeFromExtra(extra map[string]any) codexFingerprintMode { } raw, _ := extra[codexFingerprintModeExtraKey].(string) switch codexFingerprintMode(strings.TrimSpace(raw)) { - case codexFingerprintOff, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull: + case codexFingerprintOff, codexFingerprintAccountDevice, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull: return codexFingerprintMode(strings.TrimSpace(raw)) default: return codexFingerprintOff @@ -239,7 +243,8 @@ func resolveCodexFingerprintMode(account *Account, enabled bool) (codexFingerpri } if account.Extra != nil { if _, configured := account.Extra[codexFingerprintModeExtraKey]; configured { - return codexFingerprintModeFromExtra(account.Extra), false + mode := codexFingerprintModeFromExtra(account.Extra) + return mode, mode == codexFingerprintAccountDevice } } if enabled { @@ -361,7 +366,7 @@ func resolveCodexFingerprintIDsWithSeed(account *Account, clientSessionID string } switch mode { - case codexFingerprintDevice: + case codexFingerprintAccountDevice, codexFingerprintDevice: return ids case codexFingerprintSession: @@ -433,7 +438,7 @@ func applyCodexFingerprintHeaders(h http.Header, ids *codexFingerprintIDs) { // 所有非 off 模式都收敛 installation_id h.Set("x-codex-installation-id", ids.installationID) - if ids.mode == codexFingerprintDevice { + if ids.mode == codexFingerprintAccountDevice || ids.mode == codexFingerprintDevice { rewriteCodexTurnMetadataFields(h, map[string]any{ "installation_id": ids.installationID, }) @@ -519,7 +524,7 @@ func applyCodexFingerprintToClientMetadataMap(existing map[string]any, ids *code modified = true } - if ids.mode == codexFingerprintDevice { + if ids.mode == codexFingerprintAccountDevice || ids.mode == codexFingerprintDevice { rewriteClientMetadataEmbeddedTurnMetadata(existing, map[string]any{ "installation_id": ids.installationID, }) diff --git a/backend/internal/service/openai_codex_fingerprint_test.go b/backend/internal/service/openai_codex_fingerprint_test.go index 338a0fb5f..45ffaa55f 100644 --- a/backend/internal/service/openai_codex_fingerprint_test.go +++ b/backend/internal/service/openai_codex_fingerprint_test.go @@ -70,6 +70,7 @@ func TestGetCodexFingerprintMode(t *testing.T) { {"空值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: ""}), codexFingerprintOff}, {"非法值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "invalid"}), codexFingerprintOff}, {"显式 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"}), codexFingerprintOff}, + {"账号唯一设备", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "account_device"}), codexFingerprintAccountDevice}, {"device", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "device"}), codexFingerprintDevice}, {"session", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "session"}), codexFingerprintSession}, {"full", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "full"}), codexFingerprintFull}, @@ -81,6 +82,18 @@ func TestGetCodexFingerprintMode(t *testing.T) { } } +func TestResolveCodexFingerprintIDsFromRequest_AccountDeviceModeUsesStableAccountSeed(t *testing.T) { + account := newTestOAuthAccount(904, map[string]any{codexFingerprintModeExtraKey: "account_device"}) + first := resolveCodexFingerprintIDsFromRequest(account, nil) + second := resolveCodexFingerprintIDsFromRequest(account, nil) + require.NotNil(t, first) + require.NotNil(t, second) + assert.Equal(t, codexFingerprintAccountDevice, first.mode) + assert.Equal(t, first.installationID, second.installationID) + assert.Equal(t, resolveConvergedInstallationID(account, deriveAccountCodexFingerprintSeed(account)), first.installationID) + assert.Empty(t, account.Extra[codexFingerprintSeedExtraKey], "账号唯一设备模式不需要写入系统种子") +} + // --- resolveConvergedInstallationID --- func TestResolveConvergedInstallationID_UsesDeviceID(t *testing.T) { diff --git a/backend/internal/service/openai_ws_pool.go b/backend/internal/service/openai_ws_pool.go index b9a941b20..9a38e64fe 100644 --- a/backend/internal/service/openai_ws_pool.go +++ b/backend/internal/service/openai_ws_pool.go @@ -2040,7 +2040,7 @@ func normalizeOpenAIWSHandshakeCompatibility(account *Account, headers http.Head } else { key.codexInstallationID = normalizeOpenAIWSStableIdentityHeader(headers, "x-codex-installation-id") } - if mode == codexFingerprintDevice { + if mode == codexFingerprintAccountDevice || mode == codexFingerprintDevice { return key } key.sessionIDHyphen = normalizeOpenAIWSStableIdentityHeader(headers, "session-id") diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 057619424..72b9557e9 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -1705,11 +1705,12 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OF const upstreamBillingAutoProbeMode = ref<'enabled' | 'disabled'>('enabled') const codexCLIOnlyEnabled = ref(false) const codexCLIOnlyAppServerEnabled = ref(false) -type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full' +type CodexFingerprintMode = 'off' | 'account_device' | 'device' | 'session' | 'full' const enableCodexFingerprintMode = ref(false) const codexFingerprintMode = ref('off') const codexFingerprintModeOptions = computed(() => [ { value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') }, + { value: 'account_device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintAccountDevice') }, { value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') }, { value: 'session' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintSession') }, { value: 'full' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintFull') }, @@ -2089,7 +2090,7 @@ const buildUpdatePayload = (): Record | null => { if (enableCodexFingerprintMode.value) { const extra = ensureExtra() - // off = 默认值,清键即可;device/session/full 是显式 opt-in,必须落键(#5610)。 + // off = 默认值,清键即可;account_device/device/session/full 是显式 opt-in,必须落键。 if (codexFingerprintMode.value !== 'off') { extra.codex_fingerprint_mode = codexFingerprintMode.value } else { diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 2c9621f17..7a6271f83 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -4171,10 +4171,11 @@ const openaiOAuthResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OFF const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OFF) const codexCLIOnlyEnabled = ref(false) const codexCLIOnlyAppServerEnabled = ref(false) -type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full' +type CodexFingerprintMode = 'off' | 'account_device' | 'device' | 'session' | 'full' const codexFingerprintMode = ref('off') const codexFingerprintModeOptions = computed(() => [ { value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') }, + { value: 'account_device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintAccountDevice') }, { value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') }, { value: 'session' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintSession') }, { value: 'full' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintFull') }, @@ -5164,7 +5165,7 @@ const buildOpenAIExtra = (base?: Record): Record(OPENAI_WS_MODE_OFF const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OFF) const codexCLIOnlyEnabled = ref(false) const codexCLIOnlyAppServerEnabled = ref(false) -type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full' +type CodexFingerprintMode = 'off' | 'account_device' | 'device' | 'session' | 'full' const codexFingerprintMode = ref('off') type CodexImageToolMode = 'inherit' | 'enabled' | 'disabled' | 'block' const codexImageToolMode = ref('inherit') @@ -3203,6 +3203,7 @@ const editWeeklyResetHour = ref(null) const editResetTimezone = ref(null) const codexFingerprintModeOptions = computed(() => [ { value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') }, + { value: 'account_device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintAccountDevice') }, { value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') }, { value: 'session' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintSession') }, { value: 'full' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintFull') }, @@ -3698,7 +3699,7 @@ const syncFormFromAccount = (newAccount: Account | null) => { if (newAccount.type === 'oauth') { const fpMode = extra?.codex_fingerprint_mode as string | undefined // 缺省/非法值按 off 呈现,与后端 GetCodexFingerprintMode 的 opt-in 语义一致(#5610) - codexFingerprintMode.value = (['off', 'device', 'session', 'full'].includes(fpMode || '') + codexFingerprintMode.value = (['off', 'account_device', 'device', 'session', 'full'].includes(fpMode || '') ? fpMode as CodexFingerprintMode : 'off') } @@ -5117,7 +5118,7 @@ const handleSubmit = async () => { } } - // 指纹收敛模式:默认 off(不写入);device/session/full 是显式 opt-in, + // 指纹收敛模式:默认 off(不写入);account_device/device/session/full 是显式 opt-in, // 必须落键,否则管理员的选择会被后端当作"未设置"而回落到 off(#5610)。 if (props.account.type === 'oauth') { if (codexFingerprintMode.value !== 'off') { diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index e39a06712..9fdfb85ee 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -612,8 +612,9 @@ export default { codexCLIOnlyAppServerDesc: "Effective only when the switch above is on. When enabled, this account also allows third-party clients that embed the Codex engine over the app-server protocol (e.g. Claude Code's codex plugin); they still pass the global engine-fingerprint gate. OR-combined with the global app-server toggle.", codexFingerprintMode: 'Codex fingerprint convergence', - codexFingerprintModeDesc: 'When multiple users share the same OAuth account, converge device/session identifiers to account-level stable values to reduce upstream-visible device and session count. Off by default (client identifiers pass through as-is); opt in explicitly when needed. Some accounts reported quota shrinkage after enabling convergence, so choose based on your own measurements.', + codexFingerprintModeDesc: 'When multiple users share the same OAuth account, converge device/session identifiers to account-level stable values. The global account-unique device fingerprint is enabled by default; use this field for an explicit per-account override.', codexFingerprintOff: 'Off (passthrough, default)', + codexFingerprintAccountDevice: 'Account-unique device (new, recommended)', codexFingerprintDevice: 'Device only', codexFingerprintSession: 'Device + Session', codexFingerprintFull: 'Full convergence', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 20b71aa44..217c7e3aa 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -682,8 +682,9 @@ export default { codexCLIOnlyAppServer: '允许 Codex app-server 客户端', codexCLIOnlyAppServerDesc: '仅在上方开关开启时生效。开启后本账号额外放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件),仍需通过全局引擎指纹门;与全局 app-server 开关取 OR(任一开即放行)。', codexFingerprintMode: 'Codex 指纹收敛', - codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。默认关闭(原样透传客户端标识),需要时再显式开启;部分账号开启收敛后出现过额度缩水,请按自己的实测结果选择。', + codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。全局默认已启用账号唯一设备指纹;此处可为单个账号显式覆盖。', codexFingerprintOff: '关闭(透传,默认)', + codexFingerprintAccountDevice: '账号唯一设备(新增,推荐)', codexFingerprintDevice: '仅设备', codexFingerprintSession: '设备+会话', codexFingerprintFull: '完全收敛',