🐛 fix(config-secret-storage): 修复密文编辑与状态残留问题

- 修复自定义连接编辑时已保存 DSN 无法留空沿用的问题
- 重置 AI 供应商编辑态与清空密钥开关,避免关闭后状态残留
- 对齐浏览器 mock 复制连接的 config.id 语义并补充回归测试
This commit is contained in:
tianqijiuyun-latiao
2026-04-04 10:51:32 +08:00
parent 4718755208
commit 255cc14bf6
9 changed files with 355 additions and 62 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Modal, Button, Input, Select, Form, Checkbox, message as antdMessage, Tooltip, Tabs, Space, Popconfirm, Slider } from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, ApiOutlined, SafetyCertificateOutlined, RobotOutlined, ThunderboltOutlined, CloudOutlined, ExperimentOutlined, KeyOutlined, LinkOutlined, AppstoreOutlined, ToolOutlined } from '@ant-design/icons';
import type { AIProviderConfig, AIProviderType, AISafetyLevel, AIContextLevel } from '../types';
@@ -19,6 +19,7 @@ import {
PROVIDER_PRESET_CARD_TITLE_STYLE,
} from '../utils/aiSettingsPresetLayout';
import { resolveProviderSecretDraft } from '../utils/providerSecretDraft';
import { buildAddProviderEditorSession, buildClosedProviderEditorSession, buildEditProviderEditorSession, type ProviderEditorSession } from '../utils/aiProviderEditorState';
import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
@@ -134,18 +135,41 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
useEffect(() => { if (open) void loadConfig(); }, [open, loadConfig]);
const applyProviderEditorSession = useCallback((session: ProviderEditorSession) => {
setEditingProvider(session.editingProvider as AIProviderConfig | null);
setIsEditing(session.isEditing);
setTestStatus(session.testStatus);
setClearProviderSecret(session.clearProviderSecret);
form.resetFields();
if (session.formValues) {
form.setFieldsValue(session.formValues);
}
}, [form]);
const resetProviderEditorSession = useCallback(() => {
applyProviderEditorSession(buildClosedProviderEditorSession());
}, [applyProviderEditorSession]);
const handleModalClose = useCallback(() => {
resetProviderEditorSession();
onClose();
}, [onClose, resetProviderEditorSession]);
useEffect(() => {
if (!open) {
resetProviderEditorSession();
}
}, [open, resetProviderEditorSession]);
const handleAddProvider = () => {
const preset = findPreset('openai');
const newProvider: AIProviderConfig = {
id: '', type: preset.backendType, name: '', apiKey: '',
baseUrl: preset.defaultBaseUrl, model: preset.defaultModel,
models: [], maxTokens: 4096, temperature: 0.7,
};
setEditingProvider({ ...newProvider, presetKey: 'openai' } as any);
setIsEditing(true);
setTestStatus('idle');
form.resetFields();
form.setFieldsValue({ ...newProvider, presetKey: 'openai', apiFormat: 'openai' });
applyProviderEditorSession(buildAddProviderEditorSession({
presetKey: 'openai',
presetBackendType: preset.backendType,
presetBaseUrl: preset.defaultBaseUrl,
presetModel: preset.defaultModel,
presetModels: preset.models,
apiFormat: 'openai',
}));
};
const handleEditProvider = (p: AIProviderConfig) => {
@@ -156,17 +180,16 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
presetFixedApiFormat: matchedPreset.fixedApiFormat,
valuesApiFormat: p.apiFormat,
});
setEditingProvider(p);
setIsEditing(true);
setTestStatus('idle');
form.resetFields();
form.setFieldsValue({
...p,
type: resolvedTransport.type,
models: p.models || [],
presetKey: matchedPreset.key,
apiFormat: resolvedTransport.apiFormat || p.apiFormat || 'openai',
});
applyProviderEditorSession(buildEditProviderEditorSession({
provider: { ...p, presetKey: matchedPreset.key } as any,
formValues: {
...p,
type: resolvedTransport.type,
models: p.models || [],
presetKey: matchedPreset.key,
apiFormat: resolvedTransport.apiFormat || p.apiFormat || 'openai',
},
}));
};
const handleDeleteProvider = async (id: string) => {
@@ -239,7 +262,7 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
};
// 后端 AISaveProvider 统一处理新增和更新,返回 void失败抛异常
await Service?.AISaveProvider?.(payload);
void messageApi.success('已保存'); setIsEditing(false); setEditingProvider(null); setClearProviderSecret(false); void loadConfig();
void messageApi.success('已保存'); resetProviderEditorSession(); void loadConfig();
window.dispatchEvent(new CustomEvent('gonavi:ai:provider-changed'));
} catch (e: any) {
if (e?.errorFields) { /* antd form validation error, ignore */ }
@@ -420,7 +443,7 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
<div>
{/* 顶部返回 */}
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 10 }}>
<Button size="small" onClick={() => { setIsEditing(false); setEditingProvider(null); setClearProviderSecret(false); }}
<Button size="small" onClick={resetProviderEditorSession}
style={{ borderRadius: 8 }}> </Button>
<span style={{ fontWeight: 700, fontSize: 16, color: overlayTheme.titleText }}>
{editingProvider?.id ? '编辑模型供应商' : '添加模型供应商'}
@@ -732,7 +755,7 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
</div>
}
open={open}
onCancel={onClose}
onCancel={handleModalClose}
footer={null}
width={820}
styles={{
@@ -802,3 +825,5 @@ export default AISettingsModal;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Select, Alert, Card, Row, Col, Typography, Collapse, Space, Table, Tag } from 'antd';
import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined, CheckCircleFilled, CloseCircleFilled, LinkOutlined, EditOutlined, AppstoreOutlined, BgColorsOutlined } from '@ant-design/icons';
import { getDbIcon, getDbDefaultColor, getDbIconLabel, DB_ICON_TYPES, PRESET_ICON_COLORS } from './DatabaseIcons';
@@ -6,6 +6,7 @@ import { useStore } from '../store';
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import { resolveConnectionSecretDraft } from '../utils/connectionSecretDraft';
import { getCustomConnectionDsnValidationMessage } from '../utils/customConnectionDsn';
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectDatabaseFile, SelectSSHKeyFile } from '../../wailsjs/go/app/App';
import { ConnectionConfig, MongoMemberInfo, SavedConnection } from '../types';
@@ -819,6 +820,19 @@ const ConnectionModal: React.FC<{
}
});
const createCustomDsnRule = () => ({
validator(_: unknown, value: unknown) {
const validationMessage = getCustomConnectionDsnValidationMessage({
dsnInput: value,
hasStoredSecret: initialValues?.hasOpaqueDSN,
clearStoredSecret: clearSecrets.opaqueDSN,
});
return validationMessage
? Promise.reject(new Error(validationMessage))
: Promise.resolve();
}
});
const getUriPlaceholder = () => {
if (dbType === 'mysql' || dbType === 'mariadb' || dbType === 'diros' || dbType === 'sphinx') {
const defaultPort = getDefaultPortByType(dbType);
@@ -2100,7 +2114,7 @@ const ConnectionModal: React.FC<{
<Form.Item name="driver" label="驱动名称 (Driver Name)" rules={[{ required: true, message: '请输入驱动名称' }]} help="已支持: mysql, postgres, sqlite, oracle, dm, kingbase">
<Input placeholder="例如: mysql, postgres" />
</Form.Item>
<Form.Item name="dsn" label="连接字符串 (DSN)" rules={[{ required: true, message: '请输入连接字符串' }]}>
<Form.Item name="dsn" label="连接字符串 (DSN)" rules={[createCustomDsnRule()]}>
<Input.TextArea rows={4} placeholder="例如: user:pass@tcp(localhost:3306)/dbname?charset=utf8" />
</Form.Item>
{renderStoredSecretControls({
@@ -3108,3 +3122,5 @@ export default ConnectionModal;