Skip to content

模型管理 ​

接口设计 ​

本文将详细介绍 Java MossKB 系统中模型管理的实现,包括其接口设计、后端代码实现以及整体架构。通过这一介绍,您将了解如何通过 API 进行模型的添加、更新、查询和删除,以及后端如何处理这些请求。

tio-boot 提供了一系列 RESTful API,用于管理不同提供者的模型。这些接口涵盖了模型提供者的获取、模型类型和列表的查询、模型的增删改查等操作。以下将逐一介绍这些接口及其响应格式。

默认模型:系统自带两个默认种子模型实例——推理用 Gitee 的 deepseek-v4.1-flash(官方平台把同系列模型 ID 写作 deepseek-flash),向量用 Qwen3-Embedding-8B(1024 维);扫描页 OCR 固定调用 PaddleOCR-VL-1.5,不随对话模型变化。平台、维度与配置键汇总见默认模型。

获取模型提供者列表 ​

请求

http
GET http://localhost:10060/api/provider

响应

json
{
  "code": 200,
  "message": "成功",
  "data": [
    {
      "provider": "model_gitee_provider",
      "name": "Gitee AI",
      "icon": "<svg ...></svg>"
    },
    {
      "provider": "model_openrouter_provider",
      "name": "OpenRouter",
      "icon": "<svg ...></svg>"
    }
  ]
}

说明

该接口返回所有可用的模型提供者,包括提供者名称、标识符以及图标。

获取模型列表 ​

根据不同的查询参数,模型列表的获取有所不同。

  1. 根据模型名称查询

    请求

    http
    GET http://localhost:10060/api/model?name=<model_name>

    响应

    json
    { "message": null, "data": [], "code": 200 }

    说明

    通过指定模型名称查询模型,返回匹配的模型列表。

  2. 根据提供者查询

    请求

    http
    GET http://localhost:10060/api/provider/model_list?provider=model_openai_provider&model_type=LLM

    响应

    json
    { "message": null, "data": [], "code": 200 }

    说明

    此接口返回指定平台的基础模型目录。/api/model 返回用户可用的模型实例,不能将其未实现的 provider 查询参数当作平台过滤。

获取模型类型列表 ​

请求

http
GET http://localhost:10060/api/provider/model_type_list?provider=model_openai_provider

响应

json
{
  "message": "成功",
  "data": [
    {
      "value": "LLM",
      "key": "大语言模型"
    },
    {
      "value": "EMBEDDING",
      "key": "向量模型"
    }
  ],
  "code": 200
}

说明

该接口返回指定提供者支持的模型类型列表,如大语言模型(LLM)和向量模型(EMBEDDING)。

获取模型详情 ​

请求

http
GET http://localhost:10060/api/model/442895624269676544

响应

json
{
  "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 获取具体模型的详细信息,包括提供者、模型类型、名称、状态及凭证信息等。

添加模型 ​

  1. 添加向量模型

    请求

    http
    POST http://localhost:10060/api/model

    请求体

    json
    {
      "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"
    }
  2. 添加大语言模型

    请求

    http
    POST http://localhost:10060/api/model

    请求体

    json
    {
      "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 请求可以添加新的模型,需提供模型名称、类型、具体名称、权限类型、凭证信息及所属提供者。

更新模型 ​

请求

http
PUT http://localhost:10060/api/model/442895624269676544

请求体

json
{
  "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 的模型信息,包括名称、类型、权限及凭证信息。

删除模型 ​

请求

http
DELETE http://localhost:10060/api/model/442895624269676544

说明

通过 DELETE 请求删除指定 ID 的模型。

后端代码实现 ​

Java MossKB 的后端采用 Java 语言的 tio-boot 框架实现,主要包括控制器、服务层和数据访问层(DAO)。以下将详细解析各部分代码及其功能。

ApiProviderController 控制器 ​

java
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 控制器 ​

java
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 服务层 ​

java
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 数据访问层 ​

java
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管理员登记或停用目录项

管理员登记目录请求:

json
{"provider":"model_default_provider","models":[{"name":"vendor/model-id","model_type":"LLM","desc":"自定义中转模型","enabled":true}]}

ModelCatalogService 目录服务实现 ​

java
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 章。

MIT Licensed