mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-13 21:27:19 +08:00
新增知识库管理功能,包括知识库和文档的增删改查,优化文档上传和状态管理,更新相关API接口和前端页面,添加知识库和文档的视图组件。
This commit is contained in:
@@ -170,9 +170,8 @@ jooq {
|
||||
}
|
||||
forcedTypes {
|
||||
forcedType {
|
||||
name = "varchar"
|
||||
includeExpression = ".*"
|
||||
includeTypes = "INET"
|
||||
isJsonConverter = true
|
||||
includeTypes = "(?i:JSON|JSONB)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
backend/src/main/java/com/zl/mjga/config/JacksonConfig.java
Normal file
35
backend/src/main/java/com/zl/mjga/config/JacksonConfig.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package com.zl.mjga.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import java.io.IOException;
|
||||
import org.jooq.JSON;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
|
||||
return builder ->
|
||||
builder
|
||||
.serializationInclusion(JsonInclude.Include.USE_DEFAULTS)
|
||||
.serializers(new JooqJsonSerializer());
|
||||
}
|
||||
|
||||
private static class JooqJsonSerializer extends StdSerializer<JSON> {
|
||||
public JooqJsonSerializer() {
|
||||
super(JSON.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JSON value, JsonGenerator gen, SerializerProvider serializers)
|
||||
throws IOException {
|
||||
gen.writeRawValue(value.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import com.zl.mjga.repository.LibraryRepository;
|
||||
import com.zl.mjga.service.RagService;
|
||||
import com.zl.mjga.service.UploadService;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -37,7 +39,10 @@ public class LibraryController {
|
||||
|
||||
@GetMapping("/docs")
|
||||
public List<LibraryDoc> queryLibraryDocs(@RequestParam Long libraryId) {
|
||||
return libraryDocRepository.fetchByLibId(libraryId);
|
||||
List<LibraryDoc> libraryDocs = libraryDocRepository.fetchByLibId(libraryId);
|
||||
return libraryDocs.stream().sorted(
|
||||
Comparator.comparing(LibraryDoc::getId).reversed()
|
||||
).toList();
|
||||
}
|
||||
|
||||
@GetMapping("/segments")
|
||||
@@ -66,22 +71,19 @@ public class LibraryController {
|
||||
|
||||
@PutMapping("/doc")
|
||||
public void updateLibraryDoc(@RequestBody @Valid DocUpdateDto docUpdateDto) {
|
||||
LibraryDoc libraryDoc = new LibraryDoc();
|
||||
libraryDoc.setId(docUpdateDto.id());
|
||||
libraryDoc.setEnable(docUpdateDto.enable());
|
||||
libraryDocRepository.merge(libraryDoc);
|
||||
LibraryDoc exist = libraryDocRepository.fetchOneById(docUpdateDto.id());
|
||||
exist.setEnable(docUpdateDto.enable());
|
||||
libraryDocRepository.merge(exist);
|
||||
}
|
||||
|
||||
@PostMapping(
|
||||
value = "/doc/upload",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(value = "/doc/upload", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String uploadLibraryDoc(
|
||||
@RequestPart("libraryId") Long libraryId, @RequestPart("file") MultipartFile multipartFile)
|
||||
@RequestPart("libraryId") String libraryId, @RequestPart("file") MultipartFile multipartFile)
|
||||
throws Exception {
|
||||
String objectName = uploadService.uploadLibraryDoc(multipartFile);
|
||||
Long libraryDocId =
|
||||
ragService.createLibraryDocBy(libraryId, objectName, multipartFile.getOriginalFilename());
|
||||
ragService.createLibraryDocBy(
|
||||
Long.valueOf(libraryId), objectName, multipartFile.getOriginalFilename());
|
||||
ragService.embeddingAndCreateDocSegment(libraryDocId, objectName);
|
||||
return objectName;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.zl.mjga.dto.knowledge;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record DocUpdateDto(@NotNull Long id, @NotEmpty Boolean enable) {}
|
||||
public record DocUpdateDto(@NotNull Long id, @NotNull Long libId, @NotNull Boolean enable) {}
|
||||
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@@ -186,3 +186,5 @@ compose.yaml
|
||||
Dockerfile
|
||||
Caddyfile
|
||||
start.sh
|
||||
|
||||
.cursor
|
||||
|
||||
@@ -44,6 +44,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/doc": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "updateLibraryDoc",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DocUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "deleteLibraryDoc",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "libraryDocId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ai/llm": {
|
||||
"put": {
|
||||
"tags": [
|
||||
@@ -192,6 +237,93 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/library": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "upsertLibrary",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LibraryUpsertDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "deleteLibrary",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "libraryId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/doc/upload": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "uploadLibraryDoc",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"file",
|
||||
"libraryId"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"libraryId": {
|
||||
"type": "string"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/iam/user": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -963,6 +1095,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/segments": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "queryLibraryDocSegments",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "libraryDocId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LibraryDocSegment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/libraries": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "queryLibraries",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Library"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/docs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"library-controller"
|
||||
],
|
||||
"operationId": "queryLibraryDocs",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "libraryId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LibraryDoc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/iam/users": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1354,6 +1577,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocUpdateDto": {
|
||||
"required": [
|
||||
"enable",
|
||||
"id"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"enable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LlmVm": {
|
||||
"required": [
|
||||
"apiKey",
|
||||
@@ -1422,6 +1661,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LibraryUpsertDto": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserUpsertDto": {
|
||||
"required": [
|
||||
"enable",
|
||||
@@ -1789,6 +2046,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LibraryDocSegment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"docId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"embeddingId": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokenUsage": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Library": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"JSON": {
|
||||
"type": "object"
|
||||
},
|
||||
"LibraryDoc": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"libId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"identify": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/JSON"
|
||||
},
|
||||
"enable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"SUCCESS",
|
||||
"INDEXING"
|
||||
]
|
||||
},
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updateTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserQueryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
319
frontend/src/api/types/schema.d.ts
vendored
319
frontend/src/api/types/schema.d.ts
vendored
@@ -20,6 +20,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/knowledge/doc": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put: operations["updateLibraryDoc"];
|
||||
post?: never;
|
||||
delete: operations["deleteLibraryDoc"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/ai/llm": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -100,6 +116,38 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/knowledge/library": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["upsertLibrary"];
|
||||
delete: operations["deleteLibrary"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/knowledge/doc/upload": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["uploadLibraryDoc"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/iam/user": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -484,6 +532,54 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/knowledge/segments": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["queryLibraryDocSegments"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/knowledge/libraries": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["queryLibraries"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/knowledge/docs": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["queryLibraryDocs"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/iam/users": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -684,6 +780,11 @@ export interface components {
|
||||
name: string;
|
||||
group: string;
|
||||
};
|
||||
DocUpdateDto: {
|
||||
/** Format: int64 */
|
||||
id: number;
|
||||
enable: boolean;
|
||||
};
|
||||
LlmVm: {
|
||||
/** Format: int64 */
|
||||
id: number;
|
||||
@@ -705,6 +806,12 @@ export interface components {
|
||||
id?: number;
|
||||
name?: string;
|
||||
};
|
||||
LibraryUpsertDto: {
|
||||
/** Format: int64 */
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
UserUpsertDto: {
|
||||
/** Format: int64 */
|
||||
id?: number;
|
||||
@@ -829,6 +936,42 @@ export interface components {
|
||||
name: string;
|
||||
isBound?: boolean;
|
||||
};
|
||||
LibraryDocSegment: {
|
||||
/** Format: int64 */
|
||||
id?: number;
|
||||
/** Format: int64 */
|
||||
docId?: number;
|
||||
embeddingId?: string;
|
||||
content?: string;
|
||||
/** Format: int32 */
|
||||
tokenUsage?: number;
|
||||
};
|
||||
Library: {
|
||||
/** Format: int64 */
|
||||
id?: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
/** Format: date-time */
|
||||
createTime?: string;
|
||||
};
|
||||
JSON: Record<string, never>;
|
||||
LibraryDoc: {
|
||||
/** Format: int64 */
|
||||
id?: number;
|
||||
/** Format: int64 */
|
||||
libId?: number;
|
||||
name?: string;
|
||||
identify?: string;
|
||||
path?: string;
|
||||
meta?: components["schemas"]["JSON"];
|
||||
enable?: boolean;
|
||||
/** @enum {string} */
|
||||
status?: "SUCCESS" | "INDEXING";
|
||||
/** Format: date-time */
|
||||
createTime?: string;
|
||||
/** Format: date-time */
|
||||
updateTime?: string;
|
||||
};
|
||||
UserQueryDto: {
|
||||
username?: string;
|
||||
/** Format: date-time */
|
||||
@@ -973,6 +1116,48 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
updateLibraryDoc: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DocUpdateDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
deleteLibraryDoc: {
|
||||
parameters: {
|
||||
query: {
|
||||
libraryDocId: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
updateLlm: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1105,6 +1290,76 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
upsertLibrary: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["LibraryUpsertDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
deleteLibrary: {
|
||||
parameters: {
|
||||
query: {
|
||||
libraryId: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
uploadLibraryDoc: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": {
|
||||
libraryId: string;
|
||||
/** Format: binary */
|
||||
file: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/plain": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
queryUserWithRolePermission: {
|
||||
parameters: {
|
||||
query: {
|
||||
@@ -1782,6 +2037,70 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
queryLibraryDocSegments: {
|
||||
parameters: {
|
||||
query: {
|
||||
libraryDocId: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["LibraryDocSegment"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
queryLibraries: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["Library"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
queryLibraryDocs: {
|
||||
parameters: {
|
||||
query: {
|
||||
libraryId: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["LibraryDoc"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
queryUsers: {
|
||||
parameters: {
|
||||
query: {
|
||||
|
||||
@@ -21,25 +21,25 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
imageSrc: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
imageAlt: {
|
||||
type: String,
|
||||
default: 'promotion'
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '官方教程'
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageSrc: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageAlt: {
|
||||
type: String,
|
||||
default: "promotion",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "官方教程",
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
7
frontend/src/components/icons/KnowledgeIcon.vue
Normal file
7
frontend/src/components/icons/KnowledgeIcon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-icon">
|
||||
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
||||
<path d="m9 10 2 2 4-4" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -11,3 +11,4 @@ export { default as RoleIcon } from "./RoleIcon.vue";
|
||||
export { default as SettingsIcon } from "./SettingsIcon.vue";
|
||||
export { default as UsersIcon } from "./UsersIcon.vue";
|
||||
export { default as PermissionIcon } from "./PermissionIcon.vue";
|
||||
export { default as KnowledgeIcon } from "./KnowledgeIcon.vue";
|
||||
|
||||
@@ -35,6 +35,7 @@ import { RouterLink, useRoute } from "vue-router";
|
||||
|
||||
import {
|
||||
DepartmentIcon,
|
||||
KnowledgeIcon,
|
||||
LlmConfigIcon,
|
||||
PermissionIcon,
|
||||
PositionIcon,
|
||||
@@ -113,6 +114,11 @@ const menuItems = [
|
||||
path: Routes.LLMCONFIGVIEW.fullPath(),
|
||||
icon: LlmConfigIcon,
|
||||
},
|
||||
{
|
||||
title: "知识库管理",
|
||||
path: Routes.KNOWLEDGEVIEW.fullPath(),
|
||||
icon: KnowledgeIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
93
frontend/src/components/modals/LibraryFormDialog.vue
Normal file
93
frontend/src/components/modals/LibraryFormDialog.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<BaseDialog :id="id" title="知识库管理" size="md" :closeModal="closeModal">
|
||||
<!-- Modal body -->
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="grid gap-4 mb-4 grid-cols-1">
|
||||
<div class="col-span-full">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">知识库名称</label>
|
||||
<input type="text" id="name" v-model="formData.name"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
required />
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label for="description" class="block mb-2 text-sm font-medium text-gray-900">知识库描述</label>
|
||||
<textarea id="description" v-model="formData.description" rows="3"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" @click="handleSubmit"
|
||||
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import type { Library } from "@/types/KnowledgeTypes";
|
||||
import type { LibraryUpsertModel } from "@/types/KnowledgeTypes";
|
||||
import { ref, watch } from "vue";
|
||||
import { z } from "zod";
|
||||
import BaseDialog from "./BaseDialog.vue";
|
||||
|
||||
const alertStore = useAlertStore();
|
||||
const { library, closeModal, onSubmit, id } = defineProps<{
|
||||
library?: Library;
|
||||
closeModal: () => void;
|
||||
onSubmit: (data: LibraryUpsertModel) => Promise<void>;
|
||||
id: string;
|
||||
}>();
|
||||
|
||||
const formData = ref<LibraryUpsertModel>({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const updateFormData = (newLibrary: typeof library) => {
|
||||
if (!newLibrary) {
|
||||
formData.value = {
|
||||
name: "",
|
||||
description: "",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
formData.value = {
|
||||
id: newLibrary.id,
|
||||
name: newLibrary.name ?? "",
|
||||
description: newLibrary.description ?? "",
|
||||
};
|
||||
};
|
||||
|
||||
watch(() => library, updateFormData, { immediate: true });
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "知识库名称不能为空"),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = schema.parse(formData.value);
|
||||
await onSubmit({
|
||||
...formData.value,
|
||||
name: validatedData.name,
|
||||
description: validatedData.description,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
alertStore.showAlert({
|
||||
type: "error",
|
||||
message: error.errors[0].message,
|
||||
});
|
||||
} else {
|
||||
console.error("表单提交错误:", error);
|
||||
alertStore.showAlert({
|
||||
type: "error",
|
||||
message: "表单提交失败,请重试",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -116,10 +116,9 @@ const handleFileChange = (event: Event) => {
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
} finally {
|
||||
(event.target as HTMLInputElement).value = "";
|
||||
uploadLoading.value = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
51
frontend/src/composables/knowledge/useKnowledgeQuery.ts
Normal file
51
frontend/src/composables/knowledge/useKnowledgeQuery.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import client from "@/api/client";
|
||||
import type {
|
||||
DocQueryParams,
|
||||
Library,
|
||||
LibraryDoc,
|
||||
LibraryDocSegment,
|
||||
SegmentQueryParams,
|
||||
} from "@/types/KnowledgeTypes";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useKnowledgeQuery = () => {
|
||||
const libraries = ref<Library[]>([]);
|
||||
const docs = ref<LibraryDoc[]>([]);
|
||||
const segments = ref<LibraryDocSegment[]>([]);
|
||||
|
||||
const fetchLibraries = async () => {
|
||||
const { data } = await client.GET("/knowledge/libraries", {});
|
||||
libraries.value = data || [];
|
||||
};
|
||||
|
||||
const fetchLibraryDocs = async (params: DocQueryParams) => {
|
||||
const { data } = await client.GET("/knowledge/docs", {
|
||||
params: {
|
||||
query: {
|
||||
libraryId: params.libraryId,
|
||||
},
|
||||
},
|
||||
});
|
||||
docs.value = data || [];
|
||||
};
|
||||
|
||||
const fetchDocSegments = async (params: SegmentQueryParams) => {
|
||||
const { data } = await client.GET("/knowledge/segments", {
|
||||
params: {
|
||||
query: {
|
||||
libraryDocId: params.libraryDocId,
|
||||
},
|
||||
},
|
||||
});
|
||||
segments.value = data || [];
|
||||
};
|
||||
|
||||
return {
|
||||
libraries,
|
||||
fetchLibraries,
|
||||
docs,
|
||||
fetchLibraryDocs,
|
||||
segments,
|
||||
fetchDocSegments,
|
||||
};
|
||||
};
|
||||
72
frontend/src/composables/knowledge/useKnowledgeUpsert.ts
Normal file
72
frontend/src/composables/knowledge/useKnowledgeUpsert.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import client from "@/api/client";
|
||||
import type {
|
||||
DocUpdateModel,
|
||||
LibraryUpsertModel,
|
||||
} from "@/types/KnowledgeTypes";
|
||||
|
||||
export const useKnowledgeUpsert = () => {
|
||||
const upsertLibrary = async (library: LibraryUpsertModel) => {
|
||||
await client.POST("/knowledge/library", {
|
||||
body: {
|
||||
id: library.id,
|
||||
name: library.name,
|
||||
description: library.description,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteLibrary = async (libraryId: number) => {
|
||||
await client.DELETE("/knowledge/library", {
|
||||
params: {
|
||||
query: {
|
||||
libraryId,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const uploadDoc = async (libraryId: number, file: File) => {
|
||||
await client.POST("/knowledge/doc/upload", {
|
||||
body: {
|
||||
libraryId: libraryId.toString(),
|
||||
file: file as unknown as string,
|
||||
},
|
||||
bodySerializer: (body) => {
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(body!)) {
|
||||
formData.set(key, value as unknown as string);
|
||||
}
|
||||
return formData;
|
||||
},
|
||||
parseAs: "text",
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDoc = async (libraryDocId: number) => {
|
||||
await client.DELETE("/knowledge/doc", {
|
||||
params: {
|
||||
query: {
|
||||
libraryDocId,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateDoc = async (doc: DocUpdateModel) => {
|
||||
await client.PUT("/knowledge/doc", {
|
||||
body: {
|
||||
id: doc.id,
|
||||
libId: doc.libId,
|
||||
enable: doc.enable,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
upsertLibrary,
|
||||
deleteLibrary,
|
||||
uploadDoc,
|
||||
deleteDoc,
|
||||
updateDoc,
|
||||
};
|
||||
};
|
||||
@@ -133,9 +133,9 @@ export const UserRoutes = {
|
||||
export const AiRoutes = {
|
||||
LLMCONFIGVIEW: {
|
||||
path: "llm/config",
|
||||
name: "llm/config",
|
||||
name: "llm-config",
|
||||
fullPath: () => `${BaseRoutes.DASHBOARD.path}/llm/config`,
|
||||
withParams: () => ({ name: "llm/config" }),
|
||||
withParams: () => ({ name: "llm-config" }),
|
||||
},
|
||||
SCHEDULERVIEW: {
|
||||
path: "scheduler",
|
||||
@@ -143,6 +143,37 @@ export const AiRoutes = {
|
||||
fullPath: () => `${BaseRoutes.DASHBOARD.path}/scheduler`,
|
||||
withParams: () => ({ name: "scheduler" }),
|
||||
},
|
||||
KNOWLEDGEVIEW: {
|
||||
path: "knowledge",
|
||||
name: "knowledge",
|
||||
fullPath: () => `${BaseRoutes.DASHBOARD.path}/knowledge`,
|
||||
withParams: () => ({ name: "knowledge" }),
|
||||
},
|
||||
KNOWLEDGEDOCVIEW: {
|
||||
path: "knowledge/:libraryId",
|
||||
name: "knowledge-docs",
|
||||
fullPath: () => `${BaseRoutes.DASHBOARD.path}/knowledge/:libraryId`,
|
||||
withParams: <T extends { libraryId: string | number }>(params: T) => ({
|
||||
name: "knowledge-docs",
|
||||
params: { libraryId: params.libraryId.toString() },
|
||||
}),
|
||||
},
|
||||
KNOWLEDGESEGMENTSVIEW: {
|
||||
path: "knowledge/:libraryId/:docId",
|
||||
name: "knowledge-segments",
|
||||
fullPath: () => `${BaseRoutes.DASHBOARD.path}/knowledge/:libraryId/:docId`,
|
||||
withParams: <
|
||||
T extends { libraryId: string | number; docId: string | number },
|
||||
>(
|
||||
params: T,
|
||||
) => ({
|
||||
name: "knowledge-segments",
|
||||
params: {
|
||||
libraryId: params.libraryId.toString(),
|
||||
docId: params.docId.toString(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const Routes = {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const authGuard: NavigationGuard = (to) => {
|
||||
};
|
||||
}
|
||||
if (to.path === Routes.LOGIN.path && userStore.user) {
|
||||
return { path: `${Routes.DASHBOARD.path}/${Routes.USERVIEW.path}` };
|
||||
return { path: Routes.USERVIEW.fullPath() };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: Routes.HOME.path,
|
||||
name: Routes.HOME.name,
|
||||
redirect: {
|
||||
path: `${Routes.DASHBOARD.path}/${Routes.USERVIEW.path}`,
|
||||
path: Routes.USERVIEW.fullPath(),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -27,7 +27,7 @@ const router = createRouter({
|
||||
|
||||
router.onError((err) => {
|
||||
console.error("router err:", err);
|
||||
router.push(Routes.USERVIEW.name);
|
||||
router.push(Routes.USERVIEW.fullPath());
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,30 @@ const aiRoutes: RouteRecordRaw[] = [
|
||||
hasPermission: EPermission.READ_LLM_CONFIG_PERMISSION,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: Routes.KNOWLEDGEVIEW.path,
|
||||
name: Routes.KNOWLEDGEVIEW.name,
|
||||
component: () => import("@/views/KnowledgeManagementPage.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: Routes.KNOWLEDGEDOCVIEW.path,
|
||||
name: Routes.KNOWLEDGEDOCVIEW.name,
|
||||
component: () => import("@/views/KnowledgeDocManagementPage.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: Routes.KNOWLEDGESEGMENTSVIEW.path,
|
||||
name: Routes.KNOWLEDGESEGMENTSVIEW.name,
|
||||
component: () => import("@/views/KnowledgeSegmentsPage.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default aiRoutes;
|
||||
|
||||
@@ -12,6 +12,8 @@ const dashboardRoutes: RouteRecordRaw = {
|
||||
requiresAuth: true,
|
||||
},
|
||||
children: [
|
||||
...userManagementRoutes,
|
||||
...aiRoutes,
|
||||
{
|
||||
path: Routes.OVERVIEW.path,
|
||||
name: Routes.OVERVIEW.name,
|
||||
@@ -28,8 +30,6 @@ const dashboardRoutes: RouteRecordRaw = {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
...userManagementRoutes,
|
||||
...aiRoutes,
|
||||
{
|
||||
path: Routes.NOTFOUND.path,
|
||||
name: Routes.NOTFOUND.name,
|
||||
|
||||
35
frontend/src/types/KnowledgeTypes.ts
Normal file
35
frontend/src/types/KnowledgeTypes.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { components } from "@/api/types/schema";
|
||||
|
||||
export type Library = components["schemas"]["Library"];
|
||||
export type LibraryDoc = components["schemas"]["LibraryDoc"];
|
||||
export type LibraryDocSegment = components["schemas"]["LibraryDocSegment"];
|
||||
|
||||
export interface LibraryUpsertModel {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface DocUpdateModel {
|
||||
id: number;
|
||||
libId: number;
|
||||
enable: boolean;
|
||||
}
|
||||
|
||||
export interface LibraryQueryParams {
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface DocQueryParams {
|
||||
libraryId: number;
|
||||
}
|
||||
|
||||
export interface SegmentQueryParams {
|
||||
libraryDocId: number;
|
||||
}
|
||||
|
||||
export enum DocStatus {
|
||||
SUCCESS = "SUCCESS",
|
||||
INDEXING = "INDEXING",
|
||||
}
|
||||
238
frontend/src/views/KnowledgeDocManagementPage.vue
Normal file
238
frontend/src/views/KnowledgeDocManagementPage.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||
<Breadcrumbs :names="['知识库管理', '文档管理']" :routes="[Routes.KNOWLEDGEVIEW.fullPath()]" />
|
||||
<div class="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">{{ currentLibrary?.name || '知识库' }} - 文档管理</h1>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div v-for="doc in docs" :key="doc.id"
|
||||
class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ doc.name }}</h5>
|
||||
<div class="flex items-center mb-2">
|
||||
<span :class="`inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${
|
||||
doc.status === DocStatus.SUCCESS ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
} mr-2`">
|
||||
{{ doc.status === DocStatus.SUCCESS ? '解析完成' : '解析中' }}
|
||||
</span>
|
||||
<span :class="`inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${
|
||||
doc.enable ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'
|
||||
}`">
|
||||
{{ doc.enable ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<label class="inline-flex items-center mb-5"
|
||||
:class="!doc.enable ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'">
|
||||
<input type="checkbox" class="sr-only peer" :checked="doc.enable" @change="handleToggleDocStatus(doc)">
|
||||
<div
|
||||
class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="text-sm text-gray-600 mb-3">
|
||||
<div class="truncate">{{ doc.path || '无' }}</div>
|
||||
</div> -->
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500">
|
||||
上传时间: {{ formatDate(doc.createTime) }}
|
||||
</span>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="navigateToDocSegments(doc)" :class="
|
||||
['text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-content',
|
||||
doc.status !== DocStatus.SUCCESS ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer']"
|
||||
:disabled="doc.status !== DocStatus.SUCCESS">
|
||||
查看内容
|
||||
</button>
|
||||
<button @click="handleDeleteDoc(doc)"
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center py-10">
|
||||
<div v-if="docs.length === 0" class="text-gray-500 text-lg mb-4">暂无文档</div>
|
||||
<div>
|
||||
<input ref="fileInputRef" class="hidden" id="doc_file_input" type="file" @change="handleFileChange">
|
||||
<TableButton variant="primary" size="md" @click="triggerFileInput">
|
||||
<template #icon>
|
||||
<PlusIcon class="w-4 h-4" />
|
||||
</template>
|
||||
上传文档
|
||||
</TableButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<ConfirmationDialog :id="'doc-delete-modal'" :title="`确定删除文档 '${selectedDoc?.name || ''}' 吗?`"
|
||||
content="删除后将无法恢复,且其中的所有分段内容也将被删除。" :closeModal="() => {
|
||||
docDeleteModal?.hide();
|
||||
}" :onSubmit="handleDocDeleteSubmit" />
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||
import { onMounted, ref, watchEffect } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { PlusIcon } from "@/components/icons";
|
||||
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
||||
import TableButton from "@/components/tables/TableButton.vue";
|
||||
|
||||
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import { Routes } from "@/router/constants";
|
||||
|
||||
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
|
||||
import { DocStatus } from "@/types/KnowledgeTypes";
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const libraryId = ref<number>(
|
||||
Number.parseInt(route.params.libraryId as string, 10),
|
||||
);
|
||||
|
||||
// 文件输入引用
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 获取知识库信息
|
||||
const { libraries, fetchLibraries } = useKnowledgeQuery();
|
||||
const currentLibrary = ref<Library | undefined>();
|
||||
|
||||
// 获取文档列表
|
||||
const { docs, fetchLibraryDocs } = useKnowledgeQuery();
|
||||
const { uploadDoc, deleteDoc, updateDoc } = useKnowledgeUpsert();
|
||||
|
||||
// 模态框引用
|
||||
const docDeleteModal = ref<ModalInterface>();
|
||||
|
||||
// 选中的文档
|
||||
const selectedDoc = ref<LibraryDoc | undefined>();
|
||||
|
||||
// 提示store
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "未知";
|
||||
return dayjs(dateString).format("YYYY-MM-DD HH:mm");
|
||||
};
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.value?.click();
|
||||
};
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileChange = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
await uploadDoc(libraryId.value, file);
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "文档上传成功",
|
||||
});
|
||||
|
||||
// 清空文件选择框
|
||||
(event.target as HTMLInputElement).value = "";
|
||||
|
||||
// 刷新文档列表
|
||||
await fetchLibraryDocs({ libraryId: libraryId.value });
|
||||
} finally {
|
||||
// 清空文件选择框
|
||||
(event.target as HTMLInputElement).value = "";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除文档
|
||||
const handleDeleteDoc = (doc: LibraryDoc) => {
|
||||
selectedDoc.value = doc;
|
||||
docDeleteModal.value?.show();
|
||||
};
|
||||
|
||||
// 处理文档删除确认
|
||||
const handleDocDeleteSubmit = async () => {
|
||||
if (!selectedDoc.value?.id) return;
|
||||
|
||||
await deleteDoc(selectedDoc.value.id);
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "文档删除成功",
|
||||
});
|
||||
docDeleteModal.value?.hide();
|
||||
await fetchLibraryDocs({ libraryId: libraryId.value });
|
||||
};
|
||||
|
||||
// 处理切换文档状态
|
||||
const handleToggleDocStatus = async (doc: LibraryDoc) => {
|
||||
try {
|
||||
doc.enable = !doc.enable;
|
||||
await updateDoc({
|
||||
id: doc.id!,
|
||||
libId: doc.libId!,
|
||||
enable: doc.enable,
|
||||
});
|
||||
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "操作成功",
|
||||
});
|
||||
} finally {
|
||||
await fetchLibraryDocs({ libraryId: libraryId.value });
|
||||
}
|
||||
};
|
||||
|
||||
// 导航到文档分段页面
|
||||
const navigateToDocSegments = (doc: LibraryDoc) => {
|
||||
router.push(
|
||||
Routes.KNOWLEDGESEGMENTSVIEW.withParams({
|
||||
libraryId: libraryId.value,
|
||||
docId: doc.id!,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
initFlowbite();
|
||||
|
||||
// 初始化模态框
|
||||
const docDeleteElement = document.getElementById("doc-delete-modal");
|
||||
if (docDeleteElement) {
|
||||
docDeleteModal.value = new Modal(docDeleteElement);
|
||||
}
|
||||
|
||||
// 获取知识库列表和文档列表
|
||||
await fetchLibraries();
|
||||
await fetchLibraryDocs({ libraryId: libraryId.value });
|
||||
});
|
||||
|
||||
// 监听知识库列表变化,找到当前知识库
|
||||
watchEffect(() => {
|
||||
if (libraries.value && libraries.value.length > 0) {
|
||||
currentLibrary.value = libraries.value.find(
|
||||
(lib) => lib.id === libraryId.value,
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
177
frontend/src/views/KnowledgeManagementPage.vue
Normal file
177
frontend/src/views/KnowledgeManagementPage.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||
<Breadcrumbs :names="['知识库管理']" />
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">知识库管理</h1>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div v-for="library in libraries" :key="library.id"
|
||||
class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ library.name }}</h5>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="handleEditLibrary(library)" class="text-gray-500 hover:text-blue-700 focus:outline-none">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="handleDeleteLibrary(library)" class="text-gray-500 hover:text-red-700 focus:outline-none">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-3 line-clamp-2">
|
||||
{{ library.description || '暂无描述' }}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500">
|
||||
创建时间: {{ formatDate(library.createTime) }}
|
||||
</span>
|
||||
<button @click="navigateToLibraryDocs(library)"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5">
|
||||
查看知识库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="libraries.length === 0" class="flex flex-col items-center justify-center py-10">
|
||||
<div class="text-gray-500 text-lg mb-4">暂无知识库</div>
|
||||
<div>
|
||||
<button @click="handleCreateLibraryClick"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 focus:outline-none">
|
||||
创建知识库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识库表单对话框 -->
|
||||
<LibraryFormDialog :id="'library-form-modal'" :library="selectedLibrary" :closeModal="() => {
|
||||
libraryFormModal?.hide();
|
||||
}" :onSubmit="handleLibraryFormSubmit" />
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<ConfirmationDialog :id="'library-delete-modal'" :title="`确定删除知识库 '${selectedLibrary?.name || ''}' 吗?`"
|
||||
content="删除后将无法恢复,且其中的所有文档也将被删除。" :closeModal="() => {
|
||||
libraryDeleteModal?.hide();
|
||||
}" :onSubmit="handleLibraryDeleteSubmit" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
||||
import LibraryFormDialog from "@/components/modals/LibraryFormDialog.vue";
|
||||
|
||||
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import { Routes } from "@/router/constants";
|
||||
|
||||
import type { Library, LibraryUpsertModel } from "@/types/KnowledgeTypes";
|
||||
|
||||
// 获取知识库列表
|
||||
const { libraries, fetchLibraries } = useKnowledgeQuery();
|
||||
// 知识库操作
|
||||
const { upsertLibrary, deleteLibrary } = useKnowledgeUpsert();
|
||||
|
||||
// 模态框引用
|
||||
const libraryFormModal = ref<ModalInterface>();
|
||||
const libraryDeleteModal = ref<ModalInterface>();
|
||||
|
||||
// 选中的知识库
|
||||
const selectedLibrary = ref<Library | undefined>();
|
||||
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "未知";
|
||||
return dayjs(dateString).format("YYYY-MM-DD HH:mm");
|
||||
};
|
||||
|
||||
// 处理创建知识库点击
|
||||
const handleCreateLibraryClick = () => {
|
||||
selectedLibrary.value = undefined;
|
||||
libraryFormModal.value?.show();
|
||||
};
|
||||
|
||||
// 处理编辑知识库
|
||||
const handleEditLibrary = (library: Library) => {
|
||||
selectedLibrary.value = library;
|
||||
libraryFormModal.value?.show();
|
||||
};
|
||||
|
||||
// 处理删除知识库
|
||||
const handleDeleteLibrary = (library: Library) => {
|
||||
selectedLibrary.value = library;
|
||||
libraryDeleteModal.value?.show();
|
||||
};
|
||||
|
||||
// 处理知识库表单提交
|
||||
const handleLibraryFormSubmit = async (data: LibraryUpsertModel) => {
|
||||
await upsertLibrary(data);
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: data.id ? "知识库更新成功" : "知识库创建成功",
|
||||
});
|
||||
libraryFormModal.value?.hide();
|
||||
await fetchLibraries();
|
||||
};
|
||||
|
||||
// 处理知识库删除确认
|
||||
const handleLibraryDeleteSubmit = async () => {
|
||||
if (!selectedLibrary.value?.id) return;
|
||||
|
||||
await deleteLibrary(selectedLibrary.value.id);
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "知识库删除成功",
|
||||
});
|
||||
libraryDeleteModal.value?.hide();
|
||||
await fetchLibraries();
|
||||
};
|
||||
|
||||
// 导航到知识库文档页面
|
||||
const navigateToLibraryDocs = (library: Library) => {
|
||||
if (!library.id) return;
|
||||
router.push(Routes.KNOWLEDGEDOCVIEW.withParams({ libraryId: library.id }));
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
initFlowbite();
|
||||
|
||||
// 初始化模态框
|
||||
const libraryFormElement = document.getElementById("library-form-modal");
|
||||
if (libraryFormElement) {
|
||||
libraryFormModal.value = new Modal(libraryFormElement);
|
||||
}
|
||||
|
||||
const libraryDeleteElement = document.getElementById("library-delete-modal");
|
||||
if (libraryDeleteElement) {
|
||||
libraryDeleteModal.value = new Modal(libraryDeleteElement);
|
||||
}
|
||||
|
||||
// 获取知识库列表
|
||||
await fetchLibraries();
|
||||
});
|
||||
</script>
|
||||
97
frontend/src/views/KnowledgeSegmentsPage.vue
Normal file
97
frontend/src/views/KnowledgeSegmentsPage.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||
<Breadcrumbs :names="['知识库管理', '文档管理', '内容管理']"
|
||||
:routes="[Routes.KNOWLEDGEVIEW.fullPath(), Routes.KNOWLEDGEDOCVIEW.withParams({ libraryId: libraryId })]" />
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-2">{{ currentDoc?.name || '文档' }} - 分段内容</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
共 {{ segments.length }} 个分段
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 分段列表 -->
|
||||
<div v-if="segments.length === 0" class="flex flex-col items-center justify-center py-10">
|
||||
<div class="text-gray-500 text-lg">暂无分段内容</div>
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="(segment, index) in segments" :key="segment.id"
|
||||
class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h5 class="text-lg font-semibold text-gray-900">分段 #{{ index + 1 }}</h5>
|
||||
<div class="text-xs text-gray-500">
|
||||
ID: {{ segment.id }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mb-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
||||
Embedding ID: {{ segment.embeddingId || '无' }}
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
|
||||
Token 使用量: {{ segment.tokenUsage || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 pt-3 mt-3">
|
||||
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
|
||||
<pre
|
||||
class="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-60 overflow-y-auto">{{ segment.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watchEffect } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||
import { Routes } from "@/router/constants";
|
||||
|
||||
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute();
|
||||
const libraryId = ref<number>(
|
||||
Number.parseInt(route.params.libraryId as string, 10),
|
||||
);
|
||||
const docId = ref<number>(Number.parseInt(route.params.docId as string, 10));
|
||||
|
||||
// 获取知识库信息
|
||||
const { libraries, fetchLibraries } = useKnowledgeQuery();
|
||||
const currentLibrary = ref<Library | undefined>();
|
||||
|
||||
// 获取文档信息
|
||||
const { docs, fetchLibraryDocs } = useKnowledgeQuery();
|
||||
const currentDoc = ref<LibraryDoc | undefined>();
|
||||
|
||||
// 获取分段列表
|
||||
const { segments, fetchDocSegments } = useKnowledgeQuery();
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 获取知识库列表、文档列表和分段列表
|
||||
await fetchLibraries();
|
||||
await fetchLibraryDocs({ libraryId: libraryId.value });
|
||||
await fetchDocSegments({ libraryDocId: docId.value });
|
||||
});
|
||||
|
||||
// 监听知识库列表变化,找到当前知识库
|
||||
watchEffect(() => {
|
||||
if (libraries.value && libraries.value.length > 0) {
|
||||
currentLibrary.value = libraries.value.find(
|
||||
(lib) => lib.id === libraryId.value,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听文档列表变化,找到当前文档
|
||||
watchEffect(() => {
|
||||
if (docs.value && docs.value.length > 0) {
|
||||
currentDoc.value = docs.value.find((doc) => doc.id === docId.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||
<div class="mb-4 col-span-full">
|
||||
<div class="mb-4">
|
||||
<Breadcrumbs :names="['角色管理']" />
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">角色管理</h1>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user