mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-01 04:41:43 +08:00
✨ feat(ai): 增强 Redis 拓扑诊断能力
- 增加 Redis Sentinel/Cluster 状态分级、阻断原因、后端适配器和脱敏 URI 示例 - 区分配置拓扑与后端实际拓扑,修正多节点单机配置的 ClusterClient 诊断 - 补充 AI 工具目录流程与 Redis 哨兵/集群提示文案
This commit is contained in:
@@ -47,11 +47,26 @@ describe('buildRedisTopologySnapshot', () => {
|
||||
expect(snapshot.totalRedisConnections).toBe(1);
|
||||
expect(snapshot.totalMatched).toBe(1);
|
||||
expect(snapshot.topologyBreakdown).toEqual({ sentinel: 1 });
|
||||
expect(snapshot.blockedCount).toBe(1);
|
||||
expect(snapshot.connections[0].status).toBe('blocked');
|
||||
expect(snapshot.connections[0].backendAdapter).toBe('go-redis FailoverClient');
|
||||
expect(snapshot.connections[0].effectiveTopology).toBe('sentinel');
|
||||
expect(snapshot.connections[0].topologyMismatch).toBe(false);
|
||||
expect(snapshot.connections[0].sentinelMaster).toBe('');
|
||||
expect(snapshot.connections[0].hasRedisAuth).toBe(true);
|
||||
expect(snapshot.connections[0].hasSentinelAuth).toBe(true);
|
||||
expect(snapshot.connections[0].safeUriExample).toContain('rediss://app:<hidden>@sentinel-a.local:6379,sentinel-b.local:26379/2');
|
||||
expect(snapshot.connections[0].safeUriExample).toContain('topology=sentinel');
|
||||
expect(snapshot.connections[0].safeUriExample).toContain('sentinel_user=sentinel-user');
|
||||
expect(snapshot.connections[0].safeUriExample).toContain('sentinel_password=%3Chidden%3E');
|
||||
expect(snapshot.connections[0].dbSemantics).toMatchObject({
|
||||
physicalDb: 2,
|
||||
mode: 'failover_selected_db',
|
||||
});
|
||||
expect(snapshot.connections[0].warnings).toContain('Sentinel master 名称为空,go-redis FailoverClient 无法发现主节点');
|
||||
expect(snapshot.connections[0].warnings).toContain('Sentinel 主地址端口是 6379,请确认这里填写的是 Sentinel 端口,常见默认值是 26379');
|
||||
expect(snapshot.connections[0].blockingReasons).toContain('Sentinel master 名称为空,go-redis FailoverClient 无法发现主节点');
|
||||
expect(snapshot.connections[0].nextActions.join('\n')).toContain('补充 Sentinel master 名称');
|
||||
expect(JSON.stringify(snapshot)).not.toContain('redis-secret');
|
||||
expect(JSON.stringify(snapshot)).not.toContain('sentinel-secret');
|
||||
});
|
||||
@@ -74,9 +89,20 @@ describe('buildRedisTopologySnapshot', () => {
|
||||
});
|
||||
|
||||
expect(snapshot.connections[0].topology).toBe('cluster');
|
||||
expect(snapshot.connections[0].effectiveTopology).toBe('cluster');
|
||||
expect(snapshot.connections[0].topologyMismatch).toBe(false);
|
||||
expect(snapshot.connections[0].status).toBe('blocked');
|
||||
expect(snapshot.connections[0].backendAdapter).toBe('go-redis ClusterClient');
|
||||
expect(snapshot.connections[0].seedAddressCount).toBe(3);
|
||||
expect(snapshot.connections[0].safeUriExample).toBe('redis://10.10.1.10:6379,10.10.1.11:6379,10.10.1.12:6379/0?topology=cluster');
|
||||
expect(snapshot.connections[0].dbSemantics).toMatchObject({
|
||||
physicalDb: 0,
|
||||
selectedDb: 4,
|
||||
mode: 'cluster_logical_namespace',
|
||||
});
|
||||
expect(snapshot.connections[0].warnings).toContain('Redis Cluster 当前后端不支持 SSH 隧道,请改用直连、代理或远程 MCP HTTP 方案');
|
||||
expect(snapshot.connections[0].warnings).toContain('Redis Cluster 物理上只支持 db0;GoNavi 会用 __gonavi_db_N__: 前缀模拟逻辑库隔离');
|
||||
expect(snapshot.connections[0].nextActions.join('\n')).toContain('关闭 SSH 隧道');
|
||||
expect(snapshot.connections[0].recommendations?.join('\n')).toContain('种子节点');
|
||||
});
|
||||
|
||||
@@ -104,6 +130,17 @@ describe('buildRedisTopologySnapshot', () => {
|
||||
|
||||
expect(snapshot.totalRedisConnections).toBe(1);
|
||||
expect(snapshot.connections).toHaveLength(1);
|
||||
expect(snapshot.connections[0].status).toBe('needs_attention');
|
||||
expect(snapshot.connections[0].topology).toBe('single');
|
||||
expect(snapshot.connections[0].effectiveTopology).toBe('cluster');
|
||||
expect(snapshot.connections[0].topologyMismatch).toBe(true);
|
||||
expect(snapshot.connections[0].backendAdapter).toBe('go-redis ClusterClient');
|
||||
expect(snapshot.connections[0].safeUriExample).toBe('redis://redis.local:6379,redis-2.local:6379/0?topology=cluster');
|
||||
expect(snapshot.connections[0].dbSemantics).toMatchObject({
|
||||
physicalDb: 0,
|
||||
mode: 'cluster_logical_namespace',
|
||||
});
|
||||
expect(snapshot.connections[0].warnings).toContain('单机模式下存在多个节点地址,后端会按多节点集群路径处理,建议显式改为 Cluster 模式');
|
||||
expect(snapshot.connections[0].nextActions.join('\n')).toContain('显式切换为 Cluster 模式');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { SavedConnection } from '../../types';
|
||||
|
||||
type RedisTopology = 'single' | 'cluster' | 'sentinel';
|
||||
|
||||
const normalizeText = (input: unknown): string => String(input || '').trim();
|
||||
|
||||
const normalizeLowerText = (input: unknown): string => normalizeText(input).toLowerCase();
|
||||
@@ -11,13 +13,34 @@ const normalizeLimit = (input: unknown, fallback: number, max: number): number =
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeRedisTopology = (input: unknown): 'single' | 'cluster' | 'sentinel' => {
|
||||
const normalizeRedisTopology = (input: unknown): RedisTopology => {
|
||||
const value = normalizeLowerText(input);
|
||||
if (value === 'cluster') return 'cluster';
|
||||
if (value === 'sentinel') return 'sentinel';
|
||||
return 'single';
|
||||
};
|
||||
|
||||
const resolveEffectiveRedisTopology = (
|
||||
configuredTopology: RedisTopology,
|
||||
seedAddresses: string[],
|
||||
): RedisTopology => {
|
||||
if (configuredTopology === 'sentinel') return 'sentinel';
|
||||
if (configuredTopology === 'cluster' || seedAddresses.length > 1) return 'cluster';
|
||||
return 'single';
|
||||
};
|
||||
|
||||
const topologyAdapterName = (topology: RedisTopology): string => {
|
||||
if (topology === 'sentinel') return 'go-redis FailoverClient';
|
||||
if (topology === 'cluster') return 'go-redis ClusterClient';
|
||||
return 'go-redis Client';
|
||||
};
|
||||
|
||||
const topologyModeLabel = (topology: RedisTopology): string => {
|
||||
if (topology === 'sentinel') return 'Redis Sentinel';
|
||||
if (topology === 'cluster') return 'Redis Cluster';
|
||||
return 'Redis 单机';
|
||||
};
|
||||
|
||||
const buildSeedAddresses = (connection: SavedConnection): string[] => {
|
||||
const config = connection.config || {};
|
||||
const defaultPort = Number.isFinite(Number(config.port)) ? Number(config.port) : 6379;
|
||||
@@ -49,16 +72,17 @@ const matchesKeyword = (keyword: string, connection: SavedConnection, seedAddres
|
||||
|
||||
const buildRedisTopologyWarnings = (connection: SavedConnection, seedAddresses: string[]): string[] => {
|
||||
const config = connection.config || {};
|
||||
const topology = normalizeRedisTopology(config.topology);
|
||||
const configuredTopology = normalizeRedisTopology(config.topology);
|
||||
const effectiveTopology = resolveEffectiveRedisTopology(configuredTopology, seedAddresses);
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!normalizeText(config.host)) {
|
||||
warnings.push('主机地址为空,连接前需要填写 Redis 节点或 Sentinel 地址');
|
||||
}
|
||||
if ((topology === 'cluster' || topology === 'sentinel') && config.useSSH === true) {
|
||||
warnings.push(`${topology === 'cluster' ? 'Redis Cluster' : 'Redis Sentinel'} 当前后端不支持 SSH 隧道,请改用直连、代理或远程 MCP HTTP 方案`);
|
||||
if ((effectiveTopology === 'cluster' || effectiveTopology === 'sentinel') && config.useSSH === true) {
|
||||
warnings.push(`${effectiveTopology === 'cluster' ? 'Redis Cluster' : 'Redis Sentinel'} 当前后端不支持 SSH 隧道,请改用直连、代理或远程 MCP HTTP 方案`);
|
||||
}
|
||||
if (topology === 'sentinel') {
|
||||
if (configuredTopology === 'sentinel') {
|
||||
if (!normalizeText(config.redisSentinelMaster)) {
|
||||
warnings.push('Sentinel master 名称为空,go-redis FailoverClient 无法发现主节点');
|
||||
}
|
||||
@@ -69,7 +93,7 @@ const buildRedisTopologyWarnings = (connection: SavedConnection, seedAddresses:
|
||||
warnings.push('Sentinel 主地址端口是 6379,请确认这里填写的是 Sentinel 端口,常见默认值是 26379');
|
||||
}
|
||||
}
|
||||
if (topology === 'cluster') {
|
||||
if (effectiveTopology === 'cluster') {
|
||||
if (seedAddresses.length < 2) {
|
||||
warnings.push('Cluster 只配置了一个种子节点,建议填写多个 master/replica 节点提高发现成功率');
|
||||
}
|
||||
@@ -84,7 +108,7 @@ const buildRedisTopologyWarnings = (connection: SavedConnection, seedAddresses:
|
||||
warnings.push('Cluster 模式下 Sentinel master / Sentinel 用户字段不会生效');
|
||||
}
|
||||
}
|
||||
if (topology === 'single') {
|
||||
if (configuredTopology === 'single') {
|
||||
if (seedAddresses.length > 1) {
|
||||
warnings.push('单机模式下存在多个节点地址,后端会按多节点集群路径处理,建议显式改为 Cluster 模式');
|
||||
}
|
||||
@@ -96,15 +120,127 @@ const buildRedisTopologyWarnings = (connection: SavedConnection, seedAddresses:
|
||||
return warnings;
|
||||
};
|
||||
|
||||
const buildRedisTopologyRecommendations = (connection: SavedConnection, warnings: string[]): string[] => {
|
||||
const isBlockingRedisWarning = (warning: string): boolean =>
|
||||
warning.includes('主机地址为空') ||
|
||||
warning.includes('master 名称为空') ||
|
||||
warning.includes('不支持 SSH 隧道');
|
||||
|
||||
const buildSafeRedisUriExample = (
|
||||
connection: SavedConnection,
|
||||
seedAddresses: string[],
|
||||
): string => {
|
||||
const config = connection.config || {};
|
||||
const configuredTopology = normalizeRedisTopology(config.topology);
|
||||
const effectiveTopology = resolveEffectiveRedisTopology(configuredTopology, seedAddresses);
|
||||
const scheme = config.useSSL === true ? 'rediss' : 'redis';
|
||||
const hosts = seedAddresses.length > 0 ? seedAddresses : ['localhost:6379'];
|
||||
const params = new URLSearchParams();
|
||||
if (effectiveTopology === 'sentinel') {
|
||||
params.set('topology', 'sentinel');
|
||||
const masterName = normalizeText(config.redisSentinelMaster);
|
||||
if (masterName) {
|
||||
params.set('master', masterName);
|
||||
}
|
||||
const sentinelUser = normalizeText(config.redisSentinelUser);
|
||||
if (sentinelUser) {
|
||||
params.set('sentinel_user', sentinelUser);
|
||||
}
|
||||
if (normalizeText(config.redisSentinelPassword) || connection.hasRedisSentinelPassword === true) {
|
||||
params.set('sentinel_password', '<hidden>');
|
||||
}
|
||||
} else if (effectiveTopology === 'cluster') {
|
||||
params.set('topology', 'cluster');
|
||||
}
|
||||
if (config.useSSL === true) {
|
||||
const sslMode = normalizeLowerText(config.sslMode || 'preferred');
|
||||
if (sslMode === 'skip-verify' || sslMode === 'preferred') {
|
||||
params.set('skip_verify', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
const redisUser = normalizeText(config.user);
|
||||
const hasRedisPassword = normalizeText(config.password) || connection.hasPrimaryPassword === true;
|
||||
const auth = redisUser
|
||||
? `${encodeURIComponent(redisUser)}${hasRedisPassword ? ':<hidden>' : ''}@`
|
||||
: hasRedisPassword
|
||||
? ':<hidden>@'
|
||||
: '';
|
||||
const redisDB = effectiveTopology === 'cluster' ? 0 : Number(config.redisDB || 0);
|
||||
const query = params.toString();
|
||||
return `${scheme}://${auth}${hosts.join(',')}/${Number.isFinite(redisDB) ? Math.max(0, Math.trunc(redisDB)) : 0}${query ? `?${query}` : ''}`;
|
||||
};
|
||||
|
||||
const buildRedisDBSemantics = (
|
||||
connection: SavedConnection,
|
||||
effectiveTopology: RedisTopology,
|
||||
) => {
|
||||
const config = connection.config || {};
|
||||
const redisDB = typeof config.redisDB === 'number' ? config.redisDB : 0;
|
||||
const includeRedisDatabases = Array.isArray(connection.includeRedisDatabases)
|
||||
? connection.includeRedisDatabases.filter((item) => typeof item === 'number')
|
||||
: [];
|
||||
if (effectiveTopology === 'cluster') {
|
||||
return {
|
||||
physicalDb: 0,
|
||||
selectedDb: redisDB,
|
||||
includeRedisDatabases,
|
||||
mode: 'cluster_logical_namespace',
|
||||
note: 'Redis Cluster 物理只支持 db0;GoNavi 用 __gonavi_db_N__: 前缀模拟多库视图。',
|
||||
};
|
||||
}
|
||||
return {
|
||||
physicalDb: redisDB,
|
||||
selectedDb: redisDB,
|
||||
includeRedisDatabases,
|
||||
mode: effectiveTopology === 'sentinel' ? 'failover_selected_db' : 'selected_db',
|
||||
note: effectiveTopology === 'sentinel'
|
||||
? 'Sentinel 发现 master 后连接指定 DB,切库会保留 Sentinel 配置重连。'
|
||||
: '单机模式直接使用 Redis SELECT DB。',
|
||||
};
|
||||
};
|
||||
|
||||
const buildRedisNextActions = (
|
||||
connection: SavedConnection,
|
||||
warnings: string[],
|
||||
): string[] => {
|
||||
const config = connection.config || {};
|
||||
const topology = normalizeRedisTopology(config.topology);
|
||||
const actions: string[] = [];
|
||||
if (!normalizeText(config.host)) {
|
||||
actions.push('先填写主机地址;Sentinel 模式填写 Sentinel 地址,Cluster 模式填写 Redis Cluster 种子节点。');
|
||||
}
|
||||
if (topology === 'sentinel' && !normalizeText(config.redisSentinelMaster)) {
|
||||
actions.push('补充 Sentinel master 名称,例如 mymaster。');
|
||||
}
|
||||
if (warnings.some((warning) => warning.includes('不支持 SSH 隧道'))) {
|
||||
actions.push('关闭 SSH 隧道,改用直连、代理/VPN,或使用 GoNavi MCP HTTP 让远端 Agent 通过本机 GoNavi 访问。');
|
||||
}
|
||||
if (warnings.some((warning) => warning.includes('26379'))) {
|
||||
actions.push('把 Sentinel 主地址端口改为 26379,除非你的 Sentinel 明确监听其他端口。');
|
||||
}
|
||||
if (warnings.some((warning) => warning.includes('db0'))) {
|
||||
actions.push('确认业务是否真的需要 Redis Cluster 多库视图;如果只是 key 分组,优先使用业务命名空间。');
|
||||
}
|
||||
if (warnings.some((warning) => warning.includes('多节点集群路径'))) {
|
||||
actions.push('显式切换为 Cluster 模式,或删除附加节点只保留一个单机地址,避免配置拓扑和后端实际拓扑不一致。');
|
||||
}
|
||||
if (actions.length === 0) {
|
||||
actions.push(`配置看起来可用于 ${topologyModeLabel(topology)},下一步可以测试连接并查看 Redis DB/Key 树。`);
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
|
||||
const buildRedisTopologyRecommendations = (connection: SavedConnection, warnings: string[]): string[] => {
|
||||
const config = connection.config || {};
|
||||
const configuredTopology = normalizeRedisTopology(config.topology);
|
||||
const seedAddresses = buildSeedAddresses(connection);
|
||||
const effectiveTopology = resolveEffectiveRedisTopology(configuredTopology, seedAddresses);
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (topology === 'sentinel') {
|
||||
if (configuredTopology === 'sentinel') {
|
||||
recommendations.push('确认主机和附加节点填写的是 Sentinel 地址,不是 Redis master 地址');
|
||||
recommendations.push('分别填写 Redis 数据节点账号密码和 Sentinel 自身账号密码,二者不要混用');
|
||||
} else if (topology === 'cluster') {
|
||||
} else if (effectiveTopology === 'cluster') {
|
||||
recommendations.push('优先配置 2 个以上种子节点,并确认这些节点属于同一个 Redis Cluster');
|
||||
recommendations.push('如果需要多库视图,优先在业务 key 上显式使用命名空间,避免误解 Cluster 的物理 db0 限制');
|
||||
} else {
|
||||
@@ -159,14 +295,28 @@ export const buildRedisTopologySnapshot = (params: {
|
||||
const config = connection.config || {};
|
||||
const topology = normalizeRedisTopology(config.topology);
|
||||
const seedAddresses = buildSeedAddresses(connection);
|
||||
const effectiveTopology = resolveEffectiveRedisTopology(topology, seedAddresses);
|
||||
const warnings = buildRedisTopologyWarnings(connection, seedAddresses);
|
||||
const includeRedisDatabases = Array.isArray(connection.includeRedisDatabases)
|
||||
? connection.includeRedisDatabases.filter((item) => typeof item === 'number')
|
||||
: [];
|
||||
const blockingReasons = warnings.filter(isBlockingRedisWarning);
|
||||
const status = blockingReasons.length > 0
|
||||
? 'blocked'
|
||||
: warnings.length > 0
|
||||
? 'needs_attention'
|
||||
: 'ready';
|
||||
return {
|
||||
id: connection.id,
|
||||
name: connection.name,
|
||||
topology,
|
||||
topologyLabel: topologyModeLabel(topology),
|
||||
effectiveTopology,
|
||||
effectiveTopologyLabel: topologyModeLabel(effectiveTopology),
|
||||
topologyMismatch: topology !== effectiveTopology,
|
||||
status,
|
||||
blockingReasons,
|
||||
backendAdapter: topologyAdapterName(effectiveTopology),
|
||||
host: normalizeText(config.host),
|
||||
port: Number.isFinite(Number(config.port)) ? Number(config.port) : null,
|
||||
seedAddresses,
|
||||
@@ -183,7 +333,10 @@ export const buildRedisTopologySnapshot = (params: {
|
||||
hasSentinelAuth: topology === 'sentinel'
|
||||
? Boolean(normalizeText(config.redisSentinelUser) || normalizeText(config.redisSentinelPassword) || connection.hasRedisSentinelPassword === true)
|
||||
: false,
|
||||
safeUriExample: buildSafeRedisUriExample(connection, seedAddresses),
|
||||
dbSemantics: buildRedisDBSemantics(connection, effectiveTopology),
|
||||
warnings,
|
||||
nextActions: buildRedisNextActions(connection, warnings),
|
||||
recommendations: shouldIncludeRecommendations
|
||||
? buildRedisTopologyRecommendations(connection, warnings)
|
||||
: undefined,
|
||||
@@ -191,6 +344,7 @@ export const buildRedisTopologySnapshot = (params: {
|
||||
});
|
||||
|
||||
const warningCount = visibleConnections.reduce((total, connection) => total + connection.warnings.length, 0);
|
||||
const blockedCount = visibleConnections.filter((connection) => connection.status === 'blocked').length;
|
||||
|
||||
return {
|
||||
connectionId: safeConnectionId,
|
||||
@@ -202,6 +356,7 @@ export const buildRedisTopologySnapshot = (params: {
|
||||
truncated: matchedConnections.length > visibleConnections.length,
|
||||
topologyBreakdown,
|
||||
warningCount,
|
||||
blockedCount,
|
||||
connections: visibleConnections,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -109,14 +109,14 @@ export const BUILTIN_AI_INSPECTION_CONTEXT_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
icon: "🧰",
|
||||
desc: "诊断 Redis 单机/哨兵/集群配置",
|
||||
detail:
|
||||
"读取本地 Redis 连接拓扑摘要,返回单机、Sentinel、Cluster 的节点、master、认证状态、DB 范围和风险提示。适合用户问 Redis 哨兵/集群怎么配、为什么切库后失败、Cluster 多 DB 怎么处理时先读真实配置。",
|
||||
"读取本地 Redis 连接拓扑摘要,返回单机、Sentinel、Cluster 的节点、master、认证状态、DB 范围、脱敏 URI 示例、状态分级和下一步动作。适合用户问 Redis 哨兵/集群怎么配、为什么切库后失败、Cluster 多 DB 怎么处理时先读真实配置。",
|
||||
params: "connectionId?, keyword?, limit?, includeRecommendations?(默认 true)",
|
||||
tool: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "inspect_redis_topology",
|
||||
description:
|
||||
"读取本地 Redis 连接的单机、Sentinel、Cluster 拓扑配置摘要,返回节点列表、Sentinel master、认证状态、DB 选择、TLS/SSH/代理状态、潜在配置风险和建议。适用于用户提到 Redis 哨兵、Redis Cluster、切换数据库失败、多节点地址、Sentinel master、Cluster 逻辑库或跨网络访问 Redis 时,先读取真实连接配置再回答;结果不会回显 Redis 密码或 Sentinel 密码。",
|
||||
"读取本地 Redis 连接的单机、Sentinel、Cluster 拓扑配置摘要,返回节点列表、Sentinel master、认证状态、DB 选择、TLS/SSH/代理状态、后端适配器、脱敏 URI 示例、状态分级、阻断原因、潜在配置风险和建议。适用于用户提到 Redis 哨兵、Redis Cluster、切换数据库失败、多节点地址、Sentinel master、Cluster 逻辑库或跨网络访问 Redis 时,先读取真实连接配置再回答;结果不会回显 Redis 密码或 Sentinel 密码。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
||||
@@ -137,6 +137,11 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [
|
||||
steps: 'inspect_saved_connections -> inspect_current_connection / get_databases',
|
||||
description: '适合先按关键词或类型筛出本地保存的数据源,再挑目标连接继续看当前状态或库表结构。',
|
||||
},
|
||||
{
|
||||
title: '诊断 Redis 拓扑',
|
||||
steps: 'inspect_redis_topology -> inspect_current_connection / inspect_app_logs',
|
||||
description: '适合用户问 Redis 哨兵、Cluster、多节点、切库失败或 SSH 隧道不可用时,先拿到状态分级、脱敏 URI、后端适配器、DB 语义和下一步动作。',
|
||||
},
|
||||
{
|
||||
title: '盘点外部 SQL 目录',
|
||||
steps: 'inspect_external_sql_directories -> inspect_workspace_tabs / inspect_active_tab',
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('connectionTypeCatalog', () => {
|
||||
});
|
||||
|
||||
it('keeps concise localized hints for special connection types', () => {
|
||||
expect(getConnectionTypeHint('redis')).toBe('单机 / 集群');
|
||||
expect(getConnectionTypeHint('redis')).toBe('单机 / 哨兵 / 集群');
|
||||
expect(getConnectionTypeHint('mongodb')).toBe('单机 / 副本集');
|
||||
expect(getConnectionTypeHint('elasticsearch')).toContain('Mapping');
|
||||
expect(getConnectionTypeHint('oceanbase')).toBe('MySQL / Oracle 租户');
|
||||
|
||||
@@ -118,7 +118,7 @@ export const getConnectionTypeHint = (type: string): string => {
|
||||
case 'custom':
|
||||
return '自定义驱动与 DSN';
|
||||
case 'redis':
|
||||
return '单机 / 集群';
|
||||
return '单机 / 哨兵 / 集群';
|
||||
case 'mongodb':
|
||||
return '单机 / 副本集';
|
||||
case 'elasticsearch':
|
||||
|
||||
Reference in New Issue
Block a user