refactor: establish frontend request and state boundaries
This commit is contained in:
+193
-237
@@ -21,166 +21,185 @@ const THEME_STORAGE_KEY = "xiaobaiTheme";
|
||||
let activeThemeTransition = null;
|
||||
let themeSwitchSequence = 0;
|
||||
|
||||
const state = {
|
||||
user: null,
|
||||
csrfToken: "",
|
||||
authMode: "login",
|
||||
started: false,
|
||||
dashboard: null,
|
||||
filter: "all",
|
||||
query: "",
|
||||
sortKey: "streak",
|
||||
sortDirection: "desc",
|
||||
brokenQuery: "",
|
||||
brokenSortKey: "",
|
||||
brokenSortDirection: "desc",
|
||||
downQuery: "",
|
||||
downSortKey: "",
|
||||
downSortDirection: "asc",
|
||||
yesterdayFilter: "all",
|
||||
yesterdayQuery: "",
|
||||
yesterdaySortKey: "",
|
||||
yesterdaySortDirection: "desc",
|
||||
activeView: "sentimentCycleView",
|
||||
dragonTiger: null,
|
||||
dragonViewMode: "daily",
|
||||
dragonFilter: "all",
|
||||
dragonQuery: "",
|
||||
selectedDragonTraderId: "",
|
||||
hotMoneyProfiles: null,
|
||||
hotMoneyProfileQuery: "",
|
||||
selectedHotMoneyProfileId: "",
|
||||
rotationHistory: null,
|
||||
rotationHistoryKey: "",
|
||||
rotationSelectedSector: "",
|
||||
rotationSelectedDate: "",
|
||||
rotationMembers: null,
|
||||
rotationMembersKey: "",
|
||||
rotationMembersLoading: false,
|
||||
rotationOrder: localStorage.getItem("xiaobaiRotationOrder") === "latest" ? "latest" : "oldest",
|
||||
rotationLoading: false,
|
||||
auctionData: null,
|
||||
auctionDataset: "focus",
|
||||
auctionFilter: "all",
|
||||
auctionQuery: "",
|
||||
auctionSortKey: "attention_score",
|
||||
auctionSortDirection: "desc",
|
||||
auctionLoading: false,
|
||||
auctionTimer: null,
|
||||
themeLibrary: null,
|
||||
themeQuery: "",
|
||||
selectedThemeCode: "",
|
||||
themeDetail: null,
|
||||
themeLoading: false,
|
||||
popularityData: null,
|
||||
popularitySource: "combined",
|
||||
popularityQuery: "",
|
||||
popularityLoading: false,
|
||||
expandedLadderLevels: new Set(),
|
||||
ladderSortMode: "time",
|
||||
stockDetail: null,
|
||||
activeStock: null,
|
||||
stockDetailChartMode: "daily",
|
||||
stockDetailIntraday: null,
|
||||
stockDetailRequestSequence: 0,
|
||||
entityDetailItem: null,
|
||||
entityDetailPayload: null,
|
||||
entityDetailChartMode: "daily",
|
||||
entityDetailIntraday: null,
|
||||
entityDetailRequestSequence: 0,
|
||||
stockPreviewCode: "",
|
||||
stockPreviewType: "stock",
|
||||
stockPreviewItem: null,
|
||||
stockPreviewPayload: null,
|
||||
stockPreviewChart: "daily",
|
||||
stockPreviewFallback: null,
|
||||
watchlist: [],
|
||||
watchlistSelection: null,
|
||||
watchlistSearchResults: [],
|
||||
watchlistSearchRequestSequence: 0,
|
||||
editingDailyNoteId: 0,
|
||||
notes: [],
|
||||
tradeEntries: [],
|
||||
tradeSummary: {},
|
||||
editingTradeId: 0,
|
||||
initialStockOpened: false,
|
||||
screenerSetup: null,
|
||||
screenerSetupKey: "",
|
||||
screenerSetupRequestKey: "",
|
||||
screenerSetupPromise: null,
|
||||
selectedRegime: "",
|
||||
selectedStrategy: null,
|
||||
customStrategyDraft: null,
|
||||
screenerRunning: false,
|
||||
screenerRunningMode: "",
|
||||
screenerResults: { smart: null, curated: null, quant: null },
|
||||
screenerResultContexts: { smart: null, curated: null, quant: null },
|
||||
screenerResultStore: {},
|
||||
screenerTracking: null,
|
||||
screenerMode: ["smart", "curated", "quant"].includes(localStorage.getItem("xiaobaiScreenerMode"))
|
||||
? localStorage.getItem("xiaobaiScreenerMode")
|
||||
: "smart",
|
||||
curatedCategory: "全部",
|
||||
curatedSchool: "全部",
|
||||
curatedQuery: "",
|
||||
curatedViewMode: localStorage.getItem("xiaobaiCuratedViewMode") === "grid" ? "grid" : "list",
|
||||
selectedCuratedStrategyId: 0,
|
||||
quantFilters: [],
|
||||
quantScores: [],
|
||||
alerts: [],
|
||||
alertFilter: "all",
|
||||
alertUnreadCount: 0,
|
||||
assistantMessages: [],
|
||||
assistantLoading: false,
|
||||
assistantController: null,
|
||||
screenerMobileView: "strategy",
|
||||
sentimentHistory: null,
|
||||
sentimentRange: 20,
|
||||
sentimentHistoryKey: "",
|
||||
sentimentLoading: false,
|
||||
mentorSetup: null,
|
||||
selectedMentorId: "",
|
||||
mentorMessages: [],
|
||||
mentorLoading: false,
|
||||
mentorQuery: "",
|
||||
mentorGrade: "all",
|
||||
mentorDirectoryOpen: false,
|
||||
mentorSortMode: false,
|
||||
mentorSavingPreferences: false,
|
||||
mentorController: null,
|
||||
heavenSetup: null,
|
||||
heavenManualData: null,
|
||||
personalField: null,
|
||||
heavenPanel: "trend",
|
||||
heavenInterpretations: { trend: "", fortune: "", heart: "" },
|
||||
heavenReadingMode: "trend",
|
||||
heavenReadingTab: "current",
|
||||
heavenReadingHistory: { trend: [], fortune: [], heart: [] },
|
||||
heavenReadingSelectedId: 0,
|
||||
heavenReadingLoading: false,
|
||||
heavenReadingError: "",
|
||||
heartStage: "intro",
|
||||
heartTimer: null,
|
||||
heartSeconds: HEART_BREATH_TOTAL_MS / 1000,
|
||||
heartBreathingEndsAt: 0,
|
||||
heartLines: [],
|
||||
heartThrows: [],
|
||||
heartHexagram: null,
|
||||
heartCurtainTimer: null,
|
||||
heartStageToken: 0,
|
||||
heartRevealToken: 0,
|
||||
heavenPerformanceKey: "",
|
||||
heavenPerformancePanels: new Set(),
|
||||
heavenPerformanceActive: "",
|
||||
dashboardLoading: false,
|
||||
dashboardRequestSequence: 0,
|
||||
heavenRequestSequence: 0,
|
||||
dashboardRequestDate: "",
|
||||
adminModels: [],
|
||||
globalSearchResults: [],
|
||||
globalSearchActiveIndex: -1,
|
||||
globalSearchRequestSequence: 0,
|
||||
};
|
||||
const state = window.XiaobaiState.create({
|
||||
session: {
|
||||
user: null,
|
||||
csrfToken: "",
|
||||
authMode: "login",
|
||||
started: false,
|
||||
activeView: "sentimentCycleView",
|
||||
dashboardLoading: false,
|
||||
dashboardRequestSequence: 0,
|
||||
dashboardRequestDate: "",
|
||||
adminModels: [],
|
||||
globalSearchResults: [],
|
||||
globalSearchActiveIndex: -1,
|
||||
globalSearchRequestSequence: 0,
|
||||
},
|
||||
market: {
|
||||
dashboard: null,
|
||||
filter: "all",
|
||||
query: "",
|
||||
sortKey: "streak",
|
||||
sortDirection: "desc",
|
||||
brokenQuery: "",
|
||||
brokenSortKey: "",
|
||||
brokenSortDirection: "desc",
|
||||
downQuery: "",
|
||||
downSortKey: "",
|
||||
downSortDirection: "asc",
|
||||
yesterdayFilter: "all",
|
||||
yesterdayQuery: "",
|
||||
yesterdaySortKey: "",
|
||||
yesterdaySortDirection: "desc",
|
||||
dragonTiger: null,
|
||||
dragonViewMode: "daily",
|
||||
dragonFilter: "all",
|
||||
dragonQuery: "",
|
||||
selectedDragonTraderId: "",
|
||||
hotMoneyProfiles: null,
|
||||
hotMoneyProfileQuery: "",
|
||||
selectedHotMoneyProfileId: "",
|
||||
rotationHistory: null,
|
||||
rotationHistoryKey: "",
|
||||
rotationSelectedSector: "",
|
||||
rotationSelectedDate: "",
|
||||
rotationMembers: null,
|
||||
rotationMembersKey: "",
|
||||
rotationMembersLoading: false,
|
||||
rotationOrder: localStorage.getItem("xiaobaiRotationOrder") === "latest" ? "latest" : "oldest",
|
||||
rotationLoading: false,
|
||||
auctionData: null,
|
||||
auctionDataset: "focus",
|
||||
auctionFilter: "all",
|
||||
auctionQuery: "",
|
||||
auctionSortKey: "attention_score",
|
||||
auctionSortDirection: "desc",
|
||||
auctionLoading: false,
|
||||
auctionTimer: null,
|
||||
themeLibrary: null,
|
||||
themeQuery: "",
|
||||
selectedThemeCode: "",
|
||||
themeDetail: null,
|
||||
themeLoading: false,
|
||||
popularityData: null,
|
||||
popularitySource: "combined",
|
||||
popularityQuery: "",
|
||||
popularityLoading: false,
|
||||
expandedLadderLevels: new Set(),
|
||||
ladderSortMode: "time",
|
||||
sentimentHistory: null,
|
||||
sentimentRange: 20,
|
||||
sentimentHistoryKey: "",
|
||||
sentimentLoading: false,
|
||||
},
|
||||
details: {
|
||||
stockDetail: null,
|
||||
activeStock: null,
|
||||
stockDetailChartMode: "daily",
|
||||
stockDetailIntraday: null,
|
||||
stockDetailRequestSequence: 0,
|
||||
entityDetailItem: null,
|
||||
entityDetailPayload: null,
|
||||
entityDetailChartMode: "daily",
|
||||
entityDetailIntraday: null,
|
||||
entityDetailRequestSequence: 0,
|
||||
stockPreviewCode: "",
|
||||
stockPreviewType: "stock",
|
||||
stockPreviewItem: null,
|
||||
stockPreviewPayload: null,
|
||||
stockPreviewChart: "daily",
|
||||
stockPreviewFallback: null,
|
||||
initialStockOpened: false,
|
||||
},
|
||||
review: {
|
||||
watchlist: [],
|
||||
watchlistSelection: null,
|
||||
watchlistSearchResults: [],
|
||||
watchlistSearchRequestSequence: 0,
|
||||
editingDailyNoteId: 0,
|
||||
notes: [],
|
||||
tradeEntries: [],
|
||||
tradeSummary: {},
|
||||
editingTradeId: 0,
|
||||
alerts: [],
|
||||
alertFilter: "all",
|
||||
alertUnreadCount: 0,
|
||||
assistantMessages: [],
|
||||
assistantLoading: false,
|
||||
assistantController: null,
|
||||
},
|
||||
screener: {
|
||||
screenerSetup: null,
|
||||
screenerSetupKey: "",
|
||||
screenerSetupRequestKey: "",
|
||||
screenerSetupPromise: null,
|
||||
selectedRegime: "",
|
||||
selectedStrategy: null,
|
||||
customStrategyDraft: null,
|
||||
screenerRunning: false,
|
||||
screenerRunningMode: "",
|
||||
screenerResults: { smart: null, curated: null, quant: null },
|
||||
screenerResultContexts: { smart: null, curated: null, quant: null },
|
||||
screenerResultStore: {},
|
||||
screenerTracking: null,
|
||||
screenerMode: ["smart", "curated", "quant"].includes(localStorage.getItem("xiaobaiScreenerMode"))
|
||||
? localStorage.getItem("xiaobaiScreenerMode")
|
||||
: "smart",
|
||||
curatedCategory: "全部",
|
||||
curatedSchool: "全部",
|
||||
curatedQuery: "",
|
||||
curatedViewMode: localStorage.getItem("xiaobaiCuratedViewMode") === "grid" ? "grid" : "list",
|
||||
selectedCuratedStrategyId: 0,
|
||||
quantFilters: [],
|
||||
quantScores: [],
|
||||
screenerMobileView: "strategy",
|
||||
},
|
||||
mentor: {
|
||||
mentorSetup: null,
|
||||
selectedMentorId: "",
|
||||
mentorMessages: [],
|
||||
mentorLoading: false,
|
||||
mentorQuery: "",
|
||||
mentorGrade: "all",
|
||||
mentorDirectoryOpen: false,
|
||||
mentorSortMode: false,
|
||||
mentorSavingPreferences: false,
|
||||
mentorController: null,
|
||||
},
|
||||
heaven: {
|
||||
heavenSetup: null,
|
||||
heavenManualData: null,
|
||||
personalField: null,
|
||||
heavenPanel: "trend",
|
||||
heavenInterpretations: { trend: "", fortune: "", heart: "" },
|
||||
heavenReadingMode: "trend",
|
||||
heavenReadingTab: "current",
|
||||
heavenReadingHistory: { trend: [], fortune: [], heart: [] },
|
||||
heavenReadingSelectedId: 0,
|
||||
heavenReadingLoading: false,
|
||||
heavenReadingError: "",
|
||||
heartStage: "intro",
|
||||
heartTimer: null,
|
||||
heartSeconds: HEART_BREATH_TOTAL_MS / 1000,
|
||||
heartBreathingEndsAt: 0,
|
||||
heartLines: [],
|
||||
heartThrows: [],
|
||||
heartHexagram: null,
|
||||
heartCurtainTimer: null,
|
||||
heartStageToken: 0,
|
||||
heartRevealToken: 0,
|
||||
heavenPerformanceKey: "",
|
||||
heavenPerformancePanels: new Set(),
|
||||
heavenPerformanceActive: "",
|
||||
heavenRequestSequence: 0,
|
||||
},
|
||||
});
|
||||
|
||||
window.XiaobaiAPI.configure({
|
||||
csrfToken: () => state.csrfToken,
|
||||
onUnauthorized: () => showAuthGate("登录状态已失效,请重新登录。"),
|
||||
});
|
||||
|
||||
const elements = {
|
||||
tradeDate: document.querySelector("#tradeDate"),
|
||||
@@ -4698,39 +4717,16 @@ function scheduleMentorRender() {
|
||||
}
|
||||
|
||||
async function streamMentorRequest(body, signal, onDelta, onMeta) {
|
||||
const response = await fetch("/api/mentors/chat", {
|
||||
await window.XiaobaiAPI.streamNdjson("/api/mentors/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(state.csrfToken ? { "X-CSRF-Token": state.csrfToken } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body,
|
||||
signal,
|
||||
errorMessage: "问师暂不可用",
|
||||
onEvent: (event) => {
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "meta") onMeta(event);
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
throw new Error(payload.error || "问师暂不可用");
|
||||
}
|
||||
if (!response.body) throw new Error("当前浏览器不支持流式回答");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const consume = (line) => {
|
||||
if (!line.trim()) return;
|
||||
const event = JSON.parse(line);
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "meta") onMeta(event);
|
||||
if (event.type === "error") throw new Error(event.error || "问师回答失败");
|
||||
};
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
lines.forEach(consume);
|
||||
if (done) break;
|
||||
}
|
||||
if (buffer.trim()) consume(buffer);
|
||||
}
|
||||
|
||||
function useMentorQuickPrompt(prompt) {
|
||||
@@ -7907,41 +7903,15 @@ async function sendAssistantQuestion(event) {
|
||||
}
|
||||
|
||||
async function streamAssistantRequest(question, signal, onDelta) {
|
||||
const response = await fetch("/api/assistant/chat", {
|
||||
await window.XiaobaiAPI.streamNdjson("/api/assistant/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(state.csrfToken ? { "X-CSRF-Token": state.csrfToken } : {}),
|
||||
},
|
||||
body: JSON.stringify({ question, trade_date: elements.tradeDate.value }),
|
||||
body: { question, trade_date: elements.tradeDate.value },
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
throw new Error(payload.error || "复盘助手暂不可用");
|
||||
}
|
||||
if (!response.body) throw new Error("当前浏览器不支持流式回答");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
const event = JSON.parse(line);
|
||||
errorMessage: "复盘助手暂不可用",
|
||||
onEvent: (event) => {
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "error") throw new Error(event.error || "复盘助手回答失败");
|
||||
}
|
||||
if (done) break;
|
||||
}
|
||||
if (buffer.trim()) {
|
||||
const event = JSON.parse(buffer);
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "error") throw new Error(event.error || "复盘助手回答失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function stopAssistantResponse() {
|
||||
@@ -9167,21 +9137,7 @@ function formatMoneyMillion(value) {
|
||||
}
|
||||
|
||||
async function apiRequest(url, method = "GET", body = null, requestOptions = {}) {
|
||||
const options = { method, headers: {}, signal: requestOptions.signal };
|
||||
if (method !== "GET" && state.csrfToken) {
|
||||
options.headers["X-CSRF-Token"] = state.csrfToken;
|
||||
}
|
||||
if (body !== null) {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const response = await fetch(url, options);
|
||||
const payload = await response.json();
|
||||
if (response.status === 401 && !url.startsWith("/api/auth/")) {
|
||||
showAuthGate("登录状态已失效,请重新登录。");
|
||||
}
|
||||
if (!response.ok || payload.error) throw new Error(payload.error || "请求失败");
|
||||
return payload;
|
||||
return window.XiaobaiAPI.request(url, method, body, requestOptions);
|
||||
}
|
||||
|
||||
function setLoading(loading, text = "正在加载复盘数据", context = "default") {
|
||||
|
||||
Reference in New Issue
Block a user