mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(i18n): add Home control plane settings and various enhancements
- Introduced Home control plane configuration options in English, Russian, Simplified Chinese, and Traditional Chinese locales. - Added new fields for error log retention, usage statistics, and image generation settings in visual configuration. - Enhanced payload model entries to include header management and condition matching capabilities. - Updated default visual configuration values to accommodate new settings.
This commit is contained in:
@@ -602,13 +602,41 @@
|
||||
|
||||
.payloadRuleModelRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 160px auto;
|
||||
grid-template-columns: 1fr 160px auto auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.payloadRuleModelRowProtocolFirst {
|
||||
grid-template-columns: 160px 1fr auto;
|
||||
grid-template-columns: 160px 1fr auto auto;
|
||||
}
|
||||
|
||||
.payloadModelGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.payloadModelAdvanced {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-left: 10px;
|
||||
padding-left: 12px;
|
||||
border-left: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.payloadAdvancedGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.payloadHeaderRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.payloadRuleParamRow {
|
||||
@@ -660,6 +688,7 @@
|
||||
@media (max-width: 900px) {
|
||||
.payloadRuleModelRow,
|
||||
.payloadRuleModelRowProtocolFirst,
|
||||
.payloadHeaderRow,
|
||||
.payloadRuleParamRow,
|
||||
.payloadFilterModelRow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
@@ -40,13 +40,7 @@ import {
|
||||
} from './VisualConfigEditorBlocks';
|
||||
import styles from './VisualConfigEditor.module.scss';
|
||||
|
||||
type VisualSectionId =
|
||||
| 'server'
|
||||
| 'auth'
|
||||
| 'system'
|
||||
| 'quota'
|
||||
| 'streaming'
|
||||
| 'payload';
|
||||
type VisualSectionId = 'server' | 'auth' | 'system' | 'quota' | 'streaming' | 'payload';
|
||||
|
||||
type VisualSection = {
|
||||
id: VisualSectionId;
|
||||
@@ -175,6 +169,8 @@ export function VisualConfigEditor({
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
const routingStrategyLabelId = useId();
|
||||
const routingStrategyHintId = `${routingStrategyLabelId}-hint`;
|
||||
const disableImageGenerationLabelId = useId();
|
||||
const disableImageGenerationHintId = `${disableImageGenerationLabelId}-hint`;
|
||||
const keepaliveInputId = useId();
|
||||
const keepaliveHintId = `${keepaliveInputId}-hint`;
|
||||
const keepaliveErrorId = `${keepaliveInputId}-error`;
|
||||
@@ -195,10 +191,20 @@ export function VisualConfigEditor({
|
||||
values.streaming.nonstreamKeepaliveInterval === '0';
|
||||
|
||||
const portError = getValidationMessage(t, validationErrors?.port);
|
||||
const homePortError = getValidationMessage(t, validationErrors?.homePort);
|
||||
const logsMaxSizeError = getValidationMessage(t, validationErrors?.logsMaxTotalSizeMb);
|
||||
const errorLogsMaxFilesError = getValidationMessage(t, validationErrors?.errorLogsMaxFiles);
|
||||
const redisUsageQueueRetentionError = getValidationMessage(
|
||||
t,
|
||||
validationErrors?.redisUsageQueueRetentionSeconds
|
||||
);
|
||||
const requestRetryError = getValidationMessage(t, validationErrors?.requestRetry);
|
||||
const maxRetryCredentialsError = getValidationMessage(t, validationErrors?.maxRetryCredentials);
|
||||
const maxRetryIntervalError = getValidationMessage(t, validationErrors?.maxRetryInterval);
|
||||
const authAutoRefreshWorkersError = getValidationMessage(
|
||||
t,
|
||||
validationErrors?.authAutoRefreshWorkers
|
||||
);
|
||||
const keepaliveError = getValidationMessage(t, validationErrors?.['streaming.keepaliveSeconds']);
|
||||
const bootstrapRetriesError = getValidationMessage(
|
||||
t,
|
||||
@@ -233,6 +239,23 @@ export function VisualConfigEditor({
|
||||
(payloadFilterRules: PayloadFilterRule[]) => onChange({ payloadFilterRules }),
|
||||
[onChange]
|
||||
);
|
||||
const disableImageGenerationOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: 'false',
|
||||
label: t('config_management.visual.sections.network.disable_image_generation_false'),
|
||||
},
|
||||
{
|
||||
value: 'true',
|
||||
label: t('config_management.visual.sections.network.disable_image_generation_true'),
|
||||
},
|
||||
{
|
||||
value: 'chat',
|
||||
label: t('config_management.visual.sections.network.disable_image_generation_chat'),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const countErrors = useCallback(
|
||||
(fields: VisualConfigFieldPath[]) =>
|
||||
@@ -246,7 +269,7 @@ export function VisualConfigEditor({
|
||||
id: 'server',
|
||||
title: t('config_management.visual.sections.server.title'),
|
||||
icon: IconSettings,
|
||||
errorCount: countErrors(['port']),
|
||||
errorCount: countErrors(['port', 'homePort']),
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
@@ -259,10 +282,13 @@ export function VisualConfigEditor({
|
||||
title: t('config_management.visual.sections.system.title'),
|
||||
icon: IconDiamond,
|
||||
errorCount: countErrors([
|
||||
'errorLogsMaxFiles',
|
||||
'logsMaxTotalSizeMb',
|
||||
'redisUsageQueueRetentionSeconds',
|
||||
'requestRetry',
|
||||
'maxRetryCredentials',
|
||||
'maxRetryInterval',
|
||||
'authAutoRefreshWorkers',
|
||||
]),
|
||||
},
|
||||
{
|
||||
@@ -513,6 +539,102 @@ export function VisualConfigEditor({
|
||||
</SectionStack>
|
||||
</SectionSubsection>
|
||||
|
||||
<SectionSubsection
|
||||
title={t('config_management.visual.sections.home.title')}
|
||||
description={t('config_management.visual.sections.home.description')}
|
||||
>
|
||||
<SectionStack>
|
||||
<SectionGrid>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.home.enabled')}
|
||||
description={t('config_management.visual.sections.home.enabled_desc')}
|
||||
checked={values.homeEnabled}
|
||||
disabled={disabled}
|
||||
onChange={(homeEnabled) => onChange({ homeEnabled })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.home.disable_cluster_discovery')}
|
||||
description={t(
|
||||
'config_management.visual.sections.home.disable_cluster_discovery_desc'
|
||||
)}
|
||||
checked={values.homeDisableClusterDiscovery}
|
||||
disabled={disabled}
|
||||
onChange={(homeDisableClusterDiscovery) =>
|
||||
onChange({ homeDisableClusterDiscovery })
|
||||
}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<SectionGrid>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.home.host')}
|
||||
placeholder="127.0.0.1"
|
||||
value={values.homeHost}
|
||||
onChange={(e) => onChange({ homeHost: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.home.port')}
|
||||
type="number"
|
||||
placeholder="6379"
|
||||
value={values.homePort}
|
||||
onChange={(e) => onChange({ homePort: e.target.value })}
|
||||
disabled={disabled}
|
||||
error={homePortError}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.home.password')}
|
||||
type="password"
|
||||
placeholder={t('config_management.visual.sections.home.password_placeholder')}
|
||||
value={values.homePassword}
|
||||
onChange={(e) => onChange({ homePassword: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<Divider />
|
||||
<div className={styles.subsectionHeader}>
|
||||
<h3 className={styles.subsectionTitle}>
|
||||
{t('config_management.visual.sections.home.tls_title')}
|
||||
</h3>
|
||||
<p className={styles.subsectionDescription}>
|
||||
{t('config_management.visual.sections.home.tls_description')}
|
||||
</p>
|
||||
</div>
|
||||
<SectionGrid>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.home.tls_enable')}
|
||||
checked={values.homeTlsEnable}
|
||||
disabled={disabled}
|
||||
onChange={(homeTlsEnable) => onChange({ homeTlsEnable })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.home.tls_insecure')}
|
||||
description={t('config_management.visual.sections.home.tls_insecure_desc')}
|
||||
checked={values.homeTlsInsecureSkipVerify}
|
||||
disabled={disabled}
|
||||
onChange={(homeTlsInsecureSkipVerify) =>
|
||||
onChange({ homeTlsInsecureSkipVerify })
|
||||
}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<SectionGrid>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.home.tls_server_name')}
|
||||
placeholder="home.example.com"
|
||||
value={values.homeTlsServerName}
|
||||
onChange={(e) => onChange({ homeTlsServerName: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.home.tls_ca_cert')}
|
||||
placeholder="/path/to/ca.pem"
|
||||
value={values.homeTlsCaCert}
|
||||
onChange={(e) => onChange({ homeTlsCaCert: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SectionGrid>
|
||||
</SectionStack>
|
||||
</SectionSubsection>
|
||||
|
||||
<SectionSubsection
|
||||
title={t('config_management.visual.sections.remote.title')}
|
||||
description={t('config_management.visual.sections.remote.description')}
|
||||
@@ -533,6 +655,19 @@ export function VisualConfigEditor({
|
||||
disabled={disabled}
|
||||
onChange={(rmDisableControlPanel) => onChange({ rmDisableControlPanel })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t(
|
||||
'config_management.visual.sections.remote.disable_auto_update_panel'
|
||||
)}
|
||||
description={t(
|
||||
'config_management.visual.sections.remote.disable_auto_update_panel_desc'
|
||||
)}
|
||||
checked={values.rmDisableAutoUpdatePanel}
|
||||
disabled={disabled}
|
||||
onChange={(rmDisableAutoUpdatePanel) =>
|
||||
onChange({ rmDisableAutoUpdatePanel })
|
||||
}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<SectionGrid>
|
||||
<Input
|
||||
@@ -632,7 +767,151 @@ export function VisualConfigEditor({
|
||||
disabled={disabled}
|
||||
error={logsMaxSizeError}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.system.error_logs_max_files')}
|
||||
type="number"
|
||||
placeholder="10"
|
||||
value={values.errorLogsMaxFiles}
|
||||
onChange={(e) => onChange({ errorLogsMaxFiles: e.target.value })}
|
||||
disabled={disabled}
|
||||
error={errorLogsMaxFilesError}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.system.redis_usage_retention')}
|
||||
type="number"
|
||||
placeholder="60"
|
||||
value={values.redisUsageQueueRetentionSeconds}
|
||||
onChange={(e) => onChange({ redisUsageQueueRetentionSeconds: e.target.value })}
|
||||
disabled={disabled}
|
||||
hint={t('config_management.visual.sections.system.redis_usage_retention_hint')}
|
||||
error={redisUsageQueueRetentionError}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<SectionGrid>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.system.usage_statistics_enabled')}
|
||||
description={t(
|
||||
'config_management.visual.sections.system.usage_statistics_enabled_desc'
|
||||
)}
|
||||
checked={values.usageStatisticsEnabled}
|
||||
disabled={disabled}
|
||||
onChange={(usageStatisticsEnabled) => onChange({ usageStatisticsEnabled })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.system.antigravity_signature_cache')}
|
||||
description={t(
|
||||
'config_management.visual.sections.system.antigravity_signature_cache_desc'
|
||||
)}
|
||||
checked={values.antigravitySignatureCacheEnabled}
|
||||
disabled={disabled}
|
||||
onChange={(antigravitySignatureCacheEnabled) =>
|
||||
onChange({ antigravitySignatureCacheEnabled })
|
||||
}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.system.antigravity_signature_strict')}
|
||||
description={t(
|
||||
'config_management.visual.sections.system.antigravity_signature_strict_desc'
|
||||
)}
|
||||
checked={values.antigravitySignatureBypassStrict}
|
||||
disabled={disabled}
|
||||
onChange={(antigravitySignatureBypassStrict) =>
|
||||
onChange({ antigravitySignatureBypassStrict })
|
||||
}
|
||||
/>
|
||||
</SectionGrid>
|
||||
|
||||
<SectionSubsection
|
||||
title={t('config_management.visual.sections.headers.title')}
|
||||
description={t('config_management.visual.sections.headers.description')}
|
||||
>
|
||||
<SectionStack>
|
||||
<div className={styles.subsectionHeader}>
|
||||
<h3 className={styles.subsectionTitle}>
|
||||
{t('config_management.visual.sections.headers.claude_title')}
|
||||
</h3>
|
||||
</div>
|
||||
<SectionGrid>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.user_agent')}
|
||||
placeholder="claude-cli/2.1.44 (external, sdk-cli)"
|
||||
value={values.claudeHeaderUserAgent}
|
||||
onChange={(e) => onChange({ claudeHeaderUserAgent: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.package_version')}
|
||||
placeholder="0.74.0"
|
||||
value={values.claudeHeaderPackageVersion}
|
||||
onChange={(e) => onChange({ claudeHeaderPackageVersion: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.runtime_version')}
|
||||
placeholder="v24.3.0"
|
||||
value={values.claudeHeaderRuntimeVersion}
|
||||
onChange={(e) => onChange({ claudeHeaderRuntimeVersion: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.os')}
|
||||
placeholder="MacOS"
|
||||
value={values.claudeHeaderOs}
|
||||
onChange={(e) => onChange({ claudeHeaderOs: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.arch')}
|
||||
placeholder="arm64"
|
||||
value={values.claudeHeaderArch}
|
||||
onChange={(e) => onChange({ claudeHeaderArch: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.timeout')}
|
||||
placeholder="600"
|
||||
value={values.claudeHeaderTimeout}
|
||||
onChange={(e) => onChange({ claudeHeaderTimeout: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<SectionGrid>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.headers.stabilize_device')}
|
||||
description={t(
|
||||
'config_management.visual.sections.headers.stabilize_device_desc'
|
||||
)}
|
||||
checked={values.claudeHeaderStabilizeDeviceProfile}
|
||||
disabled={disabled}
|
||||
onChange={(claudeHeaderStabilizeDeviceProfile) =>
|
||||
onChange({ claudeHeaderStabilizeDeviceProfile })
|
||||
}
|
||||
/>
|
||||
</SectionGrid>
|
||||
<Divider />
|
||||
<div className={styles.subsectionHeader}>
|
||||
<h3 className={styles.subsectionTitle}>
|
||||
{t('config_management.visual.sections.headers.codex_title')}
|
||||
</h3>
|
||||
</div>
|
||||
<SectionGrid>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.user_agent')}
|
||||
placeholder="codex_cli_rs/0.114.0 (Mac OS 14.2.0; x86_64) vscode/1.111.0"
|
||||
value={values.codexHeaderUserAgent}
|
||||
onChange={(e) => onChange({ codexHeaderUserAgent: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.headers.beta_features')}
|
||||
placeholder="multi_agent"
|
||||
value={values.codexHeaderBetaFeatures}
|
||||
onChange={(e) => onChange({ codexHeaderBetaFeatures: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SectionGrid>
|
||||
</SectionStack>
|
||||
</SectionSubsection>
|
||||
|
||||
<SectionSubsection
|
||||
title={t('config_management.visual.sections.network.title')}
|
||||
@@ -677,6 +956,20 @@ export function VisualConfigEditor({
|
||||
disabled={disabled}
|
||||
error={maxRetryIntervalError}
|
||||
/>
|
||||
<Input
|
||||
label={t(
|
||||
'config_management.visual.sections.network.auth_auto_refresh_workers'
|
||||
)}
|
||||
type="number"
|
||||
placeholder="16"
|
||||
value={values.authAutoRefreshWorkers}
|
||||
onChange={(e) => onChange({ authAutoRefreshWorkers: e.target.value })}
|
||||
disabled={disabled}
|
||||
hint={t(
|
||||
'config_management.visual.sections.network.auth_auto_refresh_workers_hint'
|
||||
)}
|
||||
error={authAutoRefreshWorkersError}
|
||||
/>
|
||||
<FieldShell
|
||||
label={t('config_management.visual.sections.network.routing_strategy')}
|
||||
labelId={routingStrategyLabelId}
|
||||
@@ -710,6 +1003,31 @@ export function VisualConfigEditor({
|
||||
}
|
||||
/>
|
||||
</FieldShell>
|
||||
<FieldShell
|
||||
label={t(
|
||||
'config_management.visual.sections.network.disable_image_generation'
|
||||
)}
|
||||
labelId={disableImageGenerationLabelId}
|
||||
hint={t(
|
||||
'config_management.visual.sections.network.disable_image_generation_hint'
|
||||
)}
|
||||
hintId={disableImageGenerationHintId}
|
||||
>
|
||||
<Select
|
||||
value={values.disableImageGeneration}
|
||||
options={disableImageGenerationOptions}
|
||||
id={`${disableImageGenerationLabelId}-select`}
|
||||
disabled={disabled}
|
||||
ariaLabelledBy={disableImageGenerationLabelId}
|
||||
ariaDescribedBy={disableImageGenerationHintId}
|
||||
onChange={(nextValue) =>
|
||||
onChange({
|
||||
disableImageGeneration:
|
||||
nextValue as VisualConfigValues['disableImageGeneration'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FieldShell>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.network.session_affinity_ttl')}
|
||||
placeholder="1h"
|
||||
@@ -729,6 +1047,24 @@ export function VisualConfigEditor({
|
||||
disabled={disabled}
|
||||
onChange={(forceModelPrefix) => onChange({ forceModelPrefix })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.network.passthrough_headers')}
|
||||
description={t(
|
||||
'config_management.visual.sections.network.passthrough_headers_desc'
|
||||
)}
|
||||
checked={values.passthroughHeaders}
|
||||
disabled={disabled}
|
||||
onChange={(passthroughHeaders) => onChange({ passthroughHeaders })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.network.disable_cooling')}
|
||||
description={t(
|
||||
'config_management.visual.sections.network.disable_cooling_desc'
|
||||
)}
|
||||
checked={values.disableCooling}
|
||||
disabled={disabled}
|
||||
onChange={(disableCooling) => onChange({ disableCooling })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t('config_management.visual.sections.network.session_affinity')}
|
||||
checked={values.routingSessionAffinity}
|
||||
@@ -742,6 +1078,17 @@ export function VisualConfigEditor({
|
||||
disabled={disabled}
|
||||
onChange={(wsAuth) => onChange({ wsAuth })}
|
||||
/>
|
||||
<ToggleRow
|
||||
title={t(
|
||||
'config_management.visual.sections.network.enable_gemini_cli_endpoint'
|
||||
)}
|
||||
description={t(
|
||||
'config_management.visual.sections.network.enable_gemini_cli_endpoint_desc'
|
||||
)}
|
||||
checked={values.enableGeminiCliEndpoint}
|
||||
disabled={disabled}
|
||||
onChange={(enableGeminiCliEndpoint) => onChange({ enableGeminiCliEndpoint })}
|
||||
/>
|
||||
</SectionGrid>
|
||||
</SectionStack>
|
||||
</SectionSubsection>
|
||||
|
||||
@@ -8,6 +8,7 @@ import styles from './VisualConfigEditor.module.scss';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import type {
|
||||
PayloadFilterRule,
|
||||
PayloadHeaderEntry,
|
||||
PayloadModelEntry,
|
||||
PayloadParamEntry,
|
||||
PayloadParamValidationErrorCode,
|
||||
@@ -449,6 +450,17 @@ const StringListEditor = memo(function StringListEditor({
|
||||
);
|
||||
});
|
||||
|
||||
function hasPayloadModelAdvancedSettings(model: PayloadModelEntry) {
|
||||
return Boolean(
|
||||
model.fromProtocol ||
|
||||
(model.headers?.length ?? 0) > 0 ||
|
||||
(model.match?.length ?? 0) > 0 ||
|
||||
(model.notMatch?.length ?? 0) > 0 ||
|
||||
(model.exist?.length ?? 0) > 0 ||
|
||||
(model.notExist?.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
export const PayloadRulesEditor = memo(function PayloadRulesEditor({
|
||||
value,
|
||||
disabled,
|
||||
@@ -465,6 +477,31 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
|
||||
const { t } = useTranslation();
|
||||
const rules = value;
|
||||
const protocolOptions = useMemo(() => buildProtocolOptions(t, rules), [rules, t]);
|
||||
const fromProtocolOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: '',
|
||||
label: t('config_management.visual.payload_rules.provider_default'),
|
||||
},
|
||||
{
|
||||
value: 'openai',
|
||||
label: t('config_management.visual.payload_rules.provider_openai'),
|
||||
},
|
||||
{
|
||||
value: 'responses',
|
||||
label: t('config_management.visual.payload_rules.provider_responses'),
|
||||
},
|
||||
{
|
||||
value: 'gemini',
|
||||
label: t('config_management.visual.payload_rules.provider_gemini'),
|
||||
},
|
||||
{
|
||||
value: 'claude',
|
||||
label: t('config_management.visual.payload_rules.provider_claude'),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const payloadValueTypeOptions = useMemo(
|
||||
() =>
|
||||
VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS.map((option) => ({
|
||||
@@ -480,6 +517,7 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const [modelAdvancedOverrides, setModelAdvancedOverrides] = useState<Record<string, boolean>>({});
|
||||
|
||||
const addRule = () => onChange([...rules, { id: makeClientId(), models: [], params: [] }]);
|
||||
const removeRule = (ruleIndex: number) => onChange(rules.filter((_, i) => i !== ruleIndex));
|
||||
@@ -509,6 +547,79 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
|
||||
});
|
||||
};
|
||||
|
||||
const toggleModelAdvanced = (modelId: string, defaultExpanded: boolean) => {
|
||||
setModelAdvancedOverrides((current) => ({
|
||||
...current,
|
||||
[modelId]: !(current[modelId] ?? defaultExpanded),
|
||||
}));
|
||||
};
|
||||
|
||||
const addHeader = (ruleIndex: number, modelIndex: number) => {
|
||||
const rule = rules[ruleIndex];
|
||||
const model = rule.models[modelIndex];
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
headers: [...(model.headers ?? []), { id: makeClientId(), name: '', value: '' }],
|
||||
});
|
||||
};
|
||||
|
||||
const updateHeader = (
|
||||
ruleIndex: number,
|
||||
modelIndex: number,
|
||||
headerIndex: number,
|
||||
patch: Partial<PayloadHeaderEntry>
|
||||
) => {
|
||||
const model = rules[ruleIndex].models[modelIndex];
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
headers: (model.headers ?? []).map((header, i) =>
|
||||
i === headerIndex ? { ...header, ...patch } : header
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const removeHeader = (ruleIndex: number, modelIndex: number, headerIndex: number) => {
|
||||
const model = rules[ruleIndex].models[modelIndex];
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
headers: (model.headers ?? []).filter((_, i) => i !== headerIndex),
|
||||
});
|
||||
};
|
||||
|
||||
const addCondition = (ruleIndex: number, modelIndex: number, key: 'match' | 'notMatch') => {
|
||||
const model = rules[ruleIndex].models[modelIndex];
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
[key]: [
|
||||
...(model[key] ?? []),
|
||||
{ id: makeClientId(), path: '', valueType: 'string', value: '' },
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const updateCondition = (
|
||||
ruleIndex: number,
|
||||
modelIndex: number,
|
||||
key: 'match' | 'notMatch',
|
||||
conditionIndex: number,
|
||||
patch: Partial<PayloadParamEntry>
|
||||
) => {
|
||||
const model = rules[ruleIndex].models[modelIndex];
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
[key]: (model[key] ?? []).map((condition, i) =>
|
||||
i === conditionIndex ? { ...condition, ...patch } : condition
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const removeCondition = (
|
||||
ruleIndex: number,
|
||||
modelIndex: number,
|
||||
key: 'match' | 'notMatch',
|
||||
conditionIndex: number
|
||||
) => {
|
||||
const model = rules[ruleIndex].models[modelIndex];
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
[key]: (model[key] ?? []).filter((_, i) => i !== conditionIndex),
|
||||
});
|
||||
};
|
||||
|
||||
const addParam = (ruleIndex: number) => {
|
||||
const rule = rules[ruleIndex];
|
||||
const nextParam: PayloadParamEntry = {
|
||||
@@ -558,6 +669,62 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
|
||||
return getValidationMessage(t, errorCode);
|
||||
};
|
||||
|
||||
const renderConditionValueEditor = (
|
||||
ruleIndex: number,
|
||||
modelIndex: number,
|
||||
key: 'match' | 'notMatch',
|
||||
conditionIndex: number,
|
||||
condition: PayloadParamEntry
|
||||
) => {
|
||||
if (condition.valueType === 'boolean') {
|
||||
return (
|
||||
<Select
|
||||
value={
|
||||
condition.value.toLowerCase() === 'true' || condition.value.toLowerCase() === 'false'
|
||||
? condition.value.toLowerCase()
|
||||
: ''
|
||||
}
|
||||
options={booleanValueOptions}
|
||||
placeholder={t('config_management.visual.payload_rules.value_boolean')}
|
||||
disabled={disabled}
|
||||
ariaLabel={t('config_management.visual.payload_rules.condition_value')}
|
||||
onChange={(nextValue) =>
|
||||
updateCondition(ruleIndex, modelIndex, key, conditionIndex, { value: nextValue })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (condition.valueType === 'json') {
|
||||
return (
|
||||
<textarea
|
||||
className={`input ${styles.payloadJsonInput}`}
|
||||
placeholder={getValuePlaceholder(condition.valueType)}
|
||||
aria-label={t('config_management.visual.payload_rules.condition_value')}
|
||||
value={condition.value}
|
||||
onChange={(e) =>
|
||||
updateCondition(ruleIndex, modelIndex, key, conditionIndex, {
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpandableInput
|
||||
placeholder={getValuePlaceholder(condition.valueType)}
|
||||
ariaLabel={t('config_management.visual.payload_rules.condition_value')}
|
||||
value={condition.value}
|
||||
onChange={(nextValue) =>
|
||||
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({
|
||||
<div className={styles.blockLabel}>
|
||||
{t('config_management.visual.payload_rules.models')}
|
||||
</div>
|
||||
{(rule.models.length ? rule.models : []).map((model, modelIndex) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className={[
|
||||
styles.payloadRuleModelRow,
|
||||
protocolFirst ? styles.payloadRuleModelRowProtocolFirst : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{protocolFirst ? (
|
||||
<>
|
||||
<Select
|
||||
value={model.protocol ?? ''}
|
||||
options={protocolOptions}
|
||||
{(rule.models.length ? rule.models : []).map((model, modelIndex) => {
|
||||
const hasAdvancedSettings = hasPayloadModelAdvancedSettings(model);
|
||||
const advancedExpanded = modelAdvancedOverrides[model.id] ?? hasAdvancedSettings;
|
||||
|
||||
return (
|
||||
<div key={model.id} className={styles.payloadModelGroup}>
|
||||
<div
|
||||
className={[
|
||||
styles.payloadRuleModelRow,
|
||||
protocolFirst ? styles.payloadRuleModelRowProtocolFirst : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{protocolFirst ? (
|
||||
<>
|
||||
<Select
|
||||
value={model.protocol ?? ''}
|
||||
options={protocolOptions}
|
||||
disabled={disabled}
|
||||
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ExpandableInput
|
||||
placeholder={t('config_management.visual.payload_rules.model_name')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.model_name')}
|
||||
value={model.name}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, { name: nextValue })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ExpandableInput
|
||||
placeholder={t('config_management.visual.payload_rules.model_name')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.model_name')}
|
||||
value={model.name}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, { name: nextValue })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select
|
||||
value={model.protocol ?? ''}
|
||||
options={protocolOptions}
|
||||
disabled={disabled}
|
||||
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={styles.payloadRowActionButton}
|
||||
onClick={() => toggleModelAdvanced(model.id, hasAdvancedSettings)}
|
||||
disabled={disabled}
|
||||
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ExpandableInput
|
||||
placeholder={t('config_management.visual.payload_rules.model_name')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.model_name')}
|
||||
value={model.name}
|
||||
onChange={(nextValue) => updateModel(ruleIndex, modelIndex, { name: nextValue })}
|
||||
>
|
||||
{advancedExpanded
|
||||
? t('config_management.visual.payload_rules.hide_advanced')
|
||||
: t('config_management.visual.payload_rules.advanced')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={styles.payloadRowActionButton}
|
||||
onClick={() => removeModel(ruleIndex, modelIndex)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ExpandableInput
|
||||
placeholder={t('config_management.visual.payload_rules.model_name')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.model_name')}
|
||||
value={model.name}
|
||||
onChange={(nextValue) => updateModel(ruleIndex, modelIndex, { name: nextValue })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select
|
||||
value={model.protocol ?? ''}
|
||||
options={protocolOptions}
|
||||
disabled={disabled}
|
||||
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={styles.payloadRowActionButton}
|
||||
onClick={() => removeModel(ruleIndex, modelIndex)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('config_management.visual.common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
>
|
||||
{t('config_management.visual.common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{advancedExpanded ? (
|
||||
<div className={styles.payloadModelAdvanced}>
|
||||
<div className={styles.payloadAdvancedGrid}>
|
||||
<div className={styles.fieldShell}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('config_management.visual.payload_rules.from_protocol')}
|
||||
</label>
|
||||
<Select
|
||||
value={model.fromProtocol ?? ''}
|
||||
options={fromProtocolOptions}
|
||||
disabled={disabled}
|
||||
ariaLabel={t('config_management.visual.payload_rules.from_protocol')}
|
||||
onChange={(nextValue) =>
|
||||
updateModel(ruleIndex, modelIndex, {
|
||||
fromProtocol: (nextValue ||
|
||||
undefined) as PayloadModelEntry['fromProtocol'],
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.blockStack}>
|
||||
<div className={styles.blockLabel}>
|
||||
{t('config_management.visual.payload_rules.headers')}
|
||||
</div>
|
||||
{(model.headers ?? []).map((header, headerIndex) => (
|
||||
<div key={header.id} className={styles.payloadHeaderRow}>
|
||||
<ExpandableInput
|
||||
placeholder={t('config_management.visual.payload_rules.header_name')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.header_name')}
|
||||
value={header.name}
|
||||
onChange={(nextValue) =>
|
||||
updateHeader(ruleIndex, modelIndex, headerIndex, {
|
||||
name: nextValue,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ExpandableInput
|
||||
placeholder={t('config_management.visual.payload_rules.header_value')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.header_value')}
|
||||
value={header.value}
|
||||
onChange={(nextValue) =>
|
||||
updateHeader(ruleIndex, modelIndex, headerIndex, {
|
||||
value: nextValue,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={styles.payloadRowActionButton}
|
||||
onClick={() => removeHeader(ruleIndex, modelIndex, headerIndex)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('config_management.visual.common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.actionRow}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => addHeader(ruleIndex, modelIndex)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('config_management.visual.payload_rules.add_header')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(['match', 'notMatch'] as const).map((conditionKey) => (
|
||||
<div key={conditionKey} className={styles.blockStack}>
|
||||
<div className={styles.blockLabel}>
|
||||
{t(`config_management.visual.payload_rules.${conditionKey}`)}
|
||||
</div>
|
||||
{(model[conditionKey] ?? []).map((condition, conditionIndex) => {
|
||||
const conditionError = getValidationMessage(
|
||||
t,
|
||||
getPayloadParamValidationError(condition)
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={condition.id} className={styles.payloadRuleParamGroup}>
|
||||
<div className={styles.payloadRuleParamRow}>
|
||||
<ExpandableInput
|
||||
placeholder={t(
|
||||
'config_management.visual.payload_rules.condition_path'
|
||||
)}
|
||||
ariaLabel={t(
|
||||
'config_management.visual.payload_rules.condition_path'
|
||||
)}
|
||||
value={condition.path}
|
||||
onChange={(nextValue) =>
|
||||
updateCondition(
|
||||
ruleIndex,
|
||||
modelIndex,
|
||||
conditionKey,
|
||||
conditionIndex,
|
||||
{ path: nextValue }
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select
|
||||
value={condition.valueType}
|
||||
options={payloadValueTypeOptions}
|
||||
disabled={disabled}
|
||||
ariaLabel={t(
|
||||
'config_management.visual.payload_rules.param_type'
|
||||
)}
|
||||
onChange={(nextValue) =>
|
||||
updateCondition(
|
||||
ruleIndex,
|
||||
modelIndex,
|
||||
conditionKey,
|
||||
conditionIndex,
|
||||
{
|
||||
valueType: nextValue as PayloadParamValueType,
|
||||
value:
|
||||
nextValue === 'boolean'
|
||||
? 'true'
|
||||
: nextValue === 'json' &&
|
||||
condition.value.trim() === ''
|
||||
? '{}'
|
||||
: condition.value,
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
{renderConditionValueEditor(
|
||||
ruleIndex,
|
||||
modelIndex,
|
||||
conditionKey,
|
||||
conditionIndex,
|
||||
condition
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={styles.payloadRowActionButton}
|
||||
onClick={() =>
|
||||
removeCondition(
|
||||
ruleIndex,
|
||||
modelIndex,
|
||||
conditionKey,
|
||||
conditionIndex
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('config_management.visual.common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
{conditionError ? (
|
||||
<div className={`error-box ${styles.payloadParamError}`}>
|
||||
{conditionError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.actionRow}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => addCondition(ruleIndex, modelIndex, conditionKey)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('config_management.visual.payload_rules.add_condition')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className={styles.payloadAdvancedGrid}>
|
||||
<div className={styles.blockStack}>
|
||||
<div className={styles.blockLabel}>
|
||||
{t('config_management.visual.payload_rules.exist')}
|
||||
</div>
|
||||
<StringListEditor
|
||||
value={model.exist ?? []}
|
||||
disabled={disabled}
|
||||
placeholder={t('config_management.visual.payload_rules.condition_path')}
|
||||
inputAriaLabel={t(
|
||||
'config_management.visual.payload_rules.condition_path'
|
||||
)}
|
||||
onChange={(exist) => updateModel(ruleIndex, modelIndex, { exist })}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.blockStack}>
|
||||
<div className={styles.blockLabel}>
|
||||
{t('config_management.visual.payload_rules.notExist')}
|
||||
</div>
|
||||
<StringListEditor
|
||||
value={model.notExist ?? []}
|
||||
disabled={disabled}
|
||||
placeholder={t('config_management.visual.payload_rules.condition_path')}
|
||||
inputAriaLabel={t(
|
||||
'config_management.visual.payload_rules.condition_path'
|
||||
)}
|
||||
onChange={(notExist) =>
|
||||
updateModel(ruleIndex, modelIndex, { notExist })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.actionRow}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -731,7 +1131,9 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
|
||||
placeholder={t('config_management.visual.payload_rules.json_path')}
|
||||
ariaLabel={t('config_management.visual.payload_rules.json_path')}
|
||||
value={param.path}
|
||||
onChange={(nextValue) => updateParam(ruleIndex, paramIndex, { path: nextValue })}
|
||||
onChange={(nextValue) =>
|
||||
updateParam(ruleIndex, paramIndex, { path: nextValue })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{rawJsonValues ? null : (
|
||||
|
||||
+469
-87
@@ -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<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const header of headers ?? []) {
|
||||
const name = header.name.trim();
|
||||
if (!name) continue;
|
||||
result[name] = header.value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function serializePayloadConditionsForYaml(
|
||||
conditions?: PayloadParamEntry[]
|
||||
): Array<Record<string, unknown>> {
|
||||
const result: Array<Record<string, unknown>> = [];
|
||||
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<Record<string, unknown>> {
|
||||
return (models || [])
|
||||
.filter((m) => m.name?.trim())
|
||||
.map((m) => {
|
||||
const obj: Record<string, unknown> = { 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<Record<string, unknown>> {
|
||||
return rules
|
||||
.map((rule) => {
|
||||
const models = (rule.models || [])
|
||||
.filter((m) => m.name?.trim())
|
||||
.map((m) => {
|
||||
const obj: Record<string, unknown> = { name: m.name.trim() };
|
||||
if (m.protocol) obj.protocol = m.protocol;
|
||||
return obj;
|
||||
});
|
||||
const models = serializePayloadModelsForYaml(rule.models);
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
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<Record<string, unknown>> {
|
||||
return rules
|
||||
.map((rule) => {
|
||||
const models = (rule.models || [])
|
||||
.filter((m) => m.name?.trim())
|
||||
.map((m) => {
|
||||
const obj: Record<string, unknown> = { 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<Record<string, unknown>> {
|
||||
return rules
|
||||
.map((rule) => {
|
||||
const models = (rule.models || [])
|
||||
.filter((m) => m.name?.trim())
|
||||
.map((m) => {
|
||||
const obj: Record<string, unknown> = { name: m.name.trim() };
|
||||
if (m.protocol) obj.protocol = m.protocol;
|
||||
return obj;
|
||||
});
|
||||
const models = serializePayloadModelsForYaml(rule.models);
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
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<keyof VisualConfigValues>
|
||||
).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']);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Булево",
|
||||
|
||||
@@ -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": "布尔",
|
||||
|
||||
@@ -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": "布林",
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user