refactor: remove quota section and migrate antigravity credits to advanced section

- Deleted the SectionQuota component and its associated tests, fields, and constants.
- Moved the quotaAntigravityCredits field to the SectionAdvanced component.
- Updated related constants and translations to reflect the removal of the quota section.
- Adjusted tests to ensure consistency with the new configuration structure.
- Ensured that deprecated quota switches are no longer present in the visual config and YAML parsing logic.
This commit is contained in:
Supra4E8C
2026-10-10 21:29:42 +08:00
parent c9e2f7cb0b
commit 622b011de3
28 changed files with 202 additions and 267 deletions
-3
View File
@@ -43,7 +43,6 @@ import { SectionConnectivity } from './components/sections/SectionConnectivity';
import { SectionLogging } from './components/sections/SectionLogging';
import { SectionNetwork } from './components/sections/SectionNetwork';
import { SectionPayload } from './components/sections/SectionPayload';
import { SectionQuota } from './components/sections/SectionQuota';
import { SectionStreaming } from './components/sections/SectionStreaming';
import styles from './ConfigPage.module.scss';
@@ -283,8 +282,6 @@ export function ConfigPage() {
return <SectionNetwork {...sectionProps} />;
case 'logging':
return <SectionLogging {...sectionProps} />;
case 'quota':
return <SectionQuota {...sectionProps} />;
case 'streaming':
return <SectionStreaming {...sectionProps} />;
case 'advanced':
@@ -1,4 +1,4 @@
// 8 个高频字段的唯一渲染源:SectionCommon(常用 tab)与各正典分区共用这些组件,
// 6 个高频字段的唯一渲染源:SectionCommon(常用 tab)与各正典分区共用这些组件,
// 两处渲染结构性不可能漂移(旧简单模式靠共享 JSX 常量达成同一目的)。
// 注意:只挂载激活 tab,所以设置行的 DOM id 不会重复。
@@ -138,38 +138,3 @@ export function LoggingToFileToggle({ values, disabled, onChange, wide }: Shared
/>
);
}
export function QuotaSwitchProjectToggle({ values, disabled, onChange, wide }: SharedFieldProps) {
const { t } = useTranslation();
return (
<ToggleSetting
fieldId="quotaSwitchProject"
wide={wide}
label={t('config_management.visual.sections.quota.switch_project')}
description={t('config_management.visual.sections.quota.switch_project_desc')}
checked={values.quotaSwitchProject}
disabled={disabled}
onChange={(quotaSwitchProject) => onChange({ quotaSwitchProject })}
/>
);
}
export function QuotaSwitchPreviewModelToggle({
values,
disabled,
onChange,
wide,
}: SharedFieldProps) {
const { t } = useTranslation();
return (
<ToggleSetting
fieldId="quotaSwitchPreviewModel"
wide={wide}
label={t('config_management.visual.sections.quota.switch_preview_model')}
description={t('config_management.visual.sections.quota.switch_preview_model_desc')}
checked={values.quotaSwitchPreviewModel}
disabled={disabled}
onChange={(quotaSwitchPreviewModel) => onChange({ quotaSwitchPreviewModel })}
/>
);
}
@@ -21,7 +21,7 @@ import { SectionOAuthBehavior } from './SectionOAuthBehavior';
const Icon = CONFIG_TAB_ICONS.advanced;
/** 06 高级与实验:插件源、供应商敏感词、签名缓存与请求头默认值。 */
/** 05 高级与实验:插件源、供应商敏感词、签名缓存与请求头默认值。 */
export function SectionAdvanced({
values,
validationErrors,
@@ -127,6 +127,15 @@ export function SectionAdvanced({
defaultOpen={false}
>
<FieldStack>
<SettingList>
<ToggleSetting
fieldId="quotaAntigravityCredits"
label={t('config_management.visual.sections.advanced.antigravity_credits')}
checked={values.quotaAntigravityCredits}
disabled={disabled}
onChange={(quotaAntigravityCredits) => onChange({ quotaAntigravityCredits })}
/>
</SettingList>
<SettingList
title={t('config_management.visual.sections.system.antigravity_sensitive_words')}
description={t(
@@ -11,14 +11,12 @@ import {
LoggingToFileToggle,
PortField,
ProxyUrlField,
QuotaSwitchPreviewModelToggle,
QuotaSwitchProjectToggle,
} from '../fields/sharedFields';
const Icon = CONFIG_TAB_ICONS.common;
/**
* 「常用」tab:原简单模式的 8 个高频字段,别名视图(不占分区序号)。
* 「常用」tab:原简单模式的 6 个高频字段,别名视图(不占分区序号)。
* 渲染源与正典分区共享(sharedFields),数据同为 useVisualConfig 一份状态。
*/
export function SectionCommon({
@@ -50,8 +48,6 @@ export function SectionCommon({
<SettingList>
<DebugToggle {...fieldProps} />
<LoggingToFileToggle {...fieldProps} />
<QuotaSwitchProjectToggle {...fieldProps} />
<QuotaSwitchPreviewModelToggle {...fieldProps} />
</SettingList>
</SectionCard>
);
@@ -16,7 +16,7 @@ export type SectionPayloadProps = ConfigSectionProps & {
hasPayloadValidationErrors: boolean;
};
/** 07 Payload 配置:默认值 / 原始 JSON / 覆盖 / 过滤 五个规则组。 */
/** 06 Payload 配置:默认值 / 原始 JSON / 覆盖 / 过滤 五个规则组。 */
export function SectionPayload({
values,
disabled,
@@ -1,36 +0,0 @@
import { useTranslation } from 'react-i18next';
import { CONFIG_TAB_ICONS, SECTION_INDEX_LABELS } from '../../constants';
import type { ConfigSectionProps } from '../../types';
import { SectionCard } from '../SectionCard';
import { SettingList, ToggleSetting } from '../fields/FieldPrimitives';
import { QuotaSwitchPreviewModelToggle, QuotaSwitchProjectToggle } from '../fields/sharedFields';
const Icon = CONFIG_TAB_ICONS.quota;
/** 04 配额回退:配额耗尽时的回退策略(两个开关默认 true)。 */
export function SectionQuota({ values, disabled, animateIn, onChange }: ConfigSectionProps) {
const { t } = useTranslation();
const fieldProps = { values, disabled, onChange };
return (
<SectionCard
indexLabel={SECTION_INDEX_LABELS.quota}
icon={<Icon size={16} />}
title={t('config_management.visual.sections.quota.title')}
description={t('config_management.visual.sections.quota.description')}
animateIn={animateIn}
>
<SettingList>
<QuotaSwitchProjectToggle {...fieldProps} />
<QuotaSwitchPreviewModelToggle {...fieldProps} />
<ToggleSetting
fieldId="quotaAntigravityCredits"
label={t('config_management.visual.sections.quota.antigravity_credits')}
checked={values.quotaAntigravityCredits}
disabled={disabled}
onChange={(quotaAntigravityCredits) => onChange({ quotaAntigravityCredits })}
/>
</SettingList>
</SectionCard>
);
}
@@ -7,7 +7,7 @@ import { getValidationMessage } from '../blocks/shared';
const Icon = CONFIG_TAB_ICONS.streaming;
/** 05 流式传输:keepalive 与 bootstrap 重试;nonstream-keepalive-interval 是顶层 YAML 键。 */
/** 04 流式传输:keepalive 与 bootstrap 重试;nonstream-keepalive-interval 是顶层 YAML 键。 */
export function SectionStreaming({
values,
validationErrors,
+7 -17
View File
@@ -7,7 +7,6 @@ import {
IconScrollText,
IconShield,
IconSlidersHorizontal,
IconTimer,
type IconProps,
} from '@/components/ui/icons';
import type { VisualConfigFieldPath } from '@/types/visualConfig';
@@ -16,14 +15,13 @@ import type { VisualSectionId } from './searchIndex';
/** 编辑模式:可视化表单 or YAML 源码。 */
export type ConfigEditorMode = 'visual' | 'source';
/** 顶部 tabs:'common'(常用,原简单模式的继任者)+ 7 个正典分区。 */
/** 顶部 tabs:'common'(常用,原简单模式的继任者)+ 6 个正典分区。 */
export type ConfigTabId = 'common' | VisualSectionId;
export const CONFIG_SECTION_IDS = [
'connectivity',
'network',
'logging',
'quota',
'streaming',
'advanced',
'payload',
@@ -31,15 +29,14 @@ export const CONFIG_SECTION_IDS = [
export const CONFIG_TAB_IDS: readonly ConfigTabId[] = ['common', ...CONFIG_SECTION_IDS];
/** 分区序号(01–07)。常用 tab 是别名视图,不占序号。 */
/** 分区序号(01–06)。常用 tab 是别名视图,不占序号。 */
export const SECTION_INDEX_LABELS: Record<VisualSectionId, string> = {
connectivity: '01',
network: '02',
logging: '03',
quota: '04',
streaming: '05',
advanced: '06',
payload: '07',
streaming: '04',
advanced: '05',
payload: '06',
};
export const CONFIG_TAB_ICONS: Record<ConfigTabId, ComponentType<IconProps>> = {
@@ -47,13 +44,12 @@ export const CONFIG_TAB_ICONS: Record<ConfigTabId, ComponentType<IconProps>> = {
connectivity: IconKey,
network: IconNetwork,
logging: IconScrollText,
quota: IconTimer,
streaming: IconSatellite,
advanced: IconShield,
payload: IconCode,
};
/** 常用 tab 的 8 个字段(原简单模式),渲染源与正典分区共享(fields/sharedFields.tsx)。 */
/** 常用 tab 的 6 个字段(原简单模式),渲染源与正典分区共享(fields/sharedFields.tsx)。 */
export const COMMON_FIELD_IDS = [
'host',
'port',
@@ -61,8 +57,6 @@ export const COMMON_FIELD_IDS = [
'proxyUrl',
'debug',
'loggingToFile',
'quotaSwitchProject',
'quotaSwitchPreviewModel',
] as const;
/**
@@ -81,7 +75,6 @@ export const SECTION_VALIDATION_FIELDS: Record<VisualSectionId, readonly VisualC
'videoResultAuthCacheTTL',
],
logging: ['errorLogsMaxFiles', 'logsMaxTotalSizeMb', 'redisUsageQueueRetentionSeconds'],
quota: [],
streaming: [
'streaming.keepaliveSeconds',
'streaming.bootstrapRetries',
@@ -180,15 +173,12 @@ export const FIELD_VALUE_KEYS: Record<string, readonly string[]> = {
errorLogsMaxFiles: ['errorLogsMaxFiles'],
redisUsageQueueRetentionSeconds: ['redisUsageQueueRetentionSeconds'],
usageStatisticsEnabled: ['usageStatisticsEnabled'],
// ── quota ─────────────────────────────────────────────────────────────────
quotaSwitchProject: ['quotaSwitchProject'],
quotaSwitchPreviewModel: ['quotaSwitchPreviewModel'],
quotaAntigravityCredits: ['quotaAntigravityCredits'],
// ── streaming ─────────────────────────────────────────────────────────────
streamingKeepaliveSeconds: ['streaming.keepaliveSeconds'],
streamingBootstrapRetries: ['streaming.bootstrapRetries'],
streamingNonstreamKeepalive: ['streaming.nonstreamKeepaliveInterval'],
// ── advanced ──────────────────────────────────────────────────────────────
quotaAntigravityCredits: ['quotaAntigravityCredits'],
pluginsEnabled: ['pluginsEnabled'],
pluginStoreSources: ['pluginStoreSources'],
pluginStoreAuth: ['pluginStoreAuth'],
+7 -22
View File
@@ -9,7 +9,7 @@
// extra entry anywhere fails CI.
export type VisualSectionId =
'connectivity' | 'network' | 'logging' | 'quota' | 'streaming' | 'advanced' | 'payload';
'connectivity' | 'network' | 'logging' | 'streaming' | 'advanced' | 'payload';
export interface ConfigFieldSearchEntry {
/** Stable anchor id; matches FieldAnchor's `fieldId` and the rendered DOM id. */
@@ -518,27 +518,6 @@ export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [
hintKey: L('sections.system.usage_statistics_enabled_desc'),
yamlKeys: ['observability', 'usage', 'usage-statistics-enabled'],
},
// ── quota ─────────────────────────────────────────────────────────────────
{
fieldId: 'quotaSwitchProject',
sectionId: 'quota',
labelKey: L('sections.quota.switch_project'),
hintKey: L('sections.quota.switch_project_desc'),
yamlKeys: ['quota-exceeded', 'switch-project'],
},
{
fieldId: 'quotaSwitchPreviewModel',
sectionId: 'quota',
labelKey: L('sections.quota.switch_preview_model'),
hintKey: L('sections.quota.switch_preview_model_desc'),
yamlKeys: ['quota-exceeded', 'switch-preview-model'],
},
{
fieldId: 'quotaAntigravityCredits',
sectionId: 'quota',
labelKey: L('sections.quota.antigravity_credits'),
yamlKeys: ['oauth', 'providers', 'antigravity', 'antigravity-credits'],
},
// ── streaming ─────────────────────────────────────────────────────────────
{
fieldId: 'streamingKeepaliveSeconds',
@@ -562,6 +541,12 @@ export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [
yamlKeys: ['requests', 'nonstream-keepalive-interval'],
},
// ── advanced ──────────────────────────────────────────────────────────────
{
fieldId: 'quotaAntigravityCredits',
sectionId: 'advanced',
labelKey: L('sections.advanced.antigravity_credits'),
yamlKeys: ['oauth', 'providers', 'antigravity', 'antigravity-credits'],
},
{
fieldId: 'pluginsEnabled',
sectionId: 'advanced',
+6 -28
View File
@@ -1250,8 +1250,6 @@ function getNextDirtyFields(
'maxRetryCredentials',
'maxRetryInterval',
'wsAuth',
'quotaSwitchProject',
'quotaSwitchPreviewModel',
'quotaAntigravityCredits',
'routingStrategy',
'routingSessionAffinity',
@@ -1426,7 +1424,6 @@ function parseVisualValuesFromYaml(yamlContent: string): VisualConfigValues {
const v8ObservabilityUsage = asRecord(v8Observability?.['usage']);
const tls = asRecord(v8Server?.['tls']);
const remoteManagement = asRecord(parsed['management']);
const quotaExceeded = asRecord(parsed['quota-exceeded']);
const routing = asRecord(parsed.routing);
const payload = asRecord(v8Requests?.['payload']);
const streaming = asRecord(v8Requests?.['streaming']);
@@ -1524,12 +1521,6 @@ function parseVisualValuesFromYaml(yamlContent: string): VisualConfigValues {
? codexHeaderDefaults['beta-features']
: '',
quotaSwitchProject: Boolean(
quotaExceeded?.['switch-project'] ?? DEFAULT_VISUAL_VALUES.quotaSwitchProject
),
quotaSwitchPreviewModel: Boolean(
quotaExceeded?.['switch-preview-model'] ?? DEFAULT_VISUAL_VALUES.quotaSwitchPreviewModel
),
quotaAntigravityCredits: Boolean(antigravity?.['antigravity-credits'] ?? false),
routingStrategy: parseRoutingStrategy(routing?.strategy),
@@ -1979,25 +1970,12 @@ export function useVisualConfig() {
deleteIfMapEmpty(doc, ['oauth', 'providers', 'codex', 'header-defaults']);
}
const quotaDirty =
dirtyFields.has('quotaSwitchProject') ||
dirtyFields.has('quotaSwitchPreviewModel') ||
dirtyFields.has('quotaAntigravityCredits');
if (quotaDirty) {
ensureMapInDoc(doc, ['quota-exceeded']);
if (dirtyFields.has('quotaSwitchProject')) {
doc.setIn(['quota-exceeded', 'switch-project'], values.quotaSwitchProject);
}
if (dirtyFields.has('quotaSwitchPreviewModel')) {
doc.setIn(['quota-exceeded', 'switch-preview-model'], values.quotaSwitchPreviewModel);
}
if (dirtyFields.has('quotaAntigravityCredits')) {
doc.setIn(
['oauth', 'providers', 'antigravity', 'antigravity-credits'],
values.quotaAntigravityCredits
);
}
deleteIfMapEmpty(doc, ['quota-exceeded']);
if (dirtyFields.has('quotaAntigravityCredits')) {
ensureMapInDoc(doc, ['oauth', 'providers', 'antigravity']);
doc.setIn(
['oauth', 'providers', 'antigravity', 'antigravity-credits'],
values.quotaAntigravityCredits
);
}
const routingDirty =
+1 -9
View File
@@ -1279,6 +1279,7 @@
"description": "Debug, log output, and usage statistics"
},
"advanced": {
"antigravity_credits": "Use Antigravity Credits",
"title": "Advanced & Experimental",
"description": "Header spoofing, signature cache, plugin sources, and other rarely changed settings",
"plugins_title": "Plugins",
@@ -1427,15 +1428,6 @@
"ws_auth": "WebSocket Authentication",
"ws_auth_desc": "Require authentication for AI Studio WebSocket connections (oauth.providers.aistudio.ws-auth)"
},
"quota": {
"title": "Quota Fallback",
"description": "Fallback strategy when quota is exceeded",
"switch_project": "Switch Project",
"switch_project_desc": "Automatically switch to another project when quota is exceeded",
"switch_preview_model": "Switch to Preview Model",
"switch_preview_model_desc": "Switch to preview model version when quota is exceeded",
"antigravity_credits": "Use Antigravity Credits"
},
"streaming": {
"title": "Streaming Configuration",
"description": "Keepalive and bootstrap retry settings",
+1 -9
View File
@@ -1279,6 +1279,7 @@
"description": "디버그, 로그 출력 및 사용량 통계"
},
"advanced": {
"antigravity_credits": "Antigravity 크레딧 사용",
"title": "고급 및 실험",
"description": "헤더 위장, 서명 캐시, 플러그인 소스 및 거의 변경되지 않는 기타 설정",
"plugins_title": "플러그인",
@@ -1427,15 +1428,6 @@
"ws_auth": "WebSocket 인증",
"ws_auth_desc": "AI Studio WebSocket 연결에 인증을 요구합니다 (oauth.providers.aistudio.ws-auth)"
},
"quota": {
"title": "할당량 폴백",
"description": "할당량이 초과되었을 때의 폴백 전략",
"switch_project": "프로젝트 전환",
"switch_project_desc": "할당량이 초과되면 다른 프로젝트로 자동 전환합니다.",
"switch_preview_model": "프리뷰 모델로 전환",
"switch_preview_model_desc": "할당량이 초과되면 프리뷰 모델 버전으로 전환합니다.",
"antigravity_credits": "Antigravity 크레딧 사용"
},
"streaming": {
"title": "스트리밍 설정",
"description": "Keepalive 및 부트스트랩 재시도 설정",
+1 -9
View File
@@ -1282,6 +1282,7 @@
"description": "Отладка, вывод логов и статистика использования"
},
"advanced": {
"antigravity_credits": "Использовать Antigravity Credits",
"title": "Дополнительно и эксперименты",
"description": "Подмена заголовков, кэш подписей, источники плагинов и другие редко изменяемые настройки",
"plugins_title": "Плагины",
@@ -1430,15 +1431,6 @@
"ws_auth": "Аутентификация WebSocket",
"ws_auth_desc": "Аутентификация WebSocket AI Studio (oauth.providers.aistudio.ws-auth)"
},
"quota": {
"title": "Резерв по квоте",
"description": "Стратегия при превышении квоты",
"switch_project": "Переключить проект",
"switch_project_desc": "Автоматически переходить на другой проект при превышении квоты",
"switch_preview_model": "Переключить на preview-модель",
"switch_preview_model_desc": "Переключаться на preview-версию модели при превышении квоты",
"antigravity_credits": "Использовать Antigravity Credits"
},
"streaming": {
"title": "Настройки стриминга",
"description": "Параметры keepalive и повторов запуска",
+1 -9
View File
@@ -1072,6 +1072,7 @@
"description": "Debug, xuất log và thống kê sử dụng"
},
"advanced": {
"antigravity_credits": "Dùng credit Antigravity",
"title": "Nâng cao & Thử nghiệm",
"description": "Giả mạo header, cache chữ ký, nguồn plugin và các cài đặt ít thay đổi khác",
"plugins_title": "Plugin",
@@ -1220,15 +1221,6 @@
"proxy_url_sponsor_hint": "Chưa có proxy phù hợp?",
"max_retry_interval_hint": "Đặt thành 0 hoặc số nguyên âm để bỏ qua thời gian chờ cooldown giữa các lượt thử lại."
},
"quota": {
"title": "Quota dự phòng",
"description": "Chiến lược dự phòng khi vượt quota",
"switch_project": "Chuyển Project",
"switch_project_desc": "Tự động chuyển sang project khác khi vượt quota",
"switch_preview_model": "Chuyển sang model Preview",
"switch_preview_model_desc": "Chuyển sang phiên bản model preview khi vượt quota",
"antigravity_credits": "Dùng credit Antigravity"
},
"streaming": {
"title": "Cấu hình Streaming",
"description": "Cài đặt keepalive và thử lại bootstrap",
+1 -9
View File
@@ -1278,6 +1278,7 @@
"description": "调试、日志输出与使用统计"
},
"advanced": {
"antigravity_credits": "使用Antigravity Credits",
"title": "高级与实验",
"description": "请求头伪装、签名缓存、插件源等不常修改的设置",
"plugins_title": "插件",
@@ -1426,15 +1427,6 @@
"ws_auth": "WebSocket 认证",
"ws_auth_desc": "启用 AI Studio WebSocket 连接认证(oauth.providers.aistudio.ws-auth)"
},
"quota": {
"title": "配额回退",
"description": "配额耗尽时的回退策略",
"switch_project": "切换项目",
"switch_project_desc": "配额耗尽时自动切换到其他项目",
"switch_preview_model": "切换预览模型",
"switch_preview_model_desc": "配额耗尽时切换到预览版本模型",
"antigravity_credits": "使用Antigravity Credits"
},
"streaming": {
"title": "流式传输配置",
"description": "Keepalive 与 bootstrap 重试设置",
+1 -9
View File
@@ -1306,6 +1306,7 @@
"description": "除錯、日誌輸出與使用統計"
},
"advanced": {
"antigravity_credits": "使用Antigravity Credits",
"title": "進階與實驗",
"description": "請求標頭偽裝、簽章快取、外掛來源等不常修改的設定",
"plugins_title": "外掛",
@@ -1454,15 +1455,6 @@
"ws_auth": "WebSocket 驗證",
"ws_auth_desc": "啟用 AI Studio WebSocket 連線驗證(oauth.providers.aistudio.ws-auth)"
},
"quota": {
"title": "配額回退",
"description": "配額耗盡時的回退策略",
"switch_project": "切換專案",
"switch_project_desc": "配額耗盡時自動切換到其他專案",
"switch_preview_model": "切換預覽模型",
"switch_preview_model_desc": "配額耗盡時切換到預覽版本模型",
"antigravity_credits": "使用Antigravity Credits"
},
"streaming": {
"title": "串流傳輸設定",
"description": "Keepalive 與 bootstrap 重試設定",
-3
View File
@@ -360,10 +360,7 @@ export const normalizeConfigResponse = (raw: unknown): Config => {
config.apiKeys = Array.isArray(keys)
? keys.filter((key): key is string => typeof key === 'string')
: [];
const quota = at('quota-exceeded');
config.quotaExceeded = {
switchProject: isRecord(quota) ? normalizeBoolean(quota['switch-project']) : false,
switchPreviewModel: isRecord(quota) ? normalizeBoolean(quota['switch-preview-model']) : false,
antigravityCredits:
normalizeBoolean(at('oauth.providers.antigravity.antigravity-credits')) ?? false,
};
-2
View File
@@ -6,8 +6,6 @@
import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from './provider';
export interface QuotaExceededConfig {
switchProject?: boolean;
switchPreviewModel?: boolean;
antigravityCredits?: boolean;
}
-4
View File
@@ -187,8 +187,6 @@ export type VisualConfigValues = {
disableImageGeneration: DisableImageGenerationMode;
gptImage2BaseModel: string;
authAutoRefreshWorkers: string;
quotaSwitchProject: boolean;
quotaSwitchPreviewModel: boolean;
/** OAuth-only: oauth.providers.antigravity.antigravity-credits. */
quotaAntigravityCredits: boolean;
routingStrategy: RoutingStrategy;
@@ -291,8 +289,6 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
disableImageGeneration: 'false',
gptImage2BaseModel: '',
authAutoRefreshWorkers: '',
quotaSwitchProject: false,
quotaSwitchPreviewModel: false,
quotaAntigravityCredits: false,
routingStrategy: 'round-robin',
routingSessionAffinity: false,
+9 -3
View File
@@ -113,7 +113,7 @@ describe('JSX anchor parity (source scan)', () => {
});
describe('registry consistency', () => {
test('tab id registry is common + the seven canonical sections', () => {
test('tab id registry is common + the canonical sections', () => {
expect([...CONFIG_TAB_IDS]).toEqual(['common', ...CONFIG_SECTION_IDS]);
});
@@ -138,13 +138,19 @@ describe('registry consistency', () => {
});
describe('v8 YAML search paths', () => {
test('deprecated quota switches are absent from values and search metadata', () => {
for (const fieldId of ['quotaSwitchProject', 'quotaSwitchPreviewModel']) {
expect(LEAF_VALUE_KEYS.has(fieldId)).toBe(false);
expect(findConfigFieldById(fieldId)).toBeUndefined();
expect(FIELD_VALUE_KEYS).not.toHaveProperty(fieldId);
}
});
for (const [fieldId, path] of [
['apiKeys', 'access.api-keys'],
['commercialMode', 'server.commercial-mode'],
['requestRetry', 'routing.retry.request-retry'],
['routingStrategy', 'routing.strategy'],
['quotaSwitchProject', 'quota-exceeded.switch-project'],
['quotaSwitchPreviewModel', 'quota-exceeded.switch-preview-model'],
['quotaAntigravityCredits', 'oauth.providers.antigravity.antigravity-credits'],
['wsAuth', 'oauth.providers.aistudio.ws-auth'],
['codexHeaderUserAgent', 'oauth.providers.codex.header-defaults.user-agent'],
+21 -5
View File
@@ -9,12 +9,28 @@ describe('v8 persisted configuration normalization', () => {
).toBe(false);
});
test('reads Antigravity credits without requiring the unrelated quota-exceeded block', () => {
test('reads only Antigravity credits from the OAuth provider path', () => {
expect(normalizeConfigResponse({}).quotaExceeded).toEqual({ antigravityCredits: false });
for (const enabled of [true, false]) {
const oauth = { providers: { antigravity: { 'antigravity-credits': enabled } } };
expect(normalizeConfigResponse({ oauth }).quotaExceeded).toEqual({
antigravityCredits: enabled,
});
const config = normalizeConfigResponse({
oauth,
'quota-exceeded': {
'switch-project': true,
'switch-preview-model': true,
'antigravity-credits': !enabled,
},
});
expect(config.quotaExceeded).toEqual({ antigravityCredits: enabled });
expect(config.quotaExceeded).not.toHaveProperty('switchProject');
expect(config.quotaExceeded).not.toHaveProperty('switchPreviewModel');
}
expect(
normalizeConfigResponse({
oauth: { providers: { antigravity: { 'antigravity-credits': true } } },
}).quotaExceeded?.antigravityCredits
).toBe(true);
normalizeConfigResponse({ 'quota-exceeded': { 'antigravity-credits': true } }).quotaExceeded
).toEqual({ antigravityCredits: false });
});
test('does not interpret legacy root settings as v8 values', () => {
+2 -2
View File
@@ -28,8 +28,8 @@ const server = {
};
describe('OAuth behavior configuration UI', () => {
test('keeps seven canonical tabs and places every addition in its designated section', () => {
expect(CONFIG_SECTION_IDS).toHaveLength(7);
test('keeps six canonical tabs and places every addition in its designated section', () => {
expect(CONFIG_SECTION_IDS).toHaveLength(6);
const network = render(createElement(SectionNetwork, props));
const advanced = render(createElement(SectionAdvanced, props));
const additions = CONFIG_FIELD_SEARCH_INDEX.filter((entry) =>
+1 -1
View File
@@ -32,7 +32,7 @@ const populated = {
describe('server configuration UI', () => {
test('keeps nine searchable anchors in connectivity without adding a tab', () => {
expect(CONFIG_SECTION_IDS).toHaveLength(7);
expect(CONFIG_SECTION_IDS).toHaveLength(6);
expect(extras).toHaveLength(9);
const markup = render(createElement(SectionConnectivity, props));
for (const entry of extras) {
-1
View File
@@ -25,7 +25,6 @@ const sections = {
.SectionConnectivity,
network: (await import('@/features/config/components/sections/SectionNetwork')).SectionNetwork,
logging: (await import('@/features/config/components/sections/SectionLogging')).SectionLogging,
quota: (await import('@/features/config/components/sections/SectionQuota')).SectionQuota,
streaming: (await import('@/features/config/components/sections/SectionStreaming'))
.SectionStreaming,
advanced: (await import('@/features/config/components/sections/SectionAdvanced')).SectionAdvanced,
+84 -4
View File
@@ -1,4 +1,16 @@
import { describe, expect, test } from 'bun:test';
import {
COMMON_FIELD_IDS,
CONFIG_SECTION_IDS,
CONFIG_TAB_IDS,
FIELD_VALUE_KEYS,
SECTION_INDEX_LABELS,
} from '@/features/config/constants';
import {
CONFIG_FIELD_SEARCH_INDEX,
findConfigFieldById,
searchConfigFields,
} from '@/features/config/searchIndex';
import {
buildHeaderMeta,
countSectionErrors,
@@ -94,7 +106,7 @@ describe('countSectionErrors', () => {
expect(counts.network).toBe(1);
expect(counts.streaming).toBe(1);
expect(counts.logging).toBe(0);
expect(counts.quota).toBe(0);
expect(counts).not.toHaveProperty('quota');
expect(counts.advanced).toBe(0);
expect(counts.payload).toBe(0);
// port 由常用 tab 渲染,同一错误在两个 tab 都要可见
@@ -144,9 +156,10 @@ describe('resolveDirtyTabs', () => {
expect(tabs.has('connectivity')).toBe(true);
expect(tabs.size).toBe(2);
const quotaTabs = resolveDirtyTabs(new Set(['quotaSwitchProject']));
expect(quotaTabs.has('common')).toBe(true);
expect(quotaTabs.has('quota')).toBe(true);
expect([...resolveDirtyTabs(new Set(['quotaAntigravityCredits']))]).toEqual(['advanced']);
expect(resolveDirtyTabs(new Set(['quotaSwitchProject', 'quotaSwitchPreviewModel'])).size).toBe(
0
);
});
test('unknown keys are ignored instead of crashing', () => {
@@ -155,6 +168,72 @@ describe('resolveDirtyTabs', () => {
});
});
describe('Antigravity credits UI and search', () => {
test('removes the quota tab and keeps section numbering contiguous', () => {
expect(CONFIG_TAB_IDS).not.toContain('quota');
expect(CONFIG_SECTION_IDS.map((id) => SECTION_INDEX_LABELS[id])).toEqual([
'01',
'02',
'03',
'04',
'05',
'06',
]);
expect(CONFIG_FIELD_SEARCH_INDEX.some((entry) => String(entry.sectionId) === 'quota')).toBe(
false
);
for (const field of ['quotaSwitchProject', 'quotaSwitchPreviewModel']) {
expect(findConfigFieldById(field)).toBeUndefined();
expect(FIELD_VALUE_KEYS).not.toHaveProperty(field);
expect(COMMON_FIELD_IDS).not.toContain(field);
}
for (const query of ['switch-project', 'switch-preview-model', 'quota-exceeded']) {
expect(searchConfigFields(query, (key) => key)).toEqual([]);
}
});
test('credits search targets Advanced in every locale', async () => {
const entry = findConfigFieldById('quotaAntigravityCredits');
expect(entry?.sectionId).toBe('advanced');
expect(entry?.labelKey).toBe('config_management.visual.sections.advanced.antigravity_credits');
for (const locale of LANGUAGE_ORDER) {
const json = await Bun.file(`src/i18n/locales/${locale}.json`).json();
const sections = json.config_management.visual.sections;
expect(sections).not.toHaveProperty('quota');
expect(typeof sections.advanced.antigravity_credits).toBe('string');
const translate = (key: string): string =>
key === entry?.labelKey ? sections.advanced.antigravity_credits : key;
for (const query of [sections.advanced.antigravity_credits, 'antigravity-credits']) {
expect(searchConfigFields(query, translate)).toContainEqual(entry);
}
}
});
test('renders credits inside the Advanced Antigravity group, not common', async () => {
const root = 'src/features/config';
const advanced = await Bun.file(`${root}/components/sections/SectionAdvanced.tsx`).text();
const group = advanced.slice(
advanced.indexOf("label={t('config_management.visual.sections.advanced.antigravity_title')}"),
advanced.indexOf("label={t('config_management.visual.sections.advanced.devin_title')}")
);
expect(group).toContain('fieldId="quotaAntigravityCredits"');
expect(group).toContain('checked={values.quotaAntigravityCredits}');
expect(group).toContain(
'onChange={(quotaAntigravityCredits) => onChange({ quotaAntigravityCredits })}'
);
expect(group).toContain('config_management.visual.sections.advanced.antigravity_credits');
for (const file of [
'components/sections/SectionCommon.tsx',
'components/fields/sharedFields.tsx',
'ConfigPage.tsx',
]) {
const source = await Bun.file(`${root}/${file}`).text();
expect(source).not.toMatch(/QuotaSwitch|quotaSwitch|SectionQuota|case 'quota'/);
}
expect(await Bun.file(`${root}/components/sections/SectionQuota.tsx`).exists()).toBe(false);
});
});
describe('buildHeaderMeta', () => {
const base = {
fieldCount: 58,
@@ -216,6 +295,7 @@ describe('localStorage readers', () => {
});
test('readSavedSection falls back to common on stale values', () => {
expect(readSavedSection('quota')).toBe('common');
expect(readSavedSection('payload')).toBe('payload');
expect(readSavedSection('common')).toBe('common');
expect(readSavedSection('server')).toBe('common'); // 历史分区 id 不再存在
-3
View File
@@ -61,9 +61,6 @@ oauth:
sensitive-words: [fixture-word]
devin:
sensitive-words: [fixture-devin]
quota-exceeded:
switch-project: false
switch-preview-model: false
multimedia:
disable-image-generation: false
gpt-image-2-base-model: fixture-base
+40 -29
View File
@@ -2,36 +2,54 @@ import { describe, expect, test } from 'bun:test';
import { parse as parseYaml } from 'yaml';
import { runVisualConfig } from './helpers/visualConfig';
// Backend contract: config_load.go defaults WebsocketAuth to true; QuotaExceeded
// uses Go's false zero values. config.example.yaml values are not missing-key defaults.
// Backend contract: WebsocketAuth defaults to true and Antigravity credits to false.
// config.example.yaml values are not missing-key defaults.
describe('visual config boolean defaults', () => {
test('initial and missing-key values agree with backend defaults', () => {
for (const config of [runVisualConfig(), runVisualConfig('server:\n port: 8317\n')]) {
expect(config.visualValues.wsAuth).toBe(true);
expect(config.visualValues.quotaSwitchProject).toBe(false);
expect(config.visualValues.quotaSwitchPreviewModel).toBe(false);
expect(config.visualValues).not.toHaveProperty('quotaSwitchProject');
expect(config.visualValues).not.toHaveProperty('quotaSwitchPreviewModel');
expect(config.visualValues.quotaAntigravityCredits).toBe(false);
expect(config.visualDirty).toBe(false);
}
});
for (const enabled of [true, false]) {
test(`preserves explicit boolean values: ${enabled}`, () => {
const yaml = `quota-exceeded:
switch-project: ${enabled}
switch-preview-model: ${enabled}
oauth:
const yaml = `oauth:
providers:
aistudio:
ws-auth: ${enabled}
antigravity:
antigravity-credits: ${enabled}
`;
const config = runVisualConfig(yaml);
expect(config.visualValues.wsAuth).toBe(enabled);
expect(config.visualValues.quotaSwitchProject).toBe(enabled);
expect(config.visualValues.quotaSwitchPreviewModel).toBe(enabled);
expect(config.visualValues.quotaAntigravityCredits).toBe(enabled);
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual(parseYaml(yaml));
});
}
test('does not expose deprecated quota switches from existing YAML', () => {
const yaml = 'quota-exceeded: {switch-project: true, switch-preview-model: true}\n';
const config = runVisualConfig(yaml, [{ quotaAntigravityCredits: true }]);
expect(config.visualValues).not.toHaveProperty('quotaSwitchProject');
expect(config.visualValues).not.toHaveProperty('quotaSwitchPreviewModel');
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({
...parseYaml(yaml),
oauth: { providers: { antigravity: { 'antigravity-credits': true } } },
});
});
test('writes credits when the OAuth provider map is null', () => {
const yaml = 'oauth: {providers: {antigravity: null}}\n';
const config = runVisualConfig(yaml, [{ quotaAntigravityCredits: true }]);
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({
oauth: { providers: { antigravity: { 'antigravity-credits': true } } },
});
});
test('writes explicit false when disabling omitted ws-auth', () => {
const yaml = 'server:\n port: 8317\n';
const config = runVisualConfig(yaml, [{ wsAuth: false }]);
@@ -42,43 +60,36 @@ oauth:
});
});
test('writes enabled quota options when their block was omitted', () => {
test('writes enabled credits without creating quota-exceeded', () => {
const yaml = 'server:\n port: 8317\n';
const config = runVisualConfig(yaml, [
{ quotaSwitchProject: true, quotaSwitchPreviewModel: true },
]);
const config = runVisualConfig(yaml, [{ quotaAntigravityCredits: true }]);
expect([...config.visualDirtyFields]).toEqual(['quotaAntigravityCredits']);
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({
server: { port: 8317 },
'quota-exceeded': { 'switch-project': true, 'switch-preview-model': true },
oauth: { providers: { antigravity: { 'antigravity-credits': true } } },
});
});
test('writes toggles in both directions without changing unrelated settings', () => {
for (const enabled of [true, false]) {
const yaml = `quota-exceeded:
switch-project: ${!enabled}
switch-preview-model: ${!enabled}
const yaml = `server:
port: 8317
oauth:
providers:
aistudio:
ws-auth: ${!enabled}
antigravity:
antigravity-credits: true
antigravity-credits: ${!enabled}
`;
const config = runVisualConfig(yaml, [
{ wsAuth: enabled, quotaSwitchProject: enabled, quotaSwitchPreviewModel: enabled },
]);
const config = runVisualConfig(yaml, [{ wsAuth: enabled, quotaAntigravityCredits: enabled }]);
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({
oauth: {
providers: {
aistudio: { 'ws-auth': enabled },
antigravity: { 'antigravity-credits': true },
antigravity: { 'antigravity-credits': enabled },
},
},
'quota-exceeded': {
'switch-project': enabled,
'switch-preview-model': enabled,
},
server: { port: 8317 },
});
}
});
@@ -86,8 +97,8 @@ oauth:
test('reverting toggles clears dirty state without inserting missing keys', () => {
const yaml = 'server:\n port: 8317\n';
const config = runVisualConfig(yaml, [
{ wsAuth: false, quotaSwitchProject: true, quotaSwitchPreviewModel: true },
{ wsAuth: true, quotaSwitchProject: false, quotaSwitchPreviewModel: false },
{ wsAuth: false, quotaAntigravityCredits: true },
{ wsAuth: true, quotaAntigravityCredits: false },
]);
expect(config.visualDirty).toBe(false);
expect(config.visualDirtyFields.size).toBe(0);
+5 -6
View File
@@ -29,6 +29,9 @@ describe('v8 visual config contract', () => {
codexHeaderUserAgent: 'fixture-oauth-agent',
streaming: { keepaliveSeconds: '0', bootstrapRetries: '0', nonstreamKeepaliveInterval: '0' },
});
expect(config.visualValues).not.toHaveProperty('quotaSwitchProject');
expect(config.visualValues).not.toHaveProperty('quotaSwitchPreviewModel');
expect(parseYaml(fixture)).not.toHaveProperty('quota-exceeded');
expect(config.visualDirty).toBe(false);
expect(parseYaml(config.applyVisualChangesToYaml(fixture))).toEqual(parseYaml(fixture));
});
@@ -43,13 +46,12 @@ describe('v8 visual config contract', () => {
expect(runVisualConfig('api-keys:\n codex: []\n').visualValues.apiKeysText).toBe('');
});
test('OAuth edits stay OAuth-only and preserve unmoved routing, quota, and plugin fields', () => {
test('OAuth edits stay OAuth-only and preserve unmoved routing and plugin fields', () => {
const config = runVisualConfig(fixture, [
{
codexHeaderUserAgent: 'new-oauth-agent',
antigravitySensitiveWords: [],
quotaAntigravityCredits: true,
quotaSwitchProject: true,
},
]);
const original = parseYaml(fixture);
@@ -57,10 +59,7 @@ describe('v8 visual config contract', () => {
expect(output['api-keys']).toEqual(original['api-keys']);
expect(output.routing).toEqual(original.routing);
expect(output.plugins).toEqual(original.plugins);
expect(output['quota-exceeded']).toEqual({
'switch-project': true,
'switch-preview-model': false,
});
expect(output).not.toHaveProperty('quota-exceeded');
expect(output.oauth.providers.codex['header-defaults']['user-agent']).toBe('new-oauth-agent');
expect(output.oauth.providers.antigravity['sensitive-words']).toBeUndefined();
expect(output.oauth.providers.antigravity['antigravity-credits']).toBe(true);