mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-14 05:33:42 +08:00
新增 ChatDto 数据传输对象,更新聊天接口以支持知识库功能,优化聊天服务逻辑,调整前端组件以提升用户体验。
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package com.zl.mjga.config.ai;
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
|
||||||
|
|
||||||
import com.zl.mjga.component.PromptConfiguration;
|
import com.zl.mjga.component.PromptConfiguration;
|
||||||
import com.zl.mjga.service.LlmService;
|
import com.zl.mjga.service.LlmService;
|
||||||
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
||||||
@@ -72,6 +74,11 @@ public class ChatModelInitializer {
|
|||||||
.embeddingModel(zhipuEmbeddingModel)
|
.embeddingModel(zhipuEmbeddingModel)
|
||||||
.minScore(0.75)
|
.minScore(0.75)
|
||||||
.maxResults(5)
|
.maxResults(5)
|
||||||
|
.dynamicFilter(
|
||||||
|
query -> {
|
||||||
|
String libraryId = (String) query.metadata().chatMemoryId();
|
||||||
|
return metadataKey("libraryId").isEqualTo(libraryId);
|
||||||
|
})
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.zl.mjga.controller;
|
|||||||
|
|
||||||
import com.zl.mjga.dto.PageRequestDto;
|
import com.zl.mjga.dto.PageRequestDto;
|
||||||
import com.zl.mjga.dto.PageResponseDto;
|
import com.zl.mjga.dto.PageResponseDto;
|
||||||
|
import com.zl.mjga.dto.ai.ChatDto;
|
||||||
import com.zl.mjga.dto.ai.LlmQueryDto;
|
import com.zl.mjga.dto.ai.LlmQueryDto;
|
||||||
import com.zl.mjga.dto.ai.LlmVm;
|
import com.zl.mjga.dto.ai.LlmVm;
|
||||||
import com.zl.mjga.exception.BusinessException;
|
import com.zl.mjga.exception.BusinessException;
|
||||||
@@ -72,9 +73,9 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
public Flux<String> chat(Principal principal, @RequestBody String userMessage) {
|
public Flux<String> chat(Principal principal, @RequestBody ChatDto chatDto) {
|
||||||
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
|
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||||
TokenStream chat = aiChatService.chatPrecedenceLlmWith(principal.getName(), userMessage);
|
TokenStream chat = aiChatService.chat(principal.getName(), chatDto);
|
||||||
chat.onPartialResponse(
|
chat.onPartialResponse(
|
||||||
text ->
|
text ->
|
||||||
sink.tryEmitNext(
|
sink.tryEmitNext(
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import com.zl.mjga.repository.LibraryRepository;
|
|||||||
import com.zl.mjga.service.RagService;
|
import com.zl.mjga.service.RagService;
|
||||||
import com.zl.mjga.service.UploadService;
|
import com.zl.mjga.service.UploadService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -34,16 +33,16 @@ public class LibraryController {
|
|||||||
|
|
||||||
@GetMapping("/libraries")
|
@GetMapping("/libraries")
|
||||||
public List<Library> queryLibraries() {
|
public List<Library> queryLibraries() {
|
||||||
return libraryRepository.findAll().stream().sorted(
|
return libraryRepository.findAll().stream()
|
||||||
Comparator.comparing(Library::getId).reversed()
|
.sorted(Comparator.comparing(Library::getId).reversed())
|
||||||
).toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/docs")
|
@GetMapping("/docs")
|
||||||
public List<LibraryDoc> queryLibraryDocs(@RequestParam Long libraryId) {
|
public List<LibraryDoc> queryLibraryDocs(@RequestParam Long libraryId) {
|
||||||
return libraryDocRepository.fetchByLibId(libraryId).stream().sorted(
|
return libraryDocRepository.fetchByLibId(libraryId).stream()
|
||||||
Comparator.comparing(LibraryDoc::getId).reversed()
|
.sorted(Comparator.comparing(LibraryDoc::getId).reversed())
|
||||||
).toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/segments")
|
@GetMapping("/segments")
|
||||||
|
|||||||
7
backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java
Normal file
7
backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.zl.mjga.dto.ai;
|
||||||
|
|
||||||
|
import com.zl.mjga.model.urp.ChatMode;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record ChatDto(@NotNull ChatMode mode, Long libraryId, @NotEmpty String message) {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.zl.mjga.model.urp;
|
||||||
|
|
||||||
|
public enum ChatMode {
|
||||||
|
NORMAL,
|
||||||
|
WITH_LIBRARY
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.zl.mjga.service;
|
|||||||
|
|
||||||
import com.zl.mjga.config.ai.AiChatAssistant;
|
import com.zl.mjga.config.ai.AiChatAssistant;
|
||||||
import com.zl.mjga.config.ai.SystemToolAssistant;
|
import com.zl.mjga.config.ai.SystemToolAssistant;
|
||||||
|
import com.zl.mjga.dto.ai.ChatDto;
|
||||||
import com.zl.mjga.exception.BusinessException;
|
import com.zl.mjga.exception.BusinessException;
|
||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -39,8 +40,20 @@ public class AiChatService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) {
|
public TokenStream chat(String sessionIdentifier, ChatDto chatDto) {
|
||||||
|
return switch (chatDto.mode()) {
|
||||||
|
case NORMAL -> chatWithPrecedenceLlm(sessionIdentifier, chatDto);
|
||||||
|
case WITH_LIBRARY -> chatWithLibrary(chatDto.libraryId(), chatDto);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenStream chatWithLibrary(Long libraryId, ChatDto chatDto) {
|
||||||
|
return zhiPuChatAssistant.chat(String.valueOf(libraryId), chatDto.message());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenStream chatWithPrecedenceLlm(String sessionIdentifier, ChatDto chatDto) {
|
||||||
LlmCodeEnum code = getPrecedenceLlmCode();
|
LlmCodeEnum code = getPrecedenceLlmCode();
|
||||||
|
String userMessage = chatDto.message();
|
||||||
return switch (code) {
|
return switch (code) {
|
||||||
case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
|
case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
|
||||||
case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage);
|
case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage);
|
||||||
|
|||||||
@@ -71,10 +71,6 @@ public class UploadService {
|
|||||||
if (size > 1024 * 1024) {
|
if (size > 1024 * 1024) {
|
||||||
throw new BusinessException("知识库文档大小不能超过1MB");
|
throw new BusinessException("知识库文档大小不能超过1MB");
|
||||||
}
|
}
|
||||||
String contentType = multipartFile.getContentType();
|
|
||||||
if (!StringUtils.startsWith(contentType, "text/")) {
|
|
||||||
throw new BusinessException("非法的上传文件");
|
|
||||||
}
|
|
||||||
minioClient.putObject(
|
minioClient.putObject(
|
||||||
PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream(
|
PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream(
|
||||||
multipartFile.getInputStream(), size, -1)
|
multipartFile.getInputStream(), size, -1)
|
||||||
|
|||||||
@@ -894,7 +894,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"$ref": "#/components/schemas/ChatDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1580,7 +1580,8 @@
|
|||||||
"DocUpdateDto": {
|
"DocUpdateDto": {
|
||||||
"required": [
|
"required": [
|
||||||
"enable",
|
"enable",
|
||||||
"id"
|
"id",
|
||||||
|
"libId"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1588,6 +1589,10 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
|
"libId": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"enable": {
|
"enable": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
@@ -1868,6 +1873,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ChatDto": {
|
||||||
|
"required": [
|
||||||
|
"message",
|
||||||
|
"mode"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"NORMAL",
|
||||||
|
"WITH_LIBRARY"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"libraryId": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"PageRequestDto": {
|
"PageRequestDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
11
frontend/src/api/types/schema.d.ts
vendored
11
frontend/src/api/types/schema.d.ts
vendored
@@ -783,6 +783,8 @@ export interface components {
|
|||||||
DocUpdateDto: {
|
DocUpdateDto: {
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
id: number;
|
id: number;
|
||||||
|
/** Format: int64 */
|
||||||
|
libId: number;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
};
|
};
|
||||||
LlmVm: {
|
LlmVm: {
|
||||||
@@ -867,6 +869,13 @@ export interface components {
|
|||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
ChatDto: {
|
||||||
|
/** @enum {string} */
|
||||||
|
mode: "NORMAL" | "WITH_LIBRARY";
|
||||||
|
/** Format: int64 */
|
||||||
|
libraryId?: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
PageRequestDto: {
|
PageRequestDto: {
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
page?: number;
|
page?: number;
|
||||||
@@ -1888,7 +1897,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": string;
|
"application/json": components["schemas"]["ChatDto"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
|||||||
@@ -5,14 +5,18 @@
|
|||||||
<div class="flex flex-col gap-y-5 flex-1 pb-2">
|
<div class="flex flex-col gap-y-5 flex-1 pb-2">
|
||||||
<li v-for="chatElement in messages" :key="chatElement.content"
|
<li v-for="chatElement in messages" :key="chatElement.content"
|
||||||
:class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']">
|
:class="['flex items-start gap-2.5', chatElement.isUser ? 'flex-row-reverse' : 'flex-row']">
|
||||||
<Avatar :src="chatElement.isUser ? user.avatar : '/trump.jpg'" size="sm"
|
<Avatar :src="chatElement.isUser ? user.avatar : undefined" size="sm"
|
||||||
:alt="chatElement.isUser ? '用户头像' : 'AI头像'" />
|
:alt="chatElement.isUser ? '用户头像' : 'AI头像'" />
|
||||||
<div
|
<div
|
||||||
:class="['flex flex-col leading-1.5 p-4 border-gray-200 rounded-e-xl rounded-es-xl max-w-[calc(100%-40px)]', chatElement.isUser ? 'bg-blue-100' : 'bg-gray-100']">
|
:class="['flex flex-col leading-1.5 p-4 border-gray-200 max-w-[calc(100%-40px)]', chatElement.isUser ? 'bg-blue-100 rounded-tl-xl rounded-bl-xl rounded-br-xl' : 'bg-gray-100 rounded-e-xl rounded-es-xl']">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span class="text-sm font-semibold text-gray-900 ">{{ chatElement.username }}</span>
|
<span class="text-sm font-semibold text-gray-900 ">{{ chatElement.username }}</span>
|
||||||
<LoadingIcon :textColor="'text-gray-900'"
|
<LoadingIcon :textColor="'text-gray-900'"
|
||||||
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
|
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
|
||||||
|
<span v-if="!chatElement.isUser && chatElement.withLibrary"
|
||||||
|
class="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full">
|
||||||
|
{{ chatElement.libraryName }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 break-words"
|
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 break-words"
|
||||||
@@ -34,14 +38,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="sticky">
|
<form class="sticky">
|
||||||
<button @click.prevent="clearConversation"
|
<div class="flex items-center justify-between gap-2 mb-2">
|
||||||
class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2
|
<button @click.prevent="clearConversation"
|
||||||
overflow-hidden text-sm font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-600 to-blue-500 group-hover:from-purple-600 group-hover:to-blue-500 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 ">
|
class="relative inline-flex items-center justify-center p-0.5
|
||||||
<span
|
overflow-hidden text-sm font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-600 to-blue-500 group-hover:from-purple-600 group-hover:to-blue-500 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 ">
|
||||||
class="relative px-3 py-2 text-xs font-medium transition-all ease-in duration-75 bg-white rounded-md group-hover:bg-transparent">
|
<span
|
||||||
开启新对话
|
class="relative px-3 py-2 text-xs font-medium transition-all ease-in duration-75 bg-white rounded-md group-hover:bg-transparent">
|
||||||
</span>
|
开启新对话
|
||||||
</button>
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<select v-if="commandMode === 'chat'" v-model="selectedLibraryId"
|
||||||
|
class="bg-white border border-gray-300 text-gray-900 text-xs rounded-lg py-2 px-2 flex-1 max-w-48">
|
||||||
|
<option :value="undefined">不使用知识库</option>
|
||||||
|
<option v-for="library in libraries" :key="library.id" :value="library.id">
|
||||||
|
{{ library.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="w-full border border-gray-200 rounded-lg bg-gray-50">
|
<div class="w-full border border-gray-200 rounded-lg bg-gray-50">
|
||||||
<div class="px-4 py-2 bg-white rounded-t-lg">
|
<div class="px-4 py-2 bg-white rounded-t-lg">
|
||||||
<label for="comment" class="sr-only"></label>
|
<label for="comment" class="sr-only"></label>
|
||||||
@@ -51,7 +65,7 @@
|
|||||||
" required></textarea>
|
" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between px-2 py-2 border-t border-gray-200">
|
<div class="flex justify-between px-2 py-2 border-t border-gray-200">
|
||||||
<select id="countries" v-model="commandMode"
|
<select id="commandMode" v-model="commandMode"
|
||||||
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg block">
|
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg block">
|
||||||
<option selected :value="'execute'">指令模式</option>
|
<option selected :value="'execute'">指令模式</option>
|
||||||
<option :value="'search'">搜索模式</option>
|
<option :value="'search'">搜索模式</option>
|
||||||
@@ -62,7 +76,6 @@
|
|||||||
{{ isLoading ? '中止' : '发送' }}
|
{{ isLoading ? '中止' : '发送' }}
|
||||||
</TableButton>
|
</TableButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,6 +157,9 @@ import PositionDeleteModal from "@/components/modals/ConfirmationDialog.vue";
|
|||||||
import PositionFormDialog from "@/components/modals/PositionFormDialog.vue";
|
import PositionFormDialog from "@/components/modals/PositionFormDialog.vue";
|
||||||
import RoleDeleteModal from "@/components/modals/ConfirmationDialog.vue";
|
import RoleDeleteModal from "@/components/modals/ConfirmationDialog.vue";
|
||||||
import RoleFormDialog from "@/components/modals/RoleFormDialog.vue";
|
import RoleFormDialog from "@/components/modals/RoleFormDialog.vue";
|
||||||
|
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||||
|
import { UserFormDialog } from "../modals";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@@ -190,6 +206,15 @@ const actionExcStore = useActionExcStore();
|
|||||||
const { availableDepartments, fetchAvailableDepartments } =
|
const { availableDepartments, fetchAvailableDepartments } =
|
||||||
useDepartmentQuery();
|
useDepartmentQuery();
|
||||||
|
|
||||||
|
// 知识库相关
|
||||||
|
const { libraries, fetchLibraries } = useKnowledgeQuery();
|
||||||
|
const selectedLibraryId = ref<number | null | undefined>(undefined);
|
||||||
|
const selectedLibraryName = computed(() => {
|
||||||
|
return libraries.value.find(
|
||||||
|
(library) => library.id === selectedLibraryId.value,
|
||||||
|
)?.name;
|
||||||
|
});
|
||||||
|
|
||||||
const commandPlaceholderMap: Record<string, string> = {
|
const commandPlaceholderMap: Record<string, string> = {
|
||||||
chat: "随便聊聊",
|
chat: "随便聊聊",
|
||||||
search: "输入「创建用户、删除部门、创建岗位、创建角色、创建权限」试试看",
|
search: "输入「创建用户、删除部门、创建岗位、创建角色、创建权限」试试看",
|
||||||
@@ -235,31 +260,6 @@ const renderMarkdown = (content: string | undefined) => {
|
|||||||
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
|
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
|
||||||
// }, { deep: true });
|
// }, { deep: true });
|
||||||
|
|
||||||
const handleDeleteUserClick = (input: string) => {
|
|
||||||
currentDeleteUsername.value = input;
|
|
||||||
userDeleteModal.value?.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteDepartmentClick = (input: string) => {
|
|
||||||
currentDeleteDepartmentName.value = input;
|
|
||||||
departmentDeleteModal.value?.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePositionClick = (input: string) => {
|
|
||||||
currentDeletePositionName.value = input;
|
|
||||||
positionDeleteModal.value?.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteRoleClick = (input: string) => {
|
|
||||||
currentDeleteRoleName.value = input;
|
|
||||||
roleDeleteModal.value?.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePermissionClick = (input: string) => {
|
|
||||||
currentDeletePermissionName.value = input;
|
|
||||||
permissionDeleteModal.value?.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
|
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
|
||||||
await userUpsert.upsertUser(data);
|
await userUpsert.upsertUser(data);
|
||||||
userUpsertModal.value?.hide();
|
userUpsertModal.value?.hide();
|
||||||
@@ -402,7 +402,12 @@ const chatByMode = async (
|
|||||||
await executeAction(message);
|
await executeAction(message);
|
||||||
actionExcStore.notify(true);
|
actionExcStore.notify(true);
|
||||||
} else {
|
} else {
|
||||||
await chat(message);
|
// 聊天模式,判断是否使用知识库
|
||||||
|
if (selectedLibraryId.value !== undefined) {
|
||||||
|
await chat(message, selectedLibraryId.value);
|
||||||
|
} else {
|
||||||
|
await chat(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -427,6 +432,9 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initFlowbite();
|
initFlowbite();
|
||||||
|
// 加载知识库列表
|
||||||
|
await fetchLibraries();
|
||||||
|
|
||||||
const $upsertModalElement: HTMLElement | null =
|
const $upsertModalElement: HTMLElement | null =
|
||||||
document.querySelector("#user-upsert-modal");
|
document.querySelector("#user-upsert-modal");
|
||||||
if ($upsertModalElement) {
|
if ($upsertModalElement) {
|
||||||
|
|||||||
@@ -36,9 +36,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
defineProps({
|
||||||
titleClass: {
|
titleClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: "",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import CardBase from "./CardBase.vue";
|
import CardBase from "./CardBase.vue";
|
||||||
import PromotionBanner from "./PromotionBanner.vue";
|
import PromotionBanner from "./PromotionBanner.vue";
|
||||||
|
|
||||||
export { CardBase, PromotionBanner };
|
export { CardBase, PromotionBanner };
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer-left>
|
<template #footer-left>
|
||||||
<span class="text-xs text-gray-500">
|
<span class="text-xs text-gray-500">
|
||||||
上传时间: {{ formatDateString(doc.createTime) }}
|
{{ formatDateString(doc.createTime) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #footer-actions>
|
<template #footer-actions>
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CardBase from '@/components/common/CardBase.vue';
|
import CardBase from "@/components/common/CardBase.vue";
|
||||||
import { KnowledgeStatusBadge } from '@/components/common/knowledge';
|
import { KnowledgeStatusBadge } from "@/components/common/knowledge";
|
||||||
import type { LibraryDoc } from "@/types/KnowledgeTypes";
|
import type { LibraryDoc } from "@/types/KnowledgeTypes";
|
||||||
import { formatDateString } from '@/utils/dateUtil';
|
import { formatDateString } from "@/utils/dateUtil";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
doc: LibraryDoc;
|
doc: LibraryDoc;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CardBase from '@/components/common/CardBase.vue';
|
import CardBase from "@/components/common/CardBase.vue";
|
||||||
import type { Library } from "@/types/KnowledgeTypes";
|
import type { Library } from "@/types/KnowledgeTypes";
|
||||||
import { formatDateString } from '@/utils/dateUtil';
|
import { formatDateString } from "@/utils/dateUtil";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
library: Library;
|
library: Library;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,28 +10,28 @@
|
|||||||
import { DocStatus } from "@/types/KnowledgeTypes";
|
import { DocStatus } from "@/types/KnowledgeTypes";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
status?: string;
|
status?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
type: 'status' | 'enabled';
|
type: "status" | "enabled";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const getStatusClass = () => {
|
const getStatusClass = () => {
|
||||||
if (props.type === 'status') {
|
if (props.type === "status") {
|
||||||
return props.status === DocStatus.SUCCESS
|
return props.status === DocStatus.SUCCESS
|
||||||
? 'bg-green-100 text-green-800'
|
? "bg-green-100 text-green-800"
|
||||||
: 'bg-yellow-100 text-yellow-800';
|
: "bg-yellow-100 text-yellow-800";
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.enabled
|
return props.enabled
|
||||||
? 'bg-blue-100 text-blue-800'
|
? "bg-blue-100 text-blue-800"
|
||||||
: 'bg-gray-100 text-gray-800';
|
: "bg-gray-100 text-gray-800";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = () => {
|
const getStatusText = () => {
|
||||||
if (props.type === 'status') {
|
if (props.type === "status") {
|
||||||
return props.status === DocStatus.SUCCESS ? '解析完成' : '解析中';
|
return props.status === DocStatus.SUCCESS ? "解析完成" : "解析中";
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.enabled ? '已启用' : '已禁用';
|
return props.enabled ? "已启用" : "已禁用";
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,11 +27,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CardBase from '@/components/common/CardBase.vue';
|
import CardBase from "@/components/common/CardBase.vue";
|
||||||
import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
|
import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
segment: LibraryDocSegment;
|
segment: LibraryDocSegment;
|
||||||
index: number;
|
index: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ export {
|
|||||||
KnowledgeDocCard,
|
KnowledgeDocCard,
|
||||||
KnowledgeLibraryCard,
|
KnowledgeLibraryCard,
|
||||||
SegmentCard,
|
SegmentCard,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import FormInput from './FormInput.vue';
|
import FormInput from "./FormInput.vue";
|
||||||
import FormSelect from './FormSelect.vue';
|
import FormSelect from "./FormSelect.vue";
|
||||||
|
|
||||||
export {
|
export { FormInput, FormSelect };
|
||||||
FormInput,
|
|
||||||
FormSelect
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -29,7 +29,18 @@
|
|||||||
import { Modal, initFlowbite } from "flowbite";
|
import { Modal, initFlowbite } from "flowbite";
|
||||||
import { computed, onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
|
|
||||||
export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl";
|
export type ModalSize =
|
||||||
|
| "xs"
|
||||||
|
| "sm"
|
||||||
|
| "md"
|
||||||
|
| "lg"
|
||||||
|
| "xl"
|
||||||
|
| "2xl"
|
||||||
|
| "3xl"
|
||||||
|
| "4xl"
|
||||||
|
| "5xl"
|
||||||
|
| "6xl"
|
||||||
|
| "7xl";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 对话框标题 */
|
/** 对话框标题 */
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ export {
|
|||||||
RoleFormDialog,
|
RoleFormDialog,
|
||||||
SchedulerFormDialog,
|
SchedulerFormDialog,
|
||||||
UserFormDialog,
|
UserFormDialog,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export {
|
|||||||
TableFilterForm,
|
TableFilterForm,
|
||||||
TableFormLayout,
|
TableFormLayout,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const sizeClass = computed(() => {
|
|||||||
sm: "w-8 h-8",
|
sm: "w-8 h-8",
|
||||||
md: "w-10 h-10",
|
md: "w-10 h-10",
|
||||||
lg: "w-12 h-12",
|
lg: "w-12 h-12",
|
||||||
xl: "w-16 h-16"
|
xl: "w-16 h-16",
|
||||||
};
|
};
|
||||||
return sizes[props.size || "md"];
|
return sizes[props.size || "md"];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,95 +28,95 @@ import { StopIcon } from "@/components/icons";
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
export type ButtonVariant =
|
export type ButtonVariant =
|
||||||
| "primary"
|
| "primary"
|
||||||
| "secondary"
|
| "secondary"
|
||||||
| "success"
|
| "success"
|
||||||
| "danger"
|
| "danger"
|
||||||
| "warning"
|
| "warning"
|
||||||
| "info";
|
| "info";
|
||||||
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
||||||
export type ButtonType = "button" | "submit" | "reset";
|
export type ButtonType = "button" | "submit" | "reset";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 按钮变体类型 */
|
/** 按钮变体类型 */
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
/** 按钮尺寸 */
|
/** 按钮尺寸 */
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
/** 是否禁用 */
|
/** 是否禁用 */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/** 自定义CSS类名 */
|
/** 自定义CSS类名 */
|
||||||
className?: string;
|
className?: string;
|
||||||
/** 是否为移动端尺寸 */
|
/** 是否为移动端尺寸 */
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
/** 是否处于加载状态 */
|
/** 是否处于加载状态 */
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
/** 是否可中止 */
|
/** 是否可中止 */
|
||||||
abortable?: boolean;
|
abortable?: boolean;
|
||||||
/** 按钮类型 */
|
/** 按钮类型 */
|
||||||
type?: ButtonType;
|
type?: ButtonType;
|
||||||
/** 是否占满宽度 */
|
/** 是否占满宽度 */
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [event: MouseEvent];
|
click: [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/** 按钮颜色样式映射 */
|
/** 按钮颜色样式映射 */
|
||||||
const colorClasses = computed(() => {
|
const colorClasses = computed(() => {
|
||||||
const variants: Record<ButtonVariant, string> = {
|
const variants: Record<ButtonVariant, string> = {
|
||||||
primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300",
|
primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300",
|
||||||
secondary:
|
secondary:
|
||||||
"text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100",
|
"text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100",
|
||||||
success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300",
|
success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300",
|
||||||
danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300",
|
danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300",
|
||||||
warning:
|
warning:
|
||||||
"text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300",
|
"text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300",
|
||||||
info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300",
|
info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
return variants[props.variant || "primary"];
|
return variants[props.variant || "primary"];
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 按钮尺寸样式映射 */
|
/** 按钮尺寸样式映射 */
|
||||||
const sizeClasses = computed(() => {
|
const sizeClasses = computed(() => {
|
||||||
// 移动端尺寸
|
// 移动端尺寸
|
||||||
if (props.isMobile) {
|
if (props.isMobile) {
|
||||||
const sizes: Record<ButtonSize, string> = {
|
const sizes: Record<ButtonSize, string> = {
|
||||||
xs: "text-xs px-2 py-1",
|
xs: "text-xs px-2 py-1",
|
||||||
sm: "text-xs px-3 py-1.5",
|
sm: "text-xs px-3 py-1.5",
|
||||||
md: "text-sm px-3 py-2",
|
md: "text-sm px-3 py-2",
|
||||||
lg: "text-sm px-4 py-2.5",
|
lg: "text-sm px-4 py-2.5",
|
||||||
};
|
};
|
||||||
return sizes[props.size || "sm"];
|
return sizes[props.size || "sm"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// PC端尺寸
|
// PC端尺寸
|
||||||
const sizes: Record<ButtonSize, string> = {
|
const sizes: Record<ButtonSize, string> = {
|
||||||
xs: "text-xs px-3 py-1.5",
|
xs: "text-xs px-3 py-1.5",
|
||||||
sm: "text-sm px-3 py-2",
|
sm: "text-sm px-3 py-2",
|
||||||
md: "text-sm px-4 py-2.5",
|
md: "text-sm px-4 py-2.5",
|
||||||
lg: "text-base px-5 py-3",
|
lg: "text-base px-5 py-3",
|
||||||
};
|
};
|
||||||
|
|
||||||
return sizes[props.size || "md"];
|
return sizes[props.size || "md"];
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 图标尺寸样式映射 */
|
/** 图标尺寸样式映射 */
|
||||||
const iconSizeClasses = computed(() => {
|
const iconSizeClasses = computed(() => {
|
||||||
const sizes: Record<ButtonSize, string> = {
|
const sizes: Record<ButtonSize, string> = {
|
||||||
xs: "w-3.5 h-3.5",
|
xs: "w-3.5 h-3.5",
|
||||||
sm: "w-4 h-4",
|
sm: "w-4 h-4",
|
||||||
md: "w-4.5 h-4.5",
|
md: "w-4.5 h-4.5",
|
||||||
lg: "w-5 h-5",
|
lg: "w-5 h-5",
|
||||||
};
|
};
|
||||||
return sizes[props.size || "md"];
|
return sizes[props.size || "md"];
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 处理点击事件 */
|
/** 处理点击事件 */
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
if (!props.disabled && !(props.isLoading && !props.abortable)) {
|
if (!props.disabled && !(props.isLoading && !props.abortable)) {
|
||||||
emit("click", event);
|
emit("click", event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ import Button from "./Button.vue";
|
|||||||
import InputButton from "./InputButton.vue";
|
import InputButton from "./InputButton.vue";
|
||||||
import SortIcon from "./SortIcon.vue";
|
import SortIcon from "./SortIcon.vue";
|
||||||
|
|
||||||
export { Alert, Avatar, Button, InputButton, SortIcon };
|
export { Alert, Avatar, Button, InputButton, SortIcon };
|
||||||
|
|||||||
@@ -11,13 +11,19 @@ export const useAiChat = () => {
|
|||||||
isUser: boolean;
|
isUser: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
withLibrary?: boolean;
|
||||||
|
libraryName?: string;
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
let currentController: AbortController | null = null;
|
let currentController: AbortController | null = null;
|
||||||
|
|
||||||
const chat = async (message: string) => {
|
const chat = async (
|
||||||
|
message: string,
|
||||||
|
libraryId?: number | null,
|
||||||
|
libraryName?: string,
|
||||||
|
) => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
@@ -27,6 +33,8 @@ export const useAiChat = () => {
|
|||||||
type: "chat",
|
type: "chat",
|
||||||
isUser: false,
|
isUser: false,
|
||||||
username: "知路智能体",
|
username: "知路智能体",
|
||||||
|
withLibrary: libraryId !== undefined,
|
||||||
|
libraryName: libraryName,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
|
const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
|
||||||
@@ -36,7 +44,11 @@ export const useAiChat = () => {
|
|||||||
Authorization: authStore.get(),
|
Authorization: authStore.get(),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: message,
|
body: JSON.stringify({
|
||||||
|
mode: libraryId !== undefined ? "WITH_LIBRARY" : "NORMAL",
|
||||||
|
libraryId: libraryId,
|
||||||
|
message: message,
|
||||||
|
}),
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
onmessage(ev) {
|
onmessage(ev) {
|
||||||
messages.value[messages.value.length - 1].content += ev.data;
|
messages.value[messages.value.length - 1].content += ev.data;
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import { usePagination } from "./usePagination";
|
|||||||
import { useSorting } from "./useSorting";
|
import { useSorting } from "./useSorting";
|
||||||
import { useStyleSystem } from "./useStyleSystem";
|
import { useStyleSystem } from "./useStyleSystem";
|
||||||
|
|
||||||
export { useErrorHandling, usePagination, useSorting, useStyleSystem };
|
export { useErrorHandling, usePagination, useSorting, useStyleSystem };
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ export interface DocQueryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SegmentQueryParams {
|
export interface SegmentQueryParams {
|
||||||
libraryDocId?: number;
|
libraryDocId?: number;
|
||||||
docId?: number;
|
docId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DocStatus {
|
export enum DocStatus {
|
||||||
SUCCESS = "SUCCESS",
|
SUCCESS = "SUCCESS",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getUserAvatarUrl } from "./avatarUtil";
|
import { getUserAvatarUrl } from "./avatarUtil";
|
||||||
import { dayjs, formatDate, formatDateString } from "./dateUtil";
|
import { dayjs, formatDate, formatDateString } from "./dateUtil";
|
||||||
|
|
||||||
export { getUserAvatarUrl, dayjs, formatDate, formatDateString };
|
export { getUserAvatarUrl, dayjs, formatDate, formatDateString };
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
|||||||
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import { formatDateString } from '@/utils/dateUtil';
|
import { formatDateString } from "@/utils/dateUtil";
|
||||||
|
|
||||||
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
|
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
|
||||||
import { DocStatus } from "@/types/KnowledgeTypes";
|
import { DocStatus } from "@/types/KnowledgeTypes";
|
||||||
|
|||||||
@@ -28,9 +28,6 @@
|
|||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="flex flex-col items-center justify-center py-10">
|
<div v-else class="flex flex-col items-center justify-center py-10">
|
||||||
<div class="text-gray-500 text-lg mb-4">暂无分段内容</div>
|
<div class="text-gray-500 text-lg mb-4">暂无分段内容</div>
|
||||||
<Button variant="secondary" @click="navigateBack">
|
|
||||||
返回文档列表
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -55,7 +52,8 @@ const libraryId = Number.parseInt(route.params.libraryId as string, 10);
|
|||||||
const docId = Number.parseInt(route.params.docId as string, 10);
|
const docId = Number.parseInt(route.params.docId as string, 10);
|
||||||
|
|
||||||
// 获取文档信息和分段列表
|
// 获取文档信息和分段列表
|
||||||
const { docs, segments, fetchLibraryDocs, fetchDocSegments } = useKnowledgeQuery();
|
const { docs, segments, fetchLibraryDocs, fetchDocSegments } =
|
||||||
|
useKnowledgeQuery();
|
||||||
const currentDoc = ref<LibraryDoc | undefined>();
|
const currentDoc = ref<LibraryDoc | undefined>();
|
||||||
|
|
||||||
// 导航回文档列表
|
// 导航回文档列表
|
||||||
|
|||||||
Reference in New Issue
Block a user