🐛 fix(connection-package): 导出/导入连接包携带 Redis DB 别名

- 连接包 payload 与导出选项支持 redisDbAliases(连接 ID → db 序号 → 别名)
- 导入返回 ConnectionPackageImportResult,一并恢复别名映射
- 前端导出时注入 appearance.redisDbAliases,导入后 merge 回 appearance
- 补充别名往返测试与 mergeRedisDbAliases 单元测试
This commit is contained in:
Syngnat
2026-07-09 13:57:32 +08:00
parent 097826508a
commit 901b436107
12 changed files with 400 additions and 68 deletions

View File

@@ -65,6 +65,11 @@ import {
resolveConnectionPackageExportResult,
normalizeConnectionPackagePassword,
} from './utils/connectionExport';
import {
mergeRedisDbAliases,
sanitizeRedisDbAliases,
type RedisDbAliasMap,
} from './utils/redisDbAlias';
import {
bootstrapSecureConfig,
finalizeSecurityUpdateStatus,
@@ -400,6 +405,32 @@ const mergeSavedConnections = (current: SavedConnection[], imported: SavedConnec
return Array.from(merged.values());
};
type ConnectionPackageImportPayload = {
connections: SavedConnection[];
redisDbAliases: RedisDbAliasMap;
};
/** Normalize ImportConnectionsPayload results: object (new) or bare array (legacy/mock). */
const normalizeConnectionPackageImportPayload = (value: unknown): ConnectionPackageImportPayload | null => {
if (Array.isArray(value)) {
return {
connections: value as SavedConnection[],
redisDbAliases: {},
};
}
if (!value || typeof value !== 'object') {
return null;
}
const record = value as { connections?: unknown; redisDbAliases?: unknown };
if (!Array.isArray(record.connections)) {
return null;
}
return {
connections: record.connections as SavedConnection[],
redisDbAliases: sanitizeRedisDbAliases(record.redisDbAliases),
};
};
type ConnectionPackageDialogMode = 'import' | 'export';
type ToolCenterGroupKey = 'config' | 'workflow' | 'workspace';
type ToolCenterPaneKey =
@@ -2363,9 +2394,9 @@ function App() {
throw new Error(t('app.connection_package.error.import_capability_unavailable'));
}
let importedViews: unknown;
let importedRaw: unknown;
try {
importedViews = await backendApp.ImportConnectionsPayload(raw, password);
importedRaw = await backendApp.ImportConnectionsPayload(raw, password);
} catch (error) {
if (isConnectionPackagePasswordRequiredError(error)) {
throw error;
@@ -2377,11 +2408,19 @@ function App() {
: t('app.connection_package.message.import_failed'),
);
}
if (!Array.isArray(importedViews)) {
const imported = normalizeConnectionPackageImportPayload(importedRaw);
if (!imported) {
throw new Error(t('app.connection_package.error.import_no_connections'));
}
await refreshConnectionsAfterImport(importedViews as SavedConnection[]);
return importedViews as SavedConnection[];
await refreshConnectionsAfterImport(imported.connections);
// Redis DB 别名存在前端 appearance需随连接包一并恢复
if (Object.keys(imported.redisDbAliases).length > 0) {
const currentAliases = useStore.getState().appearance.redisDbAliases;
useStore.getState().setAppearance({
redisDbAliases: mergeRedisDbAliases(currentAliases, imported.redisDbAliases),
});
}
return imported.connections;
}, [refreshConnectionsAfterImport, t]);
const handleImportConnections = async (sourceGroup?: ToolCenterGroupKey) => {
@@ -2502,6 +2541,8 @@ function App() {
connectionPackageDialog.includeSecrets
&& connectionPackageDialog.useFilePassword
) ? password : '',
// Redis DB 别名仅存前端,导出时注入连接包
redisDbAliases: useStore.getState().appearance.redisDbAliases,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error ?? '').trim();

View File

@@ -420,14 +420,29 @@ if (
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
return parsed.map((item) => saveMockConnection(item));
return {
connections: parsed.map((item) => saveMockConnection(item)),
redisDbAliases: {},
};
}
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.connections)) {
return {
connections: parsed.connections.map((item: unknown) => saveMockConnection(item)),
redisDbAliases: parsed.redisDbAliases && typeof parsed.redisDbAliases === 'object'
? parsed.redisDbAliases
: {},
};
}
} catch {
throw new Error(t('app.browser_mock.import_connection_package_unsupported'));
}
throw new Error(t('app.browser_mock.import_connection_package_unsupported'));
},
ExportConnectionsPackage: async (_options?: { includeSecrets?: boolean; filePassword?: string }) => ({ success: false, message: t('app.browser_mock.export_connection_package_unsupported') }),
ExportConnectionsPackage: async (_options?: {
includeSecrets?: boolean;
filePassword?: string;
redisDbAliases?: Record<string, Record<string, string>>;
}) => ({ success: false, message: t('app.browser_mock.export_connection_package_unsupported') }),
ExportData: async () => ({ success: false }),
GetGlobalProxyConfig: async () => ({ success: true, data: cloneBrowserMockValue(mockGlobalProxy) }),
SetUpdateChannel: async (channel: string) => {

View File

@@ -4,6 +4,7 @@ import {
MAX_REDIS_DB_ALIAS_LENGTH,
buildRedisDbNodeLabel,
getRedisDbAlias,
mergeRedisDbAliases,
sanitizeRedisDbAlias,
sanitizeRedisDbAliases,
setRedisDbAlias,
@@ -76,4 +77,20 @@ describe('redisDbAlias helpers', () => {
it('does not append key counts to the sidebar label', () => {
expect(buildRedisDbNodeLabel(0, '12')).toBe('db0 12');
});
it('merges imported aliases over local ones without dropping unrelated labels', () => {
const current = {
'conn-a': { '0': 'local-cache', '1': 'local-queue' },
'conn-b': { '0': 'sessions' },
};
const incoming = {
'conn-a': { '0': 'imported-cache' },
'conn-c': { '2': 'metrics' },
};
expect(mergeRedisDbAliases(current, incoming)).toEqual({
'conn-a': { '0': 'imported-cache', '1': 'local-queue' },
'conn-b': { '0': 'sessions' },
'conn-c': { '2': 'metrics' },
});
});
});

View File

@@ -126,6 +126,26 @@ export const setRedisDbAlias = (
return next;
};
/**
* Merge imported Redis DB aliases into the local map. Imported labels win for
* the same connection + DB index; unrelated local aliases are preserved.
*/
export const mergeRedisDbAliases = (
current: RedisDbAliasMap | undefined,
incoming: RedisDbAliasMap | undefined,
): RedisDbAliasMap => {
const base = sanitizeRedisDbAliases(current);
const imported = sanitizeRedisDbAliases(incoming);
const next: RedisDbAliasMap = { ...base };
Object.entries(imported).forEach(([connectionId, aliases]) => {
next[connectionId] = {
...(base[connectionId] || {}),
...aliases,
};
});
return next;
};
/**
* Build the sidebar label for a Redis DB node. Returns `dbN` when there is no
* alias, and `dbN alias` when one is set. Key counts are intentionally not