mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-20 09:23:42 +08:00
Merge branch 'dev'
This commit is contained in:
@@ -61,6 +61,7 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
||||
implementation("dev.langchain4j:langchain4j: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("io.projectreactor:reactor-core:3.7.6")
|
||||
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
|
||||
|
||||
@@ -14,17 +14,17 @@ import org.springframework.context.annotation.DependsOn;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class ChatModelConfig {
|
||||
public class ChatModelInitializer {
|
||||
|
||||
private final LlmService llmService;
|
||||
private final PromptConfiguration promptConfiguration;
|
||||
|
||||
@Bean
|
||||
@DependsOn("flywayInitializer")
|
||||
public ZhipuAiStreamingChatModel zhipuChatModel(ZhiPuConfiguration zhiPuConfiguration) {
|
||||
public ZhipuAiStreamingChatModel zhipuChatModel(ZhiPuChatModelConfig zhiPuChatModelConfig) {
|
||||
return ZhipuAiStreamingChatModel.builder()
|
||||
.model(zhiPuConfiguration.getModelName())
|
||||
.apiKey(zhiPuConfiguration.getApiKey())
|
||||
.model(zhiPuChatModelConfig.getModelName())
|
||||
.apiKey(zhiPuChatModelConfig.getApiKey())
|
||||
.logRequests(true)
|
||||
.logResponses(true)
|
||||
.build();
|
||||
@@ -32,11 +32,12 @@ public class ChatModelConfig {
|
||||
|
||||
@Bean
|
||||
@DependsOn("flywayInitializer")
|
||||
public OpenAiStreamingChatModel deepSeekChatModel(DeepSeekConfiguration deepSeekConfiguration) {
|
||||
public OpenAiStreamingChatModel deepSeekChatModel(
|
||||
DeepSeekChatModelConfig deepSeekChatModelConfig) {
|
||||
return OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(deepSeekConfiguration.getBaseUrl())
|
||||
.apiKey(deepSeekConfiguration.getApiKey())
|
||||
.modelName(deepSeekConfiguration.getModelName())
|
||||
.baseUrl(deepSeekChatModelConfig.getBaseUrl())
|
||||
.apiKey(deepSeekChatModelConfig.getApiKey())
|
||||
.modelName(deepSeekChatModelConfig.getModelName())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -62,19 +63,19 @@ public class ChatModelConfig {
|
||||
|
||||
@Bean
|
||||
@DependsOn("flywayInitializer")
|
||||
public DeepSeekConfiguration deepSeekConfiguration() {
|
||||
DeepSeekConfiguration deepSeekConfiguration = new DeepSeekConfiguration();
|
||||
public DeepSeekChatModelConfig deepSeekConfiguration() {
|
||||
DeepSeekChatModelConfig deepSeekChatModelConfig = new DeepSeekChatModelConfig();
|
||||
AiLlmConfig deepSeek = llmService.loadConfig(LlmCodeEnum.DEEP_SEEK);
|
||||
deepSeekConfiguration.init(deepSeek);
|
||||
return deepSeekConfiguration;
|
||||
deepSeekChatModelConfig.init(deepSeek);
|
||||
return deepSeekChatModelConfig;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("flywayInitializer")
|
||||
public ZhiPuConfiguration zhiPuConfiguration() {
|
||||
ZhiPuConfiguration zhiPuConfiguration = new ZhiPuConfiguration();
|
||||
public ZhiPuChatModelConfig zhiPuConfiguration() {
|
||||
ZhiPuChatModelConfig zhiPuChatModelConfig = new ZhiPuChatModelConfig();
|
||||
AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU);
|
||||
zhiPuConfiguration.init(aiLlmConfig);
|
||||
return zhiPuConfiguration;
|
||||
zhiPuChatModelConfig.init(aiLlmConfig);
|
||||
return zhiPuChatModelConfig;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@ package com.zl.mjga.config.ai;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
public class ZhiPuConfiguration {
|
||||
public class DeepSeekChatModelConfig {
|
||||
|
||||
private String baseUrl;
|
||||
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;
|
||||
|
||||
@Data
|
||||
public class DeepSeekConfiguration {
|
||||
public class ZhiPuChatModelConfig {
|
||||
|
||||
private String baseUrl;
|
||||
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.ai.LlmQueryDto;
|
||||
import com.zl.mjga.dto.ai.LlmVm;
|
||||
import com.zl.mjga.exception.BusinessException;
|
||||
import com.zl.mjga.service.AiChatService;
|
||||
import com.zl.mjga.service.EmbeddingService;
|
||||
import com.zl.mjga.service.LlmService;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import jakarta.validation.Valid;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -28,6 +33,7 @@ public class AiController {
|
||||
|
||||
private final AiChatService aiChatService;
|
||||
private final LlmService llmService;
|
||||
private final EmbeddingService embeddingService;
|
||||
|
||||
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<String> chat(Principal principal, @RequestBody String userMessage) {
|
||||
@@ -57,4 +63,13 @@ public class AiController {
|
||||
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto 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;
|
||||
|
||||
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 type;
|
||||
|
||||
@NotEmpty(message = "apikey 不能为空") private String apiKey;
|
||||
|
||||
@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.Record;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.generated.default_schema.enums.LlmTypeEnum;
|
||||
import org.jooq.generated.mjga.tables.daos.AiLlmConfigDao;
|
||||
import org.jooq.impl.DSL;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -25,12 +26,17 @@ public class LlmRepository extends AiLlmConfigDao {
|
||||
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) {
|
||||
return ctx()
|
||||
.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)
|
||||
.where(
|
||||
StringUtils.isNotEmpty(llmQueryDto.name())
|
||||
? AI_LLM_CONFIG.NAME.eq(llmQueryDto.name())
|
||||
: noCondition())
|
||||
.and(
|
||||
StringUtils.isNotEmpty(llmQueryDto.type())
|
||||
? AI_LLM_CONFIG.TYPE.eq(LlmTypeEnum.lookupLiteral(llmQueryDto.type()))
|
||||
: noCondition())
|
||||
.orderBy(pageRequestDto.getSortFields())
|
||||
.limit(pageRequestDto.getSize())
|
||||
.offset(pageRequestDto.getOffset())
|
||||
|
||||
@@ -28,12 +28,13 @@ public class AiChatService {
|
||||
}
|
||||
|
||||
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("没有开启的大模型"));
|
||||
LlmCodeEnum code = aiLlmConfig.getCode();
|
||||
return switch (code) {
|
||||
case ZHI_PU -> zhiPuChatAssistant.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 org.jooq.Record;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.generated.default_schema.enums.LlmTypeEnum;
|
||||
import org.jooq.generated.mjga.enums.LlmCodeEnum;
|
||||
import org.jooq.generated.mjga.tables.pojos.AiLlmConfig;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import static org.jooq.generated.mjga.Tables.AI_LLM_CONFIG;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@@ -28,9 +31,11 @@ public class LlmService {
|
||||
return llmRepository.fetchOneByCode(llmCodeEnum);
|
||||
}
|
||||
|
||||
public Optional<AiLlmConfig> getPrecedenceLlmBy(Boolean enable) {
|
||||
public Optional<AiLlmConfig> getPrecedenceChatLlmBy(Boolean 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(
|
||||
@@ -39,7 +44,11 @@ public class LlmService {
|
||||
if (records.isEmpty()) {
|
||||
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);
|
||||
return new PageResponseDto<>(totalLlm, llmVms);
|
||||
}
|
||||
|
||||
@@ -68,15 +68,21 @@ CREATE TABLE mjga.user_position_map (
|
||||
|
||||
CREATE TYPE mjga.llm_code_enum AS ENUM (
|
||||
'DEEP_SEEK',
|
||||
'ZHI_PU'
|
||||
'ZHI_PU',
|
||||
'ZHI_PU_EMBEDDING'
|
||||
);
|
||||
|
||||
CREATE TYPE "llm_type_enum" AS ENUM (
|
||||
'CHAT',
|
||||
'EMBEDDING'
|
||||
);
|
||||
|
||||
CREATE TABLE mjga.ai_llm_config (
|
||||
id BIGSERIAL 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,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
enable BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
@@ -33,7 +33,8 @@ VALUES (1, 1),
|
||||
(1, 9),
|
||||
(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
|
||||
('DeepSeek','DEEP_SEEK','deepseek-chat','your_api_key', 'https://api.deepseek.com', false, 0),
|
||||
('智谱清言','ZHI_PU','glm-4-flash', 'your_api_key', 'https://open.bigmodel.cn/', false, 1);
|
||||
('DeepSeek','DEEP_SEEK','deepseek-chat','CHAT','your_api_key', 'https://api.deepseek.com', false, 0),
|
||||
('智谱清言','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
|
||||
);
|
||||
|
||||
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 (
|
||||
id BIGSERIAL 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,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
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": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1096,6 +1129,7 @@
|
||||
"modelName",
|
||||
"name",
|
||||
"priority",
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object",
|
||||
@@ -1110,6 +1144,9 @@
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1788,6 +1825,9 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"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;
|
||||
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": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -529,6 +545,7 @@ export interface components {
|
||||
id: number;
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: string;
|
||||
apiKey: string;
|
||||
url: string;
|
||||
enable: boolean;
|
||||
@@ -759,6 +776,7 @@ export interface components {
|
||||
};
|
||||
LlmQueryDto: {
|
||||
name?: string;
|
||||
type?: string;
|
||||
};
|
||||
PageResponseDtoListLlmVm: {
|
||||
/** 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: {
|
||||
parameters: {
|
||||
query: {
|
||||
|
||||
@@ -90,19 +90,9 @@ const handleSubmit = async () => {
|
||||
.max(15, "部门名称最多15个字符"),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = schema.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;
|
||||
}
|
||||
const validatedData = schema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
};
|
||||
|
||||
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 "
|
||||
required />
|
||||
</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">
|
||||
<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"
|
||||
@@ -98,42 +106,35 @@ watch(() => llm, updateFormData, {
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
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: "优先级必须为数字",
|
||||
}),
|
||||
type: z.string({
|
||||
message: "类型不能为空",
|
||||
}),
|
||||
});
|
||||
const validatedData = llmSchema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import type { PermissionUpsertModel } from "@/types/permission";
|
||||
import { ref, watch } from "vue";
|
||||
import { z } from "zod";
|
||||
@@ -55,8 +54,6 @@ const { permission, onSubmit, closeModal } = defineProps<{
|
||||
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
|
||||
}>();
|
||||
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const updateFormData = (newPermission: typeof permission) => {
|
||||
@@ -86,18 +83,8 @@ const handleSubmit = async () => {
|
||||
.max(15, "权限代码最多15个字符"),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = permissionSchema.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;
|
||||
}
|
||||
const validatedData = permissionSchema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
<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"
|
||||
fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" 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" />
|
||||
<svg class="w-16 h-16 mx-auto text-red-600 mt-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 ">
|
||||
<h3 class="mb-3 text-lg font-normal text-gray-500 ">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<span>
|
||||
|
||||
@@ -78,19 +78,9 @@ const handleSubmit = async () => {
|
||||
.max(15, "岗位名称最多15个字符"),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = schema.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;
|
||||
}
|
||||
const validatedData = schema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -86,19 +86,9 @@ const handleSubmit = async () => {
|
||||
.max(15, "角色代码最多15个字符"),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = roleSchema.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;
|
||||
}
|
||||
const validatedData = roleSchema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -69,17 +69,7 @@ const handleSubmit = async () => {
|
||||
.min(5, "表达式的长度非法"),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = jobSchema.parse(formData.value);
|
||||
await onSubmit(validatedData.cronExpression);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
alertStore.showAlert({
|
||||
level: "error",
|
||||
content: error.errors[0].message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const validatedData = jobSchema.parse(formData.value);
|
||||
await onSubmit(validatedData.cronExpression);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -61,15 +61,12 @@
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import type { UserUpsertSubmitModel } from "@/types/user";
|
||||
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 { user, onSubmit } = defineProps<{
|
||||
user?: components["schemas"]["UserRolePermissionDto"];
|
||||
closeModal: () => void;
|
||||
@@ -130,19 +127,9 @@ const handleSubmit = async () => {
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const validatedData = userSchema.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;
|
||||
}
|
||||
const validatedData = userSchema.parse(formData.value);
|
||||
await onSubmit(validatedData);
|
||||
updateFormData(undefined);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
||||
import { ref } from "vue";
|
||||
import client from "../../api/client";
|
||||
import useAuthStore from "../store/useAuthStore";
|
||||
import useAlertStore from "../store/useAlertStore";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
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);
|
||||
|
||||
let currentController: AbortController | null = null;
|
||||
|
||||
const chat = async (message: string) => {
|
||||
isLoading.value = true;
|
||||
messages.value.push(message);
|
||||
messages.value.push("");
|
||||
const ctrl = new AbortController();
|
||||
currentController = ctrl;
|
||||
|
||||
messages.value.push({
|
||||
content: "",
|
||||
type: "chat",
|
||||
isUser: false,
|
||||
username: "知路智能体",
|
||||
});
|
||||
try {
|
||||
const baseUrl = `${import.meta.env.VITE_BASE_URL}`;
|
||||
await fetchEventSource(`${baseUrl}/ai/chat`, {
|
||||
@@ -29,7 +40,7 @@ export const useAiChat = () => {
|
||||
body: message,
|
||||
signal: ctrl.signal,
|
||||
onmessage(ev) {
|
||||
messages.value[messages.value.length - 1] += ev.data;
|
||||
messages.value[messages.value.length - 1].content += ev.data;
|
||||
},
|
||||
onclose() {
|
||||
console.log("onclose");
|
||||
@@ -38,6 +49,29 @@ export const useAiChat = () => {
|
||||
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 {
|
||||
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,
|
||||
UnAuthError,
|
||||
} from "../types/error";
|
||||
import { z } from "zod";
|
||||
|
||||
const makeErrorHandler =
|
||||
(
|
||||
@@ -21,7 +22,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);
|
||||
@@ -44,6 +45,16 @@ const makeErrorHandler =
|
||||
level: "error",
|
||||
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>
|
||||
<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">
|
||||
<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']">
|
||||
<img class="w-8 h-8 rounded-full" src="/trump.jpg" alt="Jese image">
|
||||
<div
|
||||
@@ -11,16 +11,39 @@
|
||||
<LoadingIcon :textColor="'text-gray-900'"
|
||||
v-if="isLoading && !chatElement.isUser && chatElement.content === ''" />
|
||||
</div>
|
||||
<div class="markdown-content markdown-body text-base font-normal py-2.5 text-gray-900 "
|
||||
v-html="renderMarkdown(chatElement.content)">
|
||||
<div>
|
||||
<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>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<form class="sticky bottom-4 mt-14">
|
||||
<div class="w-full border border-gray-200 rounded-lg bg-gray-50 ">
|
||||
<div class="px-4 py-2 bg-white rounded-t-lg ">
|
||||
<button @click.prevent="toggleMode"
|
||||
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>
|
||||
<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>
|
||||
@@ -52,31 +75,56 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<UserUpsertModal :id="'user-upsert-modal'" :onSubmit="handleUpsertUserSubmit" :closeModal="() => {
|
||||
userUpsertModal!.hide();
|
||||
}">
|
||||
</UserUpsertModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
|
||||
import useAlertStore from "@/composables/store/useAlertStore";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||
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 Button from "../components/Button.vue";
|
||||
import UserUpsertModal from "../components/UserUpsertModal.vue";
|
||||
import { useAiChat } from "../composables/ai/useAiChat";
|
||||
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 userUpsertModal = ref<ModalInterface>();
|
||||
const inputMessage = ref("");
|
||||
const chatContainer = ref<HTMLElement | null>(null);
|
||||
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({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
const renderMarkdown = (content: string) => {
|
||||
const renderMarkdown = (content: string | undefined) => {
|
||||
if (!content) return "";
|
||||
|
||||
const restoredContent = content
|
||||
@@ -91,23 +139,23 @@ const renderMarkdown = (content: string) => {
|
||||
const rawHtml = marked(processedContent);
|
||||
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) => {
|
||||
// console.log('原始消息:', newVal[newVal.length - 1]);
|
||||
// console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
|
||||
// }, { deep: true });
|
||||
|
||||
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
|
||||
await userUpsert.upsertUser(data);
|
||||
userUpsertModal.value?.hide();
|
||||
alertStore.showAlert({
|
||||
content: "操作成功",
|
||||
level: "success",
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
chatElements,
|
||||
messages,
|
||||
async () => {
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
@@ -125,38 +173,50 @@ const abortChat = () => {
|
||||
cancel();
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
try {
|
||||
const validInputMessage = z
|
||||
.string({ message: "消息不能为空" })
|
||||
.min(1, "消息不能为空")
|
||||
.parse(inputMessage.value);
|
||||
scrollToBottom();
|
||||
inputMessage.value = "";
|
||||
await chat(validInputMessage);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
alertStore.showAlert({
|
||||
level: "error",
|
||||
content: error.errors[0].message,
|
||||
});
|
||||
const chatByMode = async (message: string) => {
|
||||
inputMessage.value = "";
|
||||
messages.value.push({
|
||||
content: message,
|
||||
type: "chat",
|
||||
isUser: true,
|
||||
username: user.username!,
|
||||
});
|
||||
if (isCommandMode.value) {
|
||||
await actionChat(message);
|
||||
} else {
|
||||
if (isLoading.value) {
|
||||
abortChat();
|
||||
} else {
|
||||
throw error;
|
||||
await chat(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendClick = async () => {
|
||||
if (isLoading.value) {
|
||||
abortChat();
|
||||
} else {
|
||||
sendMessage();
|
||||
}
|
||||
scrollToBottom();
|
||||
const validInputMessage = z
|
||||
.string({ message: "消息不能为空" })
|
||||
.min(1, "消息不能为空")
|
||||
.parse(inputMessage.value);
|
||||
await chatByMode(validInputMessage);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
cancel();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
initFlowbite();
|
||||
const $upsertModalElement: HTMLElement | null =
|
||||
document.querySelector("#user-upsert-modal");
|
||||
userUpsertModal.value = new Modal(
|
||||
$upsertModalElement,
|
||||
{},
|
||||
{
|
||||
id: "user-upsert-modal",
|
||||
},
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
</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">url</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">{{
|
||||
`${llm.modelName}` }}
|
||||
</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">{{
|
||||
llm.apiKey }}
|
||||
</td>
|
||||
|
||||
@@ -45,26 +45,19 @@ const handleLogin = async () => {
|
||||
password: z.string().min(1, "密码至少1个字符"),
|
||||
});
|
||||
|
||||
try {
|
||||
const validatedData = userSchema.parse({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
await userAuth.signIn(validatedData.username, validatedData.password);
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "登录成功",
|
||||
});
|
||||
const redirectPath =
|
||||
(route.query.redirect as string) ||
|
||||
`${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}`;
|
||||
router.push(redirectPath);
|
||||
} catch (e) {
|
||||
alertStore.showAlert({
|
||||
level: "error",
|
||||
content: e instanceof z.ZodError ? e.errors[0].message : "账号或密码错误",
|
||||
});
|
||||
}
|
||||
const validatedData = userSchema.parse({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
await userAuth.signIn(validatedData.username, validatedData.password);
|
||||
alertStore.showAlert({
|
||||
level: "success",
|
||||
content: "登录成功",
|
||||
});
|
||||
const redirectPath =
|
||||
(route.query.redirect as string) ||
|
||||
`${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}`;
|
||||
router.push(redirectPath);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -78,47 +78,38 @@ onMounted(() => {
|
||||
|
||||
const handleUpdateClick = async () => {
|
||||
let validatedData = undefined;
|
||||
try {
|
||||
validatedData = z
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
message: "用户名不能为空",
|
||||
})
|
||||
.min(4, "用户名长度不能小于4个字符"),
|
||||
password: z
|
||||
.string()
|
||||
.min(5, "密码长度不能小于5个字符")
|
||||
.optional()
|
||||
.nullable(),
|
||||
confirmPassword: z.string().optional().nullable(),
|
||||
enable: z.boolean({
|
||||
message: "状态不能为空",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.password) {
|
||||
return data.password === data.confirmPassword;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: "密码输入不一致。" },
|
||||
)
|
||||
.parse(userForm.value);
|
||||
await upsertCurrentUser(validatedData);
|
||||
alertStore.showAlert({
|
||||
content: "操作成功",
|
||||
level: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
alertStore.showAlert({
|
||||
level: "error",
|
||||
content: error.errors[0].message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
validatedData = z
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
message: "用户名不能为空",
|
||||
})
|
||||
.min(4, "用户名长度不能小于4个字符"),
|
||||
password: z
|
||||
.string()
|
||||
.min(5, "密码长度不能小于5个字符")
|
||||
.optional()
|
||||
.nullable(),
|
||||
confirmPassword: z.string().optional().nullable(),
|
||||
enable: z.boolean({
|
||||
message: "状态不能为空",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.password) {
|
||||
return data.password === data.confirmPassword;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: "密码输入不一致。" },
|
||||
)
|
||||
.parse(userForm.value);
|
||||
await upsertCurrentUser(validatedData);
|
||||
alertStore.showAlert({
|
||||
content: "操作成功",
|
||||
level: "success",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -169,7 +169,7 @@ const router = useRouter();
|
||||
const { total, users, fetchUsersWith } = useUserQuery();
|
||||
const { deleteUser } = useUserDelete();
|
||||
const userUpsert = useUserUpsert();
|
||||
const { sortFields, sortBy, handleSort, getSortField } = useSort();
|
||||
const { sortBy, handleSort, getSortField } = useSort();
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
Reference in New Issue
Block a user