This commit is contained in:
lky-spec
2026-04-25 20:24:46 +08:00
parent c244e4cdf1
commit 9ee931ee30
28 changed files with 1319 additions and 255 deletions

View File

@@ -18,6 +18,16 @@ export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
return null
}
const sanitizedSteps = steps.filter((step) => {
const label = step.label.trim().toLowerCase()
const detail = (step.detail || '').trim().toLowerCase()
return !(label.startsWith('tool:') || detail === 'central_brain')
})
if (sanitizedSteps.length === 0) {
return null
}
return (
<div
style={{
@@ -41,7 +51,7 @@ export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
Live Run
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{steps.map((step) => {
{sanitizedSteps.map((step) => {
const style = statusStyles[step.status]
return (
<div

View File

@@ -10,7 +10,14 @@ interface ChatMessagesProps {
function hasMeaningfulExecutionSteps(steps?: AgentStep[]) {
if (!steps || steps.length === 0) return false
return steps.some((step) => step.status !== 'planning')
return steps.some((step) => {
const label = step.label.trim().toLowerCase()
const detail = (step.detail || '').trim().toLowerCase()
if (label.startsWith('tool:') || detail === 'central_brain') {
return false
}
return step.status !== 'planning'
})
}
export const ChatMessages = forwardRef<HTMLDivElement, ChatMessagesProps>(

View File

@@ -1,8 +1,7 @@
import { useState, useEffect } from 'react'
import type { AIModel, Exchange, CreateTraderRequest, ExchangeAccountStateResponse, Strategy } from '../../types'
import type { AIModel, Exchange, CreateTraderRequest, Strategy, TraderConfigData } from '../../types'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
import { toast } from 'sonner'
import { Pencil, Plus, X as IconX, Sparkles, ExternalLink, UserPlus } from 'lucide-react'
import { httpClient } from '../../lib/httpClient'
import { NofxSelect } from '../ui/select'
@@ -22,9 +21,6 @@ const EXCHANGE_REGISTRATION_LINKS: Record<string, { url: string; hasReferral?: b
aster: { url: 'https://www.asterdex.com/en/referral/fdfc0e', hasReferral: true },
lighter: { url: 'https://app.lighter.xyz/?referral=68151432', hasReferral: true },
}
import type { TraderConfigData } from '../../types'
// 表单内部状态类型
interface FormState {
trader_id?: string
@@ -35,7 +31,6 @@ interface FormState {
is_cross_margin: boolean
show_in_competition: boolean
scan_interval_minutes: number
initial_balance?: number
}
interface TraderConfigModalProps {
@@ -69,8 +64,6 @@ export function TraderConfigModal({
})
const [isSaving, setIsSaving] = useState(false)
const [strategies, setStrategies] = useState<Strategy[]>([])
const [isFetchingBalance, setIsFetchingBalance] = useState(false)
const [balanceFetchError, setBalanceFetchError] = useState<string>('')
// 获取用户的策略列表
useEffect(() => {
@@ -125,64 +118,7 @@ export function TraderConfigModal({
}
const handleExchangeChange = (exchangeId: string) => {
setBalanceFetchError('')
setFormData((prev) => {
if (prev.exchange_id === exchangeId) {
return prev
}
const next: FormState = { ...prev, exchange_id: exchangeId }
// Exchange balance belongs to the selected exchange, not the trader record.
// Clear the old baseline so we don't carry Exchange B's balance into Exchange A.
if (isEditMode) {
next.initial_balance = undefined
}
return next
})
}
const handleFetchCurrentBalance = async () => {
if (!isEditMode) {
setBalanceFetchError(t('fetchBalanceEditModeOnly', language))
return
}
if (!formData.exchange_id) {
setBalanceFetchError(t('balanceFetchFailed', language))
return
}
setIsFetchingBalance(true)
setBalanceFetchError('')
try {
const result = await httpClient.get<ExchangeAccountStateResponse>('/api/exchanges/account-state')
const selectedState = result.data?.states?.[formData.exchange_id]
if (result.success && selectedState?.status === 'ok') {
const currentBalance =
selectedState.total_equity ??
selectedState.available_balance ??
0
setFormData((prev) => ({ ...prev, initial_balance: currentBalance }))
toast.success(t('balanceFetched', language))
} else {
setBalanceFetchError(
selectedState?.error_message || result.message || t('balanceFetchFailed', language)
)
}
} catch (error) {
console.error(t('balanceFetchFailed', language) + ':', error)
setBalanceFetchError(
error instanceof Error && error.message
? error.message
: t('balanceFetchNetworkError', language)
)
} finally {
setIsFetchingBalance(false)
}
setFormData((prev) => ({ ...prev, exchange_id: exchangeId }))
}
const handleSave = async () => {
@@ -200,11 +136,6 @@ export function TraderConfigModal({
scan_interval_minutes: formData.scan_interval_minutes,
}
// 只在编辑模式时包含initial_balance
if (isEditMode && formData.initial_balance !== undefined) {
saveData.initial_balance = formData.initial_balance
}
await onSave(saveData)
} catch (error) {
console.error(t('saveFailed', language) + ':', error)
@@ -495,68 +426,26 @@ export function TraderConfigModal({
</p>
</div>
{/* Initial Balance (Edit mode only) */}
{isEditMode && (
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-[#EAECEF]">
{t('initialBalanceLabel', language)}
</label>
<button
type="button"
onClick={handleFetchCurrentBalance}
disabled={isFetchingBalance}
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors disabled:bg-[#848E9C] disabled:cursor-not-allowed"
>
{isFetchingBalance ? t('fetching', language) : t('fetchCurrentBalance', language)}
</button>
</div>
<input
type="number"
value={formData.initial_balance || 0}
onChange={(e) =>
handleInputChange(
'initial_balance',
Number(e.target.value)
)
}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="100"
step="0.01"
/>
<p className="text-xs text-[#848E9C] mt-1">
{t('balanceUpdateHint', language)}
</p>
{balanceFetchError && (
<p className="text-xs text-red-500 mt-1">
{balanceFetchError}
</p>
)}
</div>
)}
<div className="p-3 bg-[#1E2329] border border-[#2B3139] rounded flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 text-[#F0B90B]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>
<span className="text-sm text-[#848E9C]">
{t('autoFetchBalanceInfo', language)}
</span>
</div>
{/* Create mode info */}
{!isEditMode && (
<div className="p-3 bg-[#1E2329] border border-[#2B3139] rounded flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 text-[#F0B90B]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>
<span className="text-sm text-[#848E9C]">
{t('autoFetchBalanceInfo', language)}
</span>
</div>
)}
</div>
</div>

View File

@@ -261,25 +261,6 @@ async function runAgentStream(params: {
),
storageUserId
)
} else if (eventType === 'tool') {
patchMessagesInStore(
(prev) =>
prev.map((m) =>
m.id === botId
? {
...m,
steps: appendStep(m.steps, {
id: `tool-${Date.now()}`,
label: `Tool: ${data}`,
status: 'running',
detail: data,
}),
time: now(),
}
: m
),
storageUserId
)
} else if (eventType === 'done') {
patchMessagesInStore(
(prev) =>

View File

@@ -96,7 +96,6 @@ export interface CreateTraderRequest {
ai_model_id: string
exchange_id: string
strategy_id?: string // 策略ID新版使用保存的策略配置
initial_balance?: number // 可选:创建时由后端自动获取,编辑时可手动更新
scan_interval_minutes?: number
is_cross_margin?: boolean
show_in_competition?: boolean // 是否在竞技场显示