This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new d06c773 fix: avoid returning General Settings API key (#512)
d06c773 is described below
commit d06c7738662852b8b7a7e764cd8905b067178a28
Author: yx9o <[email protected]>
AuthorDate: Fri Jul 24 13:43:28 2026 +0800
fix: avoid returning General Settings API key (#512)
Fix security issue where General Settings endpoint returned API key in
plaintext. Replace with apiKeyConfigured boolean flag and support explicit key
clearing.
---
docs/api-spec.md | 18 +++++-
.../studio/settings/GeneralSettingsUpdateDTO.java | 73 ++++++++++++++++++++++
.../studio/settings/GeneralSettingsVO.java | 12 ++++
.../studio/settings/SettingsController.java | 5 +-
.../rocketmq/studio/settings/SettingsService.java | 10 ++-
.../studio/settings/SettingsControllerTest.java | 71 ++++++++++++++++++---
.../studio/settings/SettingsServiceTest.java | 61 ++++++++++++++++--
web/src/api/generalSettings.test.ts | 47 ++++++++++++--
web/src/api/settings.ts | 14 ++++-
web/src/pages/settings/index.tsx | 38 +++++++++--
10 files changed, 318 insertions(+), 31 deletions(-)
diff --git a/docs/api-spec.md b/docs/api-spec.md
index 2b0bd14..e75158f 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1483,7 +1483,7 @@ GET /api/settings/general
| `sessionTimeout` | `number` | 会话超时(分钟,5-1440) |
| `requireLogin` | `boolean` | 是否需要登录 |
| `llmProvider` | `string` | LLM 提供商: `openai` / `azure` / `ollama` / `qwen` |
-| `apiKey` | `string` | API Key |
+| `apiKeyConfigured` | `boolean` | 是否已配置 API Key;响应不会返回密钥内容 |
| `model` | `string` | 模型名称 |
| `baseUrl` | `string` | Base URL |
@@ -1493,7 +1493,21 @@ GET /api/settings/general
POST /api/settings/general/save
```
-**Request Body:** 同 14.1 响应格式
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `theme` | `string` | 是 | 主题: `light` / `dark` / `system` |
+| `compact` | `boolean` | 是 | 紧凑模式 |
+| `desktopNotify` | `boolean` | 是 | 桌面通知 |
+| `notifySound` | `boolean` | 是 | 通知声音 |
+| `sessionTimeout` | `number` | 是 | 会话超时(分钟,5-1440) |
+| `requireLogin` | `boolean` | 是 | 是否需要登录 |
+| `llmProvider` | `string` | 是 | LLM 提供商 |
+| `apiKey` | `string` | 否 | 新 API Key;省略或传空值时保留现有密钥 |
+| `clearApiKey` | `boolean` | 否 | 传 `true` 时显式清除现有密钥,优先级高于 `apiKey` |
+| `model` | `string` | 是 | 模型名称 |
+| `baseUrl` | `string` | 是 | Base URL |
**Response `data`:** `null`
diff --git
a/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
new file mode 100644
index 0000000..fbfea40
--- /dev/null
+++
b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.rocketmq.studio.settings;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class GeneralSettingsUpdateDTO {
+ @NotBlank
+ private String theme;
+ @NotNull
+ private Boolean compact;
+ @NotNull
+ private Boolean desktopNotify;
+ @NotNull
+ private Boolean notifySound;
+ @NotNull
+ @Min(5)
+ @Max(1440)
+ private Integer sessionTimeout;
+ @NotNull
+ private Boolean requireLogin;
+ @NotBlank
+ private String llmProvider;
+ @ToString.Exclude
+ private String apiKey;
+ private boolean clearApiKey;
+ @NotBlank
+ private String model;
+ @NotNull
+ private String baseUrl;
+
+ public GeneralSettingsVO toSettings() {
+ return GeneralSettingsVO.builder()
+ .theme(theme)
+ .compact(compact)
+ .desktopNotify(desktopNotify)
+ .notifySound(notifySound)
+ .sessionTimeout(sessionTimeout)
+ .requireLogin(requireLogin)
+ .llmProvider(llmProvider)
+ .apiKey(apiKey)
+ .clearApiKey(clearApiKey)
+ .model(model)
+ .baseUrl(baseUrl)
+ .build();
+ }
+}
diff --git
a/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java
b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java
index 8dd3bd6..bfb6083 100644
--- a/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java
+++ b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java
@@ -16,10 +16,13 @@
*/
package com.rocketmq.studio.settings;
+import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.springframework.util.StringUtils;
@Data
@Builder
@@ -33,7 +36,16 @@ public class GeneralSettingsVO {
private int sessionTimeout;
private boolean requireLogin;
private String llmProvider;
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ @ToString.Exclude
private String apiKey;
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ private boolean clearApiKey;
private String model;
private String baseUrl;
+
+ @JsonProperty(value = "apiKeyConfigured", access =
JsonProperty.Access.READ_ONLY)
+ public boolean isApiKeyConfigured() {
+ return StringUtils.hasText(apiKey);
+ }
}
diff --git
a/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java
b/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java
index a9a9123..a84923d 100644
--- a/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java
+++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java
@@ -17,6 +17,7 @@
package com.rocketmq.studio.settings;
import com.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -40,8 +41,8 @@ public class SettingsController {
}
@PostMapping("/general/save")
- public Result<Void> saveGeneralSettings(@RequestBody GeneralSettingsVO
settings) {
- settingsService.saveGeneralSettings(settings);
+ public Result<Void> saveGeneralSettings(@Valid @RequestBody
GeneralSettingsUpdateDTO request) {
+ settingsService.saveGeneralSettings(request.toSettings());
return Result.ok();
}
diff --git
a/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java
b/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java
index 5505d27..2c2db87 100644
--- a/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java
+++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java
@@ -19,6 +19,7 @@ package com.rocketmq.studio.settings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
import java.util.List;
import java.util.UUID;
@@ -37,8 +38,15 @@ public class SettingsService {
}
- public void saveGeneralSettings(GeneralSettingsVO settings) {
+ public synchronized void saveGeneralSettings(GeneralSettingsVO settings) {
log.info("Saving general settings");
+ GeneralSettingsVO currentSettings =
settingsRepository.loadGeneralSettings();
+ if (settings.isClearApiKey()) {
+ settings.setApiKey("");
+ } else if (!StringUtils.hasText(settings.getApiKey()) &&
currentSettings != null) {
+ settings.setApiKey(currentSettings.getApiKey());
+ }
+ settings.setClearApiKey(false);
settingsRepository.saveGeneralSettings(settings);
}
diff --git
a/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java
b/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java
index 42a3c0e..f82bb1a 100644
---
a/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java
+++
b/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java
@@ -30,8 +30,10 @@ import java.util.Collections;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -77,26 +79,81 @@ class SettingsControllerTest {
.andExpect(jsonPath("$.data.sessionTimeout", is(30)))
.andExpect(jsonPath("$.data.requireLogin", is(true)))
.andExpect(jsonPath("$.data.llmProvider", is("openai")))
+ .andExpect(jsonPath("$.data.apiKey").doesNotExist())
+ .andExpect(jsonPath("$.data.apiKeyConfigured", is(true)))
+ .andExpect(jsonPath("$.data.clearApiKey").doesNotExist())
.andExpect(jsonPath("$.data.model", is("gpt-4")));
}
@Test
void saveGeneralSettingsShouldReturnSuccess() throws Exception {
- GeneralSettingsVO settings = GeneralSettingsVO.builder()
- .theme("light")
- .compact(false)
- .sessionTimeout(60)
- .build();
doNothing().when(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class));
mockMvc.perform(post("/api/settings/general/save")
.contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(settings)))
+ .content("""
+ {
+ "theme": "light",
+ "compact": false,
+ "desktopNotify": true,
+ "notifySound": false,
+ "sessionTimeout": 60,
+ "requireLogin": true,
+ "llmProvider": "openai",
+ "apiKey": "sk-new",
+ "apiKeyConfigured": false,
+ "model": "gpt-4",
+ "baseUrl": ""
+ }
+ """))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code", is(200)))
.andExpect(jsonPath("$.data").doesNotExist());
-
verify(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class));
+ verify(settingsService).saveGeneralSettings(argThat(settings ->
+ "sk-new".equals(settings.getApiKey()) &&
settings.isApiKeyConfigured()));
+ }
+
+ @Test
+ void
saveGeneralSettingsShouldAcceptExplicitApiKeyClearWithoutBindingResponseState()
throws Exception {
+
doNothing().when(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class));
+
+ mockMvc.perform(post("/api/settings/general/save")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
+ "theme": "light",
+ "compact": false,
+ "desktopNotify": true,
+ "notifySound": false,
+ "sessionTimeout": 60,
+ "requireLogin": true,
+ "llmProvider": "openai",
+ "clearApiKey": true,
+ "apiKeyConfigured": true,
+ "model": "gpt-4",
+ "baseUrl": ""
+ }
+ """))
+ .andExpect(status().isOk());
+
+ verify(settingsService).saveGeneralSettings(argThat(settings ->
+ settings.isClearApiKey() && !settings.isApiKeyConfigured()));
+ }
+
+ @Test
+ void saveGeneralSettingsShouldRejectIncompleteReplacement() throws
Exception {
+ mockMvc.perform(post("/api/settings/general/save")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("""
+ {
+ "theme": "light",
+ "apiKey": "sk-new"
+ }
+ """))
+ .andExpect(status().isBadRequest());
+
+ verifyNoInteractions(settingsService);
}
@Test
diff --git
a/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java
b/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java
index 430021a..024a9a7 100644
--- a/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java
+++ b/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java
@@ -68,17 +68,68 @@ class SettingsServiceTest {
}
@Test
- void saveGeneralSettingsShouldDelegateToRepository() {
- GeneralSettingsVO settings = GeneralSettingsVO.builder()
+ void saveGeneralSettingsShouldPreserveExistingApiKeyWhenOmitted() {
+ GeneralSettingsVO existing = GeneralSettingsVO.builder()
+ .apiKey("sk-existing")
+ .build();
+ GeneralSettingsVO update = GeneralSettingsVO.builder()
.theme("light")
.compact(false)
.sessionTimeout(60)
.build();
- doNothing().when(settingsRepository).saveGeneralSettings(settings);
+ when(settingsRepository.loadGeneralSettings()).thenReturn(existing);
+
+ settingsService.saveGeneralSettings(update);
+
+ assertThat(update.getApiKey()).isEqualTo("sk-existing");
+ verify(settingsRepository).saveGeneralSettings(update);
+ }
+
+ @Test
+ void saveGeneralSettingsShouldReplaceExistingApiKey() {
+ GeneralSettingsVO existing = GeneralSettingsVO.builder()
+ .apiKey("sk-existing")
+ .build();
+ GeneralSettingsVO update = GeneralSettingsVO.builder()
+ .apiKey("sk-new")
+ .build();
+ when(settingsRepository.loadGeneralSettings()).thenReturn(existing);
+
+ settingsService.saveGeneralSettings(update);
+
+ assertThat(update.getApiKey()).isEqualTo("sk-new");
+ verify(settingsRepository).saveGeneralSettings(update);
+ }
+
+ @Test
+ void saveGeneralSettingsShouldClearApiKeyOnlyWhenExplicitlyRequested() {
+ GeneralSettingsVO existing = GeneralSettingsVO.builder()
+ .apiKey("sk-existing")
+ .build();
+ GeneralSettingsVO update = GeneralSettingsVO.builder()
+ .clearApiKey(true)
+ .build();
+ when(settingsRepository.loadGeneralSettings()).thenReturn(existing);
+
+ settingsService.saveGeneralSettings(update);
+
+ assertThat(update.getApiKey()).isEmpty();
+ assertThat(update.isClearApiKey()).isFalse();
+ verify(settingsRepository).saveGeneralSettings(update);
+ }
+
+ @Test
+ void
saveGeneralSettingsShouldLetClearTakePrecedenceOverReplacementApiKey() {
+ GeneralSettingsVO update = GeneralSettingsVO.builder()
+ .apiKey("sk-new")
+ .clearApiKey(true)
+ .build();
- settingsService.saveGeneralSettings(settings);
+ settingsService.saveGeneralSettings(update);
- verify(settingsRepository).saveGeneralSettings(settings);
+ assertThat(update.getApiKey()).isEmpty();
+ assertThat(update.isClearApiKey()).isFalse();
+ verify(settingsRepository).saveGeneralSettings(update);
}
@Test
diff --git a/web/src/api/generalSettings.test.ts
b/web/src/api/generalSettings.test.ts
index 1a0700c..b8928fb 100644
--- a/web/src/api/generalSettings.test.ts
+++ b/web/src/api/generalSettings.test.ts
@@ -19,7 +19,7 @@ import MockAdapter from 'axios-mock-adapter';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import client from './client';
import { getGeneralSettings, saveGeneralSettings } from './settings';
-import type { GeneralSettings } from './settings';
+import type { GeneralSettings, GeneralSettingsUpdate } from './settings';
const mock = new MockAdapter(client);
const settings: GeneralSettings = {
@@ -30,10 +30,21 @@ const settings: GeneralSettings = {
sessionTimeout: 60,
requireLogin: true,
llmProvider: 'openai',
- apiKey: 'sk-test',
+ apiKeyConfigured: true,
model: 'gpt-5',
baseUrl: 'https://api.example.com/v1',
};
+const editableSettings: GeneralSettingsUpdate = {
+ theme: settings.theme,
+ compact: settings.compact,
+ desktopNotify: settings.desktopNotify,
+ notifySound: settings.notifySound,
+ sessionTimeout: settings.sessionTimeout,
+ requireLogin: settings.requireLogin,
+ llmProvider: settings.llmProvider,
+ model: settings.model,
+ baseUrl: settings.baseUrl,
+};
describe('general settings API', () => {
beforeEach(() => {
@@ -52,12 +63,38 @@ describe('general settings API', () => {
await expect(getGeneralSettings()).resolves.toEqual(settings);
});
- it('persists all editable settings fields', async () => {
+ it('sends a replacement API key without requiring the stored secret', async
() => {
+ const update: GeneralSettingsUpdate = { ...editableSettings, apiKey:
'sk-new' };
+ mock.onPost('/settings/general/save').reply((config) => {
+ expect(JSON.parse(config.data)).toEqual(update);
+ return [200, { code: 200, data: null }];
+ });
+
+ await expect(saveGeneralSettings(update)).resolves.toBeUndefined();
+ });
+
+ it('omits blank and response-only API key fields when preserving the stored
secret', async () => {
+ const update = {
+ ...settings,
+ apiKey: ' ',
+ } as GeneralSettingsUpdate & { apiKeyConfigured: boolean };
+ mock.onPost('/settings/general/save').reply((config) => {
+ const body = JSON.parse(config.data);
+ expect(body).not.toHaveProperty('apiKey');
+ expect(body).not.toHaveProperty('apiKeyConfigured');
+ return [200, { code: 200, data: null }];
+ });
+
+ await expect(saveGeneralSettings(update)).resolves.toBeUndefined();
+ });
+
+ it('sends the explicit API key clear flag', async () => {
+ const update: GeneralSettingsUpdate = { ...editableSettings, clearApiKey:
true };
mock.onPost('/settings/general/save').reply((config) => {
- expect(JSON.parse(config.data)).toEqual(settings);
+ expect(JSON.parse(config.data)).toEqual(update);
return [200, { code: 200, data: null }];
});
- await expect(saveGeneralSettings(settings)).resolves.toBeUndefined();
+ await expect(saveGeneralSettings(update)).resolves.toBeUndefined();
});
});
diff --git a/web/src/api/settings.ts b/web/src/api/settings.ts
index e8b67e6..19829f7 100644
--- a/web/src/api/settings.ts
+++ b/web/src/api/settings.ts
@@ -26,11 +26,16 @@ export interface GeneralSettings {
sessionTimeout: number;
requireLogin: boolean;
llmProvider: string;
- apiKey: string;
+ apiKeyConfigured: boolean;
model: string;
baseUrl: string;
}
+export type GeneralSettingsUpdate = Omit<GeneralSettings, 'apiKeyConfigured'>
& {
+ apiKey?: string;
+ clearApiKey?: boolean;
+};
+
export interface DataSource {
key: string;
name: string;
@@ -46,8 +51,11 @@ export async function getGeneralSettings() {
return res.data.data;
}
-export async function saveGeneralSettings(data: Partial<GeneralSettings>) {
- await client.post('/settings/general/save', data);
+export async function saveGeneralSettings(data: GeneralSettingsUpdate) {
+ const payload = { ...data } as GeneralSettingsUpdate & { apiKeyConfigured?:
boolean };
+ delete payload.apiKeyConfigured;
+ if (!payload.apiKey?.trim()) delete payload.apiKey;
+ await client.post('/settings/general/save', payload);
}
// ─── Data Sources ───────────────────────────────────────────────
diff --git a/web/src/pages/settings/index.tsx b/web/src/pages/settings/index.tsx
index 2e5a321..9344eea 100644
--- a/web/src/pages/settings/index.tsx
+++ b/web/src/pages/settings/index.tsx
@@ -18,6 +18,7 @@
import { useEffect, useState } from 'react';
import {
Button,
+ Checkbox,
Descriptions,
Divider,
Flex,
@@ -50,7 +51,7 @@ import PageHeader from '../../components/PageHeader';
import { useLang } from '../../i18n/LangContext';
import StatusBadge from '../../components/StatusBadge';
import { getGeneralSettings, saveGeneralSettings } from '../../api/settings';
-import type { GeneralSettings } from '../../api/settings';
+import type { GeneralSettingsUpdate } from '../../api/settings';
import {
createDataSource,
deleteDataSource,
@@ -72,15 +73,20 @@ const typeTagColor: Record<string, string> = {
// ─── General Settings Tab ───────────────────────────────────────────────────
const GeneralSettingsTab = () => {
- const [form] = Form.useForm();
+ const [form] = Form.useForm<GeneralSettingsUpdate>();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
+ const [apiKeyConfigured, setApiKeyConfigured] = useState(false);
+ const clearApiKey = Form.useWatch('clearApiKey', form);
useEffect(() => {
let cancelled = false;
void getGeneralSettings()
.then((settings) => {
- if (!cancelled) form.setFieldsValue(settings);
+ if (!cancelled) {
+ setApiKeyConfigured(settings.apiKeyConfigured);
+ form.setFieldsValue({ ...settings, apiKey: undefined, clearApiKey:
false });
+ }
})
.catch(() => {
if (!cancelled) message.error('通用设置加载失败,请稍后重试');
@@ -94,10 +100,14 @@ const GeneralSettingsTab = () => {
};
}, [form]);
- const handleFinish = async (values: GeneralSettings) => {
+ const handleFinish = async (values: GeneralSettingsUpdate) => {
setSaving(true);
try {
await saveGeneralSettings(values);
+ setApiKeyConfigured(
+ values.clearApiKey ? false : apiKeyConfigured ||
Boolean(values.apiKey?.trim()),
+ );
+ form.setFieldsValue({ apiKey: undefined, clearApiKey: false });
message.success('设置已保存');
} catch {
message.error('设置保存失败,请稍后重试');
@@ -187,10 +197,26 @@ const GeneralSettingsTab = () => {
/>
</Form.Item>
- <Form.Item label="API Key" name="apiKey">
- <Input.Password placeholder="sk-..." />
+ <Form.Item
+ label="API Key"
+ name="apiKey"
+ extra={apiKeyConfigured ? '已配置;留空将保留现有密钥' : '尚未配置'}
+ >
+ <Input.Password placeholder="sk-..." disabled={clearApiKey} />
</Form.Item>
+ {apiKeyConfigured && (
+ <Form.Item name="clearApiKey" valuePropName="checked" wrapperCol={{
offset: 4, span: 14 }}>
+ <Checkbox
+ onChange={(event) => {
+ if (event.target.checked) form.setFieldValue('apiKey',
undefined);
+ }}
+ >
+ 清除已保存的 API Key
+ </Checkbox>
+ </Form.Item>
+ )}
+
<Form.Item label="模型名称" name="model">
<Input placeholder="qwen-max" />
</Form.Item>