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:
tinkle-community
2025-12-08 12:58:13 +08:00
parent 9c1a322901
commit 7a6e6f2d92
4 changed files with 83 additions and 20 deletions

View File

@@ -204,16 +204,25 @@ func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) {
}
// 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 {
// Try exact ID match first
var existingID string
err := s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1`, userID, id).Scan(&existingID)
if err == nil {
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)
// If apiKey is empty, preserve the existing API key
if apiKey == "" {
_, err = s.db.Exec(`
UPDATE ai_models SET enabled = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
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
}
@@ -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)
if err == nil {
logger.Warnf("⚠️ Using legacy provider matching to update model: %s -> %s", provider, existingID)
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)
// If apiKey is empty, preserve the existing API key
if apiKey == "" {
_, err = s.db.Exec(`
UPDATE ai_models SET enabled = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
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
}

View File

@@ -191,7 +191,8 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
CoinPoolLimit: 30,
CoinPoolAPIURL: "http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c",
UseOITop: false,
OITopLimit: 0,
OITopLimit: 20,
OITopAPIURL: "http://nofxaios.com:30006/api/oi/top-ranking?limit=20&duration=1h&auth=cm_568c67eae410d912c54c",
},
Indicators: IndicatorConfig{
Klines: KlineConfig{

View File

@@ -2,6 +2,10 @@ import { useState } from 'react'
import { Plus, X, Database, TrendingUp, List, Link, AlertCircle } from 'lucide-react'
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 {
config: CoinSourceConfig
onChange: (config: CoinSourceConfig) => void
@@ -49,6 +53,7 @@ export function CoinSourceEditor({
},
apiUrlRequired: { zh: '需要填写 API URL 才能获取数据', en: 'API URL required to fetch data' },
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
fillDefault: { zh: '填入默认', en: 'Fill Default' },
}
return translations[key]?.[language] || key
}
@@ -228,9 +233,21 @@ export function CoinSourceEditor({
{config.use_coin_pool && (
<div>
<label className="block text-sm mb-2" style={{ color: '#848E9C' }}>
{t('coinPoolApiUrl')}
</label>
<div className="flex items-center justify-between mb-2">
<label className="text-sm" style={{ color: '#848E9C' }}>
{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
type="url"
value={config.coin_pool_api_url || ''}
@@ -312,9 +329,21 @@ export function CoinSourceEditor({
{config.use_oi_top && (
<div>
<label className="block text-sm mb-2" style={{ color: '#848E9C' }}>
{t('oiTopApiUrl')}
</label>
<div className="flex items-center justify-between mb-2">
<label className="text-sm" style={{ color: '#848E9C' }}>
{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
type="url"
value={config.oi_top_api_url || ''}

View File

@@ -1,6 +1,9 @@
import { Clock, Activity, Database } from 'lucide-react'
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 {
config: IndicatorConfig
onChange: (config: IndicatorConfig) => void
@@ -54,6 +57,7 @@ export function IndicatorEditor({
quantData: { zh: '量化数据', en: 'Quant Data' },
quantDataDesc: { zh: '资金流向、持仓变化、价格变化(按币种查询)', en: 'Netflow, OI delta, price change (per coin)' },
quantDataUrl: { zh: '量化数据 API', en: 'Quant Data API' },
fillDefault: { zh: '填入默认', en: 'Fill Default' },
}
return translations[key]?.[language] || key
}
@@ -293,9 +297,21 @@ export function IndicatorEditor({
{/* API URL */}
{config.enable_quant_data && (
<div>
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
{t('quantDataUrl')} <span style={{ color: '#5E6673' }}>({'{symbol}'} = )</span>
</label>
<div className="flex items-center justify-between mb-1">
<label className="text-[10px]" style={{ color: '#848E9C' }}>
{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
type="text"
value={config.quant_data_api_url || ''}