diff --git a/docs/dify-cloud.md b/docs/dify-cloud.md index dbf25ca0..ed17dd16 100644 --- a/docs/dify-cloud.md +++ b/docs/dify-cloud.md @@ -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 中验证应用可以回答。 | diff --git a/docs/model-configuration.md b/docs/model-configuration.md index 5dfab510..cac5d7f4 100644 --- a/docs/model-configuration.md +++ b/docs/model-configuration.md @@ -11,7 +11,8 @@ ## 修改与批量更新 -- 编辑模型时 Key 不回显;留空保存会保留原 Key,输入新值会替换原 Key。 +- 编辑模型时回显已保存的 Key,默认以密码形式隐藏,点击眼睛可查看原文;每次重新打开编辑框都会恢复隐藏。 +- 密钥按输入内容原样保存,不限制前缀或格式,也不去除首尾空格。清空后保存会清除原 Key。 - 按厂商批量更新接口会将提交的 Key 保存到该厂商的模型中。 - 保存后新的模型调用会读取最新配置,无需重启后端。正在进行的请求继续使用开始时的配置。 - 模型列表、聊天用户端和导出文件不返回 Key。 diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelDetailVo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelDetailVo.java new file mode 100644 index 00000000..4f3a285a --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelDetailVo.java @@ -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; + } +} diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java index cf7f9ce5..d7d0c183 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java @@ -77,8 +77,7 @@ public class ChatModelVo implements Serializable { private String apiHost; /** - * 密钥仅用于服务端模型调用和写入配置。任何读取接口、日志字符串或 Excel 导出 - * 都不得把它带出服务端边界。 + * 列表、日志及导出不包含密钥;管理员编辑详情由 ChatModelDetailVo 回显原值。 */ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @ToString.Exclude diff --git a/ruoyi-common/ruoyi-common-chat/src/test/java/org/ruoyi/common/chat/security/ModelApiKeyValidationTest.java b/ruoyi-common/ruoyi-common-chat/src/test/java/org/ruoyi/common/chat/security/ModelApiKeyValidationTest.java index d16a702c..2e06750d 100644 --- a/ruoyi-common/ruoyi-common-chat/src/test/java/org/ruoyi/common/chat/security/ModelApiKeyValidationTest.java +++ b/ruoyi-common/ruoyi-common-chat/src/test/java/org/ruoyi/common/chat/security/ModelApiKeyValidationTest.java @@ -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( diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java index cca64794..685fd4b3 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java @@ -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 getInfo(@NotNull(message = "主键不能为空") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { - return R.ok(chatModelService.queryById(id)); + return R.ok(ChatModelDetailVo.from(chatModelService.queryById(id))); } /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java index d5142167..0c47bce2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java @@ -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; diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/ChatModelApiKeyTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/ChatModelApiKeyTest.java index 2fb15c2b..6f33491d 100644 --- a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/ChatModelApiKeyTest.java +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/ChatModelApiKeyTest.java @@ -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);