mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 19:20:49 +08:00
fix(config-editor): preserve comments when saving config.yaml in visual mode
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
import { isMap, parse as parseYaml, parseDocument } from 'yaml';
|
||||||
import type {
|
import type {
|
||||||
PayloadFilterRule,
|
PayloadFilterRule,
|
||||||
PayloadParamValueType,
|
PayloadParamValueType,
|
||||||
@@ -8,10 +8,6 @@ import type {
|
|||||||
} from '@/types/visualConfig';
|
} from '@/types/visualConfig';
|
||||||
import { DEFAULT_VISUAL_VALUES } from '@/types/visualConfig';
|
import { DEFAULT_VISUAL_VALUES } from '@/types/visualConfig';
|
||||||
|
|
||||||
function hasOwn(obj: unknown, key: string): obj is Record<string, unknown> {
|
|
||||||
return obj !== null && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||||
return value as Record<string, unknown>;
|
return value as Record<string, unknown>;
|
||||||
@@ -48,53 +44,58 @@ function parseApiKeysText(raw: unknown): string {
|
|||||||
return keys.join('\n');
|
return keys.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureRecord(parent: Record<string, unknown>, key: string): Record<string, unknown> {
|
type YamlDocument = ReturnType<typeof parseDocument>;
|
||||||
const existing = asRecord(parent[key]);
|
type YamlPath = string[];
|
||||||
if (existing) return existing;
|
|
||||||
const next: Record<string, unknown> = {};
|
function docHas(doc: YamlDocument, path: YamlPath): boolean {
|
||||||
parent[key] = next;
|
return doc.hasIn(path);
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteIfEmpty(parent: Record<string, unknown>, key: string): void {
|
function ensureMapInDoc(doc: YamlDocument, path: YamlPath): void {
|
||||||
const value = asRecord(parent[key]);
|
const existing = doc.getIn(path, true);
|
||||||
if (!value) return;
|
if (isMap(existing)) return;
|
||||||
if (Object.keys(value).length === 0) delete parent[key];
|
doc.setIn(path, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBoolean(obj: Record<string, unknown>, key: string, value: boolean): void {
|
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) {
|
if (value) {
|
||||||
obj[key] = true;
|
doc.setIn(path, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasOwn(obj, key)) obj[key] = false;
|
if (docHas(doc, path)) doc.setIn(path, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setString(obj: Record<string, unknown>, key: string, value: unknown): void {
|
function setStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown): void {
|
||||||
const safe = typeof value === 'string' ? value : '';
|
const safe = typeof value === 'string' ? value : '';
|
||||||
const trimmed = safe.trim();
|
const trimmed = safe.trim();
|
||||||
if (trimmed !== '') {
|
if (trimmed !== '') {
|
||||||
obj[key] = safe;
|
doc.setIn(path, safe);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasOwn(obj, key)) delete obj[key];
|
if (docHas(doc, path)) doc.deleteIn(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIntFromString(obj: Record<string, unknown>, key: string, value: unknown): void {
|
function setIntFromStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown): void {
|
||||||
const safe = typeof value === 'string' ? value : '';
|
const safe = typeof value === 'string' ? value : '';
|
||||||
const trimmed = safe.trim();
|
const trimmed = safe.trim();
|
||||||
if (trimmed === '') {
|
if (trimmed === '') {
|
||||||
if (hasOwn(obj, key)) delete obj[key];
|
if (docHas(doc, path)) doc.deleteIn(path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = Number.parseInt(trimmed, 10);
|
const parsed = Number.parseInt(trimmed, 10);
|
||||||
if (Number.isFinite(parsed)) {
|
if (Number.isFinite(parsed)) {
|
||||||
obj[key] = parsed;
|
doc.setIn(path, parsed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOwn(obj, key)) delete obj[key];
|
if (docHas(doc, path)) doc.deleteIn(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepClone<T>(value: T): T {
|
function deepClone<T>(value: T): T {
|
||||||
@@ -351,78 +352,95 @@ export function useVisualConfig() {
|
|||||||
const applyVisualChangesToYaml = useCallback(
|
const applyVisualChangesToYaml = useCallback(
|
||||||
(currentYaml: string): string => {
|
(currentYaml: string): string => {
|
||||||
try {
|
try {
|
||||||
const parsed = (parseYaml(currentYaml) || {}) as Record<string, unknown>;
|
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;
|
const values = visualValues;
|
||||||
|
|
||||||
setString(parsed, 'host', values.host);
|
setStringInDoc(doc, ['host'], values.host);
|
||||||
setIntFromString(parsed, 'port', values.port);
|
setIntFromStringInDoc(doc, ['port'], values.port);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasOwn(parsed, 'tls') ||
|
docHas(doc, ['tls']) ||
|
||||||
values.tlsEnable ||
|
values.tlsEnable ||
|
||||||
values.tlsCert.trim() ||
|
values.tlsCert.trim() ||
|
||||||
values.tlsKey.trim()
|
values.tlsKey.trim()
|
||||||
) {
|
) {
|
||||||
const tls = ensureRecord(parsed, 'tls');
|
ensureMapInDoc(doc, ['tls']);
|
||||||
setBoolean(tls, 'enable', values.tlsEnable);
|
setBooleanInDoc(doc, ['tls', 'enable'], values.tlsEnable);
|
||||||
setString(tls, 'cert', values.tlsCert);
|
setStringInDoc(doc, ['tls', 'cert'], values.tlsCert);
|
||||||
setString(tls, 'key', values.tlsKey);
|
setStringInDoc(doc, ['tls', 'key'], values.tlsKey);
|
||||||
deleteIfEmpty(parsed, 'tls');
|
deleteIfMapEmpty(doc, ['tls']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasOwn(parsed, 'remote-management') ||
|
docHas(doc, ['remote-management']) ||
|
||||||
values.rmAllowRemote ||
|
values.rmAllowRemote ||
|
||||||
values.rmSecretKey.trim() ||
|
values.rmSecretKey.trim() ||
|
||||||
values.rmDisableControlPanel ||
|
values.rmDisableControlPanel ||
|
||||||
values.rmPanelRepo.trim()
|
values.rmPanelRepo.trim()
|
||||||
) {
|
) {
|
||||||
const rm = ensureRecord(parsed, 'remote-management');
|
ensureMapInDoc(doc, ['remote-management']);
|
||||||
setBoolean(rm, 'allow-remote', values.rmAllowRemote);
|
setBooleanInDoc(doc, ['remote-management', 'allow-remote'], values.rmAllowRemote);
|
||||||
setString(rm, 'secret-key', values.rmSecretKey);
|
setStringInDoc(doc, ['remote-management', 'secret-key'], values.rmSecretKey);
|
||||||
setBoolean(rm, 'disable-control-panel', values.rmDisableControlPanel);
|
setBooleanInDoc(
|
||||||
setString(rm, 'panel-github-repository', values.rmPanelRepo);
|
doc,
|
||||||
if (hasOwn(rm, 'panel-repo')) delete rm['panel-repo'];
|
['remote-management', 'disable-control-panel'],
|
||||||
deleteIfEmpty(parsed, 'remote-management');
|
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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
setString(parsed, 'auth-dir', values.authDir);
|
setStringInDoc(doc, ['auth-dir'], values.authDir);
|
||||||
if (values.apiKeysText !== baselineValues.apiKeysText) {
|
if (values.apiKeysText !== baselineValues.apiKeysText) {
|
||||||
const apiKeys = values.apiKeysText
|
const apiKeys = values.apiKeysText
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((key) => key.trim())
|
.map((key) => key.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (apiKeys.length > 0) {
|
if (apiKeys.length > 0) {
|
||||||
parsed['api-keys'] = apiKeys;
|
doc.setIn(['api-keys'], apiKeys);
|
||||||
} else if (hasOwn(parsed, 'api-keys')) {
|
} else if (docHas(doc, ['api-keys'])) {
|
||||||
delete parsed['api-keys'];
|
doc.deleteIn(['api-keys']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setBoolean(parsed, 'debug', values.debug);
|
setBooleanInDoc(doc, ['debug'], values.debug);
|
||||||
|
|
||||||
setBoolean(parsed, 'commercial-mode', values.commercialMode);
|
setBooleanInDoc(doc, ['commercial-mode'], values.commercialMode);
|
||||||
setBoolean(parsed, 'logging-to-file', values.loggingToFile);
|
setBooleanInDoc(doc, ['logging-to-file'], values.loggingToFile);
|
||||||
setIntFromString(parsed, 'logs-max-total-size-mb', values.logsMaxTotalSizeMb);
|
setIntFromStringInDoc(doc, ['logs-max-total-size-mb'], values.logsMaxTotalSizeMb);
|
||||||
setBoolean(parsed, 'usage-statistics-enabled', values.usageStatisticsEnabled);
|
setBooleanInDoc(doc, ['usage-statistics-enabled'], values.usageStatisticsEnabled);
|
||||||
|
|
||||||
setString(parsed, 'proxy-url', values.proxyUrl);
|
setStringInDoc(doc, ['proxy-url'], values.proxyUrl);
|
||||||
setBoolean(parsed, 'force-model-prefix', values.forceModelPrefix);
|
setBooleanInDoc(doc, ['force-model-prefix'], values.forceModelPrefix);
|
||||||
setIntFromString(parsed, 'request-retry', values.requestRetry);
|
setIntFromStringInDoc(doc, ['request-retry'], values.requestRetry);
|
||||||
setIntFromString(parsed, 'max-retry-interval', values.maxRetryInterval);
|
setIntFromStringInDoc(doc, ['max-retry-interval'], values.maxRetryInterval);
|
||||||
setBoolean(parsed, 'ws-auth', values.wsAuth);
|
setBooleanInDoc(doc, ['ws-auth'], values.wsAuth);
|
||||||
|
|
||||||
if (hasOwn(parsed, 'quota-exceeded') || !values.quotaSwitchProject || !values.quotaSwitchPreviewModel) {
|
if (
|
||||||
const quota = ensureRecord(parsed, 'quota-exceeded');
|
docHas(doc, ['quota-exceeded']) ||
|
||||||
quota['switch-project'] = values.quotaSwitchProject;
|
!values.quotaSwitchProject ||
|
||||||
quota['switch-preview-model'] = values.quotaSwitchPreviewModel;
|
!values.quotaSwitchPreviewModel
|
||||||
deleteIfEmpty(parsed, 'quota-exceeded');
|
) {
|
||||||
|
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 (hasOwn(parsed, 'routing') || values.routingStrategy !== 'round-robin') {
|
if (docHas(doc, ['routing']) || values.routingStrategy !== 'round-robin') {
|
||||||
const routing = ensureRecord(parsed, 'routing');
|
ensureMapInDoc(doc, ['routing']);
|
||||||
routing.strategy = values.routingStrategy;
|
doc.setIn(['routing', 'strategy'], values.routingStrategy);
|
||||||
deleteIfEmpty(parsed, 'routing');
|
deleteIfMapEmpty(doc, ['routing']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keepaliveSeconds =
|
const keepaliveSeconds =
|
||||||
@@ -435,42 +453,55 @@ export function useVisualConfig() {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
const streamingDefined =
|
const streamingDefined =
|
||||||
hasOwn(parsed, 'streaming') || keepaliveSeconds.trim() || bootstrapRetries.trim();
|
docHas(doc, ['streaming']) || keepaliveSeconds.trim() || bootstrapRetries.trim();
|
||||||
if (streamingDefined) {
|
if (streamingDefined) {
|
||||||
const streaming = ensureRecord(parsed, 'streaming');
|
ensureMapInDoc(doc, ['streaming']);
|
||||||
setIntFromString(streaming, 'keepalive-seconds', keepaliveSeconds);
|
setIntFromStringInDoc(doc, ['streaming', 'keepalive-seconds'], keepaliveSeconds);
|
||||||
setIntFromString(streaming, 'bootstrap-retries', bootstrapRetries);
|
setIntFromStringInDoc(doc, ['streaming', 'bootstrap-retries'], bootstrapRetries);
|
||||||
deleteIfEmpty(parsed, 'streaming');
|
deleteIfMapEmpty(doc, ['streaming']);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIntFromString(parsed, 'nonstream-keepalive-interval', nonstreamKeepaliveInterval);
|
setIntFromStringInDoc(
|
||||||
|
doc,
|
||||||
|
['nonstream-keepalive-interval'],
|
||||||
|
nonstreamKeepaliveInterval
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasOwn(parsed, 'payload') ||
|
docHas(doc, ['payload']) ||
|
||||||
values.payloadDefaultRules.length > 0 ||
|
values.payloadDefaultRules.length > 0 ||
|
||||||
values.payloadOverrideRules.length > 0 ||
|
values.payloadOverrideRules.length > 0 ||
|
||||||
values.payloadFilterRules.length > 0
|
values.payloadFilterRules.length > 0
|
||||||
) {
|
) {
|
||||||
const payload = ensureRecord(parsed, 'payload');
|
ensureMapInDoc(doc, ['payload']);
|
||||||
if (values.payloadDefaultRules.length > 0) {
|
if (values.payloadDefaultRules.length > 0) {
|
||||||
payload.default = serializePayloadRulesForYaml(values.payloadDefaultRules);
|
doc.setIn(
|
||||||
} else if (hasOwn(payload, 'default')) {
|
['payload', 'default'],
|
||||||
delete payload.default;
|
serializePayloadRulesForYaml(values.payloadDefaultRules)
|
||||||
|
);
|
||||||
|
} else if (docHas(doc, ['payload', 'default'])) {
|
||||||
|
doc.deleteIn(['payload', 'default']);
|
||||||
}
|
}
|
||||||
if (values.payloadOverrideRules.length > 0) {
|
if (values.payloadOverrideRules.length > 0) {
|
||||||
payload.override = serializePayloadRulesForYaml(values.payloadOverrideRules);
|
doc.setIn(
|
||||||
} else if (hasOwn(payload, 'override')) {
|
['payload', 'override'],
|
||||||
delete payload.override;
|
serializePayloadRulesForYaml(values.payloadOverrideRules)
|
||||||
|
);
|
||||||
|
} else if (docHas(doc, ['payload', 'override'])) {
|
||||||
|
doc.deleteIn(['payload', 'override']);
|
||||||
}
|
}
|
||||||
if (values.payloadFilterRules.length > 0) {
|
if (values.payloadFilterRules.length > 0) {
|
||||||
payload.filter = serializePayloadFilterRulesForYaml(values.payloadFilterRules);
|
doc.setIn(
|
||||||
} else if (hasOwn(payload, 'filter')) {
|
['payload', 'filter'],
|
||||||
delete payload.filter;
|
serializePayloadFilterRulesForYaml(values.payloadFilterRules)
|
||||||
|
);
|
||||||
|
} else if (docHas(doc, ['payload', 'filter'])) {
|
||||||
|
doc.deleteIn(['payload', 'filter']);
|
||||||
}
|
}
|
||||||
deleteIfEmpty(parsed, 'payload');
|
deleteIfMapEmpty(doc, ['payload']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stringifyYaml(parsed, { indent: 2, lineWidth: 120, minContentWidth: 0 });
|
return doc.toString({ indent: 2, lineWidth: 120, minContentWidth: 0 });
|
||||||
} catch {
|
} catch {
|
||||||
return currentYaml;
|
return currentYaml;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user