diff --git a/config/database_pg.go b/config/database_pg.go index a029948c..a7da471e 100644 --- a/config/database_pg.go +++ b/config/database_pg.go @@ -140,8 +140,9 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error SELECT id, user_id, name, provider, enabled, api_key, COALESCE(custom_api_url, '') as custom_api_url, COALESCE(custom_model_name, '') as custom_model_name, + COALESCE(deleted, FALSE) as deleted, created_at, updated_at - FROM ai_models WHERE user_id = $1 ORDER BY id + FROM ai_models WHERE user_id = $1 AND COALESCE(deleted, FALSE) = FALSE ORDER BY id `, userID) if err != nil { return nil, err @@ -152,10 +153,11 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error models := make([]*AIModelConfig, 0) for rows.Next() { var model AIModelConfig + var deleted bool // 临时变量,用于读取 deleted 字段但不保存到结构体 err := rows.Scan( &model.ID, &model.UserID, &model.Name, &model.Provider, &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, - &model.CreatedAt, &model.UpdatedAt, + &deleted, &model.CreatedAt, &model.UpdatedAt, ) if err != nil { return nil, err @@ -168,7 +170,59 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error // UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置 func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error { - // 先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型) + log.Printf("🔧 UpdateAIModel: userID=%s, id=%s, enabled=%v", userID, id, enabled) + + // 检查是否为删除操作(API Key 为空且 enabled 为 false 表示删除) + isDelete := !enabled && apiKey == "" && customAPIURL == "" && customModelName == "" + + if isDelete { + // 执行软删除:标记为已删除并清空敏感数据 + // 先尝试精确匹配 ID + var existingID string + err := d.db.QueryRow(` + SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1 + `, userID, id).Scan(&existingID) + + if err == nil { + // 找到了现有配置(精确匹配 ID),标记为删除并清空敏感数据 + _, err = d.db.Exec(` + UPDATE ai_models SET enabled = FALSE, deleted = TRUE, api_key = '', custom_api_url = '', custom_model_name = '', updated_at = CURRENT_TIMESTAMP + WHERE id = $1 AND user_id = $2 + `, existingID, userID) + if err != nil { + log.Printf("❌ UpdateAIModel: 标记删除失败: %v", err) + return err + } + log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s", userID, existingID) + return nil + } + + // ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找 + provider := id + err = d.db.QueryRow(` + SELECT id FROM ai_models WHERE user_id = $1 AND provider = $2 LIMIT 1 + `, userID, provider).Scan(&existingID) + + if err == nil { + // 找到了现有配置(通过 provider 匹配),标记为删除并清空敏感数据 + _, err = d.db.Exec(` + UPDATE ai_models SET enabled = FALSE, deleted = TRUE, api_key = '', custom_api_url = '', custom_model_name = '', updated_at = CURRENT_TIMESTAMP + WHERE id = $1 AND user_id = $2 + `, existingID, userID) + if err != nil { + log.Printf("❌ UpdateAIModel: 标记删除失败: %v", err) + return err + } + log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s (通过provider匹配)", userID, existingID) + return nil + } + + // 没有找到配置,返回成功(幂等性) + log.Printf("ℹ️ UpdateAIModel: 模型配置不存在,跳过删除: %s", id) + return nil + } + + // 启用模型的情况:先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型) var existingID string err := d.db.QueryRow(` SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1 @@ -177,7 +231,7 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK if err == nil { // 找到了现有配置(精确匹配 ID),更新它 _, err = d.db.Exec(` - UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP + UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $5 AND user_id = $6 `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) return err @@ -193,7 +247,7 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK // 找到了现有配置(通过 provider 匹配,兼容旧版),更新它 log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) _, err = d.db.Exec(` - UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP + UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $5 AND user_id = $6 `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) return err diff --git a/db/init.sql b/db/init.sql index efec4646..c89fa78a 100644 --- a/db/init.sql +++ b/db/init.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS ai_models ( api_key TEXT DEFAULT '', custom_api_url TEXT DEFAULT '', custom_model_name TEXT DEFAULT '', + deleted BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE @@ -167,6 +168,9 @@ INSERT INTO system_config (key, value) VALUES ('jwt_secret', '') ON CONFLICT (key) DO NOTHING; +-- 数据库迁移:添加 deleted 字段到现有 ai_models 表 +ALTER TABLE ai_models ADD COLUMN IF NOT EXISTS deleted BOOLEAN DEFAULT FALSE; + -- 创建索引 CREATE INDEX IF NOT EXISTS idx_ai_models_user_id ON ai_models(user_id); CREATE INDEX IF NOT EXISTS idx_exchanges_user_id ON exchanges(user_id); diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 8b85d6c8..198821bd 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react' +import React, { useState, useEffect } from 'react' import useSWR from 'swr' import { api } from '../lib/api' import type { @@ -58,6 +58,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const [showModelModal, setShowModelModal] = useState(false) const [showExchangeModal, setShowExchangeModal] = useState(false) const [showSignalSourceModal, setShowSignalSourceModal] = useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [deleteTarget, setDeleteTarget] = useState<{type: 'model' | 'exchange', id: string} | null>(null) const [editingModel, setEditingModel] = useState(null) const [editingExchange, setEditingExchange] = useState(null) const [editingTrader, setEditingTrader] = useState(null) @@ -135,20 +137,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const configuredModels = allModels || [] const configuredExchanges = allExchanges || [] - const selectableModels = useMemo(() => { - return (supportedModels || []).map((model) => { - const configured = allModels?.find((m) => m.id === model.id) - return configured ? { ...model, ...configured } : model - }) - }, [supportedModels, allModels]) - - const selectableExchanges = useMemo(() => { - return (supportedExchanges || []).map((exchange) => { - const configured = allExchanges?.find((e) => e.id === exchange.id) - return configured ? { ...exchange, ...configured } : exchange - }) - }, [supportedExchanges, allExchanges]) - // 只在创建交易员时使用已启用且配置完整的 const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || [] const enabledExchanges = @@ -313,8 +301,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } const handleDeleteModelConfig = async (modelId: string) => { - if (!confirm(t('confirmDeleteModel', language))) return - try { const updatedModels = allModels?.map((m) => @@ -344,15 +330,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } await api.updateModelConfigs(request) - setAllModels(updatedModels) + + // 重新获取用户配置以确保数据同步 + const refreshedModels = await api.getModelConfigs() + setAllModels(refreshedModels) + setShowModelModal(false) setEditingModel(null) + setShowDeleteConfirm(false) + setDeleteTarget(null) } catch (error) { console.error('Failed to delete model config:', error) alert(t('deleteConfigFailed', language)) } } + const handleConfirmDelete = () => { + if (!deleteTarget) return + + if (deleteTarget.type === 'model') { + handleDeleteModelConfig(deleteTarget.id) + } else if (deleteTarget.type === 'exchange') { + handleDeleteExchangeConfig(deleteTarget.id) + } + } + const handleSaveModelConfig = async ( modelId: string, apiKey: string, @@ -427,8 +429,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } const handleDeleteExchangeConfig = async (exchangeId: string) => { - if (!confirm(t('confirmDeleteExchange', language))) return - try { const request = { exchanges: { @@ -451,6 +451,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { setAllExchanges(refreshed) setShowExchangeModal(false) setEditingExchange(null) + setShowDeleteConfirm(false) + setDeleteTarget(null) } catch (error) { console.error('Failed to delete exchange config:', error) alert(t('deleteExchangeConfigFailed', language)) @@ -1043,15 +1045,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {/* Model Configuration Modal */} {showModelModal && ( { setShowModelModal(false) setEditingModel(null) }} + onDelete={(modelId) => { + setDeleteTarget({ type: 'model', id: modelId }) + setShowDeleteConfirm(true) + }} language={language} /> )} @@ -1059,15 +1064,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {/* Exchange Configuration Modal */} {showExchangeModal && ( { setShowExchangeModal(false) setEditingExchange(null) }} + onDelete={(exchangeId) => { + setDeleteTarget({ type: 'exchange', id: exchangeId }) + setShowDeleteConfirm(true) + }} language={language} /> )} @@ -1082,6 +1090,27 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { language={language} /> )} + + {/* Delete Confirmation Modal */} + {showDeleteConfirm && deleteTarget && ( + { + setShowDeleteConfirm(false) + setDeleteTarget(null) + }} + language={language} + /> + )} ) } @@ -1255,17 +1284,80 @@ function SignalSourceModal({ ) } +// Delete Confirmation Modal Component +function DeleteConfirmModal({ + isOpen, + title, + message, + onConfirm, + onCancel, + language, +}: { + isOpen: boolean + title: string + message: string + onConfirm: () => void + onCancel: () => void + language: Language +}) { + if (!isOpen) return null + + return ( +
+
+
+
+ +
+

+ {title} +

+
+ +

+ {message} +

+ +
+ + +
+
+
+ ) +} + // Model Configuration Modal Component function ModelConfigModal({ - allModels, + supportedModels, configuredModels, editingModelId, onSave, - onDelete, onClose, + onDelete, language, }: { - allModels: AIModel[] + supportedModels: AIModel[] configuredModels: AIModel[] editingModelId: string | null onSave: ( @@ -1274,8 +1366,8 @@ function ModelConfigModal({ baseUrl?: string, modelName?: string ) => void - onDelete: (modelId: string) => void onClose: () => void + onDelete: (modelId: string) => void language: Language }) { const [selectedModelId, setSelectedModelId] = useState(editingModelId || '') @@ -1283,10 +1375,10 @@ function ModelConfigModal({ const [baseUrl, setBaseUrl] = useState('') const [modelName, setModelName] = useState('') - // 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找 + // 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从支持的模型中查找 const selectedModel = editingModelId - ? configuredModels?.find((m) => m.id === selectedModelId) - : allModels?.find((m) => m.id === selectedModelId) + ? configuredModels?.find((m) => m.id === selectedModelId) // 编辑:从已配置中获取完整信息 + : supportedModels?.find((m) => m.id === selectedModelId) // 新建:从支持列表获取基本信息 // 如果是编辑现有模型,初始化API Key、Base URL和Model Name useEffect(() => { @@ -1309,8 +1401,8 @@ function ModelConfigModal({ ) } - // 可选择的模型列表(所有支持的模型) - const availableModels = allModels || [] + // 可选择的模型列表:直接使用系统支持的模型 + const availableModels = supportedModels || [] return (
@@ -1327,14 +1419,10 @@ function ModelConfigModal({ {editingModelId && ( @@ -1528,15 +1616,15 @@ function ModelConfigModal({ // Exchange Configuration Modal Component function ExchangeConfigModal({ - allExchanges, + supportedExchanges, configuredExchanges, editingExchangeId, onSave, - onDelete, onClose, + onDelete, language, }: { - allExchanges: Exchange[] + supportedExchanges: Exchange[] configuredExchanges: Exchange[] editingExchangeId: string | null onSave: ( @@ -1549,8 +1637,8 @@ function ExchangeConfigModal({ asterSigner?: string, asterPrivateKey?: string ) => Promise - onDelete: (exchangeId: string) => void onClose: () => void + onDelete: (exchangeId: string) => void language: Language }) { const [selectedExchangeId, setSelectedExchangeId] = useState( @@ -1581,10 +1669,10 @@ function ExchangeConfigModal({ // 获取当前选择的交易所信息 // 编辑模式:从 configuredExchanges 查找(包含用户配置的 apiKey、secretKey 等) - // 新增模式:从 allExchanges 查找(系统支持的交易所列表) + // 新增模式:从 supportedExchanges 查找(系统支持的交易所列表) const selectedExchange = editingExchangeId ? configuredExchanges?.find(e => e.id === selectedExchangeId) - : allExchanges?.find(e => e.id === selectedExchangeId); + : supportedExchanges?.find(e => e.id === selectedExchangeId); // 如果是编辑现有交易所,初始化表单数据 useEffect(() => { @@ -1622,6 +1710,9 @@ function ExchangeConfigModal({ } }, [selectedExchangeId]) + // 可选择的交易所列表:直接使用系统支持的交易所 + const availableExchanges = supportedExchanges || [] + const handleCopyIP = (ip: string) => { navigator.clipboard.writeText(ip).then(() => { setCopiedIP(true) @@ -1693,17 +1784,13 @@ function ExchangeConfigModal({ {editingExchangeId && ( @@ -1732,7 +1819,7 @@ function ExchangeConfigModal({ required > - {(allExchanges || []).map((exchange) => ( + {availableExchanges.map((exchange) => (