Skip to content

Commit cde620c

Browse files
pythonHuangclaude
andcommitted
feat: Redis多实例管理-新增t_redis_config表(实例名称/地址/端口/密码/库/默认实例唯一),配置页改多实例表格CRUD+测试+设默认,流程REDIS_GET/SET节点按实例ID选择(0=默认),执行器按实例解析连接串未配置明确报错
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 535e1d8 commit cde620c

12 files changed

Lines changed: 242 additions & 99 deletions

File tree

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Juggle.Application.Models.Response;
2+
using Juggle.Application.Services;
3+
using Juggle.Domain.Entities;
24
using Juggle.Infrastructure.Persistence;
35
using Microsoft.AspNetCore.Authorization;
46
using Microsoft.AspNetCore.Mvc;
@@ -7,49 +9,79 @@
79

810
namespace Juggle.Api.Controllers.Api;
911

10-
/// <summary>Redis 配置:流程 REDIS_GET/REDIS_SET 节点与缓存能力使用。</summary>
12+
/// <summary>
13+
/// Redis 实例配置:支持多个实例(一个默认),流程 REDIS_GET/REDIS_SET 节点按实例选择(0=默认)。
14+
/// </summary>
1115
[ApiController]
1216
[Route("api/system/redis")]
1317
[Authorize]
1418
public class RedisConfigController : ControllerBase
1519
{
1620
private readonly JuggleDbContext _db;
21+
private readonly ITenantAccessor _tenant;
1722

18-
public RedisConfigController(JuggleDbContext db) => _db = db;
23+
public RedisConfigController(JuggleDbContext db, ITenantAccessor tenant)
24+
{
25+
_db = db;
26+
_tenant = tenant;
27+
}
28+
29+
/// <summary>实例列表(含是否默认)</summary>
30+
[HttpGet("configs")]
31+
public async Task<ApiResult> List()
32+
=> ApiResult.Success(await _db.RedisConfigs.Where(r => r.Deleted == 0).OrderByDescending(r => r.Id).ToListAsync());
1933

20-
[HttpGet("config")]
21-
public async Task<ApiResult> GetConfig()
34+
/// <summary>保存(新增/更新)实例</summary>
35+
[HttpPost("config/save")]
36+
public async Task<ApiResult> Save([FromBody] RedisConfigSaveRequest req)
2237
{
23-
var configs = await _db.SystemConfigs
24-
.Where(c => c.Deleted == 0 && c.ConfigKey!.StartsWith("redis."))
25-
.ToListAsync();
26-
string? Get(string key) => configs.FirstOrDefault(c => c.ConfigKey == key)?.ConfigValue;
27-
return ApiResult.Success(new
38+
RedisConfigEntity entity;
39+
if (req.Id > 0)
40+
{
41+
entity = await _db.RedisConfigs.FindAsync(req.Id) ?? throw new Exception("实例不存在");
42+
}
43+
else
44+
{
45+
entity = new RedisConfigEntity { CreatedAt = DateTime.Now.ToString("o"), TenantId = _tenant.TenantId };
46+
_db.RedisConfigs.Add(entity);
47+
}
48+
entity.ConfigName = req.ConfigName;
49+
entity.Host = req.Host;
50+
entity.Port = req.Port;
51+
entity.Password = req.Password;
52+
entity.Db = req.Db;
53+
entity.IsDefault = req.IsDefault ? 1 : 0;
54+
entity.UpdatedAt = DateTime.Now.ToString("o");
55+
await _db.SaveChangesAsync();
56+
57+
// 默认实例唯一
58+
if (entity.IsDefault == 1)
2859
{
29-
host = Get("redis.host") ?? "",
30-
port = Get("redis.port") ?? "6379",
31-
password = Get("redis.password") ?? "",
32-
db = Get("redis.db") ?? "0"
33-
});
60+
var others = await _db.RedisConfigs.Where(r => r.Deleted == 0 && r.Id != entity.Id && r.IsDefault == 1).ToListAsync();
61+
foreach (var o in others) o.IsDefault = 0;
62+
if (others.Count > 0) await _db.SaveChangesAsync();
63+
}
64+
return ApiResult.Success(entity.Id);
3465
}
3566

36-
[HttpPost("config")]
37-
public async Task<ApiResult> SaveConfig([FromBody] RedisConfigRequest req)
67+
/// <summary>删除实例</summary>
68+
[HttpDelete("config/{id}")]
69+
public async Task<ApiResult> Delete(long id)
3870
{
39-
await UpsertAsync("redis.host", req.Host?.Trim() ?? "", "Redis地址");
40-
await UpsertAsync("redis.port", req.Port?.Trim() ?? "6379", "Redis端口");
41-
await UpsertAsync("redis.password", req.Password?.Trim() ?? "", "Redis密码");
42-
await UpsertAsync("redis.db", req.Db?.Trim() ?? "0", "Redis数据库");
71+
var entity = await _db.RedisConfigs.FindAsync(id);
72+
if (entity == null) return ApiResult.Fail("实例不存在");
73+
entity.Deleted = 1;
74+
await _db.SaveChangesAsync();
4375
return ApiResult.Success();
4476
}
4577

46-
/// <summary>测试 Redis 连接</summary>
47-
[HttpPost("test")]
48-
public async Task<ApiResult> Test([FromBody] RedisConfigRequest req)
78+
/// <summary>测试连接</summary>
79+
[HttpPost("config/test")]
80+
public async Task<ApiResult> Test([FromBody] RedisConfigSaveRequest req)
4981
{
5082
try
5183
{
52-
var connStr = $"{req.Host?.Trim()}:{req.Port?.Trim() ?? "6379"},password={req.Password?.Trim()},defaultDatabase={req.Db?.Trim() ?? "0"},abortConnect=false,connectTimeout=5000";
84+
var connStr = BuildConnStr(req.Host, req.Port, req.Password, req.Db);
5385
var conn = await ConnectionMultiplexer.ConnectAsync(connStr);
5486
var pong = await conn.GetDatabase().PingAsync();
5587
await conn.CloseAsync();
@@ -61,30 +93,18 @@ public async Task<ApiResult> Test([FromBody] RedisConfigRequest req)
6193
}
6294
}
6395

64-
private async Task UpsertAsync(string key, string value, string name)
65-
{
66-
var entity = await _db.SystemConfigs.FirstOrDefaultAsync(c => c.ConfigKey == key);
67-
if (entity == null)
68-
{
69-
_db.SystemConfigs.Add(new Domain.Entities.SystemConfigEntity
70-
{
71-
ConfigKey = key, ConfigValue = value, ConfigName = name, ConfigGroup = "Redis",
72-
CreatedAt = DateTime.Now.ToString("o")
73-
});
74-
}
75-
else
76-
{
77-
entity.ConfigValue = value;
78-
entity.UpdatedAt = DateTime.Now.ToString("o");
79-
}
80-
await _db.SaveChangesAsync();
81-
}
96+
/// <summary>组装连接串(供节点执行与测试共用)</summary>
97+
public static string BuildConnStr(string? host, string? port, string? password, string? db)
98+
=> $"{host?.Trim()}:{port?.Trim() ?? "6379"},password={password?.Trim()},defaultDatabase={db?.Trim() ?? "0"},abortConnect=false,connectTimeout=5000";
8299
}
83100

84-
public class RedisConfigRequest
101+
public class RedisConfigSaveRequest
85102
{
103+
public long Id { get; set; }
104+
public string ConfigName { get; set; } = "";
86105
public string Host { get; set; } = "";
87106
public string Port { get; set; } = "6379";
88107
public string Password { get; set; } = "";
89108
public string Db { get; set; } = "0";
109+
public bool IsDefault { get; set; }
90110
}

‎Juggle.Api/Program.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ await ctx.Response.WriteAsync(
256256
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_ai_assistant ADD COLUMN icon TEXT DEFAULT NULL;"); } catch { }
257257
// AI 助手对话会话表
258258
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_ai_conversation(id INTEGER PRIMARY KEY AUTOINCREMENT, assistant_id INTEGER, assistant_name TEXT, system_prompt TEXT, input_params TEXT, output_params TEXT, messages TEXT, outputs TEXT, provider_id INTEGER, model TEXT, title TEXT, status INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
259+
// Redis 多实例配置表
260+
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_redis_config(id INTEGER PRIMARY KEY AUTOINCREMENT, config_name TEXT, host TEXT, port TEXT, password TEXT, db TEXT, is_default INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
259261
// 知识库 4 表
260262
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_kb(id INTEGER PRIMARY KEY AUTOINCREMENT, kb_name TEXT, description TEXT, chunk_size INTEGER DEFAULT 500, chunk_overlap INTEGER DEFAULT 50, retrieve_type TEXT DEFAULT 'text', vector_model TEXT, enabled INTEGER DEFAULT 1, chunk_count INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
261263
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_kb_document(id INTEGER PRIMARY KEY AUTOINCREMENT, kb_id INTEGER, doc_name TEXT, doc_type TEXT, content TEXT, status INTEGER DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }

‎Juggle.Application/Services/Flow/FlowExecutionService.cs‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,29 @@ public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFac
3232
}
3333

3434
/// <summary>读取 Redis 连接配置(系统配置表),未配置返回 null。</summary>
35-
private async Task<string?> GetRedisConnStrAsync()
35+
/// <summary>读取全部 Redis 实例连接(实例ID → 连接串,0=默认实例)。</summary>
36+
private async Task<Dictionary<long, string>> GetRedisConnStrsAsync()
3637
{
37-
var configs = await _db.SystemConfigs
38-
.Where(c => c.Deleted == 0 && c.ConfigKey!.StartsWith("redis."))
39-
.ToListAsync();
40-
var host = configs.FirstOrDefault(c => c.ConfigKey == "redis.host")?.ConfigValue;
41-
if (string.IsNullOrWhiteSpace(host)) return null;
42-
var port = configs.FirstOrDefault(c => c.ConfigKey == "redis.port")?.ConfigValue ?? "6379";
43-
var password = configs.FirstOrDefault(c => c.ConfigKey == "redis.password")?.ConfigValue ?? "";
44-
var dbIndex = configs.FirstOrDefault(c => c.ConfigKey == "redis.db")?.ConfigValue ?? "0";
45-
return $"{host}:{port},password={password},defaultDatabase={dbIndex},abortConnect=false,connectTimeout=5000";
38+
var map = new Dictionary<long, string>();
39+
var configs = await _db.RedisConfigs.Where(r => r.Deleted == 0).ToListAsync();
40+
foreach (var c in configs)
41+
{
42+
if (string.IsNullOrWhiteSpace(c.Host)) continue;
43+
var connStr = $"{(c.Host ?? "").Trim()}:{c.Port?.Trim() ?? "6379"},password={c.Password?.Trim()},defaultDatabase={c.Db?.Trim() ?? "0"},abortConnect=false,connectTimeout=5000";
44+
map[c.Id] = connStr;
45+
if (c.IsDefault == 1) map[0] = connStr; // 0=默认实例
46+
}
47+
return map;
4648
}
4749

48-
/// <summary>构建引擎(注入大模型对话/视觉函数、Redis 连接与知识库检索,供 AI/文件解析/Redis/KB_SEARCH 节点使用)。</summary>
50+
/// <summary>构建引擎(注入大模型对话/视觉函数、Redis 多实例连接与知识库检索,供 AI/文件解析/Redis/KB_SEARCH 节点使用)。</summary>
4951
private async Task<FlowEngine> BuildEngineAsync(Dictionary<string, DataSourceInfo> dsInfos,
5052
Dictionary<string, string?> staticVars, Func<string, Task<string?>> flowContentLoader)
5153
=> new FlowEngine(_httpClientFactory, dsInfos, staticVars, flowContentLoader,
5254
aiChatFunc: req => req.Images.Count > 0
5355
? _aiService.ChatWithImagesAsync(req.SystemPrompt, req.UserInput, req.Images, req.ProviderId, req.Model)
5456
: _aiService.ChatAsync(req.SystemPrompt, req.UserInput, req.ProviderId, modelOverride: req.Model),
55-
redisConnStr: await GetRedisConnStrAsync(),
57+
redisConnStrs: await GetRedisConnStrsAsync(),
5658
kbSearchFunc: (kbId, query, topK) => _kbService.SearchAsContextAsync(kbId, query, topK));
5759

5860
// ────────────────────────────────────────────────────────────────

‎Juggle.Domain/Engine/FlowEngine.cs‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ public class FlowEngine
1919
private readonly Func<string, Task<string?>>? _flowContentLoader;
2020
/// <summary>大模型对话函数(AiChatRequest → reply),供 AI 节点与文件解析(图片)调用,可为 null(未接入大模型)</summary>
2121
private readonly Func<AiChatRequest, Task<string>>? _aiChatFunc;
22-
/// <summary>Redis 连接串(供 REDIS_GET/REDIS_SET 节点使用),可为 null(未配置 Redis)</summary>
23-
private readonly string? _redisConnStr;
22+
/// <summary>Redis 实例连接串(实例ID → 连接串,0=默认实例),供 REDIS_GET/REDIS_SET 节点使用</summary>
23+
private readonly Dictionary<long, string> _redisConnStrs;
2424
/// <summary>知识库检索函数(kbId, query, topK → 上下文文本),供 KB_SEARCH 节点使用</summary>
2525
private readonly Func<long, string, int, Task<string>>? _kbSearchFunc;
2626

@@ -29,18 +29,26 @@ public FlowEngine(IHttpClientFactory httpClientFactory,
2929
Dictionary<string, string?>? staticVariables = null,
3030
Func<string, Task<string?>>? flowContentLoader = null,
3131
Func<AiChatRequest, Task<string>>? aiChatFunc = null,
32-
string? redisConnStr = null,
32+
Dictionary<long, string>? redisConnStrs = null,
3333
Func<long, string, int, Task<string>>? kbSearchFunc = null)
3434
{
3535
_httpClientFactory = httpClientFactory;
3636
_dataSources = dataSources ?? new();
3737
_staticVarSnapshot = staticVariables ?? new(StringComparer.OrdinalIgnoreCase);
3838
_flowContentLoader = flowContentLoader;
3939
_aiChatFunc = aiChatFunc;
40-
_redisConnStr = redisConnStr;
40+
_redisConnStrs = redisConnStrs ?? new();
4141
_kbSearchFunc = kbSearchFunc;
4242
}
4343

44+
/// <summary>解析 Redis 连接串(按实例 ID,0=默认实例);未配置时返回 null。</summary>
45+
private string? ResolveRedisConnStr(long redisId)
46+
{
47+
if (_redisConnStrs.TryGetValue(redisId, out var connStr)) return connStr;
48+
if (redisId != 0 && _redisConnStrs.TryGetValue(0, out var def)) return def;
49+
return null;
50+
}
51+
4452
public async Task<FlowResult> ExecuteAsync(
4553
string flowContent,
4654
Dictionary<string, object?> inputParams,
@@ -165,10 +173,8 @@ public async Task<FlowResult> ExecuteAsync(
165173
"FILE_PARSE" => new FileParseNodeExecutor(_aiChatFunc),
166174
"EXCEL_READ" => new ExcelReadNodeExecutor(),
167175
"FILE_WRITE" => new FileWriteNodeExecutor(),
168-
"REDIS_GET" => new RedisGetNodeExecutor(
169-
_redisConnStr ?? throw new InvalidOperationException("未配置 Redis(系统设置 → Redis 配置),无法执行 Redis 查询节点")),
170-
"REDIS_SET" => new RedisSetNodeExecutor(
171-
_redisConnStr ?? throw new InvalidOperationException("未配置 Redis(系统设置 → Redis 配置),无法执行 Redis 设置节点")),
176+
"REDIS_GET" => new RedisGetNodeExecutor(_redisConnStrs),
177+
"REDIS_SET" => new RedisSetNodeExecutor(_redisConnStrs),
172178
"KB_SEARCH" => new KbSearchNodeExecutor(
173179
_kbSearchFunc ?? throw new InvalidOperationException("流程引擎未接入知识库检索能力,无法执行知识库检索节点")),
174180
"DATA_EXTRACT" => new DataExtractNodeExecutor(),

‎Juggle.Domain/Engine/FlowModels.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@ public class FileWriteConfig
357357
/// <summary>Redis 缓存查询节点配置(REDIS_GET 节点)</summary>
358358
public class RedisGetConfig
359359
{
360+
/// <summary>Redis 实例 ID(0=默认实例,系统设置 → Redis 配置中维护)</summary>
361+
[JsonPropertyName("redisId")]
362+
public long RedisId { get; set; }
363+
360364
/// <summary>key(支持 ${变量} 模板)</summary>
361365
[JsonPropertyName("key")]
362366
public string Key { get; set; } = "";
@@ -369,6 +373,10 @@ public class RedisGetConfig
369373
/// <summary>Redis 缓存设置节点配置(REDIS_SET 节点)</summary>
370374
public class RedisSetConfig
371375
{
376+
/// <summary>Redis 实例 ID(0=默认实例,系统设置 → Redis 配置中维护)</summary>
377+
[JsonPropertyName("redisId")]
378+
public long RedisId { get; set; }
379+
372380
/// <summary>key(支持 ${变量} 模板)</summary>
373381
[JsonPropertyName("key")]
374382
public string Key { get; set; } = "";

‎Juggle.Domain/Engine/NodeExecutors/RedisNodeExecutors.cs‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@ public static ConnectionMultiplexer Get(string connStr)
2626
/// <summary>Redis 缓存查询节点:按 key 取值(JSON 字符串自动解析为对象)。</summary>
2727
public class RedisGetNodeExecutor : INodeExecutor
2828
{
29-
private readonly string _connStr;
29+
private readonly Dictionary<long, string> _connStrs;
3030

31-
public RedisGetNodeExecutor(string connStr) => _connStr = connStr;
31+
public RedisGetNodeExecutor(Dictionary<long, string> connStrs) => _connStrs = connStrs;
3232

3333
public async Task<string?> ExecuteAsync(FlowNode node, FlowContext context)
3434
{
3535
var cfg = node.RedisGetConfig
3636
?? throw new InvalidOperationException($"Redis 查询节点 [{node.Key}] 未配置 redisGetConfig。");
3737
if (string.IsNullOrWhiteSpace(cfg.Key))
3838
throw new InvalidOperationException($"Redis 查询节点 [{node.Key}] 未配置 key。");
39+
var connStr = Resolve(cfg.RedisId, $"Redis 查询节点 [{node.Key}]");
3940

4041
var key = RenderTemplate(cfg.Key, context);
41-
var db = RedisConnectionPool.Get(_connStr).GetDatabase();
42+
var db = RedisConnectionPool.Get(connStr).GetDatabase();
4243

4344
var value = await db.StringGetAsync(key);
4445
object? result = null;
@@ -60,6 +61,14 @@ public class RedisGetNodeExecutor : INodeExecutor
6061
return node.Outgoings.FirstOrDefault();
6162
}
6263

64+
/// <summary>解析实例连接串(0=默认实例),未配置时抛明确错误。</summary>
65+
private string Resolve(long redisId, string nodeDesc)
66+
{
67+
if (_connStrs.TryGetValue(redisId, out var connStr)) return connStr;
68+
if (redisId != 0 && _connStrs.TryGetValue(0, out var def)) return def;
69+
throw new InvalidOperationException($"{nodeDesc} 未配置 Redis(系统设置 → Redis 配置,或该实例已被删除)");
70+
}
71+
6372
internal static string RenderTemplate(string template, FlowContext context)
6473
=> System.Text.RegularExpressions.Regex.Replace(template, @"\$\{([^}]+)\}", m =>
6574
{
@@ -71,16 +80,17 @@ internal static string RenderTemplate(string template, FlowContext context)
7180
/// <summary>Redis 缓存设置节点:写入 key-value(可设过期秒数),结果写入输出变量。</summary>
7281
public class RedisSetNodeExecutor : INodeExecutor
7382
{
74-
private readonly string _connStr;
83+
private readonly Dictionary<long, string> _connStrs;
7584

76-
public RedisSetNodeExecutor(string connStr) => _connStr = connStr;
85+
public RedisSetNodeExecutor(Dictionary<long, string> connStrs) => _connStrs = connStrs;
7786

7887
public async Task<string?> ExecuteAsync(FlowNode node, FlowContext context)
7988
{
8089
var cfg = node.RedisSetConfig
8190
?? throw new InvalidOperationException($"Redis 设置节点 [{node.Key}] 未配置 redisSetConfig。");
8291
if (string.IsNullOrWhiteSpace(cfg.Key))
8392
throw new InvalidOperationException($"Redis 设置节点 [{node.Key}] 未配置 key。");
93+
var connStr = Resolve(cfg.RedisId, $"Redis 设置节点 [{node.Key}]");
8494

8595
var key = RedisGetNodeExecutor.RenderTemplate(cfg.Key, context);
8696
var val = string.IsNullOrWhiteSpace(cfg.Value)
@@ -90,7 +100,7 @@ public class RedisSetNodeExecutor : INodeExecutor
90100
string s => s,
91101
var v => JsonSerializer.Serialize(v)
92102
};
93-
var db = RedisConnectionPool.Get(_connStr).GetDatabase();
103+
var db = RedisConnectionPool.Get(connStr).GetDatabase();
94104

95105
var ok = cfg.ExpireSeconds > 0
96106
? await db.StringSetAsync(key, val, TimeSpan.FromSeconds(cfg.ExpireSeconds))
@@ -101,4 +111,12 @@ public class RedisSetNodeExecutor : INodeExecutor
101111

102112
return node.Outgoings.FirstOrDefault();
103113
}
114+
115+
/// <summary>解析实例连接串(0=默认实例),未配置时抛明确错误。</summary>
116+
private string Resolve(long redisId, string nodeDesc)
117+
{
118+
if (_connStrs.TryGetValue(redisId, out var connStr)) return connStr;
119+
if (redisId != 0 && _connStrs.TryGetValue(0, out var def)) return def;
120+
throw new InvalidOperationException($"{nodeDesc} 未配置 Redis(系统设置 → Redis 配置,或该实例已被删除)");
121+
}
104122
}

0 commit comments

Comments
 (0)