mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-04 11:17:32 +00:00
Merge branch 'dev'
This commit is contained in:
@@ -61,6 +61,7 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
||||||
implementation("dev.langchain4j:langchain4j:1.0.0")
|
implementation("dev.langchain4j:langchain4j:1.0.0")
|
||||||
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0")
|
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0")
|
||||||
|
implementation("dev.langchain4j:langchain4j-pgvector:1.0.1-beta6")
|
||||||
implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6")
|
implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6")
|
||||||
implementation("io.projectreactor:reactor-core:3.7.6")
|
implementation("io.projectreactor:reactor-core:3.7.6")
|
||||||
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
|
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ import org.springframework.context.annotation.DependsOn;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChatModelConfig {
|
public class ChatModelInitializer {
|
||||||
|
|
||||||
private final LlmService llmService;
|
private final LlmService llmService;
|
||||||
private final PromptConfiguration promptConfiguration;
|
private final PromptConfiguration promptConfiguration;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn("flywayInitializer")
|
@DependsOn("flywayInitializer")
|
||||||
public ZhipuAiStreamingChatModel zhipuChatModel(ZhiPuConfiguration zhiPuConfiguration) {
|
public ZhipuAiStreamingChatModel zhipuChatModel(ZhiPuChatModelConfig zhiPuChatModelConfig) {
|
||||||
return ZhipuAiStreamingChatModel.builder()
|
return ZhipuAiStreamingChatModel.builder()
|
||||||
.model(zhiPuConfiguration.getModelName())
|
.model(zhiPuChatModelConfig.getModelName())
|
||||||
.apiKey(zhiPuConfiguration.getApiKey())
|
.apiKey(zhiPuChatModelConfig.getApiKey())
|
||||||
.logRequests(true)
|
.logRequests(true)
|
||||||
.logResponses(true)
|
.logResponses(true)
|
||||||
.build();
|
.build();
|
||||||
@@ -32,11 +32,12 @@ public class ChatModelConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn("flywayInitializer")
|
@DependsOn("flywayInitializer")
|
||||||
public OpenAiStreamingChatModel deepSeekChatModel(DeepSeekConfiguration deepSeekConfiguration) {
|
public OpenAiStreamingChatModel deepSeekChatModel(
|
||||||
|
DeepSeekChatModelConfig deepSeekChatModelConfig) {
|
||||||
return OpenAiStreamingChatModel.builder()
|
return OpenAiStreamingChatModel.builder()
|
||||||
.baseUrl(deepSeekConfiguration.getBaseUrl())
|
.baseUrl(deepSeekChatModelConfig.getBaseUrl())
|
||||||
.apiKey(deepSeekConfiguration.getApiKey())
|
.apiKey(deepSeekChatModelConfig.getApiKey())
|
||||||
.modelName(deepSeekConfiguration.getModelName())
|
.modelName(deepSeekChatModelConfig.getModelName())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,19 +63,19 @@ public class ChatModelConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn("flywayInitializer")
|
@DependsOn("flywayInitializer")
|
||||||
public DeepSeekConfiguration deepSeekConfiguration() {
|
public DeepSeekChatModelConfig deepSeekConfiguration() {
|
||||||
DeepSeekConfiguration deepSeekConfiguration = new DeepSeekConfiguration();
|
DeepSeekChatModelConfig deepSeekChatModelConfig = new DeepSeekChatModelConfig();
|
||||||
AiLlmConfig deepSeek = llmService.loadConfig(LlmCodeEnum.DEEP_SEEK);
|
AiLlmConfig deepSeek = llmService.loadConfig(LlmCodeEnum.DEEP_SEEK);
|
||||||
deepSeekConfiguration.init(deepSeek);
|
deepSeekChatModelConfig.init(deepSeek);
|
||||||
return deepSeekConfiguration;
|
return deepSeekChatModelConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn("flywayInitializer")
|
@DependsOn("flywayInitializer")
|
||||||
public ZhiPuConfiguration zhiPuConfiguration() {
|
public ZhiPuChatModelConfig zhiPuConfiguration() {
|
||||||
ZhiPuConfiguration zhiPuConfiguration = new ZhiPuConfiguration();
|
ZhiPuChatModelConfig zhiPuChatModelConfig = new ZhiPuChatModelConfig();
|
||||||
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
|
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
|
||||||
zhiPuConfiguration.init(aiLlmConfig);
|
zhiPuChatModelConfig.init(aiLlmConfig);
|
||||||
return zhiPuConfiguration;
|
return zhiPuChatModelConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,9 @@ package com.zl.mjga.config.ai;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Component
|
public class DeepSeekChatModelConfig {
|
||||||
public class ZhiPuConfiguration {
|
|
||||||
|
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import com.zl.mjga.service.LlmService;
|
||||||
|
import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel;
|
||||||
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||||
|
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jooq.generated.mjga.enums.LlmCodeEnum;
|
||||||
|
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EmbeddingInitializer {
|
||||||
|
|
||||||
|
@Resource private Environment env;
|
||||||
|
private final LlmService llmService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@DependsOn("flywayInitializer")
|
||||||
|
public ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig() {
|
||||||
|
ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig = new ZhiPuEmbeddingModelConfig();
|
||||||
|
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU_EMBEDDING);
|
||||||
|
zhiPuEmbeddingModelConfig.init(aiLlmConfig);
|
||||||
|
return zhiPuEmbeddingModelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@DependsOn("flywayInitializer")
|
||||||
|
public EmbeddingModel zhipuEmbeddingModel(ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig) {
|
||||||
|
return ZhipuAiEmbeddingModel.builder()
|
||||||
|
.apiKey(zhiPuEmbeddingModelConfig.getApiKey())
|
||||||
|
.model(zhiPuEmbeddingModelConfig.getModelName())
|
||||||
|
.dimensions(2048)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public EmbeddingStore<TextSegment> zhiPuEmbeddingStore(EmbeddingModel zhipuEmbeddingModel) {
|
||||||
|
String hostPort = env.getProperty("DATABASE_HOST_PORT");
|
||||||
|
String host = hostPort.split(":")[0];
|
||||||
|
return PgVectorEmbeddingStore.builder()
|
||||||
|
.host(host)
|
||||||
|
.port(env.getProperty("DATABASE_EXPOSE_PORT", Integer.class))
|
||||||
|
.database(env.getProperty("DATABASE_DB"))
|
||||||
|
.user(env.getProperty("DATABASE_USER"))
|
||||||
|
.password(env.getProperty("DATABASE_PASSWORD"))
|
||||||
|
.table("mjga.zhipu_embedding_store")
|
||||||
|
.dimension(2048)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import lombok.Data;
|
|||||||
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class DeepSeekConfiguration {
|
public class ZhiPuChatModelConfig {
|
||||||
|
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.zl.mjga.config.ai;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ZhiPuEmbeddingModelConfig {
|
||||||
|
|
||||||
|
private String baseUrl;
|
||||||
|
private String apiKey;
|
||||||
|
private String modelName;
|
||||||
|
private Boolean enable;
|
||||||
|
|
||||||
|
public void init(AiLlmConfig config) {
|
||||||
|
this.baseUrl = config.getUrl();
|
||||||
|
this.apiKey = config.getApiKey();
|
||||||
|
this.modelName = config.getModelName();
|
||||||
|
this.enable = config.getEnable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,20 @@ import com.zl.mjga.dto.PageRequestDto;
|
|||||||
import com.zl.mjga.dto.PageResponseDto;
|
import com.zl.mjga.dto.PageResponseDto;
|
||||||
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.service.AiChatService;
|
import com.zl.mjga.service.AiChatService;
|
||||||
|
import com.zl.mjga.service.EmbeddingService;
|
||||||
import com.zl.mjga.service.LlmService;
|
import com.zl.mjga.service.LlmService;
|
||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jooq.generated.mjga.enums.LlmCodeEnum;
|
||||||
|
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -28,6 +33,7 @@ public class AiController {
|
|||||||
|
|
||||||
private final AiChatService aiChatService;
|
private final AiChatService aiChatService;
|
||||||
private final LlmService llmService;
|
private final LlmService llmService;
|
||||||
|
private final EmbeddingService embeddingService;
|
||||||
|
|
||||||
@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 String userMessage) {
|
||||||
@@ -57,4 +63,13 @@ public class AiController {
|
|||||||
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto llmQueryDto) {
|
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto llmQueryDto) {
|
||||||
return llmService.pageQueryLlm(pageRequestDto, llmQueryDto);
|
return llmService.pageQueryLlm(pageRequestDto, llmQueryDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/action/chat")
|
||||||
|
public Map<String, String> actionChat(@RequestBody String message) {
|
||||||
|
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
|
||||||
|
if (!aiLlmConfig.getEnable()) {
|
||||||
|
throw new BusinessException("命令模型未启用,请开启后再试。");
|
||||||
|
}
|
||||||
|
return embeddingService.searchAction(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package com.zl.mjga.dto.ai;
|
package com.zl.mjga.dto.ai;
|
||||||
|
|
||||||
public record LlmQueryDto(String name) {}
|
public record LlmQueryDto(String name, String type) {}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class LlmVm {
|
|||||||
|
|
||||||
@NotEmpty(message = "模型名称不能为空") private String modelName;
|
@NotEmpty(message = "模型名称不能为空") private String modelName;
|
||||||
|
|
||||||
|
@NotEmpty(message = "模型类型不能为空") private String type;
|
||||||
|
|
||||||
@NotEmpty(message = "apikey 不能为空") private String apiKey;
|
@NotEmpty(message = "apikey 不能为空") private String apiKey;
|
||||||
|
|
||||||
@NotEmpty(message = "url 不能为空") private String url;
|
@NotEmpty(message = "url 不能为空") private String url;
|
||||||
|
|||||||
14
backend/src/main/java/com/zl/mjga/model/urp/Actions.java
Normal file
14
backend/src/main/java/com/zl/mjga/model/urp/Actions.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zl.mjga.model.urp;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum Actions {
|
||||||
|
CREATE_USER("CREATE_USER", "创建新用户"),
|
||||||
|
DELETE_USER("DELETE_USER", "删除用户");
|
||||||
|
public static final String INDEX_KEY = "action";
|
||||||
|
private final String code;
|
||||||
|
private final String content;
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.jooq.Configuration;
|
import org.jooq.Configuration;
|
||||||
import org.jooq.Record;
|
import org.jooq.Record;
|
||||||
import org.jooq.Result;
|
import org.jooq.Result;
|
||||||
|
import org.jooq.generated.default_schema.enums.LlmTypeEnum;
|
||||||
import org.jooq.generated.mjga.tables.daos.AiLlmConfigDao;
|
import org.jooq.generated.mjga.tables.daos.AiLlmConfigDao;
|
||||||
import org.jooq.impl.DSL;
|
import org.jooq.impl.DSL;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -25,12 +26,17 @@ public class LlmRepository extends AiLlmConfigDao {
|
|||||||
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) {
|
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) {
|
||||||
return ctx()
|
return ctx()
|
||||||
.select(
|
.select(
|
||||||
AI_LLM_CONFIG.asterisk(), DSL.count().over().as("total_llm").convertFrom(Long::valueOf))
|
AI_LLM_CONFIG.asterisk(),
|
||||||
|
DSL.count().over().as("total_llm").convertFrom(Long::valueOf))
|
||||||
.from(AI_LLM_CONFIG)
|
.from(AI_LLM_CONFIG)
|
||||||
.where(
|
.where(
|
||||||
StringUtils.isNotEmpty(llmQueryDto.name())
|
StringUtils.isNotEmpty(llmQueryDto.name())
|
||||||
? AI_LLM_CONFIG.NAME.eq(llmQueryDto.name())
|
? AI_LLM_CONFIG.NAME.eq(llmQueryDto.name())
|
||||||
: noCondition())
|
: noCondition())
|
||||||
|
.and(
|
||||||
|
StringUtils.isNotEmpty(llmQueryDto.type())
|
||||||
|
? AI_LLM_CONFIG.TYPE.eq(LlmTypeEnum.lookupLiteral(llmQueryDto.type()))
|
||||||
|
: noCondition())
|
||||||
.orderBy(pageRequestDto.getSortFields())
|
.orderBy(pageRequestDto.getSortFields())
|
||||||
.limit(pageRequestDto.getSize())
|
.limit(pageRequestDto.getSize())
|
||||||
.offset(pageRequestDto.getOffset())
|
.offset(pageRequestDto.getOffset())
|
||||||
|
|||||||
@@ -28,12 +28,13 @@ public class AiChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) {
|
public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) {
|
||||||
Optional<AiLlmConfig> precedenceLlmBy = llmService.getPrecedenceLlmBy(true);
|
Optional<AiLlmConfig> precedenceLlmBy = llmService.getPrecedenceChatLlmBy(true);
|
||||||
AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型"));
|
AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型"));
|
||||||
LlmCodeEnum code = aiLlmConfig.getCode();
|
LlmCodeEnum code = aiLlmConfig.getCode();
|
||||||
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);
|
||||||
|
default -> throw new BusinessException(String.format("无效的模型代码 %s", code));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.zl.mjga.service;
|
||||||
|
|
||||||
|
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
|
||||||
|
|
||||||
|
import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig;
|
||||||
|
import com.zl.mjga.model.urp.Actions;
|
||||||
|
import dev.langchain4j.data.document.Metadata;
|
||||||
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||||
|
import dev.langchain4j.store.embedding.filter.Filter;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class EmbeddingService {
|
||||||
|
|
||||||
|
private final EmbeddingModel zhipuEmbeddingModel;
|
||||||
|
|
||||||
|
private final EmbeddingStore<TextSegment> zhiPuEmbeddingStore;
|
||||||
|
|
||||||
|
private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig;
|
||||||
|
|
||||||
|
public Map<String, String> searchAction(String message) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
EmbeddingSearchRequest embeddingSearchRequest =
|
||||||
|
EmbeddingSearchRequest.builder()
|
||||||
|
.queryEmbedding(zhipuEmbeddingModel.embed(message).content())
|
||||||
|
.build();
|
||||||
|
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
|
||||||
|
zhiPuEmbeddingStore.search(embeddingSearchRequest);
|
||||||
|
if (!embeddingSearchResult.matches().isEmpty()) {
|
||||||
|
Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata();
|
||||||
|
result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initActionIndex() {
|
||||||
|
if (!zhiPuEmbeddingModelConfig.getEnable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Actions action : Actions.values()) {
|
||||||
|
Embedding queryEmbedding = zhipuEmbeddingModel.embed(action.getContent()).content();
|
||||||
|
Filter createUserFilter = metadataKey(Actions.INDEX_KEY).isEqualTo(action.getCode());
|
||||||
|
EmbeddingSearchRequest embeddingSearchRequest =
|
||||||
|
EmbeddingSearchRequest.builder()
|
||||||
|
.queryEmbedding(queryEmbedding)
|
||||||
|
.filter(createUserFilter)
|
||||||
|
.build();
|
||||||
|
EmbeddingSearchResult<TextSegment> embeddingSearchResult =
|
||||||
|
zhiPuEmbeddingStore.search(embeddingSearchRequest);
|
||||||
|
if (embeddingSearchResult.matches().isEmpty()) {
|
||||||
|
TextSegment segment =
|
||||||
|
TextSegment.from(
|
||||||
|
action.getContent(), Metadata.metadata(Actions.INDEX_KEY, action.getCode()));
|
||||||
|
Embedding embedding = zhipuEmbeddingModel.embed(segment).content();
|
||||||
|
zhiPuEmbeddingStore.add(embedding, segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,14 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jooq.Record;
|
import org.jooq.Record;
|
||||||
import org.jooq.Result;
|
import org.jooq.Result;
|
||||||
|
import org.jooq.generated.default_schema.enums.LlmTypeEnum;
|
||||||
import org.jooq.generated.mjga.enums.LlmCodeEnum;
|
import org.jooq.generated.mjga.enums.LlmCodeEnum;
|
||||||
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import static org.jooq.generated.mjga.Tables.AI_LLM_CONFIG;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -28,9 +31,11 @@ public class LlmService {
|
|||||||
return llmRepository.fetchOneByCode(llmCodeEnum);
|
return llmRepository.fetchOneByCode(llmCodeEnum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<AiLlmConfig> getPrecedenceLlmBy(Boolean enable) {
|
public Optional<AiLlmConfig> getPrecedenceChatLlmBy(Boolean enable) {
|
||||||
List<AiLlmConfig> aiLlmConfigs = llmRepository.fetchByEnable(enable);
|
List<AiLlmConfig> aiLlmConfigs = llmRepository.fetchByEnable(enable);
|
||||||
return aiLlmConfigs.stream().max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority()));
|
return aiLlmConfigs.stream()
|
||||||
|
.filter(aiLlmConfig -> LlmTypeEnum.CHAT.equals(aiLlmConfig.getType()))
|
||||||
|
.max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResponseDto<List<LlmVm>> pageQueryLlm(
|
public PageResponseDto<List<LlmVm>> pageQueryLlm(
|
||||||
@@ -39,7 +44,11 @@ public class LlmService {
|
|||||||
if (records.isEmpty()) {
|
if (records.isEmpty()) {
|
||||||
return PageResponseDto.empty();
|
return PageResponseDto.empty();
|
||||||
}
|
}
|
||||||
List<LlmVm> llmVms = records.into(LlmVm.class);
|
List<LlmVm> llmVms = records.map((record) -> {
|
||||||
|
LlmVm into = record.into(LlmVm.class);
|
||||||
|
into.setType(record.get(AI_LLM_CONFIG.TYPE).getLiteral());
|
||||||
|
return into;
|
||||||
|
});
|
||||||
Long totalLlm = records.get(0).getValue("total_llm", Long.class);
|
Long totalLlm = records.get(0).getValue("total_llm", Long.class);
|
||||||
return new PageResponseDto<>(totalLlm, llmVms);
|
return new PageResponseDto<>(totalLlm, llmVms);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,15 +68,21 @@ CREATE TABLE mjga.user_position_map (
|
|||||||
|
|
||||||
CREATE TYPE mjga.llm_code_enum AS ENUM (
|
CREATE TYPE mjga.llm_code_enum AS ENUM (
|
||||||
'DEEP_SEEK',
|
'DEEP_SEEK',
|
||||||
'ZHI_PU'
|
'ZHI_PU',
|
||||||
|
'ZHI_PU_EMBEDDING'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE "llm_type_enum" AS ENUM (
|
||||||
|
'CHAT',
|
||||||
|
'EMBEDDING'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE mjga.ai_llm_config (
|
CREATE TABLE mjga.ai_llm_config (
|
||||||
id BIGSERIAL NOT NULL UNIQUE,
|
id BIGSERIAL NOT NULL UNIQUE,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
code mjga.llm_code_enum NOT NULL UNIQUE,
|
code mjga.llm_code_enum NOT NULL UNIQUE,
|
||||||
model_name VARCHAR(255) NOT NULL,
|
model_name VARCHAR(255) NOT NULL,
|
||||||
|
type LLM_TYPE_ENUM NOT NULL,
|
||||||
api_key VARCHAR(255) NOT NULL,
|
api_key VARCHAR(255) NOT NULL,
|
||||||
url VARCHAR(255) NOT NULL,
|
url VARCHAR(255) NOT NULL,
|
||||||
enable BOOLEAN NOT NULL DEFAULT true,
|
enable BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ VALUES (1, 1),
|
|||||||
(1, 9),
|
(1, 9),
|
||||||
(1, 10);
|
(1, 10);
|
||||||
|
|
||||||
INSERT INTO mjga.ai_llm_config (name,code,model_name, api_key, url, enable, priority)
|
INSERT INTO mjga.ai_llm_config (name,code,model_name, type, api_key, url, enable, priority)
|
||||||
VALUES
|
VALUES
|
||||||
('DeepSeek','DEEP_SEEK','deepseek-chat','your_api_key', 'https://api.deepseek.com', false, 0),
|
('DeepSeek','DEEP_SEEK','deepseek-chat','CHAT','your_api_key', 'https://api.deepseek.com', false, 0),
|
||||||
('智谱清言','ZHI_PU','glm-4-flash', 'your_api_key', 'https://open.bigmodel.cn/', false, 1);
|
('智谱清言','ZHI_PU','glm-4-flash','CHAT', 'your_api_key', 'https://open.bigmodel.cn/', false, 1),
|
||||||
|
('智谱清言向量','ZHI_PU_EMBEDDING','Embeddings-3','EMBEDDING', 'your_api_key', 'https://open.bigmodel.cn/', false, 0);
|
||||||
@@ -66,9 +66,23 @@ CREATE TABLE mjga.user_position_map (
|
|||||||
FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT
|
FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE mjga.llm_code_enum AS ENUM (
|
||||||
|
'DEEP_SEEK',
|
||||||
|
'ZHI_PU',
|
||||||
|
'ZHI_PU_EMBEDDING'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE "llm_type_enum" AS ENUM (
|
||||||
|
'CHAT',
|
||||||
|
'EMBEDDING'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE mjga.ai_llm_config (
|
CREATE TABLE mjga.ai_llm_config (
|
||||||
id BIGSERIAL NOT NULL UNIQUE,
|
id BIGSERIAL NOT NULL UNIQUE,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
code mjga.llm_code_enum NOT NULL UNIQUE,
|
||||||
|
model_name VARCHAR(255) NOT NULL,
|
||||||
|
type LLM_TYPE_ENUM NOT NULL,
|
||||||
api_key VARCHAR(255) NOT NULL,
|
api_key VARCHAR(255) NOT NULL,
|
||||||
url VARCHAR(255) NOT NULL,
|
url VARCHAR(255) NOT NULL,
|
||||||
enable BOOLEAN NOT NULL DEFAULT true,
|
enable BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|||||||
@@ -747,6 +747,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/ai/action/chat": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"ai-controller"
|
||||||
|
],
|
||||||
|
"operationId": "actionChat",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/scheduler/page-query": {
|
"/scheduler/page-query": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -1096,6 +1129,7 @@
|
|||||||
"modelName",
|
"modelName",
|
||||||
"name",
|
"name",
|
||||||
"priority",
|
"priority",
|
||||||
|
"type",
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1110,6 +1144,9 @@
|
|||||||
"modelName": {
|
"modelName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1788,6 +1825,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
44
frontend/src/api/types/schema.d.ts
vendored
44
frontend/src/api/types/schema.d.ts
vendored
@@ -372,6 +372,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/ai/action/chat": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["actionChat"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/scheduler/page-query": {
|
"/scheduler/page-query": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -529,6 +545,7 @@ export interface components {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
|
type: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
url: string;
|
url: string;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
@@ -759,6 +776,7 @@ export interface components {
|
|||||||
};
|
};
|
||||||
LlmQueryDto: {
|
LlmQueryDto: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
type?: string;
|
||||||
};
|
};
|
||||||
PageResponseDtoListLlmVm: {
|
PageResponseDtoListLlmVm: {
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
@@ -1446,6 +1464,32 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
actionChat: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
pageQuery: {
|
pageQuery: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -90,19 +90,9 @@ const handleSubmit = async () => {
|
|||||||
.max(15, "部门名称最多15个字符"),
|
.max(15, "部门名称最多15个字符"),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const validatedData = schema.parse(formData.value);
|
||||||
const validatedData = schema.parse(formData.value);
|
await onSubmit(validatedData);
|
||||||
await onSubmit(validatedData);
|
updateFormData(undefined);
|
||||||
updateFormData(undefined);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
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 "
|
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 />
|
required />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-span-2 sm:col-span-1">
|
||||||
|
<label for="type" class="block mb-2 text-sm font-medium text-gray-900 ">类型</label>
|
||||||
|
<select id="type" v-model="formData.type"
|
||||||
|
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="'CHAT'">聊天</option>
|
||||||
|
<option :value="'EMBEDDING'">嵌入</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label for="apiKey" class="block mb-2 text-sm font-medium autocomplete text-gray-900 ">apiKey</label>
|
<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"
|
<input type="text" id="apiKey" autocomplete="new-password" v-model="formData.apiKey"
|
||||||
@@ -98,42 +106,35 @@ watch(() => llm, updateFormData, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
const llmSchema = z.object({
|
||||||
const llmSchema = z.object({
|
id: z.number({
|
||||||
id: z.number({
|
message: "id不能为空",
|
||||||
message: "id不能为空",
|
}),
|
||||||
}),
|
name: z.string({
|
||||||
name: z.string({
|
message: "名称不能为空",
|
||||||
message: "名称不能为空",
|
}),
|
||||||
}),
|
modelName: z.string({
|
||||||
modelName: z.string({
|
message: "模型名称不能为空",
|
||||||
message: "模型名称不能为空",
|
}),
|
||||||
}),
|
apiKey: z.string({
|
||||||
apiKey: z.string({
|
message: "apiKey不能为空",
|
||||||
message: "apiKey不能为空",
|
}),
|
||||||
}),
|
url: z.string({
|
||||||
url: z.string({
|
message: "url不能为空",
|
||||||
message: "url不能为空",
|
}),
|
||||||
}),
|
enable: z.boolean({
|
||||||
enable: z.boolean({
|
message: "状态不能为空",
|
||||||
message: "状态不能为空",
|
}),
|
||||||
}),
|
priority: z.number({
|
||||||
priority: z.number({
|
message: "优先级必须为数字",
|
||||||
message: "优先级必须为数字",
|
}),
|
||||||
}),
|
type: z.string({
|
||||||
});
|
message: "类型不能为空",
|
||||||
const validatedData = llmSchema.parse(formData.value);
|
}),
|
||||||
await onSubmit(validatedData);
|
});
|
||||||
updateFormData(undefined);
|
const validatedData = llmSchema.parse(formData.value);
|
||||||
} catch (error) {
|
await onSubmit(validatedData);
|
||||||
if (error instanceof z.ZodError) {
|
updateFormData(undefined);
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
|
||||||
import type { PermissionUpsertModel } from "@/types/permission";
|
import type { PermissionUpsertModel } from "@/types/permission";
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -55,8 +54,6 @@ const { permission, onSubmit, closeModal } = defineProps<{
|
|||||||
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
|
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const alertStore = useAlertStore();
|
|
||||||
|
|
||||||
const formData = ref();
|
const formData = ref();
|
||||||
|
|
||||||
const updateFormData = (newPermission: typeof permission) => {
|
const updateFormData = (newPermission: typeof permission) => {
|
||||||
@@ -86,18 +83,8 @@ const handleSubmit = async () => {
|
|||||||
.max(15, "权限代码最多15个字符"),
|
.max(15, "权限代码最多15个字符"),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const validatedData = permissionSchema.parse(formData.value);
|
||||||
const validatedData = permissionSchema.parse(formData.value);
|
await onSubmit(validatedData);
|
||||||
await onSubmit(validatedData);
|
updateFormData(undefined);
|
||||||
updateFormData(undefined);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
<span class="sr-only">Close modal</span>
|
<span class="sr-only">Close modal</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="p-4 md:p-5 text-center flex flex-col items-center gap-y-3">
|
<div class="p-4 md:p-5 text-center flex flex-col items-center gap-y-3">
|
||||||
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
<svg class="w-16 h-16 mx-auto text-red-600 mt-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
fill="none" viewBox="0 0 20 20">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="mb-5 text-lg font-normal text-gray-500 ">
|
<h3 class="mb-3 text-lg font-normal text-gray-500 ">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -78,19 +78,9 @@ const handleSubmit = async () => {
|
|||||||
.max(15, "岗位名称最多15个字符"),
|
.max(15, "岗位名称最多15个字符"),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const validatedData = schema.parse(formData.value);
|
||||||
const validatedData = schema.parse(formData.value);
|
await onSubmit(validatedData);
|
||||||
await onSubmit(validatedData);
|
updateFormData(undefined);
|
||||||
updateFormData(undefined);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -86,19 +86,9 @@ const handleSubmit = async () => {
|
|||||||
.max(15, "角色代码最多15个字符"),
|
.max(15, "角色代码最多15个字符"),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const validatedData = roleSchema.parse(formData.value);
|
||||||
const validatedData = roleSchema.parse(formData.value);
|
await onSubmit(validatedData);
|
||||||
await onSubmit(validatedData);
|
updateFormData(undefined);
|
||||||
updateFormData(undefined);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -69,17 +69,7 @@ const handleSubmit = async () => {
|
|||||||
.min(5, "表达式的长度非法"),
|
.min(5, "表达式的长度非法"),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const validatedData = jobSchema.parse(formData.value);
|
||||||
const validatedData = jobSchema.parse(formData.value);
|
await onSubmit(validatedData.cronExpression);
|
||||||
await onSubmit(validatedData.cronExpression);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,15 +61,12 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
|
||||||
import type { UserUpsertSubmitModel } from "@/types/user";
|
import type { UserUpsertSubmitModel } from "@/types/user";
|
||||||
import { initFlowbite } from "flowbite";
|
import { initFlowbite } from "flowbite";
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { components } from "../api/types/schema";
|
import type { components } from "../api/types/schema";
|
||||||
|
|
||||||
const alertStore = useAlertStore();
|
|
||||||
|
|
||||||
const { user, onSubmit } = defineProps<{
|
const { user, onSubmit } = defineProps<{
|
||||||
user?: components["schemas"]["UserRolePermissionDto"];
|
user?: components["schemas"]["UserRolePermissionDto"];
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
@@ -130,19 +127,9 @@ const handleSubmit = async () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
const validatedData = userSchema.parse(formData.value);
|
||||||
const validatedData = userSchema.parse(formData.value);
|
await onSubmit(validatedData);
|
||||||
await onSubmit(validatedData);
|
updateFormData(undefined);
|
||||||
updateFormData(undefined);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import client from "../../api/client";
|
||||||
import useAuthStore from "../store/useAuthStore";
|
import useAuthStore from "../store/useAuthStore";
|
||||||
import useAlertStore from "../store/useAlertStore";
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
export const useAiChat = () => {
|
export const useAiChat = () => {
|
||||||
const messages = ref<string[]>([]);
|
const messages = ref<
|
||||||
|
{
|
||||||
|
content: string;
|
||||||
|
type: "chat" | "action";
|
||||||
|
isUser: boolean;
|
||||||
|
username: string;
|
||||||
|
command?: 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) => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
messages.value.push(message);
|
|
||||||
messages.value.push("");
|
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
currentController = ctrl;
|
currentController = ctrl;
|
||||||
|
messages.value.push({
|
||||||
|
content: "",
|
||||||
|
type: "chat",
|
||||||
|
isUser: false,
|
||||||
|
username: "知路智能体",
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
|
const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
|
||||||
await fetchEventSource(`${baseUrl}/ai/chat`, {
|
await fetchEventSource(`${baseUrl}/ai/chat`, {
|
||||||
@@ -29,7 +40,7 @@ export const useAiChat = () => {
|
|||||||
body: message,
|
body: message,
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
onmessage(ev) {
|
onmessage(ev) {
|
||||||
messages.value[messages.value.length - 1] += ev.data;
|
messages.value[messages.value.length - 1].content += ev.data;
|
||||||
},
|
},
|
||||||
onclose() {
|
onclose() {
|
||||||
console.log("onclose");
|
console.log("onclose");
|
||||||
@@ -38,6 +49,29 @@ export const useAiChat = () => {
|
|||||||
throw err;
|
throw err;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
messages.value.pop();
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionChat = async (message: string) => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await client.POST("/ai/action/chat", {
|
||||||
|
body: message,
|
||||||
|
});
|
||||||
|
messages.value.push({
|
||||||
|
content: data?.action
|
||||||
|
? "接收到指令,请您执行。"
|
||||||
|
: "未找到有效指令,请重新输入。",
|
||||||
|
type: "action",
|
||||||
|
isUser: false,
|
||||||
|
username: "知路智能体",
|
||||||
|
command: data?.action,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -50,5 +84,5 @@ export const useAiChat = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { messages, chat, isLoading, cancel };
|
return { messages, chat, isLoading, cancel, actionChat };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
RequestError,
|
RequestError,
|
||||||
UnAuthError,
|
UnAuthError,
|
||||||
} from "../types/error";
|
} from "../types/error";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const makeErrorHandler =
|
const makeErrorHandler =
|
||||||
(
|
(
|
||||||
@@ -21,7 +22,7 @@ const makeErrorHandler =
|
|||||||
}) => void,
|
}) => void,
|
||||||
) =>
|
) =>
|
||||||
(err: unknown, instance: ComponentPublicInstance | null, info: string) => {
|
(err: unknown, instance: ComponentPublicInstance | null, info: string) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
if (err instanceof UnAuthError) {
|
if (err instanceof UnAuthError) {
|
||||||
signOut();
|
signOut();
|
||||||
router.push(RoutePath.LOGIN);
|
router.push(RoutePath.LOGIN);
|
||||||
@@ -44,6 +45,16 @@ const makeErrorHandler =
|
|||||||
level: "error",
|
level: "error",
|
||||||
content: err.detail ?? err.message,
|
content: err.detail ?? err.message,
|
||||||
});
|
});
|
||||||
|
} else if (err instanceof z.ZodError) {
|
||||||
|
showAlert({
|
||||||
|
level: "error",
|
||||||
|
content: err.errors[0].message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showAlert({
|
||||||
|
level: "error",
|
||||||
|
content: "发生异常,请稍候再试",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col px-96 box-border pt-14 min-h-screen max-h-screen overflow-auto" ref="chatContainer">
|
<div class="flex flex-col px-96 box-border pt-14 min-h-screen max-h-screen overflow-auto" ref="chatContainer">
|
||||||
<div class="flex flex-col gap-y-5 flex-1 pt-14">
|
<div class="flex flex-col gap-y-5 flex-1 pt-14">
|
||||||
<li v-for="chatElement in chatElements" :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']">
|
||||||
<img class="w-8 h-8 rounded-full" src="/trump.jpg" alt="Jese image">
|
<img class="w-8 h-8 rounded-full" src="/trump.jpg" alt="Jese image">
|
||||||
<div
|
<div
|
||||||
@@ -11,16 +11,39 @@
|
|||||||
<LoadingIcon :textColor="'text-gray-900'"
|
<LoadingIcon :textColor="'text-gray-900'"
|
||||||
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
|
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
|
||||||
</div>
|
</div>
|
||||||
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 "
|
<div>
|
||||||
v-html="renderMarkdown(chatElement.content)">
|
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 "
|
||||||
|
v-html="renderMarkdown(chatElement.content)">
|
||||||
|
</div>
|
||||||
|
<button v-if="chatElement.type === 'action' && chatElement.command" type="button"
|
||||||
|
@click="commandActionMap[chatElement.command!]"
|
||||||
|
class="px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
{{
|
||||||
|
commandContentMap[chatElement.command!]
|
||||||
|
}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="sticky bottom-4 mt-14">
|
<form class="sticky bottom-4 mt-14">
|
||||||
<div class="w-full border border-gray-200 rounded-lg bg-gray-50 ">
|
<button @click.prevent="toggleMode"
|
||||||
<div class="px-4 py-2 bg-white rounded-t-lg ">
|
class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium text-gray-900 rounded-lg group focus:ring-4 focus:outline-none focus:ring-lime-200"
|
||||||
|
:class="[
|
||||||
|
isCommandMode
|
||||||
|
? 'bg-gradient-to-br from-teal-300 to-lime-300 '
|
||||||
|
: 'bg-gradient-to-br from-gray-300 to-gray-300 group-hover:from-teal-300 group-hover:to-lime-300'
|
||||||
|
]">
|
||||||
|
<span class="relative px-3 py-2 transition-all ease-in duration-75 rounded-md" :class="[
|
||||||
|
isCommandMode
|
||||||
|
? 'bg-transparent'
|
||||||
|
: 'bg-white group-hover:bg-transparent'
|
||||||
|
]">
|
||||||
|
命令模式
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="w-full border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<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>
|
||||||
<textarea id="comment" rows="3" v-model="inputMessage"
|
<textarea id="comment" rows="3" v-model="inputMessage"
|
||||||
class="w-full px-0 text-gray-900 bg-white border-0 focus:ring-0 " placeholder="发送消息" required></textarea>
|
class="w-full px-0 text-gray-900 bg-white border-0 focus:ring-0 " placeholder="发送消息" required></textarea>
|
||||||
@@ -52,31 +75,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<UserUpsertModal :id="'user-upsert-modal'" :onSubmit="handleUpsertUserSubmit" :closeModal="() => {
|
||||||
|
userUpsertModal!.hide();
|
||||||
|
}">
|
||||||
|
</UserUpsertModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
|
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { computed, nextTick, onUnmounted, ref, watch } from "vue";
|
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Button from "../components/Button.vue";
|
import Button from "../components/Button.vue";
|
||||||
|
import UserUpsertModal from "../components/UserUpsertModal.vue";
|
||||||
import { useAiChat } from "../composables/ai/useAiChat";
|
import { useAiChat } from "../composables/ai/useAiChat";
|
||||||
import useUserStore from "../composables/store/useUserStore";
|
import useUserStore from "../composables/store/useUserStore";
|
||||||
|
import { useUserUpsert } from "../composables/user/useUserUpsert";
|
||||||
|
import type { UserUpsertSubmitModel } from "../types/user";
|
||||||
|
|
||||||
const { messages, chat, isLoading, cancel } = useAiChat();
|
const { messages, chat, isLoading, cancel, actionChat } = useAiChat();
|
||||||
const { user } = useUserStore();
|
const { user } = useUserStore();
|
||||||
|
const userUpsertModal = ref<ModalInterface>();
|
||||||
const inputMessage = ref("");
|
const inputMessage = ref("");
|
||||||
const chatContainer = ref<HTMLElement | null>(null);
|
const chatContainer = ref<HTMLElement | null>(null);
|
||||||
const alertStore = useAlertStore();
|
const alertStore = useAlertStore();
|
||||||
|
const isCommandMode = ref(false);
|
||||||
|
const userUpsert = useUserUpsert();
|
||||||
|
|
||||||
|
const commandActionMap: Record<string, () => void> = {
|
||||||
|
CREATE_USER: () => {
|
||||||
|
userUpsertModal.value?.show();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandContentMap: Record<string, string> = {
|
||||||
|
CREATE_USER: "创建新用户",
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMode = () => {
|
||||||
|
isCommandMode.value = !isCommandMode.value;
|
||||||
|
};
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderMarkdown = (content: string) => {
|
const renderMarkdown = (content: string | undefined) => {
|
||||||
if (!content) return "";
|
if (!content) return "";
|
||||||
|
|
||||||
const restoredContent = content
|
const restoredContent = content
|
||||||
@@ -91,23 +139,23 @@ const renderMarkdown = (content: string) => {
|
|||||||
const rawHtml = marked(processedContent);
|
const rawHtml = marked(processedContent);
|
||||||
return DOMPurify.sanitize(rawHtml as string);
|
return DOMPurify.sanitize(rawHtml as string);
|
||||||
};
|
};
|
||||||
const chatElements = computed(() => {
|
|
||||||
return messages.value.map((message, index) => {
|
|
||||||
return {
|
|
||||||
content: message,
|
|
||||||
username: index % 2 === 0 ? user.username : "知路智能体",
|
|
||||||
isUser: index % 2 === 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// watch(messages, (newVal) => {
|
// watch(messages, (newVal) => {
|
||||||
// console.log('原始消息:', newVal[newVal.length - 1]);
|
// console.log('原始消息:', newVal[newVal.length - 1]);
|
||||||
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
|
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
|
||||||
// }, { deep: true });
|
// }, { deep: true });
|
||||||
|
|
||||||
|
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
|
||||||
|
await userUpsert.upsertUser(data);
|
||||||
|
userUpsertModal.value?.hide();
|
||||||
|
alertStore.showAlert({
|
||||||
|
content: "操作成功",
|
||||||
|
level: "success",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
chatElements,
|
messages,
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
@@ -125,38 +173,50 @@ const abortChat = () => {
|
|||||||
cancel();
|
cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const chatByMode = async (message: string) => {
|
||||||
try {
|
inputMessage.value = "";
|
||||||
const validInputMessage = z
|
messages.value.push({
|
||||||
.string({ message: "消息不能为空" })
|
content: message,
|
||||||
.min(1, "消息不能为空")
|
type: "chat",
|
||||||
.parse(inputMessage.value);
|
isUser: true,
|
||||||
scrollToBottom();
|
username: user.username!,
|
||||||
inputMessage.value = "";
|
});
|
||||||
await chat(validInputMessage);
|
if (isCommandMode.value) {
|
||||||
} catch (error) {
|
await actionChat(message);
|
||||||
if (error instanceof z.ZodError) {
|
} else {
|
||||||
alertStore.showAlert({
|
if (isLoading.value) {
|
||||||
level: "error",
|
abortChat();
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
await chat(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendClick = async () => {
|
const handleSendClick = async () => {
|
||||||
if (isLoading.value) {
|
scrollToBottom();
|
||||||
abortChat();
|
const validInputMessage = z
|
||||||
} else {
|
.string({ message: "消息不能为空" })
|
||||||
sendMessage();
|
.min(1, "消息不能为空")
|
||||||
}
|
.parse(inputMessage.value);
|
||||||
|
await chatByMode(validInputMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cancel();
|
cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
initFlowbite();
|
||||||
|
const $upsertModalElement: HTMLElement | null =
|
||||||
|
document.querySelector("#user-upsert-modal");
|
||||||
|
userUpsertModal.value = new Modal(
|
||||||
|
$upsertModalElement,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
id: "user-upsert-modal",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
</th>
|
</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>
|
<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">apiKey</th>
|
||||||
<th scope="col" class="px-6 py-3">url</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>
|
||||||
@@ -62,6 +63,9 @@
|
|||||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
||||||
`${llm.modelName}` }}
|
`${llm.modelName}` }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
||||||
|
llm.type === 'CHAT' ? '聊天' : '嵌入' }}
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">{{
|
||||||
llm.apiKey }}
|
llm.apiKey }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -45,26 +45,19 @@ const handleLogin = async () => {
|
|||||||
password: z.string().min(1, "密码至少1个字符"),
|
password: z.string().min(1, "密码至少1个字符"),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const validatedData = userSchema.parse({
|
||||||
const validatedData = userSchema.parse({
|
username: username.value,
|
||||||
username: username.value,
|
password: password.value,
|
||||||
password: password.value,
|
});
|
||||||
});
|
await userAuth.signIn(validatedData.username, validatedData.password);
|
||||||
await userAuth.signIn(validatedData.username, validatedData.password);
|
alertStore.showAlert({
|
||||||
alertStore.showAlert({
|
level: "success",
|
||||||
level: "success",
|
content: "登录成功",
|
||||||
content: "登录成功",
|
});
|
||||||
});
|
const redirectPath =
|
||||||
const redirectPath =
|
(route.query.redirect as string) ||
|
||||||
(route.query.redirect as string) ||
|
`${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}`;
|
||||||
`${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}`;
|
router.push(redirectPath);
|
||||||
router.push(redirectPath);
|
|
||||||
} catch (e) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: e instanceof z.ZodError ? e.errors[0].message : "账号或密码错误",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -78,47 +78,38 @@ onMounted(() => {
|
|||||||
|
|
||||||
const handleUpdateClick = async () => {
|
const handleUpdateClick = async () => {
|
||||||
let validatedData = undefined;
|
let validatedData = undefined;
|
||||||
try {
|
|
||||||
validatedData = z
|
validatedData = z
|
||||||
.object({
|
.object({
|
||||||
username: z
|
username: z
|
||||||
.string({
|
.string({
|
||||||
message: "用户名不能为空",
|
message: "用户名不能为空",
|
||||||
})
|
})
|
||||||
.min(4, "用户名长度不能小于4个字符"),
|
.min(4, "用户名长度不能小于4个字符"),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "密码长度不能小于5个字符")
|
.min(5, "密码长度不能小于5个字符")
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
confirmPassword: z.string().optional().nullable(),
|
confirmPassword: z.string().optional().nullable(),
|
||||||
enable: z.boolean({
|
enable: z.boolean({
|
||||||
message: "状态不能为空",
|
message: "状态不能为空",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
return data.password === data.confirmPassword;
|
return data.password === data.confirmPassword;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{ message: "密码输入不一致。" },
|
{ message: "密码输入不一致。" },
|
||||||
)
|
)
|
||||||
.parse(userForm.value);
|
.parse(userForm.value);
|
||||||
await upsertCurrentUser(validatedData);
|
await upsertCurrentUser(validatedData);
|
||||||
alertStore.showAlert({
|
alertStore.showAlert({
|
||||||
content: "操作成功",
|
content: "操作成功",
|
||||||
level: "success",
|
level: "success",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
alertStore.showAlert({
|
|
||||||
level: "error",
|
|
||||||
content: error.errors[0].message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const router = useRouter();
|
|||||||
const { total, users, fetchUsersWith } = useUserQuery();
|
const { total, users, fetchUsersWith } = useUserQuery();
|
||||||
const { deleteUser } = useUserDelete();
|
const { deleteUser } = useUserDelete();
|
||||||
const userUpsert = useUserUpsert();
|
const userUpsert = useUserUpsert();
|
||||||
const { sortFields, sortBy, handleSort, getSortField } = useSort();
|
const { sortBy, handleSort, getSortField } = useSort();
|
||||||
const alertStore = useAlertStore();
|
const alertStore = useAlertStore();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user