Files
daily_stock_analysis/apps/dsa-web/src/api/alphasift.ts
mumu 4b3f679b2d feat: adapt AlphaSift hotspot topics (#1635)
* feat: adapt AlphaSift hotspot topics

* fix(review-feedback-1635): preserve cached hotspots when refresh returns no rows and Avoid

* fix(review-feedback-1635): Reload detail when refreshing the selected hotspot and Fall back when

* fix(review-feedback-1635): update by the requested topic or a request token

* fix(review-feedback-1635): preserve analysis when optional serve startup fails and Fall back when

* fix(review-feedback-1635): 当前 CI 状态为 failure,且 backend-gate 为 cancelled、docker-build 为 skipped

* fix(review-feedback-1635): 让 backend-gate / docker-build 在当前 head 上完成并通过,或给出可接受的维护者级豁免说明

* fix(review-feedback-1635): 当前 CI 仍未闭环:结构化事实显示 backend-gate 被取消、docker-build skipped

* fix(review-feedback-1635): 修复 hotspot detail 的上游 schema 容错问题,并让当前 head 的阻断 CI 重新跑通

* fix(review-feedback-1635): docs/CHANGELOG.md:当前 diff 不是在 [Unreleased] 扁平格式下追加本 PR

* fix(review-feedback-1635): CI 闭环未完成:结构化事实显示当前 CI 状态为 failure,backend-gate cancelled、docker-build

* fix(review-feedback-1635): 让当前 head 的阻断型 CI 完整通过,并修正文档中的 AlphaSift commit hash 不一致

* fix(review-feedback-1635): CI 仍未在当前 head 完整通过:结构化事实显示

* fix(review-feedback-1635): 修复 docs/CHANGELOG.md 对主线既有 [Unreleased] 条目的删除,并补齐 AlphaSift

* fix(review-feedback-1635): Accept slash-containing hotspot topics

* fix(review-feedback-1635): Try industry constituents for industry hotspots

* fix(review-feedback-1635): Clear old hotspot details when loading a new topic and preserve the

* test: isolate main schedule environment

* chore: clean AlphaSift PR scope

* fix: respect AlphaSift hotspot fallbacks
2026-06-12 22:38:42 +08:00

316 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import apiClient from './index';
import { systemConfigApi } from './systemConfig';
import { toCamelCase } from './utils';
const ALPHASIFT_SCREEN_TIMEOUT_MS = 180000;
const ALPHASIFT_INSTALL_TIMEOUT_MS = 300000;
export const ALPHASIFT_CONFIG_CHANGED_EVENT = 'alphasift-config-changed';
export const SYSTEM_CONFIG_CHANGED_EVENT = 'dsa-system-config-changed';
export type AlphaSiftStatus = {
enabled: boolean;
available: boolean;
installSpecIsDefault: boolean;
contractVersion?: string | null;
version?: string | null;
strategyCount?: number | null;
diagnostics?: Record<string, string>;
};
export type AlphaSiftInstallResponse = {
installed: boolean;
alreadyInstalled: boolean;
installSpecIsDefault: boolean;
};
export type AlphaSiftCandidate = {
rank: number;
code: string;
name: string;
score?: number | null;
screenScore?: number | null;
reason: string;
riskLevel?: string;
riskFlags?: string[];
llmScore?: number | null;
llmConfidence?: number | null;
llmSector?: string;
llmTheme?: string;
llmTags?: string[];
llmThesis?: string;
llmCatalysts?: string[];
llmRisks?: string[];
llmWatchItems?: string[];
llmInvalidators?: string[];
llmStyleFit?: string;
price?: number | null;
changePct?: number | null;
amount?: number | null;
industry?: string;
factorScores?: Record<string, number>;
postAnalysisSummaries?: Record<string, string>;
postAnalysisTags?: string[];
dsaContext?: {
enriched?: boolean;
quote?: Record<string, unknown>;
fundamentals?: Record<string, unknown>;
news?: {
success?: boolean;
query?: string;
provider?: string;
results?: Array<Record<string, unknown>>;
error?: string | null;
};
warnings?: string[];
};
dsaNews?: Array<{
title?: string;
snippet?: string;
url?: string;
source?: string;
publishedDate?: string | null;
}>;
dsaAnalysisSummary?: string;
raw: Record<string, unknown>;
};
export type AlphaSiftStrategy = {
id: string;
name: string;
title?: string;
description: string;
version?: string;
category?: string;
tag?: string;
tags?: string[];
marketScope?: string[];
market?: string;
};
export type AlphaSiftStrategiesResponse = {
enabled: boolean;
strategies: AlphaSiftStrategy[];
strategyCount: number;
};
export type AlphaSiftHotspot = {
topic: string;
name?: string;
source?: string;
rank?: number | null;
changePct?: number | null;
heatScore?: number | null;
trendScore?: number | null;
persistenceScore?: number | null;
coolingScore?: number | null;
observations?: number | null;
state?: string;
stage?: string;
sampleStockCount?: number | null;
leaders?: string[];
providerUsed?: string;
fallbackUsed?: boolean;
cacheUsed?: boolean;
cachedAt?: string | null;
sourceErrors?: string[];
stale?: boolean;
staleAgeHours?: number | null;
};
export type AlphaSiftHotspotRouteItem = {
title: string;
description: string;
source?: string;
};
export type AlphaSiftHotspotStock = {
code?: string;
name?: string;
changePct?: number | null;
amount?: number | null;
turnoverRate?: number | null;
volumeRatio?: number | null;
role?: string;
hotStockScore?: number | null;
};
export type AlphaSiftHotspotDetail = {
enabled: boolean;
provider: string;
topic: string;
name?: string;
summary?: string;
route: AlphaSiftHotspotRouteItem[];
stocks: AlphaSiftHotspotStock[];
stockCount: number;
sourceErrors?: string[];
};
export type AlphaSiftHotspotsResponse = {
enabled: boolean;
provider: string;
providerUsed?: string;
fallbackUsed?: boolean;
cacheUsed?: boolean;
cachedAt?: string | null;
sourceErrors?: string[];
stale?: boolean;
staleAgeHours?: number | null;
hotspots: AlphaSiftHotspot[];
hotspotCount: number;
};
export type AlphaSiftScreenResponse = {
enabled: boolean;
candidates: AlphaSiftCandidate[];
candidateCount: number;
runId?: string;
strategy?: string;
market?: string;
snapshotCount?: number;
afterFilterCount?: number;
llmRanked?: boolean;
llmMarketView?: string;
llmSelectionLogic?: string;
llmPortfolioRisk?: string;
llmCoverage?: number | null;
llmParseErrors?: string[];
warnings?: string[];
sourceErrors?: string[];
dsaEnrichment?: {
enabled?: boolean;
maxCandidates?: number;
requestedCount?: number;
enrichedCount?: number;
warnings?: string[];
};
deepAnalysisRequested?: boolean | null;
postAnalyzers?: string[];
dailyEnriched?: boolean | null;
dailyEnrichCount?: number | null;
riskEnabled?: boolean | null;
portfolioDiversityEnabled?: boolean | null;
portfolioConcentrationNotes?: string[];
};
export type AlphaSiftScreenAccepted = {
taskId: string;
traceId?: string | null;
status: 'pending' | 'processing' | 'completed' | 'failed' | string;
message: string;
strategy: string;
market: string;
maxResults: number;
};
export type AlphaSiftScreenTaskStatus = {
taskId: string;
traceId?: string | null;
status: 'pending' | 'processing' | 'completed' | 'failed' | string;
progress?: number | null;
message?: string | null;
error?: string | null;
result?: AlphaSiftScreenResponse | null;
};
export function notifyAlphaSiftConfigChanged(): void {
window.dispatchEvent(new Event(ALPHASIFT_CONFIG_CHANGED_EVENT));
notifySystemConfigChanged();
}
export function notifySystemConfigChanged(): void {
window.dispatchEvent(new Event(SYSTEM_CONFIG_CHANGED_EVENT));
}
async function setAlphaSiftEnabled(value: 'true' | 'false'): Promise<void> {
const config = await systemConfigApi.getConfig(false);
await systemConfigApi.update({
configVersion: config.configVersion,
maskToken: config.maskToken,
reloadNow: true,
items: [{ key: 'ALPHASIFT_ENABLED', value }],
});
notifyAlphaSiftConfigChanged();
}
export const alphasiftApi = {
async getStatus(): Promise<AlphaSiftStatus> {
const response = await apiClient.get<Record<string, unknown>>('/api/v1/alphasift/status');
return toCamelCase<AlphaSiftStatus>(response.data);
},
async screen(payload: { market: string; strategy: string; maxResults: number }): Promise<AlphaSiftScreenResponse> {
const response = await apiClient.post<Record<string, unknown>>('/api/v1/alphasift/screen', {
market: payload.market,
strategy: payload.strategy,
max_results: payload.maxResults,
}, { timeout: ALPHASIFT_SCREEN_TIMEOUT_MS });
return toCamelCase<AlphaSiftScreenResponse>(response.data);
},
async startScreen(payload: { market: string; strategy: string; maxResults: number }): Promise<AlphaSiftScreenAccepted> {
const response = await apiClient.post<Record<string, unknown>>('/api/v1/alphasift/screen/tasks', {
market: payload.market,
strategy: payload.strategy,
max_results: payload.maxResults,
});
return toCamelCase<AlphaSiftScreenAccepted>(response.data);
},
async getScreenTask(taskId: string): Promise<AlphaSiftScreenTaskStatus> {
const response = await apiClient.get<Record<string, unknown>>(`/api/v1/alphasift/screen/tasks/${encodeURIComponent(taskId)}`);
return toCamelCase<AlphaSiftScreenTaskStatus>(response.data);
},
async getStrategies(): Promise<AlphaSiftStrategiesResponse> {
const response = await apiClient.get<Record<string, unknown>>('/api/v1/alphasift/strategies', { timeout: ALPHASIFT_INSTALL_TIMEOUT_MS });
return toCamelCase<AlphaSiftStrategiesResponse>(response.data);
},
async getHotspots(payload: { provider?: string; top?: number; refresh?: boolean } = {}): Promise<AlphaSiftHotspotsResponse> {
const response = await apiClient.get<Record<string, unknown>>('/api/v1/alphasift/hotspots', {
params: {
provider: payload.provider || 'akshare',
top: payload.top ?? 12,
refresh: payload.refresh ?? false,
},
timeout: ALPHASIFT_INSTALL_TIMEOUT_MS,
});
return toCamelCase<AlphaSiftHotspotsResponse>(response.data);
},
async getHotspotDetail(payload: { topic: string; provider?: string }): Promise<AlphaSiftHotspotDetail> {
const response = await apiClient.get<Record<string, unknown>>(
`/api/v1/alphasift/hotspots/${encodeURIComponent(payload.topic)}`,
{
params: { provider: payload.provider || 'akshare' },
timeout: ALPHASIFT_INSTALL_TIMEOUT_MS,
},
);
return toCamelCase<AlphaSiftHotspotDetail>(response.data);
},
async install(): Promise<AlphaSiftInstallResponse> {
const response = await apiClient.post<Record<string, unknown>>('/api/v1/alphasift/install', {}, { timeout: ALPHASIFT_INSTALL_TIMEOUT_MS });
return toCamelCase<AlphaSiftInstallResponse>(response.data);
},
async enable(): Promise<void> {
await setAlphaSiftEnabled('true');
try {
const status = await alphasiftApi.getStatus();
if (!status.available) {
const reason = status.diagnostics?.reason ? `${status.diagnostics.reason}` : '';
throw new Error(`AlphaSift 适配层不可用${reason}。请确认后端已安装项目依赖,必要时执行 pip install -r requirements.txt 或重建 Docker/桌面后端。`);
}
} catch (error) {
try {
await setAlphaSiftEnabled('false');
} catch {
// Preserve the original install/status failure for the caller.
}
throw error;
}
},
};