diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java index ad51bfe..b77f900 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -2,6 +2,7 @@ package com.zl.mjga.controller; import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.ai.LlmQueryDto; import com.zl.mjga.dto.ai.LlmVm; import com.zl.mjga.service.AiChatService; import com.zl.mjga.service.LlmService; @@ -52,7 +53,8 @@ public class AiController { @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_LLM_CONFIG_PERMISSION)") @GetMapping("/llm/page-query") @ResponseStatus(HttpStatus.OK) - public PageResponseDto> pageQueryLlm(@ModelAttribute PageRequestDto pageRequestDto) { - return llmService.pageQueryLlm(pageRequestDto); + public PageResponseDto> pageQueryLlm( + @ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto llmQueryDto) { + return llmService.pageQueryLlm(pageRequestDto, llmQueryDto); } } diff --git a/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java new file mode 100644 index 0000000..d9fec07 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java @@ -0,0 +1,3 @@ +package com.zl.mjga.dto.ai; + +public record LlmQueryDto(String name) {} diff --git a/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java b/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java index 74db706..0e6dfa1 100644 --- a/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java +++ b/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java @@ -1,8 +1,11 @@ package com.zl.mjga.repository; import static org.jooq.generated.mjga.Tables.AI_LLM_CONFIG; +import static org.jooq.impl.DSL.noCondition; import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.ai.LlmQueryDto; +import org.apache.commons.lang3.StringUtils; import org.jooq.Configuration; import org.jooq.Record; import org.jooq.Result; @@ -19,11 +22,15 @@ public class LlmRepository extends AiLlmConfigDao { super(configuration); } - public Result pageFetchBy(PageRequestDto pageRequestDto) { + public Result pageFetchBy(PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) { return ctx() .select( AI_LLM_CONFIG.asterisk(), DSL.count().over().as("total_llm").convertFrom(Long::valueOf)) .from(AI_LLM_CONFIG) + .where( + StringUtils.isNotEmpty(llmQueryDto.name()) + ? AI_LLM_CONFIG.NAME.eq(llmQueryDto.name()) + : noCondition()) .orderBy(pageRequestDto.getSortFields()) .limit(pageRequestDto.getSize()) .offset(pageRequestDto.getOffset()) diff --git a/backend/src/main/java/com/zl/mjga/service/AiChatService.java b/backend/src/main/java/com/zl/mjga/service/AiChatService.java index 75c142d..9612281 100644 --- a/backend/src/main/java/com/zl/mjga/service/AiChatService.java +++ b/backend/src/main/java/com/zl/mjga/service/AiChatService.java @@ -1,7 +1,9 @@ package com.zl.mjga.service; import com.zl.mjga.config.ai.AiChatAssistant; +import com.zl.mjga.exception.BusinessException; import dev.langchain4j.service.TokenStream; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jooq.generated.mjga.enums.LlmCodeEnum; @@ -26,8 +28,9 @@ public class AiChatService { } public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) { - AiLlmConfig precedenceLlmBy = llmService.getPrecedenceLlmBy(true); - LlmCodeEnum code = precedenceLlmBy.getCode(); + Optional precedenceLlmBy = llmService.getPrecedenceLlmBy(true); + AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型")); + LlmCodeEnum code = aiLlmConfig.getCode(); return switch (code) { case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage); case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage); diff --git a/backend/src/main/java/com/zl/mjga/service/LlmService.java b/backend/src/main/java/com/zl/mjga/service/LlmService.java index ad4348f..54d0415 100644 --- a/backend/src/main/java/com/zl/mjga/service/LlmService.java +++ b/backend/src/main/java/com/zl/mjga/service/LlmService.java @@ -2,10 +2,12 @@ package com.zl.mjga.service; import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.ai.LlmQueryDto; import com.zl.mjga.dto.ai.LlmVm; import com.zl.mjga.repository.LlmRepository; import java.util.List; import java.util.Objects; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jooq.Record; @@ -26,16 +28,14 @@ public class LlmService { return llmRepository.fetchOneByCode(llmCodeEnum); } - public AiLlmConfig getPrecedenceLlmBy(Boolean enable) { + public Optional getPrecedenceLlmBy(Boolean enable) { List aiLlmConfigs = llmRepository.fetchByEnable(enable); - //noinspection OptionalGetWithoutIsPresent - return aiLlmConfigs.stream() - .max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority())) - .get(); + return aiLlmConfigs.stream().max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority())); } - public PageResponseDto> pageQueryLlm(PageRequestDto pageRequestDto) { - Result records = llmRepository.pageFetchBy(pageRequestDto); + public PageResponseDto> pageQueryLlm( + PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) { + Result records = llmRepository.pageFetchBy(pageRequestDto, llmQueryDto); if (records.isEmpty()) { return PageResponseDto.empty(); } diff --git a/frontend/package.json b/frontend/package.json index 60f3b4c..c9fe9a4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,8 +57,6 @@ "vue-tsc": "^2.2.8" }, "msw": { - "workerDirectory": [ - "public" - ] + "workerDirectory": ["public"] } } diff --git a/frontend/src/api/mocks/aiHandlers.ts b/frontend/src/api/mocks/aiHandlers.ts index 72814b2..6a571cb 100644 --- a/frontend/src/api/mocks/aiHandlers.ts +++ b/frontend/src/api/mocks/aiHandlers.ts @@ -8,4 +8,26 @@ export default [ }); return response; }), + http.get("/ai/llm/page-query", () => { + const generateLlm = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + name: faker.lorem.word(), + modelName: faker.lorem.word(), + apiKey: faker.string.uuid(), + url: faker.internet.url(), + enable: faker.datatype.boolean(), + priority: faker.number.int({ min: 1, max: 10 }), + }); + + const mockData = { + data: faker.helpers.multiple(generateLlm, { count: 10 }), + total: 30, + }; + return HttpResponse.json(mockData); + }), + http.put("/ai/llm", () => { + return HttpResponse.json({ + message: "Llm updated successfully", + }); + }), ]; diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json index 49aa550..8f2039a 100644 --- a/frontend/src/api/schema/openapi.json +++ b/frontend/src/api/schema/openapi.json @@ -44,6 +44,29 @@ } } }, + "/ai/llm": { + "put": { + "tags": [ + "ai-controller" + ], + "operationId": "updateLlm", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LlmVm" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/scheduler/trigger/resume": { "post": { "tags": [ @@ -1008,6 +1031,44 @@ } } } + }, + "/ai/llm/page-query": { + "get": { + "tags": [ + "ai-controller" + ], + "operationId": "pageQueryLlm", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "llmQueryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/LlmQueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListLlmVm" + } + } + } + } + } + } } }, "components": { @@ -1027,6 +1088,43 @@ } } }, + "LlmVm": { + "required": [ + "apiKey", + "enable", + "id", + "modelName", + "name", + "priority", + "url" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "modelName": { + "type": "string" + }, + "apiKey": { + "type": "string" + }, + "url": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "priority": { + "type": "integer", + "format": "int32" + } + } + }, "JobKeyDto": { "required": [ "group", @@ -1684,6 +1782,29 @@ } } } + }, + "LlmQueryDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "PageResponseDtoListLlmVm": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LlmVm" + } + } + } } } } diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts index ca1b349..5bb5086 100644 --- a/frontend/src/api/types/schema.d.ts +++ b/frontend/src/api/types/schema.d.ts @@ -20,6 +20,22 @@ export interface paths { patch?: never; trace?: never; }; + "/ai/llm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["updateLlm"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/scheduler/trigger/resume": { parameters: { query?: never; @@ -484,6 +500,22 @@ export interface paths { patch?: never; trace?: never; }; + "/ai/llm/page-query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["pageQueryLlm"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -492,6 +524,17 @@ export interface components { name: string; group: string; }; + LlmVm: { + /** Format: int64 */ + id: number; + name: string; + modelName: string; + apiKey: string; + url: string; + enable: boolean; + /** Format: int32 */ + priority: number; + }; JobKeyDto: { name: string; group: string; @@ -714,6 +757,14 @@ export interface components { total?: number; data?: components["schemas"]["DepartmentRespDto"][]; }; + LlmQueryDto: { + name?: string; + }; + PageResponseDtoListLlmVm: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["LlmVm"][]; + }; }; responses: never; parameters: never; @@ -747,6 +798,28 @@ export interface operations { }; }; }; + updateLlm: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LlmVm"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; resumeTrigger: { parameters: { query?: never; @@ -1553,4 +1626,27 @@ export interface operations { }; }; }; + pageQueryLlm: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + llmQueryDto: components["schemas"]["LlmQueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListLlmVm"]; + }; + }; + }; + }; } diff --git a/frontend/src/components/LlmUpdateModal.vue b/frontend/src/components/LlmUpdateModal.vue new file mode 100644 index 0000000..40efce4 --- /dev/null +++ b/frontend/src/components/LlmUpdateModal.vue @@ -0,0 +1,142 @@ + + diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 09f940d..d6841e0 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -32,6 +32,7 @@ import SchedulerIcon from "./icons/SchedulerIcon.vue"; import SettingsIcon from "./icons/SettingsIcon.vue"; import UsersIcon from "./icons/UsersIcon.vue"; import AiChatIcon from "./icons/AiChatIcon.vue"; +import LlmConfigIcon from "./icons/LlmConfigIcon.vue"; // 菜单配置 const menuItems = [ @@ -75,6 +76,11 @@ const menuItems = [ path: `${RoutePath.DASHBOARD}/${RoutePath.AICHATVIEW}`, icon: AiChatIcon, }, + { + title: "大模型管理", + path: `${RoutePath.DASHBOARD}/${RoutePath.LLMCONFIGVIEW}`, + icon: LlmConfigIcon, + }, ]; const route = useRoute(); diff --git a/frontend/src/components/UserUpsertModal.vue b/frontend/src/components/UserUpsertModal.vue index 637c76f..d5c87a2 100644 --- a/frontend/src/components/UserUpsertModal.vue +++ b/frontend/src/components/UserUpsertModal.vue @@ -42,8 +42,8 @@ required placeholder="编辑时非必填" />
- - diff --git a/frontend/src/components/icons/LlmConfigIcon.vue b/frontend/src/components/icons/LlmConfigIcon.vue new file mode 100644 index 0000000..de3407f --- /dev/null +++ b/frontend/src/components/icons/LlmConfigIcon.vue @@ -0,0 +1,15 @@ + + diff --git a/frontend/src/composables/ai/useAiChat.ts b/frontend/src/composables/ai/useAiChat.ts index 72ac244..6974f81 100644 --- a/frontend/src/composables/ai/useAiChat.ts +++ b/frontend/src/composables/ai/useAiChat.ts @@ -1,6 +1,7 @@ import { fetchEventSource } from "@microsoft/fetch-event-source"; import { ref } from "vue"; import useAuthStore from "../store/useAuthStore"; +import useAlertStore from "../store/useAlertStore"; const authStore = useAuthStore(); diff --git a/frontend/src/composables/ai/useLlmQuery.ts b/frontend/src/composables/ai/useLlmQuery.ts new file mode 100644 index 0000000..3c9dbe7 --- /dev/null +++ b/frontend/src/composables/ai/useLlmQuery.ts @@ -0,0 +1,32 @@ +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; +import client from "../../api/client"; + +export const useLlmQuery = () => { + const total = ref(0); + const llms = ref([]); + + const fetchLlmConfigs = async (page = 1, size = 10, name?: string) => { + const { data } = await client.GET("/ai/llm/page-query", { + params: { + query: { + pageRequestDto: { + page, + size, + }, + llmQueryDto: { + name, + }, + }, + }, + }); + llms.value = data?.data ?? []; + total.value = !data || !data.total ? 0 : data.total; + }; + + return { + llms, + total, + fetchLlmConfigs, + }; +}; diff --git a/frontend/src/composables/ai/useLlmUpdate.ts b/frontend/src/composables/ai/useLlmUpdate.ts new file mode 100644 index 0000000..5d2fed7 --- /dev/null +++ b/frontend/src/composables/ai/useLlmUpdate.ts @@ -0,0 +1,13 @@ +import type { components } from "../../api/types/schema"; +import client from "../../api/client"; + +export const useLlmUpdate = () => { + const updateLlmConfig = async (llm: components["schemas"]["LlmVm"]) => { + await client.PUT("/ai/llm", { + body: llm, + }); + }; + return { + updateLlmConfig, + }; +}; diff --git a/frontend/src/router/constants.ts b/frontend/src/router/constants.ts index aba99d6..901d049 100644 --- a/frontend/src/router/constants.ts +++ b/frontend/src/router/constants.ts @@ -16,6 +16,7 @@ export enum RoutePath { POSITIONVIEW = "positions", CREATEUSERVIEW = "create-user", AICHATVIEW = "ai/chat", + LLMCONFIGVIEW = "llm/config", SCHEDULERVIEW = "scheduler", UPSERTUSERVIEW = "upsert-user", UPSERTROLEVIEW = "upsert-role", @@ -41,6 +42,7 @@ export enum RouteName { POSITIONVIEW = "positions", CREATEUSERVIEW = "create-user", AICHATVIEW = "ai/chat", + LLMCONFIGVIEW = "llm/config", SCHEDULERVIEW = "scheduler", UPSERTUSERVIEW = "upsert-user", UPSERTROLEVIEW = "upsert-role", @@ -67,4 +69,6 @@ export enum EPermission { WRITE_USER_ROLE_PERMISSION = "WRITE_USER_ROLE_PERMISSION", DELETE_USER_ROLE_PERMISSION = "DELETE_USER_ROLE_PERMISSION", READ_USER_ROLE_PERMISSION = "READ_USER_ROLE_PERMISSION", + READ_LLM_CONFIG_PERMISSION = "READ_LLM_CONFIG_PERMISSION", + WRITE_LLM_CONFIG_PERMISSION = "WRITE_LLM_CONFIG_PERMISSION", } diff --git a/frontend/src/router/modules/ai.ts b/frontend/src/router/modules/ai.ts index 8a98e7f..3108d22 100644 --- a/frontend/src/router/modules/ai.ts +++ b/frontend/src/router/modules/ai.ts @@ -8,7 +8,15 @@ const aiRoutes: RouteRecordRaw[] = [ component: () => import("@/views/AiChatView.vue"), meta: { requiresAuth: true, - // hasPermission: EPermission.READ_USER_ROLE_PERMISSION, + }, + }, + { + path: RoutePath.LLMCONFIGVIEW, + name: RouteName.LLMCONFIGVIEW, + component: () => import("@/views/LlmConfigView.vue"), + meta: { + requiresAuth: true, + hasPermission: EPermission.READ_LLM_CONFIG_PERMISSION, }, }, ]; diff --git a/frontend/src/utils/errorHandler.ts b/frontend/src/utils/errorHandler.ts index 40a7ae2..72e2c0b 100644 --- a/frontend/src/utils/errorHandler.ts +++ b/frontend/src/utils/errorHandler.ts @@ -21,7 +21,7 @@ const makeErrorHandler = }) => void, ) => (err: unknown, instance: ComponentPublicInstance | null, info: string) => { - console.error(err); + console.error(err); if (err instanceof UnAuthError) { signOut(); router.push(RoutePath.LOGIN); diff --git a/frontend/src/views/AiChatView.vue b/frontend/src/views/AiChatView.vue index 2b741bc..3686cc8 100644 --- a/frontend/src/views/AiChatView.vue +++ b/frontend/src/views/AiChatView.vue @@ -57,8 +57,8 @@ + +