Add restore-mode task launch flow

This commit is contained in:
2026-03-24 15:27:35 +08:00
parent 6413edf8c9
commit 8d6c3c5647
12 changed files with 572 additions and 52 deletions

View File

@@ -96,7 +96,40 @@ export default function LiveTradingApp() {
setWorkspaceDraftContent,
} = useAgentStore();
const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage } = useFeedProcessor();
const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage, clearFeed } = useFeedProcessor();
const resetRuntimeViewState = useCallback(() => {
clearFeed();
useMarketStore.getState().setPriceHistoryByTicker({});
useMarketStore.getState().setOhlcHistoryByTicker({});
useMarketStore.getState().setHistorySourceByTicker({});
useMarketStore.getState().setExplainEventsByTicker({});
useMarketStore.getState().setNewsByTicker({});
useMarketStore.getState().setInsiderTradesByTicker({});
useMarketStore.getState().setTechnicalIndicatorsByTicker({});
usePortfolioStore.getState().setHoldings([]);
usePortfolioStore.getState().setTrades([]);
usePortfolioStore.getState().setStats(null);
usePortfolioStore.getState().setLeaderboard([]);
usePortfolioStore.getState().setPortfolioData({
netValue: 10000,
pnl: 0,
equity: [],
baseline: [],
baseline_vw: [],
momentum: [],
strategies: [],
equity_return: 0,
baseline_return: 0,
baseline_vw_return: 0,
momentum_return: 0,
});
useRuntimeStore.getState().setLastDayHistory([]);
useUIStore.getState().setBubbles({});
}, [clearFeed]);
const {
clientRef,
setRequestStockHistory,
@@ -112,6 +145,7 @@ export default function LiveTradingApp() {
clientRef,
currentTickers: tickers,
addSystemMessage,
onRuntimeStarted: resetRuntimeViewState,
});
const stockRequests = useStockDataRequests(clientRef, {
@@ -367,6 +401,9 @@ export default function LiveTradingApp() {
isWatchlistSaving={runtimeControls.isWatchlistSaving}
runtimeConfigFeedback={runtimeControls.runtimeConfigFeedback}
watchlistFeedback={runtimeControls.watchlistFeedback}
launchModeDraft={runtimeControls.launchModeDraft}
restoreRunIdDraft={runtimeControls.restoreRunIdDraft}
runtimeHistoryRuns={runtimeControls.runtimeHistoryRuns}
scheduleModeDraft={runtimeControls.scheduleModeDraft}
intervalMinutesDraft={runtimeControls.intervalMinutesDraft}
triggerTimeDraft={runtimeControls.triggerTimeDraft}
@@ -382,6 +419,8 @@ export default function LiveTradingApp() {
watchlistDraftSymbols={runtimeControls.watchlistDraftSymbols}
watchlistInputValue={runtimeControls.watchlistInputValue}
watchlistSuggestions={runtimeControls.watchlistSuggestions}
onLaunchModeChange={runtimeControls.setLaunchModeDraft}
onRestoreRunIdChange={runtimeControls.setRestoreRunIdDraft}
onScheduleModeChange={runtimeControls.setScheduleModeDraft}
onIntervalMinutesChange={runtimeControls.setIntervalMinutesDraft}
onTriggerTimeChange={runtimeControls.setTriggerTimeDraft}

View File

@@ -58,6 +58,9 @@ export default function AppShell({
isWatchlistSaving,
runtimeConfigFeedback,
watchlistFeedback,
launchModeDraft,
restoreRunIdDraft,
runtimeHistoryRuns,
scheduleModeDraft,
intervalMinutesDraft,
triggerTimeDraft,
@@ -73,6 +76,8 @@ export default function AppShell({
watchlistDraftSymbols,
watchlistInputValue,
watchlistSuggestions,
onLaunchModeChange,
onRestoreRunIdChange,
onScheduleModeChange,
onIntervalMinutesChange,
onTriggerTimeChange,
@@ -300,6 +305,9 @@ export default function AppShell({
isConnected={isConnected}
isSaving={isRuntimeConfigSaving || isWatchlistSaving}
feedback={runtimeConfigFeedback || watchlistFeedback}
launchMode={launchModeDraft}
restoreRunId={restoreRunIdDraft}
runtimeHistoryRuns={runtimeHistoryRuns}
scheduleMode={scheduleModeDraft}
intervalMinutes={intervalMinutesDraft}
triggerTime={triggerTimeDraft}
@@ -317,6 +325,8 @@ export default function AppShell({
watchlistSuggestions={watchlistSuggestions}
onToggle={onRuntimeSettingsToggle}
onClose={() => setIsRuntimeSettingsOpen(false)}
onLaunchModeChange={onLaunchModeChange}
onRestoreRunIdChange={onRestoreRunIdChange}
onScheduleModeChange={onScheduleModeChange}
onIntervalMinutesChange={onIntervalMinutesChange}
onTriggerTimeChange={onTriggerTimeChange}

View File

@@ -1,12 +1,24 @@
import React from 'react';
import { createPortal } from 'react-dom';
const formatHistorySummary = (run) => {
const updatedAt = run?.updated_at ? String(run.updated_at).replace("T", " ").slice(0, 16) : "未知时间";
const mode = run?.bootstrap?.mode ? String(run.bootstrap.mode).toUpperCase() : "LIVE";
const tickers = Array.isArray(run?.bootstrap?.tickers) ? run.bootstrap.tickers.length : 0;
const assetValue = Number(run?.total_asset_value ?? 0).toFixed(2);
const trades = Number(run?.total_trades ?? 0);
return `${run.run_id} · ${updatedAt} · ${mode} · ${tickers}标的 · ${trades}笔交易 · $${assetValue}`;
};
export default function RuntimeSettingsPanel({
showTrigger = true,
isOpen,
isConnected,
isSaving,
feedback,
launchMode,
restoreRunId,
runtimeHistoryRuns,
scheduleMode,
intervalMinutes,
triggerTime,
@@ -25,6 +37,8 @@ export default function RuntimeSettingsPanel({
onToggle,
onClose,
onScheduleModeChange,
onLaunchModeChange,
onRestoreRunIdChange,
onIntervalMinutesChange,
onTriggerTimeChange,
onMaxCommCyclesChange,
@@ -142,6 +156,75 @@ export default function RuntimeSettingsPanel({
display: 'grid',
gap: 12
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>启动形式</div>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>任务模式</span>
<select
value={launchMode}
onChange={(e) => onLaunchModeChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px'
}}
>
<option value="fresh">重新启动</option>
<option value="restore">从历史任务恢复</option>
</select>
</label>
{launchMode === 'restore' && (
<>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>历史任务</span>
<select
value={restoreRunId}
onChange={(e) => onRestoreRunIdChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px'
}}
>
<option value="">请选择历史任务</option>
{runtimeHistoryRuns.map((run) => (
<option key={run.run_id} value={run.run_id}>
{formatHistorySummary(run)}
</option>
))}
</select>
</label>
<div style={{
fontSize: '11px',
color: '#6B7280',
lineHeight: 1.6,
padding: '10px 12px',
borderRadius: 8,
background: '#FFFFFF',
border: '1px dashed #D0D7DE'
}}>
恢复启动会从所选历史任务复制运行状态组合交易记录和 Agent 工作区资产并以新的任务 ID 继续运行
</div>
</>
)}
</div>
{launchMode === 'fresh' && (
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 12
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>自选股</div>
<div style={{
@@ -272,16 +355,18 @@ export default function RuntimeSettingsPanel({
恢复默认
</button>
</div>
</div>
</div>
)}
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 12
}}>
{launchMode === 'fresh' && (
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 12
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>调度参数</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<label style={{ display: 'grid', gap: 4 }}>
@@ -510,7 +595,8 @@ export default function RuntimeSettingsPanel({
/>
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用模拟数据 (Mock)</span>
</label>
</div>
</div>
)}
<div style={{
border: '1px solid #E5EAF1',

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { INITIAL_TICKERS } from "../config/constants";
import { startRuntime } from "../services/runtimeApi";
import { fetchRuntimeHistory, startRuntime } from "../services/runtimeApi";
import {
buildRuntimeSummaryLabel,
normalizeTickerSymbols,
@@ -19,7 +19,7 @@ const DEFAULT_MARGIN_REQUIREMENT = "0";
const DEFAULT_MODE = "live";
const DEFAULT_POLL_INTERVAL = "10";
export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage }) {
export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage, onRuntimeStarted }) {
const {
runtimeConfig,
setRuntimeConfig,
@@ -35,6 +35,12 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setWatchlistFeedback,
isWatchlistSaving,
setIsWatchlistSaving,
launchModeDraft,
setLaunchModeDraft,
restoreRunIdDraft,
setRestoreRunIdDraft,
runtimeHistoryRuns,
setRuntimeHistoryRuns,
scheduleModeDraft,
setScheduleModeDraft,
intervalMinutesDraft,
@@ -152,6 +158,32 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setTriggerTimeDraft
]);
useEffect(() => {
if (!isRuntimeSettingsOpen) {
return;
}
let cancelled = false;
void fetchRuntimeHistory(20)
.then((payload) => {
if (cancelled) return;
const runs = Array.isArray(payload?.runs) ? payload.runs : [];
setRuntimeHistoryRuns(runs);
if (!restoreRunIdDraft && runs.length > 0) {
setRestoreRunIdDraft(runs[0].run_id);
}
})
.catch(() => {
if (!cancelled) {
setRuntimeHistoryRuns([]);
}
});
return () => {
cancelled = true;
};
}, [isRuntimeSettingsOpen, restoreRunIdDraft, setRestoreRunIdDraft, setRuntimeHistoryRuns]);
const commitWatchlistInput = useCallback((value) => {
const parsed = parseWatchlistInput(value);
if (parsed.length === 0) {
@@ -340,6 +372,10 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setRuntimeConfigFeedback({ type: "error", text: "保证金要求不能为负数" });
return;
}
if (launchModeDraft === "restore" && !restoreRunIdDraft) {
setRuntimeConfigFeedback({ type: "error", text: "请选择一个历史任务用于恢复启动" });
return;
}
setIsRuntimeConfigSaving(true);
setIsWatchlistSaving(true);
@@ -350,6 +386,8 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
try {
const result = await startRuntime({
launch_mode: launchModeDraft,
restore_run_id: launchModeDraft === "restore" ? restoreRunIdDraft : null,
tickers: nextTickers,
schedule_mode: scheduleModeDraft,
interval_minutes: interval,
@@ -373,6 +411,7 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
text: `任务已启动: ${result.run_id}`
});
addSystemMessage(`新任务已启动: ${result.run_id}`);
onRuntimeStarted?.(result);
} catch (error) {
setIsRuntimeConfigSaving(false);
setIsWatchlistSaving(false);
@@ -389,10 +428,12 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
endDateDraft,
initialCashDraft,
intervalMinutesDraft,
launchModeDraft,
marginRequirementDraft,
maxCommCyclesDraft,
modeDraft,
pollIntervalDraft,
restoreRunIdDraft,
scheduleModeDraft,
setIsRuntimeConfigSaving,
setIsRuntimeSettingsOpen,
@@ -402,6 +443,7 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setWatchlistFeedback,
setWatchlistInputValue,
startDateDraft,
onRuntimeStarted,
triggerTimeDraft,
watchlistDraftSymbols,
watchlistInputValue
@@ -415,6 +457,8 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setInitialCashDraft(DEFAULT_INITIAL_CASH);
setMarginRequirementDraft(DEFAULT_MARGIN_REQUIREMENT);
setEnableMemoryDraft(false);
setLaunchModeDraft("fresh");
setRestoreRunIdDraft("");
setModeDraft(DEFAULT_MODE);
setPollIntervalDraft(DEFAULT_POLL_INTERVAL);
setStartDateDraft("");
@@ -427,10 +471,12 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setEndDateDraft,
setInitialCashDraft,
setIntervalMinutesDraft,
setLaunchModeDraft,
setMarginRequirementDraft,
setMaxCommCyclesDraft,
setModeDraft,
setPollIntervalDraft,
setRestoreRunIdDraft,
setRuntimeConfigFeedback,
setScheduleModeDraft,
setStartDateDraft,
@@ -482,6 +528,9 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
watchlistInputValue,
watchlistFeedback,
isWatchlistSaving,
launchModeDraft,
restoreRunIdDraft,
runtimeHistoryRuns,
scheduleModeDraft,
intervalMinutesDraft,
triggerTimeDraft,
@@ -527,6 +576,8 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setInitialCashDraft,
setMarginRequirementDraft,
setEnableMemoryDraft,
setLaunchModeDraft,
setRestoreRunIdDraft,
setModeDraft,
setPollIntervalDraft,
setStartDateDraft,

View File

@@ -38,6 +38,10 @@ export function fetchRuntimeEvents() {
return safeFetch(RUNTIME_API_BASE, '/events');
}
export function fetchRuntimeHistory(limit = 20) {
return safeFetch(RUNTIME_API_BASE, `/history?limit=${limit}`);
}
export function fetchPendingApprovals() {
return safeFetch(CONTROL_API_BASE, '/guard/pending');
}

View File

@@ -61,6 +61,9 @@ export const useRuntimeStore = create((set) => ({
setIsRuntimeSettingsOpen: (isRuntimeSettingsOpen) => set((state) => ({ isRuntimeSettingsOpen: resolveValue(isRuntimeSettingsOpen, state.isRuntimeSettingsOpen) })),
// Runtime config drafts
launchModeDraft: 'fresh',
restoreRunIdDraft: '',
runtimeHistoryRuns: [],
scheduleModeDraft: 'daily',
intervalMinutesDraft: '60',
triggerTimeDraft: 'now',
@@ -73,6 +76,9 @@ export const useRuntimeStore = create((set) => ({
startDateDraft: '',
endDateDraft: '',
enableMockDraft: false,
setLaunchModeDraft: (launchModeDraft) => set((state) => ({ launchModeDraft: resolveValue(launchModeDraft, state.launchModeDraft) })),
setRestoreRunIdDraft: (restoreRunIdDraft) => set((state) => ({ restoreRunIdDraft: resolveValue(restoreRunIdDraft, state.restoreRunIdDraft) })),
setRuntimeHistoryRuns: (runtimeHistoryRuns) => set((state) => ({ runtimeHistoryRuns: resolveValue(runtimeHistoryRuns, state.runtimeHistoryRuns) })),
setScheduleModeDraft: (scheduleModeDraft) => set((state) => ({ scheduleModeDraft: resolveValue(scheduleModeDraft, state.scheduleModeDraft) })),
setIntervalMinutesDraft: (intervalMinutesDraft) => set((state) => ({ intervalMinutesDraft: resolveValue(intervalMinutesDraft, state.intervalMinutesDraft) })),
setTriggerTimeDraft: (triggerTimeDraft) => set((state) => ({ triggerTimeDraft: resolveValue(triggerTimeDraft, state.triggerTimeDraft) })),