mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 10:59:41 +08:00
✨ feat(ai): 新增 Redis 拓扑诊断探针
- 新增 inspect_redis_topology 内置工具,输出 Redis 单机、Sentinel、Cluster 脱敏拓扑与风险提示 - 接入本地工具执行链、工具目录、系统引导和工具调用状态文案 - 补充工具注册、目录渲染、执行器和拓扑规则测试
This commit is contained in:
@@ -73,6 +73,9 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
expect(markup).toContain('inspect_connection_capabilities');
|
||||
expect(markup).toContain('盘点本地连接资产');
|
||||
expect(markup).toContain('inspect_saved_connections');
|
||||
expect(markup).toContain('诊断 Redis 单机/哨兵/集群配置');
|
||||
expect(markup).toContain('inspect_redis_topology');
|
||||
expect(markup).toContain('Sentinel master');
|
||||
expect(markup).toContain('盘点外部 SQL 目录');
|
||||
expect(markup).toContain('inspect_external_sql_directories');
|
||||
expect(markup).toContain('读取外部 SQL 文件');
|
||||
|
||||
@@ -162,4 +162,58 @@ describe('aiLocalToolExecutor connection inspection tools', () => {
|
||||
expect(result.content).toContain('"useSSH":true');
|
||||
expect(result.content).not.toContain('分析仓库');
|
||||
});
|
||||
|
||||
it('returns a Redis topology snapshot with Sentinel and Cluster risks', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_redis_topology', {
|
||||
keyword: '订单',
|
||||
}),
|
||||
connections: [
|
||||
{
|
||||
id: 'redis-sentinel',
|
||||
name: '订单 Redis Sentinel',
|
||||
config: {
|
||||
type: 'redis',
|
||||
host: 'sentinel-a.local',
|
||||
port: 6379,
|
||||
hosts: ['sentinel-b.local:26379'],
|
||||
topology: 'sentinel',
|
||||
user: 'app',
|
||||
password: 'redis-secret',
|
||||
redisSentinelPassword: 'sentinel-secret',
|
||||
},
|
||||
hasRedisSentinelPassword: true,
|
||||
},
|
||||
{
|
||||
id: 'redis-cluster',
|
||||
name: '缓存集群',
|
||||
config: {
|
||||
type: 'redis',
|
||||
host: '10.10.1.10',
|
||||
port: 6379,
|
||||
user: '',
|
||||
topology: 'cluster',
|
||||
hosts: ['10.10.1.11:6379'],
|
||||
redisDB: 3,
|
||||
useSSH: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
mcpTools: [],
|
||||
toolContextMap: new Map(),
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('"totalRedisConnections":2');
|
||||
expect(result.content).toContain('"totalMatched":1');
|
||||
expect(result.content).toContain('"topology":"sentinel"');
|
||||
expect(result.content).toContain('Sentinel master 名称为空');
|
||||
expect(result.content).toContain('Sentinel 主地址端口是 6379');
|
||||
expect(result.content).not.toContain('redis-secret');
|
||||
expect(result.content).not.toContain('sentinel-secret');
|
||||
});
|
||||
});
|
||||
|
||||
109
frontend/src/components/ai/aiRedisTopologyInsights.test.ts
Normal file
109
frontend/src/components/ai/aiRedisTopologyInsights.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { buildRedisTopologySnapshot } from './aiRedisTopologyInsights';
|
||||
|
||||
const buildRedisConnection = (
|
||||
id: string,
|
||||
name: string,
|
||||
config: Partial<SavedConnection['config']>,
|
||||
extra: Partial<SavedConnection> = {},
|
||||
): SavedConnection => ({
|
||||
id,
|
||||
name,
|
||||
config: {
|
||||
type: 'redis',
|
||||
host: '127.0.0.1',
|
||||
port: 6379,
|
||||
user: '',
|
||||
...config,
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
describe('buildRedisTopologySnapshot', () => {
|
||||
it('summarizes Redis Sentinel settings without exposing passwords', () => {
|
||||
const snapshot = buildRedisTopologySnapshot({
|
||||
connections: [
|
||||
buildRedisConnection('redis-sentinel', '生产 Redis Sentinel', {
|
||||
host: 'sentinel-a.local',
|
||||
port: 6379,
|
||||
hosts: ['sentinel-b.local:26379'],
|
||||
topology: 'sentinel',
|
||||
user: 'app',
|
||||
password: 'redis-secret',
|
||||
redisSentinelUser: 'sentinel-user',
|
||||
redisSentinelPassword: 'sentinel-secret',
|
||||
redisDB: 2,
|
||||
useSSL: true,
|
||||
sslMode: 'required',
|
||||
}, {
|
||||
hasRedisSentinelPassword: true,
|
||||
}),
|
||||
],
|
||||
keyword: '生产',
|
||||
});
|
||||
|
||||
expect(snapshot.totalRedisConnections).toBe(1);
|
||||
expect(snapshot.totalMatched).toBe(1);
|
||||
expect(snapshot.topologyBreakdown).toEqual({ sentinel: 1 });
|
||||
expect(snapshot.connections[0].sentinelMaster).toBe('');
|
||||
expect(snapshot.connections[0].hasRedisAuth).toBe(true);
|
||||
expect(snapshot.connections[0].hasSentinelAuth).toBe(true);
|
||||
expect(snapshot.connections[0].warnings).toContain('Sentinel master 名称为空,go-redis FailoverClient 无法发现主节点');
|
||||
expect(snapshot.connections[0].warnings).toContain('Sentinel 主地址端口是 6379,请确认这里填写的是 Sentinel 端口,常见默认值是 26379');
|
||||
expect(JSON.stringify(snapshot)).not.toContain('redis-secret');
|
||||
expect(JSON.stringify(snapshot)).not.toContain('sentinel-secret');
|
||||
});
|
||||
|
||||
it('reports cluster logical-db and SSH risks', () => {
|
||||
const snapshot = buildRedisTopologySnapshot({
|
||||
connections: [
|
||||
buildRedisConnection('redis-cluster', '订单 Redis Cluster', {
|
||||
host: '10.10.1.10',
|
||||
port: 6379,
|
||||
hosts: ['10.10.1.11:6379', '10.10.1.12:6379'],
|
||||
topology: 'cluster',
|
||||
redisDB: 4,
|
||||
useSSH: true,
|
||||
}, {
|
||||
includeRedisDatabases: [0, 4],
|
||||
}),
|
||||
],
|
||||
connectionId: 'redis-cluster',
|
||||
});
|
||||
|
||||
expect(snapshot.connections[0].topology).toBe('cluster');
|
||||
expect(snapshot.connections[0].seedAddressCount).toBe(3);
|
||||
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].recommendations?.join('\n')).toContain('种子节点');
|
||||
});
|
||||
|
||||
it('filters out non-Redis connections and warns about multi-host single mode', () => {
|
||||
const snapshot = buildRedisTopologySnapshot({
|
||||
connections: [
|
||||
buildRedisConnection('redis-single', '缓存单机', {
|
||||
host: 'redis.local',
|
||||
port: 6379,
|
||||
topology: 'single',
|
||||
hosts: ['redis-2.local:6379'],
|
||||
}),
|
||||
{
|
||||
id: 'mysql-1',
|
||||
name: '业务库',
|
||||
config: {
|
||||
type: 'mysql',
|
||||
host: 'mysql.local',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(snapshot.totalRedisConnections).toBe(1);
|
||||
expect(snapshot.connections).toHaveLength(1);
|
||||
expect(snapshot.connections[0].warnings).toContain('单机模式下存在多个节点地址,后端会按多节点集群路径处理,建议显式改为 Cluster 模式');
|
||||
});
|
||||
});
|
||||
207
frontend/src/components/ai/aiRedisTopologyInsights.ts
Normal file
207
frontend/src/components/ai/aiRedisTopologyInsights.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import type { SavedConnection } from '../../types';
|
||||
|
||||
const normalizeText = (input: unknown): string => String(input || '').trim();
|
||||
|
||||
const normalizeLowerText = (input: unknown): string => normalizeText(input).toLowerCase();
|
||||
|
||||
const normalizeLimit = (input: unknown, fallback: number, max: number): number => {
|
||||
const value = Math.floor(Number(input) || fallback);
|
||||
if (value < 1) return 1;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeRedisTopology = (input: unknown): 'single' | 'cluster' | 'sentinel' => {
|
||||
const value = normalizeLowerText(input);
|
||||
if (value === 'cluster') return 'cluster';
|
||||
if (value === 'sentinel') return 'sentinel';
|
||||
return 'single';
|
||||
};
|
||||
|
||||
const buildSeedAddresses = (connection: SavedConnection): string[] => {
|
||||
const config = connection.config || {};
|
||||
const defaultPort = Number.isFinite(Number(config.port)) ? Number(config.port) : 6379;
|
||||
const primary = normalizeText(config.host)
|
||||
? `${normalizeText(config.host)}:${defaultPort}`
|
||||
: '';
|
||||
const extraHosts = Array.isArray(config.hosts)
|
||||
? config.hosts.map((host) => normalizeText(host)).filter(Boolean)
|
||||
: [];
|
||||
return [primary, ...extraHosts].filter(Boolean);
|
||||
};
|
||||
|
||||
const matchesKeyword = (keyword: string, connection: SavedConnection, seedAddresses: string[]): boolean => {
|
||||
if (!keyword) return true;
|
||||
const config = connection.config || {};
|
||||
return [
|
||||
connection.id,
|
||||
connection.name,
|
||||
config.type,
|
||||
config.host,
|
||||
config.user,
|
||||
config.database,
|
||||
config.topology,
|
||||
config.redisSentinelMaster,
|
||||
config.redisSentinelUser,
|
||||
...seedAddresses,
|
||||
].some((field) => normalizeLowerText(field).includes(keyword));
|
||||
};
|
||||
|
||||
const buildRedisTopologyWarnings = (connection: SavedConnection, seedAddresses: string[]): string[] => {
|
||||
const config = connection.config || {};
|
||||
const topology = normalizeRedisTopology(config.topology);
|
||||
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 (topology === 'sentinel') {
|
||||
if (!normalizeText(config.redisSentinelMaster)) {
|
||||
warnings.push('Sentinel master 名称为空,go-redis FailoverClient 无法发现主节点');
|
||||
}
|
||||
if (seedAddresses.length < 2) {
|
||||
warnings.push('Sentinel 只配置了一个节点,建议至少填写 2-3 个 Sentinel 地址以避免单点失败');
|
||||
}
|
||||
if (Number(config.port) === 6379) {
|
||||
warnings.push('Sentinel 主地址端口是 6379,请确认这里填写的是 Sentinel 端口,常见默认值是 26379');
|
||||
}
|
||||
}
|
||||
if (topology === 'cluster') {
|
||||
if (seedAddresses.length < 2) {
|
||||
warnings.push('Cluster 只配置了一个种子节点,建议填写多个 master/replica 节点提高发现成功率');
|
||||
}
|
||||
const redisDB = Number(config.redisDB || 0);
|
||||
const includeRedisDatabases = Array.isArray(connection.includeRedisDatabases)
|
||||
? connection.includeRedisDatabases.filter((item) => typeof item === 'number')
|
||||
: [];
|
||||
if (redisDB > 0 || includeRedisDatabases.some((item) => item > 0)) {
|
||||
warnings.push('Redis Cluster 物理上只支持 db0;GoNavi 会用 __gonavi_db_N__: 前缀模拟逻辑库隔离');
|
||||
}
|
||||
if (normalizeText(config.redisSentinelMaster) || normalizeText(config.redisSentinelUser)) {
|
||||
warnings.push('Cluster 模式下 Sentinel master / Sentinel 用户字段不会生效');
|
||||
}
|
||||
}
|
||||
if (topology === 'single') {
|
||||
if (seedAddresses.length > 1) {
|
||||
warnings.push('单机模式下存在多个节点地址,后端会按多节点集群路径处理,建议显式改为 Cluster 模式');
|
||||
}
|
||||
if (normalizeText(config.redisSentinelMaster) || normalizeText(config.redisSentinelUser)) {
|
||||
warnings.push('单机模式下 Sentinel 字段不会生效,如需哨兵发现请切换为 Sentinel 模式');
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
};
|
||||
|
||||
const buildRedisTopologyRecommendations = (connection: SavedConnection, warnings: string[]): string[] => {
|
||||
const config = connection.config || {};
|
||||
const topology = normalizeRedisTopology(config.topology);
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (topology === 'sentinel') {
|
||||
recommendations.push('确认主机和附加节点填写的是 Sentinel 地址,不是 Redis master 地址');
|
||||
recommendations.push('分别填写 Redis 数据节点账号密码和 Sentinel 自身账号密码,二者不要混用');
|
||||
} else if (topology === 'cluster') {
|
||||
recommendations.push('优先配置 2 个以上种子节点,并确认这些节点属于同一个 Redis Cluster');
|
||||
recommendations.push('如果需要多库视图,优先在业务 key 上显式使用命名空间,避免误解 Cluster 的物理 db0 限制');
|
||||
} else {
|
||||
recommendations.push('单机模式只填写一个 Redis 地址;如果有多个节点,请改用 Cluster 或 Sentinel 模式');
|
||||
}
|
||||
|
||||
if (warnings.some((warning) => warning.includes('SSH'))) {
|
||||
recommendations.push('跨网络访问 Redis Cluster/Sentinel 时,优先使用网络代理、VPN 或 GoNavi MCP HTTP,而不是单端口 SSH 隧道');
|
||||
}
|
||||
if (config.useSSL === true) {
|
||||
recommendations.push('已启用 TLS,连接失败时优先核对 sslMode、CA/证书路径和服务端 SNI');
|
||||
}
|
||||
return recommendations;
|
||||
};
|
||||
|
||||
export const buildRedisTopologySnapshot = (params: {
|
||||
connections: SavedConnection[];
|
||||
connectionId?: unknown;
|
||||
keyword?: unknown;
|
||||
limit?: unknown;
|
||||
includeRecommendations?: unknown;
|
||||
}) => {
|
||||
const {
|
||||
connections,
|
||||
connectionId,
|
||||
keyword,
|
||||
limit,
|
||||
includeRecommendations,
|
||||
} = params;
|
||||
const safeConnectionId = normalizeText(connectionId);
|
||||
const safeKeyword = normalizeLowerText(keyword);
|
||||
const safeLimit = normalizeLimit(limit, 20, 100);
|
||||
const shouldIncludeRecommendations = includeRecommendations !== false;
|
||||
|
||||
const redisConnections = connections.filter((connection) =>
|
||||
normalizeLowerText(connection.config?.type) === 'redis',
|
||||
);
|
||||
const matchedConnections = redisConnections.filter((connection) => {
|
||||
if (safeConnectionId && connection.id !== safeConnectionId) {
|
||||
return false;
|
||||
}
|
||||
return matchesKeyword(safeKeyword, connection, buildSeedAddresses(connection));
|
||||
});
|
||||
|
||||
const topologyBreakdown = matchedConnections.reduce<Record<string, number>>((acc, connection) => {
|
||||
const key = normalizeRedisTopology(connection.config?.topology);
|
||||
acc[key] = (acc[key] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const visibleConnections = matchedConnections.slice(0, safeLimit).map((connection) => {
|
||||
const config = connection.config || {};
|
||||
const topology = normalizeRedisTopology(config.topology);
|
||||
const seedAddresses = buildSeedAddresses(connection);
|
||||
const warnings = buildRedisTopologyWarnings(connection, seedAddresses);
|
||||
const includeRedisDatabases = Array.isArray(connection.includeRedisDatabases)
|
||||
? connection.includeRedisDatabases.filter((item) => typeof item === 'number')
|
||||
: [];
|
||||
return {
|
||||
id: connection.id,
|
||||
name: connection.name,
|
||||
topology,
|
||||
host: normalizeText(config.host),
|
||||
port: Number.isFinite(Number(config.port)) ? Number(config.port) : null,
|
||||
seedAddresses,
|
||||
seedAddressCount: seedAddresses.length,
|
||||
redisDB: typeof config.redisDB === 'number' ? config.redisDB : 0,
|
||||
includeRedisDatabases,
|
||||
useSSL: config.useSSL === true,
|
||||
sslMode: config.sslMode || '',
|
||||
useSSH: config.useSSH === true,
|
||||
useProxy: config.useProxy === true,
|
||||
useHttpTunnel: config.useHttpTunnel === true,
|
||||
sentinelMaster: topology === 'sentinel' ? normalizeText(config.redisSentinelMaster) : '',
|
||||
hasRedisAuth: Boolean(normalizeText(config.user) || normalizeText(config.password) || connection.hasPrimaryPassword === true),
|
||||
hasSentinelAuth: topology === 'sentinel'
|
||||
? Boolean(normalizeText(config.redisSentinelUser) || normalizeText(config.redisSentinelPassword) || connection.hasRedisSentinelPassword === true)
|
||||
: false,
|
||||
warnings,
|
||||
recommendations: shouldIncludeRecommendations
|
||||
? buildRedisTopologyRecommendations(connection, warnings)
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const warningCount = visibleConnections.reduce((total, connection) => total + connection.warnings.length, 0);
|
||||
|
||||
return {
|
||||
connectionId: safeConnectionId,
|
||||
keyword: safeKeyword,
|
||||
limit: safeLimit,
|
||||
totalRedisConnections: redisConnections.length,
|
||||
totalMatched: matchedConnections.length,
|
||||
returnedConnections: visibleConnections.length,
|
||||
truncated: matchedConnections.length > visibleConnections.length,
|
||||
topologyBreakdown,
|
||||
warningCount,
|
||||
connections: visibleConnections,
|
||||
};
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { buildAIContextSnapshot } from './aiContextInsights';
|
||||
import { buildConnectionCapabilitiesSnapshot } from './aiConnectionCapabilitiesInsights';
|
||||
import { buildCurrentConnectionSnapshot } from './aiConnectionInsights';
|
||||
import { buildSavedConnectionsSnapshot } from './aiSavedConnectionInsights';
|
||||
import { buildRedisTopologySnapshot } from './aiRedisTopologyInsights';
|
||||
import { buildExternalSQLFileSnapshot } from './aiExternalSqlFileInsights';
|
||||
import { buildExternalSQLDirectoriesSnapshot } from './aiExternalSqlInsights';
|
||||
import { findBestMatchingExternalSQLDirectory } from './aiExternalSqlPathUtils';
|
||||
@@ -75,6 +76,17 @@ export async function executeConnectionWorkspaceSnapshotToolCall({
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_redis_topology':
|
||||
return {
|
||||
content: JSON.stringify(buildRedisTopologySnapshot({
|
||||
connections,
|
||||
connectionId: args.connectionId,
|
||||
keyword: args.keyword,
|
||||
limit: args.limit,
|
||||
includeRecommendations: args.includeRecommendations,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_external_sql_directories':
|
||||
return {
|
||||
content: JSON.stringify(buildExternalSQLDirectoriesSnapshot({
|
||||
|
||||
@@ -163,6 +163,7 @@ export async function executeSnapshotInspectionToolCall(
|
||||
inspect_current_connection: '读取当前连接失败',
|
||||
inspect_connection_capabilities: '读取当前连接能力矩阵失败',
|
||||
inspect_saved_connections: '读取本地连接清单失败',
|
||||
inspect_redis_topology: '读取 Redis 拓扑配置失败',
|
||||
inspect_external_sql_directories: '读取外部 SQL 目录失败',
|
||||
inspect_external_sql_file: '读取外部 SQL 文件失败',
|
||||
inspect_ai_sessions: '读取本地 AI 会话清单失败',
|
||||
|
||||
@@ -194,6 +194,12 @@ export const appendDatabaseInspectionGuidanceMessages = (
|
||||
'inspect_saved_connections',
|
||||
'如果用户提到“本地存了哪些连接”“帮我找 mysql / postgres / redis 连接”“哪条连接配了 SSH/代理”,优先调用 inspect_saved_connections 读取真实本地连接清单,再决定继续查看哪条连接。',
|
||||
);
|
||||
appendGuidanceIfToolAvailable(
|
||||
messages,
|
||||
availableToolNames,
|
||||
'inspect_redis_topology',
|
||||
'如果用户提到“Redis 哨兵/集群”“Sentinel master”“Redis Cluster 多库”“切换 Redis DB 失败”“Redis 多节点怎么填”,优先调用 inspect_redis_topology 读取真实 Redis 拓扑、节点、认证状态和风险提示,不要凭默认端口或经验猜测。',
|
||||
);
|
||||
appendGuidanceIfToolAvailable(
|
||||
messages,
|
||||
availableToolNames,
|
||||
|
||||
@@ -49,6 +49,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
|
||||
inspect_current_connection: '读取当前连接摘要',
|
||||
inspect_connection_capabilities: '读取当前连接能力矩阵',
|
||||
inspect_saved_connections: '盘点本地已保存连接',
|
||||
inspect_redis_topology: '诊断 Redis 拓扑配置',
|
||||
inspect_external_sql_directories: '盘点外部 SQL 目录',
|
||||
inspect_external_sql_file: '读取外部 SQL 文件',
|
||||
inspect_ai_sessions: '盘点本地 AI 历史会话',
|
||||
|
||||
@@ -104,6 +104,31 @@ export const BUILTIN_AI_INSPECTION_CONTEXT_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_redis_topology",
|
||||
icon: "🧰",
|
||||
desc: "诊断 Redis 单机/哨兵/集群配置",
|
||||
detail:
|
||||
"读取本地 Redis 连接拓扑摘要,返回单机、Sentinel、Cluster 的节点、master、认证状态、DB 范围和风险提示。适合用户问 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 密码。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
connectionId: { type: "string", description: "可选,只诊断某个 Redis 连接 ID" },
|
||||
keyword: { type: "string", description: "可选,按连接名、地址、拓扑、Sentinel master 或节点地址筛选" },
|
||||
limit: { type: "number", description: "可选,最多返回多少条 Redis 连接,默认 20,最大 100" },
|
||||
includeRecommendations: { type: "boolean", description: "可选,是否返回修复建议,默认 true" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_external_sql_directories",
|
||||
icon: "🗂️",
|
||||
|
||||
@@ -147,6 +147,15 @@ describe('aiToolRegistry', () => {
|
||||
expect(info?.tool.function.description).toContain('本地已保存连接清单');
|
||||
});
|
||||
|
||||
it('registers the Redis topology inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_redis_topology');
|
||||
expect(info).toBeTruthy();
|
||||
expect(info?.desc).toContain('Redis 单机/哨兵/集群');
|
||||
expect(info?.tool.function.description).toContain('Sentinel');
|
||||
expect(info?.tool.function.description).toContain('不会回显 Redis 密码');
|
||||
expect(info?.tool.function.parameters?.properties?.connectionId?.description).toContain('Redis 连接 ID');
|
||||
});
|
||||
|
||||
it('registers the external-sql-directory inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_external_sql_directories');
|
||||
expect(info).toBeTruthy();
|
||||
@@ -280,6 +289,7 @@ describe('aiToolRegistry', () => {
|
||||
expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_connection_capabilities')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_saved_connections')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_redis_topology')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_external_sql_directories')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_external_sql_file')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_recent_sql_activity')).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user