Skip to content

Commit d15f35a

Browse files
pythonHuangclaude
andcommitted
feat: 知识库子系统后端-多知识库CRUD与配置(切片大小/重叠/文本或向量检索/向量模型),文档上传自动解析(word/excel/pdf/txt/markdown)切片入库,手动片段,文本关键词检索与向量余弦相似度检索(embeddings接口),知识库清洗(去空去重超短)与重新向量化,匹配记录查询,流程设计器新增知识库检索节点(KB_SEARCH)供AI节点RAG问答
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent c815986 commit d15f35a

10 files changed

Lines changed: 746 additions & 4 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using Juggle.Application.Models.Response;
2+
using Juggle.Application.Services.Impl;
3+
using Juggle.Domain.Entities;
4+
using Juggle.Infrastructure.Persistence;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.EntityFrameworkCore;
8+
9+
namespace Juggle.Api.Controllers.Api;
10+
11+
/// <summary>知识库:多库管理、文档上传解析、手动片段、切片配置、文本/向量检索、清洗、匹配记录。</summary>
12+
[ApiController]
13+
[Route("api/kb")]
14+
[Authorize]
15+
public class KnowledgeController : ControllerBase
16+
{
17+
private readonly JuggleDbContext _db;
18+
private readonly KnowledgeService _kbService;
19+
20+
public KnowledgeController(JuggleDbContext db, KnowledgeService kbService)
21+
{
22+
_db = db;
23+
_kbService = kbService;
24+
}
25+
26+
// ==================== 知识库管理 ====================
27+
28+
[HttpGet("list")]
29+
public async Task<ApiResult> List()
30+
=> ApiResult.Success(await _db.KnowledgeBases.Where(k => k.Deleted == 0).OrderByDescending(k => k.Id).ToListAsync());
31+
32+
[HttpPost("save")]
33+
public async Task<ApiResult> Save([FromBody] KnowledgeBaseEntity entity)
34+
{
35+
KnowledgeBaseEntity kb;
36+
if (entity.Id > 0)
37+
{
38+
kb = await _db.KnowledgeBases.FindAsync(entity.Id) ?? throw new Exception("知识库不存在");
39+
}
40+
else
41+
{
42+
kb = new KnowledgeBaseEntity { CreatedAt = DateTime.Now.ToString("o") };
43+
_db.KnowledgeBases.Add(kb);
44+
}
45+
kb.KbName = entity.KbName;
46+
kb.Description = entity.Description;
47+
kb.ChunkSize = entity.ChunkSize > 0 ? entity.ChunkSize : 500;
48+
kb.ChunkOverlap = entity.ChunkOverlap >= 0 ? entity.ChunkOverlap : 50;
49+
kb.RetrieveType = entity.RetrieveType == "vector" ? "vector" : "text";
50+
kb.VectorModel = entity.VectorModel;
51+
kb.Enabled = entity.Enabled;
52+
kb.UpdatedAt = DateTime.Now.ToString("o");
53+
await _db.SaveChangesAsync();
54+
return ApiResult.Success(kb.Id);
55+
}
56+
57+
[HttpDelete("delete/{id}")]
58+
public async Task<ApiResult> Delete(long id)
59+
{
60+
var kb = await _db.KnowledgeBases.FindAsync(id);
61+
if (kb == null) return ApiResult.Fail("知识库不存在");
62+
kb.Deleted = 1;
63+
await _db.SaveChangesAsync();
64+
return ApiResult.Success();
65+
}
66+
67+
// ==================== 文档与片段 ====================
68+
69+
/// <summary>上传文档(word/excel/pdf/txt/markdown),自动解析并切片入库。</summary>
70+
[HttpPost("upload/{id}")]
71+
public async Task<ApiResult> Upload(long id, IFormFile file)
72+
{
73+
try
74+
{
75+
if (file == null || file.Length == 0) return ApiResult.Fail("未选择文件");
76+
using var ms = new MemoryStream();
77+
await file.CopyToAsync(ms);
78+
var content = _kbService.ParseDocument(file.FileName, ms.ToArray());
79+
var ext = Path.GetExtension(file.FileName).TrimStart('.').ToLower();
80+
var result = await _kbService.AddDocumentAsync(id, file.FileName, ext, content);
81+
return ApiResult.Success(result);
82+
}
83+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
84+
}
85+
86+
/// <summary>手动增加片段</summary>
87+
[HttpPost("chunk/{id}")]
88+
public async Task<ApiResult> AddChunk(long id, [FromBody] KbChunkRequest req)
89+
{
90+
try
91+
{
92+
var result = await _kbService.AddChunkAsync(id, req.Content ?? "");
93+
return ApiResult.Success(result);
94+
}
95+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
96+
}
97+
98+
/// <summary>知识库文档列表</summary>
99+
[HttpGet("documents/{id}")]
100+
public async Task<ApiResult> Documents(long id)
101+
=> ApiResult.Success(await _db.KnowledgeDocuments.Where(d => d.KbId == id && d.Deleted == 0).OrderByDescending(d => d.Id).Take(200).ToListAsync());
102+
103+
/// <summary>片段列表(分页)</summary>
104+
[HttpGet("chunks/{id}")]
105+
public async Task<ApiResult> Chunks(long id, int page = 1, int pageSize = 50)
106+
=> ApiResult.Success(await _db.KnowledgeChunks
107+
.Where(c => c.KbId == id && c.Deleted == 0)
108+
.OrderBy(c => c.Id)
109+
.Skip((page - 1) * pageSize).Take(pageSize)
110+
.Select(c => new { c.Id, c.DocId, c.SeqNo, c.Content })
111+
.ToListAsync());
112+
113+
// ==================== 检索 / 清洗 / 匹配记录 ====================
114+
115+
/// <summary>检索测试(记录匹配日志)</summary>
116+
[HttpPost("search/{id}")]
117+
public async Task<ApiResult> Search(long id, [FromBody] KbSearchRequest req)
118+
{
119+
try
120+
{
121+
var results = await _kbService.SearchAsync(id, req.Query ?? "", req.TopK > 0 ? req.TopK : 5);
122+
return ApiResult.Success(results);
123+
}
124+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
125+
}
126+
127+
/// <summary>知识库清洗(去空/去重/超短)</summary>
128+
[HttpPost("clean/{id}")]
129+
public async Task<ApiResult> Clean(long id)
130+
{
131+
try
132+
{
133+
return ApiResult.Success(await _kbService.CleanAsync(id));
134+
}
135+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
136+
}
137+
138+
/// <summary>重新向量化(vector 模式)</summary>
139+
[HttpPost("revectorize/{id}")]
140+
public async Task<ApiResult> Revectorize(long id)
141+
{
142+
try
143+
{
144+
return ApiResult.Success(await _kbService.RevectorizeAsync(id));
145+
}
146+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
147+
}
148+
149+
/// <summary>匹配记录查询</summary>
150+
[HttpGet("match-logs/{id}")]
151+
public async Task<ApiResult> MatchLogs(long id)
152+
=> ApiResult.Success(await _db.KnowledgeMatchLogs
153+
.Where(m => m.KbId == id && m.Deleted == 0)
154+
.OrderByDescending(m => m.Id)
155+
.Take(200)
156+
.Select(m => new { m.Id, m.Query, m.Score, m.CreatedAt, m.ResultsJson })
157+
.ToListAsync());
158+
}
159+
160+
public class KbChunkRequest
161+
{
162+
public string? Content { get; set; }
163+
}
164+
165+
public class KbSearchRequest
166+
{
167+
public string? Query { get; set; }
168+
public int TopK { get; set; } = 5;
169+
}

‎Juggle.Api/Program.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ await ctx.Response.WriteAsync(
166166
builder.Services.AddScoped<ReportExecutionService>(); // 报表数据执行 + 渲染
167167
builder.Services.AddScoped<ITenantAccessor, TenantAccessor>(); // 多租户上下文
168168
builder.Services.AddScoped<AiService>(); // AiService
169+
builder.Services.AddScoped<KnowledgeService>(); // 知识库(文档解析/切片/检索)
169170

170171
// Swagger
171172
builder.Services.AddEndpointsApiExplorer();
@@ -255,6 +256,11 @@ await ctx.Response.WriteAsync(
255256
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_ai_assistant ADD COLUMN icon TEXT DEFAULT NULL;"); } catch { }
256257
// AI 助手对话会话表
257258
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+
// 知识库 4 表
260+
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 { }
261+
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 { }
262+
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_kb_chunk(id INTEGER PRIMARY KEY AUTOINCREMENT, kb_id INTEGER, doc_id INTEGER, content TEXT, seq_no INTEGER DEFAULT 0, vector_json TEXT, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
263+
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_kb_match_log(id INTEGER PRIMARY KEY AUTOINCREMENT, kb_id INTEGER, query TEXT, results_json TEXT, score REAL DEFAULT 0, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
258264
// 补建参数位置字段
259265
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_parameter ADD COLUMN param_position TEXT DEFAULT NULL;"); } catch { }
260266
}

‎Juggle.Application/Juggle.Application.csproj‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
<ItemGroup>
99
<PackageReference Include="ClosedXML" Version="0.104.2" />
10+
<PackageReference Include="DocumentFormat.OpenXml" Version="3.1.1" />
1011
<PackageReference Include="QuestPDF" Version="2026.5.0" />
1112
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
1213
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" />
14+
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
1315
</ItemGroup>
1416

1517
<PropertyGroup>

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ public class FlowExecutionService
1919
private readonly IHttpClientFactory _httpClientFactory;
2020
private readonly ITenantAccessor? _tenant;
2121
private readonly AiService _aiService;
22+
private readonly KnowledgeService _kbService;
2223

23-
public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFactory, AiService aiService, ITenantAccessor? tenant = null)
24+
public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFactory, AiService aiService,
25+
KnowledgeService kbService, ITenantAccessor? tenant = null)
2426
{
2527
_db = db;
2628
_httpClientFactory = httpClientFactory;
2729
_tenant = tenant;
2830
_aiService = aiService;
31+
_kbService = kbService;
2932
}
3033

3134
/// <summary>读取 Redis 连接配置(系统配置表),未配置返回 null。</summary>
@@ -42,12 +45,13 @@ public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFac
4245
return $"{host}:{port},password={password},defaultDatabase={dbIndex},abortConnect=false,connectTimeout=5000";
4346
}
4447

45-
/// <summary>构建引擎(注入大模型对话函数与 Redis 连接,供 AI/Redis 节点使用)。</summary>
48+
/// <summary>构建引擎(注入大模型对话函数、Redis 连接与知识库检索,供 AI/Redis/KB_SEARCH 节点使用)。</summary>
4649
private async Task<FlowEngine> BuildEngineAsync(Dictionary<string, DataSourceInfo> dsInfos,
4750
Dictionary<string, string?> staticVars, Func<string, Task<string?>> flowContentLoader)
4851
=> new FlowEngine(_httpClientFactory, dsInfos, staticVars, flowContentLoader,
4952
aiChatFunc: (systemPrompt, userInput) => _aiService.ChatAsync(systemPrompt, userInput),
50-
redisConnStr: await GetRedisConnStrAsync());
53+
redisConnStr: await GetRedisConnStrAsync(),
54+
kbSearchFunc: (kbId, query, topK) => _kbService.SearchAsContextAsync(kbId, query, topK));
5155

5256
// ────────────────────────────────────────────────────────────────
5357
// 数据源

0 commit comments

Comments
 (0)