mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
release: model switching fix + active strategy deletion guard (#1465)
* feat(store): prevent deletion of active strategies and update translations (#1461) Co-authored-by: Dean <afei.wuhao@gmail.com> * fix: allow model switching without re-entering wallet key Users with existing wallets could not switch AI models because the "Start Trading" button required a valid private key even when one was already configured. Now the button is enabled when hasExistingWallet is true, and handleSubmit passes an empty key so the backend preserves the existing key. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: deanokk <wuhao@vergex.trade> Co-authored-by: Dean <afei.wuhao@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -444,6 +444,9 @@ func (s *StrategyStore) Delete(userID, id string) error {
|
|||||||
if st.IsDefault {
|
if st.IsDefault {
|
||||||
return fmt.Errorf("cannot delete system default strategy")
|
return fmt.Errorf("cannot delete system default strategy")
|
||||||
}
|
}
|
||||||
|
if st.IsActive {
|
||||||
|
return fmt.Errorf("cannot delete active strategy")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any trader references this strategy
|
// Check if any trader references this strategy
|
||||||
|
|||||||
@@ -77,8 +77,11 @@ export function ModelConfigModal({
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!selectedModelId || !apiKey.trim()) return
|
if (!selectedModelId) return
|
||||||
onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined)
|
const key = apiKey.trim()
|
||||||
|
// Allow empty key when editing an existing model (backend preserves existing key)
|
||||||
|
if (!key && !editingModelId) return
|
||||||
|
onSave(selectedModelId, key, baseUrl.trim() || undefined, modelName.trim() || undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableModels = allModels || []
|
const availableModels = allModels || []
|
||||||
@@ -833,9 +836,9 @@ function Claw402ConfigForm({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isKeyValid}
|
disabled={!isKeyValid && !hasExistingWallet}
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={{ background: isKeyValid ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
style={{ background: (isKeyValid || hasExistingWallet) ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
||||||
>
|
>
|
||||||
{'🚀 ' + t('modelConfig.startTrading', language)}
|
{'🚀 ' + t('modelConfig.startTrading', language)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ export const translations = {
|
|||||||
newStrategyName: 'New Strategy',
|
newStrategyName: 'New Strategy',
|
||||||
strategyCopy: 'Strategy Copy',
|
strategyCopy: 'Strategy Copy',
|
||||||
strategyDeleted: 'Strategy deleted',
|
strategyDeleted: 'Strategy deleted',
|
||||||
|
cannotDeleteActiveStrategy: 'Active strategy cannot be deleted',
|
||||||
confirmDeleteStrategy: 'Delete this strategy?',
|
confirmDeleteStrategy: 'Delete this strategy?',
|
||||||
confirmDelete: 'Confirm Delete',
|
confirmDelete: 'Confirm Delete',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
@@ -2373,6 +2374,7 @@ export const translations = {
|
|||||||
newStrategyName: '新策略',
|
newStrategyName: '新策略',
|
||||||
strategyCopy: '策略副本',
|
strategyCopy: '策略副本',
|
||||||
strategyDeleted: '策略已删除',
|
strategyDeleted: '策略已删除',
|
||||||
|
cannotDeleteActiveStrategy: '激活中的策略不能删除',
|
||||||
confirmDeleteStrategy: '确定删除此策略?',
|
confirmDeleteStrategy: '确定删除此策略?',
|
||||||
confirmDelete: '确认删除',
|
confirmDelete: '确认删除',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
@@ -3482,6 +3484,7 @@ export const translations = {
|
|||||||
newStrategyName: 'Strategi Baru',
|
newStrategyName: 'Strategi Baru',
|
||||||
strategyCopy: 'Salinan Strategi',
|
strategyCopy: 'Salinan Strategi',
|
||||||
strategyDeleted: 'Strategi dihapus',
|
strategyDeleted: 'Strategi dihapus',
|
||||||
|
cannotDeleteActiveStrategy: 'Strategi aktif tidak bisa dihapus',
|
||||||
confirmDeleteStrategy: 'Hapus strategi ini?',
|
confirmDeleteStrategy: 'Hapus strategi ini?',
|
||||||
confirmDelete: 'Konfirmasi Hapus',
|
confirmDelete: 'Konfirmasi Hapus',
|
||||||
delete: 'Hapus',
|
delete: 'Hapus',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
Globe,
|
Globe,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { Strategy, StrategyConfig, AIModel } from '../types'
|
import type { Strategy, StrategyConfig, AIModel, GridStrategyConfig } from '../types'
|
||||||
import { confirmToast, notify } from '../lib/notify'
|
import { confirmToast, notify } from '../lib/notify'
|
||||||
import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
|
import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
|
||||||
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
||||||
@@ -94,6 +94,7 @@ export function StrategyStudioPage() {
|
|||||||
duration_ms?: number
|
duration_ms?: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [isRunningAiTest, setIsRunningAiTest] = useState(false)
|
const [isRunningAiTest, setIsRunningAiTest] = useState(false)
|
||||||
|
const gridConfigCacheRef = useRef<Record<string, GridStrategyConfig>>({})
|
||||||
|
|
||||||
const toggleSection = (section: keyof typeof expandedSections) => {
|
const toggleSection = (section: keyof typeof expandedSections) => {
|
||||||
setExpandedSections((prev) => ({
|
setExpandedSections((prev) => ({
|
||||||
@@ -156,6 +157,12 @@ export function StrategyStudioPage() {
|
|||||||
fetchAiModels()
|
fetchAiModels()
|
||||||
}, [fetchStrategies, fetchAiModels])
|
}, [fetchStrategies, fetchAiModels])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedStrategy?.id || !editingConfig?.grid_config) return
|
||||||
|
|
||||||
|
gridConfigCacheRef.current[selectedStrategy.id] = { ...editingConfig.grid_config }
|
||||||
|
}, [selectedStrategy?.id, editingConfig?.grid_config])
|
||||||
|
|
||||||
// Track previous language to detect actual changes
|
// Track previous language to detect actual changes
|
||||||
const prevLanguageRef = useRef(language)
|
const prevLanguageRef = useRef(language)
|
||||||
|
|
||||||
@@ -248,6 +255,12 @@ export function StrategyStudioPage() {
|
|||||||
// Delete strategy
|
// Delete strategy
|
||||||
const handleDeleteStrategy = async (id: string) => {
|
const handleDeleteStrategy = async (id: string) => {
|
||||||
if (!token) return
|
if (!token) return
|
||||||
|
const strategy = strategies.find((item) => item.id === id)
|
||||||
|
|
||||||
|
if (strategy?.is_active) {
|
||||||
|
notify.error(tr('cannotDeleteActiveStrategy'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if strategy is in use by any trader before showing dialog
|
// Check if strategy is in use by any trader before showing dialog
|
||||||
try {
|
try {
|
||||||
@@ -443,14 +456,51 @@ export function StrategyStudioPage() {
|
|||||||
section: K,
|
section: K,
|
||||||
value: StrategyConfig[K]
|
value: StrategyConfig[K]
|
||||||
) => {
|
) => {
|
||||||
if (!editingConfig) return
|
setEditingConfig((prev) => {
|
||||||
setEditingConfig({
|
if (!prev) return prev
|
||||||
...editingConfig,
|
return {
|
||||||
[section]: value,
|
...prev,
|
||||||
|
[section]: value,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setHasChanges(true)
|
setHasChanges(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStrategyTypeChange = (strategyType: NonNullable<StrategyConfig['strategy_type']>) => {
|
||||||
|
if (selectedStrategy?.is_default) return
|
||||||
|
|
||||||
|
const cachedGridConfig = selectedStrategy?.id
|
||||||
|
? gridConfigCacheRef.current[selectedStrategy.id]
|
||||||
|
: null
|
||||||
|
|
||||||
|
setEditingConfig((prev) => {
|
||||||
|
if (!prev) return prev
|
||||||
|
|
||||||
|
if (strategyType === 'ai_trading') {
|
||||||
|
if (selectedStrategy?.id && prev.grid_config) {
|
||||||
|
gridConfigCacheRef.current[selectedStrategy.id] = { ...prev.grid_config }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
strategy_type: 'ai_trading',
|
||||||
|
// Use null so the field is preserved in JSON and backend merge can actually clear it.
|
||||||
|
grid_config: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
strategy_type: 'grid_trading',
|
||||||
|
grid_config: cachedGridConfig ?? prev.grid_config ?? { ...defaultGridConfig },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setPromptPreview(null)
|
||||||
|
setAiTestResult(null)
|
||||||
|
setHasChanges(true)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch prompt preview
|
// Fetch prompt preview
|
||||||
const fetchPromptPreview = async () => {
|
const fetchPromptPreview = async () => {
|
||||||
if (!token || !editingConfig) return
|
if (!token || !editingConfig) return
|
||||||
@@ -666,7 +716,7 @@ export function StrategyStudioPage() {
|
|||||||
<Sparkles className="w-5 h-5 text-black" />
|
<Sparkles className="w-5 h-5 text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
|
<h1 className="text-lg font-bold text-nofx-text">{tr('title')}</h1>
|
||||||
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
|
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -743,8 +793,9 @@ export function StrategyStudioPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||||
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
|
disabled={strategy.is_active}
|
||||||
title={tr('deleteTooltip')}
|
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
||||||
|
title={strategy.is_active ? tr('cannotDeleteActiveStrategy') : tr('deleteTooltip')}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -848,13 +899,7 @@ export function StrategyStudioPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => handleStrategyTypeChange('ai_trading')}
|
||||||
if (!selectedStrategy?.is_default) {
|
|
||||||
updateConfig('strategy_type', 'ai_trading')
|
|
||||||
// Clear grid config when switching to AI trading
|
|
||||||
updateConfig('grid_config', undefined)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={selectedStrategy?.is_default}
|
disabled={selectedStrategy?.is_default}
|
||||||
className={`p-3 rounded-lg border transition-all ${
|
className={`p-3 rounded-lg border transition-all ${
|
||||||
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
|
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
|
||||||
@@ -869,15 +914,7 @@ export function StrategyStudioPage() {
|
|||||||
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
|
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => handleStrategyTypeChange('grid_trading')}
|
||||||
if (!selectedStrategy?.is_default) {
|
|
||||||
updateConfig('strategy_type', 'grid_trading')
|
|
||||||
// Initialize grid config if not exists
|
|
||||||
if (!editingConfig.grid_config) {
|
|
||||||
updateConfig('grid_config', defaultGridConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={selectedStrategy?.is_default}
|
disabled={selectedStrategy?.is_default}
|
||||||
className={`p-3 rounded-lg border transition-all ${
|
className={`p-3 rounded-lg border transition-all ${
|
||||||
editingConfig.strategy_type === 'grid_trading'
|
editingConfig.strategy_type === 'grid_trading'
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export interface StrategyConfig {
|
|||||||
risk_control: RiskControlConfig;
|
risk_control: RiskControlConfig;
|
||||||
prompt_sections?: PromptSectionsConfig;
|
prompt_sections?: PromptSectionsConfig;
|
||||||
// Grid trading configuration (only used when strategy_type is 'grid_trading')
|
// Grid trading configuration (only used when strategy_type is 'grid_trading')
|
||||||
grid_config?: GridStrategyConfig;
|
grid_config?: GridStrategyConfig | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid trading specific configuration
|
// Grid trading specific configuration
|
||||||
|
|||||||
Reference in New Issue
Block a user