@@ -7,13 +7,22 @@ const REQUEST_TIMEOUT = 10000 // 10s timeout for all API requests
77
88async function fetchWithTimeout ( path : string , init ?: RequestInit , timeoutMs = REQUEST_TIMEOUT ) : Promise < Response > {
99 const controller = new AbortController ( )
10- const timer = setTimeout ( ( ) => controller . abort ( ) , timeoutMs )
10+ let timedOut = false
11+ const timer = setTimeout ( ( ) => {
12+ timedOut = true
13+ controller . abort ( )
14+ } , timeoutMs )
1115 const external = init ?. signal
1216 const onExternalAbort = ( ) => controller . abort ( )
1317 external ?. addEventListener ( 'abort' , onExternalAbort )
1418 try {
1519 const { signal : _ignored , ...rest } = init ?? { }
1620 return await fetch ( path , { ...rest , signal : controller . signal } )
21+ } catch ( e ) {
22+ if ( timedOut && e instanceof Error && e . name === 'AbortError' ) {
23+ throw new Error ( '请求超时' )
24+ }
25+ throw e
1726 } finally {
1827 clearTimeout ( timer )
1928 external ?. removeEventListener ( 'abort' , onExternalAbort )
@@ -33,13 +42,14 @@ export async function apiCall<T>(
3342 feature : string ,
3443 params : Record < string , any > = { } ,
3544 init ?: RequestInit ,
45+ timeoutMs = REQUEST_TIMEOUT ,
3646) : Promise < ApiResponse < T > > {
3747 const resp = await fetchWithTimeout ( `${ API_BASE } /research` , {
3848 method : 'POST' ,
3949 headers : { 'Content-Type' : 'application/json' } ,
4050 body : JSON . stringify ( { feature, params } ) ,
4151 ...init ,
42- } )
52+ } , timeoutMs )
4353 if ( ! resp . ok ) throw new Error ( `API error: ${ resp . status } ` )
4454 return resp . json ( )
4555}
@@ -56,14 +66,25 @@ export const research = {
5666 diagnose : ( code : string ) =>
5767 apiCall < StockDiagnosisData > ( 'stock_diagnosis' , { code } ) ,
5868
59- institutionRating : ( code : string , groups ?: string [ ] ) =>
60- apiCall < InstitutionRatingData > ( 'institution_rating' , { code, groups } ) ,
69+ institutionRating : ( code : string , groups ?: string [ ] , signal ?: AbortSignal ) =>
70+ apiCall < InstitutionRatingData > ( 'institution_rating' , { code, groups } , { signal } , 20000 ) ,
71+
72+ screen : ( conditions : any [ ] , scorecard = '综合评估' , topN = 20 , signal ?: AbortSignal ) =>
73+ apiCall < ScreeningData > ( 'screening' , { conditions, scorecard, top_n : topN } , { signal } , 120000 ) ,
6174
62- screen : ( conditions : any [ ] , scorecard = '综合评估' , topN = 20 ) =>
63- apiCall < ScreeningData > ( 'screening' , { conditions, scorecard, top_n : topN } ) ,
75+ marketDbStatus : ( ) =>
76+ apiCall < import ( '../types/market' ) . MarketDbStatusData > ( 'market_db_status' ) ,
77+
78+ marketDbSync : ( mode : 'full' | 'incremental' | 'resume' = 'full' , background = true , force = false ) =>
79+ apiCall < { started : boolean ; running : boolean ; mode : string } > (
80+ 'market_db_sync' ,
81+ { mode, background, force } ,
82+ undefined ,
83+ background ? 15000 : 600000 ,
84+ ) ,
6485
65- strategySignals : ( code : string ) =>
66- apiCall < StrategySignalData > ( 'strategy_signal' , { code } ) ,
86+ strategySignals : ( code : string , signal ?: AbortSignal ) =>
87+ apiCall < StrategySignalData > ( 'strategy_signal' , { code } , { signal } , 30000 ) ,
6788
6889 strategyVerify : ( code : string , checkpoints = 30 , forwardDays = 5 ) =>
6990 apiCall < StrategyVerifyData > ( 'strategy_verify' , { code, checkpoints, forward_days : forwardDays } ) ,
@@ -83,6 +104,9 @@ export const research = {
83104 stockQuotes : ( codes : string [ ] ) =>
84105 apiCall < import ( '../types/market' ) . StockQuotesData > ( 'stock_quotes' , { codes } ) ,
85106
107+ watchlistRadar : ( codes : string [ ] , signal ?: AbortSignal ) =>
108+ apiCall < import ( '../types/schemas' ) . WatchlistRadarData > ( 'watchlist_radar' , { codes } , { signal } , 15000 ) ,
109+
86110 stockKline : ( code : string , count = 90 ) =>
87111 apiCall < import ( '../types/market' ) . StockKlineData > ( 'stock_kline' , { code, count } ) ,
88112
@@ -100,10 +124,12 @@ export const research = {
100124 { signal } ,
101125 ) ,
102126
103- stockCyq : ( code : string ) =>
127+ stockCyq : ( code : string , signal ?: AbortSignal ) =>
104128 apiCall < { code : string ; rows : import ( '../types/market' ) . ChipDistributionPoint [ ] ; latest : import ( '../types/market' ) . ChipDistributionPoint } > (
105129 'stock_cyq' ,
106130 { code } ,
131+ { signal } ,
132+ 15000 ,
107133 ) ,
108134
109135 stockDetail : ( code : string ) =>
@@ -112,8 +138,8 @@ export const research = {
112138 backtest : ( codes : string [ ] , scorecard = '综合评估' , periods = 5 ) =>
113139 apiCall < BacktestResultData > ( 'backtest' , { codes, scorecard, periods } ) ,
114140
115- latestEval : ( code : string ) =>
116- apiCall < LatestEvalData > ( 'latest_evaluation' , { code } ) ,
141+ latestEval : ( code : string , signal ?: AbortSignal ) =>
142+ apiCall < LatestEvalData > ( 'latest_evaluation' , { code } , { signal } , 30000 ) ,
117143
118144 strategyReport : ( code : string ) =>
119145 apiCall < ReportTextData > ( 'strategy_report' , { code } ) ,
@@ -134,6 +160,61 @@ export const research = {
134160 apiCall < import ( '../types/schemas' ) . PortfolioSummaryData > ( 'portfolio_summary' , { } ) ,
135161}
136162
163+ export async function getMarketDataSyncState ( ) {
164+ const resp = await jsonFetch < { success : boolean ; data : import ( '../types/market' ) . MarketDataSyncState } > (
165+ '/market-data/sync-state' ,
166+ )
167+ if ( ! resp . data ) throw new Error ( '无法获取同步状态' )
168+ return resp . data
169+ }
170+
171+ export async function startMarketDataSync (
172+ mode : 'full' | 'incremental' | 'resume' = 'full' ,
173+ options : { force ?: boolean ; jobs ?: string [ ] ; profile ?: string } = { } ,
174+ ) {
175+ return jsonFetch < { success : boolean ; message ?: string } > ( '/market-data/sync' , {
176+ method : 'POST' ,
177+ headers : { 'Content-Type' : 'application/json' } ,
178+ body : JSON . stringify ( {
179+ mode,
180+ background : true ,
181+ force : options . force ?? false ,
182+ jobs : options . jobs ,
183+ profile : options . profile ,
184+ } ) ,
185+ } )
186+ }
187+
188+ export interface TusharePublicConfig {
189+ enabled : boolean
190+ token : string
191+ token_configured : boolean
192+ token_preview : string
193+ config_path : string
194+ }
195+
196+ export async function getTushareConfig ( ) {
197+ const resp = await jsonFetch < { success : boolean ; data : TusharePublicConfig } > ( '/tushare/config' )
198+ if ( ! resp . data ) throw new Error ( '无法读取 Tushare 配置' )
199+ return resp . data
200+ }
201+
202+ export async function saveTushareConfig ( payload : { enabled : boolean ; token ?: string } ) {
203+ return jsonFetch < { success : boolean ; data : TusharePublicConfig ; message ?: string } > ( '/tushare/config' , {
204+ method : 'POST' ,
205+ headers : { 'Content-Type' : 'application/json' } ,
206+ body : JSON . stringify ( payload ) ,
207+ } )
208+ }
209+
210+ export async function testTushareConfig ( token ?: string ) {
211+ return jsonFetch < { success : boolean ; data : { ok : boolean ; message : string } ; message ?: string } > ( '/tushare/test' , {
212+ method : 'POST' ,
213+ headers : { 'Content-Type' : 'application/json' } ,
214+ body : JSON . stringify ( token ? { token } : { } ) ,
215+ } )
216+ }
217+
137218export async function writerTypes ( ) {
138219 const resp = await fetchWithTimeout ( `${ API_BASE } /writer/types` )
139220 if ( ! resp . ok ) throw new Error ( 'writer types failed' )
0 commit comments