Files
dbx/apps/desktop/src/lib/nacosAdmin.ts
T
二丫讲梵 380d497fd3 fix(nacos): 配置导出功能与配置格式完善
- 添加导出配置文件名生成逻辑
- 实现剪贴板内容验证功能
- 更新多语言支持的导出相关文本
- 增强配置格式处理逻辑
2026-06-25 19:22:55 +08:00

355 lines
12 KiB
TypeScript

import type { NacosConfigHistoryItem, NacosConfigItem, NacosInstanceInfo, NacosRawRequest, NacosServiceInfo } from "@/types/nacos";
import { diffChars, diffLines } from "diff";
export type NacosRawTemplateKey = "serverState" | "namespaceList" | "configDetail" | "serviceList" | "instanceList";
export interface NacosRawTemplate {
key: NacosRawTemplateKey;
method: string;
path: string;
query: string;
body: string;
}
export const NACOS_RAW_TEMPLATES: NacosRawTemplate[] = [
{ key: "serverState", method: "GET", path: "/v3/console/server/state", query: "", body: "" },
{ key: "namespaceList", method: "GET", path: "/v3/console/core/namespace/list", query: "", body: "" },
{
key: "configDetail",
method: "GET",
path: "/v3/console/cs/config",
query: "dataId=application.yaml&groupName=DEFAULT_GROUP&namespaceId=",
body: "",
},
{
key: "serviceList",
method: "GET",
path: "/v3/console/ns/service/list",
query: "pageNo=1&pageSize=20&namespaceId=",
body: "",
},
{
key: "instanceList",
method: "GET",
path: "/v3/console/ns/instance/list",
query: "serviceName=DEFAULT_GROUP@@example&namespaceId=",
body: "",
},
];
export function parseNacosRawQuery(text: string): Record<string, string> | undefined {
const trimmed = text.trim().replace(/^\?/, "");
if (!trimmed) return undefined;
return Object.fromEntries(new URLSearchParams(trimmed).entries());
}
export function parseNacosRawBody(text: string): unknown {
const trimmed = text.trim();
if (!trimmed) return undefined;
try {
return JSON.parse(trimmed);
} catch {
return text;
}
}
export function buildNacosRawRequest(method: string, path: string, queryText: string, bodyText: string): NacosRawRequest {
return {
method,
path: path.trim(),
query: parseNacosRawQuery(queryText),
body: parseNacosRawBody(bodyText),
};
}
export function isNacosRawMutation(method: string): boolean {
return method.trim().toUpperCase() !== "GET";
}
export function formatNacosConfigIdentity(item: Pick<NacosConfigItem, "namespace" | "dataId" | "group">, fallbackNamespace = ""): string {
return [`namespace=${item.namespace || fallbackNamespace || "public"}`, `dataId=${item.dataId}`, `group=${item.group || "DEFAULT_GROUP"}`].join("\n");
}
export function buildNacosConfigExport(item: NacosConfigItem, content: string): string {
return [`# namespace: ${item.namespace || "public"}`, `# dataId: ${item.dataId}`, `# group: ${item.group || "DEFAULT_GROUP"}`, item.configType ? `# type: ${item.configType}` : "", "", content].filter((line, index) => index >= 4 || line).join("\n");
}
export function buildNacosConfigCopy(item: NacosConfigItem, content: string): string {
return `${formatNacosConfigIdentity(item)}\n\n${content}`;
}
export function resolveNacosConfigCopyText(selectionText: string, editorText: string | undefined, stateText: string): string {
return selectionText || editorText || stateText;
}
export function sanitizeNacosConfigFileNameSegment(value: string): string {
const sanitized = value
.trim()
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_")
.replace(/\s+/g, " ")
.replace(/^[._\s-]+/, "")
.replace(/\.+$/, "");
return sanitized || "nacos-config";
}
export function nacosConfigFileExtension(configType?: string): string {
const normalized = configType?.trim().toLowerCase();
if (normalized === "yaml" || normalized === "yml") return "yaml";
if (normalized === "json") return "json";
if (normalized === "xml") return "xml";
if (normalized === "html") return "html";
if (normalized === "properties" || normalized === "props") return "properties";
if (normalized === "toml") return "toml";
return "txt";
}
export function buildNacosConfigExportFileName(item: Pick<NacosConfigItem, "dataId" | "configType">): string {
const baseName = sanitizeNacosConfigFileNameSegment(item.dataId || "nacos-config");
if (/\.[A-Za-z0-9][A-Za-z0-9_-]{0,15}$/.test(baseName)) return baseName;
return `${baseName}.${nacosConfigFileExtension(item.configType)}`;
}
export function createNacosSaveAsCopy(item: NacosConfigItem): NacosConfigItem {
return {
...item,
dataId: item.dataId ? `${item.dataId}.copy` : "",
content: item.content ?? "",
};
}
export interface NacosDiffSummary {
changed: boolean;
addedLines: number;
removedLines: number;
preview: string;
}
export type NacosDiffLineType = "equal" | "delete" | "insert" | "modify" | "padding";
export interface NacosInlineSegment {
value: string;
changed: boolean;
}
export interface NacosSideBySideDiffRow {
id: string;
leftLineNumber: number | null;
rightLineNumber: number | null;
leftContent: string;
rightContent: string;
leftType: NacosDiffLineType;
rightType: NacosDiffLineType;
leftInline: NacosInlineSegment[];
rightInline: NacosInlineSegment[];
}
export interface NacosInlineDiffRow {
id: string;
lineNumber: number | null;
content: string;
type: Exclude<NacosDiffLineType, "modify" | "padding">;
segments: NacosInlineSegment[];
}
export function summarizeNacosConfigDiff(before: string, after: string, maxPreviewLines = 40): NacosDiffSummary {
if (before === after) {
return { changed: false, addedLines: 0, removedLines: 0, preview: "No content changes." };
}
const beforeLines = before.split(/\r?\n/);
const afterLines = after.split(/\r?\n/);
const max = Math.max(beforeLines.length, afterLines.length);
const lines: string[] = [];
let addedLines = 0;
let removedLines = 0;
for (let index = 0; index < max; index += 1) {
const left = beforeLines[index];
const right = afterLines[index];
if (left === right) continue;
if (left !== undefined) {
removedLines += 1;
lines.push(`- ${left}`);
}
if (right !== undefined) {
addedLines += 1;
lines.push(`+ ${right}`);
}
if (lines.length >= maxPreviewLines) {
lines.push("...");
break;
}
}
return { changed: true, addedLines, removedLines, preview: lines.join("\n") };
}
function normalizeNacosDiffText(value: string): string {
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
function splitDiffLines(value: string): string[] {
const lines = normalizeNacosDiffText(value).split("\n");
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
return lines;
}
function inlineSegments(left: string, right: string): { left: NacosInlineSegment[]; right: NacosInlineSegment[] } {
const changes = diffChars(left, right);
const leftSegments: NacosInlineSegment[] = [];
const rightSegments: NacosInlineSegment[] = [];
for (const change of changes) {
if (change.removed) {
leftSegments.push({ value: change.value, changed: true });
} else if (change.added) {
rightSegments.push({ value: change.value, changed: true });
} else {
leftSegments.push({ value: change.value, changed: false });
rightSegments.push({ value: change.value, changed: false });
}
}
return { left: leftSegments, right: rightSegments };
}
function pairChangedLines(leftLines: string[], rightLines: string[], leftStart: number, rightStart: number, rows: NacosSideBySideDiffRow[], nextId: () => string) {
const max = Math.max(leftLines.length, rightLines.length);
for (let index = 0; index < max; index += 1) {
const left = leftLines[index];
const right = rightLines[index];
const hasLeft = left !== undefined;
const hasRight = right !== undefined;
const inline = hasLeft && hasRight ? inlineSegments(left, right) : { left: [], right: [] };
rows.push({
id: nextId(),
leftLineNumber: hasLeft ? leftStart + index : null,
rightLineNumber: hasRight ? rightStart + index : null,
leftContent: left ?? "",
rightContent: right ?? "",
leftType: hasLeft ? (hasRight ? "modify" : "delete") : "padding",
rightType: hasRight ? (hasLeft ? "modify" : "insert") : "padding",
leftInline: inline.left,
rightInline: inline.right,
});
}
}
export function buildNacosSideBySideDiff(before: string, after: string): NacosSideBySideDiffRow[] {
const changes = diffLines(normalizeNacosDiffText(before), normalizeNacosDiffText(after), { newlineIsToken: false });
const rows: NacosSideBySideDiffRow[] = [];
let leftLineNumber = 1;
let rightLineNumber = 1;
let id = 0;
const nextId = () => `nacos-diff-${id++}`;
for (let index = 0; index < changes.length; index += 1) {
const change = changes[index];
if (!change.added && !change.removed) {
for (const line of splitDiffLines(change.value)) {
rows.push({
id: nextId(),
leftLineNumber,
rightLineNumber,
leftContent: line,
rightContent: line,
leftType: "equal",
rightType: "equal",
leftInline: [{ value: line, changed: false }],
rightInline: [{ value: line, changed: false }],
});
leftLineNumber += 1;
rightLineNumber += 1;
}
continue;
}
if (change.removed) {
const leftLines = splitDiffLines(change.value);
const next = changes[index + 1];
if (next?.added) {
const rightLines = splitDiffLines(next.value);
pairChangedLines(leftLines, rightLines, leftLineNumber, rightLineNumber, rows, nextId);
leftLineNumber += leftLines.length;
rightLineNumber += rightLines.length;
index += 1;
} else {
pairChangedLines(leftLines, [], leftLineNumber, rightLineNumber, rows, nextId);
leftLineNumber += leftLines.length;
}
continue;
}
if (change.added) {
const rightLines = splitDiffLines(change.value);
pairChangedLines([], rightLines, leftLineNumber, rightLineNumber, rows, nextId);
rightLineNumber += rightLines.length;
}
}
return rows;
}
function visibleInlineSegments(content: string, segments: NacosInlineSegment[]): NacosInlineSegment[] {
return segments.length ? segments : [{ value: content, changed: false }];
}
export function buildNacosInlineDiff(before: string, after: string): NacosInlineDiffRow[] {
return buildNacosSideBySideDiff(before, after).flatMap((row) => {
if (row.leftType === "equal" && row.rightType === "equal") {
return [
{
id: `${row.id}-equal`,
lineNumber: row.leftLineNumber,
content: row.leftContent,
type: "equal" as const,
segments: visibleInlineSegments(row.leftContent, row.leftInline),
},
];
}
const rows: NacosInlineDiffRow[] = [];
if (row.leftType === "delete" || row.leftType === "modify") {
rows.push({
id: `${row.id}-delete`,
lineNumber: row.leftLineNumber,
content: row.leftContent,
type: "delete",
segments: visibleInlineSegments(row.leftContent, row.leftInline),
});
}
if (row.rightType === "insert" || row.rightType === "modify") {
rows.push({
id: `${row.id}-insert`,
lineNumber: row.rightLineNumber,
content: row.rightContent,
type: "insert",
segments: visibleInlineSegments(row.rightContent, row.rightInline),
});
}
return rows;
});
}
export function buildNacosConfigDeleteConfirm(item: NacosConfigItem, fallbackNamespace = ""): string {
return formatNacosConfigIdentity(item, fallbackNamespace);
}
export function buildNacosConfigHistoryRollbackConfirm(item: NacosConfigHistoryItem, fallbackNamespace = ""): string {
return [`namespace=${item.namespace || fallbackNamespace || "public"}`, `dataId=${item.dataId}`, `group=${item.group || "DEFAULT_GROUP"}`, item.lastModifiedTime ? `historyTime=${item.lastModifiedTime}` : "", item.operator ? `operator=${item.operator}` : ""].filter(Boolean).join("\n");
}
export function buildNacosInstanceConfirm(service: NacosServiceInfo, instance: NacosInstanceInfo, patch: Partial<NacosInstanceInfo>, fallbackGroup = "", namespace = ""): string {
const targetEnabled = patch.enabled ?? instance.enabled;
const targetHealthy = patch.healthy ?? instance.healthy;
return [
`namespace=${namespace || "public"}`,
`serviceName=${service.serviceName}`,
`group=${instance.groupName || service.groupName || fallbackGroup || "DEFAULT_GROUP"}`,
`instance=${instance.ip}:${instance.port}`,
patch.enabled == null ? "" : `targetEnabled=${targetEnabled === false ? "false" : "true"}`,
patch.healthy == null ? "" : `targetHealthy=${targetHealthy === false ? "false" : "true"}`,
]
.filter(Boolean)
.join("\n");
}
export function buildNacosRawMutationConfirm(req: NacosRawRequest): string {
return [`method=${req.method.toUpperCase()}`, `path=${req.path}`, req.query ? `query=${JSON.stringify(req.query)}` : ""].filter(Boolean).join("\n");
}