fix: return model keys in admin details and allow clearing

This commit is contained in:
ageerle
2026-09-22 23:05:59 +08:00
parent 8e3b286190
commit 197dc40510
8 changed files with 49 additions and 14 deletions

View File

@@ -26,7 +26,7 @@ API Key 统一在 **ruoyi-admin → 对话管理 → 模型管理** 中直接填
| 密钥 | 从 Dify 应用复制的真实 API Key,如 `app-...` |
| 是否显示 | 是 |
直接填写真实应用 Key,不需要 `env:` 或 `Bearer ` 前缀。编辑时 Key 不回显;留空保留原 Key,填写新值会替换原值。保存后新的调用读取最新配置,无需重启后端。
直接填写真实应用 Key,不需要 `env:` 或 `Bearer ` 前缀。编辑时回显已保存的 Key,默认隐藏,点击眼睛可查看原文。输入内容原样保存,清空后保存会清除原 Key。保存后新的调用读取最新配置,无需重启后端。
请求地址填写到 `/v1`,不要追加 `/chat-messages`,也不要填写 Dify 应用的网页分享链接。模型名称是本系统的选择标识,可以取一个不重复的应用名称;实际调用哪个 Dify 应用由应用 Key 决定。
@@ -45,6 +45,6 @@ API Key 统一在 **ruoyi-admin → 对话管理 → 模型管理** 中直接填
| 现象 | 检查方法 |
| --- | --- |
| 后台无法保存 | 检查厂商是否启用,以及模型分类、名称等必填项是否齐全。 |
| 编辑时密钥框为空 | 密钥不会回显;留空保存会保留原 Key。 |
| 查看已保存的密钥 | 编辑时默认隐藏,点击密钥框右侧的眼睛查看原文;清空后保存会清除密钥。 |
| Dify 返回鉴权失败 | 在模型管理中填写当前应用的有效 API Key,检查复制时是否带入空格或 `Bearer ` 前缀。 |
| Dify 返回应用类型或输入参数错误 | 检查是否使用聊天应用,以及应用是否要求额外输入变量;先在 Dify 中验证应用可以回答。 |

View File

@@ -11,7 +11,8 @@
## 修改与批量更新
- 编辑模型时 Key 不回显;留空保存会保留原 Key,输入新值会替换原 Key。
- 编辑模型时回显已保存的 Key,默认以密码形式隐藏,点击眼睛可查看原文;每次重新打开编辑框都会恢复隐藏。
- 密钥按输入内容原样保存,不限制前缀或格式,也不去除首尾空格。清空后保存会清除原 Key。
- 按厂商批量更新接口会将提交的 Key 保存到该厂商的模型中。
- 保存后新的模型调用会读取最新配置,无需重启后端。正在进行的请求继续使用开始时的配置。
- 模型列表、聊天用户端和导出文件不返回 Key。

View File

@@ -0,0 +1,23 @@
package org.ruoyi.common.chat.domain.vo.chat;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.beans.BeanUtils;
/** 模型管理编辑详情,允许有查询权限的管理员回显已保存的密钥。 */
public class ChatModelDetailVo extends ChatModelVo {
@Override
@JsonProperty(access = JsonProperty.Access.READ_WRITE)
public String getApiKey() {
return super.getApiKey();
}
public static ChatModelDetailVo from(ChatModelVo model) {
if (model == null) {
return null;
}
ChatModelDetailVo detail = new ChatModelDetailVo();
BeanUtils.copyProperties(model, detail);
return detail;
}
}

View File

@@ -77,8 +77,7 @@ public class ChatModelVo implements Serializable {
private String apiHost;
/**
* 密钥仅用于服务端模型调用和写入配置。任何读取接口、日志字符串或 Excel 导出
* 都不得把它带出服务端边界。
* 列表、日志及导出不包含密钥;管理员编辑详情由 ChatModelDetailVo 回显原值。
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ToString.Exclude

View File

@@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
import org.ruoyi.common.chat.domain.bo.chat.ModelBatchKeyBo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelDetailVo;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
@@ -61,6 +62,20 @@ class ModelApiKeyValidationTest {
assertFalse(view.toString().contains("test-secret"));
}
@Test
void adminDetailReturnsExactKeyWithoutChangingListOrLogSerialization() throws Exception {
var mapper = new ObjectMapper();
var model = new ChatModelVo();
model.setId(12L);
model.setApiKey(" 任意/key: value ");
var detail = ChatModelDetailVo.from(model);
assertEquals(model.getApiKey(), mapper.readTree(mapper.writeValueAsString(detail)).get("apiKey").asText());
assertEquals(model.getId(), detail.getId());
assertFalse(mapper.writeValueAsString(model).contains("apiKey"));
assertFalse(detail.toString().contains(model.getApiKey()));
assertNull(ChatModelDetailVo.from(null));
}
@Test
void customEndpointsNeedNoDeploymentVariablesAndSupportLocalServices() {
assertEquals("http://localhost:11434/v1", CustomApiCredentialPolicy.normalizeBaseUrl(

View File

@@ -13,6 +13,7 @@ import org.ruoyi.common.chat.domain.bo.chat.ChatModelSelectQuery;
import org.ruoyi.common.chat.domain.bo.chat.ModelBatchKeyBo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelSelectVo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelDetailVo;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.enums.ModelType;
@@ -105,9 +106,9 @@ public class ChatModelController extends BaseController {
*/
@SaCheckPermission("system:model:query")
@GetMapping("/{id}")
public R<ChatModelVo> getInfo(@NotNull(message = "主键不能为空")
public R<ChatModelDetailVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(chatModelService.queryById(id));
return R.ok(ChatModelDetailVo.from(chatModelService.queryById(id)));
}
/**

View File

@@ -147,10 +147,6 @@ public class ChatModelServiceImpl implements IChatModelService {
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(ChatModelBo bo) {
ChatModel update = toEntity(bo);
// 编辑表单不回显密钥;留空时保留数据库中的原值。
if ("".equals(update.getApiKey())) {
update.setApiKey(null);
}
ChatModel current = selectByIdForUpdate(update.getId());
if (current == null) {
return false;

View File

@@ -9,7 +9,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
@@ -47,7 +47,7 @@ class ChatModelApiKeyTest {
}
@ParameterizedTest
@ValueSource(strings = {"sk-new-key", " env:LITERAL_VALUE ", "env:ATLAS_API_KEY"})
@ValueSource(strings = {"", " ", "任意内容/key:123", "sk-new-key", " env:LITERAL_VALUE ", "env:ATLAS_API_KEY"})
void editReplacesAnOldReferenceWithExactlyTheSuppliedValue(String key) {
when(mapper.selectOne(any())).thenReturn(currentModel());
when(mapper.updateById(any(ChatModel.class))).thenReturn(1);
@@ -58,7 +58,7 @@ class ChatModelApiKeyTest {
}
@ParameterizedTest
@NullAndEmptySource
@NullSource
void editWithoutANewKeyDoesNotOverwriteTheExistingKey(String key) {
when(mapper.selectOne(any())).thenReturn(currentModel());
when(mapper.updateById(any(ChatModel.class))).thenReturn(1);