Skip to content

Commit e344f80

Browse files
pythonHuangclaude
andcommitted
feat: 模型助手管理-自定义智能助手CRUD(名称/描述/系统提示词/输入参数列表(文本数字日期开关下拉)/输出参数列表/启停),启用后自动加入模型助手菜单,助手运行页(参数表单+补充说明→大模型→输出参数JSON解析展示+原文)
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 02d3e41 commit e344f80

9 files changed

Lines changed: 519 additions & 2 deletions

File tree

‎Juggle.Api/Controllers/Api/AiController.cs‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,80 @@ public async Task<ApiResult> ApplyApis([FromBody] AiApplyApisRequest req)
200200
}
201201
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
202202
}
203+
204+
// ==================== 自定义助手 ====================
205+
206+
/// <summary>助手列表(管理页)</summary>
207+
[HttpGet("assistants")]
208+
public async Task<ApiResult> Assistants()
209+
=> ApiResult.Success(await _db.AiAssistants.Where(a => a.Deleted == 0).OrderByDescending(a => a.Id).ToListAsync());
210+
211+
/// <summary>启用的助手(菜单用)</summary>
212+
[HttpGet("assistants/enabled")]
213+
public async Task<ApiResult> EnabledAssistants()
214+
=> ApiResult.Success(await _db.AiAssistants.Where(a => a.Deleted == 0 && a.Enabled == 1).OrderBy(a => a.Id).ToListAsync());
215+
216+
/// <summary>新增/更新助手</summary>
217+
[HttpPost("assistant/save")]
218+
public async Task<ApiResult> SaveAssistant([FromBody] AiAssistantSaveRequest req)
219+
{
220+
AiAssistantEntity entity;
221+
if (req.Id > 0)
222+
{
223+
entity = await _db.AiAssistants.FindAsync(req.Id) ?? throw new Exception("助手不存在");
224+
}
225+
else
226+
{
227+
entity = new AiAssistantEntity { CreatedAt = DateTime.Now.ToString("o"), TenantId = _tenant.TenantId };
228+
_db.AiAssistants.Add(entity);
229+
}
230+
entity.AssistantName = req.AssistantName;
231+
entity.Description = req.Description;
232+
entity.SystemPrompt = req.SystemPrompt;
233+
entity.InputParams = req.InputParams;
234+
entity.OutputParams = req.OutputParams;
235+
entity.Enabled = req.Enabled ? 1 : 0;
236+
entity.UpdatedAt = DateTime.Now.ToString("o");
237+
await _db.SaveChangesAsync();
238+
return ApiResult.Success(entity.Id);
239+
}
240+
241+
/// <summary>删除助手</summary>
242+
[HttpDelete("assistant/{id}")]
243+
public async Task<ApiResult> DeleteAssistant(long id)
244+
{
245+
var entity = await _db.AiAssistants.FindAsync(id);
246+
if (entity == null) return ApiResult.Fail("助手不存在");
247+
entity.Deleted = 1;
248+
await _db.SaveChangesAsync();
249+
return ApiResult.Success();
250+
}
251+
252+
/// <summary>启用/禁用助手</summary>
253+
[HttpPost("assistant/toggle")]
254+
public async Task<ApiResult> ToggleAssistant([FromBody] AiAssistantToggleRequest req)
255+
{
256+
var entity = await _db.AiAssistants.FindAsync(req.Id);
257+
if (entity == null) return ApiResult.Fail("助手不存在");
258+
entity.Enabled = req.Enabled ? 1 : 0;
259+
entity.UpdatedAt = DateTime.Now.ToString("o");
260+
await _db.SaveChangesAsync();
261+
return ApiResult.Success();
262+
}
263+
264+
/// <summary>运行助手:输入参数 + 补充说明 → 大模型 → 输出参数(JSON)与原文</summary>
265+
[HttpPost("assistant-run")]
266+
public async Task<ApiResult> RunAssistant([FromBody] AiAssistantRunRequest req)
267+
{
268+
try
269+
{
270+
var assistant = await _db.AiAssistants.FirstOrDefaultAsync(a => a.Id == req.AssistantId && a.Deleted == 0)
271+
?? throw new Exception("助手不存在");
272+
var result = await _aiService.RunAssistantAsync(assistant, req.Inputs, req.ExtraText, req.ProviderId, req.Model);
273+
return ApiResult.Success(result);
274+
}
275+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
276+
}
203277
}
204278

205279
public class AiConfigRequest
@@ -264,3 +338,29 @@ public class AiFetchModelsRequest
264338
public string BaseUrl { get; set; } = "";
265339
public string ApiKey { get; set; } = "";
266340
}
341+
342+
public class AiAssistantSaveRequest
343+
{
344+
public long Id { get; set; }
345+
public string AssistantName { get; set; } = "";
346+
public string? Description { get; set; }
347+
public string? SystemPrompt { get; set; }
348+
public string? InputParams { get; set; }
349+
public string? OutputParams { get; set; }
350+
public bool Enabled { get; set; } = true;
351+
}
352+
353+
public class AiAssistantToggleRequest
354+
{
355+
public long Id { get; set; }
356+
public bool Enabled { get; set; }
357+
}
358+
359+
public class AiAssistantRunRequest
360+
{
361+
public long AssistantId { get; set; }
362+
public long ProviderId { get; set; }
363+
public string? Model { get; set; }
364+
public Dictionary<string, object?>? Inputs { get; set; }
365+
public string? ExtraText { get; set; }
366+
}

‎Juggle.Api/Program.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ await ctx.Response.WriteAsync(
165165
builder.Services.AddScoped<JwtService>(); // JWT Token 签发
166166
builder.Services.AddScoped<ReportExecutionService>(); // 报表数据执行 + 渲染
167167
builder.Services.AddScoped<ITenantAccessor, TenantAccessor>(); // 多租户上下文
168+
builder.Services.AddScoped<AiService>(); // AiService
168169

169170
// Swagger
170171
builder.Services.AddEndpointsApiExplorer();
@@ -248,6 +249,8 @@ await ctx.Response.WriteAsync(
248249
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_data_view ADD COLUMN column_mapping TEXT DEFAULT NULL;"); } catch { }
249250
// AI 大模型供应商表
250251
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_ai_provider(id INTEGER PRIMARY KEY AUTOINCREMENT, provider_name TEXT, base_url TEXT, api_key TEXT, model TEXT, models TEXT, enabled INTEGER DEFAULT 1, remark TEXT, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
252+
// AI 自定义助手表
253+
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_ai_assistant(id INTEGER PRIMARY KEY AUTOINCREMENT, assistant_name TEXT, description TEXT, system_prompt TEXT, input_params TEXT, output_params TEXT, enabled INTEGER DEFAULT 1, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
251254
// 补建参数位置字段
252255
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_parameter ADD COLUMN param_position TEXT DEFAULT NULL;"); } catch { }
253256
}

‎Juggle.Application/Services/Impl/AiService.cs‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,79 @@ public async Task<object> ApplyFlowAsync(string flowName, string? flowDesc, stri
331331
private static string GetStr(JsonElement el, string prop)
332332
=> el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : "";
333333

334+
// ==================== 自定义助手 ====================
335+
336+
/// <summary>
337+
/// 运行自定义助手:系统提示词 + 输入参数(+补充说明)→ 大模型。
338+
/// 配置了输出参数时要求模型返回 JSON 并按参数名解析,解析失败保留原文。
339+
/// </summary>
340+
public async Task<object> RunAssistantAsync(AiAssistantEntity assistant, Dictionary<string, object?>? inputs,
341+
string? extraText, long providerId = 0, string? model = null)
342+
{
343+
var inputParams = ParseParamList(assistant.InputParams);
344+
var outputParams = ParseParamList(assistant.OutputParams);
345+
346+
var sb = new StringBuilder();
347+
if (inputParams.Count > 0)
348+
{
349+
sb.AppendLine("## 输入参数");
350+
foreach (var p in inputParams)
351+
{
352+
var name = p.GetValueOrDefault("name")?.ToString() ?? "";
353+
var label = p.GetValueOrDefault("label")?.ToString() ?? name;
354+
sb.AppendLine($"- {label}:{inputs?.GetValueOrDefault(name) ?? ""}");
355+
}
356+
}
357+
if (!string.IsNullOrWhiteSpace(extraText))
358+
{
359+
sb.AppendLine("## 补充说明");
360+
sb.AppendLine(extraText.Trim());
361+
}
362+
if (outputParams.Count > 0)
363+
{
364+
var names = string.Join(", ", outputParams.Select(p => $"\"{p.GetValueOrDefault("name")}\""));
365+
var descs = string.Join("、", outputParams.Select(p =>
366+
$"\"{p.GetValueOrDefault("name")}\" 表示{p.GetValueOrDefault("label") ?? p.GetValueOrDefault("name")}"));
367+
sb.AppendLine("## 输出要求");
368+
sb.AppendLine($"请只输出 JSON(不要解释、不要 markdown 围栏),字段:{names},其中 {descs}。");
369+
}
370+
371+
var content = await ChatAsync(assistant.SystemPrompt ?? "你是一个智能助手。", sb.ToString(), providerId, modelOverride: model);
372+
373+
var outputs = new Dictionary<string, object?>();
374+
if (outputParams.Count > 0)
375+
{
376+
try
377+
{
378+
var json = ExtractJson(content);
379+
using var doc = JsonDocument.Parse(json);
380+
foreach (var p in outputParams)
381+
{
382+
var name = p.GetValueOrDefault("name")?.ToString() ?? "";
383+
if (name.Length > 0 && doc.RootElement.TryGetProperty(name, out var v))
384+
outputs[name] = CloneToObject(v);
385+
}
386+
}
387+
catch { /* 模型未按 JSON 输出时保留原文 */ }
388+
}
389+
return new { reply = content, outputs };
390+
}
391+
392+
private static List<Dictionary<string, object?>> ParseParamList(string? json)
393+
{
394+
if (string.IsNullOrWhiteSpace(json)) return new List<Dictionary<string, object?>>();
395+
try
396+
{
397+
using var doc = JsonDocument.Parse(json);
398+
if (doc.RootElement.ValueKind != JsonValueKind.Array) return new List<Dictionary<string, object?>>();
399+
return doc.RootElement.EnumerateArray()
400+
.Where(e => e.ValueKind == JsonValueKind.Object)
401+
.Select(e => e.EnumerateObject().ToDictionary(p => p.Name, p => CloneToObject(p.Value)))
402+
.ToList();
403+
}
404+
catch { return new List<Dictionary<string, object?>>(); }
405+
}
406+
334407
/// <summary>流程生成的系统提示词:描述与设计器一致的节点格式。</summary>
335408
private static string BuildSystemPrompt()
336409
{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Juggle.Domain.Entities;
2+
3+
/// <summary>
4+
/// 自定义智能助手
5+
/// 每个助手可配置名称、系统提示词、输入/输出参数列表,
6+
/// 启用后出现在「模型助手」菜单中,运行页按参数表单输入并调用大模型。
7+
/// </summary>
8+
public class AiAssistantEntity : BaseEntity
9+
{
10+
/// <summary>助手名称(菜单显示)</summary>
11+
public string? AssistantName { get; set; }
12+
13+
/// <summary>助手描述</summary>
14+
public string? Description { get; set; }
15+
16+
/// <summary>系统提示词(人设与任务说明)</summary>
17+
public string? SystemPrompt { get; set; }
18+
19+
/// <summary>输入参数列表(JSON: [{name,label,type,options,default}])</summary>
20+
public string? InputParams { get; set; }
21+
22+
/// <summary>输出参数列表(JSON: [{name,label,type}])</summary>
23+
public string? OutputParams { get; set; }
24+
25+
/// <summary>是否启用:1=启用(出现在菜单) 0=禁用</summary>
26+
public int Enabled { get; set; } = 1;
27+
}

‎Juggle.Infrastructure/Persistence/JuggleDbContext.cs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public JuggleDbContext(DbContextOptions<JuggleDbContext> options, ICurrentTenant
5959
public DbSet<AuditLogEntity> AuditLogs { get; set; } = null!;
6060
public DbSet<LoginLogEntity> LoginLogs { get; set; } = null!;
6161
public DbSet<AiProviderEntity> AiProviders { get; set; } = null!;
62+
public DbSet<AiAssistantEntity> AiAssistants { get; set; } = null!;
6263

6364
protected override void OnModelCreating(ModelBuilder modelBuilder)
6465
{
@@ -106,6 +107,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
106107
e.Property(p => p.Enabled).HasColumnName("enabled");
107108
e.Property(p => p.Remark).HasColumnName("remark");
108109
});
110+
modelBuilder.Entity<AiAssistantEntity>().ToTable("t_ai_assistant");
111+
modelBuilder.Entity<AiAssistantEntity>(e => {
112+
e.Property(p => p.Id).HasColumnName("id");
113+
e.Property(p => p.Deleted).HasColumnName("deleted");
114+
e.Property(p => p.CreatedAt).HasColumnName("created_at");
115+
e.Property(p => p.CreatedBy).HasColumnName("created_by");
116+
e.Property(p => p.UpdatedAt).HasColumnName("updated_at");
117+
e.Property(p => p.UpdatedBy).HasColumnName("updated_by");
118+
e.Property(p => p.TenantId).HasColumnName("tenant_id");
119+
e.Property(p => p.AssistantName).HasColumnName("assistant_name");
120+
e.Property(p => p.Description).HasColumnName("description");
121+
e.Property(p => p.SystemPrompt).HasColumnName("system_prompt");
122+
e.Property(p => p.InputParams).HasColumnName("input_params");
123+
e.Property(p => p.OutputParams).HasColumnName("output_params");
124+
e.Property(p => p.Enabled).HasColumnName("enabled");
125+
});
109126
modelBuilder.Entity<AlertRuleEntity>().ToTable("t_alert_rule");
110127
modelBuilder.Entity<AlertRecordEntity>().ToTable("t_alert_record");
111128
modelBuilder.Entity<DataViewEntity>().ToTable("t_data_view");

‎JuggleNet6.Frontend/src/router/index.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ const routes = [
4242
{ path: 'report/view/:id', component: () => import('../views/report/ReportView.vue') },
4343
{ path: 'system/ai-provider', component: () => import('../views/system/AiProviderList.vue') },
4444
{ path: 'ai/flow-assistant', component: () => import('../views/ai/FlowAssistant.vue') },
45-
{ path: 'ai/api-assistant', component: () => import('../views/ai/ApiAssistant.vue') }
45+
{ path: 'ai/api-assistant', component: () => import('../views/ai/ApiAssistant.vue') },
46+
{ path: 'ai/assistants', component: () => import('../views/ai/AiAssistantList.vue') },
47+
{ path: 'ai/assistant/:id', component: () => import('../views/ai/AssistantRun.vue') }
4648
]
4749
},
4850
{

‎JuggleNet6.Frontend/src/views/Layout.vue‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@
5959
<el-icon><MagicStick /></el-icon>
6060
<span>模型助手</span>
6161
</template>
62-
<el-menu-item index="/system/ai-provider">模型助手管理</el-menu-item>
62+
<el-menu-item index="/ai/assistants">模型助手管理</el-menu-item>
6363
<el-menu-item index="/ai/flow-assistant">流程智能编排助手</el-menu-item>
6464
<el-menu-item index="/ai/api-assistant">接口智能接入助手</el-menu-item>
65+
<el-menu-item v-for="a in assistantMenuList" :key="a.id" :index="`/ai/assistant/${a.id}`">{{ a.assistantName }}</el-menu-item>
6566
</el-sub-menu>
6667
<el-sub-menu index="system" v-if="hasMenu('/system/token')">
6768
<template #title>
@@ -128,11 +129,17 @@ const activeMenu = computed(() => route.path)
128129
129130
// 报表查询菜单:每个启用报表一个子菜单
130131
const reportMenuList = ref<any[]>([])
132+
// 模型助手菜单:每个启用的自定义助手一个子菜单
133+
const assistantMenuList = ref<any[]>([])
131134
onMounted(async () => {
132135
try {
133136
const res = await request.post('/report/page', { pageNum: 1, pageSize: 200 })
134137
reportMenuList.value = (res.data?.list || []).filter((r: any) => r.status === 1)
135138
} catch { /* 忽略菜单加载失败 */ }
139+
try {
140+
const res2 = await request.get('/ai/assistants/enabled')
141+
assistantMenuList.value = res2.data || []
142+
} catch { /* 未配置助手时忽略 */ }
136143
})
137144
138145
// 权限菜单列表

0 commit comments

Comments
 (0)