mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 15:57:01 +08:00
fix: preserve AI model API key when updating and add default URLs
Backend: - Fix AIModelStore.Update to preserve existing API key when new key is empty (prevents clearing API key when adding a new model) - Add default OI Top API URL to strategy config Frontend: - Add "Fill Default" buttons for Coin Pool, OI Top, and Quant Data URLs - Pre-configured default URLs for all data sources - Users can click to auto-fill with working defaults
This commit is contained in:
@@ -204,16 +204,25 @@ func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update updates AI model, creates if not exists
|
// Update updates AI model, creates if not exists
|
||||||
|
// IMPORTANT: If apiKey is empty string, the existing API key will be preserved (not overwritten)
|
||||||
func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
|
func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
|
||||||
// Try exact ID match first
|
// Try exact ID match first
|
||||||
var existingID string
|
var existingID string
|
||||||
err := s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1`, userID, id).Scan(&existingID)
|
err := s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1`, userID, id).Scan(&existingID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
encryptedAPIKey := s.encrypt(apiKey)
|
// If apiKey is empty, preserve the existing API key
|
||||||
_, err = s.db.Exec(`
|
if apiKey == "" {
|
||||||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
_, err = s.db.Exec(`
|
||||||
WHERE id = ? AND user_id = ?
|
UPDATE ai_models SET enabled = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||||||
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
WHERE id = ? AND user_id = ?
|
||||||
|
`, enabled, customAPIURL, customModelName, existingID, userID)
|
||||||
|
} else {
|
||||||
|
encryptedAPIKey := s.encrypt(apiKey)
|
||||||
|
_, err = s.db.Exec(`
|
||||||
|
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,11 +231,19 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
|
|||||||
err = s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1`, userID, provider).Scan(&existingID)
|
err = s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1`, userID, provider).Scan(&existingID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Warnf("⚠️ Using legacy provider matching to update model: %s -> %s", provider, existingID)
|
logger.Warnf("⚠️ Using legacy provider matching to update model: %s -> %s", provider, existingID)
|
||||||
encryptedAPIKey := s.encrypt(apiKey)
|
// If apiKey is empty, preserve the existing API key
|
||||||
_, err = s.db.Exec(`
|
if apiKey == "" {
|
||||||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
_, err = s.db.Exec(`
|
||||||
WHERE id = ? AND user_id = ?
|
UPDATE ai_models SET enabled = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||||||
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
WHERE id = ? AND user_id = ?
|
||||||
|
`, enabled, customAPIURL, customModelName, existingID, userID)
|
||||||
|
} else {
|
||||||
|
encryptedAPIKey := s.encrypt(apiKey)
|
||||||
|
_, err = s.db.Exec(`
|
||||||
|
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
|||||||
CoinPoolLimit: 30,
|
CoinPoolLimit: 30,
|
||||||
CoinPoolAPIURL: "http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c",
|
CoinPoolAPIURL: "http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c",
|
||||||
UseOITop: false,
|
UseOITop: false,
|
||||||
OITopLimit: 0,
|
OITopLimit: 20,
|
||||||
|
OITopAPIURL: "http://nofxaios.com:30006/api/oi/top-ranking?limit=20&duration=1h&auth=cm_568c67eae410d912c54c",
|
||||||
},
|
},
|
||||||
Indicators: IndicatorConfig{
|
Indicators: IndicatorConfig{
|
||||||
Klines: KlineConfig{
|
Klines: KlineConfig{
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { useState } from 'react'
|
|||||||
import { Plus, X, Database, TrendingUp, List, Link, AlertCircle } from 'lucide-react'
|
import { Plus, X, Database, TrendingUp, List, Link, AlertCircle } from 'lucide-react'
|
||||||
import type { CoinSourceConfig } from '../../types'
|
import type { CoinSourceConfig } from '../../types'
|
||||||
|
|
||||||
|
// Default API URLs for data sources
|
||||||
|
const DEFAULT_COIN_POOL_API_URL = 'http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c'
|
||||||
|
const DEFAULT_OI_TOP_API_URL = 'http://nofxaios.com:30006/api/oi/top-ranking?limit=20&duration=1h&auth=cm_568c67eae410d912c54c'
|
||||||
|
|
||||||
interface CoinSourceEditorProps {
|
interface CoinSourceEditorProps {
|
||||||
config: CoinSourceConfig
|
config: CoinSourceConfig
|
||||||
onChange: (config: CoinSourceConfig) => void
|
onChange: (config: CoinSourceConfig) => void
|
||||||
@@ -49,6 +53,7 @@ export function CoinSourceEditor({
|
|||||||
},
|
},
|
||||||
apiUrlRequired: { zh: '需要填写 API URL 才能获取数据', en: 'API URL required to fetch data' },
|
apiUrlRequired: { zh: '需要填写 API URL 才能获取数据', en: 'API URL required to fetch data' },
|
||||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
||||||
|
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||||
}
|
}
|
||||||
return translations[key]?.[language] || key
|
return translations[key]?.[language] || key
|
||||||
}
|
}
|
||||||
@@ -228,9 +233,21 @@ export function CoinSourceEditor({
|
|||||||
|
|
||||||
{config.use_coin_pool && (
|
{config.use_coin_pool && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-2" style={{ color: '#848E9C' }}>
|
<div className="flex items-center justify-between mb-2">
|
||||||
{t('coinPoolApiUrl')}
|
<label className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
</label>
|
{t('coinPoolApiUrl')}
|
||||||
|
</label>
|
||||||
|
{!disabled && !config.coin_pool_api_url && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange({ ...config, coin_pool_api_url: DEFAULT_COIN_POOL_API_URL })}
|
||||||
|
className="text-xs px-2 py-1 rounded"
|
||||||
|
style={{ background: '#F0B90B20', color: '#F0B90B' }}
|
||||||
|
>
|
||||||
|
{t('fillDefault')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={config.coin_pool_api_url || ''}
|
value={config.coin_pool_api_url || ''}
|
||||||
@@ -312,9 +329,21 @@ export function CoinSourceEditor({
|
|||||||
|
|
||||||
{config.use_oi_top && (
|
{config.use_oi_top && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-2" style={{ color: '#848E9C' }}>
|
<div className="flex items-center justify-between mb-2">
|
||||||
{t('oiTopApiUrl')}
|
<label className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
</label>
|
{t('oiTopApiUrl')}
|
||||||
|
</label>
|
||||||
|
{!disabled && !config.oi_top_api_url && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange({ ...config, oi_top_api_url: DEFAULT_OI_TOP_API_URL })}
|
||||||
|
className="text-xs px-2 py-1 rounded"
|
||||||
|
style={{ background: '#0ECB8120', color: '#0ECB81' }}
|
||||||
|
>
|
||||||
|
{t('fillDefault')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={config.oi_top_api_url || ''}
|
value={config.oi_top_api_url || ''}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Clock, Activity, Database } from 'lucide-react'
|
import { Clock, Activity, Database } from 'lucide-react'
|
||||||
import type { IndicatorConfig } from '../../types'
|
import type { IndicatorConfig } from '../../types'
|
||||||
|
|
||||||
|
// Default API URL for quant data (must contain {symbol} placeholder)
|
||||||
|
const DEFAULT_QUANT_DATA_API_URL = 'http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c'
|
||||||
|
|
||||||
interface IndicatorEditorProps {
|
interface IndicatorEditorProps {
|
||||||
config: IndicatorConfig
|
config: IndicatorConfig
|
||||||
onChange: (config: IndicatorConfig) => void
|
onChange: (config: IndicatorConfig) => void
|
||||||
@@ -54,6 +57,7 @@ export function IndicatorEditor({
|
|||||||
quantData: { zh: '量化数据', en: 'Quant Data' },
|
quantData: { zh: '量化数据', en: 'Quant Data' },
|
||||||
quantDataDesc: { zh: '资金流向、持仓变化、价格变化(按币种查询)', en: 'Netflow, OI delta, price change (per coin)' },
|
quantDataDesc: { zh: '资金流向、持仓变化、价格变化(按币种查询)', en: 'Netflow, OI delta, price change (per coin)' },
|
||||||
quantDataUrl: { zh: '量化数据 API', en: 'Quant Data API' },
|
quantDataUrl: { zh: '量化数据 API', en: 'Quant Data API' },
|
||||||
|
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||||
}
|
}
|
||||||
return translations[key]?.[language] || key
|
return translations[key]?.[language] || key
|
||||||
}
|
}
|
||||||
@@ -293,9 +297,21 @@ export function IndicatorEditor({
|
|||||||
{/* API URL */}
|
{/* API URL */}
|
||||||
{config.enable_quant_data && (
|
{config.enable_quant_data && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
|
<div className="flex items-center justify-between mb-1">
|
||||||
{t('quantDataUrl')} <span style={{ color: '#5E6673' }}>({'{symbol}'} = 币种)</span>
|
<label className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||||
</label>
|
{t('quantDataUrl')} <span style={{ color: '#5E6673' }}>({'{symbol}'} = 币种)</span>
|
||||||
|
</label>
|
||||||
|
{!disabled && !config.quant_data_api_url && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange({ ...config, quant_data_api_url: DEFAULT_QUANT_DATA_API_URL })}
|
||||||
|
className="text-[10px] px-2 py-0.5 rounded"
|
||||||
|
style={{ background: '#22c55e20', color: '#22c55e' }}
|
||||||
|
>
|
||||||
|
{t('fillDefault')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={config.quant_data_api_url || ''}
|
value={config.quant_data_api_url || ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user