diff --git a/README.md b/README.md index b488688..3b6a4f7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 智能选股模块包含 45 日全市场因子库、六阶段市场识别、六套内置策略、受控公式 DSL、自然语言策略编译、候选排名和滚动回测。首次使用需在页面点击“同步因子数据”。未配置 LLM 时使用本地策略模板;配置兼容 API 后自动切换为主模型编译,主模型失败时自动使用辅助模型,两者均支持独立连通性测试。 -问师模块会读取当前复盘、近十日市场情绪、涨跌停、昨日反馈、板块轮动、市场阶段、龙虎榜和指定个股数据,再按选中的游资思维 Skill 进行单师对话。对话记录按老师和交易日期保存在浏览器本机;主模型不可用时自动切换辅助模型。 +问师模块会读取当前复盘、近十日市场情绪、涨跌停、昨日反馈、板块轮动、市场阶段、龙虎榜和指定个股数据,再按选中的游资思维 Skill 进行单师对话。对话记录按账号、老师和交易日期保存在服务端;主模型不可用时自动切换辅助模型。 新增问师角色时,在 `游资skills` 下增加一个包含 `SKILL.md` 的独立目录即可。系统会从 Skill 的 frontmatter、一级标题、核心模型和引用语中自动生成角色信息,无需修改注册代码。 diff --git a/server.py b/server.py index a2acd4b..a8fa75f 100644 --- a/server.py +++ b/server.py @@ -459,7 +459,7 @@ class DashboardService: profile = self._resolved_llm_profile() source = str(profile.get("source") or "none") if source == "none" or not self._profile_configured(profile.get("primary") or {}): - raise ValueError("请配置个人 LLM,或使用已开通会员的平台模型。") + raise ValueError("智能功能尚未配置,请联系管理员。") if source == "platform": limit = max(1, int(self._system_credentials.get("member_daily_limit") or 50)) if self._platform_usage_today() >= limit: @@ -1126,18 +1126,14 @@ class DashboardService: self.llm_fallback_model, ) compiled["compiler"] = "llm_fallback" - notice = f"主模型调用失败,已自动切换辅助模型。{primary_error}" if primary_error else "已使用辅助模型编译。" + notice = "智能策略生成服务已自动切换。" except LLMCompilerError as exc: fallback_error = str(exc) compiled = compile_local_strategy(prompt, regime) - notice = f"主模型和辅助模型均不可用,已使用本地模板。主模型:{primary_error or '未配置'};辅助模型:{fallback_error}" + notice = "智能策略生成暂不可用,已使用本地模板。" if compiled is None: compiled = compile_local_strategy(prompt, regime) - notice = ( - f"主模型调用失败且未配置辅助模型,已使用本地模板:{primary_error}" - if primary_error - else "尚未配置 LLM,当前使用本地受控模板编译。" - ) + notice = "智能策略生成暂不可用,已使用本地模板。" compiled["formula"] = self.screener.validate_formula(compiled["formula"]) compiled["notice"] = notice if source in {"personal", "platform"}: @@ -1234,13 +1230,10 @@ class DashboardService: self.record_llm_usage( "mentor", source, self.llm_fallback_model, "failed" ) - raise ValueError( - f"主模型和辅助模型均不可用。主模型:{primary_error or '未配置'};" - f"辅助模型:{fallback_error}" - ) from exc + raise ValueError("智能解读服务暂不可用,请稍后重试。") from exc if result is None: self.record_llm_usage("mentor", source, self.llm_primary_model, "failed") - raise ValueError(f"主模型不可用且未配置辅助模型:{primary_error}") + raise ValueError("智能解读服务暂不可用,请稍后重试。") self.record_llm_usage( "mentor", source, @@ -1262,7 +1255,7 @@ class DashboardService: "compiler": compiler, "requested_trade_date": trade_date, "data_trade_date": context["data_trade_date"], - "notice": "主模型调用失败,已自动切换辅助模型。" if compiler == "fallback" else "", + "notice": "智能解读已自动切换可用服务。" if compiler == "fallback" else "", } def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]: @@ -2196,7 +2189,7 @@ class DashboardService: **result, "mode": mode, "compiler": compiler, - "notice": "主模型调用失败,已自动切换辅助模型。" if compiler == "fallback" else "", + "notice": "智能解读已自动切换可用服务。" if compiler == "fallback" else "", } def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]: @@ -2236,12 +2229,9 @@ class DashboardService: self.record_llm_usage( f"heaven_{mode}", source, self.llm_fallback_model, "failed" ) - raise ValueError( - f"主模型和辅助模型均不可用。主模型:{primary_error or '未配置'};" - f"辅助模型:{exc}" - ) from exc + raise ValueError("智能解读服务暂不可用,请稍后重试。") from exc self.record_llm_usage(f"heaven_{mode}", source, self.llm_primary_model, "failed") - raise ValueError(f"主模型不可用且未配置辅助模型:{primary_error}") + raise ValueError("智能解读服务暂不可用,请稍后重试。") def _heaven_index_context( self, @@ -2612,7 +2602,7 @@ class DashboardService: "schema_version": 3, "cached": False, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), - "notice": f"游资接口不可用:{exc}", + "notice": "龙虎榜数据暂不可用,请稍后重试。", }, "summary": { "trader_count": 0, @@ -2640,7 +2630,7 @@ class DashboardService: "status": "unavailable", "schema_version": 3, "cached": False, - "notice": "公共行情尚未配置,暂无龙虎榜数据。", + "notice": "龙虎榜数据暂不可用,请联系管理员检查行情配置。", }, "summary": { "trader_count": 0, @@ -2997,7 +2987,7 @@ class DashboardService: compact_date = normalize_date(resolved_date) intraday_points: list[dict[str, Any]] = [] intraday_status = "unavailable" - intraday_notice = "未配置 Tushare Token,分时数据不可用。" + intraday_notice = "分时行情暂不可用。" if self.configured: cache_key = f"{code}:{compact_date}" @@ -3023,7 +3013,7 @@ class DashboardService: intraday_notice = "该交易日暂无分时数据。" except TushareError as exc: intraday_status = "unavailable" - intraday_notice = f"Tushare 分时接口不可用:{exc}" + intraday_notice = "分时行情暂不可用,请稍后重试。" prices = list(detail.get("prices") or [])[-60:] stock = dict(detail.get("stock") or {"code": code}) diff --git a/static/app.js b/static/app.js index 5d358b1..70c511b 100644 --- a/static/app.js +++ b/static/app.js @@ -1,3 +1,15 @@ +const { + clamp, + displayCompactDate, + escapeHtml, + formatNumber, + formatTimestamp, + localDateString, + number, + parseLocalDate, + todayString, +} = window.XiaobaiUI; + const state = { user: null, csrfToken: "", @@ -598,11 +610,7 @@ function applyDashboard(payload, background = false) { document.querySelector("#qiObservationDate").value = selectedDate; document.querySelector("#journalDate").value = selectedDate; renderDashboard(); - const source = payload.meta.source === "tushare" ? "Tushare" : "演示数据"; - const cacheText = payload.meta.realtime - ? "rt_k 实时行情" - : payload.meta.cached ? "SQLite 缓存" : "已写入 SQLite"; - setStatus(`${source} · ${cacheText}`); + setStatus(`${dashboardSourceLabel(payload.meta)} · 数据已更新`); if (!background) { if (state.activeView === "dragonView") loadDragonTiger(); if (state.activeView === "screenerView") loadScreenerSetup(); @@ -619,10 +627,10 @@ function applyDashboard(payload, background = false) { } function dashboardSourceLabel(meta = {}) { - if (meta.source === "demo") return "演示数据"; - if (meta.realtime && !["closed", "after_hours"].includes(String(meta.market_status || ""))) return "Tushare 实时行情"; - if (meta.source === "tushare") return "Tushare 盘后行情"; - return "本地行情"; + if (meta.realtime && !["closed", "after_hours"].includes(String(meta.market_status || ""))) return "盘中行情"; + if (meta.carried_forward) return "最近收盘行情"; + if (meta.market_status === "historical") return "历史行情"; + return "收盘行情"; } function renderDashboard() { @@ -1140,7 +1148,7 @@ async function loadDragonTiger(force = false) { const requestedDate = elements.tradeDate.value; if ( !force - && ["success", "demo"].includes(state.dragonTiger?.meta?.status) + && ["success", "empty", "partial", "unavailable"].includes(state.dragonTiger?.meta?.status) && (state.dragonTiger?.meta?.requested_date || state.dragonTiger?.meta?.trade_date) === requestedDate ) { renderDragonTiger(); @@ -1159,7 +1167,7 @@ async function loadDragonTiger(force = false) { ? "当日暂无公开游资明细" : payload.meta.status === "partial" ? "当日有龙虎榜,暂无命名游资明细" - : payload.meta.source === "tushare" ? "龙虎榜明细" : "演示数据"; + : payload.meta.status === "unavailable" ? "龙虎榜数据暂不可用" : "龙虎榜明细"; setStatus(`${statusLabel} · 龙虎榜已加载`); } catch (error) { showToast(error.message || "龙虎榜加载失败"); @@ -1202,8 +1210,8 @@ function renderDragonTraderList() { const container = document.querySelector("#dragonTraderList"); let emptyMessage = "没有符合当前条件的游资操作"; - if (!Array.isArray(payload.traders)) emptyMessage = "后端服务尚未重启,当前接口仍是旧版龙虎榜结构"; - else if (payload.meta?.status === "error") emptyMessage = "游资接口调用失败,请查看上方提示并检查 Tushare 积分权限"; + if (!Array.isArray(payload.traders)) emptyMessage = "龙虎榜数据格式暂不可用,请稍后重试"; + else if (["error", "unavailable"].includes(payload.meta?.status)) emptyMessage = "龙虎榜数据暂不可用,请稍后重试"; else if (payload.meta?.status === "empty") emptyMessage = "该交易日暂无游资每日明细"; else if (payload.meta?.status === "partial") emptyMessage = `当日有 ${number(payload.summary?.official_stock_count)} 只股票上榜,但暂无可识别的游资明细`; if (!traders.some((item) => item.id === state.selectedDragonTraderId)) { @@ -1600,8 +1608,7 @@ function renderScreenerSetup() { ? `${displayCompactDate(setup.factor_data.start_date)} 至 ${displayCompactDate(setup.factor_data.end_date)}` : "尚未达到 21 个交易日"); setText("factorTaskStatus", setup.factor_data.ready ? `已就绪 · ${number(setup.factor_data.date_count)} 日` : "需要同步"); - const compilerText = setup.llm.configured ? "模型编译就绪" : "本地编译就绪"; - setText("compilerStatus", compilerText); + setText("compilerStatus", "策略生成已就绪"); setText( "screenerRunStatus", state.screenerResult ? `已有结果 · ${(state.screenerResult.candidates || []).length} 只` : "等待执行", @@ -1721,10 +1728,7 @@ async function compileStrategy() { document.querySelector("#strategyNameInput").value = strategy.name; document.querySelector("#strategyDescriptionInput").value = strategy.description; document.querySelector("#formulaEditor").value = JSON.stringify(strategy.formula, null, 2); - setText( - "compilerStatus", - strategy.compiler === "local" ? "本地编译完成" : "模型编译完成", - ); + setText("compilerStatus", "策略生成完成"); if (strategy.notice) showToast(strategy.notice); setStatus("选股策略已编译"); } catch (error) { @@ -1923,11 +1927,10 @@ async function sendMentorQuestion(event) { question, history, }); - const modelRole = payload.compiler === "fallback" ? "辅助模型" : "主模型"; state.mentorMessages.push({ role: "assistant", content: payload.answer, - meta: `${displayCompactDate(payload.data_trade_date)} · ${modelRole} ${payload.model} · ${number(payload.latency_ms)}ms`, + meta: `${displayCompactDate(payload.data_trade_date)} · 回答完成`, }); if (payload.notice) showMentorNotice(payload.notice); setStatus("问师回答完成"); @@ -2756,12 +2759,13 @@ function renderSectorPhaseOverrides(items) {
${escapeHtml(item.element)} ${escapeHtml(item.name)} - ${canManage ? `` : ""} + ${canManage ? `` : ""}
`).join("") : '

暂无手动归类

'; container.querySelectorAll("[data-sector-phase-delete]").forEach((button) => { button.addEventListener("click", () => deleteSectorPhaseOverride(button.dataset.sectorPhaseDelete)); }); + refreshIcons(); } async function saveSectorPhaseOverride(event) { @@ -2933,10 +2937,9 @@ async function interpretHeaven(mode) { if (mode === "trend" && state.heavenManualData) payload.manual_data = state.heavenManualData; if (mode === "heart") payload.lines = state.heartLines; const result = await apiRequest("/api/heaven/interpret", "POST", payload); - const modelRole = result.compiler === "fallback" ? "辅助模型" : "主模型"; state.heavenInterpretations[mode] = { answer: result.answer, - meta: `${modelRole} ${result.model} · ${number(result.latency_ms)}ms`, + meta: mode === "trend" ? "观势已成" : mode === "fortune" ? "观气已成" : "解卦已成", }; if (result.notice) showHeavenNotice(result.notice); if (mode === "heart") { @@ -3525,7 +3528,7 @@ function renderScreenerResult() { setText( "screenerDisclaimer", meta.realtime - ? `rt_k 实时截面 · 历史因子截至 ${displayCompactDate(meta.history_cutoff)} · ${result.disclaimer}` + ? `盘中行情 · 历史样本截至 ${displayCompactDate(meta.history_cutoff)} · ${result.disclaimer}` : `盘后数据 ${displayCompactDate(meta.trade_date)} · ${result.disclaimer}`, ); document.querySelector("#screenerEmpty").hidden = candidates.length > 0; @@ -4083,9 +4086,7 @@ function renderStockPreview(payload) { setText("stockPreviewDate", payload.meta?.trade_date || elements.tradeDate.value); document.querySelector("#stockPreviewLoading").hidden = true; const intradayAvailable = (payload.intraday || []).length > 0; - const source = payload.meta?.realtime - ? "Tushare 实时行情" - : payload.meta?.source === "tushare" ? "Tushare 日K" : "演示日K"; + const source = payload.meta?.realtime ? "盘中行情" : "日 K 行情"; setText( "stockPreviewSource", intradayAvailable ? `${source} · 1分钟` : `${source} · 分时不可用`, @@ -4478,7 +4479,7 @@ async function openStock(code, fallback = null) { setText("detailName", stock.name || row.name); setText("detailPrice", formatNumber(stock.price || row.price, 2)); setText("detailChange", `${signed(stock.change ?? row.change)}%`); - setText("chartSource", `${payload.meta.source === "tushare" ? "Tushare" : "演示"} · ${payload.prices.length} 个交易日`); + setText("chartSource", `日 K 行情 · ${payload.prices.length} 个交易日`); renderMoneyflow(payload.moneyflow || {}); renderStockNotes(payload.notes || []); updateWatchButton(); @@ -4705,8 +4706,8 @@ async function openSettings(panel = "profile") { updateAccountIdentityBadges(membership); applyMembershipAccess(); } - const sourceLabel = membership.active && access.platform_configured ? "平台会员模型" : "尚未开通会员算力"; - status.textContent = `公共行情${payload.configured ? "已就绪" : "使用演示数据"} · ${sourceLabel}`; + const sourceLabel = membership.active && access.platform_configured ? "智能功能已就绪" : "智能功能尚未开通"; + status.textContent = `公共行情${payload.configured ? "已就绪" : "等待管理员配置"} · ${sourceLabel}`; status.classList.toggle("connected", Boolean(payload.configured)); setText("membershipBadge", membership.subscribed ? "会员有效" : membership.is_admin ? "管理员权限" : "普通用户"); setText("membershipStateValue", membership.subscribed ? "已开通" : membership.is_admin ? "管理员可用" : "未开通"); @@ -4717,11 +4718,11 @@ async function openSettings(panel = "profile") { ? `${membership.plan || "会员"}${membership.expires_at ? ` · 有效至 ${membershipDateDisplay(membership.expires_at, true)}` : " · 长期有效"}` : membership.is_admin ? "管理员拥有智能功能管理权限,但不会因此显示为已开通会员。" - : "开通会员后可使用智能选股、问师、问天及平台 LLM 算力。"); - setText("membershipQuotaHint", `会员默认每日 LLM 用量 ${number(access.daily_limit)} 次,由管理员统一设置。`); + : "开通会员后可使用智能选股、问师、问天等智能功能。"); + setText("membershipQuotaHint", `会员默认每日智能分析额度 ${number(access.daily_limit)} 次,由管理员统一设置。`); setText("membershipUsage", membership.active ? `今日已用 ${number(access.used_today)} 次` - : "今日 LLM 用量:--"); + : "今日智能分析:--"); setText("membershipUsageSummary", membership.active ? `${number(access.used_today)} / ${number(access.daily_limit)}` : "--"); setText("membershipRemainingUsage", membership.is_admin ? "不限" : membership.active ? `${number(access.remaining_calls)} 次` : "--"); const birth = payload.birth_profile || {}; @@ -5142,16 +5143,6 @@ function signed(value) { return `${parsed > 0 ? "+" : ""}${formatNumber(parsed, 2)}`; } -function formatNumber(value, digits = 0) { - return new Intl.NumberFormat("zh-CN", { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(number(value)); -} - -function formatTimestamp(value) { - const parsed = new Date(value); - if (Number.isNaN(parsed.getTime())) return "--"; - return parsed.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" }); -} - function dashboardDataTimestamp(meta = {}) { const tradeDate = displayCompactDate(meta.trade_date); if (tradeDate === "--") return "--"; @@ -5174,12 +5165,6 @@ function formatMoneyMillion(value) { return `${sign}${formatNumber(parsed * 100, 0)} 万`; } -function displayCompactDate(value) { - const text = String(value || "").replaceAll("-", ""); - if (text.length !== 8) return value || "--"; - return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`; -} - async function apiRequest(url, method = "GET", body = null, requestOptions = {}) { const options = { method, headers: {}, signal: requestOptions.signal }; if (method !== "GET" && state.csrfToken) { @@ -5427,34 +5412,3 @@ function animateRows(container) { function waitForMotion(duration) { return new Promise((resolve) => setTimeout(resolve, motionEnabled() ? duration : 0)); } - -function number(value) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : 0; -} - -function clamp(value, minimum, maximum) { - return Math.min(maximum, Math.max(minimum, number(value))); -} - -function escapeHtml(value) { - return String(value ?? "").replace(/[&<>"']/g, (character) => ({ - "&": "&", "<": "<", ">": ">", '"': """, "'": "'", - })[character]); -} - -function todayString() { - return localDateString(new Date()); -} - -function localDateString(value) { - const year = value.getFullYear(); - const month = String(value.getMonth() + 1).padStart(2, "0"); - const day = String(value.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -} - -function parseLocalDate(value) { - const [year, month, day] = value.split("-").map(Number); - return new Date(year, month - 1, day); -} diff --git a/static/index.html b/static/index.html index 1b1dd29..67a6e3e 100644 --- a/static/index.html +++ b/static/index.html @@ -1008,7 +1008,7 @@
- +
@@ -1069,27 +1069,27 @@ 当前账号

个人资料

- +
正在读取账号状态

个人资料

尚未设置
@@ -1103,7 +1103,7 @@ -

原始信息加密保存且不在观气页回显;模型只接收排盘后的派生结果。

+

原始信息加密保存且不在观气页回显;智能解读只使用排盘后的派生结果。