Merge branch 'dev'

This commit is contained in:
Chuck1sn
2025-05-26 12:02:33 +08:00
34 changed files with 594 additions and 264 deletions

View File

@@ -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")

View File

@@ -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;
} }
} }

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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);
}
} }

View File

@@ -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) {}

View File

@@ -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;

View 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;
}

View File

@@ -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())

View File

@@ -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));
}; };
} }
} }

View File

@@ -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);
}
}
}
}

View File

@@ -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);
} }

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,

View File

@@ -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"
} }
} }
}, },

View File

@@ -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: {

View File

@@ -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(() => {

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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 };
}; };

View File

@@ -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: "发生异常,请稍候再试",
});
} }
}; };

View File

@@ -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">

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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 () => {