mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-13 21:27:19 +08:00
add llm config
This commit is contained in:
@@ -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<List<LlmVm>> pageQueryLlm(@ModelAttribute PageRequestDto pageRequestDto) {
|
||||
return llmService.pageQueryLlm(pageRequestDto);
|
||||
public PageResponseDto<List<LlmVm>> pageQueryLlm(
|
||||
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto llmQueryDto) {
|
||||
return llmService.pageQueryLlm(pageRequestDto, llmQueryDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.zl.mjga.dto.ai;
|
||||
|
||||
public record LlmQueryDto(String name) {}
|
||||
@@ -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<Record> pageFetchBy(PageRequestDto pageRequestDto) {
|
||||
public Result<Record> 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())
|
||||
|
||||
@@ -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<AiLlmConfig> 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);
|
||||
|
||||
@@ -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<AiLlmConfig> getPrecedenceLlmBy(Boolean enable) {
|
||||
List<AiLlmConfig> 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<List<LlmVm>> pageQueryLlm(PageRequestDto pageRequestDto) {
|
||||
Result<Record> records = llmRepository.pageFetchBy(pageRequestDto);
|
||||
public PageResponseDto<List<LlmVm>> pageQueryLlm(
|
||||
PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) {
|
||||
Result<Record> records = llmRepository.pageFetchBy(pageRequestDto, llmQueryDto);
|
||||
if (records.isEmpty()) {
|
||||
return PageResponseDto.empty();
|
||||
}
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
"public"
|
||||
]
|
||||
"workerDirectory": ["public"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
frontend/src/api/types/schema.d.ts
vendored
96
frontend/src/api/types/schema.d.ts
vendored
@@ -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<string, never>;
|
||||
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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
142
frontend/src/components/LlmUpdateModal.vue
Normal file
142
frontend/src/components/LlmUpdateModal.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<!-- Main modal -->
|
||||
<div id="user-upsert-modal" tabindex="-1" aria-hidden="true"
|
||||
class="bg-gray-900/50 /80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full">
|
||||
<!-- Modal content -->
|
||||
<div class="relative bg-white rounded-lg shadow-sm ">
|
||||
<!-- Modal header -->
|
||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-900 ">
|
||||
大模型管理
|
||||
</h3>
|
||||
<button type="button" @click="closeModal"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center ">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<form class="p-4 md:p-5">
|
||||
<div class="grid gap-4 mb-4 grid-cols-2">
|
||||
<div class="col-span-2">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 ">名称</label>
|
||||
<input type="text" name="名称" id="name" v-model="formData.name"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 "
|
||||
required="true">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label for="modelName" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">模型名称</label>
|
||||
<input type="text" id="modelName" autocomplete="new-password" v-model="formData.modelName"
|
||||
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-2">
|
||||
<label for="apiKey" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">apiKey</label>
|
||||
<input type="text" id="apiKey" autocomplete="new-password" v-model="formData.apiKey"
|
||||
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-2">
|
||||
<label for="url" class="block mb-2 text-sm font-medium text-gray-900 ">url</label>
|
||||
<input type="text" id="url" autocomplete="new-password" v-model="formData.url"
|
||||
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-2 sm:col-span-1">
|
||||
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 ">状态</label>
|
||||
<select id="status" v-model="formData.enable"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
|
||||
<option :value=true>启用</option>
|
||||
<option :value=false>禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label for="priority" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">优先级</label>
|
||||
<input type="text" id="priority" autocomplete="new-password" v-model="formData.priority"
|
||||
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>
|
||||
<button type="submit" @click.prevent="handleSubmit"
|
||||
class="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-sm px-5 py-2.5 text-center self-start mt-5">
|
||||
保存
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import { initFlowbite } from "flowbite";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { z } from "zod";
|
||||
import type { components } from "../api/types/schema";
|
||||
|
||||
const alertStore = useAlertStore();
|
||||
const { llm, onSubmit } = defineProps<{
|
||||
llm?: components["schemas"]["LlmVm"];
|
||||
closeModal: () => void;
|
||||
onSubmit: (data: components["schemas"]["LlmVm"]) => Promise<void>;
|
||||
}>();
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const updateFormData = (newLlm: typeof llm) => {
|
||||
formData.value = {
|
||||
...newLlm,
|
||||
};
|
||||
};
|
||||
|
||||
watch(() => llm, updateFormData, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const llmSchema = z.object({
|
||||
id: z.number({
|
||||
message: "id不能为空",
|
||||
}),
|
||||
name: z.string({
|
||||
message: "名称不能为空",
|
||||
}),
|
||||
modelName: z.string({
|
||||
message: "模型名称不能为空",
|
||||
}),
|
||||
apiKey: z.string({
|
||||
message: "apiKey不能为空",
|
||||
}),
|
||||
url: z.string({
|
||||
message: "url不能为空",
|
||||
}),
|
||||
enable: z.boolean({
|
||||
message: "状态不能为空",
|
||||
}),
|
||||
priority: z.number({
|
||||
message: "优先级必须为数字",
|
||||
}),
|
||||
});
|
||||
const validatedData = llmSchema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
alertStore.showAlert({
|
||||
level: "error",
|
||||
content: error.errors[0].message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
});
|
||||
</script>
|
||||
@@ -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();
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
required placeholder="编辑时非必填" />
|
||||
</div>
|
||||
<div class="col-span-2 sm:col-span-1">
|
||||
<label for="category" class="block mb-2 text-sm font-medium text-gray-900 ">状态</label>
|
||||
<select id="category" v-model="formData.enable"
|
||||
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 ">状态</label>
|
||||
<select id="status" v-model="formData.enable"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
|
||||
<option :value=true>启用</option>
|
||||
<option :value=false>禁用</option>
|
||||
|
||||
15
frontend/src/components/icons/LlmConfigIcon.vue
Normal file
15
frontend/src/components/icons/LlmConfigIcon.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<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-brain-icon lucide-brain">
|
||||
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
||||
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
||||
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
||||
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
||||
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
||||
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
|
||||
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
|
||||
<path d="M6 18a4 4 0 0 1-1.967-.516" />
|
||||
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
32
frontend/src/composables/ai/useLlmQuery.ts
Normal file
32
frontend/src/composables/ai/useLlmQuery.ts
Normal file
@@ -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<number>(0);
|
||||
const llms = ref<components["schemas"]["LlmVm"][]>([]);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
13
frontend/src/composables/ai/useLlmUpdate.ts
Normal file
13
frontend/src/composables/ai/useLlmUpdate.ts
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
<script setup lang="ts">
|
||||
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from "vue";
|
||||
import { z } from "zod";
|
||||
import Button from "../components/Button.vue";
|
||||
@@ -72,26 +72,24 @@ const chatContainer = ref<HTMLElement | null>(null);
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
|
||||
const renderMarkdown = (content: string) => {
|
||||
if (!content) return '';
|
||||
|
||||
const restoredContent = content
|
||||
.replace(/␣/g, ' ')
|
||||
.replace(/⇥/g, '\t')
|
||||
.replace(//g, '\n');
|
||||
|
||||
|
||||
const processedContent = restoredContent
|
||||
.replace(/^(\s*)(`{3,})/gm, '$1$2')
|
||||
.replace(/(\s+)`/g, '$1`');
|
||||
if (!content) return "";
|
||||
|
||||
const rawHtml = marked(processedContent);
|
||||
return DOMPurify.sanitize(rawHtml as string);
|
||||
const restoredContent = content
|
||||
.replace(/␣/g, " ")
|
||||
.replace(/⇥/g, "\t")
|
||||
.replace(//g, "\n");
|
||||
|
||||
const processedContent = restoredContent
|
||||
.replace(/^(\s*)(`{3,})/gm, "$1$2")
|
||||
.replace(/(\s+)`/g, "$1`");
|
||||
|
||||
const rawHtml = marked(processedContent);
|
||||
return DOMPurify.sanitize(rawHtml as string);
|
||||
};
|
||||
const chatElements = computed(() => {
|
||||
return messages.value.map((message, index) => {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</form>
|
||||
<!-- Create Modal toggle -->
|
||||
<button @click="handleUpsertDepartmentClick()"
|
||||
class="flex items-center block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center absolute right-5 bottom-2"
|
||||
class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center absolute right-5 bottom-2"
|
||||
type="button">
|
||||
新增部门
|
||||
</button>
|
||||
|
||||
166
frontend/src/views/LlmConfigView.vue
Normal file
166
frontend/src/views/LlmConfigView.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg mt-14">
|
||||
<div class="mb-4 col-span-full">
|
||||
<Breadcrumbs :names="['大模型管理']" />
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl ">大模型管理</h1>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<form class="max-w-xs mb-4 ">
|
||||
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only ">Search</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" id="default-search" v-model="name"
|
||||
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 "
|
||||
placeholder="模型名称" required />
|
||||
<button type="submit"
|
||||
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 "
|
||||
@click.prevent="handleSearch">搜索</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Create Modal toggle -->
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-x-auto">
|
||||
<table
|
||||
class="w-full whitespace-nowrap text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 overflow-x-auto">
|
||||
<thead class="text-xs uppercase bg-gray-50 ">
|
||||
<tr>
|
||||
<th scope="col" class="p-4">
|
||||
<div class="flex items-center">
|
||||
<input id="checkbox-all-search" disabled type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 ">
|
||||
<label for="checkbox-all-search" class="sr-only">checkbox</label>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">名称</th>
|
||||
<th scope="col" class="px-6 py-3">模型名称</th>
|
||||
<th scope="col" class="px-6 py-3">apiKey</th>
|
||||
<th scope="col" class="px-6 py-3">url</th>
|
||||
<th scope="col" class="px-6 py-3">状态</th>
|
||||
<th scope="col" class="px-6 py-3">优先级</th>
|
||||
<th scope="col" class="px-6 py-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="llm in llms" :key="llm.id" class="bg-white border-b border-gray-200 hover:bg-gray-50 ">
|
||||
<td class="w-4 p-4">
|
||||
<div class="flex items-center">
|
||||
<input :id="'checkbox-table-search-' + llm.id" type="checkbox" disabled
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 ">
|
||||
<label :for="'checkbox-table-search-' + llm.id" class="sr-only">checkbox</label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis font-medium text-gray-900">
|
||||
{{
|
||||
`${llm.name}` }}</td>
|
||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
||||
`${llm.modelName}` }}
|
||||
</td>
|
||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
||||
llm.apiKey }}
|
||||
</td>
|
||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{ llm.url }}</td>
|
||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
|
||||
<div class="flex items-center">
|
||||
<div class="h-2.5 w-2.5 rounded-full me-2" :class="llm.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
|
||||
llm.enable === true ? "启用" : "禁用" }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{ llm.priority }}
|
||||
</td>
|
||||
<td class="px-6 py-4 ">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<button @click="handleLlmUpdateClick(llm)"
|
||||
:class="['flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 ']"
|
||||
type="button">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
|
||||
<path fill-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<TablePagination :pageChange="handlePageChange" :total="total" />
|
||||
</div>
|
||||
|
||||
<LlmUpdateModal :llm="selectedLlm" :id="'llm-update-modal'" :closeModal="() => {
|
||||
llmUpdateModal!.hide();
|
||||
}" :onSubmit="handleUpdateModalSubmit"></LlmUpdateModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||
import TablePagination from "@/components/TablePagination.vue";
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import type { components } from "../api/types/schema";
|
||||
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
|
||||
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
|
||||
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
|
||||
|
||||
const llmUpdateModal = ref<ModalInterface>();
|
||||
const selectedLlm = ref<components["schemas"]["LlmVm"]>();
|
||||
const name = ref<string>("");
|
||||
|
||||
const { llms, fetchLlmConfigs, total } = useLlmQuery();
|
||||
const { updateLlmConfig } = useLlmUpdate();
|
||||
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
const handleUpdateModalSubmit = async (llm: components["schemas"]["LlmVm"]) => {
|
||||
await updateLlmConfig(llm);
|
||||
llmUpdateModal.value?.hide();
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "操作成功",
|
||||
});
|
||||
await fetchLlmConfigs();
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
await fetchLlmConfigs();
|
||||
};
|
||||
|
||||
const handlePageChange = async (page: number, pageSize: number) => {
|
||||
await fetchLlmConfigs(page, pageSize);
|
||||
};
|
||||
|
||||
const handleLlmUpdateClick = async (llm: components["schemas"]["LlmVm"]) => {
|
||||
selectedLlm.value = llm;
|
||||
await nextTick(() => {
|
||||
llmUpdateModal.value?.show();
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchLlmConfigs();
|
||||
initFlowbite();
|
||||
const $llmUpdateModalElement: HTMLElement | null =
|
||||
document.querySelector("#llm-update-modal");
|
||||
|
||||
llmUpdateModal.value = new Modal(
|
||||
$llmUpdateModalElement,
|
||||
{},
|
||||
{
|
||||
id: "llm-update-modal",
|
||||
},
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user