+ updateCondition(ruleIndex, modelIndex, key, conditionIndex, { value: nextValue })
+ }
+ disabled={disabled}
+ />
+ );
+ };
+
const renderParamValueEditor = (
ruleIndex: number,
paramIndex: number,
@@ -641,70 +808,303 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
{t('config_management.visual.payload_rules.models')}
- {(rule.models.length ? rule.models : []).map((model, modelIndex) => (
-
- {protocolFirst ? (
- <>
-
+ );
+ })}
updateParam(ruleIndex, paramIndex, { path: nextValue })}
+ onChange={(nextValue) =>
+ updateParam(ruleIndex, paramIndex, { path: nextValue })
+ }
disabled={disabled}
/>
{rawJsonValues ? null : (
diff --git a/src/hooks/useVisualConfig.ts b/src/hooks/useVisualConfig.ts
index c299535..b5ec5dd 100644
--- a/src/hooks/useVisualConfig.ts
+++ b/src/hooks/useVisualConfig.ts
@@ -1,7 +1,9 @@
import { useCallback, useMemo, useReducer } from 'react';
import { isMap, parse as parseYaml, parseDocument } from 'yaml';
import type {
+ DisableImageGenerationMode,
PayloadFilterRule,
+ PayloadHeaderEntry,
PayloadParamEntry,
PayloadParamValueType,
PayloadRule,
@@ -137,6 +139,24 @@ function setIntFromStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown
}
}
+function setDisableImageGenerationInDoc(
+ doc: YamlDocument,
+ path: YamlPath,
+ value: DisableImageGenerationMode
+): void {
+ if (value === 'chat') {
+ doc.setIn(path, 'chat');
+ return;
+ }
+
+ if (value === 'true') {
+ doc.setIn(path, true);
+ return;
+ }
+
+ if (docHas(doc, path)) doc.setIn(path, false);
+}
+
function getNonNegativeIntegerError(value: string): 'non_negative_integer' | undefined {
const trimmed = value.trim();
if (!trimmed) return undefined;
@@ -156,11 +176,17 @@ export function getVisualConfigValidationErrors(
values: VisualConfigValues
): VisualConfigValidationErrors {
return {
+ homePort: getPortError(values.homePort),
port: getPortError(values.port),
+ errorLogsMaxFiles: getNonNegativeIntegerError(values.errorLogsMaxFiles),
logsMaxTotalSizeMb: getNonNegativeIntegerError(values.logsMaxTotalSizeMb),
+ redisUsageQueueRetentionSeconds: getNonNegativeIntegerError(
+ values.redisUsageQueueRetentionSeconds
+ ),
requestRetry: getNonNegativeIntegerError(values.requestRetry),
maxRetryCredentials: getNonNegativeIntegerError(values.maxRetryCredentials),
maxRetryInterval: getNonNegativeIntegerError(values.maxRetryInterval),
+ authAutoRefreshWorkers: getNonNegativeIntegerError(values.authAutoRefreshWorkers),
'streaming.keepaliveSeconds': getNonNegativeIntegerError(values.streaming.keepaliveSeconds),
'streaming.bootstrapRetries': getNonNegativeIntegerError(values.streaming.bootstrapRetries),
'streaming.nonstreamKeepaliveInterval': getNonNegativeIntegerError(
@@ -201,8 +227,14 @@ export function getPayloadParamValidationError(
}
function hasPayloadParamValidationErrors(rules: PayloadRule[]): boolean {
- return rules.some((rule) =>
- rule.params.some((param) => Boolean(getPayloadParamValidationError(param)))
+ return rules.some(
+ (rule) =>
+ rule.params.some((param) => Boolean(getPayloadParamValidationError(param))) ||
+ rule.models.some(
+ (model) =>
+ (model.match ?? []).some((param) => Boolean(getPayloadParamValidationError(param))) ||
+ (model.notMatch ?? []).some((param) => Boolean(getPayloadParamValidationError(param)))
+ )
);
}
@@ -221,7 +253,19 @@ function arePayloadModelEntriesEqual(
const a = left[i];
const b = right[i];
if (!a || !b) return false;
- if (a.id !== b.id || a.name !== b.name || a.protocol !== b.protocol) return false;
+ if (
+ a.id !== b.id ||
+ a.name !== b.name ||
+ a.protocol !== b.protocol ||
+ a.fromProtocol !== b.fromProtocol
+ ) {
+ return false;
+ }
+ if (!arePayloadHeaderEntriesEqual(a.headers, b.headers)) return false;
+ if (!arePayloadParamEntriesEqual(a.match ?? [], b.match ?? [])) return false;
+ if (!arePayloadParamEntriesEqual(a.notMatch ?? [], b.notMatch ?? [])) return false;
+ if (!areStringArraysEqual(a.exist, b.exist)) return false;
+ if (!areStringArraysEqual(a.notExist, b.notExist)) return false;
}
return true;
}
@@ -243,6 +287,34 @@ function arePayloadParamEntriesEqual(
return true;
}
+function arePayloadHeaderEntriesEqual(
+ left: PayloadHeaderEntry[] | undefined,
+ right: PayloadHeaderEntry[] | undefined
+): boolean {
+ const leftEntries = left ?? [];
+ const rightEntries = right ?? [];
+ if (leftEntries === rightEntries) return true;
+ if (leftEntries.length !== rightEntries.length) return false;
+ for (let i = 0; i < leftEntries.length; i += 1) {
+ const a = leftEntries[i];
+ const b = rightEntries[i];
+ if (!a || !b) return false;
+ if (a.id !== b.id || a.name !== b.name || a.value !== b.value) return false;
+ }
+ return true;
+}
+
+function areStringArraysEqual(left: string[] | undefined, right: string[] | undefined): boolean {
+ const leftItems = left ?? [];
+ const rightItems = right ?? [];
+ if (leftItems === rightItems) return true;
+ if (leftItems.length !== rightItems.length) return false;
+ for (let i = 0; i < leftItems.length; i += 1) {
+ if (leftItems[i] !== rightItems[i]) return false;
+ }
+ return true;
+}
+
function arePayloadRulesEqual(left: PayloadRule[], right: PayloadRule[]): boolean {
if (left === right) return true;
if (left.length !== right.length) return false;
@@ -314,6 +386,63 @@ function parsePayloadProtocol(raw: unknown): string | undefined {
return raw.trim() ? raw : undefined;
}
+function parseDisableImageGenerationMode(raw: unknown): DisableImageGenerationMode {
+ if (raw === true) return 'true';
+ if (typeof raw === 'string') {
+ const normalized = raw.trim().toLowerCase();
+ if (normalized === 'true') return 'true';
+ if (normalized === 'chat') return 'chat';
+ }
+ return 'false';
+}
+
+function parsePayloadHeaders(raw: unknown, idPrefix: string): PayloadHeaderEntry[] {
+ const record = asRecord(raw);
+ if (!record) return [];
+
+ return Object.entries(record).map(([name, value], index) => ({
+ id: `${idPrefix}-header-${index}`,
+ name,
+ value: String(value ?? ''),
+ }));
+}
+
+function parsePayloadConditions(raw: unknown, idPrefix: string): PayloadParamEntry[] {
+ if (!Array.isArray(raw)) return [];
+
+ const entries: PayloadParamEntry[] = [];
+ raw.forEach((item, itemIndex) => {
+ const record = asRecord(item);
+ if (!record) {
+ if (typeof item === 'string') {
+ entries.push({
+ id: `${idPrefix}-condition-${itemIndex}-0`,
+ path: item,
+ valueType: 'string',
+ value: '',
+ });
+ }
+ return;
+ }
+
+ Object.entries(record).forEach(([path, value], valueIndex) => {
+ const parsedValue = parsePayloadParamValue(value);
+ entries.push({
+ id: `${idPrefix}-condition-${itemIndex}-${valueIndex}`,
+ path,
+ valueType: parsedValue.valueType,
+ value: parsedValue.value,
+ });
+ });
+ });
+
+ return entries;
+}
+
+function parseStringList(raw: unknown): string[] {
+ return Array.isArray(raw) ? raw.map((item) => String(item ?? '').trim()).filter(Boolean) : [];
+}
+
function deleteLegacyApiKeysProvider(doc: YamlDocument): void {
if (docHas(doc, ['auth', 'providers', 'config-api-key', 'api-key-entries'])) {
doc.deleteIn(['auth', 'providers', 'config-api-key', 'api-key-entries']);
@@ -326,26 +455,37 @@ function deleteLegacyApiKeysProvider(doc: YamlDocument): void {
deleteIfMapEmpty(doc, ['auth']);
}
+function parsePayloadModelEntries(raw: unknown, idPrefix: string): PayloadRule['models'] {
+ if (!Array.isArray(raw)) return [];
+
+ return raw.map((model, modelIndex) => {
+ const modelRecord = asRecord(model);
+ const nameRaw =
+ typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
+ const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
+ const modelId = `${idPrefix}-${modelIndex}`;
+
+ return {
+ id: modelId,
+ name,
+ protocol: parsePayloadProtocol(modelRecord?.protocol),
+ fromProtocol: parsePayloadProtocol(modelRecord?.['from-protocol']),
+ headers: parsePayloadHeaders(modelRecord?.headers, modelId),
+ match: parsePayloadConditions(modelRecord?.match, `${modelId}-match`),
+ notMatch: parsePayloadConditions(modelRecord?.['not-match'], `${modelId}-not-match`),
+ exist: parseStringList(modelRecord?.exist),
+ notExist: parseStringList(modelRecord?.['not-exist']),
+ };
+ });
+}
+
function parsePayloadRules(rules: unknown): PayloadRule[] {
if (!Array.isArray(rules)) return [];
return rules.map((rule, index) => {
const record = asRecord(rule) ?? {};
- const modelsRaw = record.models;
- const models = Array.isArray(modelsRaw)
- ? modelsRaw.map((model, modelIndex) => {
- const modelRecord = asRecord(model);
- const nameRaw =
- typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
- const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
- return {
- id: `model-${index}-${modelIndex}`,
- name,
- protocol: parsePayloadProtocol(modelRecord?.protocol),
- };
- })
- : [];
+ const models = parsePayloadModelEntries(record.models, `model-${index}`);
const paramsRecord = asRecord(record.params);
const params = paramsRecord
@@ -370,20 +510,7 @@ function parsePayloadFilterRules(rules: unknown): PayloadFilterRule[] {
return rules.map((rule, index) => {
const record = asRecord(rule) ?? {};
- const modelsRaw = record.models;
- const models = Array.isArray(modelsRaw)
- ? modelsRaw.map((model, modelIndex) => {
- const modelRecord = asRecord(model);
- const nameRaw =
- typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
- const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
- return {
- id: `filter-model-${index}-${modelIndex}`,
- name,
- protocol: parsePayloadProtocol(modelRecord?.protocol),
- };
- })
- : [];
+ const models = parsePayloadModelEntries(record.models, `filter-model-${index}`);
const paramsRaw = record.params;
const params = Array.isArray(paramsRaw) ? paramsRaw.map(String) : [];
@@ -398,20 +525,7 @@ function parseRawPayloadRules(rules: unknown): PayloadRule[] {
return rules.map((rule, index) => {
const record = asRecord(rule) ?? {};
- const modelsRaw = record.models;
- const models = Array.isArray(modelsRaw)
- ? modelsRaw.map((model, modelIndex) => {
- const modelRecord = asRecord(model);
- const nameRaw =
- typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
- const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
- return {
- id: `raw-model-${index}-${modelIndex}`,
- name,
- protocol: parsePayloadProtocol(modelRecord?.protocol),
- };
- })
- : [];
+ const models = parsePayloadModelEntries(record.models, `raw-model-${index}`);
const paramsRecord = asRecord(record.params);
const params = paramsRecord
@@ -427,34 +541,88 @@ function parseRawPayloadRules(rules: unknown): PayloadRule[] {
});
}
+function serializePayloadParamEntryValue(param: PayloadParamEntry): unknown {
+ if (param.valueType === 'number') {
+ const num = Number(param.value);
+ return Number.isFinite(num) ? num : param.value;
+ }
+ if (param.valueType === 'boolean') {
+ return param.value === 'true';
+ }
+ if (param.valueType === 'json') {
+ try {
+ return JSON.parse(param.value);
+ } catch {
+ return param.value;
+ }
+ }
+ return param.value;
+}
+
+function serializePayloadHeadersForYaml(headers?: PayloadHeaderEntry[]): Record {
+ const result: Record = {};
+ for (const header of headers ?? []) {
+ const name = header.name.trim();
+ if (!name) continue;
+ result[name] = header.value;
+ }
+ return result;
+}
+
+function serializePayloadConditionsForYaml(
+ conditions?: PayloadParamEntry[]
+): Array> {
+ const result: Array> = [];
+ for (const condition of conditions ?? []) {
+ const path = condition.path.trim();
+ if (!path) continue;
+ result.push({ [path]: serializePayloadParamEntryValue(condition) });
+ }
+ return result;
+}
+
+function serializeStringListForYaml(items?: string[]): string[] {
+ return (items ?? []).map((item) => item.trim()).filter(Boolean);
+}
+
+function serializePayloadModelsForYaml(
+ models: PayloadRule['models']
+): Array> {
+ return (models || [])
+ .filter((m) => m.name?.trim())
+ .map((m) => {
+ const obj: Record = { name: m.name.trim() };
+ if (m.protocol) obj.protocol = m.protocol;
+ if (m.fromProtocol) obj['from-protocol'] = m.fromProtocol;
+
+ const headers = serializePayloadHeadersForYaml(m.headers);
+ if (Object.keys(headers).length) obj.headers = headers;
+
+ const match = serializePayloadConditionsForYaml(m.match);
+ if (match.length) obj.match = match;
+
+ const notMatch = serializePayloadConditionsForYaml(m.notMatch);
+ if (notMatch.length) obj['not-match'] = notMatch;
+
+ const exist = serializeStringListForYaml(m.exist);
+ if (exist.length) obj.exist = exist;
+
+ const notExist = serializeStringListForYaml(m.notExist);
+ if (notExist.length) obj['not-exist'] = notExist;
+
+ return obj;
+ });
+}
+
function serializePayloadRulesForYaml(rules: PayloadRule[]): Array> {
return rules
.map((rule) => {
- const models = (rule.models || [])
- .filter((m) => m.name?.trim())
- .map((m) => {
- const obj: Record = { name: m.name.trim() };
- if (m.protocol) obj.protocol = m.protocol;
- return obj;
- });
+ const models = serializePayloadModelsForYaml(rule.models);
const params: Record = {};
for (const param of rule.params || []) {
if (!param.path?.trim()) continue;
- let value: unknown = param.value;
- if (param.valueType === 'number') {
- const num = Number(param.value);
- value = Number.isFinite(num) ? num : param.value;
- } else if (param.valueType === 'boolean') {
- value = param.value === 'true';
- } else if (param.valueType === 'json') {
- try {
- value = JSON.parse(param.value);
- } catch {
- value = param.value;
- }
- }
- params[param.path.trim()] = value;
+ params[param.path.trim()] = serializePayloadParamEntryValue(param);
}
return { models, params };
@@ -467,13 +635,7 @@ function serializePayloadFilterRulesForYaml(
): Array> {
return rules
.map((rule) => {
- const models = (rule.models || [])
- .filter((m) => m.name?.trim())
- .map((m) => {
- const obj: Record = { name: m.name.trim() };
- if (m.protocol) obj.protocol = m.protocol;
- return obj;
- });
+ const models = serializePayloadModelsForYaml(rule.models);
const params = (Array.isArray(rule.params) ? rule.params : [])
.map((path) => String(path).trim())
@@ -487,13 +649,7 @@ function serializePayloadFilterRulesForYaml(
function serializeRawPayloadRulesForYaml(rules: PayloadRule[]): Array> {
return rules
.map((rule) => {
- const models = (rule.models || [])
- .filter((m) => m.name?.trim())
- .map((m) => {
- const obj: Record = { name: m.name.trim() };
- if (m.protocol) obj.protocol = m.protocol;
- return obj;
- });
+ const models = serializePayloadModelsForYaml(rule.models);
const params: Record = {};
for (const param of rule.params || []) {
@@ -562,6 +718,45 @@ function getNextDirtyFields(
nextDirtyFields.add(key);
}
};
+ const updateScalarDirty = (key: keyof VisualConfigValues) => {
+ if (Object.prototype.hasOwnProperty.call(patch, key)) {
+ updateDirty(key, nextValues[key] === baselineValues[key]);
+ }
+ };
+
+ (
+ [
+ 'homeEnabled',
+ 'homeHost',
+ 'homePort',
+ 'homePassword',
+ 'homeDisableClusterDiscovery',
+ 'homeTlsEnable',
+ 'homeTlsServerName',
+ 'homeTlsCaCert',
+ 'homeTlsInsecureSkipVerify',
+ 'rmDisableAutoUpdatePanel',
+ 'errorLogsMaxFiles',
+ 'usageStatisticsEnabled',
+ 'redisUsageQueueRetentionSeconds',
+ 'passthroughHeaders',
+ 'disableCooling',
+ 'disableImageGeneration',
+ 'authAutoRefreshWorkers',
+ 'enableGeminiCliEndpoint',
+ 'antigravitySignatureCacheEnabled',
+ 'antigravitySignatureBypassStrict',
+ 'claudeHeaderUserAgent',
+ 'claudeHeaderPackageVersion',
+ 'claudeHeaderRuntimeVersion',
+ 'claudeHeaderOs',
+ 'claudeHeaderArch',
+ 'claudeHeaderTimeout',
+ 'claudeHeaderStabilizeDeviceProfile',
+ 'codexHeaderUserAgent',
+ 'codexHeaderBetaFeatures',
+ ] as Array
+ ).forEach(updateScalarDirty);
if (Object.prototype.hasOwnProperty.call(patch, 'host')) {
updateDirty('host', nextValues.host === baselineValues.host);
@@ -806,11 +1001,15 @@ export function useVisualConfig() {
const parsedRaw: unknown = parseYaml(yamlContent) || {};
const parsed = asRecord(parsedRaw) ?? {};
const tls = asRecord(parsed.tls);
+ const home = asRecord(parsed.home);
+ const homeTls = asRecord(home?.tls);
const remoteManagement = asRecord(parsed['remote-management']);
const quotaExceeded = asRecord(parsed['quota-exceeded']);
const routing = asRecord(parsed.routing);
const payload = asRecord(parsed.payload);
const streaming = asRecord(parsed.streaming);
+ const claudeHeaderDefaults = asRecord(parsed['claude-header-defaults']);
+ const codexHeaderDefaults = asRecord(parsed['codex-header-defaults']);
const newValues: VisualConfigValues = {
host: typeof parsed.host === 'string' ? parsed.host : '',
@@ -820,12 +1019,24 @@ export function useVisualConfig() {
tlsCert: typeof tls?.cert === 'string' ? tls.cert : '',
tlsKey: typeof tls?.key === 'string' ? tls.key : '',
+ homeEnabled: Boolean(home?.enabled),
+ homeHost: typeof home?.host === 'string' ? home.host : '',
+ homePort: String(home?.port ?? ''),
+ homePassword: typeof home?.password === 'string' ? home.password : '',
+ homeDisableClusterDiscovery: Boolean(home?.['disable-cluster-discovery']),
+ homeTlsEnable: Boolean(homeTls?.enable),
+ homeTlsServerName:
+ typeof homeTls?.['server-name'] === 'string' ? homeTls['server-name'] : '',
+ homeTlsCaCert: typeof homeTls?.['ca-cert'] === 'string' ? homeTls['ca-cert'] : '',
+ homeTlsInsecureSkipVerify: Boolean(homeTls?.['insecure-skip-verify']),
+
rmAllowRemote: Boolean(remoteManagement?.['allow-remote']),
rmSecretKey:
typeof remoteManagement?.['secret-key'] === 'string'
? remoteManagement['secret-key']
: '',
rmDisableControlPanel: Boolean(remoteManagement?.['disable-control-panel']),
+ rmDisableAutoUpdatePanel: Boolean(remoteManagement?.['disable-auto-update-panel']),
rmPanelRepo:
typeof remoteManagement?.['panel-github-repository'] === 'string'
? remoteManagement['panel-github-repository']
@@ -840,13 +1051,56 @@ export function useVisualConfig() {
commercialMode: Boolean(parsed['commercial-mode']),
loggingToFile: Boolean(parsed['logging-to-file']),
logsMaxTotalSizeMb: String(parsed['logs-max-total-size-mb'] ?? ''),
+ errorLogsMaxFiles: String(parsed['error-logs-max-files'] ?? ''),
+ usageStatisticsEnabled: Boolean(parsed['usage-statistics-enabled']),
+ redisUsageQueueRetentionSeconds: String(
+ parsed['redis-usage-queue-retention-seconds'] ?? ''
+ ),
proxyUrl: typeof parsed['proxy-url'] === 'string' ? parsed['proxy-url'] : '',
forceModelPrefix: Boolean(parsed['force-model-prefix']),
+ passthroughHeaders: Boolean(parsed['passthrough-headers']),
requestRetry: String(parsed['request-retry'] ?? ''),
maxRetryCredentials: String(parsed['max-retry-credentials'] ?? ''),
maxRetryInterval: String(parsed['max-retry-interval'] ?? ''),
+ disableCooling: Boolean(parsed['disable-cooling']),
+ disableImageGeneration: parseDisableImageGenerationMode(parsed['disable-image-generation']),
+ authAutoRefreshWorkers: String(parsed['auth-auto-refresh-workers'] ?? ''),
wsAuth: Boolean(parsed['ws-auth']),
+ enableGeminiCliEndpoint: Boolean(parsed['enable-gemini-cli-endpoint']),
+ antigravitySignatureCacheEnabled: Boolean(
+ parsed['antigravity-signature-cache-enabled'] ?? true
+ ),
+ antigravitySignatureBypassStrict: Boolean(parsed['antigravity-signature-bypass-strict']),
+
+ claudeHeaderUserAgent:
+ typeof claudeHeaderDefaults?.['user-agent'] === 'string'
+ ? claudeHeaderDefaults['user-agent']
+ : '',
+ claudeHeaderPackageVersion:
+ typeof claudeHeaderDefaults?.['package-version'] === 'string'
+ ? claudeHeaderDefaults['package-version']
+ : '',
+ claudeHeaderRuntimeVersion:
+ typeof claudeHeaderDefaults?.['runtime-version'] === 'string'
+ ? claudeHeaderDefaults['runtime-version']
+ : '',
+ claudeHeaderOs: typeof claudeHeaderDefaults?.os === 'string' ? claudeHeaderDefaults.os : '',
+ claudeHeaderArch:
+ typeof claudeHeaderDefaults?.arch === 'string' ? claudeHeaderDefaults.arch : '',
+ claudeHeaderTimeout:
+ typeof claudeHeaderDefaults?.timeout === 'string' ? claudeHeaderDefaults.timeout : '',
+ claudeHeaderStabilizeDeviceProfile: Boolean(
+ claudeHeaderDefaults?.['stabilize-device-profile']
+ ),
+ codexHeaderUserAgent:
+ typeof codexHeaderDefaults?.['user-agent'] === 'string'
+ ? codexHeaderDefaults['user-agent']
+ : '',
+ codexHeaderBetaFeatures:
+ typeof codexHeaderDefaults?.['beta-features'] === 'string'
+ ? codexHeaderDefaults['beta-features']
+ : '',
quotaSwitchProject: Boolean(quotaExceeded?.['switch-project'] ?? true),
quotaSwitchPreviewModel: Boolean(quotaExceeded?.['switch-preview-model'] ?? true),
@@ -854,9 +1108,7 @@ export function useVisualConfig() {
routingStrategy: routing?.strategy === 'fill-first' ? 'fill-first' : 'round-robin',
routingSessionAffinity: Boolean(
- routing?.['session-affinity'] ??
- routing?.sessionAffinity ??
- routing?.['sessionAffinity']
+ routing?.['session-affinity'] ?? routing?.sessionAffinity ?? routing?.['sessionAffinity']
),
routingSessionAffinityTTL:
typeof routing?.['session-affinity-ttl'] === 'string'
@@ -915,11 +1167,57 @@ export function useVisualConfig() {
deleteIfMapEmpty(doc, ['tls']);
}
+ if (
+ docHas(doc, ['home']) ||
+ values.homeEnabled ||
+ values.homeHost.trim() ||
+ values.homePort.trim() ||
+ values.homePassword.trim() ||
+ values.homeDisableClusterDiscovery ||
+ values.homeTlsEnable ||
+ values.homeTlsServerName.trim() ||
+ values.homeTlsCaCert.trim() ||
+ values.homeTlsInsecureSkipVerify
+ ) {
+ ensureMapInDoc(doc, ['home']);
+ setBooleanInDoc(doc, ['home', 'enabled'], values.homeEnabled);
+ setStringInDoc(doc, ['home', 'host'], values.homeHost);
+ setIntFromStringInDoc(doc, ['home', 'port'], values.homePort);
+ setStringInDoc(doc, ['home', 'password'], values.homePassword);
+ setBooleanInDoc(
+ doc,
+ ['home', 'disable-cluster-discovery'],
+ values.homeDisableClusterDiscovery
+ );
+
+ if (
+ docHas(doc, ['home', 'tls']) ||
+ values.homeTlsEnable ||
+ values.homeTlsServerName.trim() ||
+ values.homeTlsCaCert.trim() ||
+ values.homeTlsInsecureSkipVerify
+ ) {
+ ensureMapInDoc(doc, ['home', 'tls']);
+ setBooleanInDoc(doc, ['home', 'tls', 'enable'], values.homeTlsEnable);
+ setStringInDoc(doc, ['home', 'tls', 'server-name'], values.homeTlsServerName);
+ setStringInDoc(doc, ['home', 'tls', 'ca-cert'], values.homeTlsCaCert);
+ setBooleanInDoc(
+ doc,
+ ['home', 'tls', 'insecure-skip-verify'],
+ values.homeTlsInsecureSkipVerify
+ );
+ deleteIfMapEmpty(doc, ['home', 'tls']);
+ }
+
+ deleteIfMapEmpty(doc, ['home']);
+ }
+
if (
docHas(doc, ['remote-management']) ||
values.rmAllowRemote ||
values.rmSecretKey.trim() ||
values.rmDisableControlPanel ||
+ values.rmDisableAutoUpdatePanel ||
values.rmPanelRepo.trim()
) {
ensureMapInDoc(doc, ['remote-management']);
@@ -930,6 +1228,11 @@ export function useVisualConfig() {
['remote-management', 'disable-control-panel'],
values.rmDisableControlPanel
);
+ setBooleanInDoc(
+ doc,
+ ['remote-management', 'disable-auto-update-panel'],
+ values.rmDisableAutoUpdatePanel
+ );
setStringInDoc(doc, ['remote-management', 'panel-github-repository'], values.rmPanelRepo);
if (docHas(doc, ['remote-management', 'panel-repo'])) {
doc.deleteIn(['remote-management', 'panel-repo']);
@@ -954,13 +1257,95 @@ export function useVisualConfig() {
setBooleanInDoc(doc, ['commercial-mode'], values.commercialMode);
setBooleanInDoc(doc, ['logging-to-file'], values.loggingToFile);
setIntFromStringInDoc(doc, ['logs-max-total-size-mb'], values.logsMaxTotalSizeMb);
+ setIntFromStringInDoc(doc, ['error-logs-max-files'], values.errorLogsMaxFiles);
+ setBooleanInDoc(doc, ['usage-statistics-enabled'], values.usageStatisticsEnabled);
+ setIntFromStringInDoc(
+ doc,
+ ['redis-usage-queue-retention-seconds'],
+ values.redisUsageQueueRetentionSeconds
+ );
setStringInDoc(doc, ['proxy-url'], values.proxyUrl);
setBooleanInDoc(doc, ['force-model-prefix'], values.forceModelPrefix);
+ setBooleanInDoc(doc, ['passthrough-headers'], values.passthroughHeaders);
setIntFromStringInDoc(doc, ['request-retry'], values.requestRetry);
setIntFromStringInDoc(doc, ['max-retry-credentials'], values.maxRetryCredentials);
setIntFromStringInDoc(doc, ['max-retry-interval'], values.maxRetryInterval);
+ setBooleanInDoc(doc, ['disable-cooling'], values.disableCooling);
+ setDisableImageGenerationInDoc(
+ doc,
+ ['disable-image-generation'],
+ values.disableImageGeneration
+ );
+ setIntFromStringInDoc(doc, ['auth-auto-refresh-workers'], values.authAutoRefreshWorkers);
setBooleanInDoc(doc, ['ws-auth'], values.wsAuth);
+ setBooleanInDoc(doc, ['enable-gemini-cli-endpoint'], values.enableGeminiCliEndpoint);
+ if (
+ docHas(doc, ['antigravity-signature-cache-enabled']) ||
+ !values.antigravitySignatureCacheEnabled
+ ) {
+ doc.setIn(
+ ['antigravity-signature-cache-enabled'],
+ values.antigravitySignatureCacheEnabled
+ );
+ }
+ setBooleanInDoc(
+ doc,
+ ['antigravity-signature-bypass-strict'],
+ values.antigravitySignatureBypassStrict
+ );
+
+ if (
+ docHas(doc, ['claude-header-defaults']) ||
+ values.claudeHeaderUserAgent.trim() ||
+ values.claudeHeaderPackageVersion.trim() ||
+ values.claudeHeaderRuntimeVersion.trim() ||
+ values.claudeHeaderOs.trim() ||
+ values.claudeHeaderArch.trim() ||
+ values.claudeHeaderTimeout.trim() ||
+ values.claudeHeaderStabilizeDeviceProfile
+ ) {
+ ensureMapInDoc(doc, ['claude-header-defaults']);
+ setStringInDoc(
+ doc,
+ ['claude-header-defaults', 'user-agent'],
+ values.claudeHeaderUserAgent
+ );
+ setStringInDoc(
+ doc,
+ ['claude-header-defaults', 'package-version'],
+ values.claudeHeaderPackageVersion
+ );
+ setStringInDoc(
+ doc,
+ ['claude-header-defaults', 'runtime-version'],
+ values.claudeHeaderRuntimeVersion
+ );
+ setStringInDoc(doc, ['claude-header-defaults', 'os'], values.claudeHeaderOs);
+ setStringInDoc(doc, ['claude-header-defaults', 'arch'], values.claudeHeaderArch);
+ setStringInDoc(doc, ['claude-header-defaults', 'timeout'], values.claudeHeaderTimeout);
+ setBooleanInDoc(
+ doc,
+ ['claude-header-defaults', 'stabilize-device-profile'],
+ values.claudeHeaderStabilizeDeviceProfile
+ );
+ deleteIfMapEmpty(doc, ['claude-header-defaults']);
+ }
+
+ if (
+ docHas(doc, ['codex-header-defaults']) ||
+ values.codexHeaderUserAgent.trim() ||
+ values.codexHeaderBetaFeatures.trim()
+ ) {
+ ensureMapInDoc(doc, ['codex-header-defaults']);
+ setStringInDoc(doc, ['codex-header-defaults', 'user-agent'], values.codexHeaderUserAgent);
+ setStringInDoc(
+ doc,
+ ['codex-header-defaults', 'beta-features'],
+ values.codexHeaderBetaFeatures
+ );
+ deleteIfMapEmpty(doc, ['codex-header-defaults']);
+ }
if (
docHas(doc, ['quota-exceeded']) ||
@@ -983,10 +1368,7 @@ export function useVisualConfig() {
doc.setIn(['quota-exceeded', 'switch-project'], values.quotaSwitchProject);
doc.setIn(['quota-exceeded', 'switch-preview-model'], values.quotaSwitchPreviewModel);
if (writeQuotaAntigravityCredits) {
- doc.setIn(
- ['quota-exceeded', 'antigravity-credits'],
- values.quotaAntigravityCredits
- );
+ doc.setIn(['quota-exceeded', 'antigravity-credits'], values.quotaAntigravityCredits);
}
deleteIfMapEmpty(doc, ['quota-exceeded']);
}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 9b85760..456a1a4 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -1085,6 +1085,25 @@
"cert": "Certificate File Path",
"key": "Private Key File Path"
},
+ "home": {
+ "title": "Home Control Plane",
+ "description": "Optional Home control plane connection over Redis protocol",
+ "enabled": "Enable Home",
+ "enabled_desc": "Enable the outbound connection to the Home control plane",
+ "host": "Home Host",
+ "port": "Home Port",
+ "password": "Home Password",
+ "password_placeholder": "Leave empty for no password",
+ "disable_cluster_discovery": "Disable Cluster Discovery",
+ "disable_cluster_discovery_desc": "Keep using the configured Home address instead of CLUSTER NODES entries",
+ "tls_title": "Home TLS",
+ "tls_description": "TLS, SNI, and certificate settings for the Home Redis connection",
+ "tls_enable": "Enable Home TLS",
+ "tls_server_name": "TLS Server Name",
+ "tls_ca_cert": "CA Certificate Path",
+ "tls_insecure": "Skip Certificate Verification",
+ "tls_insecure_desc": "Only use this for testing self-signed endpoints"
+ },
"remote": {
"title": "Remote Management",
"description": "Remote access and control panel settings",
@@ -1092,6 +1111,8 @@
"allow_remote_desc": "Allow management access from other hosts",
"disable_panel": "Disable Control Panel",
"disable_panel_desc": "Disable the built-in web control panel",
+ "disable_auto_update_panel": "Disable Panel Auto Updates",
+ "disable_auto_update_panel_desc": "Download the panel when first missing, but never auto-update it from GitHub",
"secret_key": "Management Key",
"secret_key_placeholder": "Set management key",
"panel_repo": "Panel Repository"
@@ -1111,7 +1132,31 @@
"commercial_mode_desc": "Disable high-overhead middleware to support high concurrency",
"logging_to_file": "Log to File",
"logging_to_file_desc": "Save logs to files",
- "logs_max_size": "Log File Size Limit (MB)"
+ "logs_max_size": "Log File Size Limit (MB)",
+ "error_logs_max_files": "Retained Error Log Files",
+ "usage_statistics_enabled": "Enable In-memory Usage Statistics",
+ "usage_statistics_enabled_desc": "Aggregate request usage statistics in memory",
+ "redis_usage_retention": "Redis Usage Queue Retention (seconds)",
+ "redis_usage_retention_hint": "In-memory retention for RESP LPOP/RPOP usage output, max 3600 seconds",
+ "antigravity_signature_cache": "Enable Antigravity Signature Cache",
+ "antigravity_signature_cache_desc": "Prefer and validate cached thinking-block signatures",
+ "antigravity_signature_strict": "Strict Bypass Signature Validation",
+ "antigravity_signature_strict_desc": "Only applies when the signature cache is disabled; validates the full Claude protobuf tree"
+ },
+ "headers": {
+ "title": "Header Defaults",
+ "description": "Default headers for Claude and Codex OAuth requests when the client omits them",
+ "claude_title": "Claude Header Defaults",
+ "codex_title": "Codex Header Defaults",
+ "user_agent": "User-Agent",
+ "package_version": "Package Version",
+ "runtime_version": "Runtime Version",
+ "os": "OS",
+ "arch": "Arch",
+ "timeout": "Timeout",
+ "stabilize_device": "Stabilize Device Profile",
+ "stabilize_device_desc": "Pin OS/Arch and stabilize the software fingerprint per credential/API key",
+ "beta_features": "Beta Features"
},
"network": {
"title": "Network Configuration",
@@ -1121,6 +1166,13 @@
"max_retry_credentials": "Max Retry Credentials",
"max_retry_credentials_hint": "Leave empty to keep it unset. Set to 0 to preserve legacy behavior and try all available credentials.",
"max_retry_interval": "Max Retry Interval (seconds)",
+ "auth_auto_refresh_workers": "Auth Auto-refresh Workers",
+ "auth_auto_refresh_workers_hint": "When greater than 0, overrides the default worker count (16)",
+ "disable_image_generation": "Disable Image Generation",
+ "disable_image_generation_hint": "false enables it; true disables all image generation; chat only disables non-image endpoint injection",
+ "disable_image_generation_false": "false (enabled)",
+ "disable_image_generation_true": "true (disabled everywhere)",
+ "disable_image_generation_chat": "chat (disable chat injection only)",
"routing_strategy": "Routing Strategy",
"routing_strategy_hint": "Select credential selection strategy",
"strategy_round_robin": "Round Robin",
@@ -1128,9 +1180,15 @@
"session_affinity_ttl": "Session Affinity TTL",
"force_model_prefix": "Force Model Prefix",
"force_model_prefix_desc": "Unprefixed model requests only use credentials without prefix",
+ "passthrough_headers": "Pass Through Upstream Headers",
+ "passthrough_headers_desc": "Forward filtered upstream response headers to downstream clients",
+ "disable_cooling": "Disable Cooling",
+ "disable_cooling_desc": "Globally disable auth/model cooldown windows after failures",
"session_affinity": "Session Affinity Routing",
"ws_auth": "WebSocket Authentication",
- "ws_auth_desc": "Enable WebSocket authentication (/v1/ws)"
+ "ws_auth_desc": "Enable WebSocket authentication (/v1/ws)",
+ "enable_gemini_cli_endpoint": "Enable Gemini CLI Internal Endpoint",
+ "enable_gemini_cli_endpoint_desc": "Enable /v1internal:* compatibility endpoints"
},
"quota": {
"title": "Quota Fallback",
@@ -1199,10 +1257,25 @@
"provider_default": "Default",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
+ "provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
+ "advanced": "Advanced",
+ "hide_advanced": "Hide Advanced",
+ "from_protocol": "Source Protocol (from-protocol)",
+ "headers": "Request Header Matches",
+ "header_name": "Header Name",
+ "header_value": "Header Value",
+ "add_header": "Add Header",
+ "match": "Required Matches (match)",
+ "notMatch": "Forbidden Matches (not-match)",
+ "exist": "Required Paths (exist)",
+ "notExist": "Forbidden Paths (not-exist)",
+ "condition_path": "Match Path",
+ "condition_value": "Match Value",
+ "add_condition": "Add Condition",
"value_type_string": "String",
"value_type_number": "Number",
"value_type_boolean": "Boolean",
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index a829ab3..2860c7a 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -1082,6 +1082,25 @@
"cert": "Путь к сертификату",
"key": "Путь к закрытому ключу"
},
+ "home": {
+ "title": "Панель управления Home",
+ "description": "Необязательное подключение к Home по протоколу Redis",
+ "enabled": "Включить Home",
+ "enabled_desc": "Включить исходящее подключение к панели управления Home",
+ "host": "Хост Home",
+ "port": "Порт Home",
+ "password": "Пароль Home",
+ "password_placeholder": "Оставьте пустым, если пароль не нужен",
+ "disable_cluster_discovery": "Отключить обнаружение кластера",
+ "disable_cluster_discovery_desc": "Использовать заданный адрес Home вместо записей CLUSTER NODES",
+ "tls_title": "TLS для Home",
+ "tls_description": "Настройки TLS, SNI и сертификатов для Redis-подключения Home",
+ "tls_enable": "Включить TLS для Home",
+ "tls_server_name": "TLS Server Name",
+ "tls_ca_cert": "Путь к CA-сертификату",
+ "tls_insecure": "Пропустить проверку сертификата",
+ "tls_insecure_desc": "Используйте только для тестирования самоподписанных endpoints"
+ },
"remote": {
"title": "Удалённое управление",
"description": "Настройки удалённого доступа и панели управления",
@@ -1089,6 +1108,8 @@
"allow_remote_desc": "Разрешить управление с других хостов",
"disable_panel": "Отключить панель",
"disable_panel_desc": "Отключить встроенную веб-панель управления",
+ "disable_auto_update_panel": "Отключить автообновление панели",
+ "disable_auto_update_panel_desc": "Скачивать панель при первом отсутствии, но не обновлять её автоматически с GitHub",
"secret_key": "Ключ управления",
"secret_key_placeholder": "Задайте ключ управления",
"panel_repo": "Репозиторий панели"
@@ -1108,7 +1129,31 @@
"commercial_mode_desc": "Отключить тяжёлое промежуточное ПО для поддержки высокой нагрузки",
"logging_to_file": "Журналировать в файл",
"logging_to_file_desc": "Сохранять журналы в файлы",
- "logs_max_size": "Максимальный размер файла журнала (МБ)"
+ "logs_max_size": "Максимальный размер файла журнала (МБ)",
+ "error_logs_max_files": "Файлов журнала ошибок",
+ "usage_statistics_enabled": "Включить статистику использования в памяти",
+ "usage_statistics_enabled_desc": "Агрегировать статистику использования запросов в памяти",
+ "redis_usage_retention": "Хранение очереди Redis usage (сек)",
+ "redis_usage_retention_hint": "Время хранения вывода RESP LPOP/RPOP в памяти, максимум 3600 секунд",
+ "antigravity_signature_cache": "Включить кэш подписей Antigravity",
+ "antigravity_signature_cache_desc": "Предпочитать и проверять кэшированные подписи thinking-блоков",
+ "antigravity_signature_strict": "Строгая проверка bypass-подписи",
+ "antigravity_signature_strict_desc": "Применяется только при отключённом кэше подписей; проверяет полное дерево Claude protobuf"
+ },
+ "headers": {
+ "title": "Заголовки по умолчанию",
+ "description": "Заголовки по умолчанию для OAuth-запросов Claude и Codex, когда клиент их не отправил",
+ "claude_title": "Claude Header Defaults",
+ "codex_title": "Codex Header Defaults",
+ "user_agent": "User-Agent",
+ "package_version": "Package Version",
+ "runtime_version": "Runtime Version",
+ "os": "OS",
+ "arch": "Arch",
+ "timeout": "Timeout",
+ "stabilize_device": "Стабилизировать профиль устройства",
+ "stabilize_device_desc": "Фиксировать OS/Arch и стабилизировать программный отпечаток для учётных данных/API-ключа",
+ "beta_features": "Beta Features"
},
"network": {
"title": "Сетевые настройки",
@@ -1118,6 +1163,13 @@
"max_retry_credentials": "Максимум учётных данных для повторов",
"max_retry_credentials_hint": "Оставьте пустым, чтобы не задавать поле. Значение 0 сохраняет legacy-поведение и позволяет перебрать все доступные учётные данные.",
"max_retry_interval": "Максимальный интервал повтора (сек)",
+ "auth_auto_refresh_workers": "Workers автообновления auth",
+ "auth_auto_refresh_workers_hint": "Если больше 0, переопределяет число workers по умолчанию (16)",
+ "disable_image_generation": "Отключить генерацию изображений",
+ "disable_image_generation_hint": "false включает; true отключает везде; chat отключает только инъекцию на не-image endpoints",
+ "disable_image_generation_false": "false (включено)",
+ "disable_image_generation_true": "true (отключено везде)",
+ "disable_image_generation_chat": "chat (только отключить chat-инъекцию)",
"routing_strategy": "Стратегия маршрутизации",
"routing_strategy_hint": "Выберите стратегию подбора учётных данных",
"strategy_round_robin": "По кругу",
@@ -1125,9 +1177,15 @@
"session_affinity_ttl": "TTL привязки сессии",
"force_model_prefix": "Принудительный префикс модели",
"force_model_prefix_desc": "Запросы к моделям без префикса используют только учётные данные без префикса",
+ "passthrough_headers": "Передавать upstream-заголовки",
+ "passthrough_headers_desc": "Передавать отфильтрованные upstream-заголовки ответа downstream-клиентам",
+ "disable_cooling": "Отключить cooldown",
+ "disable_cooling_desc": "Глобально отключить окна охлаждения auth/model после ошибок",
"session_affinity": "Маршрутизация с привязкой к сессии",
"ws_auth": "Аутентификация WebSocket",
- "ws_auth_desc": "Включить аутентификацию WebSocket (/v1/ws)"
+ "ws_auth_desc": "Включить аутентификацию WebSocket (/v1/ws)",
+ "enable_gemini_cli_endpoint": "Включить внутренний endpoint Gemini CLI",
+ "enable_gemini_cli_endpoint_desc": "Включить совместимые endpoints /v1internal:*"
},
"quota": {
"title": "Резерв по квоте",
@@ -1196,10 +1254,25 @@
"provider_default": "По умолчанию",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
+ "provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
+ "advanced": "Дополнительно",
+ "hide_advanced": "Скрыть дополнительно",
+ "from_protocol": "Исходный протокол (from-protocol)",
+ "headers": "Совпадения заголовков запроса",
+ "header_name": "Имя заголовка",
+ "header_value": "Значение заголовка",
+ "add_header": "Добавить заголовок",
+ "match": "Обязательные совпадения (match)",
+ "notMatch": "Запрещённые совпадения (not-match)",
+ "exist": "Обязательные пути (exist)",
+ "notExist": "Запрещённые пути (not-exist)",
+ "condition_path": "Путь условия",
+ "condition_value": "Значение условия",
+ "add_condition": "Добавить условие",
"value_type_string": "Строка",
"value_type_number": "Число",
"value_type_boolean": "Булево",
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index 3750d37..c69156f 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -1085,6 +1085,25 @@
"cert": "证书文件路径",
"key": "私钥文件路径"
},
+ "home": {
+ "title": "Home 控制平面",
+ "description": "通过 Redis 协议连接 Home 控制平面的可选设置",
+ "enabled": "启用 Home",
+ "enabled_desc": "启用到 Home 控制平面的出站连接",
+ "host": "Home 主机",
+ "port": "Home 端口",
+ "password": "Home 密码",
+ "password_placeholder": "留空表示不设置密码",
+ "disable_cluster_discovery": "禁用集群发现",
+ "disable_cluster_discovery_desc": "固定使用配置中的 Home 地址,不切换到 CLUSTER NODES 返回的地址",
+ "tls_title": "Home TLS",
+ "tls_description": "Home Redis 连接的 TLS/SNI 与证书设置",
+ "tls_enable": "启用 Home TLS",
+ "tls_server_name": "TLS Server Name",
+ "tls_ca_cert": "CA 证书路径",
+ "tls_insecure": "跳过证书校验",
+ "tls_insecure_desc": "仅用于测试自签名端点"
+ },
"remote": {
"title": "远程管理",
"description": "远程访问和控制面板设置",
@@ -1092,6 +1111,8 @@
"allow_remote_desc": "允许从其他主机访问管理接口",
"disable_panel": "禁用控制面板",
"disable_panel_desc": "禁用内置的 Web 控制面板",
+ "disable_auto_update_panel": "禁用面板自动更新",
+ "disable_auto_update_panel_desc": "首次缺失时仍可下载,但不再从 GitHub 后台自动更新",
"secret_key": "管理密钥",
"secret_key_placeholder": "设置管理密钥",
"panel_repo": "面板仓库"
@@ -1111,7 +1132,31 @@
"commercial_mode_desc": "禁用高开销中间件以支持高并发",
"logging_to_file": "写入日志文件",
"logging_to_file_desc": "将日志保存到文件",
- "logs_max_size": "日志文件大小限制 (MB)"
+ "logs_max_size": "日志文件大小限制 (MB)",
+ "error_logs_max_files": "错误日志保留文件数",
+ "usage_statistics_enabled": "启用内存用量统计",
+ "usage_statistics_enabled_desc": "启用内存中的请求用量聚合",
+ "redis_usage_retention": "Redis 用量队列保留秒数",
+ "redis_usage_retention_hint": "RESP 接口 LPOP/RPOP 用量队列的内存保留时间,最大 3600 秒",
+ "antigravity_signature_cache": "启用 Antigravity 签名缓存",
+ "antigravity_signature_cache_desc": "优先使用并校验缓存的 thinking block 签名",
+ "antigravity_signature_strict": "严格校验旁路签名",
+ "antigravity_signature_strict_desc": "仅在关闭签名缓存时生效,按完整 Claude protobuf 树校验"
+ },
+ "headers": {
+ "title": "Header 默认值",
+ "description": "Claude 与 Codex OAuth 请求在客户端缺少 Header 时使用的默认值",
+ "claude_title": "Claude Header Defaults",
+ "codex_title": "Codex Header Defaults",
+ "user_agent": "User-Agent",
+ "package_version": "Package Version",
+ "runtime_version": "Runtime Version",
+ "os": "OS",
+ "arch": "Arch",
+ "timeout": "Timeout",
+ "stabilize_device": "稳定设备指纹",
+ "stabilize_device_desc": "固定 OS/Arch,并按凭据/API Key 稳定软件指纹",
+ "beta_features": "Beta Features"
},
"network": {
"title": "网络配置",
@@ -1121,6 +1166,13 @@
"max_retry_credentials": "最大重试凭据数",
"max_retry_credentials_hint": "留空表示不设置;设为 0 表示保留 legacy 行为,并尝试所有可用凭据。",
"max_retry_interval": "最大重试间隔 (秒)",
+ "auth_auto_refresh_workers": "认证自动刷新 Worker 数",
+ "auth_auto_refresh_workers_hint": "大于 0 时覆盖默认 Worker 数(16)",
+ "disable_image_generation": "禁用图像生成",
+ "disable_image_generation_hint": "false 为启用;true 为全部禁用;chat 仅禁用非图片端点注入",
+ "disable_image_generation_false": "false(启用)",
+ "disable_image_generation_true": "true(全部禁用)",
+ "disable_image_generation_chat": "chat(仅禁用聊天注入)",
"routing_strategy": "路由策略",
"routing_strategy_hint": "选择凭据选择策略",
"strategy_round_robin": "轮询 (Round Robin)",
@@ -1128,9 +1180,15 @@
"session_affinity_ttl": "会话粘性 TTL",
"force_model_prefix": "强制模型前缀",
"force_model_prefix_desc": "未带前缀的模型请求只使用无前缀凭据",
+ "passthrough_headers": "透传上游响应 Header",
+ "passthrough_headers_desc": "将过滤后的上游响应 Header 转发给下游客户端",
+ "disable_cooling": "禁用冷却调度",
+ "disable_cooling_desc": "全局禁用认证/模型失败后的冷却窗口",
"session_affinity": "会话粘性路由",
"ws_auth": "WebSocket 认证",
- "ws_auth_desc": "启用 WebSocket 连接认证 (/v1/ws)"
+ "ws_auth_desc": "启用 WebSocket 连接认证 (/v1/ws)",
+ "enable_gemini_cli_endpoint": "启用 Gemini CLI 内部端点",
+ "enable_gemini_cli_endpoint_desc": "启用 /v1internal:* 兼容端点"
},
"quota": {
"title": "配额回退",
@@ -1199,10 +1257,25 @@
"provider_default": "默认",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
+ "provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
+ "advanced": "高级",
+ "hide_advanced": "收起高级",
+ "from_protocol": "来源协议 (from-protocol)",
+ "headers": "请求 Header 匹配",
+ "header_name": "Header 名称",
+ "header_value": "Header 值",
+ "add_header": "添加 Header",
+ "match": "必须匹配 (match)",
+ "notMatch": "不得匹配 (not-match)",
+ "exist": "必须存在 (exist)",
+ "notExist": "不得存在 (not-exist)",
+ "condition_path": "匹配路径",
+ "condition_value": "匹配值",
+ "add_condition": "添加条件",
"value_type_string": "字符串",
"value_type_number": "数字",
"value_type_boolean": "布尔",
diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json
index 94be1c7..9f75a7d 100644
--- a/src/i18n/locales/zh-TW.json
+++ b/src/i18n/locales/zh-TW.json
@@ -1111,6 +1111,25 @@
"cert": "憑證檔案路徑",
"key": "私鑰檔案路徑"
},
+ "home": {
+ "title": "Home 控制平面",
+ "description": "透過 Redis 協議連接 Home 控制平面的選填設定",
+ "enabled": "啟用 Home",
+ "enabled_desc": "啟用到 Home 控制平面的出站連線",
+ "host": "Home 主機",
+ "port": "Home 連接埠",
+ "password": "Home 密碼",
+ "password_placeholder": "留空表示不設定密碼",
+ "disable_cluster_discovery": "停用叢集探索",
+ "disable_cluster_discovery_desc": "固定使用設定中的 Home 位址,不切換到 CLUSTER NODES 返回的位址",
+ "tls_title": "Home TLS",
+ "tls_description": "Home Redis 連線的 TLS/SNI 與憑證設定",
+ "tls_enable": "啟用 Home TLS",
+ "tls_server_name": "TLS Server Name",
+ "tls_ca_cert": "CA 憑證路徑",
+ "tls_insecure": "跳過憑證校驗",
+ "tls_insecure_desc": "僅用於測試自簽端點"
+ },
"remote": {
"title": "遠端管理",
"description": "遠端存取和控制面板設定",
@@ -1118,6 +1137,8 @@
"allow_remote_desc": "允許從其他主機存取管理介面",
"disable_panel": "停用控制面板",
"disable_panel_desc": "停用內建的 Web 控制面板",
+ "disable_auto_update_panel": "停用面板自動更新",
+ "disable_auto_update_panel_desc": "首次缺失時仍可下載,但不再從 GitHub 背景自動更新",
"secret_key": "管理金鑰",
"secret_key_placeholder": "設定管理金鑰",
"panel_repo": "面板儲存庫"
@@ -1137,7 +1158,31 @@
"commercial_mode_desc": "停用高開銷中介軟體以支援高並行",
"logging_to_file": "寫入記錄檔",
"logging_to_file_desc": "將記錄儲存到檔案",
- "logs_max_size": "記錄檔大小限制(MB)"
+ "logs_max_size": "記錄檔大小限制(MB)",
+ "error_logs_max_files": "錯誤記錄保留檔案數",
+ "usage_statistics_enabled": "啟用記憶體用量統計",
+ "usage_statistics_enabled_desc": "啟用記憶體中的請求用量聚合",
+ "redis_usage_retention": "Redis 用量佇列保留秒數",
+ "redis_usage_retention_hint": "RESP 介面 LPOP/RPOP 用量佇列的記憶體保留時間,最大 3600 秒",
+ "antigravity_signature_cache": "啟用 Antigravity 簽名快取",
+ "antigravity_signature_cache_desc": "優先使用並校驗快取的 thinking block 簽名",
+ "antigravity_signature_strict": "嚴格校驗旁路簽名",
+ "antigravity_signature_strict_desc": "僅在關閉簽名快取時生效,按完整 Claude protobuf 樹校驗"
+ },
+ "headers": {
+ "title": "Header 預設值",
+ "description": "Claude 與 Codex OAuth 請求在客戶端缺少 Header 時使用的預設值",
+ "claude_title": "Claude Header Defaults",
+ "codex_title": "Codex Header Defaults",
+ "user_agent": "User-Agent",
+ "package_version": "Package Version",
+ "runtime_version": "Runtime Version",
+ "os": "OS",
+ "arch": "Arch",
+ "timeout": "Timeout",
+ "stabilize_device": "穩定設備指紋",
+ "stabilize_device_desc": "固定 OS/Arch,並按憑證/API Key 穩定軟體指紋",
+ "beta_features": "Beta Features"
},
"network": {
"title": "網路設定",
@@ -1147,6 +1192,13 @@
"max_retry_credentials": "最大重試憑證數",
"max_retry_credentials_hint": "留空表示不設定;設為 0 表示保留 legacy 行為,並嘗試所有可用憑證。",
"max_retry_interval": "最大重試間隔(秒)",
+ "auth_auto_refresh_workers": "驗證自動刷新 Worker 數",
+ "auth_auto_refresh_workers_hint": "大於 0 時覆蓋預設 Worker 數(16)",
+ "disable_image_generation": "停用圖像生成",
+ "disable_image_generation_hint": "false 為啟用;true 為全部停用;chat 僅停用非圖片端點注入",
+ "disable_image_generation_false": "false(啟用)",
+ "disable_image_generation_true": "true(全部停用)",
+ "disable_image_generation_chat": "chat(僅停用聊天注入)",
"routing_strategy": "路由策略",
"routing_strategy_hint": "選擇憑證選擇策略",
"strategy_round_robin": "輪詢(Round Robin)",
@@ -1154,9 +1206,15 @@
"session_affinity_ttl": "會話黏性 TTL",
"force_model_prefix": "強制模型前綴",
"force_model_prefix_desc": "未帶前綴的模型請求只使用無前綴憑證",
+ "passthrough_headers": "透傳上游回應 Header",
+ "passthrough_headers_desc": "將過濾後的上游回應 Header 轉發給下游客戶端",
+ "disable_cooling": "停用冷卻調度",
+ "disable_cooling_desc": "全域停用驗證/模型失敗後的冷卻視窗",
"session_affinity": "會話黏性路由",
"ws_auth": "WebSocket 驗證",
- "ws_auth_desc": "啟用 WebSocket 連線驗證(/v1/ws)"
+ "ws_auth_desc": "啟用 WebSocket 連線驗證(/v1/ws)",
+ "enable_gemini_cli_endpoint": "啟用 Gemini CLI 內部端點",
+ "enable_gemini_cli_endpoint_desc": "啟用 /v1internal:* 相容端點"
},
"quota": {
"title": "配額回退",
@@ -1225,10 +1283,25 @@
"provider_default": "預設",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
+ "provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
+ "advanced": "進階",
+ "hide_advanced": "收起進階",
+ "from_protocol": "來源協議(from-protocol)",
+ "headers": "請求 Header 匹配",
+ "header_name": "Header 名稱",
+ "header_value": "Header 值",
+ "add_header": "新增 Header",
+ "match": "必須匹配(match)",
+ "notMatch": "不得匹配(not-match)",
+ "exist": "必須存在(exist)",
+ "notExist": "不得存在(not-exist)",
+ "condition_path": "匹配路徑",
+ "condition_value": "匹配值",
+ "add_condition": "新增條件",
"value_type_string": "字串",
"value_type_number": "數字",
"value_type_boolean": "布林",
diff --git a/src/types/visualConfig.ts b/src/types/visualConfig.ts
index 40b335b..5966321 100644
--- a/src/types/visualConfig.ts
+++ b/src/types/visualConfig.ts
@@ -1,15 +1,20 @@
export type PayloadParamValueType = 'string' | 'number' | 'boolean' | 'json';
+export type DisableImageGenerationMode = 'false' | 'true' | 'chat';
export type PayloadParamValidationErrorCode =
| 'payload_invalid_number'
| 'payload_invalid_boolean'
| 'payload_invalid_json';
export type VisualConfigFieldPath =
+ | 'homePort'
| 'port'
+ | 'errorLogsMaxFiles'
| 'logsMaxTotalSizeMb'
+ | 'redisUsageQueueRetentionSeconds'
| 'requestRetry'
| 'maxRetryCredentials'
| 'maxRetryInterval'
+ | 'authAutoRefreshWorkers'
| 'streaming.keepaliveSeconds'
| 'streaming.bootstrapRetries'
| 'streaming.nonstreamKeepaliveInterval';
@@ -27,10 +32,22 @@ export type PayloadParamEntry = {
value: string;
};
+export type PayloadHeaderEntry = {
+ id: string;
+ name: string;
+ value: string;
+};
+
export type PayloadModelEntry = {
id: string;
name: string;
protocol?: string;
+ fromProtocol?: string;
+ headers?: PayloadHeaderEntry[];
+ match?: PayloadParamEntry[];
+ notMatch?: PayloadParamEntry[];
+ exist?: string[];
+ notExist?: string[];
};
export type PayloadRule = {
@@ -57,9 +74,19 @@ export type VisualConfigValues = {
tlsEnable: boolean;
tlsCert: string;
tlsKey: string;
+ homeEnabled: boolean;
+ homeHost: string;
+ homePort: string;
+ homePassword: string;
+ homeDisableClusterDiscovery: boolean;
+ homeTlsEnable: boolean;
+ homeTlsServerName: string;
+ homeTlsCaCert: string;
+ homeTlsInsecureSkipVerify: boolean;
rmAllowRemote: boolean;
rmSecretKey: string;
rmDisableControlPanel: boolean;
+ rmDisableAutoUpdatePanel: boolean;
rmPanelRepo: string;
authDir: string;
apiKeysText: string;
@@ -67,11 +94,18 @@ export type VisualConfigValues = {
commercialMode: boolean;
loggingToFile: boolean;
logsMaxTotalSizeMb: string;
+ errorLogsMaxFiles: string;
+ usageStatisticsEnabled: boolean;
+ redisUsageQueueRetentionSeconds: string;
proxyUrl: string;
forceModelPrefix: boolean;
+ passthroughHeaders: boolean;
requestRetry: string;
maxRetryCredentials: string;
maxRetryInterval: string;
+ disableCooling: boolean;
+ disableImageGeneration: DisableImageGenerationMode;
+ authAutoRefreshWorkers: string;
quotaSwitchProject: boolean;
quotaSwitchPreviewModel: boolean;
quotaAntigravityCredits: boolean;
@@ -79,6 +113,18 @@ export type VisualConfigValues = {
routingSessionAffinity: boolean;
routingSessionAffinityTTL: string;
wsAuth: boolean;
+ enableGeminiCliEndpoint: boolean;
+ antigravitySignatureCacheEnabled: boolean;
+ antigravitySignatureBypassStrict: boolean;
+ claudeHeaderUserAgent: string;
+ claudeHeaderPackageVersion: string;
+ claudeHeaderRuntimeVersion: string;
+ claudeHeaderOs: string;
+ claudeHeaderArch: string;
+ claudeHeaderTimeout: string;
+ claudeHeaderStabilizeDeviceProfile: boolean;
+ codexHeaderUserAgent: string;
+ codexHeaderBetaFeatures: string;
payloadDefaultRules: PayloadRule[];
payloadDefaultRawRules: PayloadRule[];
payloadOverrideRules: PayloadRule[];
@@ -98,9 +144,19 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
tlsEnable: false,
tlsCert: '',
tlsKey: '',
+ homeEnabled: false,
+ homeHost: '',
+ homePort: '',
+ homePassword: '',
+ homeDisableClusterDiscovery: false,
+ homeTlsEnable: false,
+ homeTlsServerName: '',
+ homeTlsCaCert: '',
+ homeTlsInsecureSkipVerify: false,
rmAllowRemote: false,
rmSecretKey: '',
rmDisableControlPanel: false,
+ rmDisableAutoUpdatePanel: false,
rmPanelRepo: '',
authDir: '',
apiKeysText: '',
@@ -108,11 +164,18 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
commercialMode: false,
loggingToFile: false,
logsMaxTotalSizeMb: '',
+ errorLogsMaxFiles: '',
+ usageStatisticsEnabled: false,
+ redisUsageQueueRetentionSeconds: '',
proxyUrl: '',
forceModelPrefix: false,
+ passthroughHeaders: false,
requestRetry: '',
maxRetryCredentials: '',
maxRetryInterval: '',
+ disableCooling: false,
+ disableImageGeneration: 'false',
+ authAutoRefreshWorkers: '',
quotaSwitchProject: true,
quotaSwitchPreviewModel: true,
quotaAntigravityCredits: false,
@@ -120,6 +183,18 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
routingSessionAffinity: false,
routingSessionAffinityTTL: '',
wsAuth: false,
+ enableGeminiCliEndpoint: false,
+ antigravitySignatureCacheEnabled: true,
+ antigravitySignatureBypassStrict: false,
+ claudeHeaderUserAgent: '',
+ claudeHeaderPackageVersion: '',
+ claudeHeaderRuntimeVersion: '',
+ claudeHeaderOs: '',
+ claudeHeaderArch: '',
+ claudeHeaderTimeout: '',
+ claudeHeaderStabilizeDeviceProfile: false,
+ codexHeaderUserAgent: '',
+ codexHeaderBetaFeatures: '',
payloadDefaultRules: [],
payloadDefaultRawRules: [],
payloadOverrideRules: [],