模型管理
接口设计
本文将详细介绍 Java MossKB 系统中模型管理的实现,包括其接口设计、后端代码实现以及整体架构。通过这一介绍,您将了解如何通过 API 进行模型的添加、更新、查询和删除,以及后端如何处理这些请求。
tio-boot 提供了一系列 RESTful API,用于管理不同提供者的模型。这些接口涵盖了模型提供者的获取、模型类型和列表的查询、模型的增删改查等操作。以下将逐一介绍这些接口及其响应格式。
默认模型:系统自带两个默认种子模型实例——推理用 Gitee 的
deepseek-v4.1-flash(官方平台把同系列模型 ID 写作deepseek-flash),向量用Qwen3-Embedding-8B(1024 维);扫描页 OCR 固定调用PaddleOCR-VL-1.5,不随对话模型变化。平台、维度与配置键汇总见默认模型。
获取模型提供者列表
请求
GET http://localhost:10060/api/provider
响应
{
"code": 200,
"message": "成功",
"data": [
{
"provider": "model_gitee_provider",
"name": "Gitee AI",
"icon": "<svg ...></svg>"
},
{
"provider": "model_openrouter_provider",
"name": "OpenRouter",
"icon": "<svg ...></svg>"
}
]
}
说明
该接口返回所有可用的模型提供者,包括提供者名称、标识符以及图标。
获取模型列表
根据不同的查询参数,模型列表的获取有所不同。
根据模型名称查询
请求
GET http://localhost:10060/api/model?name=<model_name>响应
{ "message": null, "data": [], "code": 200 }说明
通过指定模型名称查询模型,返回匹配的模型列表。
根据提供者查询
请求
GET http://localhost:10060/api/provider/model_list?provider=model_openai_provider&model_type=LLM响应
{ "message": null, "data": [], "code": 200 }说明
此接口返回指定平台的基础模型目录。
/api/model返回用户可用的模型实例,不能将其未实现的 provider 查询参数当作平台过滤。
获取模型类型列表
请求
GET http://localhost:10060/api/provider/model_type_list?provider=model_openai_provider
响应
{
"message": "成功",
"data": [
{
"value": "LLM",
"key": "大语言模型"
},
{
"value": "EMBEDDING",
"key": "向量模型"
}
],
"code": 200
}
说明
该接口返回指定提供者支持的模型类型列表,如大语言模型(LLM)和向量模型(EMBEDDING)。
获取模型详情
请求
GET http://localhost:10060/api/model/442895624269676544
响应
{
"code": 200,
"message": "成功",
"data": {
"id": "111",
"provider": "model_openai_provider",
"name": "text-embedding-3-large",
"model_type": "EMBEDDING",
"model_name": "text-embedding-3-large",
"status": "SUCCESS",
"meta": {},
"credential": {
"api_base": "https://api.openai.com/v1",
"api_key": "********"
},
"permission_type": "PRIVATE"
}
}
说明
通过模型 ID 获取具体模型的详细信息,包括提供者、模型类型、名称、状态及凭证信息等。
添加模型
添加向量模型
请求
POST http://localhost:10060/api/model请求体
{ "name": "text-embedding-3-large", "model_type": "EMBEDDING", "model_name": "text-embedding-3-large", "permission_type": "PRIVATE", "credential": { "api_base": "https://api.openai.com/v1", "api_key": "xxxx" }, "provider": "model_openai_provider" }添加大语言模型
请求
POST http://localhost:10060/api/model请求体
{ "name": "gpt-4o-mini", "model_type": "LLM", "model_name": "gpt-4o-mini", "permission_type": "PRIVATE", "credential": { "api_base": "https://api.openai.com/v1", "api_key": "sk-xxx" }, "provider": "model_openai_provider" }
说明
通过 POST 请求可以添加新的模型,需提供模型名称、类型、具体名称、权限类型、凭证信息及所属提供者。
更新模型
请求
PUT http://localhost:10060/api/model/442895624269676544
请求体
{
"provider": "model_openai_provider",
"name": "text-embedding-3-large",
"permission_type": "PRIVATE",
"model_type": "EMBEDDING",
"model_name": "text-embedding-3-large",
"credential": {
"api_base": "https://api.openai.com/v1",
"api_key": "********"
}
}
说明
通过 PUT 请求更新指定 ID 的模型信息,包括名称、类型、权限及凭证信息。
删除模型
请求
DELETE http://localhost:10060/api/model/442895624269676544
说明
通过 DELETE 请求删除指定 ID 的模型。
后端代码实现
Java MossKB 的后端采用 Java 语言的 tio-boot 框架实现,主要包括控制器、服务层和数据访问层(DAO)。以下将详细解析各部分代码及其功能。
ApiProviderController 控制器
package nexus.io.mosskb.controller;
import com.alibaba.fastjson2.JSON;
import nexus.io.annotation.Get;
import nexus.io.annotation.Post;
import nexus.io.annotation.Put;
import nexus.io.annotation.RequestPath;
import nexus.io.jfinal.aop.Aop;
import nexus.io.mosskb.service.kb.ModelCatalogService;
import nexus.io.model.result.ResultVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
@RequestPath("/api/provider")
public class ApiProviderController {
private final ModelCatalogService catalog = Aop.get(ModelCatalogService.class);
@Get("")
public ResultVo index() { return ResultVo.ok(catalog.providers()); }
@Get("/model_type_list")
public ResultVo model_type_list(String provider) { return ResultVo.ok(catalog.types(provider)); }
@Get("/model_list")
public ResultVo model_list(String provider, String model_type) { return ResultVo.ok(catalog.models(provider, model_type)); }
@Get("/model_form")
public ResultVo model_form(String provider, String model_type, String model_name) { return ResultVo.ok(catalog.form(provider, "credential_form")); }
@Get("/model_params_form")
public ResultVo model_params_form(String provider, String model_type, String model_name) { return ResultVo.ok(catalog.form(provider, "params_form")); }
@Post("/catalog/discover")
public ResultVo discover(HttpRequest request) { return catalog.discover(TioRequestContext.getUserIdLong(), JSON.parseObject(request.getBodyString())); }
@Put("/catalog")
public ResultVo saveCatalog(HttpRequest request) { return catalog.saveCatalog(TioRequestContext.getUserIdLong(), JSON.parseObject(request.getBodyString())); }
}
功能说明
index方法:处理/api/provider的GET请求,查询moss_kb_model_provider,返回已启用的模型提供者信息。model_type_list方法:处理/api/provider/model_type_list的GET请求,返回指定提供者支持的模型类型列表。model_list方法:处理/api/provider/model_list的GET请求,根据提供者和模型类型返回相应的模型列表。model_form方法:根据提供者、模型类型和模型名称返回模型表单配置,用于前端动态生成表单。
ApiModelController 控制器
package nexus.io.mosskb.controller;
import nexus.io.annotation.Delete;
import nexus.io.annotation.EnableCORS;
import nexus.io.annotation.Get;
import nexus.io.annotation.Post;
import nexus.io.annotation.Put;
import nexus.io.annotation.RequestPath;
import nexus.io.jfinal.aop.Aop;
import nexus.io.mosskb.service.kb.MossKbModelService;
import nexus.io.mosskb.vo.ModelVo;
import nexus.io.model.result.ResultVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.utils.json.FastJson2Utils;
@RequestPath("/api/model")
@EnableCORS
public class ApiModelController {
MossKbModelService modelService = Aop.get(MossKbModelService.class);
@Get("")
public ResultVo index(HttpRequest request) {
String name = request.getParam("name");
return modelService.list(name, request.getParam("model_type"));
}
@Post("")
public ResultVo save(HttpRequest request) {
Long userId = TioRequestContext.getUserIdLong();
String bodyString = request.getBodyString();
ModelVo modelVo = FastJson2Utils.parse(bodyString, ModelVo.class);
return modelService.save(userId, modelVo);
}
@Put("/{id}")
public ResultVo update(HttpRequest request,Long id) {
Long userId = TioRequestContext.getUserIdLong();
String bodyString = request.getBodyString();
ModelVo modelVo = FastJson2Utils.parse(bodyString, ModelVo.class);
modelVo.setId(id);
return modelService.save(userId, modelVo);
}
@Delete("/{id}")
public ResultVo delete(Long id) {
return modelService.delete(id);
}
@Get("/{id}")
public ResultVo get(Long id) {
return modelService.get(id);
}
@Get("/{id}/model_params_form")
public ResultVo modelParams(Long id) {
nexus.io.db.activerecord.Row row = nexus.io.db.activerecord.Db.findById("moss_kb_model", id);
if (row == null) {
return ResultVo.fail(404, "模型不存在");
}
return ResultVo.ok(com.alibaba.fastjson2.JSON.parseArray(row.getStr("model_params_form")));
}
@Put("/{id}/model_params_form")
public ResultVo updateModelParams(Long id, HttpRequest request) {
com.alibaba.fastjson2.JSONArray form = com.alibaba.fastjson2.JSON.parseArray(request.getBodyString());
nexus.io.db.activerecord.Db.update("update moss_kb_model set model_params_form=?::jsonb where id=?", form.toJSONString(), id);
return ResultVo.ok(form);
}
}
功能说明
index方法:处理/api/model的GET请求,获取查询参数name并调用服务层方法返回模型列表。save方法:处理/api/model的POST请求,解析请求体中的模型信息,调用服务层保存模型。update方法:处理/api/model/{id}的PUT请求,更新指定 ID 的模型信息。delete方法:处理/api/model/{id}的DELETE请求,删除指定 ID 的模型。get方法:处理/api/model/{id}的GET请求,获取指定 ID 的模型详情。
MossKbModelService 服务层
package nexus.io.mosskb.service.kb;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.postgresql.util.PGobject;
import com.jfinal.kit.Kv;
import lombok.extern.slf4j.Slf4j;
import nexus.io.chat.PlatformInput;
import nexus.io.chat.UniChatMessage;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.jfinal.aop.Aop;
import nexus.io.mosskb.constant.MossKbTableNames;
import nexus.io.mosskb.dao.ModelDao;
import nexus.io.mosskb.model.MossKbModel;
import nexus.io.mosskb.vo.CredentialVo;
import nexus.io.mosskb.vo.ModelVo;
import nexus.io.model.result.ResultVo;
import nexus.io.openai.client.OpenAiClient;
import nexus.io.tio.utils.json.JsonUtils;
import okhttp3.Response;
@Slf4j
public class MossKbModelService {
/**
* @param name
* @return
*/
public ResultVo list(String name) {
return list(name, null);
}
public ResultVo list(String name, String modelType) {
MossKbUserService mossKbUserService = Aop.get(MossKbUserService.class);
String[] jsonFields = new String[] { "meta" };
String sql = null;
if (name == null) {
sql = "select id,provider,name,model_type,model_name,status,meta,permission_type,user_id from %s";
sql = String.format(sql, MossKbTableNames.moss_kb_model);
List<Row> list = Db.findWithJsonField(sql, jsonFields);
List<Kv> kvs = new ArrayList<>();
for (Row r : list) {
if (!ModelAccess.canUse(nexus.io.tio.boot.http.TioRequestContext.getUserIdLong(), r.getLong("id"))) {
continue;
}
if (modelType != null && !modelType.equals(r.getStr("model_type"))) {
continue;
}
Kv kv = r.toKv();
kv.set("id", kv.get("id").toString());
kv.set("user_id", kv.get("user_id").toString());
Object meta = kv.get("meta");
if (meta instanceof PGobject) {
kv.set("meta", JsonUtils.parseObject(((PGobject) meta).getValue()));
} else if (meta == null) {
kv.set("meta", Kv.create());
}
String username = mossKbUserService.queryUsername(kv.getLong("user_id"));
kv.set("username", username);
kvs.add(kv);
}
return ResultVo.ok(kvs);
}
sql = "select id,provider,name,model_type,model_name,status,meta,permission_type,user_id from %s where name=?";
sql = String.format(sql, MossKbTableNames.moss_kb_model);
List<Kv> kvs = new ArrayList<>();
List<Row> list = Db.findWithJsonField(sql, jsonFields, name);
for (Row record : list) {
if (!ModelAccess.canUse(nexus.io.tio.boot.http.TioRequestContext.getUserIdLong(), record.getLong("id"))) {
continue;
}
if (modelType != null && !modelType.equals(record.getStr("model_type"))) {
continue;
}
Kv kv = record.toKv();
String username = mossKbUserService.queryUsername(kv.getLong("user_id"));
kv.set("username", username);
kvs.add(kv);
}
return ResultVo.ok(kvs);
}
/**
*
* @param map {"name":"text-embedding-3-large","model_type":"EMBEDDING","model_name":"text-embedding-3-large","permission_type":"PRIVATE","credential":{"api_base":"https://api.openai.com/v1","api_key":"11111111"},"provider":"model_openai_provider"}
* @return
*/
public ResultVo save(Long userId, ModelVo modelVo) {
if (modelVo.getId() != null && !ModelAccess.owns(userId, modelVo.getId())) {
return ResultVo.fail("无权修改模型");
}
String name = modelVo.getName();
if (name == null || name.isBlank() || name.length() > 64) {
return ResultVo.fail(400, "模型名称须为 1 至 64 个字符");
}
if (modelVo.getId() == null && Db.exists(MossKbTableNames.moss_kb_model, "name", name)) {
return ResultVo.fail(400, "模型名称【" + name + "】已存在");
}
try {
ModelCatalogService catalog = Aop.get(ModelCatalogService.class);
Row provider = catalog.provider(modelVo.getProvider());
if (provider == null) {
return ResultVo.fail(400, "供应商不存在或已停用");
}
if (!List.of("LLM", "EMBEDDING").contains(modelVo.getModel_type()) || !List.of("PRIVATE", "PUBLIC").contains(modelVo.getPermission_type())) {
return ResultVo.fail(400, "模型类型或权限参数无效");
}
modelVo.setModel_name(ModelCatalogService.modelId(modelVo.getModel_name()));
CredentialVo credential = modelVo.getCredential();
if (credential == null) {
return ResultVo.fail(400, "请填写模型凭据");
}
if (credential.getApi_base() == null || credential.getApi_base().isBlank()) {
credential.setApi_base(provider.getStr("api_base"));
}
credential.setApi_base(ModelCatalogService.baseUrl(credential.getApi_base()));
String key = credential.getApi_key();
if (modelVo.getId() != null && (key == null || key.isBlank() || key.contains("*"))) {
CredentialVo previous = ModelCatalogService.credential(Db.findById(MossKbTableNames.moss_kb_model, modelVo.getId()));
if (!credential.getApi_base().equals(ModelCatalogService.baseUrl(previous.getApi_base()))) {
return ResultVo.fail(400, "修改 API 域名时必须重新填写该平台的 API Key");
}
credential.setApi_key(previous.getApi_key());
}
if (credential.getApi_key() == null || credential.getApi_key().isBlank() || credential.getApi_key().contains("*")) {
return ResultVo.fail(400, "请填写该平台的 API Key");
}
if (modelVo.getId() != null) {
Row previous = Db.findById(MossKbTableNames.moss_kb_model, modelVo.getId());
CredentialVo oldCredential = ModelCatalogService.credential(previous);
if ("EMBEDDING".equals(previous.getStr("model_type"))
&& (!modelVo.getModel_name().equals(previous.getStr("model_name")) || !modelVo.getModel_type().equals(previous.getStr("model_type"))
|| !credential.getApi_base().equals(ModelCatalogService.baseUrl(oldCredential.getApi_base())))
&& Db.queryLong("select count(*) from moss_kb_dataset where embedding_mode_id=?", modelVo.getId()) > 0) {
return ResultVo.fail(400, "被知识库使用的向量模型不能直接更换模型 ID 或 API 地址,请新建模型和知识库");
}
}
try {
validateModel(modelVo);
} catch (Exception e) {
return ResultVo.fail(400, "模型校验失败,请检查模型 ID、API 地址、密钥、额度;向量模型须支持 1024 维");
}
} catch (IllegalArgumentException e) {
return ResultVo.fail(400, e.getMessage());
} catch (Exception e) {
return ResultVo.fail(400, "模型校验失败,请检查模型 ID、API 地址、密钥及平台额度");
}
Aop.get(ModelDao.class).saveOrUpdate(userId, modelVo);
return ResultVo.ok();
}
static void validateModel(ModelVo model) throws IOException {
CredentialVo credential = model.getCredential();
if ("EMBEDDING".equals(model.getModel_type())) {
com.alibaba.fastjson2.JSONObject input = new com.alibaba.fastjson2.JSONObject();
input.put("model", model.getModel_name());
input.put("input", List.of("模型连接测试"));
input.put("dimensions", 1024);
try (Response response = OpenAiClient.embeddings(credential.getApi_base(), credential.getApi_key(), input.toJSONString())) {
if (!response.isSuccessful() || response.body() == null) {
throw new IllegalArgumentException("向量模型校验失败,HTTP " + response.code());
}
float[] vector = com.alibaba.fastjson2.JSON.parseObject(response.body().string()).getJSONArray("data").getJSONObject(0).getObject("embedding", float[].class);
if (vector == null || vector.length != 1024) {
throw new IllegalArgumentException("当前知识库需要 1024 维向量,请选用支持 dimensions=1024 的模型");
}
for (float value : vector) {
if (!Float.isFinite(value)) {
throw new IllegalArgumentException("向量模型返回无效数据");
}
}
}
} else {
nexus.io.chat.UniChatRequest request = new nexus.io.chat.UniChatRequest();
request.setModel(model.getModel_name()).setApiPrefixUrl(credential.getApi_base()).setApiKey(credential.getApi_key())
.setStream(false).setMessages(List.of(new UniChatMessage("user", "Reply OK")));
nexus.io.chat.UniChatResponse response = nexus.io.chat.UniChatClient.generate(request);
if (response == null || response.getRawData() == null) {
throw new IllegalArgumentException("模型未返回有效响应");
}
}
}
public ResultVo delete(Long id) {
if (Db.queryLong("select count(*) from moss_kb_dataset where embedding_mode_id=?", id) > 0
|| Db.queryLong("select count(*) from moss_kb_application where model_id=?", id) > 0) {
return ResultVo.fail(400, "该模型仍被应用或知识库使用");
}
boolean ok = Aop.get(ModelDao.class).deleteById(id);
if (ok) {
return ResultVo.ok();
} else {
return ResultVo.fail();
}
}
public ResultVo get(Long id) {
Row record = Db.findById(MossKbTableNames.moss_kb_model, id);
if (record == null) {
return ResultVo.fail(404, "模型不存在");
}
CredentialVo credential = ModelCatalogService.credential(record);
record.getColumns().remove("credential");
Kv kv = record.toKv();
credential.setApi_key("********");
kv.set("credential", credential);
kv.set("id", String.valueOf(id));
kv.set("model_params_form", com.alibaba.fastjson2.JSON.parseArray(record.getStr("model_params_form")));
return ResultVo.ok(kv);
}
public PlatformInput getEmbeddingPlatformInput(Long embedding_mode_id) {
String sqlModelName = String.format("SELECT provider,model_name FROM %s WHERE id = ?",
MossKbTableNames.moss_kb_model);
MossKbModel mossKbModel = MossKbModel.dao.findFirst(sqlModelName, embedding_mode_id);
String embeddingPlatformName = null;
String embeddingModel = null;
if (mossKbModel != null) {
embeddingPlatformName = mossKbModel.getProvider();
embeddingModel = mossKbModel.getModelName();
} else {
embeddingPlatformName = MossKbEnvUtils.getEmbeddingPlatform();
embeddingModel = MossKbEnvUtils.getEmbeddingModel();
}
PlatformInput platformInput = new PlatformInput(embeddingPlatformName, embeddingModel);
return platformInput;
}
}
功能说明
list方法:根据模型名称查询模型列表。如果未提供名称,则返回所有模型。查询结果包括模型的基本信息,如 ID、提供者、类型、名称等。save方法:保存或更新模型信息。在保存前,先验证模型名称是否已存在。对数据库中的兼容平台,根据模型类型(LLM 或 EMBEDDING)和用户实际填写的模型 ID 进行校验,向配置的 API 地址发送测试请求。如果验证通过,则调用 DAO 层保存或更新模型信息。delete方法:删除指定 ID 的模型。get方法:获取指定 ID 的模型详情,返回模型的所有信息,并对敏感信息(如 API Key)进行脱敏处理。
ModelDao 数据访问层
package nexus.io.mosskb.dao;
import java.util.Map;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.mosskb.constant.MossKbTableNames;
import nexus.io.mosskb.vo.ModelVo;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;
public class ModelDao {
public void save(Map<String, Object> map) {
Db.save(MossKbTableNames.moss_kb_model, Row.fromMap(map));
}
public boolean deleteById(Long id) {
return Db.deleteById(MossKbTableNames.moss_kb_model, id);
}
public boolean saveOrUpdate(Long userId, ModelVo modelVo) {
Row record = new Row();
record.set("name", modelVo.getName()).set("model_type", modelVo.getModel_type()).set("model_name", modelVo.getModel_name())
//
.set("permission_type", modelVo.getPermission_type())
//
.set("credential", modelVo.getCredential()).set("status", "SUCCESS");
String provider = modelVo.getProvider();
if (provider != null) {
record.set("provider", provider);
}
if (modelVo.getModel_params_form() != null) {
record.set("model_params_form", modelVo.getModel_params_form());
}
Long id = modelVo.getId();
if (id != null) {
record.set("id", id);
return Db.update(MossKbTableNames.moss_kb_model, "id", record, new String[] { "credential", "model_params_form" });
} else {
record.set("id", SnowflakeIdUtils.id()).set("user_id", userId);
return Db.save(MossKbTableNames.moss_kb_model, record, new String[] { "credential", "model_params_form" });
}
}
}
功能说明
save方法:保存新的模型记录。deleteById方法:根据模型 ID 删除模型记录。saveOrUpdate方法:根据是否存在 ID 决定是保存新模型还是更新现有模型。生成唯一 ID 使用 Snowflake 算法。
总结
通过本文的介绍,我们详细了解了 Java MossKB 系统中模型管理的实现,包括 API 接口设计和后端代码实现。系统通过清晰的 RESTful API 提供了模型的增删改查功能,后端采用分层架构,控制器负责处理请求,服务层处理业务逻辑,数据访问层负责与数据库交互。同时,通过与 OpenAI 等模型提供者的集成,确保了模型凭证的有效性和安全性。
这种模块化和层次化的设计不仅提高了系统的可维护性和扩展性,也为未来集成更多模型提供者和类型打下了坚实的基础。对于需要管理多种机器学习模型的系统,Java MossKB 的实现提供了一个优秀的参考范例。
数据库目录、表单与自定义模型 ID
基础模型 ID 不是目录白名单:前端支持输入完整 ID 后回车;保存校验使用实际 ID,不替换为固定测试模型。向量模型请求 dimensions=1024 并验证结果。被引用的向量空间不能直接更换,被应用或知识库引用的模型不能删除。
编辑保留掩码或空 Key 时沿用原密钥,更换 API 地址时必须填写新平台 Key。最终生成和模型发现复用 UniChatClient。
| 方法 | 接口 | 功能 |
|---|---|---|
| GET | /api/provider/model_form?provider=...&model_type=LLM&model_name=... | 凭据表单,支持目录外 ID |
| GET | /api/provider/model_params_form?provider=...&model_type=LLM&model_name=... | 参数表单 |
| GET / PUT | /api/model/{id}/model_params_form | 实例参数表单 |
| POST | /api/provider/catalog/discover | 管理员用平台地址和 Key 发现模型 |
| PUT | /api/provider/catalog | 管理员登记或停用目录项 |
管理员登记目录请求:
{"provider":"model_default_provider","models":[{"name":"vendor/model-id","model_type":"LLM","desc":"自定义中转模型","enabled":true}]}
ModelCatalogService 目录服务实现
package nexus.io.mosskb.service.kb;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.jfinal.kit.Kv;
import nexus.io.chat.UniChatClient;
import nexus.io.chat.ChatModelResponse;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.mosskb.vo.CredentialVo;
import nexus.io.model.result.ResultVo;
import nexus.io.tio.utils.environment.EnvUtils;
/** Database-backed catalog; catalog membership is never a model ID whitelist. */
public class ModelCatalogService {
public List<Kv> providers() {
List<Kv> result = new ArrayList<>();
for (Row row : Db.find("select provider,name,icon from moss_kb_model_provider where enabled=true order by sort_order,name")) {
result.add(row.toKv());
}
return result;
}
public Row provider(String id) {
return Db.findFirst("select * from moss_kb_model_provider where provider=? and enabled=true", id);
}
public Object types(String provider) {
Row row = provider(provider);
return row == null ? new JSONArray() : JSON.parseArray(row.getStr("model_types"));
}
public Object form(String provider, String column) {
if (!List.of("credential_form", "params_form").contains(column)) {
throw new IllegalArgumentException("Invalid form");
}
Row row = provider(provider);
return row == null ? new JSONArray() : JSON.parseArray(row.getStr(column));
}
public List<Kv> models(String provider, String type) {
List<Kv> result = new ArrayList<>();
for (Row row : Db.find("select model_id as name,description as desc,model_type from moss_kb_model_catalog c "
+ "where provider=? and model_type=? and enabled=true and exists(select 1 from moss_kb_model_provider p where p.provider=c.provider and p.enabled=true) order by model_id", provider, type)) {
result.add(row.toKv());
}
return result;
}
public static String baseUrl(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("请填写 API 域名");
}
String base = value.trim().replaceAll("/+$", "");
URI uri = URI.create(base);
if ((!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme())) || uri.getHost() == null
|| uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null) {
throw new IllegalArgumentException("API 域名须为 HTTP(S) 基础地址,不含密钥、查询参数或片段");
}
return base;
}
public static String modelId(String value) {
if (value == null || value.isBlank() || value.length() > 256 || value.chars().anyMatch(Character::isISOControl)) {
throw new IllegalArgumentException("请填写有效的模型 ID(最多 256 字符)");
}
return value.trim();
}
public static CredentialVo credential(Row model) {
Object raw = model.get("credential");
JSONObject object = raw == null ? new JSONObject() : raw instanceof java.util.Map ? (JSONObject) JSON.toJSON(raw) : JSON.parseObject(raw.toString());
CredentialVo credential = new CredentialVo();
credential.setApi_base(object.getString("api_base"));
credential.setApi_key(object.getString("api_key"));
// Only the two seeded Gitee models may use the server's Gitee credential.
if ((Long.valueOf(1001).equals(model.getLong("id")) || Long.valueOf(1002).equals(model.getLong("id"))) && object.isEmpty()) {
credential.setApi_base(KnowledgeModelService.baseUrl());
credential.setApi_key(EnvUtils.get("GITEE_API_KEY"));
}
return credential;
}
public static boolean admin(Long userId) {
return userId != null && Db.queryLong("select count(*) from moss_kb_user where id=? and role='ADMIN' and is_active=true and deleted=0", userId) > 0;
}
public ResultVo discover(Long userId, JSONObject input) {
if (!admin(userId)) {
return ResultVo.fail(403, "仅管理员可管理共享模型目录");
}
try {
String base = baseUrl(input.getString("api_base"));
String key = input.getString("api_key");
if (key == null || key.isBlank()) {
return ResultVo.fail(400, "请提供该平台的 API Key;发现操作不保存凭据");
}
ChatModelResponse response = UniChatClient.getModels(base, key);
if (response == null || response.getData() == null) {
return ResultVo.fail(400, "模型发现失败,请检查平台是否支持 /models 接口及密钥权限");
}
return ResultVo.ok(response.getData());
} catch (Exception e) {
return ResultVo.fail(400, "模型发现失败,请检查 API 地址及凭据");
}
}
public ResultVo saveCatalog(Long userId, JSONObject input) {
if (!admin(userId)) {
return ResultVo.fail(403, "仅管理员可管理共享模型目录");
}
try {
String provider = input.getString("provider");
if (provider(provider) == null) {
return ResultVo.fail(400, "供应商不存在或已停用");
}
JSONArray models = input.getJSONArray("models");
if (models == null || models.size() > 5000) {
return ResultVo.fail(400, "models 必须是最多 5000 项的数组");
}
for (int i = 0; i < models.size(); i++) {
JSONObject model = models.getJSONObject(i);
modelId(model.getString("name"));
if (!List.of("LLM", "EMBEDDING").contains(model.getString("model_type"))) {
return ResultVo.fail(400, "当前支持 LLM 和 EMBEDDING 类型");
}
}
Db.tx(() -> {
for (int i = 0; i < models.size(); i++) {
JSONObject model = models.getJSONObject(i);
Db.update("insert into moss_kb_model_catalog(provider,model_type,model_id,description,source,enabled) values(?,?,?,?,?,?) "
+ "on conflict(provider,model_type,model_id) do update set description=excluded.description,source=excluded.source,enabled=excluded.enabled,updated_at=now()",
provider, model.getString("model_type"), modelId(model.getString("name")), model.getString("desc"),
"admin", !Boolean.FALSE.equals(model.getBoolean("enabled")));
}
return true;
});
return ResultVo.ok(Kv.by("updated", models.size()));
} catch (IllegalArgumentException e) {
return ResultVo.fail(400, e.getMessage());
}
}
}
平台地址、目录刷新命令、模型类型边界与验证记录详见第 35 章。
