Compare commits
2 Commits
cc745cad47
...
21c9eaa44d
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c9eaa44d | |||
| 5d7ca5b31f |
222
docs/MODEL_PROVIDER_SCHEMA.sql
Normal file
222
docs/MODEL_PROVIDER_SCHEMA.sql
Normal 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 '备注';
|
||||||
62
frontend/src/api/__tests__/modelProvider.spec.ts
Normal file
62
frontend/src/api/__tests__/modelProvider.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkModelProviderHealth,
|
||||||
|
deleteModelConfig,
|
||||||
|
deleteModelProvider,
|
||||||
|
deleteModelRouteRule,
|
||||||
|
getRagStoreModelConfig,
|
||||||
|
queryModelCallLogs,
|
||||||
|
queryModelConfigs,
|
||||||
|
queryModelProviders,
|
||||||
|
queryModelRouteRules,
|
||||||
|
rebuildRagStoreIndex,
|
||||||
|
saveModelConfig,
|
||||||
|
saveModelProvider,
|
||||||
|
saveModelRouteRule,
|
||||||
|
saveRagStoreModelConfig,
|
||||||
|
} from '../modelProvider';
|
||||||
|
import { get, post } from '../request';
|
||||||
|
|
||||||
|
vi.mock('../request', () => ({
|
||||||
|
get: vi.fn(),
|
||||||
|
post: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('model provider api', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls model platform endpoints with expected params', () => {
|
||||||
|
queryModelProviders();
|
||||||
|
saveModelProvider({ providerCode: 'OPENAI_MAIN' } as never);
|
||||||
|
deleteModelProvider('1');
|
||||||
|
checkModelProviderHealth('1');
|
||||||
|
queryModelConfigs();
|
||||||
|
saveModelConfig({ modelCode: 'EMB_1' } as never);
|
||||||
|
deleteModelConfig('2');
|
||||||
|
queryModelRouteRules();
|
||||||
|
saveModelRouteRule({ routeCode: 'RAG_GLOBAL' } as never);
|
||||||
|
deleteModelRouteRule('3');
|
||||||
|
queryModelCallLogs({ taskType: 'RAG_EMBEDDING' });
|
||||||
|
getRagStoreModelConfig('10');
|
||||||
|
saveRagStoreModelConfig({ storeId: '10', embeddingModelId: '2' } as never);
|
||||||
|
rebuildRagStoreIndex('10');
|
||||||
|
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/providers/query');
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/providers/save', { providerCode: 'OPENAI_MAIN' });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/providers/delete', undefined, { params: { id: '1' } });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/providers/checkHealth', undefined, { params: { id: '1' } });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/configs/query');
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/configs/save', { modelCode: 'EMB_1' });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/configs/delete', undefined, { params: { id: '2' } });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/routes/query');
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/routes/save', { routeCode: 'RAG_GLOBAL' });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/routes/delete', undefined, { params: { id: '3' } });
|
||||||
|
expect(post).toHaveBeenCalledWith('/model/call-logs/query', { taskType: 'RAG_EMBEDDING' });
|
||||||
|
expect(get).toHaveBeenCalledWith('/rag/store/modelConfig', { params: { storeId: '10' } });
|
||||||
|
expect(post).toHaveBeenCalledWith('/rag/store/modelConfig/save', { storeId: '10', embeddingModelId: '2' });
|
||||||
|
expect(post).toHaveBeenCalledWith('/rag/store/rebuildIndex', undefined, { params: { storeId: '10' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
52
frontend/src/api/modelEnums.ts
Normal file
52
frontend/src/api/modelEnums.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { listForManagement, type SysEnum } from './sysEnums';
|
||||||
|
|
||||||
|
export interface EnumOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enumCache = new Map<string, EnumOption[]>();
|
||||||
|
|
||||||
|
function buildCacheKey(catalog: string, type: string) {
|
||||||
|
return `${catalog}::${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOption(item: SysEnum): EnumOption {
|
||||||
|
const value = item.strvalue && item.strvalue.trim()
|
||||||
|
? item.strvalue
|
||||||
|
: item.name;
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadEnumOptions(catalog: string, type: string, forceRefresh = false) {
|
||||||
|
const cacheKey = buildCacheKey(catalog, type);
|
||||||
|
if (!forceRefresh && enumCache.has(cacheKey)) {
|
||||||
|
return enumCache.get(cacheKey) ?? [];
|
||||||
|
}
|
||||||
|
const response = await listForManagement({ catalog, type, keyword: '' });
|
||||||
|
const options = (response.data ?? []).map(mapOption);
|
||||||
|
enumCache.set(cacheKey, options);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadModelProviderEnumOptions(forceRefresh = false) {
|
||||||
|
const catalog = 'model_provider';
|
||||||
|
const types = [
|
||||||
|
'provider_type',
|
||||||
|
'protocol_type',
|
||||||
|
'auth_type',
|
||||||
|
'model_type',
|
||||||
|
'task_type',
|
||||||
|
'route_strategy',
|
||||||
|
'health_status',
|
||||||
|
'call_status',
|
||||||
|
'match_scope',
|
||||||
|
];
|
||||||
|
const entries = await Promise.all(
|
||||||
|
types.map(async (type) => [type, await loadEnumOptions(catalog, type, forceRefresh)] as const),
|
||||||
|
);
|
||||||
|
return Object.fromEntries(entries) as Record<string, EnumOption[]>;
|
||||||
|
}
|
||||||
141
frontend/src/api/modelProvider.ts
Normal file
141
frontend/src/api/modelProvider.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { get, post } from './request';
|
||||||
|
|
||||||
|
export interface ModelProvider {
|
||||||
|
id?: string;
|
||||||
|
providerCode: string;
|
||||||
|
providerName: string;
|
||||||
|
providerType: string;
|
||||||
|
protocolType: string;
|
||||||
|
baseUrl: string;
|
||||||
|
authType: string;
|
||||||
|
secretRef?: string;
|
||||||
|
hasApiKey?: boolean;
|
||||||
|
timeoutMs: number;
|
||||||
|
priority: number;
|
||||||
|
enabled: boolean;
|
||||||
|
healthStatus?: string;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelConfig {
|
||||||
|
id?: string;
|
||||||
|
providerId: string;
|
||||||
|
modelCode: string;
|
||||||
|
modelName: string;
|
||||||
|
upstreamModel: string;
|
||||||
|
modelType: string;
|
||||||
|
embeddingDimension?: number;
|
||||||
|
localModel: boolean;
|
||||||
|
defaultModel: boolean;
|
||||||
|
optionsJson?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelRouteRule {
|
||||||
|
id?: string;
|
||||||
|
routeCode: string;
|
||||||
|
routeName: string;
|
||||||
|
taskType: string;
|
||||||
|
matchScope: string;
|
||||||
|
scopeId?: string;
|
||||||
|
primaryModelId: string;
|
||||||
|
fallbackModelIdsJson?: string;
|
||||||
|
routeStrategy: string;
|
||||||
|
maxLatencyMs?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelCallLog {
|
||||||
|
id?: string;
|
||||||
|
requestId: string;
|
||||||
|
providerId?: string;
|
||||||
|
modelId?: string;
|
||||||
|
taskType: string;
|
||||||
|
bizType?: string;
|
||||||
|
bizId?: string;
|
||||||
|
callType: string;
|
||||||
|
status: string;
|
||||||
|
durationMs?: number;
|
||||||
|
errorCode?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelCallLogQueryRequest {
|
||||||
|
taskType?: string;
|
||||||
|
providerId?: string;
|
||||||
|
modelId?: string;
|
||||||
|
status?: string;
|
||||||
|
bizType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RagStoreModelConfig {
|
||||||
|
id?: string;
|
||||||
|
storeId: string;
|
||||||
|
embeddingModelId: string;
|
||||||
|
embeddingDimension: number;
|
||||||
|
chunkStrategy?: number;
|
||||||
|
chunkSize?: number;
|
||||||
|
chunkOverlap?: number;
|
||||||
|
delimiter?: string;
|
||||||
|
active?: boolean;
|
||||||
|
indexVersion?: number;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryModelProviders() {
|
||||||
|
return post<ModelProvider[]>('/model/providers/query');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveModelProvider(data: Partial<ModelProvider> & { id?: string }) {
|
||||||
|
return post<boolean>('/model/providers/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteModelProvider(id: string) {
|
||||||
|
return post<boolean>('/model/providers/delete', undefined, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkModelProviderHealth(id: string) {
|
||||||
|
return post<boolean>('/model/providers/checkHealth', undefined, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryModelConfigs() {
|
||||||
|
return post<ModelConfig[]>('/model/configs/query');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveModelConfig(data: Partial<ModelConfig> & { id?: string }) {
|
||||||
|
return post<boolean>('/model/configs/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteModelConfig(id: string) {
|
||||||
|
return post<boolean>('/model/configs/delete', undefined, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryModelRouteRules() {
|
||||||
|
return post<ModelRouteRule[]>('/model/routes/query');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveModelRouteRule(data: Partial<ModelRouteRule> & { id?: string }) {
|
||||||
|
return post<boolean>('/model/routes/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteModelRouteRule(id: string) {
|
||||||
|
return post<boolean>('/model/routes/delete', undefined, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryModelCallLogs(query?: ModelCallLogQueryRequest) {
|
||||||
|
return post<ModelCallLog[], ModelCallLogQueryRequest | undefined>('/model/call-logs/query', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRagStoreModelConfig(storeId: string) {
|
||||||
|
return get<RagStoreModelConfig>('/rag/store/modelConfig', { params: { storeId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveRagStoreModelConfig(data: Partial<RagStoreModelConfig> & { storeId: string }) {
|
||||||
|
return post<boolean>('/rag/store/modelConfig/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rebuildRagStoreIndex(storeId: string) {
|
||||||
|
return post<boolean>('/rag/store/rebuildIndex', undefined, { params: { storeId } });
|
||||||
|
}
|
||||||
@@ -6,10 +6,18 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Histogram,
|
Histogram,
|
||||||
List,
|
List,
|
||||||
|
Setting,
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
|
|
||||||
const menuItems = [
|
const systemMenuItems = [
|
||||||
{ path: '/system/enums', label: '系统枚举', icon: Grid },
|
{ path: '/system/enums', label: '系统枚举', icon: Grid },
|
||||||
|
{ path: '/system/model/providers', label: '模型服务商', icon: Setting },
|
||||||
|
{ path: '/system/model/configs', label: '模型配置', icon: Setting },
|
||||||
|
{ path: '/system/model/routes', label: '路由规则', icon: Setting },
|
||||||
|
{ path: '/system/model/call-logs', label: '调用日志', icon: Setting },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ragMenuItems = [
|
||||||
{ path: '/rag/stores', label: '知识库', icon: Collection },
|
{ path: '/rag/stores', label: '知识库', icon: Collection },
|
||||||
{ path: '/rag/workbench', label: 'RAG工作台', icon: Histogram },
|
{ path: '/rag/workbench', label: 'RAG工作台', icon: Histogram },
|
||||||
{ path: '/rag/documents', label: '知识文档', icon: Document },
|
{ path: '/rag/documents', label: '知识文档', icon: Document },
|
||||||
@@ -28,12 +36,24 @@ const menuItems = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-menu class="side-menu" :default-active="$route.path" router>
|
<el-menu class="side-menu" :default-active="$route.path" router>
|
||||||
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
|
<el-sub-menu index="system">
|
||||||
<el-icon>
|
<template #title>系统管理</template>
|
||||||
<component :is="item.icon" />
|
<el-menu-item v-for="item in systemMenuItems" :key="item.path" :index="item.path">
|
||||||
</el-icon>
|
<el-icon>
|
||||||
<span>{{ item.label }}</span>
|
<component :is="item.icon" />
|
||||||
</el-menu-item>
|
</el-icon>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
<el-sub-menu index="rag">
|
||||||
|
<template #title>RAG</template>
|
||||||
|
<el-menu-item v-for="item in ragMenuItems" :key="item.path" :index="item.path">
|
||||||
|
<el-icon>
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } fr
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||||
|
import {
|
||||||
|
getRagStoreModelConfig,
|
||||||
|
queryModelConfigs,
|
||||||
|
rebuildRagStoreIndex,
|
||||||
|
saveRagStoreModelConfig,
|
||||||
|
type ModelConfig,
|
||||||
|
} from '@/api/modelProvider';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteRagStore,
|
deleteRagStore,
|
||||||
@@ -28,6 +36,20 @@ const activeStoreId = ref<string | null>(null);
|
|||||||
const activeStore = ref<RagStore | null>(null);
|
const activeStore = ref<RagStore | null>(null);
|
||||||
const pageOverview = ref<RagStoreOverview | null>(null);
|
const pageOverview = ref<RagStoreOverview | null>(null);
|
||||||
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
|
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
|
||||||
|
const embeddingModels = ref<ModelConfig[]>([]);
|
||||||
|
const ragConfigLoading = ref(false);
|
||||||
|
const ragConfigSaving = ref(false);
|
||||||
|
const ragConfig = reactive({
|
||||||
|
id: '',
|
||||||
|
embeddingModelId: '',
|
||||||
|
embeddingDimension: 1024,
|
||||||
|
chunkStrategy: null as number | null,
|
||||||
|
chunkSize: null as number | null,
|
||||||
|
chunkOverlap: null as number | null,
|
||||||
|
delimiter: '',
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
const chunkStrategyOptions = ref<EnumOption[]>([]);
|
||||||
|
|
||||||
const queryForm = reactive({
|
const queryForm = reactive({
|
||||||
storeName: '',
|
storeName: '',
|
||||||
@@ -91,6 +113,8 @@ async function loadStores(preferredStoreId?: string | null) {
|
|||||||
activeStoreId.value = null;
|
activeStoreId.value = null;
|
||||||
activeStore.value = null;
|
activeStore.value = null;
|
||||||
activeStoreDocumentOverview.value = null;
|
activeStoreDocumentOverview.value = null;
|
||||||
|
ragConfig.id = '';
|
||||||
|
ragConfig.embeddingModelId = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +147,7 @@ async function selectStore(storeId: string) {
|
|||||||
]);
|
]);
|
||||||
activeStore.value = storeResponse.data ?? null;
|
activeStore.value = storeResponse.data ?? null;
|
||||||
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
|
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
|
||||||
|
await loadRagModelConfig(storeId);
|
||||||
} finally {
|
} finally {
|
||||||
detailLoading.value = false;
|
detailLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -223,10 +248,6 @@ async function removeStore() {
|
|||||||
await loadStores();
|
await loadStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFutureMessage(actionName: string) {
|
|
||||||
ElMessage.info(`${actionName} 会在下一批接口里补齐`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openBatchUploadDialog() {
|
function openBatchUploadDialog() {
|
||||||
if (!activeStore.value?.id) {
|
if (!activeStore.value?.id) {
|
||||||
ElMessage.warning('请选择知识库');
|
ElMessage.warning('请选择知识库');
|
||||||
@@ -252,13 +273,85 @@ async function refreshAfterUpload() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadRagModelConfig(storeId: string) {
|
||||||
|
ragConfigLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getRagStoreModelConfig(storeId);
|
||||||
|
const data = response.data;
|
||||||
|
ragConfig.id = data?.id ?? '';
|
||||||
|
ragConfig.embeddingModelId = data?.embeddingModelId ?? '';
|
||||||
|
ragConfig.embeddingDimension = data?.embeddingDimension ?? 1024;
|
||||||
|
ragConfig.chunkStrategy = data?.chunkStrategy ?? null;
|
||||||
|
ragConfig.chunkSize = data?.chunkSize ?? null;
|
||||||
|
ragConfig.chunkOverlap = data?.chunkOverlap ?? null;
|
||||||
|
ragConfig.delimiter = data?.delimiter ?? '';
|
||||||
|
ragConfig.remark = data?.remark ?? '';
|
||||||
|
} finally {
|
||||||
|
ragConfigLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStoreModelConfig() {
|
||||||
|
if (!activeStoreId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ragConfig.embeddingModelId) {
|
||||||
|
ElMessage.warning('请选择 Embedding 模型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ragConfigSaving.value = true;
|
||||||
|
try {
|
||||||
|
await saveRagStoreModelConfig({
|
||||||
|
id: ragConfig.id || undefined,
|
||||||
|
storeId: activeStoreId.value,
|
||||||
|
embeddingModelId: ragConfig.embeddingModelId,
|
||||||
|
embeddingDimension: ragConfig.embeddingDimension,
|
||||||
|
chunkStrategy: ragConfig.chunkStrategy ?? undefined,
|
||||||
|
chunkSize: ragConfig.chunkSize ?? undefined,
|
||||||
|
chunkOverlap: ragConfig.chunkOverlap ?? undefined,
|
||||||
|
delimiter: ragConfig.delimiter || undefined,
|
||||||
|
remark: ragConfig.remark || undefined,
|
||||||
|
});
|
||||||
|
ElMessage.success('知识库模型配置已保存');
|
||||||
|
await loadRagModelConfig(activeStoreId.value);
|
||||||
|
} finally {
|
||||||
|
ragConfigSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerRebuildIndex() {
|
||||||
|
if (!activeStoreId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await rebuildRagStoreIndex(activeStoreId.value);
|
||||||
|
ElMessage.success('已触发重建索引');
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncEmbeddingDimensionFromModel() {
|
||||||
|
const model = embeddingModels.value.find((item) => item.id === ragConfig.embeddingModelId);
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (model.embeddingDimension) {
|
||||||
|
ragConfig.embeddingDimension = model.embeddingDimension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusTagType(status?: string | null) {
|
function getStatusTagType(status?: string | null) {
|
||||||
return status === '启用' ? 'success' : 'info';
|
return status === '启用' ? 'success' : 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadOverview();
|
Promise.all([
|
||||||
loadStores();
|
loadOverview(),
|
||||||
|
loadStores(),
|
||||||
|
queryModelConfigs().then((response) => {
|
||||||
|
embeddingModels.value = (response.data ?? []).filter((item) => item.modelType === 'EMBEDDING');
|
||||||
|
}),
|
||||||
|
loadModelProviderEnumOptions().then((enums) => {
|
||||||
|
chunkStrategyOptions.value = enums.chunk_strategy ?? [];
|
||||||
|
}),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -351,7 +444,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
批量导入文件
|
批量导入文件
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :icon="FolderAdd" @click="showFutureMessage('重建索引')">重建索引</el-button>
|
<el-button :icon="FolderAdd" @click="triggerRebuildIndex">重建索引</el-button>
|
||||||
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
|
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,12 +508,56 @@ onMounted(() => {
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="detail-card detail-card--placeholder">
|
<article class="detail-card">
|
||||||
<div class="detail-card__header">
|
<div class="detail-card__header">
|
||||||
<h4>检索配置</h4>
|
<h4>检索配置</h4>
|
||||||
<span>下一批接口补充</span>
|
<el-button type="primary" link :loading="ragConfigSaving" @click="saveStoreModelConfig">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-loading="ragConfigLoading" class="rag-config-form">
|
||||||
|
<el-form :model="ragConfig" label-width="112px">
|
||||||
|
<el-form-item label="Embedding 模型" required>
|
||||||
|
<el-select
|
||||||
|
v-model="ragConfig.embeddingModelId"
|
||||||
|
placeholder="请选择模型"
|
||||||
|
@change="syncEmbeddingDimensionFromModel"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in embeddingModels"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.modelName}(${item.modelCode})`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="向量维度">
|
||||||
|
<el-input-number v-model="ragConfig.embeddingDimension" :min="1" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="切片策略">
|
||||||
|
<el-select v-model="ragConfig.chunkStrategy" clearable placeholder="可选">
|
||||||
|
<el-option
|
||||||
|
v-for="item in chunkStrategyOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="Number(item.value)"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="切片大小">
|
||||||
|
<el-input-number v-model="ragConfig.chunkSize" :min="1" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="切片重叠">
|
||||||
|
<el-input-number v-model="ragConfig.chunkOverlap" :min="0" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分隔符">
|
||||||
|
<el-input v-model="ragConfig.delimiter" placeholder="可选,如 \\n\\n" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="ragConfig.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<el-empty description="检索模式、Embedding 模型、Chunk 参数待后端补充" />
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="detail-card detail-card--placeholder">
|
<article class="detail-card detail-card--placeholder">
|
||||||
@@ -721,6 +858,11 @@ onMounted(() => {
|
|||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rag-config-form :deep(.el-select),
|
||||||
|
.rag-config-form :deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
@media (max-width: 1280px) {
|
||||||
.overview-grid {
|
.overview-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import {
|
|||||||
queryRagStores,
|
queryRagStores,
|
||||||
saveRagStore,
|
saveRagStore,
|
||||||
} from '@/api/ragStores';
|
} from '@/api/ragStores';
|
||||||
|
import {
|
||||||
|
getRagStoreModelConfig,
|
||||||
|
queryModelConfigs,
|
||||||
|
rebuildRagStoreIndex,
|
||||||
|
saveRagStoreModelConfig,
|
||||||
|
} from '@/api/modelProvider';
|
||||||
|
|
||||||
const routerPush = vi.hoisted(() => vi.fn());
|
const routerPush = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
@@ -24,6 +30,52 @@ vi.mock('@/api/ragDocuments', () => ({
|
|||||||
batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/api/modelEnums', () => ({
|
||||||
|
loadModelProviderEnumOptions: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
chunk_strategy: [
|
||||||
|
{ label: '固定长度', value: '1' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/api/modelProvider', () => ({
|
||||||
|
queryModelConfigs: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: '88',
|
||||||
|
providerId: '1',
|
||||||
|
modelCode: 'TEXT_EMBED_3_LARGE',
|
||||||
|
modelName: 'text-embedding-3-large',
|
||||||
|
modelType: 'EMBEDDING',
|
||||||
|
embeddingDimension: 1024,
|
||||||
|
localModel: false,
|
||||||
|
defaultModel: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
getRagStoreModelConfig: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
id: '9',
|
||||||
|
storeId: '1',
|
||||||
|
embeddingModelId: '88',
|
||||||
|
embeddingDimension: 1024,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
saveRagStoreModelConfig: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||||
|
rebuildRagStoreIndex: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/api/ragStores', () => ({
|
vi.mock('@/api/ragStores', () => ({
|
||||||
getRagStoreOverview: vi.fn(() =>
|
getRagStoreOverview: vi.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
@@ -142,6 +194,8 @@ describe('RagStoresPage', () => {
|
|||||||
expect(queryRagStores).toHaveBeenCalled();
|
expect(queryRagStores).toHaveBeenCalled();
|
||||||
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
||||||
expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1');
|
expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1');
|
||||||
|
expect(queryModelConfigs).toHaveBeenCalled();
|
||||||
|
expect(getRagStoreModelConfig).toHaveBeenCalledWith('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters stores by name and updates detail when a store is selected', async () => {
|
it('filters stores by name and updates detail when a store is selected', async () => {
|
||||||
@@ -225,4 +279,23 @@ describe('RagStoresPage', () => {
|
|||||||
expect(wrapper.text()).toContain('拖拽文件到此处');
|
expect(wrapper.text()).toContain('拖拽文件到此处');
|
||||||
expect(wrapper.text()).toContain('产品制度库');
|
expect(wrapper.text()).toContain('产品制度库');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('saves store model config and triggers rebuild index', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
const saveButtons = wrapper.findAll('button').filter((button) => button.text().includes('保存配置'));
|
||||||
|
await saveButtons[0]?.trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
expect(saveRagStoreModelConfig).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const rebuildButtons = wrapper.findAll('button').filter((button) => button.text().includes('重建索引'));
|
||||||
|
await rebuildButtons[0]?.trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
expect(rebuildRagStoreIndex).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
160
frontend/src/pages/system/ModelCallLogsPage.vue
Normal file
160
frontend/src/pages/system/ModelCallLogsPage.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RefreshRight, Search } from '@element-plus/icons-vue';
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||||
|
import {
|
||||||
|
queryModelCallLogs,
|
||||||
|
queryModelConfigs,
|
||||||
|
queryModelProviders,
|
||||||
|
type ModelCallLog,
|
||||||
|
type ModelConfig,
|
||||||
|
type ModelProvider,
|
||||||
|
} from '@/api/modelProvider';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const logs = ref<ModelCallLog[]>([]);
|
||||||
|
const providers = ref<ModelProvider[]>([]);
|
||||||
|
const models = ref<ModelConfig[]>([]);
|
||||||
|
const taskTypeOptions = ref<EnumOption[]>([]);
|
||||||
|
const statusOptions = ref<EnumOption[]>([]);
|
||||||
|
const enumError = ref('');
|
||||||
|
|
||||||
|
const queryForm = reactive({
|
||||||
|
taskType: '',
|
||||||
|
providerId: '',
|
||||||
|
modelId: '',
|
||||||
|
status: '',
|
||||||
|
bizType: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBaseData(forceRefresh = false) {
|
||||||
|
enumError.value = '';
|
||||||
|
try {
|
||||||
|
const [enumResult, providerResult, modelResult] = await Promise.all([
|
||||||
|
loadModelProviderEnumOptions(forceRefresh),
|
||||||
|
queryModelProviders(),
|
||||||
|
queryModelConfigs(),
|
||||||
|
]);
|
||||||
|
taskTypeOptions.value = enumResult.task_type ?? [];
|
||||||
|
statusOptions.value = enumResult.call_status ?? [];
|
||||||
|
providers.value = providerResult.data ?? [];
|
||||||
|
models.value = modelResult.data ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
enumError.value = error instanceof Error ? error.message : '基础数据加载失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLogs() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await queryModelCallLogs({
|
||||||
|
taskType: queryForm.taskType || undefined,
|
||||||
|
providerId: queryForm.providerId || undefined,
|
||||||
|
modelId: queryForm.modelId || undefined,
|
||||||
|
status: queryForm.status || undefined,
|
||||||
|
bizType: queryForm.bizType || undefined,
|
||||||
|
});
|
||||||
|
logs.value = response.data ?? [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetQuery() {
|
||||||
|
queryForm.taskType = '';
|
||||||
|
queryForm.providerId = '';
|
||||||
|
queryForm.modelId = '';
|
||||||
|
queryForm.status = '';
|
||||||
|
queryForm.bizType = '';
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerName(providerId?: string) {
|
||||||
|
return providers.value.find((item) => item.id === providerId)?.providerName ?? providerId ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelName(modelId?: string) {
|
||||||
|
const model = models.value.find((item) => item.id === modelId);
|
||||||
|
return model?.modelName ?? model?.modelCode ?? modelId ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadBaseData();
|
||||||
|
await loadLogs();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page-panel">
|
||||||
|
<div class="page-panel__header">
|
||||||
|
<h2>调用日志</h2>
|
||||||
|
<span>Call Logs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-alert v-if="enumError" type="error" :closable="false" :title="`基础数据加载失败:${enumError}`" show-icon />
|
||||||
|
<el-form :model="queryForm" inline>
|
||||||
|
<el-form-item label="任务类型">
|
||||||
|
<el-select v-model="queryForm.taskType" clearable placeholder="全部">
|
||||||
|
<el-option v-for="item in taskTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="服务商">
|
||||||
|
<el-select v-model="queryForm.providerId" clearable placeholder="全部">
|
||||||
|
<el-option
|
||||||
|
v-for="provider in providers"
|
||||||
|
:key="provider.id"
|
||||||
|
:label="provider.providerName"
|
||||||
|
:value="provider.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模型">
|
||||||
|
<el-select v-model="queryForm.modelId" clearable placeholder="全部">
|
||||||
|
<el-option v-for="model in models" :key="model.id" :label="model.modelName" :value="model.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="queryForm.status" clearable placeholder="全部">
|
||||||
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="业务类型">
|
||||||
|
<el-input v-model="queryForm.bizType" clearable placeholder="如 RAG" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :icon="Search" @click="loadLogs">查询</el-button>
|
||||||
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
|
<el-button :icon="RefreshRight" @click="loadBaseData(true)">重试基础数据</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="logs" row-key="id">
|
||||||
|
<el-table-column prop="requestId" label="请求ID" min-width="150" />
|
||||||
|
<el-table-column label="服务商" min-width="120">
|
||||||
|
<template #default="{ row }">{{ providerName(row.providerId) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="模型" min-width="120">
|
||||||
|
<template #default="{ row }">{{ modelName(row.modelId) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="taskType" label="任务类型" min-width="120" />
|
||||||
|
<el-table-column prop="bizType" label="业务类型" min-width="100" />
|
||||||
|
<el-table-column prop="bizId" label="业务ID" min-width="120" />
|
||||||
|
<el-table-column prop="callType" label="调用类型" min-width="100" />
|
||||||
|
<el-table-column prop="status" label="状态" min-width="90" />
|
||||||
|
<el-table-column prop="durationMs" label="耗时(ms)" width="100" />
|
||||||
|
<el-table-column prop="errorCode" label="错误码" min-width="100" />
|
||||||
|
<el-table-column prop="errorMessage" label="错误摘要" min-width="180" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
padding: 16px 22px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
243
frontend/src/pages/system/ModelConfigsPage.vue
Normal file
243
frontend/src/pages/system/ModelConfigsPage.vue
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||||
|
import {
|
||||||
|
deleteModelConfig,
|
||||||
|
queryModelConfigs,
|
||||||
|
queryModelProviders,
|
||||||
|
saveModelConfig,
|
||||||
|
type ModelConfig,
|
||||||
|
type ModelProvider,
|
||||||
|
} from '@/api/modelProvider';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const models = ref<ModelConfig[]>([]);
|
||||||
|
const providers = ref<ModelProvider[]>([]);
|
||||||
|
const modelTypeOptions = ref<EnumOption[]>([]);
|
||||||
|
const enumError = ref('');
|
||||||
|
|
||||||
|
const editForm = reactive<ModelConfig>({
|
||||||
|
providerId: '',
|
||||||
|
modelCode: '',
|
||||||
|
modelName: '',
|
||||||
|
upstreamModel: '',
|
||||||
|
modelType: '',
|
||||||
|
embeddingDimension: 1024,
|
||||||
|
localModel: false,
|
||||||
|
defaultModel: false,
|
||||||
|
optionsJson: '{}',
|
||||||
|
enabled: true,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => (editForm.id ? '编辑模型' : '新增模型'));
|
||||||
|
|
||||||
|
function resetForm(row?: ModelConfig) {
|
||||||
|
editForm.id = row?.id;
|
||||||
|
editForm.providerId = row?.providerId ?? providers.value[0]?.id ?? '';
|
||||||
|
editForm.modelCode = row?.modelCode ?? '';
|
||||||
|
editForm.modelName = row?.modelName ?? '';
|
||||||
|
editForm.upstreamModel = row?.upstreamModel ?? '';
|
||||||
|
editForm.modelType = row?.modelType ?? modelTypeOptions.value[0]?.value ?? '';
|
||||||
|
editForm.embeddingDimension = row?.embeddingDimension ?? 1024;
|
||||||
|
editForm.localModel = row?.localModel ?? false;
|
||||||
|
editForm.defaultModel = row?.defaultModel ?? false;
|
||||||
|
editForm.optionsJson = row?.optionsJson ?? '{}';
|
||||||
|
editForm.enabled = row?.enabled ?? true;
|
||||||
|
editForm.remark = row?.remark ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBaseData(forceRefresh = false) {
|
||||||
|
enumError.value = '';
|
||||||
|
try {
|
||||||
|
const [enumResult, providerResult] = await Promise.all([
|
||||||
|
loadModelProviderEnumOptions(forceRefresh),
|
||||||
|
queryModelProviders(),
|
||||||
|
]);
|
||||||
|
modelTypeOptions.value = enumResult.model_type ?? [];
|
||||||
|
providers.value = providerResult.data ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
enumError.value = error instanceof Error ? error.message : '基础数据加载失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModels() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await queryModelConfigs();
|
||||||
|
models.value = response.data ?? [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
resetForm();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(row: ModelConfig) {
|
||||||
|
resetForm(row);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitModel() {
|
||||||
|
if (!editForm.providerId || !editForm.modelCode || !editForm.modelName || !editForm.modelType) {
|
||||||
|
ElMessage.warning('请填写服务商、模型编码、模型名称和模型类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await saveModelConfig({ ...editForm });
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
await loadModels();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeModel(row: ModelConfig) {
|
||||||
|
if (!row.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确认删除模型「${row.modelName}」?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
await deleteModelConfig(row.id);
|
||||||
|
ElMessage.success('已删除');
|
||||||
|
await loadModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerName(providerId?: string) {
|
||||||
|
const provider = providers.value.find((item) => item.id === providerId);
|
||||||
|
return provider?.providerName ?? providerId ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadBaseData();
|
||||||
|
resetForm();
|
||||||
|
await loadModels();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page-panel">
|
||||||
|
<div class="page-panel__header">
|
||||||
|
<h2>模型配置</h2>
|
||||||
|
<span>Models</span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-alert v-if="enumError" type="error" :closable="false" :title="`基础数据加载失败:${enumError}`" show-icon />
|
||||||
|
<div class="toolbar__actions">
|
||||||
|
<el-button @click="loadBaseData(true)">重试基础数据</el-button>
|
||||||
|
<el-button :icon="RefreshRight" @click="loadModels">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增模型</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="models" row-key="id">
|
||||||
|
<el-table-column prop="modelCode" label="模型编码" min-width="140" />
|
||||||
|
<el-table-column prop="modelName" label="模型名称" min-width="140" />
|
||||||
|
<el-table-column label="服务商" min-width="130">
|
||||||
|
<template #default="{ row }">{{ providerName(row.providerId) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="upstreamModel" label="上游模型" min-width="140" />
|
||||||
|
<el-table-column prop="modelType" label="模型类型" min-width="110" />
|
||||||
|
<el-table-column prop="embeddingDimension" label="向量维度" width="100" />
|
||||||
|
<el-table-column label="本地模型" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.localModel ? 'success' : 'info'">{{ row.localModel ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="默认模型" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.defaultModel ? 'success' : 'info'">{{ row.defaultModel ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="启用" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="removeModel(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="680px">
|
||||||
|
<el-form :model="editForm" label-width="108px">
|
||||||
|
<el-form-item label="服务商" required>
|
||||||
|
<el-select v-model="editForm.providerId" placeholder="请选择服务商">
|
||||||
|
<el-option
|
||||||
|
v-for="provider in providers"
|
||||||
|
:key="provider.id"
|
||||||
|
:label="provider.providerName"
|
||||||
|
:value="provider.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模型编码" required>
|
||||||
|
<el-input v-model="editForm.modelCode" placeholder="如 TEXT_EMBED_3L" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模型名称" required>
|
||||||
|
<el-input v-model="editForm.modelName" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="上游模型">
|
||||||
|
<el-input v-model="editForm.upstreamModel" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模型类型" required>
|
||||||
|
<el-select v-model="editForm.modelType" placeholder="请选择">
|
||||||
|
<el-option v-for="item in modelTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="向量维度">
|
||||||
|
<el-input-number v-model="editForm.embeddingDimension" :min="1" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项JSON">
|
||||||
|
<el-input v-model="editForm.optionsJson" type="textarea" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="本地模型">
|
||||||
|
<el-switch v-model="editForm.localModel" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认模型">
|
||||||
|
<el-switch v-model="editForm.defaultModel" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="editForm.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitModel">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
278
frontend/src/pages/system/ModelProvidersPage.vue
Normal file
278
frontend/src/pages/system/ModelProvidersPage.vue
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||||
|
import {
|
||||||
|
checkModelProviderHealth,
|
||||||
|
deleteModelProvider,
|
||||||
|
queryModelProviders,
|
||||||
|
saveModelProvider,
|
||||||
|
type ModelProvider,
|
||||||
|
} from '@/api/modelProvider';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const checkingId = ref<string | null>(null);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const providers = ref<ModelProvider[]>([]);
|
||||||
|
const enumLoading = ref(false);
|
||||||
|
const enumError = ref('');
|
||||||
|
const providerTypeOptions = ref<EnumOption[]>([]);
|
||||||
|
const protocolTypeOptions = ref<EnumOption[]>([]);
|
||||||
|
const authTypeOptions = ref<EnumOption[]>([]);
|
||||||
|
const healthStatusOptions = ref<EnumOption[]>([]);
|
||||||
|
|
||||||
|
const editForm = reactive<ModelProvider>({
|
||||||
|
providerCode: '',
|
||||||
|
providerName: '',
|
||||||
|
providerType: '',
|
||||||
|
protocolType: '',
|
||||||
|
baseUrl: '',
|
||||||
|
authType: '',
|
||||||
|
secretRef: '',
|
||||||
|
timeoutMs: 60000,
|
||||||
|
priority: 100,
|
||||||
|
enabled: true,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => (editForm.id ? '编辑服务商' : '新增服务商'));
|
||||||
|
|
||||||
|
function resetForm(row?: ModelProvider) {
|
||||||
|
editForm.id = row?.id;
|
||||||
|
editForm.providerCode = row?.providerCode ?? '';
|
||||||
|
editForm.providerName = row?.providerName ?? '';
|
||||||
|
editForm.providerType = row?.providerType ?? providerTypeOptions.value[0]?.value ?? '';
|
||||||
|
editForm.protocolType = row?.protocolType ?? protocolTypeOptions.value[0]?.value ?? '';
|
||||||
|
editForm.baseUrl = row?.baseUrl ?? '';
|
||||||
|
editForm.authType = row?.authType ?? authTypeOptions.value[0]?.value ?? '';
|
||||||
|
editForm.secretRef = row?.secretRef ?? '';
|
||||||
|
editForm.timeoutMs = row?.timeoutMs ?? 60000;
|
||||||
|
editForm.priority = row?.priority ?? 100;
|
||||||
|
editForm.enabled = row?.enabled ?? true;
|
||||||
|
editForm.remark = row?.remark ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEnums(forceRefresh = false) {
|
||||||
|
enumLoading.value = true;
|
||||||
|
enumError.value = '';
|
||||||
|
try {
|
||||||
|
const result = await loadModelProviderEnumOptions(forceRefresh);
|
||||||
|
providerTypeOptions.value = result.provider_type ?? [];
|
||||||
|
protocolTypeOptions.value = result.protocol_type ?? [];
|
||||||
|
authTypeOptions.value = result.auth_type ?? [];
|
||||||
|
healthStatusOptions.value = result.health_status ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
enumError.value = error instanceof Error ? error.message : '枚举加载失败';
|
||||||
|
} finally {
|
||||||
|
enumLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProviders() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await queryModelProviders();
|
||||||
|
providers.value = response.data ?? [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
resetForm();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(row: ModelProvider) {
|
||||||
|
resetForm(row);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitProvider() {
|
||||||
|
if (!editForm.providerCode || !editForm.providerName || !editForm.baseUrl) {
|
||||||
|
ElMessage.warning('请填写服务商编码、服务商名称和基础地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await saveModelProvider({ ...editForm });
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
await loadProviders();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeProvider(row: ModelProvider) {
|
||||||
|
if (!row.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确认删除服务商「${row.providerName}」?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
await deleteModelProvider(row.id);
|
||||||
|
ElMessage.success('已删除');
|
||||||
|
await loadProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHealth(row: ModelProvider) {
|
||||||
|
if (!row.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkingId.value = row.id;
|
||||||
|
try {
|
||||||
|
await checkModelProviderHealth(row.id);
|
||||||
|
ElMessage.success('健康检查已完成');
|
||||||
|
await loadProviders();
|
||||||
|
} finally {
|
||||||
|
checkingId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function healthLabel(value?: string) {
|
||||||
|
const option = healthStatusOptions.value.find((item) => item.value === value);
|
||||||
|
return option?.label ?? value ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadEnums();
|
||||||
|
resetForm();
|
||||||
|
await loadProviders();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page-panel">
|
||||||
|
<div class="page-panel__header">
|
||||||
|
<h2>模型服务商</h2>
|
||||||
|
<span>Providers</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar__left">
|
||||||
|
<el-alert
|
||||||
|
v-if="enumError"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
:title="`枚举加载失败:${enumError}`"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar__actions">
|
||||||
|
<el-button :loading="enumLoading" @click="loadEnums(true)">重试枚举</el-button>
|
||||||
|
<el-button :icon="RefreshRight" @click="loadProviders">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增服务商</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="providers" row-key="id">
|
||||||
|
<el-table-column prop="providerCode" label="服务商编码" min-width="140" />
|
||||||
|
<el-table-column prop="providerName" label="服务商名称" min-width="140" />
|
||||||
|
<el-table-column prop="providerType" label="服务商类型" min-width="120" />
|
||||||
|
<el-table-column prop="protocolType" label="协议类型" min-width="130" />
|
||||||
|
<el-table-column prop="authType" label="鉴权类型" min-width="120" />
|
||||||
|
<el-table-column prop="secretRef" label="密钥引用" min-width="140" />
|
||||||
|
<el-table-column label="已配置密钥" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.hasApiKey ? 'success' : 'info'">{{ row.hasApiKey ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="健康状态" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag>{{ healthLabel(row.healthStatus) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="启用" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:loading="checkingId === row.id"
|
||||||
|
@click="checkHealth(row)"
|
||||||
|
>
|
||||||
|
健康检查
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="removeProvider(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px">
|
||||||
|
<el-form :model="editForm" label-width="108px">
|
||||||
|
<el-form-item label="服务商编码" required>
|
||||||
|
<el-input v-model="editForm.providerCode" placeholder="如 OPENAI_MAIN" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="服务商名称" required>
|
||||||
|
<el-input v-model="editForm.providerName" placeholder="如 OpenAI 主账号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="服务商类型">
|
||||||
|
<el-select v-model="editForm.providerType" placeholder="请选择">
|
||||||
|
<el-option v-for="item in providerTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="协议类型">
|
||||||
|
<el-select v-model="editForm.protocolType" placeholder="请选择">
|
||||||
|
<el-option v-for="item in protocolTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="基础地址" required>
|
||||||
|
<el-input v-model="editForm.baseUrl" placeholder="https://api.example.com" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="鉴权类型">
|
||||||
|
<el-select v-model="editForm.authType" placeholder="请选择">
|
||||||
|
<el-option v-for="item in authTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密钥引用">
|
||||||
|
<el-input v-model="editForm.secretRef" placeholder="如 OPENAI_API_KEY" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="超时(毫秒)">
|
||||||
|
<el-input-number v-model="editForm.timeoutMs" :min="1000" :step="1000" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="优先级">
|
||||||
|
<el-input-number v-model="editForm.priority" :min="1" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="editForm.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitProvider">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar__left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
245
frontend/src/pages/system/ModelRouteRulesPage.vue
Normal file
245
frontend/src/pages/system/ModelRouteRulesPage.vue
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||||
|
import {
|
||||||
|
deleteModelRouteRule,
|
||||||
|
queryModelConfigs,
|
||||||
|
queryModelRouteRules,
|
||||||
|
saveModelRouteRule,
|
||||||
|
type ModelConfig,
|
||||||
|
type ModelRouteRule,
|
||||||
|
} from '@/api/modelProvider';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const routes = ref<ModelRouteRule[]>([]);
|
||||||
|
const models = ref<ModelConfig[]>([]);
|
||||||
|
const taskTypeOptions = ref<EnumOption[]>([]);
|
||||||
|
const routeStrategyOptions = ref<EnumOption[]>([]);
|
||||||
|
const matchScopeOptions = ref<EnumOption[]>([]);
|
||||||
|
const enumError = ref('');
|
||||||
|
|
||||||
|
const editForm = reactive<ModelRouteRule>({
|
||||||
|
routeCode: '',
|
||||||
|
routeName: '',
|
||||||
|
taskType: '',
|
||||||
|
matchScope: '',
|
||||||
|
scopeId: '',
|
||||||
|
primaryModelId: '',
|
||||||
|
fallbackModelIdsJson: '[]',
|
||||||
|
routeStrategy: '',
|
||||||
|
maxLatencyMs: 0,
|
||||||
|
enabled: true,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => (editForm.id ? '编辑路由规则' : '新增路由规则'));
|
||||||
|
|
||||||
|
function resetForm(row?: ModelRouteRule) {
|
||||||
|
editForm.id = row?.id;
|
||||||
|
editForm.routeCode = row?.routeCode ?? '';
|
||||||
|
editForm.routeName = row?.routeName ?? '';
|
||||||
|
editForm.taskType = row?.taskType ?? taskTypeOptions.value[0]?.value ?? '';
|
||||||
|
editForm.matchScope = row?.matchScope ?? matchScopeOptions.value[0]?.value ?? '';
|
||||||
|
editForm.scopeId = row?.scopeId ?? '';
|
||||||
|
editForm.primaryModelId = row?.primaryModelId ?? models.value[0]?.id ?? '';
|
||||||
|
editForm.fallbackModelIdsJson = row?.fallbackModelIdsJson ?? '[]';
|
||||||
|
editForm.routeStrategy = row?.routeStrategy ?? routeStrategyOptions.value[0]?.value ?? '';
|
||||||
|
editForm.maxLatencyMs = row?.maxLatencyMs ?? 0;
|
||||||
|
editForm.enabled = row?.enabled ?? true;
|
||||||
|
editForm.remark = row?.remark ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBaseData(forceRefresh = false) {
|
||||||
|
enumError.value = '';
|
||||||
|
try {
|
||||||
|
const [enumResult, modelResult] = await Promise.all([
|
||||||
|
loadModelProviderEnumOptions(forceRefresh),
|
||||||
|
queryModelConfigs(),
|
||||||
|
]);
|
||||||
|
taskTypeOptions.value = enumResult.task_type ?? [];
|
||||||
|
routeStrategyOptions.value = enumResult.route_strategy ?? [];
|
||||||
|
matchScopeOptions.value = enumResult.match_scope ?? [];
|
||||||
|
models.value = modelResult.data ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
enumError.value = error instanceof Error ? error.message : '基础数据加载失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRouteRules() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await queryModelRouteRules();
|
||||||
|
routes.value = response.data ?? [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
resetForm();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(row: ModelRouteRule) {
|
||||||
|
resetForm(row);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRouteRule() {
|
||||||
|
if (!editForm.routeCode || !editForm.taskType || !editForm.primaryModelId) {
|
||||||
|
ElMessage.warning('请填写路由编码、任务类型和主模型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.parse(editForm.fallbackModelIdsJson ?? '[]');
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('降级模型JSON格式不正确');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await saveModelRouteRule({ ...editForm });
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
await loadRouteRules();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeRouteRule(row: ModelRouteRule) {
|
||||||
|
if (!row.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确认删除路由规则「${row.routeName || row.routeCode}」?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
await deleteModelRouteRule(row.id);
|
||||||
|
ElMessage.success('已删除');
|
||||||
|
await loadRouteRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelLabel(modelId?: string) {
|
||||||
|
const model = models.value.find((item) => item.id === modelId);
|
||||||
|
return model?.modelName ?? model?.modelCode ?? modelId ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadBaseData();
|
||||||
|
resetForm();
|
||||||
|
await loadRouteRules();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page-panel">
|
||||||
|
<div class="page-panel__header">
|
||||||
|
<h2>路由规则</h2>
|
||||||
|
<span>Routes</span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-alert v-if="enumError" type="error" :closable="false" :title="`基础数据加载失败:${enumError}`" show-icon />
|
||||||
|
<div class="toolbar__actions">
|
||||||
|
<el-button @click="loadBaseData(true)">重试基础数据</el-button>
|
||||||
|
<el-button :icon="RefreshRight" @click="loadRouteRules">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增规则</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="routes" row-key="id">
|
||||||
|
<el-table-column prop="routeCode" label="规则编码" min-width="140" />
|
||||||
|
<el-table-column prop="routeName" label="规则名称" min-width="140" />
|
||||||
|
<el-table-column prop="taskType" label="任务类型" min-width="120" />
|
||||||
|
<el-table-column prop="matchScope" label="匹配范围" min-width="100" />
|
||||||
|
<el-table-column label="主模型" min-width="140">
|
||||||
|
<template #default="{ row }">{{ modelLabel(row.primaryModelId) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="fallbackModelIdsJson" label="降级模型JSON" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="routeStrategy" label="策略" min-width="100" />
|
||||||
|
<el-table-column prop="maxLatencyMs" label="最大延迟(ms)" width="120" />
|
||||||
|
<el-table-column label="启用" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" :icon="Delete" @click="removeRouteRule(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="680px">
|
||||||
|
<el-form :model="editForm" label-width="120px">
|
||||||
|
<el-form-item label="规则编码" required>
|
||||||
|
<el-input v-model="editForm.routeCode" placeholder="如 RAG_EMBEDDING_GLOBAL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="规则名称">
|
||||||
|
<el-input v-model="editForm.routeName" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务类型" required>
|
||||||
|
<el-select v-model="editForm.taskType">
|
||||||
|
<el-option v-for="item in taskTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="匹配范围">
|
||||||
|
<el-select v-model="editForm.matchScope">
|
||||||
|
<el-option v-for="item in matchScopeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="范围业务ID">
|
||||||
|
<el-input v-model="editForm.scopeId" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="主模型" required>
|
||||||
|
<el-select v-model="editForm.primaryModelId">
|
||||||
|
<el-option v-for="item in models" :key="item.id" :label="`${item.modelName}(${item.modelCode})`" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="降级模型JSON">
|
||||||
|
<el-input v-model="editForm.fallbackModelIdsJson" type="textarea" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="路由策略">
|
||||||
|
<el-select v-model="editForm.routeStrategy">
|
||||||
|
<el-option v-for="item in routeStrategyOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最大延迟(ms)">
|
||||||
|
<el-input-number v-model="editForm.maxLatencyMs" :min="0" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch v-model="editForm.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitRouteRule">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,6 +12,10 @@ describe('router', () => {
|
|||||||
expect(paths).toContain('/rag/documents');
|
expect(paths).toContain('/rag/documents');
|
||||||
expect(paths).toContain('/rag/tasks/chunk');
|
expect(paths).toContain('/rag/tasks/chunk');
|
||||||
expect(paths).toContain('/system/enums');
|
expect(paths).toContain('/system/enums');
|
||||||
|
expect(paths).toContain('/system/model/providers');
|
||||||
|
expect(paths).toContain('/system/model/configs');
|
||||||
|
expect(paths).toContain('/system/model/routes');
|
||||||
|
expect(paths).toContain('/system/model/call-logs');
|
||||||
expect(paths).toContain('/:pathMatch(.*)*');
|
expect(paths).toContain('/:pathMatch(.*)*');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import RagStoresPage from '@/pages/rag/RagStoresPage.vue';
|
|||||||
import RagChunkTasksPage from '@/pages/rag/tasks/RagChunkTasksPage.vue';
|
import RagChunkTasksPage from '@/pages/rag/tasks/RagChunkTasksPage.vue';
|
||||||
import RagWorkbenchPage from '@/pages/rag/workbench/RagWorkbenchPage.vue';
|
import RagWorkbenchPage from '@/pages/rag/workbench/RagWorkbenchPage.vue';
|
||||||
import SystemEnumsPage from '@/pages/system/SystemEnumsPage.vue';
|
import SystemEnumsPage from '@/pages/system/SystemEnumsPage.vue';
|
||||||
|
import ModelProvidersPage from '@/pages/system/ModelProvidersPage.vue';
|
||||||
|
import ModelConfigsPage from '@/pages/system/ModelConfigsPage.vue';
|
||||||
|
import ModelRouteRulesPage from '@/pages/system/ModelRouteRulesPage.vue';
|
||||||
|
import ModelCallLogsPage from '@/pages/system/ModelCallLogsPage.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
@@ -20,6 +24,30 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
component: SystemEnumsPage,
|
component: SystemEnumsPage,
|
||||||
meta: { title: '系统枚举' },
|
meta: { title: '系统枚举' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/system/model/providers',
|
||||||
|
name: 'system-model-providers',
|
||||||
|
component: ModelProvidersPage,
|
||||||
|
meta: { title: '模型服务商' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system/model/configs',
|
||||||
|
name: 'system-model-configs',
|
||||||
|
component: ModelConfigsPage,
|
||||||
|
meta: { title: '模型配置' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system/model/routes',
|
||||||
|
name: 'system-model-routes',
|
||||||
|
component: ModelRouteRulesPage,
|
||||||
|
meta: { title: '路由规则' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system/model/call-logs',
|
||||||
|
name: 'system-model-call-logs',
|
||||||
|
component: ModelCallLogsPage,
|
||||||
|
meta: { title: '调用日志' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/rag/stores',
|
path: '/rag/stores',
|
||||||
name: 'rag-stores',
|
name: 'rag-stores',
|
||||||
@@ -67,6 +95,30 @@ const routerRoutes: RouteRecordRaw[] = [
|
|||||||
component: SystemEnumsPage,
|
component: SystemEnumsPage,
|
||||||
meta: { title: '系统枚举' },
|
meta: { title: '系统枚举' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'system/model/providers',
|
||||||
|
name: 'system-model-providers',
|
||||||
|
component: ModelProvidersPage,
|
||||||
|
meta: { title: '模型服务商' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system/model/configs',
|
||||||
|
name: 'system-model-configs',
|
||||||
|
component: ModelConfigsPage,
|
||||||
|
meta: { title: '模型配置' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system/model/routes',
|
||||||
|
name: 'system-model-routes',
|
||||||
|
component: ModelRouteRulesPage,
|
||||||
|
meta: { title: '路由规则' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system/model/call-logs',
|
||||||
|
name: 'system-model-call-logs',
|
||||||
|
component: ModelCallLogsPage,
|
||||||
|
meta: { title: '调用日志' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'rag/stores',
|
path: 'rag/stores',
|
||||||
name: 'rag-stores',
|
name: 'rag-stores',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.bruce.modelprovider.gateway;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该类属于模型平台模块,用于承载对应分层职责。
|
||||||
|
*/
|
||||||
|
public interface EmbeddingModelGateway {
|
||||||
|
/**
|
||||||
|
* 方法 embed,用于定义接口能力契约。
|
||||||
|
*/
|
||||||
|
EmbeddingResult embed(EmbeddingRequest request);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user