mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 20:12:39 +08:00
fix(frontend): 批量编辑显式提交 codex_fingerprint_mode=off,修复无法关闭指纹收敛
批量编辑选「关闭(默认)」时,buildUpdatePayload 先 ensureExtra() 建出 extra
再 delete 掉唯一的键,payload 退化成 {account_ids, extra:{}},被后端
len(req.Extra) > 0 判为空更新,直接 400 "No updates provided";device/session/full
则正常。即便后端放行空 extra 也无济于事:批量更新走 JSONB 顶层合并
(extra = COALESCE(extra,'{}') || payload),删掉 payload 里的键只表示
「本次不更新该键」,清不掉账号上已有的 device/session/full。
根因是把 Create/Edit 的写法直接搬到了 Bulk:那两个表单提交完整 extra 对象、
后端 SetExtra 整体覆盖,删键确实等于清除;批量接口只合并增量键,两种持久化
语义不能共用同一套写法。
改为 off 也显式落键,与本函数里其它「关闭/清除」字段一致——codex_cli_only
直接落 false(EditAccountModal 相邻注释已写明「关闭时显式写 false,避免 extra
为空被后端忽略导致旧值无法清除」)、load_factor 落 0、proxy_id 落 0。
读取侧完全等价,不改变 #5610 定下的语义:codexFingerprintModeFromExtra 对
空值/非法值走 default 回落 off,对 "off" 命中同一分支;
ShouldEnsureCodexFingerprintSeedForExtraUpdates 也只在 device/session/full
时要种子,显式 off 不会触发种子创建。
顺带给「编辑该项」勾选框补 id(同区域 Select 早有 data-testid,兄弟开关
codex_cli_only 也有 id),否则新增用例无法定位该控件。
新增 3 条用例:主复现(默认 off + 勾选编辑 ⇒ 提交 codex_fingerprint_mode=off,
并断言 extra 非空以钉死 400 那个形状)、显式 opt-in 仍原样提交、未勾选时不写入该键。
Fixes #6327
This commit is contained in:
@@ -978,6 +978,7 @@
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexFingerprintMode') }}</label>
|
||||
<input
|
||||
id="bulk-edit-openai-codex-fingerprint-mode-enabled"
|
||||
v-model="enableCodexFingerprintMode"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
@@ -2089,12 +2090,23 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
||||
|
||||
if (enableCodexFingerprintMode.value) {
|
||||
const extra = ensureExtra()
|
||||
// off = 默认值,清键即可;device/session/full 是显式 opt-in,必须落键(#5610)。
|
||||
if (codexFingerprintMode.value !== 'off') {
|
||||
extra.codex_fingerprint_mode = codexFingerprintMode.value
|
||||
} else {
|
||||
delete extra.codex_fingerprint_mode
|
||||
}
|
||||
// off 必须显式落键,不能靠删本地键表达。批量更新走 JSONB 顶层合并
|
||||
// (extra = COALESCE(extra,'{}') || payload),删掉 payload 里的键只表示
|
||||
// "本次不更新该键",清不掉账号上已有的 device/session/full;而且只删不写会让
|
||||
// 整个 payload 退化成 {extra:{}},被后端 len(req.Extra) > 0 判为空更新直接 400
|
||||
// "No updates provided"(#6327)。
|
||||
//
|
||||
// Create/Edit 那两个表单可以删键,是因为它们提交完整 extra 对象、后端整体
|
||||
// SetExtra 覆盖;批量接口只合并增量键,两种持久化语义不能共用同一套写法。
|
||||
//
|
||||
// 显式 off 与不设置在读取侧完全等价:codexFingerprintModeFromExtra 对空值/
|
||||
// 非法值走 default 回落 off,对 "off" 命中同一分支,所以 #5610 定下的
|
||||
// "不显式 opt-in 就保持旧客户端身份" 不受影响;ShouldEnsureCodexFingerprintSeed-
|
||||
// ForExtraUpdates 同样只在 device/session/full 时要种子,off 不会触发。
|
||||
//
|
||||
// 与本函数里其它"关闭/清除"字段的写法一致:codex_cli_only 直接落 false,
|
||||
// load_factor 落 0,proxy_id 落 0 —— 批量路径一律用显式哨兵值,不用省略。
|
||||
extra.codex_fingerprint_mode = codexFingerprintMode.value
|
||||
}
|
||||
|
||||
if (enableOpenAICompactMode.value) {
|
||||
|
||||
@@ -920,4 +920,75 @@ describe('BulkEditAccountModal', () => {
|
||||
status: 'active'
|
||||
})
|
||||
})
|
||||
// issue #6327:批量编辑无法把 Codex 指纹收敛关掉。
|
||||
//
|
||||
// 批量更新走 JSONB 顶层合并(extra = COALESCE(extra,'{}') || payload),删掉 payload
|
||||
// 里的键只表示「本次不更新该键」,清不掉账号已有的 device/session/full;而且只删不写会让
|
||||
// payload 退化成 {extra:{}},被后端 len(req.Extra) > 0 判为空更新直接 400
|
||||
// "No updates provided"。Create/Edit 那两个表单能删键,是因为它们提交完整 extra 对象、
|
||||
// 后端整体 SetExtra 覆盖——两种持久化语义不能共用同一套写法。
|
||||
it('OpenAI OAuth 批量编辑选择「关闭」时应显式提交 codex_fingerprint_mode=off(issue #6327)', async () => {
|
||||
const wrapper = mountModal({
|
||||
selectedPlatforms: ['openai'],
|
||||
selectedTypes: ['oauth']
|
||||
})
|
||||
|
||||
// 下拉框默认就是 off,用户只勾选「编辑该项」即提交——正是 issue 描述的操作路径。
|
||||
await wrapper.get('#bulk-edit-openai-codex-fingerprint-mode-enabled').setValue(true)
|
||||
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
|
||||
extra: {
|
||||
codex_fingerprint_mode: 'off'
|
||||
}
|
||||
})
|
||||
|
||||
// 缺陷时期的形状:extra 为空对象,后端必然回 400。显式钉死不得回退。
|
||||
const payload = vi.mocked(adminAPI.accounts.bulkUpdate).mock.calls[0][1] as {
|
||||
extra: Record<string, unknown>
|
||||
}
|
||||
expect(Object.keys(payload.extra).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// 与兄弟字段 codex_cli_only 的写法对齐:关闭态同样落显式值,不靠省略表达。
|
||||
it('OpenAI OAuth 批量编辑显式 opt-in 模式仍原样提交', async () => {
|
||||
const wrapper = mountModal({
|
||||
selectedPlatforms: ['openai'],
|
||||
selectedTypes: ['oauth']
|
||||
})
|
||||
|
||||
await wrapper.get('#bulk-edit-openai-codex-fingerprint-mode-enabled').setValue(true)
|
||||
await wrapper
|
||||
.get('[data-testid="bulk-codex-fingerprint-mode-select"]')
|
||||
.setValue('session')
|
||||
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
|
||||
extra: {
|
||||
codex_fingerprint_mode: 'session'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 未勾选「编辑该项」时不得写入该键,否则批量编辑别的字段会顺手清掉账号的收敛设置。
|
||||
it('未勾选编辑该项时不写入 codex_fingerprint_mode', async () => {
|
||||
const wrapper = mountModal({
|
||||
selectedPlatforms: ['openai'],
|
||||
selectedTypes: ['oauth']
|
||||
})
|
||||
|
||||
await wrapper.get('#bulk-edit-openai-codex-cli-only-enabled').setValue(true)
|
||||
await wrapper.get('#bulk-edit-openai-codex-cli-only-toggle').trigger('click')
|
||||
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
|
||||
extra: {
|
||||
codex_cli_only: true
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user