feat(model-provider): 完成模型服务商路由与RAG向量接入后端实现

This commit is contained in:
2026-05-27 22:14:13 +08:00
parent cc745cad47
commit 5d7ca5b31f
57 changed files with 2922 additions and 1 deletions

View File

@@ -0,0 +1,222 @@
-- 模型平台与RAG模型绑定核心表首期手工维护后续可迁移到 Flyway/Liquibase
CREATE TABLE IF NOT EXISTS model_provider (
id BIGSERIAL PRIMARY KEY,
provider_code VARCHAR(64) NOT NULL,
provider_name VARCHAR(100) NOT NULL,
provider_type VARCHAR(50) NOT NULL,
protocol_type VARCHAR(50) NOT NULL DEFAULT 'OPENAI_COMPATIBLE',
base_url VARCHAR(500) NOT NULL,
auth_type VARCHAR(50) NOT NULL DEFAULT 'BEARER_TOKEN',
secret_ref VARCHAR(200),
api_key_cipher TEXT,
timeout_ms INTEGER NOT NULL DEFAULT 60000,
priority INTEGER NOT NULL DEFAULT 100,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
health_status VARCHAR(50) NOT NULL DEFAULT 'UNKNOWN',
last_health_check_time TIMESTAMP,
version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP,
update_time TIMESTAMP,
remark VARCHAR(500) DEFAULT '',
create_by VARCHAR(64),
update_by VARCHAR(64),
CONSTRAINT uk_model_provider_code UNIQUE (provider_code)
);
CREATE TABLE IF NOT EXISTS model_config (
id BIGSERIAL PRIMARY KEY,
provider_id BIGINT NOT NULL,
model_code VARCHAR(100) NOT NULL,
model_name VARCHAR(200) NOT NULL,
upstream_model VARCHAR(200) NOT NULL,
model_type VARCHAR(50) NOT NULL,
context_window INTEGER,
max_output_tokens INTEGER,
embedding_dimension INTEGER,
input_price_per_1k NUMERIC(12, 8),
output_price_per_1k NUMERIC(12, 8),
local_model BOOLEAN NOT NULL DEFAULT FALSE,
default_model BOOLEAN NOT NULL DEFAULT FALSE,
capabilities_json JSONB NOT NULL DEFAULT '{}'::jsonb,
options_json JSONB NOT NULL DEFAULT '{}'::jsonb,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP,
update_time TIMESTAMP,
remark VARCHAR(500) DEFAULT '',
create_by VARCHAR(64),
update_by VARCHAR(64),
CONSTRAINT uk_model_config_provider_code UNIQUE (provider_id, model_code),
CONSTRAINT fk_model_config_provider_id FOREIGN KEY (provider_id) REFERENCES model_provider (id)
);
CREATE TABLE IF NOT EXISTS model_route_rule (
id BIGSERIAL PRIMARY KEY,
route_code VARCHAR(100) NOT NULL,
route_name VARCHAR(100) NOT NULL,
task_type VARCHAR(50) NOT NULL,
match_scope VARCHAR(50) NOT NULL DEFAULT 'GLOBAL',
scope_id BIGINT,
primary_model_id BIGINT NOT NULL,
fallback_model_ids_json JSONB NOT NULL DEFAULT '[]'::jsonb,
route_strategy VARCHAR(50) NOT NULL DEFAULT 'MANUAL',
max_latency_ms INTEGER,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP,
update_time TIMESTAMP,
remark VARCHAR(500) DEFAULT '',
create_by VARCHAR(64),
update_by VARCHAR(64),
CONSTRAINT uk_model_route_rule_code UNIQUE (route_code),
CONSTRAINT fk_model_route_primary_model_id FOREIGN KEY (primary_model_id) REFERENCES model_config (id)
);
CREATE TABLE IF NOT EXISTS rag_store_model_config (
id BIGSERIAL PRIMARY KEY,
store_id BIGINT NOT NULL,
embedding_model_id BIGINT NOT NULL,
embedding_dimension INTEGER NOT NULL DEFAULT 1024,
chunk_strategy INTEGER,
chunk_size INTEGER,
chunk_overlap INTEGER,
delimiter VARCHAR(50),
active BOOLEAN NOT NULL DEFAULT TRUE,
index_version INTEGER NOT NULL DEFAULT 1,
version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP,
update_time TIMESTAMP,
remark VARCHAR(500) DEFAULT '',
create_by VARCHAR(64),
update_by VARCHAR(64),
CONSTRAINT uk_rag_store_model_config_store_active UNIQUE (store_id, active),
CONSTRAINT fk_rag_store_model_config_embedding_model_id FOREIGN KEY (embedding_model_id) REFERENCES model_config (id)
);
CREATE TABLE IF NOT EXISTS model_call_log (
id BIGSERIAL PRIMARY KEY,
request_id VARCHAR(64) NOT NULL,
provider_id BIGINT,
model_id BIGINT,
task_type VARCHAR(50) NOT NULL,
biz_type VARCHAR(50),
biz_id VARCHAR(100),
call_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL,
prompt_tokens INTEGER,
completion_tokens INTEGER,
total_tokens INTEGER,
estimated_cost NUMERIC(14, 8),
duration_ms INTEGER,
request_hash VARCHAR(64),
error_code VARCHAR(100),
error_message VARCHAR(1000),
create_time TIMESTAMP,
remark VARCHAR(500) DEFAULT '',
CONSTRAINT uk_model_call_log_request_id UNIQUE (request_id)
);
COMMENT ON TABLE model_provider IS '模型服务商配置表';
COMMENT ON COLUMN model_provider.id IS 'ID';
COMMENT ON COLUMN model_provider.provider_code IS '服务商编码';
COMMENT ON COLUMN model_provider.provider_name IS '服务商名称';
COMMENT ON COLUMN model_provider.provider_type IS '服务商类型';
COMMENT ON COLUMN model_provider.protocol_type IS '协议类型';
COMMENT ON COLUMN model_provider.base_url IS '服务基础地址';
COMMENT ON COLUMN model_provider.auth_type IS '鉴权类型';
COMMENT ON COLUMN model_provider.secret_ref IS '密钥环境变量引用';
COMMENT ON COLUMN model_provider.api_key_cipher IS '密钥密文';
COMMENT ON COLUMN model_provider.timeout_ms IS '超时时间(毫秒)';
COMMENT ON COLUMN model_provider.priority IS '优先级';
COMMENT ON COLUMN model_provider.enabled IS '是否启用';
COMMENT ON COLUMN model_provider.health_status IS '健康状态';
COMMENT ON COLUMN model_provider.last_health_check_time IS '最近健康检查时间';
COMMENT ON COLUMN model_provider.version IS '版本';
COMMENT ON COLUMN model_provider.create_time IS '创建时间';
COMMENT ON COLUMN model_provider.update_time IS '更新时间';
COMMENT ON COLUMN model_provider.remark IS '备注';
COMMENT ON COLUMN model_provider.create_by IS '创建者';
COMMENT ON COLUMN model_provider.update_by IS '更新者';
COMMENT ON TABLE model_config IS '模型配置表';
COMMENT ON COLUMN model_config.id IS 'ID';
COMMENT ON COLUMN model_config.provider_id IS '服务商ID';
COMMENT ON COLUMN model_config.model_code IS '模型编码';
COMMENT ON COLUMN model_config.model_name IS '模型名称';
COMMENT ON COLUMN model_config.upstream_model IS '上游模型名称';
COMMENT ON COLUMN model_config.model_type IS '模型类型';
COMMENT ON COLUMN model_config.context_window IS '上下文窗口大小';
COMMENT ON COLUMN model_config.max_output_tokens IS '最大输出Token数';
COMMENT ON COLUMN model_config.embedding_dimension IS '向量维度';
COMMENT ON COLUMN model_config.input_price_per_1k IS '输入千Token单价';
COMMENT ON COLUMN model_config.output_price_per_1k IS '输出千Token单价';
COMMENT ON COLUMN model_config.local_model IS '是否本地模型';
COMMENT ON COLUMN model_config.default_model IS '是否默认模型';
COMMENT ON COLUMN model_config.capabilities_json IS '能力配置JSON';
COMMENT ON COLUMN model_config.options_json IS '扩展选项JSON';
COMMENT ON COLUMN model_config.enabled IS '是否启用';
COMMENT ON COLUMN model_config.version IS '版本';
COMMENT ON COLUMN model_config.create_time IS '创建时间';
COMMENT ON COLUMN model_config.update_time IS '更新时间';
COMMENT ON COLUMN model_config.remark IS '备注';
COMMENT ON COLUMN model_config.create_by IS '创建者';
COMMENT ON COLUMN model_config.update_by IS '更新者';
COMMENT ON TABLE model_route_rule IS '模型路由规则表';
COMMENT ON COLUMN model_route_rule.id IS 'ID';
COMMENT ON COLUMN model_route_rule.route_code IS '路由规则编码';
COMMENT ON COLUMN model_route_rule.route_name IS '路由规则名称';
COMMENT ON COLUMN model_route_rule.task_type IS '任务类型';
COMMENT ON COLUMN model_route_rule.match_scope IS '匹配范围';
COMMENT ON COLUMN model_route_rule.scope_id IS '匹配范围业务ID';
COMMENT ON COLUMN model_route_rule.primary_model_id IS '主模型ID';
COMMENT ON COLUMN model_route_rule.fallback_model_ids_json IS '降级模型ID列表JSON';
COMMENT ON COLUMN model_route_rule.route_strategy IS '路由策略';
COMMENT ON COLUMN model_route_rule.max_latency_ms IS '最大延迟限制(毫秒)';
COMMENT ON COLUMN model_route_rule.enabled IS '是否启用';
COMMENT ON COLUMN model_route_rule.version IS '版本';
COMMENT ON COLUMN model_route_rule.create_time IS '创建时间';
COMMENT ON COLUMN model_route_rule.update_time IS '更新时间';
COMMENT ON COLUMN model_route_rule.remark IS '备注';
COMMENT ON COLUMN model_route_rule.create_by IS '创建者';
COMMENT ON COLUMN model_route_rule.update_by IS '更新者';
COMMENT ON TABLE rag_store_model_config IS '知识库模型配置表';
COMMENT ON COLUMN rag_store_model_config.id IS 'ID';
COMMENT ON COLUMN rag_store_model_config.store_id IS '知识库ID';
COMMENT ON COLUMN rag_store_model_config.embedding_model_id IS 'Embedding模型ID';
COMMENT ON COLUMN rag_store_model_config.embedding_dimension IS '向量维度';
COMMENT ON COLUMN rag_store_model_config.chunk_strategy IS '切片策略';
COMMENT ON COLUMN rag_store_model_config.chunk_size IS '切片大小';
COMMENT ON COLUMN rag_store_model_config.chunk_overlap IS '切片重叠大小';
COMMENT ON COLUMN rag_store_model_config.delimiter IS '切片分隔符';
COMMENT ON COLUMN rag_store_model_config.active IS '是否生效';
COMMENT ON COLUMN rag_store_model_config.index_version IS '索引版本号';
COMMENT ON COLUMN rag_store_model_config.version IS '版本';
COMMENT ON COLUMN rag_store_model_config.create_time IS '创建时间';
COMMENT ON COLUMN rag_store_model_config.update_time IS '更新时间';
COMMENT ON COLUMN rag_store_model_config.remark IS '备注';
COMMENT ON COLUMN rag_store_model_config.create_by IS '创建者';
COMMENT ON COLUMN rag_store_model_config.update_by IS '更新者';
COMMENT ON TABLE model_call_log IS '模型调用日志表';
COMMENT ON COLUMN model_call_log.id IS 'ID';
COMMENT ON COLUMN model_call_log.request_id IS '请求唯一ID';
COMMENT ON COLUMN model_call_log.provider_id IS '服务商ID';
COMMENT ON COLUMN model_call_log.model_id IS '模型ID';
COMMENT ON COLUMN model_call_log.task_type IS '任务类型';
COMMENT ON COLUMN model_call_log.biz_type IS '业务类型';
COMMENT ON COLUMN model_call_log.biz_id IS '业务ID';
COMMENT ON COLUMN model_call_log.call_type IS '调用类型';
COMMENT ON COLUMN model_call_log.status IS '调用状态';
COMMENT ON COLUMN model_call_log.prompt_tokens IS '输入Token数';
COMMENT ON COLUMN model_call_log.completion_tokens IS '输出Token数';
COMMENT ON COLUMN model_call_log.total_tokens IS '总Token数';
COMMENT ON COLUMN model_call_log.estimated_cost IS '预估成本';
COMMENT ON COLUMN model_call_log.duration_ms IS '耗时(毫秒)';
COMMENT ON COLUMN model_call_log.request_hash IS '请求哈希';
COMMENT ON COLUMN model_call_log.error_code IS '错误码';
COMMENT ON COLUMN model_call_log.error_message IS '错误信息摘要';
COMMENT ON COLUMN model_call_log.create_time IS '创建时间';
COMMENT ON COLUMN model_call_log.remark IS '备注';

View File

@@ -0,0 +1,23 @@
package com.bruce.modelprovider.client;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.ModelProvider;
import java.util.List;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface OpenAiCompatibleModelClient {
/**
* 方法 embeddings用于定义接口能力契约。
*/
List<List<Double>> embeddings(ModelProvider provider, ModelConfig model, List<String> texts, Integer expectedDimension);
/**
* 方法 health用于定义接口能力契约。
*/
boolean health(ModelProvider provider);
}

View File

@@ -0,0 +1,112 @@
package com.bruce.modelprovider.client;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.ModelProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* OpenAI-compatible 客户端实现。
* <p>
* 说明:
* 1. 统一使用服务商 baseUrl 访问上游接口;
* 2. Embedding 调用使用 `/embeddings`
* 3. 健康检查优先使用轻量接口 `/models`
* 4. API Key 从 `secretRef` 对应环境变量读取,不在代码中硬编码。
*/
@Component
/**
* OpenAiCompatibleModelClientImpl负责模型平台对应层的职责。
*/
public class OpenAiCompatibleModelClientImpl implements OpenAiCompatibleModelClient {
/**
* 调用上游 Embedding 接口并解析向量数组。
*/
@Override
@SuppressWarnings("unchecked")
/**
* 方法 embeddings用于执行业务逻辑处理。
*/
public List<List<Double>> embeddings(ModelProvider provider, ModelConfig model, List<String> texts, Integer expectedDimension) {
RestClient client = RestClient.builder().baseUrl(provider.getBaseUrl()).build();
Map<String, Object> body = new HashMap<>();
body.put("model", model.getUpstreamModel());
body.put("input", texts);
if (expectedDimension != null) {
body.put("dimensions", expectedDimension);
}
RestClient.RequestBodySpec request = client.post().uri("/embeddings")
.contentType(MediaType.APPLICATION_JSON)
.body(body);
String apiKey = resolveApiKey(provider);
if (apiKey != null) {
request = request.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey);
}
Map<String, Object> response = request.retrieve().body(Map.class);
if (response == null || !(response.get("data") instanceof List<?> dataList)) {
throw new IllegalStateException("上游Embedding响应缺少data字段");
}
List<List<Double>> vectors = new ArrayList<>();
for (Object item : dataList) {
if (!(item instanceof Map<?, ?> itemMap)) {
continue;
}
Object embedding = itemMap.get("embedding");
if (!(embedding instanceof List<?> vectorValues)) {
continue;
}
List<Double> vector = new ArrayList<>();
for (Object value : vectorValues) {
vector.add(Double.parseDouble(String.valueOf(value)));
}
vectors.add(vector);
}
return vectors;
}
/**
* 调用 `/models` 做健康探测:成功返回 true异常返回 false。
*/
@Override
/**
* 方法 health用于执行业务逻辑处理。
*/
public boolean health(ModelProvider provider) {
try {
RestClient client = RestClient.builder().baseUrl(provider.getBaseUrl()).build();
RestClient.RequestHeadersSpec<?> request = client.get().uri("/models");
String apiKey = resolveApiKey(provider);
if (apiKey != null) {
request = request.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey);
}
request.retrieve().toBodilessEntity();
return true;
} catch (Exception ex) {
return false;
}
}
/**
* 读取服务商密钥:
* 有 secretRef 时从环境变量读取;首期不使用数据库密钥明文。
*/
private String resolveApiKey(ModelProvider provider) {
if (provider.getSecretRef() != null && !provider.getSecretRef().isBlank()) {
return System.getenv(provider.getSecretRef().trim());
}
return null;
}
}

View File

@@ -0,0 +1,40 @@
package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.service.IModelCallLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/model/call-logs")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelCallLogController {
private final IModelCallLogService modelCallLogService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelCallLogResponse>> query(@RequestBody(required = false) ModelCallLogQueryRequest request) {
return RequestResult.success(modelCallLogService.query(request));
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelCallLogResponse> detail(@RequestParam("id") Long id) {
return RequestResult.success(ModelCallLogResponse.fromEntity(modelCallLogService.getById(id)));
}
}

View File

@@ -0,0 +1,56 @@
package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.service.IModelConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/model/configs")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelConfigController {
private final IModelConfigService modelConfigService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelConfigResponse>> query() {
return RequestResult.success(modelConfigService.listResponses());
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelConfigResponse> detail(@RequestParam("id") Long id) {
return RequestResult.success(modelConfigService.getResponseById(id));
}
@PostMapping("/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody ModelConfigSaveRequest request) {
return RequestResult.success(modelConfigService.saveOrUpdate(request));
}
@PostMapping("/delete")
/**
* 方法 delete用于执行业务逻辑处理。
*/
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
return RequestResult.success(modelConfigService.removeById(id));
}
}

View File

@@ -0,0 +1,64 @@
package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.service.IModelProviderService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/model/providers")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelProviderController {
private final IModelProviderService modelProviderService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelProviderResponse>> query() {
return RequestResult.success(modelProviderService.listResponses());
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelProviderResponse> detail(@RequestParam("id") Long id) {
return RequestResult.success(modelProviderService.getResponseById(id));
}
@PostMapping("/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody ModelProviderSaveRequest request) {
return RequestResult.success(modelProviderService.saveOrUpdate(request));
}
@PostMapping("/delete")
/**
* 方法 delete用于执行业务逻辑处理。
*/
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
return RequestResult.success(modelProviderService.removeById(id));
}
@PostMapping("/checkHealth")
/**
* 方法 checkHealth用于执行业务逻辑处理。
*/
public RequestResult<Boolean> checkHealth(@RequestParam("id") Long id) {
return RequestResult.success(modelProviderService.checkHealth(id));
}
}

View File

@@ -0,0 +1,56 @@
package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/model/routes")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteRuleController {
private final IModelRouteRuleService modelRouteRuleService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelRouteRuleResponse>> query() {
return RequestResult.success(modelRouteRuleService.listResponses());
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelRouteRuleResponse> detail(@RequestParam("id") Long id) {
return RequestResult.success(modelRouteRuleService.getResponseById(id));
}
@PostMapping("/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody ModelRouteRuleSaveRequest request) {
return RequestResult.success(modelRouteRuleService.saveOrUpdate(request));
}
@PostMapping("/delete")
/**
* 方法 delete用于执行业务逻辑处理。
*/
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
return RequestResult.success(modelRouteRuleService.removeById(id));
}
}

View File

@@ -0,0 +1,46 @@
package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.RagStoreModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/rag/store")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class RagStoreModelConfigController {
private final IRagStoreModelConfigService ragStoreModelConfigService;
@GetMapping("/modelConfig")
/**
* 方法 modelConfig用于执行业务逻辑处理。
*/
public RequestResult<RagStoreModelConfigResponse> modelConfig(@RequestParam("storeId") Long storeId) {
return RequestResult.success(ragStoreModelConfigService.getByStoreId(storeId));
}
@PostMapping("/modelConfig/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody RagStoreModelConfigSaveRequest request) {
return RequestResult.success(ragStoreModelConfigService.saveOrUpdate(request));
}
@PostMapping("/rebuildIndex")
/**
* 方法 rebuildIndex用于执行业务逻辑处理。
*/
public RequestResult<Boolean> rebuildIndex(@RequestParam("storeId") Long storeId) {
return RequestResult.success(storeId != null);
}
}

View File

@@ -0,0 +1,18 @@
package com.bruce.modelprovider.dto.request;
import lombok.Data;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelCallLogQueryRequest {
private String taskType;
private Long providerId;
private Long modelId;
private String status;
private String bizType;
}

View File

@@ -0,0 +1,25 @@
package com.bruce.modelprovider.dto.request;
import lombok.Data;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelConfigSaveRequest {
private Long id;
private Long providerId;
private String modelCode;
private String modelName;
private String upstreamModel;
private String modelType;
private Integer embeddingDimension;
private Boolean localModel;
private Boolean defaultModel;
private String optionsJson;
private Boolean enabled;
private String remark;
}

View File

@@ -0,0 +1,26 @@
package com.bruce.modelprovider.dto.request;
import lombok.Data;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelProviderSaveRequest {
private Long id;
private String providerCode;
private String providerName;
private String providerType;
private String protocolType;
private String baseUrl;
private String authType;
private String secretRef;
private String apiKeyCipher;
private Integer timeoutMs;
private Integer priority;
private Boolean enabled;
private String remark;
}

View File

@@ -0,0 +1,25 @@
package com.bruce.modelprovider.dto.request;
import lombok.Data;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteRuleSaveRequest {
private Long id;
private String routeCode;
private String routeName;
private String taskType;
private String matchScope;
private Long scopeId;
private Long primaryModelId;
private String fallbackModelIdsJson;
private String routeStrategy;
private Integer maxLatencyMs;
private Boolean enabled;
private String remark;
}

View File

@@ -0,0 +1,22 @@
package com.bruce.modelprovider.dto.request;
import lombok.Data;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class RagStoreModelConfigSaveRequest {
private Long id;
private Long storeId;
private Long embeddingModelId;
private Integer embeddingDimension;
private Integer chunkStrategy;
private Integer chunkSize;
private Integer chunkOverlap;
private String delimiter;
private String remark;
}

View File

@@ -0,0 +1,39 @@
package com.bruce.modelprovider.dto.response;
import com.bruce.modelprovider.entity.ModelCallLog;
import lombok.Data;
import org.springframework.beans.BeanUtils;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelCallLogResponse {
private Long id;
private String requestId;
private Long providerId;
private Long modelId;
private String taskType;
private String bizType;
private String bizId;
private String callType;
private String status;
private Integer durationMs;
private String errorCode;
private String errorMessage;
/**
* 方法 fromEntity用于执行业务逻辑处理。
*/
public static ModelCallLogResponse fromEntity(ModelCallLog entity) {
if (entity == null) {
return null;
}
ModelCallLogResponse response = new ModelCallLogResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
}

View File

@@ -0,0 +1,39 @@
package com.bruce.modelprovider.dto.response;
import com.bruce.modelprovider.entity.ModelConfig;
import lombok.Data;
import org.springframework.beans.BeanUtils;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelConfigResponse {
private Long id;
private Long providerId;
private String modelCode;
private String modelName;
private String upstreamModel;
private String modelType;
private Integer embeddingDimension;
private Boolean localModel;
private Boolean defaultModel;
private String optionsJson;
private Boolean enabled;
private String remark;
/**
* 方法 fromEntity用于执行业务逻辑处理。
*/
public static ModelConfigResponse fromEntity(ModelConfig entity) {
if (entity == null) {
return null;
}
ModelConfigResponse response = new ModelConfigResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
}

View File

@@ -0,0 +1,42 @@
package com.bruce.modelprovider.dto.response;
import com.bruce.modelprovider.entity.ModelProvider;
import lombok.Data;
import org.springframework.beans.BeanUtils;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelProviderResponse {
private Long id;
private String providerCode;
private String providerName;
private String providerType;
private String protocolType;
private String baseUrl;
private String authType;
private String secretRef;
private Boolean hasApiKey;
private Integer timeoutMs;
private Integer priority;
private Boolean enabled;
private String healthStatus;
private String remark;
/**
* 方法 fromEntity用于执行业务逻辑处理。
*/
public static ModelProviderResponse fromEntity(ModelProvider entity) {
if (entity == null) {
return null;
}
ModelProviderResponse response = new ModelProviderResponse();
BeanUtils.copyProperties(entity, response);
response.setHasApiKey(entity.getApiKeyCipher() != null && !entity.getApiKeyCipher().isBlank());
return response;
}
}

View File

@@ -0,0 +1,39 @@
package com.bruce.modelprovider.dto.response;
import com.bruce.modelprovider.entity.ModelRouteRule;
import lombok.Data;
import org.springframework.beans.BeanUtils;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteRuleResponse {
private Long id;
private String routeCode;
private String routeName;
private String taskType;
private String matchScope;
private Long scopeId;
private Long primaryModelId;
private String fallbackModelIdsJson;
private String routeStrategy;
private Integer maxLatencyMs;
private Boolean enabled;
private String remark;
/**
* 方法 fromEntity用于执行业务逻辑处理。
*/
public static ModelRouteRuleResponse fromEntity(ModelRouteRule entity) {
if (entity == null) {
return null;
}
ModelRouteRuleResponse response = new ModelRouteRuleResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
}

View File

@@ -0,0 +1,38 @@
package com.bruce.modelprovider.dto.response;
import com.bruce.modelprovider.entity.RagStoreModelConfig;
import lombok.Data;
import org.springframework.beans.BeanUtils;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class RagStoreModelConfigResponse {
private Long id;
private Long storeId;
private Long embeddingModelId;
private Integer embeddingDimension;
private Integer chunkStrategy;
private Integer chunkSize;
private Integer chunkOverlap;
private String delimiter;
private Boolean active;
private Integer indexVersion;
private String remark;
/**
* 方法 fromEntity用于执行业务逻辑处理。
*/
public static RagStoreModelConfigResponse fromEntity(RagStoreModelConfig entity) {
if (entity == null) {
return null;
}
RagStoreModelConfigResponse response = new RagStoreModelConfigResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
}

View File

@@ -0,0 +1,91 @@
package com.bruce.modelprovider.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 模型调用日志实体。
* <p>
* 用于记录每次模型调用的请求上下文、耗时、状态与错误摘要,支持排障与成本分析。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("model_call_log")
/**
* ModelCallLog负责模型平台对应层的职责。
*/
public class ModelCallLog extends BaseEntity {
/** 数据库字段request_id请求唯一ID一次网关调用唯一标识。 */
@TableField(value = "request_id")
private String requestId;
/** 数据库字段provider_id命中的服务商ID。 */
@TableField(value = "provider_id")
private Long providerId;
/** 数据库字段model_id命中的模型ID。 */
@TableField(value = "model_id")
private Long modelId;
/** 数据库字段task_type任务类型例如 RAG_EMBEDDING。 */
@TableField(value = "task_type")
private String taskType;
/** 数据库字段biz_type业务类型例如 RAG_DOCUMENT_INDEX。 */
@TableField(value = "biz_type")
private String bizType;
/** 数据库字段biz_id业务主键字符串形式兼容 Long/UUID。 */
@TableField(value = "biz_id")
private String bizId;
/** 数据库字段call_type调用类型例如 EMBEDDING / CHAT / RERANK。 */
@TableField(value = "call_type")
private String callType;
/** 调用状态,例如 SUCCESS / FAILED / TIMEOUT / FALLBACK_SUCCESS。 */
private String status;
/** 数据库字段prompt_tokens输入 token 数)。 */
@TableField(value = "prompt_tokens")
private Integer promptTokens;
/** 数据库字段completion_tokens输出 token 数)。 */
@TableField(value = "completion_tokens")
private Integer completionTokens;
/** 数据库字段total_tokens总 token 数)。 */
@TableField(value = "total_tokens")
private Integer totalTokens;
/** 数据库字段estimated_cost费用估算。 */
@TableField(value = "estimated_cost")
private BigDecimal estimatedCost;
/** 数据库字段duration_ms调用耗时单位毫秒。 */
@TableField(value = "duration_ms")
private Integer durationMs;
/** 数据库字段request_hash请求内容哈希用于排障不落原始敏感文本。 */
@TableField(value = "request_hash")
private String requestHash;
/** 数据库字段error_code错误码业务定义。 */
@TableField(value = "error_code")
private String errorCode;
/** 数据库字段error_message错误摘要已截断。 */
@TableField(value = "error_message")
private String errorMessage;
/** 备注信息。 */
private String remark;
}

View File

@@ -0,0 +1,88 @@
package com.bruce.modelprovider.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import com.bruce.rag.typehandler.PgJsonbStringTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 模型配置实体。
* <p>
* 每条记录描述某个服务商下的一个具体模型,包含模型类型、能力、价格和默认参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "model_config", autoResultMap = true)
/**
* ModelConfig负责模型平台对应层的职责。
*/
public class ModelConfig extends BaseEntity {
/** 数据库字段provider_id关联的服务商ID。 */
@TableField(value = "provider_id")
private Long providerId;
/** 数据库字段model_code模型编码在同一服务商下唯一。 */
@TableField(value = "model_code")
private String modelCode;
/** 数据库字段model_name模型展示名称。 */
@TableField(value = "model_name")
private String modelName;
/** 数据库字段upstream_model上游真实模型名例如 Qwen/Qwen3-Embedding-0.6B)。 */
@TableField(value = "upstream_model")
private String upstreamModel;
/** 数据库字段model_type模型类型例如 CHAT / EMBEDDING / RERANK / MULTIMODAL。 */
@TableField(value = "model_type")
private String modelType;
/** 数据库字段context_window上下文窗口大小。 */
@TableField(value = "context_window")
private Integer contextWindow;
/** 数据库字段max_output_tokens单次输出 token 上限)。 */
@TableField(value = "max_output_tokens")
private Integer maxOutputTokens;
/** 数据库字段embedding_dimension向量维度仅 EMBEDDING 模型使用)。 */
@TableField(value = "embedding_dimension")
private Integer embeddingDimension;
/** 数据库字段input_price_per_1k输入单价每 1k token。 */
@TableField(value = "input_price_per_1k")
private BigDecimal inputPricePer1k;
/** 数据库字段output_price_per_1k输出单价每 1k token。 */
@TableField(value = "output_price_per_1k")
private BigDecimal outputPricePer1k;
/** 数据库字段local_model是否本地模型如自建 Ollama。 */
@TableField(value = "local_model")
private Boolean localModel;
/** 数据库字段default_model是否该类型默认模型。 */
@TableField(value = "default_model")
private Boolean defaultModel;
/** 数据库字段capabilities_json能力标签JSON例如是否支持工具调用、视觉能力等。 */
@TableField(value = "capabilities_json", typeHandler = PgJsonbStringTypeHandler.class)
private String capabilitiesJson;
/** 数据库字段options_json默认调用参数JSON例如 temperature、dimensions。 */
@TableField(value = "options_json", typeHandler = PgJsonbStringTypeHandler.class)
private String optionsJson;
/** 是否启用该模型。 */
private Boolean enabled;
/** 备注信息。 */
private String remark;
}

View File

@@ -0,0 +1,81 @@
package com.bruce.modelprovider.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 模型服务商配置实体。
* <p>
* 说明:
* 1. 数据库列名沿用英文下划线命名,便于与既有 SQL 设计保持一致;
* 2. 字段语义通过中文注释明确,减少后续维护者对英文缩写的理解成本;
* 3. 密钥类字段仅用于后端调用,不应返回明文到前端。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("model_provider")
/**
* ModelProvider负责模型平台对应层的职责。
*/
public class ModelProvider extends BaseEntity {
/** 数据库字段provider_code服务商编码全局唯一用于路由规则与配置引用。 */
@TableField(value = "provider_code")
private String providerCode;
/** 数据库字段provider_name服务商展示名称。 */
@TableField(value = "provider_name")
private String providerName;
/** 数据库字段provider_type服务商类型例如 OLLAMA / SILICONFLOW / DASHSCOPE / OPENAI / CUSTOM。 */
@TableField(value = "provider_type")
private String providerType;
/** 数据库字段protocol_type协议类型首期固定为 OPENAI_COMPATIBLE。 */
@TableField(value = "protocol_type")
private String protocolType;
/** 数据库字段base_url上游服务基础地址通常包含 /v1。 */
@TableField(value = "base_url")
private String baseUrl;
/** 数据库字段auth_type鉴权类型例如 NONE / BEARER_TOKEN。 */
@TableField(value = "auth_type")
private String authType;
/** 数据库字段secret_ref密钥引用名通常是环境变量键例如 SILICONFLOW_API_KEY。 */
@TableField(value = "secret_ref")
private String secretRef;
/** 数据库字段api_key_cipher密钥密文可选首期不默认启用该方案。 */
@TableField(value = "api_key_cipher")
private String apiKeyCipher;
/** 数据库字段timeout_ms请求超时时间单位毫秒。 */
@TableField(value = "timeout_ms")
private Integer timeoutMs;
/** 调度优先级,数值越小优先级越高(用于后续扩展)。 */
private Integer priority;
/** 是否启用该服务商。 */
private Boolean enabled;
/** 数据库字段health_status健康状态例如 UNKNOWN / HEALTHY / UNHEALTHY。 */
@TableField(value = "health_status")
private String healthStatus;
/** 数据库字段last_health_check_time最近一次健康检查时间。 */
@TableField(value = "last_health_check_time")
private Date lastHealthCheckTime;
/** 备注信息。 */
private String remark;
}

View File

@@ -0,0 +1,66 @@
package com.bruce.modelprovider.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import com.bruce.rag.typehandler.PgJsonbStringTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 模型路由规则实体。
* <p>
* 用于定义任务在不同范围(全局/知识库/Agent下如何选择主模型与备用模型。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "model_route_rule", autoResultMap = true)
/**
* ModelRouteRule负责模型平台对应层的职责。
*/
public class ModelRouteRule extends BaseEntity {
/** 数据库字段route_code路由规则编码全局唯一。 */
@TableField(value = "route_code")
private String routeCode;
/** 数据库字段route_name路由规则名称。 */
@TableField(value = "route_name")
private String routeName;
/** 数据库字段task_type任务类型例如 RAG_EMBEDDING / CHAT_SIMPLE。 */
@TableField(value = "task_type")
private String taskType;
/** 数据库字段match_scope匹配范围例如 GLOBAL / RAG_STORE / AGENT。 */
@TableField(value = "match_scope")
private String matchScope;
/** 数据库字段scope_id范围ID例如知识库ID或Agent ID。 */
@TableField(value = "scope_id")
private Long scopeId;
/** 数据库字段primary_model_id主模型ID。 */
@TableField(value = "primary_model_id")
private Long primaryModelId;
/** 数据库字段fallback_model_ids_json备用模型ID列表JSON数组。 */
@TableField(value = "fallback_model_ids_json", typeHandler = PgJsonbStringTypeHandler.class)
private String fallbackModelIdsJson;
/** 数据库字段route_strategy路由策略例如 MANUAL / LOCAL_FIRST / COST_FIRST / QUALITY_FIRST。 */
@TableField(value = "route_strategy")
private String routeStrategy;
/** 数据库字段max_latency_ms最大允许耗时单位毫秒用于策略扩展。 */
@TableField(value = "max_latency_ms")
private Integer maxLatencyMs;
/** 是否启用该规则。 */
private Boolean enabled;
/** 备注信息。 */
private String remark;
}

View File

@@ -0,0 +1,60 @@
package com.bruce.modelprovider.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 知识库模型绑定实体。
* <p>
* 用于固定知识库当前生效的 Embedding 模型与向量维度,避免同库混用不同向量空间。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("rag_store_model_config")
/**
* RagStoreModelConfig负责模型平台对应层的职责。
*/
public class RagStoreModelConfig extends BaseEntity {
/** 数据库字段store_id知识库ID。 */
@TableField(value = "store_id")
private Long storeId;
/** 数据库字段embedding_model_id绑定的 Embedding 模型ID。 */
@TableField(value = "embedding_model_id")
private Long embeddingModelId;
/** 数据库字段embedding_dimension绑定维度首期固定 1024。 */
@TableField(value = "embedding_dimension")
private Integer embeddingDimension;
/** 数据库字段chunk_strategy切片策略枚举值。 */
@TableField(value = "chunk_strategy")
private Integer chunkStrategy;
/** 数据库字段chunk_size切片长度。 */
@TableField(value = "chunk_size")
private Integer chunkSize;
/** 数据库字段chunk_overlap切片重叠长度。 */
@TableField(value = "chunk_overlap")
private Integer chunkOverlap;
/** 分隔符(分隔符策略时生效)。 */
private String delimiter;
/** 是否生效(同一知识库仅允许一个 active=true。 */
private Boolean active;
/** 数据库字段index_version索引版本号模型或维度变化时递增。 */
@TableField(value = "index_version")
private Integer indexVersion;
/** 备注信息。 */
private String remark;
}

View File

@@ -0,0 +1,35 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelCallStatusEnum implements PersistableSysEnumDefinition {
SUCCESS(1, "成功"),
FAILED(2, "失败"),
TIMEOUT(3, "超时"),
FALLBACK_SUCCESS(4, "降级成功");
private static final String CATALOG = "model_provider";
private static final String TYPE = "call_status";
private static final String REMARK = "模型调用状态";
private final Integer value;
private final String label;
@Override
public String getCatalog() { return CATALOG; }
@Override
public String getType() { return TYPE; }
@Override
public String getRemark() { return REMARK; }
}

View File

@@ -0,0 +1,34 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelHealthStatusEnum implements PersistableSysEnumDefinition {
UNKNOWN(1, "未知"),
HEALTHY(2, "健康"),
UNHEALTHY(3, "不健康");
private static final String CATALOG = "model_provider";
private static final String TYPE = "health_status";
private static final String REMARK = "模型服务商健康状态";
private final Integer value;
private final String label;
@Override
public String getCatalog() { return CATALOG; }
@Override
public String getType() { return TYPE; }
@Override
public String getRemark() { return REMARK; }
}

View File

@@ -0,0 +1,49 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelProtocolTypeEnum implements PersistableSysEnumDefinition {
OPENAI_COMPATIBLE(1, "OpenAI兼容协议");
private static final String CATALOG = "model_provider";
private static final String TYPE = "protocol_type";
private static final String REMARK = "模型服务协议类型";
private final Integer value;
private final String label;
@Override
/**
* 方法 getCatalog用于执行业务逻辑处理。
*/
public String getCatalog() {
return CATALOG;
}
@Override
/**
* 方法 getType用于执行业务逻辑处理。
*/
public String getType() {
return TYPE;
}
@Override
/**
* 方法 getRemark用于执行业务逻辑处理。
*/
public String getRemark() {
return REMARK;
}
}

View File

@@ -0,0 +1,53 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelProviderTypeEnum implements PersistableSysEnumDefinition {
OLLAMA(1, "Ollama"),
SILICONFLOW(2, "硅基流动"),
DASHSCOPE(3, "百炼"),
OPENAI(4, "OpenAI"),
CUSTOM(5, "自定义");
private static final String CATALOG = "model_provider";
private static final String TYPE = "provider_type";
private static final String REMARK = "模型服务商类型";
private final Integer value;
private final String label;
@Override
/**
* 方法 getCatalog用于执行业务逻辑处理。
*/
public String getCatalog() {
return CATALOG;
}
@Override
/**
* 方法 getType用于执行业务逻辑处理。
*/
public String getType() {
return TYPE;
}
@Override
/**
* 方法 getRemark用于执行业务逻辑处理。
*/
public String getRemark() {
return REMARK;
}
}

View File

@@ -0,0 +1,35 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelRouteStrategyEnum implements PersistableSysEnumDefinition {
LOCAL_FIRST(1, "本地优先"),
COST_FIRST(2, "成本优先"),
QUALITY_FIRST(3, "质量优先"),
MANUAL(4, "手工指定");
private static final String CATALOG = "model_provider";
private static final String TYPE = "route_strategy";
private static final String REMARK = "模型路由策略";
private final Integer value;
private final String label;
@Override
public String getCatalog() { return CATALOG; }
@Override
public String getType() { return TYPE; }
@Override
public String getRemark() { return REMARK; }
}

View File

@@ -0,0 +1,38 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelTaskTypeEnum implements PersistableSysEnumDefinition {
RAG_EMBEDDING(1, "RAG文档向量化"),
RAG_QUERY_EMBEDDING(2, "RAG查询向量化"),
RAG_ANSWER(3, "RAG问答生成"),
CHAT_SIMPLE(4, "简单文本处理"),
CHAT_COMPLEX(5, "复杂文本处理"),
AGENT_PLANNING(6, "Agent规划"),
RERANK(7, "重排序");
private static final String CATALOG = "model_provider";
private static final String TYPE = "task_type";
private static final String REMARK = "模型任务类型";
private final Integer value;
private final String label;
@Override
public String getCatalog() { return CATALOG; }
@Override
public String getType() { return TYPE; }
@Override
public String getRemark() { return REMARK; }
}

View File

@@ -0,0 +1,52 @@
package com.bruce.modelprovider.enums;
import com.bruce.common.enums.PersistableSysEnumDefinition;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public enum ModelTypeEnum implements PersistableSysEnumDefinition {
CHAT(1, "聊天模型"),
EMBEDDING(2, "向量模型"),
RERANK(3, "重排模型"),
MULTIMODAL(4, "多模态模型");
private static final String CATALOG = "model_provider";
private static final String TYPE = "model_type";
private static final String REMARK = "模型类型";
private final Integer value;
private final String label;
@Override
/**
* 方法 getCatalog用于执行业务逻辑处理。
*/
public String getCatalog() {
return CATALOG;
}
@Override
/**
* 方法 getType用于执行业务逻辑处理。
*/
public String getType() {
return TYPE;
}
@Override
/**
* 方法 getRemark用于执行业务逻辑处理。
*/
public String getRemark() {
return REMARK;
}
}

View File

@@ -0,0 +1,14 @@
package com.bruce.modelprovider.gateway;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface EmbeddingModelGateway {
/**
* 方法 embed用于定义接口能力契约。
*/
EmbeddingResult embed(EmbeddingRequest request);
}

View File

@@ -0,0 +1,140 @@
package com.bruce.modelprovider.gateway;
import com.bruce.modelprovider.client.OpenAiCompatibleModelClient;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.ModelProvider;
import com.bruce.modelprovider.enums.ModelCallStatusEnum;
import com.bruce.modelprovider.route.ModelRouteContext;
import com.bruce.modelprovider.route.ModelRouteDecision;
import com.bruce.modelprovider.service.IModelCallLogService;
import com.bruce.modelprovider.service.IModelProviderService;
import com.bruce.modelprovider.service.IModelRouteService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.UUID;
/**
* Embedding 网关实现。
* <p>
* 主要职责:
* 1. 将业务请求转换为统一路由上下文并完成模型决策;
* 2. 调用上游 Embedding 接口并执行结果校验(数量、维度);
* 3. 记录调用日志(成功/失败、耗时、错误摘要、请求哈希);
* 4. 在主模型失败时按备用模型顺序执行兜底调用。
*/
@Component
@RequiredArgsConstructor
/**
* EmbeddingModelGatewayImpl负责模型平台对应层的职责。
*/
public class EmbeddingModelGatewayImpl implements EmbeddingModelGateway {
private final IModelRouteService modelRouteService;
private final IModelProviderService modelProviderService;
private final IModelCallLogService modelCallLogService;
private final OpenAiCompatibleModelClient openAiCompatibleModelClient;
/**
* 统一 Embedding 调用入口。
*
* @param request 向量化请求,包含文本、任务类型和业务上下文
* @return 向量化结果(模型信息 + 向量数组 + 调用日志)
*/
@Override
/**
* 方法 embed用于执行业务逻辑处理。
*/
public EmbeddingResult embed(EmbeddingRequest request) {
long start = System.currentTimeMillis();
ModelCallLog callLog = new ModelCallLog();
callLog.setRequestId(UUID.randomUUID().toString().replace("-", ""));
callLog.setTaskType(request.getTaskType());
callLog.setBizType(request.getBizType());
callLog.setBizId(request.getBizId());
callLog.setCallType("EMBEDDING");
callLog.setRequestHash(DigestUtils.md5DigestAsHex(String.join("|", request.getTexts()).getBytes(StandardCharsets.UTF_8)));
try {
ModelRouteContext routeContext = new ModelRouteContext();
routeContext.setTaskType(request.getTaskType());
routeContext.setMatchScope(request.getMatchScope());
routeContext.setScopeId(request.getScopeId());
routeContext.setRequiredModelType("EMBEDDING");
routeContext.setRequiredEmbeddingDimension(request.getExpectedDimension());
routeContext.setBizType(request.getBizType());
routeContext.setBizId(request.getBizId());
ModelRouteDecision decision = modelRouteService.route(routeContext);
ModelConfig model = decision.getPrimaryModel();
ModelProvider provider = modelProviderService.getById(model.getProviderId());
if (provider == null || !Boolean.TRUE.equals(provider.getEnabled())) {
throw new IllegalStateException("模型服务商不可用");
}
List<List<Double>> vectors = executeWithFallback(provider, model, decision.getFallbackModels(), request.getTexts(), request.getExpectedDimension());
if (vectors.size() != request.getTexts().size()) {
throw new IllegalStateException("向量数量与输入文本数量不一致");
}
Integer dimension = vectors.isEmpty() ? 0 : vectors.getFirst().size();
if (request.getExpectedDimension() != null && !request.getExpectedDimension().equals(dimension)) {
throw new IllegalStateException("向量维度不匹配expected=" + request.getExpectedDimension() + ", actual=" + dimension);
}
callLog.setProviderId(provider.getId());
callLog.setModelId(model.getId());
callLog.setStatus(ModelCallStatusEnum.SUCCESS.name());
callLog.setDurationMs((int) (System.currentTimeMillis() - start));
modelCallLogService.save(callLog);
EmbeddingResult result = new EmbeddingResult();
result.setModelId(model.getId());
result.setModelName(model.getModelName());
result.setDimension(dimension);
result.setVectors(vectors);
result.setCallLog(callLog);
return result;
} catch (Exception ex) {
callLog.setStatus(ModelCallStatusEnum.FAILED.name());
callLog.setDurationMs((int) (System.currentTimeMillis() - start));
callLog.setErrorCode("EMBEDDING_FAILED");
String msg = ex.getMessage();
callLog.setErrorMessage(msg == null ? "unknown" : msg.substring(0, Math.min(msg.length(), 1000)));
modelCallLogService.save(callLog);
throw ex;
}
}
/**
* 主模型优先调用,失败后按备用模型顺序重试。
*/
private List<List<Double>> executeWithFallback(ModelProvider primaryProvider,
ModelConfig primaryModel,
List<ModelConfig> fallbackModels,
List<String> texts,
Integer expectedDimension) {
try {
return openAiCompatibleModelClient.embeddings(primaryProvider, primaryModel, texts, expectedDimension);
} catch (Exception primaryEx) {
for (ModelConfig fallback : fallbackModels) {
try {
ModelProvider provider = modelProviderService.getById(fallback.getProviderId());
if (provider == null || !Boolean.TRUE.equals(provider.getEnabled())) {
continue;
}
return openAiCompatibleModelClient.embeddings(provider, fallback, texts, expectedDimension);
} catch (Exception ignored) {
// continue fallback chain
}
}
throw primaryEx;
}
}
}

View File

@@ -0,0 +1,22 @@
package com.bruce.modelprovider.gateway;
import lombok.Data;
import java.util.List;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class EmbeddingRequest {
private List<String> texts;
private String taskType;
private String matchScope;
private Long scopeId;
private String bizType;
private String bizId;
private Integer expectedDimension;
}

View File

@@ -0,0 +1,21 @@
package com.bruce.modelprovider.gateway;
import com.bruce.modelprovider.entity.ModelCallLog;
import lombok.Data;
import java.util.List;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class EmbeddingResult {
private Long modelId;
private String modelName;
private Integer dimension;
private List<List<Double>> vectors;
private ModelCallLog callLog;
}

View File

@@ -0,0 +1,15 @@
package com.bruce.modelprovider.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.modelprovider.entity.ModelCallLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface ModelCallLogMapper extends BaseMapper<ModelCallLog> {
}

View File

@@ -0,0 +1,15 @@
package com.bruce.modelprovider.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.modelprovider.entity.ModelConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface ModelConfigMapper extends BaseMapper<ModelConfig> {
}

View File

@@ -0,0 +1,15 @@
package com.bruce.modelprovider.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.modelprovider.entity.ModelProvider;
import org.apache.ibatis.annotations.Mapper;
@Mapper
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface ModelProviderMapper extends BaseMapper<ModelProvider> {
}

View File

@@ -0,0 +1,15 @@
package com.bruce.modelprovider.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.modelprovider.entity.ModelRouteRule;
import org.apache.ibatis.annotations.Mapper;
@Mapper
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface ModelRouteRuleMapper extends BaseMapper<ModelRouteRule> {
}

View File

@@ -0,0 +1,15 @@
package com.bruce.modelprovider.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.modelprovider.entity.RagStoreModelConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface RagStoreModelConfigMapper extends BaseMapper<RagStoreModelConfig> {
}

View File

@@ -0,0 +1,21 @@
package com.bruce.modelprovider.route;
import lombok.Data;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteContext {
private String taskType;
private String matchScope;
private Long scopeId;
private String requiredModelType;
private Boolean preferredLocal;
private Integer requiredEmbeddingDimension;
private String bizType;
private String bizId;
}

View File

@@ -0,0 +1,21 @@
package com.bruce.modelprovider.route;
import com.bruce.modelprovider.entity.ModelConfig;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteDecision {
private ModelConfig primaryModel;
private List<ModelConfig> fallbackModels = new ArrayList<>();
private String routeStrategy;
private String reason;
}

View File

@@ -0,0 +1,21 @@
package com.bruce.modelprovider.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.entity.ModelCallLog;
import java.util.List;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface IModelCallLogService extends IService<ModelCallLog> {
/**
* 方法 query用于定义接口能力契约。
*/
List<ModelCallLogResponse> query(ModelCallLogQueryRequest request);
}

View File

@@ -0,0 +1,33 @@
package com.bruce.modelprovider.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.entity.ModelConfig;
import java.util.List;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface IModelConfigService extends IService<ModelConfig> {
/**
* 方法 listResponses用于定义接口能力契约。
*/
List<ModelConfigResponse> listResponses();
/**
* 方法 getResponseById用于定义接口能力契约。
*/
ModelConfigResponse getResponseById(Long id);
/**
* 方法 saveOrUpdate用于定义接口能力契约。
*/
boolean saveOrUpdate(ModelConfigSaveRequest request);
/**
* 方法 getEnabledModel用于定义接口能力契约。
*/
ModelConfig getEnabledModel(Long modelId);
}

View File

@@ -0,0 +1,33 @@
package com.bruce.modelprovider.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.entity.ModelProvider;
import java.util.List;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface IModelProviderService extends IService<ModelProvider> {
/**
* 方法 listResponses用于定义接口能力契约。
*/
List<ModelProviderResponse> listResponses();
/**
* 方法 getResponseById用于定义接口能力契约。
*/
ModelProviderResponse getResponseById(Long id);
/**
* 方法 saveOrUpdate用于定义接口能力契约。
*/
boolean saveOrUpdate(ModelProviderSaveRequest request);
/**
* 方法 checkHealth用于定义接口能力契约。
*/
boolean checkHealth(Long id);
}

View File

@@ -0,0 +1,29 @@
package com.bruce.modelprovider.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.entity.ModelRouteRule;
import java.util.List;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface IModelRouteRuleService extends IService<ModelRouteRule> {
/**
* 方法 listResponses用于定义接口能力契约。
*/
List<ModelRouteRuleResponse> listResponses();
/**
* 方法 getResponseById用于定义接口能力契约。
*/
ModelRouteRuleResponse getResponseById(Long id);
/**
* 方法 saveOrUpdate用于定义接口能力契约。
*/
boolean saveOrUpdate(ModelRouteRuleSaveRequest request);
}

View File

@@ -0,0 +1,17 @@
package com.bruce.modelprovider.service;
import com.bruce.modelprovider.route.ModelRouteContext;
import com.bruce.modelprovider.route.ModelRouteDecision;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface IModelRouteService {
/**
* 方法 route用于定义接口能力契约。
*/
ModelRouteDecision route(ModelRouteContext context);
}

View File

@@ -0,0 +1,27 @@
package com.bruce.modelprovider.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.modelprovider.dto.request.RagStoreModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse;
import com.bruce.modelprovider.entity.RagStoreModelConfig;
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public interface IRagStoreModelConfigService extends IService<RagStoreModelConfig> {
/**
* 方法 getByStoreId用于定义接口能力契约。
*/
RagStoreModelConfigResponse getByStoreId(Long storeId);
/**
* 方法 saveOrUpdate用于定义接口能力契约。
*/
boolean saveOrUpdate(RagStoreModelConfigSaveRequest request);
/**
* 方法 getActiveEntity用于定义接口能力契约。
*/
RagStoreModelConfig getActiveEntity(Long storeId);
}

View File

@@ -0,0 +1,42 @@
package com.bruce.modelprovider.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.mapper.ModelCallLogMapper;
import com.bruce.modelprovider.service.IModelCallLogService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Service
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelCallLogServiceImpl extends ServiceImpl<ModelCallLogMapper, ModelCallLog>
implements IModelCallLogService {
@Override
/**
* 方法 query用于执行业务逻辑处理。
*/
public List<ModelCallLogResponse> query(ModelCallLogQueryRequest request) {
ModelCallLogQueryRequest q = request == null ? new ModelCallLogQueryRequest() : request;
return lambdaQuery()
.eq(StringUtils.hasText(q.getTaskType()), ModelCallLog::getTaskType, q.getTaskType())
.eq(q.getProviderId() != null, ModelCallLog::getProviderId, q.getProviderId())
.eq(q.getModelId() != null, ModelCallLog::getModelId, q.getModelId())
.eq(StringUtils.hasText(q.getStatus()), ModelCallLog::getStatus, q.getStatus())
.eq(StringUtils.hasText(q.getBizType()), ModelCallLog::getBizType, q.getBizType())
.orderByDesc(ModelCallLog::getId)
.list()
.stream()
.map(ModelCallLogResponse::fromEntity)
.toList();
}
}

View File

@@ -0,0 +1,93 @@
package com.bruce.modelprovider.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.mapper.ModelConfigMapper;
import com.bruce.modelprovider.service.IModelConfigService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Service
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelConfigServiceImpl extends ServiceImpl<ModelConfigMapper, ModelConfig> implements IModelConfigService {
@Override
/**
* 方法 listResponses用于执行业务逻辑处理。
*/
public List<ModelConfigResponse> listResponses() {
/**
* 方法 list用于定义接口能力契约。
*/
return list().stream().map(ModelConfigResponse::fromEntity).toList();
}
@Override
/**
* 方法 getResponseById用于执行业务逻辑处理。
*/
public ModelConfigResponse getResponseById(Long id) {
return ModelConfigResponse.fromEntity(getById(id));
}
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(ModelConfigSaveRequest request) {
if (request == null) {
throw new IllegalArgumentException("模型保存请求不能为空");
}
if (request.getProviderId() == null) {
throw new IllegalArgumentException("服务商ID不能为空");
}
if (!StringUtils.hasText(request.getModelCode())) {
throw new IllegalArgumentException("模型编码不能为空");
}
ModelConfig duplicate = lambdaQuery()
.eq(ModelConfig::getProviderId, request.getProviderId())
.eq(ModelConfig::getModelCode, request.getModelCode().trim())
.ne(request.getId() != null, ModelConfig::getId, request.getId())
.one();
if (duplicate != null) {
throw new IllegalArgumentException("同一服务商下模型编码已存在: " + request.getModelCode().trim());
}
ModelConfig entity = request.getId() == null ? new ModelConfig() : getById(request.getId());
if (entity == null) {
throw new IllegalArgumentException("模型不存在ID: " + request.getId());
}
entity.setProviderId(request.getProviderId());
entity.setModelCode(request.getModelCode().trim());
entity.setModelName(request.getModelName());
entity.setUpstreamModel(request.getUpstreamModel());
entity.setModelType(request.getModelType());
entity.setEmbeddingDimension(request.getEmbeddingDimension());
entity.setLocalModel(request.getLocalModel());
entity.setDefaultModel(request.getDefaultModel());
entity.setOptionsJson(request.getOptionsJson());
entity.setEnabled(request.getEnabled() == null ? Boolean.TRUE : request.getEnabled());
entity.setRemark(request.getRemark());
return request.getId() == null ? save(entity) : updateById(entity);
}
@Override
/**
* 方法 getEnabledModel用于执行业务逻辑处理。
*/
public ModelConfig getEnabledModel(Long modelId) {
ModelConfig model = getById(modelId);
if (model == null || !Boolean.TRUE.equals(model.getEnabled())) {
throw new IllegalArgumentException("模型不可用ID: " + modelId);
}
return model;
}
}

View File

@@ -0,0 +1,132 @@
package com.bruce.modelprovider.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.client.OpenAiCompatibleModelClient;
import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.entity.ModelProvider;
import com.bruce.modelprovider.enums.ModelHealthStatusEnum;
import com.bruce.modelprovider.mapper.ModelProviderMapper;
import com.bruce.modelprovider.service.IModelProviderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
/**
* 模型服务商服务实现。
* <p>
* 主要职责:
* 1. 服务商配置的增删改查;
* 2. 服务商编码唯一性校验;
* 3. 默认健康状态初始化;
* 4. 对接上游健康检查并回写检查结果。
*/
@Service
@RequiredArgsConstructor
/**
* ModelProviderServiceImpl负责模型平台对应层的职责。
*/
public class ModelProviderServiceImpl extends ServiceImpl<ModelProviderMapper, ModelProvider>
implements IModelProviderService {
private final OpenAiCompatibleModelClient openAiCompatibleModelClient;
/**
* 查询全部服务商并转换为响应对象。
*/
@Override
/**
* 方法 listResponses用于执行业务逻辑处理。
*/
public List<ModelProviderResponse> listResponses() {
/**
* 方法 list用于定义接口能力契约。
*/
return list().stream().map(ModelProviderResponse::fromEntity).toList();
}
/**
* 按ID查询服务商详情响应对象已脱敏不返回明文密钥
*/
@Override
/**
* 方法 getResponseById用于执行业务逻辑处理。
*/
public ModelProviderResponse getResponseById(Long id) {
return ModelProviderResponse.fromEntity(getById(id));
}
/**
* 保存或更新服务商配置。
* 会执行必要校验:请求非空、编码/名称非空、编码唯一。
*/
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(ModelProviderSaveRequest request) {
if (request == null) {
throw new IllegalArgumentException("服务商保存请求不能为空");
}
if (!StringUtils.hasText(request.getProviderCode())) {
throw new IllegalArgumentException("服务商编码不能为空");
}
if (!StringUtils.hasText(request.getProviderName())) {
throw new IllegalArgumentException("服务商名称不能为空");
}
ModelProvider duplicate = lambdaQuery()
.eq(ModelProvider::getProviderCode, request.getProviderCode().trim())
.ne(request.getId() != null, ModelProvider::getId, request.getId())
.one();
if (duplicate != null) {
throw new IllegalArgumentException("服务商编码已存在: " + request.getProviderCode().trim());
}
ModelProvider entity = request.getId() == null ? new ModelProvider() : getById(request.getId());
if (entity == null) {
throw new IllegalArgumentException("服务商不存在ID: " + request.getId());
}
entity.setProviderCode(request.getProviderCode().trim());
entity.setProviderName(request.getProviderName().trim());
entity.setProviderType(request.getProviderType());
entity.setProtocolType(request.getProtocolType());
entity.setBaseUrl(request.getBaseUrl());
entity.setAuthType(request.getAuthType());
entity.setSecretRef(request.getSecretRef());
entity.setApiKeyCipher(request.getApiKeyCipher());
entity.setTimeoutMs(request.getTimeoutMs());
entity.setPriority(request.getPriority());
entity.setEnabled(request.getEnabled() == null ? Boolean.TRUE : request.getEnabled());
entity.setRemark(request.getRemark());
if (!StringUtils.hasText(entity.getHealthStatus())) {
entity.setHealthStatus(ModelHealthStatusEnum.UNKNOWN.name());
}
return request.getId() == null ? save(entity) : updateById(entity);
}
/**
* 主动健康检查并回写健康状态与检查时间。
*/
@Override
/**
* 方法 checkHealth用于执行业务逻辑处理。
*/
public boolean checkHealth(Long id) {
ModelProvider provider = getById(id);
if (provider == null) {
throw new IllegalArgumentException("服务商不存在ID: " + id);
}
boolean healthy = openAiCompatibleModelClient.health(provider);
provider.setHealthStatus(healthy ? ModelHealthStatusEnum.HEALTHY.name() : ModelHealthStatusEnum.UNHEALTHY.name());
provider.setLastHealthCheckTime(new Date());
/**
* 方法 updateById用于定义接口能力契约。
*/
return updateById(provider);
}
}

View File

@@ -0,0 +1,75 @@
package com.bruce.modelprovider.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.entity.ModelRouteRule;
import com.bruce.modelprovider.mapper.ModelRouteRuleMapper;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Service
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteRuleServiceImpl extends ServiceImpl<ModelRouteRuleMapper, ModelRouteRule>
implements IModelRouteRuleService {
@Override
/**
* 方法 listResponses用于执行业务逻辑处理。
*/
public List<ModelRouteRuleResponse> listResponses() {
/**
* 方法 list用于定义接口能力契约。
*/
return list().stream().map(ModelRouteRuleResponse::fromEntity).toList();
}
@Override
/**
* 方法 getResponseById用于执行业务逻辑处理。
*/
public ModelRouteRuleResponse getResponseById(Long id) {
return ModelRouteRuleResponse.fromEntity(getById(id));
}
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(ModelRouteRuleSaveRequest request) {
if (request == null || !StringUtils.hasText(request.getRouteCode())) {
throw new IllegalArgumentException("路由编码不能为空");
}
ModelRouteRule duplicate = lambdaQuery()
.eq(ModelRouteRule::getRouteCode, request.getRouteCode().trim())
.ne(request.getId() != null, ModelRouteRule::getId, request.getId())
.one();
if (duplicate != null) {
throw new IllegalArgumentException("路由编码已存在: " + request.getRouteCode().trim());
}
ModelRouteRule entity = request.getId() == null ? new ModelRouteRule() : getById(request.getId());
if (entity == null) {
throw new IllegalArgumentException("路由不存在ID: " + request.getId());
}
entity.setRouteCode(request.getRouteCode().trim());
entity.setRouteName(request.getRouteName());
entity.setTaskType(request.getTaskType());
entity.setMatchScope(request.getMatchScope());
entity.setScopeId(request.getScopeId());
entity.setPrimaryModelId(request.getPrimaryModelId());
entity.setFallbackModelIdsJson(request.getFallbackModelIdsJson());
entity.setRouteStrategy(request.getRouteStrategy());
entity.setMaxLatencyMs(request.getMaxLatencyMs());
entity.setEnabled(request.getEnabled() == null ? Boolean.TRUE : request.getEnabled());
entity.setRemark(request.getRemark());
return request.getId() == null ? save(entity) : updateById(entity);
}
}

View File

@@ -0,0 +1,153 @@
package com.bruce.modelprovider.service.impl;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.ModelRouteRule;
import com.bruce.modelprovider.route.ModelRouteContext;
import com.bruce.modelprovider.route.ModelRouteDecision;
import com.bruce.modelprovider.service.IModelConfigService;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import com.bruce.modelprovider.service.IModelRouteService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 模型路由服务实现。
* <p>
* 主要职责:
* 1. 按优先级选择路由规则:范围规则 -> 任务全局规则 -> 模型类型默认模型;
* 2. 根据规则解析主模型与备用模型,并过滤不可用模型;
* 3. 对 Embedding 场景执行维度一致性校验,避免不同向量空间混用;
* 4. 在 LOCAL_FIRST 策略下优先选择本地模型。
*/
@Service
@RequiredArgsConstructor
/**
* ModelRouteServiceImpl负责模型平台对应层的职责。
*/
public class ModelRouteServiceImpl implements IModelRouteService {
private final IModelRouteRuleService modelRouteRuleService;
private final IModelConfigService modelConfigService;
/**
* 执行一次路由决策。
*
* @param context 路由上下文,不能为空
* @return 路由决策结果,包含主模型、备用模型和命中原因
*/
@Override
/**
* 方法 route用于执行业务逻辑处理。
*/
public ModelRouteDecision route(ModelRouteContext context) {
if (context == null) {
throw new IllegalArgumentException("路由上下文不能为空");
}
ModelRouteRule rule = selectRule(context);
if (rule == null) {
ModelConfig defaultModel = modelConfigService.lambdaQuery()
.eq(ModelConfig::getModelType, context.getRequiredModelType())
.eq(ModelConfig::getDefaultModel, true)
.eq(ModelConfig::getEnabled, true)
.last("limit 1")
.one();
if (defaultModel == null) {
throw new IllegalStateException("未找到可用模型路由,请先配置规则或默认模型");
}
ModelRouteDecision decision = new ModelRouteDecision();
decision.setPrimaryModel(defaultModel);
decision.setRouteStrategy("MANUAL");
decision.setReason("命中模型类型默认模型");
return decision;
}
ModelConfig primary = modelConfigService.getEnabledModel(rule.getPrimaryModelId());
if ("EMBEDDING".equals(context.getRequiredModelType()) && context.getRequiredEmbeddingDimension() != null
&& !context.getRequiredEmbeddingDimension().equals(primary.getEmbeddingDimension())) {
throw new IllegalStateException("主模型Embedding维度不匹配");
}
List<ModelConfig> fallbackModels = new ArrayList<>();
for (Long fallbackId : parseFallbackIds(rule.getFallbackModelIdsJson())) {
ModelConfig fallback = modelConfigService.getById(fallbackId);
if (fallback == null || !Boolean.TRUE.equals(fallback.getEnabled())) {
continue;
}
if ("EMBEDDING".equals(context.getRequiredModelType()) && context.getRequiredEmbeddingDimension() != null
&& !context.getRequiredEmbeddingDimension().equals(fallback.getEmbeddingDimension())) {
continue;
}
fallbackModels.add(fallback);
}
if ("LOCAL_FIRST".equals(rule.getRouteStrategy()) && !Boolean.TRUE.equals(primary.getLocalModel())) {
ModelConfig localCandidate = fallbackModels.stream().filter(ModelConfig::getLocalModel).findFirst().orElse(null);
if (localCandidate != null) {
fallbackModels.remove(localCandidate);
fallbackModels.add(0, primary);
primary = localCandidate;
}
}
ModelRouteDecision decision = new ModelRouteDecision();
decision.setPrimaryModel(primary);
decision.setFallbackModels(fallbackModels);
decision.setRouteStrategy(rule.getRouteStrategy());
decision.setReason("命中规则: " + rule.getRouteCode());
return decision;
}
/**
* 优先匹配范围级规则;未命中时回退到同任务类型的全局规则。
*/
private ModelRouteRule selectRule(ModelRouteContext context) {
ModelRouteRule scopeRule = modelRouteRuleService.lambdaQuery()
.eq(ModelRouteRule::getEnabled, true)
.eq(ModelRouteRule::getTaskType, context.getTaskType())
.eq(context.getMatchScope() != null, ModelRouteRule::getMatchScope, context.getMatchScope())
.eq(context.getScopeId() != null, ModelRouteRule::getScopeId, context.getScopeId())
.last("limit 1")
.one();
if (scopeRule != null) {
return scopeRule;
}
return modelRouteRuleService.lambdaQuery()
.eq(ModelRouteRule::getEnabled, true)
.eq(ModelRouteRule::getTaskType, context.getTaskType())
.eq(ModelRouteRule::getMatchScope, "GLOBAL")
.last("limit 1")
.one();
}
/**
* 解析备用模型ID数组。
* <p>
* 支持最简 JSON 数组文本(如 [1,2,3]),解析失败时返回空列表,避免阻断主流程。
*/
private List<Long> parseFallbackIds(String json) {
if (json == null || json.isBlank()) {
return List.of();
}
String normalized = json.trim();
if (!normalized.startsWith("[") || !normalized.endsWith("]")) {
return List.of();
}
String body = normalized.substring(1, normalized.length() - 1).trim();
if (body.isEmpty()) {
return List.of();
}
return List.of(body.split(",")).stream()
.map(String::trim)
.map(v -> v.replace("\"", ""))
.filter(v -> !v.isEmpty())
.map(Long::valueOf)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,88 @@
package com.bruce.modelprovider.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.RagStoreModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.RagStoreModelConfig;
import com.bruce.modelprovider.mapper.RagStoreModelConfigMapper;
import com.bruce.modelprovider.service.IModelConfigService;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class RagStoreModelConfigServiceImpl extends ServiceImpl<RagStoreModelConfigMapper, RagStoreModelConfig>
implements IRagStoreModelConfigService {
private final IModelConfigService modelConfigService;
@Override
/**
* 方法 getByStoreId用于执行业务逻辑处理。
*/
public RagStoreModelConfigResponse getByStoreId(Long storeId) {
return RagStoreModelConfigResponse.fromEntity(getActiveEntity(storeId));
}
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(RagStoreModelConfigSaveRequest request) {
if (request == null || request.getStoreId() == null) {
throw new IllegalArgumentException("知识库模型配置请求不能为空");
}
ModelConfig embeddingModel = modelConfigService.getEnabledModel(request.getEmbeddingModelId());
if (!"EMBEDDING".equals(embeddingModel.getModelType())) {
throw new IllegalArgumentException("仅允许配置EMBEDDING类型模型");
}
if (request.getEmbeddingDimension() == null || request.getEmbeddingDimension() != 1024) {
throw new IllegalArgumentException("首期仅支持1024维Embedding");
}
RagStoreModelConfig current = getActiveEntity(request.getStoreId());
RagStoreModelConfig entity = current == null ? new RagStoreModelConfig() : current;
entity.setStoreId(request.getStoreId());
entity.setEmbeddingModelId(request.getEmbeddingModelId());
entity.setEmbeddingDimension(request.getEmbeddingDimension());
entity.setChunkStrategy(request.getChunkStrategy());
entity.setChunkSize(request.getChunkSize());
entity.setChunkOverlap(request.getChunkOverlap());
entity.setDelimiter(request.getDelimiter());
entity.setActive(Boolean.TRUE);
entity.setRemark(request.getRemark());
if (current == null) {
entity.setIndexVersion(1);
/**
* 方法 save用于定义接口能力契约。
*/
return save(entity);
}
boolean changed = !current.getEmbeddingModelId().equals(request.getEmbeddingModelId())
|| !current.getEmbeddingDimension().equals(request.getEmbeddingDimension());
if (changed) {
entity.setIndexVersion(current.getIndexVersion() == null ? 2 : current.getIndexVersion() + 1);
}
/**
* 方法 updateById用于定义接口能力契约。
*/
return updateById(entity);
}
@Override
/**
* 方法 getActiveEntity用于执行业务逻辑处理。
*/
public RagStoreModelConfig getActiveEntity(Long storeId) {
return lambdaQuery().eq(RagStoreModelConfig::getStoreId, storeId)
.eq(RagStoreModelConfig::getActive, true)
.one();
}
}

View File

@@ -2,14 +2,22 @@ package com.bruce.rag.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bruce.common.document.parse.DocumentParseResult; import com.bruce.common.document.parse.DocumentParseResult;
import com.bruce.modelprovider.entity.RagStoreModelConfig;
import com.bruce.modelprovider.gateway.EmbeddingModelGateway;
import com.bruce.modelprovider.gateway.EmbeddingRequest;
import com.bruce.modelprovider.gateway.EmbeddingResult;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import com.bruce.rag.dto.request.RagDocumentChunkRequest; import com.bruce.rag.dto.request.RagDocumentChunkRequest;
import com.bruce.rag.entity.RagChunk; import com.bruce.rag.entity.RagChunk;
import com.bruce.rag.entity.RagChunkEmbedding;
import com.bruce.rag.entity.RagDocument; import com.bruce.rag.entity.RagDocument;
import com.bruce.rag.entity.RagDocumentParseResult; import com.bruce.rag.entity.RagDocumentParseResult;
import com.bruce.rag.enums.RagChunkStrategyEnum; import com.bruce.rag.enums.RagChunkStrategyEnum;
import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.parse.Chunker; import com.bruce.rag.parse.Chunker;
import com.bruce.rag.parse.ChunkerFactory; import com.bruce.rag.parse.ChunkerFactory;
import com.bruce.rag.parse.RagChunkCommand; import com.bruce.rag.parse.RagChunkCommand;
import com.bruce.rag.service.IRagChunkEmbeddingService;
import com.bruce.rag.service.IRagChunkService; import com.bruce.rag.service.IRagChunkService;
import com.bruce.rag.service.IRagDocumentChunkService; import com.bruce.rag.service.IRagDocumentChunkService;
import com.bruce.rag.service.IRagDocumentParseResultService; import com.bruce.rag.service.IRagDocumentParseResultService;
@@ -19,12 +27,18 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
/**
* RagDocumentChunkServiceImpl负责模型平台对应层的职责。
*/
public class RagDocumentChunkServiceImpl implements IRagDocumentChunkService { public class RagDocumentChunkServiceImpl implements IRagDocumentChunkService {
private final IRagDocumentService ragDocumentService; private final IRagDocumentService ragDocumentService;
@@ -35,8 +49,17 @@ public class RagDocumentChunkServiceImpl implements IRagDocumentChunkService {
private final IRagChunkService ragChunkService; private final IRagChunkService ragChunkService;
private final IRagChunkEmbeddingService ragChunkEmbeddingService;
private final IRagStoreModelConfigService ragStoreModelConfigService;
private final EmbeddingModelGateway embeddingModelGateway;
@Override @Override
@Async @Async
/**
* 方法 submitChunkTask用于执行业务逻辑处理。
*/
public void submitChunkTask(RagDocumentChunkRequest request) { public void submitChunkTask(RagDocumentChunkRequest request) {
validateRequest(request); validateRequest(request);
RagChunkStrategyEnum strategy = RagChunkStrategyEnum.fromValue(request.getChunkStrategy()); RagChunkStrategyEnum strategy = RagChunkStrategyEnum.fromValue(request.getChunkStrategy());
@@ -48,10 +71,15 @@ public class RagDocumentChunkServiceImpl implements IRagDocumentChunkService {
log.warn("RagDocumentChunkServiceImpl.chunkAsync document not found, documentId={}", documentId); log.warn("RagDocumentChunkServiceImpl.chunkAsync document not found, documentId={}", documentId);
continue; continue;
} }
updateIndexStatus(document, RagIndexStatusEnum.INDEXING.name(), null);
RagDocumentParseResult snapshot = ragDocumentParseResultService.getByDocumentId(documentId); RagDocumentParseResult snapshot = ragDocumentParseResultService.getByDocumentId(documentId);
if (snapshot == null) { if (snapshot == null) {
throw new IllegalStateException("文档尚未生成解析快照documentId=" + documentId); throw new IllegalStateException("文档尚未生成解析快照documentId=" + documentId);
} }
RagStoreModelConfig storeModelConfig = ragStoreModelConfigService.getActiveEntity(document.getStoreId());
if (storeModelConfig == null || storeModelConfig.getEmbeddingModelId() == null) {
throw new IllegalStateException("请先配置知识库 Embedding 模型");
}
DocumentParseResult parseResult = ragDocumentParseResultService.toParseResult(snapshot); DocumentParseResult parseResult = ragDocumentParseResultService.toParseResult(snapshot);
RagChunkCommand command = new RagChunkCommand(); RagChunkCommand command = new RagChunkCommand();
command.setDocument(document); command.setDocument(document);
@@ -62,20 +90,78 @@ public class RagDocumentChunkServiceImpl implements IRagDocumentChunkService {
command.setDelimiter(request.getDelimiter()); command.setDelimiter(request.getDelimiter());
List<RagChunk> chunks = chunker.chunk(command); List<RagChunk> chunks = chunker.chunk(command);
ragChunkEmbeddingService.remove(Wrappers.<RagChunkEmbedding>lambdaQuery()
.eq(RagChunkEmbedding::getDocumentId, documentId));
ragChunkService.remove(Wrappers.<RagChunk>lambdaQuery() ragChunkService.remove(Wrappers.<RagChunk>lambdaQuery()
.eq(RagChunk::getDocumentId, documentId)); .eq(RagChunk::getDocumentId, documentId));
if (!chunks.isEmpty()) { if (!chunks.isEmpty()) {
ragChunkService.saveBatch(chunks); ragChunkService.saveBatch(chunks);
} }
writeEmbeddings(document, chunks, storeModelConfig);
updateIndexStatus(document, RagIndexStatusEnum.INDEXED.name(), null);
log.info("RagDocumentChunkServiceImpl.chunkAsync success, documentId={}, chunkCount={}", log.info("RagDocumentChunkServiceImpl.chunkAsync success, documentId={}, chunkCount={}",
documentId, chunks.size()); documentId, chunks.size());
} catch (RuntimeException e) { } catch (RuntimeException e) {
RagDocument failedDoc = ragDocumentService.getById(documentId);
if (failedDoc != null) {
updateIndexStatus(failedDoc, RagIndexStatusEnum.FAILED.name(), e.getMessage());
}
log.warn("RagDocumentChunkServiceImpl.chunkAsync failed, documentId={}, message={}", log.warn("RagDocumentChunkServiceImpl.chunkAsync failed, documentId={}, message={}",
documentId, e.getMessage()); documentId, e.getMessage());
} }
} }
} }
@Transactional
/**
* 方法 writeEmbeddings用于执行业务逻辑处理。
*/
public void writeEmbeddings(RagDocument document, List<RagChunk> chunks, RagStoreModelConfig storeModelConfig) {
if (chunks == null || chunks.isEmpty()) {
return;
}
EmbeddingRequest embeddingRequest = new EmbeddingRequest();
embeddingRequest.setTexts(chunks.stream().map(RagChunk::getChunkContent).toList());
embeddingRequest.setTaskType("RAG_EMBEDDING");
embeddingRequest.setMatchScope("RAG_STORE");
embeddingRequest.setScopeId(document.getStoreId());
embeddingRequest.setBizType("RAG_DOCUMENT_INDEX");
embeddingRequest.setBizId(String.valueOf(document.getId()));
embeddingRequest.setExpectedDimension(storeModelConfig.getEmbeddingDimension());
EmbeddingResult result = embeddingModelGateway.embed(embeddingRequest);
if (result.getVectors().size() != chunks.size()) {
throw new IllegalStateException("向量数量与切片数量不一致");
}
List<RagChunkEmbedding> embeddingRows = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
RagChunk chunk = chunks.get(i);
List<Double> vector = result.getVectors().get(i);
RagChunkEmbedding row = new RagChunkEmbedding();
row.setStoreId(document.getStoreId());
row.setDocumentId(document.getId());
row.setChunkId(chunk.getId());
row.setEmbeddingModel(result.getModelName());
row.setEmbeddingDimension(result.getDimension());
row.setEmbedding(vector.toString());
row.setContentHash(DigestUtils.md5DigestAsHex(chunk.getChunkContent().getBytes(StandardCharsets.UTF_8)));
row.setEnabled(true);
embeddingRows.add(row);
}
ragChunkEmbeddingService.saveBatch(embeddingRows);
}
/**
* 方法 updateIndexStatus用于执行业务逻辑处理。
*/
private void updateIndexStatus(RagDocument document, String status, String errorMessage) {
document.setIndexStatus(status);
document.setErrorMessage(errorMessage == null ? null : errorMessage.substring(0, Math.min(errorMessage.length(), 1000)));
ragDocumentService.updateById(document);
}
/**
* 方法 validateRequest用于执行业务逻辑处理。
*/
private void validateRequest(RagDocumentChunkRequest request) { private void validateRequest(RagDocumentChunkRequest request) {
if (request == null) { if (request == null) {
throw new IllegalArgumentException("切片请求不能为空"); throw new IllegalArgumentException("切片请求不能为空");
@@ -86,3 +172,5 @@ public class RagDocumentChunkServiceImpl implements IRagDocumentChunkService {
RagChunkStrategyEnum.fromValue(request.getChunkStrategy()); RagChunkStrategyEnum.fromValue(request.getChunkStrategy());
} }
} }

View File

@@ -3,6 +3,13 @@ package com.bruce.common.enumconfig;
import com.bruce.common.enums.CommonStatusEnum; import com.bruce.common.enums.CommonStatusEnum;
import com.bruce.common.enums.EnableStatusEnum; import com.bruce.common.enums.EnableStatusEnum;
import com.bruce.common.enums.PersistableSysEnumDefinition; import com.bruce.common.enums.PersistableSysEnumDefinition;
import com.bruce.modelprovider.enums.ModelCallStatusEnum;
import com.bruce.modelprovider.enums.ModelHealthStatusEnum;
import com.bruce.modelprovider.enums.ModelProtocolTypeEnum;
import com.bruce.modelprovider.enums.ModelProviderTypeEnum;
import com.bruce.modelprovider.enums.ModelRouteStrategyEnum;
import com.bruce.modelprovider.enums.ModelTaskTypeEnum;
import com.bruce.modelprovider.enums.ModelTypeEnum;
import com.bruce.rag.enums.RagChunkStrategyEnum; import com.bruce.rag.enums.RagChunkStrategyEnum;
import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.enums.RagParseStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum;
@@ -14,6 +21,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
class EnumDefinitionTests { class EnumDefinitionTests {
@Test @Test
/**
* 方法 enumValuesShouldBeStable用于执行业务逻辑处理。
*/
void enumValuesShouldBeStable() { void enumValuesShouldBeStable() {
assertEquals(1, EnableStatusEnum.ENABLED.getValue()); assertEquals(1, EnableStatusEnum.ENABLED.getValue());
assertEquals(0, EnableStatusEnum.DISABLED.getValue()); assertEquals(0, EnableStatusEnum.DISABLED.getValue());
@@ -30,9 +40,19 @@ class EnumDefinitionTests {
assertEquals(1, RagChunkStrategyEnum.FIXED_LENGTH.getValue()); assertEquals(1, RagChunkStrategyEnum.FIXED_LENGTH.getValue());
assertEquals(5, RagChunkStrategyEnum.DELIMITER.getValue()); assertEquals(5, RagChunkStrategyEnum.DELIMITER.getValue());
assertEquals(6, RagChunkStrategyEnum.SEMANTIC.getValue()); assertEquals(6, RagChunkStrategyEnum.SEMANTIC.getValue());
assertEquals(1, ModelProviderTypeEnum.OLLAMA.getValue());
assertEquals(1, ModelProtocolTypeEnum.OPENAI_COMPATIBLE.getValue());
assertEquals(2, ModelTypeEnum.EMBEDDING.getValue());
assertEquals(1, ModelTaskTypeEnum.RAG_EMBEDDING.getValue());
assertEquals(4, ModelRouteStrategyEnum.MANUAL.getValue());
assertEquals(1, ModelCallStatusEnum.SUCCESS.getValue());
assertEquals(2, ModelHealthStatusEnum.HEALTHY.getValue());
} }
@Test @Test
/**
* 方法 enumNamesShouldBeStable用于执行业务逻辑处理。
*/
void enumNamesShouldBeStable() { void enumNamesShouldBeStable() {
assertEquals("启用", EnableStatusEnum.ENABLED.getLabel()); assertEquals("启用", EnableStatusEnum.ENABLED.getLabel());
assertEquals("禁用", EnableStatusEnum.DISABLED.getLabel()); assertEquals("禁用", EnableStatusEnum.DISABLED.getLabel());
@@ -47,9 +67,19 @@ class EnumDefinitionTests {
assertEquals("固定长度切片", RagChunkStrategyEnum.FIXED_LENGTH.getLabel()); assertEquals("固定长度切片", RagChunkStrategyEnum.FIXED_LENGTH.getLabel());
assertEquals("按分隔符切片", RagChunkStrategyEnum.DELIMITER.getLabel()); assertEquals("按分隔符切片", RagChunkStrategyEnum.DELIMITER.getLabel());
assertEquals("语义切片", RagChunkStrategyEnum.SEMANTIC.getLabel()); assertEquals("语义切片", RagChunkStrategyEnum.SEMANTIC.getLabel());
assertEquals("Ollama", ModelProviderTypeEnum.OLLAMA.getLabel());
assertEquals("OpenAI兼容协议", ModelProtocolTypeEnum.OPENAI_COMPATIBLE.getLabel());
assertEquals("向量模型", ModelTypeEnum.EMBEDDING.getLabel());
assertEquals("RAG文档向量化", ModelTaskTypeEnum.RAG_EMBEDDING.getLabel());
assertEquals("手工指定", ModelRouteStrategyEnum.MANUAL.getLabel());
assertEquals("成功", ModelCallStatusEnum.SUCCESS.getLabel());
assertEquals("健康", ModelHealthStatusEnum.HEALTHY.getLabel());
} }
@Test @Test
/**
* 方法 enumsShouldExposeStableSysEnumMetadata用于执行业务逻辑处理。
*/
void enumsShouldExposeStableSysEnumMetadata() { void enumsShouldExposeStableSysEnumMetadata() {
PersistableSysEnumDefinition chunkStrategy = RagChunkStrategyEnum.DELIMITER; PersistableSysEnumDefinition chunkStrategy = RagChunkStrategyEnum.DELIMITER;
PersistableSysEnumDefinition parseStatus = RagParseStatusEnum.PARSED; PersistableSysEnumDefinition parseStatus = RagParseStatusEnum.PARSED;
@@ -69,12 +99,22 @@ class EnumDefinitionTests {
assertEquals("common", enableStatus.getCatalog()); assertEquals("common", enableStatus.getCatalog());
assertEquals("enable_status", enableStatus.getType()); assertEquals("enable_status", enableStatus.getType());
assertEquals("启用", enableStatus.getName()); assertEquals("启用", enableStatus.getName());
PersistableSysEnumDefinition providerType = ModelProviderTypeEnum.OLLAMA;
assertEquals("model_provider", providerType.getCatalog());
assertEquals("provider_type", providerType.getType());
assertEquals("Ollama", providerType.getName());
} }
@Test @Test
/**
* 方法 ragChunkStrategyShouldResolveByIntegerValue用于执行业务逻辑处理。
*/
void ragChunkStrategyShouldResolveByIntegerValue() { void ragChunkStrategyShouldResolveByIntegerValue() {
assertEquals(RagChunkStrategyEnum.FIXED_LENGTH, RagChunkStrategyEnum.fromValue(1)); assertEquals(RagChunkStrategyEnum.FIXED_LENGTH, RagChunkStrategyEnum.fromValue(1));
assertEquals(RagChunkStrategyEnum.DELIMITER, RagChunkStrategyEnum.fromValue(5)); assertEquals(RagChunkStrategyEnum.DELIMITER, RagChunkStrategyEnum.fromValue(5));
assertThrows(IllegalArgumentException.class, () -> RagChunkStrategyEnum.fromValue(999)); assertThrows(IllegalArgumentException.class, () -> RagChunkStrategyEnum.fromValue(999));
} }
} }

View File

@@ -5,6 +5,13 @@ import com.bruce.common.domain.entity.SysEnum;
import com.bruce.common.enums.PersistableSysEnumDefinition; import com.bruce.common.enums.PersistableSysEnumDefinition;
import com.bruce.common.enums.CommonStatusEnum; import com.bruce.common.enums.CommonStatusEnum;
import com.bruce.common.enums.EnableStatusEnum; import com.bruce.common.enums.EnableStatusEnum;
import com.bruce.modelprovider.enums.ModelCallStatusEnum;
import com.bruce.modelprovider.enums.ModelHealthStatusEnum;
import com.bruce.modelprovider.enums.ModelProtocolTypeEnum;
import com.bruce.modelprovider.enums.ModelProviderTypeEnum;
import com.bruce.modelprovider.enums.ModelRouteStrategyEnum;
import com.bruce.modelprovider.enums.ModelTaskTypeEnum;
import com.bruce.modelprovider.enums.ModelTypeEnum;
import com.bruce.common.service.ISysEnumService; import com.bruce.common.service.ISysEnumService;
import com.bruce.rag.enums.RagChunkStrategyEnum; import com.bruce.rag.enums.RagChunkStrategyEnum;
import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagIndexStatusEnum;
@@ -24,13 +31,23 @@ class SysEnumDataInitTests {
private ISysEnumService sysEnumService; private ISysEnumService sysEnumService;
@Test @Test
/**
* 方法 initDefaultEnums用于执行业务逻辑处理。
*/
public void initDefaultEnums() { public void initDefaultEnums() {
List<SysEnumDefinitionSyncSupport.EnumGroup> groups = List.of( List<SysEnumDefinitionSyncSupport.EnumGroup> groups = List.of(
buildGroup(EnableStatusEnum.values()), buildGroup(EnableStatusEnum.values()),
buildGroup(CommonStatusEnum.values()), buildGroup(CommonStatusEnum.values()),
buildGroup(RagParseStatusEnum.values()), buildGroup(RagParseStatusEnum.values()),
buildGroup(RagIndexStatusEnum.values()), buildGroup(RagIndexStatusEnum.values()),
buildGroup(RagChunkStrategyEnum.values()) buildGroup(RagChunkStrategyEnum.values()),
buildGroup(ModelProviderTypeEnum.values()),
buildGroup(ModelProtocolTypeEnum.values()),
buildGroup(ModelTypeEnum.values()),
buildGroup(ModelTaskTypeEnum.values()),
buildGroup(ModelRouteStrategyEnum.values()),
buildGroup(ModelCallStatusEnum.values()),
buildGroup(ModelHealthStatusEnum.values())
); );
SysEnumDefinitionSyncSupport.validateUniqueGroupKeys(groups); SysEnumDefinitionSyncSupport.validateUniqueGroupKeys(groups);
@@ -40,7 +57,12 @@ class SysEnumDataInitTests {
} }
} }
/**
* 方法 buildGroup用于执行业务逻辑处理。
*/
private SysEnumDefinitionSyncSupport.EnumGroup buildGroup(PersistableSysEnumDefinition[] definitions) { private SysEnumDefinitionSyncSupport.EnumGroup buildGroup(PersistableSysEnumDefinition[] definitions) {
return SysEnumDefinitionSyncSupport.groupOf(List.of(definitions)); return SysEnumDefinitionSyncSupport.groupOf(List.of(definitions));
} }
} }

View File

@@ -0,0 +1,100 @@
package com.bruce.modelprovider;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.controller.ModelCallLogController;
import com.bruce.modelprovider.controller.ModelConfigController;
import com.bruce.modelprovider.controller.ModelProviderController;
import com.bruce.modelprovider.controller.ModelRouteRuleController;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.ModelProvider;
import com.bruce.modelprovider.entity.ModelRouteRule;
import com.bruce.modelprovider.entity.RagStoreModelConfig;
import com.bruce.modelprovider.mapper.ModelCallLogMapper;
import com.bruce.modelprovider.mapper.ModelConfigMapper;
import com.bruce.modelprovider.mapper.ModelProviderMapper;
import com.bruce.modelprovider.mapper.ModelRouteRuleMapper;
import com.bruce.modelprovider.mapper.RagStoreModelConfigMapper;
import com.bruce.modelprovider.service.IModelCallLogService;
import com.bruce.modelprovider.service.IModelConfigService;
import com.bruce.modelprovider.service.IModelProviderService;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import com.bruce.modelprovider.service.impl.ModelCallLogServiceImpl;
import com.bruce.modelprovider.service.impl.ModelConfigServiceImpl;
import com.bruce.modelprovider.service.impl.ModelProviderServiceImpl;
import com.bruce.modelprovider.service.impl.ModelRouteRuleServiceImpl;
import com.bruce.modelprovider.service.impl.RagStoreModelConfigServiceImpl;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
class ModelProviderComponentStructureTests {
@Test
/**
* 方法 modelProviderComponentsShouldReuseMybatisPlusBaseTypes用于执行业务逻辑处理。
*/
void modelProviderComponentsShouldReuseMybatisPlusBaseTypes() {
assertTrue(BaseMapper.class.isAssignableFrom(ModelProviderMapper.class));
assertTrue(BaseMapper.class.isAssignableFrom(ModelConfigMapper.class));
assertTrue(BaseMapper.class.isAssignableFrom(ModelRouteRuleMapper.class));
assertTrue(BaseMapper.class.isAssignableFrom(ModelCallLogMapper.class));
assertTrue(BaseMapper.class.isAssignableFrom(RagStoreModelConfigMapper.class));
assertTrue(IService.class.isAssignableFrom(IModelProviderService.class));
assertTrue(IService.class.isAssignableFrom(IModelConfigService.class));
assertTrue(IService.class.isAssignableFrom(IModelRouteRuleService.class));
assertTrue(IService.class.isAssignableFrom(IModelCallLogService.class));
assertTrue(IService.class.isAssignableFrom(IRagStoreModelConfigService.class));
assertTrue(ServiceImpl.class.isAssignableFrom(ModelProviderServiceImpl.class));
assertTrue(ServiceImpl.class.isAssignableFrom(ModelConfigServiceImpl.class));
assertTrue(ServiceImpl.class.isAssignableFrom(ModelRouteRuleServiceImpl.class));
assertTrue(ServiceImpl.class.isAssignableFrom(ModelCallLogServiceImpl.class));
assertTrue(ServiceImpl.class.isAssignableFrom(RagStoreModelConfigServiceImpl.class));
}
@Test
void controllersShouldExposeRequestResult() throws NoSuchMethodException {
Method providerQuery = ModelProviderController.class.getMethod("query");
Method configQuery = ModelConfigController.class.getMethod("query");
Method routeQuery = ModelRouteRuleController.class.getMethod("query");
Method logQuery = ModelCallLogController.class.getMethod("query", com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest.class);
assertEquals(RequestResult.class, providerQuery.getReturnType());
assertEquals(RequestResult.class, configQuery.getReturnType());
assertEquals(RequestResult.class, routeQuery.getReturnType());
assertEquals(RequestResult.class, logQuery.getReturnType());
}
@Test
/**
* 方法 modelProviderResponseShouldHideApiKeyCipher用于执行业务逻辑处理。
*/
void modelProviderResponseShouldHideApiKeyCipher() {
ModelProvider provider = new ModelProvider();
provider.setProviderCode("sf");
provider.setApiKeyCipher("secret");
ModelProviderResponse response = ModelProviderResponse.fromEntity(provider);
assertTrue(response.getHasApiKey());
}
@Test
void entitiesShouldMapCoreFields() throws NoSuchFieldException {
assertEquals(String.class, ModelProvider.class.getDeclaredField("providerCode").getType());
assertEquals(String.class, ModelConfig.class.getDeclaredField("modelCode").getType());
assertEquals(Long.class, ModelRouteRule.class.getDeclaredField("primaryModelId").getType());
assertEquals(String.class, ModelCallLog.class.getDeclaredField("requestId").getType());
assertEquals(Long.class, RagStoreModelConfig.class.getDeclaredField("embeddingModelId").getType());
}
}