mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-06 22:53:35 +08:00
🐛 fix(config-secret-storage): 修复密文编辑与状态残留问题
- 修复自定义连接编辑时已保存 DSN 无法留空沿用的问题 - 重置 AI 供应商编辑态与清空密钥开关,避免关闭后状态残留 - 对齐浏览器 mock 复制连接的 config.id 语义并补充回归测试
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user