import { useCallback, useMemo, useState } from 'react'; import { isMap, parse as parseYaml, parseDocument } from 'yaml'; import type { PayloadFilterRule, PayloadParamValueType, PayloadRule, VisualConfigValues, } from '@/types/visualConfig'; import { DEFAULT_VISUAL_VALUES } from '@/types/visualConfig'; function asRecord(value: unknown): Record | null { if (value === null || typeof value !== 'object' || Array.isArray(value)) return null; return value as Record; } function extractApiKeyValue(raw: unknown): string | null { if (typeof raw === 'string') { const trimmed = raw.trim(); return trimmed ? trimmed : null; } const record = asRecord(raw); if (!record) return null; const candidates = [record['api-key'], record.apiKey, record.key, record.Key]; for (const candidate of candidates) { if (typeof candidate === 'string') { const trimmed = candidate.trim(); if (trimmed) return trimmed; } } return null; } function parseApiKeysText(raw: unknown): string { if (!Array.isArray(raw)) return ''; const keys: string[] = []; for (const item of raw) { const key = extractApiKeyValue(item); if (key) keys.push(key); } return keys.join('\n'); } type YamlDocument = ReturnType; type YamlPath = string[]; function docHas(doc: YamlDocument, path: YamlPath): boolean { return doc.hasIn(path); } function ensureMapInDoc(doc: YamlDocument, path: YamlPath): void { const existing = doc.getIn(path, true); if (isMap(existing)) return; doc.setIn(path, {}); } function deleteIfMapEmpty(doc: YamlDocument, path: YamlPath): void { const value = doc.getIn(path, true); if (!isMap(value)) return; if (value.items.length === 0) doc.deleteIn(path); } function setBooleanInDoc(doc: YamlDocument, path: YamlPath, value: boolean): void { if (value) { doc.setIn(path, true); return; } if (docHas(doc, path)) doc.setIn(path, false); } function setStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown): void { const safe = typeof value === 'string' ? value : ''; const trimmed = safe.trim(); if (trimmed !== '') { doc.setIn(path, safe); return; } if (docHas(doc, path)) doc.deleteIn(path); } function setIntFromStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown): void { const safe = typeof value === 'string' ? value : ''; const trimmed = safe.trim(); if (trimmed === '') { if (docHas(doc, path)) doc.deleteIn(path); return; } const parsed = Number.parseInt(trimmed, 10); if (Number.isFinite(parsed)) { doc.setIn(path, parsed); return; } if (docHas(doc, path)) doc.deleteIn(path); } function deepClone(value: T): T { if (typeof structuredClone === 'function') return structuredClone(value); return JSON.parse(JSON.stringify(value)) as T; } function parsePayloadParamValue(raw: unknown): { valueType: PayloadParamValueType; value: string } { if (typeof raw === 'number') { return { valueType: 'number', value: String(raw) }; } if (typeof raw === 'boolean') { return { valueType: 'boolean', value: String(raw) }; } if (raw === null || typeof raw === 'object') { try { const json = JSON.stringify(raw, null, 2); return { valueType: 'json', value: json ?? 'null' }; } catch { return { valueType: 'json', value: String(raw) }; } } return { valueType: 'string', value: String(raw ?? '') }; } const PAYLOAD_PROTOCOL_VALUES = [ 'openai', 'openai-response', 'gemini', 'claude', 'codex', 'antigravity', ] as const; type PayloadProtocol = (typeof PAYLOAD_PROTOCOL_VALUES)[number]; function parsePayloadProtocol(raw: unknown): PayloadProtocol | undefined { if (typeof raw !== 'string') return undefined; return PAYLOAD_PROTOCOL_VALUES.includes(raw as PayloadProtocol) ? (raw as PayloadProtocol) : undefined; } 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 paramsRecord = asRecord(record.params); const params = paramsRecord ? Object.entries(paramsRecord).map(([path, value], pIndex) => { const parsedValue = parsePayloadParamValue(value); return { id: `param-${index}-${pIndex}`, path, valueType: parsedValue.valueType, value: parsedValue.value, }; }) : []; return { id: `payload-rule-${index}`, models, params }; }); } function parsePayloadFilterRules(rules: unknown): PayloadFilterRule[] { 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: `filter-model-${index}-${modelIndex}`, name, protocol: parsePayloadProtocol(modelRecord?.protocol), }; }) : []; const paramsRaw = record.params; const params = Array.isArray(paramsRaw) ? paramsRaw.map(String) : []; return { id: `payload-filter-rule-${index}`, models, params }; }); } function serializePayloadRulesForYaml(rules: PayloadRule[]): Array> { return rules .map((rule) => { const models = (rule.models || []) .filter((m) => m.name?.trim()) .map((m) => { const obj: Record = { name: m.name.trim() }; if (m.protocol) obj.protocol = m.protocol; return obj; }); const params: Record = {}; for (const param of rule.params || []) { if (!param.path?.trim()) continue; let value: unknown = param.value; if (param.valueType === 'number') { const num = Number(param.value); value = Number.isFinite(num) ? num : param.value; } else if (param.valueType === 'boolean') { value = param.value === 'true'; } else if (param.valueType === 'json') { try { value = JSON.parse(param.value); } catch { value = param.value; } } params[param.path.trim()] = value; } return { models, params }; }) .filter((rule) => rule.models.length > 0); } function serializePayloadFilterRulesForYaml( rules: PayloadFilterRule[] ): Array> { return rules .map((rule) => { const models = (rule.models || []) .filter((m) => m.name?.trim()) .map((m) => { const obj: Record = { name: m.name.trim() }; if (m.protocol) obj.protocol = m.protocol; return obj; }); const params = (Array.isArray(rule.params) ? rule.params : []) .map((path) => String(path).trim()) .filter(Boolean); return { models, params }; }) .filter((rule) => rule.models.length > 0); } export function useVisualConfig() { const [visualValues, setVisualValuesState] = useState({ ...DEFAULT_VISUAL_VALUES, }); const [baselineValues, setBaselineValues] = useState({ ...DEFAULT_VISUAL_VALUES, }); const visualDirty = useMemo(() => { return JSON.stringify(visualValues) !== JSON.stringify(baselineValues); }, [baselineValues, visualValues]); const loadVisualValuesFromYaml = useCallback((yamlContent: string) => { try { const parsedRaw: unknown = parseYaml(yamlContent) || {}; const parsed = asRecord(parsedRaw) ?? {}; const tls = asRecord(parsed.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 newValues: VisualConfigValues = { host: typeof parsed.host === 'string' ? parsed.host : '', port: String(parsed.port ?? ''), tlsEnable: Boolean(tls?.enable), tlsCert: typeof tls?.cert === 'string' ? tls.cert : '', tlsKey: typeof tls?.key === 'string' ? tls.key : '', rmAllowRemote: Boolean(remoteManagement?.['allow-remote']), rmSecretKey: typeof remoteManagement?.['secret-key'] === 'string' ? remoteManagement['secret-key'] : '', rmDisableControlPanel: Boolean(remoteManagement?.['disable-control-panel']), rmPanelRepo: typeof remoteManagement?.['panel-github-repository'] === 'string' ? remoteManagement['panel-github-repository'] : typeof remoteManagement?.['panel-repo'] === 'string' ? remoteManagement['panel-repo'] : '', authDir: typeof parsed['auth-dir'] === 'string' ? parsed['auth-dir'] : '', apiKeysText: parseApiKeysText(parsed['api-keys']), debug: Boolean(parsed.debug), commercialMode: Boolean(parsed['commercial-mode']), loggingToFile: Boolean(parsed['logging-to-file']), logsMaxTotalSizeMb: String(parsed['logs-max-total-size-mb'] ?? ''), usageStatisticsEnabled: Boolean(parsed['usage-statistics-enabled']), proxyUrl: typeof parsed['proxy-url'] === 'string' ? parsed['proxy-url'] : '', forceModelPrefix: Boolean(parsed['force-model-prefix']), requestRetry: String(parsed['request-retry'] ?? ''), maxRetryInterval: String(parsed['max-retry-interval'] ?? ''), wsAuth: Boolean(parsed['ws-auth']), quotaSwitchProject: Boolean(quotaExceeded?.['switch-project'] ?? true), quotaSwitchPreviewModel: Boolean( quotaExceeded?.['switch-preview-model'] ?? true ), routingStrategy: routing?.strategy === 'fill-first' ? 'fill-first' : 'round-robin', payloadDefaultRules: parsePayloadRules(payload?.default), payloadOverrideRules: parsePayloadRules(payload?.override), payloadFilterRules: parsePayloadFilterRules(payload?.filter), streaming: { keepaliveSeconds: String(streaming?.['keepalive-seconds'] ?? ''), bootstrapRetries: String(streaming?.['bootstrap-retries'] ?? ''), nonstreamKeepaliveInterval: String(parsed['nonstream-keepalive-interval'] ?? ''), }, }; setVisualValuesState(newValues); setBaselineValues(deepClone(newValues)); } catch { setVisualValuesState({ ...DEFAULT_VISUAL_VALUES }); setBaselineValues(deepClone(DEFAULT_VISUAL_VALUES)); } }, []); const applyVisualChangesToYaml = useCallback( (currentYaml: string): string => { try { const doc = parseDocument(currentYaml); if (doc.errors.length > 0) return currentYaml; if (!isMap(doc.contents)) { doc.contents = doc.createNode({}) as unknown as typeof doc.contents; } const values = visualValues; setStringInDoc(doc, ['host'], values.host); setIntFromStringInDoc(doc, ['port'], values.port); if ( docHas(doc, ['tls']) || values.tlsEnable || values.tlsCert.trim() || values.tlsKey.trim() ) { ensureMapInDoc(doc, ['tls']); setBooleanInDoc(doc, ['tls', 'enable'], values.tlsEnable); setStringInDoc(doc, ['tls', 'cert'], values.tlsCert); setStringInDoc(doc, ['tls', 'key'], values.tlsKey); deleteIfMapEmpty(doc, ['tls']); } if ( docHas(doc, ['remote-management']) || values.rmAllowRemote || values.rmSecretKey.trim() || values.rmDisableControlPanel || values.rmPanelRepo.trim() ) { ensureMapInDoc(doc, ['remote-management']); setBooleanInDoc(doc, ['remote-management', 'allow-remote'], values.rmAllowRemote); setStringInDoc(doc, ['remote-management', 'secret-key'], values.rmSecretKey); setBooleanInDoc( doc, ['remote-management', 'disable-control-panel'], values.rmDisableControlPanel ); setStringInDoc(doc, ['remote-management', 'panel-github-repository'], values.rmPanelRepo); if (docHas(doc, ['remote-management', 'panel-repo'])) { doc.deleteIn(['remote-management', 'panel-repo']); } deleteIfMapEmpty(doc, ['remote-management']); } setStringInDoc(doc, ['auth-dir'], values.authDir); if (values.apiKeysText !== baselineValues.apiKeysText) { const apiKeys = values.apiKeysText .split('\n') .map((key) => key.trim()) .filter(Boolean); if (apiKeys.length > 0) { doc.setIn(['api-keys'], apiKeys); } else if (docHas(doc, ['api-keys'])) { doc.deleteIn(['api-keys']); } } setBooleanInDoc(doc, ['debug'], values.debug); setBooleanInDoc(doc, ['commercial-mode'], values.commercialMode); setBooleanInDoc(doc, ['logging-to-file'], values.loggingToFile); setIntFromStringInDoc(doc, ['logs-max-total-size-mb'], values.logsMaxTotalSizeMb); setBooleanInDoc(doc, ['usage-statistics-enabled'], values.usageStatisticsEnabled); setStringInDoc(doc, ['proxy-url'], values.proxyUrl); setBooleanInDoc(doc, ['force-model-prefix'], values.forceModelPrefix); setIntFromStringInDoc(doc, ['request-retry'], values.requestRetry); setIntFromStringInDoc(doc, ['max-retry-interval'], values.maxRetryInterval); setBooleanInDoc(doc, ['ws-auth'], values.wsAuth); if ( docHas(doc, ['quota-exceeded']) || !values.quotaSwitchProject || !values.quotaSwitchPreviewModel ) { ensureMapInDoc(doc, ['quota-exceeded']); doc.setIn(['quota-exceeded', 'switch-project'], values.quotaSwitchProject); doc.setIn( ['quota-exceeded', 'switch-preview-model'], values.quotaSwitchPreviewModel ); deleteIfMapEmpty(doc, ['quota-exceeded']); } if (docHas(doc, ['routing']) || values.routingStrategy !== 'round-robin') { ensureMapInDoc(doc, ['routing']); doc.setIn(['routing', 'strategy'], values.routingStrategy); deleteIfMapEmpty(doc, ['routing']); } const keepaliveSeconds = typeof values.streaming?.keepaliveSeconds === 'string' ? values.streaming.keepaliveSeconds : ''; const bootstrapRetries = typeof values.streaming?.bootstrapRetries === 'string' ? values.streaming.bootstrapRetries : ''; const nonstreamKeepaliveInterval = typeof values.streaming?.nonstreamKeepaliveInterval === 'string' ? values.streaming.nonstreamKeepaliveInterval : ''; const streamingDefined = docHas(doc, ['streaming']) || keepaliveSeconds.trim() || bootstrapRetries.trim(); if (streamingDefined) { ensureMapInDoc(doc, ['streaming']); setIntFromStringInDoc(doc, ['streaming', 'keepalive-seconds'], keepaliveSeconds); setIntFromStringInDoc(doc, ['streaming', 'bootstrap-retries'], bootstrapRetries); deleteIfMapEmpty(doc, ['streaming']); } setIntFromStringInDoc( doc, ['nonstream-keepalive-interval'], nonstreamKeepaliveInterval ); if ( docHas(doc, ['payload']) || values.payloadDefaultRules.length > 0 || values.payloadOverrideRules.length > 0 || values.payloadFilterRules.length > 0 ) { ensureMapInDoc(doc, ['payload']); if (values.payloadDefaultRules.length > 0) { doc.setIn( ['payload', 'default'], serializePayloadRulesForYaml(values.payloadDefaultRules) ); } else if (docHas(doc, ['payload', 'default'])) { doc.deleteIn(['payload', 'default']); } if (values.payloadOverrideRules.length > 0) { doc.setIn( ['payload', 'override'], serializePayloadRulesForYaml(values.payloadOverrideRules) ); } else if (docHas(doc, ['payload', 'override'])) { doc.deleteIn(['payload', 'override']); } if (values.payloadFilterRules.length > 0) { doc.setIn( ['payload', 'filter'], serializePayloadFilterRulesForYaml(values.payloadFilterRules) ); } else if (docHas(doc, ['payload', 'filter'])) { doc.deleteIn(['payload', 'filter']); } deleteIfMapEmpty(doc, ['payload']); } return doc.toString({ indent: 2, lineWidth: 120, minContentWidth: 0 }); } catch { return currentYaml; } }, [baselineValues, visualValues] ); const setVisualValues = useCallback((newValues: Partial) => { setVisualValuesState((prev) => { const next: VisualConfigValues = { ...prev, ...newValues } as VisualConfigValues; if (newValues.streaming) { next.streaming = { ...prev.streaming, ...newValues.streaming }; } return next; }); }, []); return { visualValues, visualDirty, loadVisualValuesFromYaml, applyVisualChangesToYaml, setVisualValues, }; } export const VISUAL_CONFIG_PROTOCOL_OPTIONS = [ { value: '', labelKey: 'config_management.visual.payload_rules.provider_default', defaultLabel: 'Default', }, { value: 'openai', labelKey: 'config_management.visual.payload_rules.provider_openai', defaultLabel: 'OpenAI', }, { value: 'openai-response', labelKey: 'config_management.visual.payload_rules.provider_openai_response', defaultLabel: 'OpenAI Response', }, { value: 'gemini', labelKey: 'config_management.visual.payload_rules.provider_gemini', defaultLabel: 'Gemini', }, { value: 'claude', labelKey: 'config_management.visual.payload_rules.provider_claude', defaultLabel: 'Claude', }, { value: 'codex', labelKey: 'config_management.visual.payload_rules.provider_codex', defaultLabel: 'Codex', }, { value: 'antigravity', labelKey: 'config_management.visual.payload_rules.provider_antigravity', defaultLabel: 'Antigravity', }, ] as const; export const VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS = [ { value: 'string', labelKey: 'config_management.visual.payload_rules.value_type_string', defaultLabel: 'String', }, { value: 'number', labelKey: 'config_management.visual.payload_rules.value_type_number', defaultLabel: 'Number', }, { value: 'boolean', labelKey: 'config_management.visual.payload_rules.value_type_boolean', defaultLabel: 'Boolean', }, { value: 'json', labelKey: 'config_management.visual.payload_rules.value_type_json', defaultLabel: 'JSON', }, ] as const satisfies ReadonlyArray<{ value: PayloadParamValueType; labelKey: string; defaultLabel: string; }>;