# 账号保护配置专项分析 > 本文件保留修改前的审查证据。当前本地修复与最终验证见 [本地修复结果](LOCAL_REPAIR_RESULTS_2026-09-14.md),服务器未更新。 日期:2026-09-14。范围:当前本地源码的默认配置、应用/切换/还原、普通及批量保存、HTTP/WSS 转发和前端展示。 结论:目前主要问题是保护配置没有在所有状态和转发路径上保持一致,不是缺少更多策略参数。已通过本地真实 WSS 回显和配置服务测试复现多个缺陷。优先修复身份应用、切换原子性和配置保存,再开展策略效果对比。 本次只增加专项报告和隔离复现材料,没有修改业务代码或线上账号。没有验证真实上游模型质量、风控或封号效果。 ## 配置究竟控制什么 | 字段/层级 | 实际含义 | 需要注意 | | --- | --- | --- | | `extra.anti_degradation` | 新保护开关,DTO/界面主要按它判断 | 不是运行时逐项验收结果 | | `extra.anti_degrade` | 策略标记、并发上限、应用时间和还原快照 | 只有 mode1 使用正式 `policy_version`;不同路径的解析规则不统一 | | `protection_scope` / `protection_mode` | 能力范围与策略名称 | 两者不能代替设备身份、TLS 和语义检查的实际状态 | | `codex_fingerprint_mode` | `device`、`session`、`full` 或 `off` | 不改变账号上游权限或模型能力 | | `codex_fingerprint_seed` | 系统管理的持久随机种子 | 正常启停会保留;新建/复制会重新生成;不应显示或导出为诊断信息 | | TLS 账号配置 | 开关、内置模板或自定义模板 ID | 还受全局开关及插件路由影响 | | `concurrency` + marker 中的 `max_concurrency` | 当前并发及策略保护上限 | 保存更大并发可能被重新压回策略上限;16/8/4 是工程配置,不是上游证明的安全值 | 创建入口在 `backend/internal/repository/account_repo.go:140` 统一调用 `PrepareNewAccountProtection`: - 独立 OpenAI OAuth/Setup Token 且非随机代理:默认 mode1 v3、device、标准传输,并发取原有较低值或最高 16。 - 其他类型、影子账号、随机代理账号:默认 `generic_v1`,仅持久限制并发,不具备 mode1 同等身份和语义守卫。 - 导入/复制也走该默认逻辑,传入的保护标记、开关和种子会先被清除;不会原样保留导入文件中的保护策略。 - 已存在且没有新标记的旧账号不会只因读取而自动启用。 ## 策略对照 以下是注册表声明的目标值,不能据此认定所有运行路径已实现。 | 策略 | 身份策略 | TLS 目标 | 并发上限 | mode1 语义检查 | | --- | --- | --- | ---: | --- | | 原生基线 | 恢复应用前配置 | 恢复应用前配置 | 恢复快照 | 不因“原生基线”额外开启 | | 最小兼容 `minimal_compat` | device | standard | 8 | 无 | | 会话兼容 `session_standard` | session | standard | 8 | 无 | | 初代兼容 `legacy` | session | nodejs24 | 16 | 无 | | 兼容架构 v3 `mode1` | device | standard | 16,保留较低原值 | 有,主要在 Responses HTTP/WS | | 完整收敛 `mode2` | full | nodejs22 | 8 | 无 | | Node.js 24 对照 `tls_node24` | device | nodejs24 | 8 | 无 | | 低并发稳定 `low_concurrency` | session | standard | 4 | 无 | | 通用保护 `generic` | 保留原身份配置 | 保留原传输配置 | 16,保留较低原值 | 无 | `device` 固定设备标识;`session` 将会话 ID 收敛为账号固定值,按客户端 session 派生 thread,客户端不提供 session 时 thread 会回退到固定 session;`full` 将会话和线程都收敛为固定值。因此 session/full 是诊断变量,不应理解为“数值更高、保护更强”。 “关闭”调用的是还原快照。如果应用保护前就已手动配置指纹或 TLS,关闭后仍可能启用这些手动配置。再次打开开关使用默认策略,并不记住上次选择的 legacy/mode2。模式一与其他策略的设备 ID 派生前缀也不同,即使种子不变,切换模式也可能改变最终设备 ID;这会影响对照实验的控制变量。 ## 1. P1:原生 WSS 没有一致应用保护身份 位置:`backend/internal/service/openai_ws_v2_passthrough_adapter.go:765`、`:1032`;`openai_codex_fingerprint.go:37`、`:57`;`openai_ws_forwarder_payload.go:148`。 原生 WS 透传路径只调用按账号隔离元数据的 `applyCodexAccountIdentityClientMetadataRaw`,没有像 HTTP 路径一样解析和共享策略指纹 IDs。头部构造器却会在 mode1 下另外生成固定设备 ID。 实际本地 WSS 复现: - mode1:握手头的安装 ID 是 `6e9606b6-1807-4ada-bea7-80417e796b8a`,回显请求体的安装 ID 是 `3c8fb157-5088-4dea-a666-5be6d685c4ca`,二者不相同。这些仅为本地测试生成的标识。 - mode2、legacy:同样的请求体没有应用对应固定身份;在测试客户端只提供 body 身份、没有安装 ID 头时,上游握手头也没有补出策略安装 ID。 - 请求仍可以正常完成并收到 `response.completed`,现有 mode1 语义守卫不比较这些身份字段,因而不能发现该问题。 影响:同一配置在 HTTP 和原生 WSS 下的身份行为不同。“HTTP 正常、WS 异常”可能与此有关,但本次没有证明真实上游因此拒绝请求或降低输出质量。 解决:HTTP、HTTP→WS、原生 WS 池和透传路径使用同一策略解析结果;在生成首帧/握手头前计算稳定身份,首帧和后续帧都应用对应元数据转换。明确连接级稳定字段与每轮字段的生命周期,不通过修改 TLS 参数掩盖身份不一致。 验收:对每种策略抓取本地真实 HTTP/WSS 的头和帧体,校验设备/会话/线程规则一致;覆盖首帧、后续帧、重连、不同 API Key、账号切换及客户端缺省身份字段。 ## 2. P1:切换策略不是原子操作,失败后会留下保护关闭状态 位置:`backend/internal/service/account_anti_degrade.go:302`,尤其第 317 行附近。 当前执行顺序是“提交还原旧策略 → 再读账号 → 再提交新策略”。这两个更新各自提交和刷新调度快照,并没有包在同一事务中,也没有策略版本条件更新。 本地在第二次更新注入失败,真实保护服务返回错误后,重新读取账号得到 `AntiDegradationEnabled() == false`。前端失败分支仅显示错误,不重新加载账号,因此还可能继续显示旧策略。 影响:数据库错误、取消或并发操作可能让账号停留在原生配置;即使最终切换成功,中间提交也可能被并发请求看见。代码注释中的“安全切换”不能证明事务原子性。 解决:在单个事务内锁定账号、验证目标资格、从原始快照计算目标配置,最后一次写入并提交;增加策略 revision/CAS 校验,处理并发启停。调度 outbox 与配置在同一事务提交,缓存只发布最终状态。前端失败后重新读取权威状态。 验收:在校验、写入和提交处注入失败,账号都保留原策略及完整快照;并发应用/关闭要么有序完成,要么返回明确冲突,不能产生隐式关闭窗口。 ## 3. P1:旧账号首次启用保护可能被迟到的无标记快照覆盖 位置:`backend/internal/repository/account_protection.go:31`、`:71`;`account_repo.go:576`、`:595`。 最终保存阶段先根据“传入的旧账号对象”是否包含保护字段,决定要不要读取数据库中的保护状态。若旧请求在首次启用前读到 `extra=nil` 或没有任何保护键,`accountMayHaveManagedProtection` 返回 false,直接跳过数据库保护合并,之后整对象保存仍执行 `SetExtra(extra)`。 本地 SQL mock 边界测试已复现:数据库准备了已启用 mode1 的记录,传入无保护字段的快照后,合并函数不查询已提交状态,输出仍未启用;同样的旧快照如果显式含 `anti_degradation:false`,则会正确读取并保留当前保护。这解释了为什么现有 stale-write 用例通过仍会漏掉此场景。 解决:是否保护必须依据锁定后的数据库记录,不能依据传入对象。将当前保护字段合并进已有行锁查询,或者全部更新统一读取受管状态;身份种子也应在同一最终边界保留。 验收:真实 PostgreSQL 中,让凭据刷新/编辑先读到无标记旧行,再启用保护,最后释放迟到写入;保护、身份和并发均不得回退。本次验证了合并函数缺口,没有执行真实数据库竞态验收。 ## 4. P1/P2:不同策略受到的保存保护不一致 位置:`backend/internal/service/account_protection.go:120`;`account_mode1_protection.go:53`;`backend/internal/repository/account_protection.go:15`。 已复现的 P1:开启 mode2 后,通过普通 `UpdateAccount` 提交 `codex_fingerprint_mode=off`、`enable_tls_fingerprint=false`,保存成功;账号继续显示保护开启和 mode2,但指纹模式实际已变为 off。`ProtectionManagedKeys` 排除了 mode2,配置有效性校验也只针对 mode1。 另一个有明确源码证据的 P2:单账号 Go 合并会保护新增的最小兼容/会话兼容等策略,但用于 `UpdateExtra`/`BulkUpdate` 的 SQL 只为带版本标记的策略及 legacy 保留完整受管字段,漏掉 mode2 和新增诊断策略。批量保存可能保留策略名却覆盖其底层参数。本次未连接 PostgreSQL执行该 SQL 分支,不能把这部分称为数据库实测。 解决:所有策略从同一注册表声明受管字段和不变量;普通保存、key 合并、批量更新和后台刷新执行一致规则。对受管字段修改返回明确错误或明确回显拒绝项,避免“保存成功”但实际值不一致。 验收:对所有策略 × 所有写入入口测试,策略标记与身份/TLS/并发始终一致;mode2 不得只剩一个开启徽标。 ## 5. P2:非 mode1 的还原会丢失原 TLS 配置 位置:`backend/internal/service/account_anti_degrade.go:348`、`:443`、`:453`。 两种已复现的路径: 1. 原账号 TLS=true、绑定模板 9;应用最小兼容后关闭 TLS,再还原。模板恢复了,但开关仍为 false。还原代码只处理快照为 false,没有处理 true。 2. 原账号绑定模板 9;应用 legacy 后还原。绑定 ID 变为 0,因为应用内置模板时没有保存原 ID,而还原在快照缺少该键时直接删除它。 同一个通用还原分支也服务 generic 等非 mode1 策略,因此风险不限于 UI 中的某一个按钮。对照测试显示 mode1 自身完整快照的 TLS、模板和并发还原可以通过。 解决:统一使用完整、带字段存在信息的快照,区分“原来没有键”和“原来存在且为 false/null”。还原所有被该策略接管的字段;没有接管或没有快照的字段不能无条件删除。切换与普通关闭复用同一纯配置变换函数。 验收:每种策略从 TLS 开/关、自定义模板、内置模板和无模板状态应用再还原,除了系统管理种子/审计时间外,相关配置逐字段恢复。 ## 6. P2:generic 被当作 mode1,正常升级路径被阻断 位置:`backend/internal/service/account_anti_degrade.go:38`、`:135`;`account_mode1_protection.go:175`。 `generic` 是系统真实保存的模式,却不在 `normalizeAntiDegradeMode` 认可的策略中,因此读取时变成 mode1。随机代理 OpenAI 账号默认得到 generic;之后改成固定代理,再选择 mode1,会收到 `MODE1_NOT_ELIGIBLE / 当前模式一配置无效`。 这不是账号真的损坏,而是枚举与归一化规则不一致。已通过真实 AdminService 配置流程复现。现在绕行需要先还原/关闭再重新启用,但该操作涉及前述还原与切换缺陷,不适合作为长期方案。 解决:把 generic 作为合法持久态保留;区分默认参数与持久状态解析;对“generic → mode1”进行资格校验后原子迁移,不把未知状态伪装成 mode1。 ## 7. P2:策略适用条件只是展示字段,缺少强制校验 位置:`backend/internal/service/anti_degrade_strategies.go:18`、`account_anti_degrade.go:163`、`:302`;前端 `EditAccountModal.vue:3951`。 注册表的 `requires_openai_oauth`、`diagnostic_only` 和风险等级没有形成完整的后端执行约束;前端只按 `apply_supported` 展示按钮,没有过滤平台。已复现 Gemini API Key 账号成功应用声明为 OpenAI 专属的最小兼容策略,实际仅并发变化,却保存为对应策略名。 另外,未知模式如 `mode_typo` 会被静默改为 mode1并应用,已复现。 解决:同一资格判断服务同时供预览和应用使用,按平台、类型、影子身份、代理条件返回明确可用性;未知非空模式返回 400。诊断策略明确展示实际改变的身份维度及风险,不让未支持的平台挂上同名能力标签。 ## 8. P2:配置预览没有解释全局开关和插件覆盖后的有效传输 位置:`backend/internal/service/openai_plugin_transport.go:18`、`openai_ws_pool.go:864`、`openai_ws_v2_passthrough_adapter.go:878`;预览服务没有注入全局配置/插件状态。 已复现:legacy 预览返回 `tls_profile=nodejs24`,全局 TLS 指纹关闭时,真正发送请求却走标准传输。全局关闭生效本身符合设计;不足之处是预览只说明账号目标配置,没有显示它被全局开关屏蔽。插件路由也可能接管 HTTP 传输,不能从账号模板字段推断插件实际握手。 解决:返回 `configured`(账号配置)、`effective`(按全局开关/插件计算的计划)、`observed`(最近实际传输证据,可为空)三种状态,并标明原因。没有握手证据就显示“未验证”,不要把模板名当作观测结果。 ## 配置使用与排查建议 1. 先记录具体账号的平台/类型、是否影子、策略名与版本、当前及策略并发、指纹模式、TLS 全局与账号开关、代理模式和 WS 接入模式。分享记录时去掉 token、密码、完整代理 URL 和身份种子。 2. 针对现有故障,先区分 HTTP、HTTP→WS、原生 WSS,以及 `MODE1_CONFIGURATION_INVALID`、`MODE1_LOSSY_TRANSFORM`、上游 401/429 等错误。只有先确定失败层,策略对照才有意义。 3. 修复前不要把模式二/完整收敛视为日常默认。它会改变会话/线程关系,且本轮发现保存及原生 WSS 不一致。 4. mode1 v3 是当前独立 OpenAI 账号的代码默认;不需要为了让它“更有效”而强开实验 TLS。它的 device、会话隔离和语义检查也必须先补齐 WSS 实现。API Key/Gemini 等账号应明确显示通用并发保护。 5. “原生基线”目前是还原旧快照,不一定是所有指纹都关闭。进行基线对照前核对实际字段;保存原始配置后在隔离测试账号上验证还原行为。 6. 固定代理模式仅避免系统随机挑选代理,不保证代理供应商不换出口 IP;并发上限也不等于 TPM/RPM/上游配额控制。不要用这些配置推断账号一定不会降额或被限制。 7. 当前测试历史只有保护布尔值,缺少具体策略版本、TLS、代理和有效并发快照;应补齐执行快照后再比较质量,且保持账号、实际模型、请求内容及并发一致。 ## 推荐修复次序 第一批:统一 HTTP/WSS 的身份应用;将应用/还原/切换改成单事务;取消依赖旧快照字段的保护跳过优化。 第二批:统一所有策略及所有保存入口的受管字段规则,修复 TLS 快照还原、generic 解析和资格检查。 第三批:界面展示实际能力及全局覆盖原因,简化默认选项,把诊断策略单独标识,补充测试执行快照和跨协议验收。 建议把策略解析为一个明确的执行计划,包含:策略 ID/版本、身份模式、会话隔离规则、TLS 计划、有效并发、语义守卫范围。保存时验证一次不变量,实际出站各路径消费同一计划,避免再增加散落的 `if mode1` 特判。 ## 本次验证材料 - 隔离复现目录:`verification/account-protection-review/`,包含源码、Go overlay、说明和 service 原始日志。 - 14 个叶子用例:12 个期望未满足、2 个对照通过。多个用例对应同一类缺陷;并非“12 个线上事故”。TLS 预览用例表达的是可观测性要求。 - 配置复现使用真实保护/AdminService 和内存仓库;WSS 使用真实本地 TLS WebSocket 与帧回显;旧快照用例使用 SQL mock 验证最终保护合并边界。 - 既有 service/repository 保护相关定向回归仍通过;前端相关 3 文件、20 项测试通过。它们没有覆盖本轮新增的失败场景。 - 未连接生产数据库或真实上游,未执行生产策略切换;没有把 mock 成功或 HTTP 200 当作质量/风控保证。