From 2fda6f0dcf7a0555db6189dc36996e211d9b07bc Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Wed, 21 Jan 2026 08:51:44 +0800 Subject: [PATCH] 'commit' --- Controller/YltAnalyticsController.py | 112 ++++++++++++++---- .../YltAnalyticsController.cpython-310.pyc | Bin 16006 -> 18760 bytes Util/LlmUtil.py | 2 +- Util/__pycache__/LlmUtil.cpython-310.pyc | Bin 1948 -> 1997 bytes static/dashboard.html | 9 +- static/js/dashboard.js | 94 +++++++++------ 6 files changed, 156 insertions(+), 61 deletions(-) diff --git a/Controller/YltAnalyticsController.py b/Controller/YltAnalyticsController.py index ff15a88..7b4c1f2 100644 --- a/Controller/YltAnalyticsController.py +++ b/Controller/YltAnalyticsController.py @@ -214,29 +214,97 @@ async def export_ai_report_docx(req: AiReportRequest): @router.get("/api/ai/pricing/strategy-summary") async def ai_pricing_strategy_summary(): async def generate_stream(): - resp = await get_operators_hourly_prices() - data = resp.get("operators", []) - text_data = [] - for item in data: - text_data.append({"operator": item.get("operator"), "series": item.get("series")}) - - prompt = ( - "下面是四家供应商(新电途、特来电、驿来特、艾特吉易充)基于最新爬取数据计算出的平均24小时分时电价:\n" - f"{text_data}\n" - "请根据这些数据,综合分析各司的定价策略差异,重点对比我司(驿来特)与其他供应商的分时电价水平," - "指出我司在不同时段可能存在的潜在问题和风险(例如明显偏贵、价格结构不合理等),并给出2-3条可执行的优化建议。" - "请使用Markdown格式输出,重点可以使用加粗,如有公式可使用LaTeX格式。" - "回答控制在800字以内。" - ) - - async for chunk in get_llm_response( - prompt, - stream=True, - system_prompt="你是熟悉中国充电桩行业的电价策略分析顾问。", - ): - yield chunk + try: + # 发送初始信息并增加一些空白填充,防止某些代理缓存 + yield "正在收集各供应商价格数据,请稍候...\n\n" + (" " * 512) + "\n" + print("AI分析开始: 获取运营商价格数据...") + + # 使用 asyncio.wait_for 防止数据库查询无限挂起 + try: + # 1. 获取当前最新 24 小时平均价格 + resp = await asyncio.wait_for(get_operators_hourly_prices(), timeout=30.0) + + # 2. 获取最近 3 天的价格变动趋势 + trend_resp = await asyncio.wait_for(get_operators_price_trends(days=3), timeout=30.0) + except asyncio.TimeoutError: + print("获取价格数据超时") + yield "\n\n**错误**: 获取价格数据超时,数据库响应过慢,请稍后重试。" + return - return StreamingResponse(generate_stream(), media_type="text/event-stream") + # 处理当前价格数据 + data = resp.get("operators", []) + text_data = [] + for item in data: + text_data.append({"operator": item.get("operator"), "series": item.get("series")}) + + # 处理 3 天趋势数据 + trend_dates = trend_resp.get("dates", []) + trend_series = trend_resp.get("series", []) + trend_text = [] + for s in trend_series: + trend_text.append({"operator": s.get("name"), "daily_avg_prices": s.get("data")}) + + print(f"数据获取完成,准备请求LLM. 数据条数: {len(text_data)}, 趋势天数: {len(trend_dates)}") + yield "数据收集完成,正在分析最近 3 天的价格波动趋势并生成深度建议...\n\n" + + # 增加一个心跳,确保连接不断开 + yield " " * 128 + "\n" + + prompt = ( + "你是一位专业的充电桩调价策略分析顾问。下面是四家供应商(新电途、特来电、驿来特、艾特吉易充)的电价分析数据:\n\n" + "### 1. 当前最新 24 小时平均分时电价 (元/kWh)\n" + f"{json.dumps(text_data, ensure_ascii=False)}\n\n" + "### 2. 最近 3 天的价格变动趋势 (每日平均电价)\n" + f"日期序列: {trend_dates}\n" + f"各司趋势: {json.dumps(trend_text, ensure_ascii=False)}\n\n" + "请根据以上数据进行深度分析:\n" + "1. **现状对比**:对比我司(驿来特)与竞对在不同时段的电价水平,找出我司偏高或偏低的关键时段。\n" + "2. **趋势洞察**:分析最近 3 天各供应商的价格调整动态,判断市场整体是在涨价、降价还是保持稳定,我司的反应是否及时。\n" + "3. **问题诊断**:指出我司目前定价中存在的潜在风险(如价格倒挂、错失高峰收益、低谷缺乏竞争力等)。\n" + "4. **优化方案**:给出 2-3 条具体的、可落地的调价建议,并说明理由。\n\n" + "要求:\n" + "- 使用专业、客观的语气。\n" + "- 采用 Markdown 格式,适当使用加粗和表格。\n" + "- 回答控制在 800-1000 字以内。" + ) + + # 清空之前的提示信息,开始正式输出 AI 内容 + yield "---CLEAR_PREVIOUS_HINTS---\n" + + chunk_count = 0 + # 使用 asyncio.wait_for 防止 LLM 请求完全死掉 + try: + # 某些时候 LLM 可能会卡住,设置一个合理的整体超时 + async for chunk in get_llm_response( + prompt, + stream=True, + system_prompt="你是熟悉中国充电桩行业的电价策略分析顾问。", + ): + chunk_count += 1 + if chunk_count == 1: + print("收到LLM首个chunk") + yield chunk + except Exception as llm_e: + print(f"LLM请求异常: {str(llm_e)}") + yield f"\n\n**AI 分析服务异常**: {str(llm_e)}。这可能是由于大模型服务商(如 DeepSeek)响应过慢或连接中断导致的。" + return + + print(f"AI分析完成,共发送 {chunk_count} 个chunks") + except Exception as e: + error_msg = f"\n\n**分析过程出现严重错误**: {str(e)}" + print(error_msg) + yield error_msg + + return StreamingResponse( + generate_stream(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + "Content-Type": "text/event-stream; charset=utf-8" + } + ) @router.get("/api/ylt/stations", response_model=List[StationBase]) diff --git a/Controller/__pycache__/YltAnalyticsController.cpython-310.pyc b/Controller/__pycache__/YltAnalyticsController.cpython-310.pyc index bcfba8a6e52c279d0e3dadaff31ad74ec6459f69..e94bf766eeb0a3d4ac445b06dd05cd7c1f2eb426 100644 GIT binary patch delta 5274 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 Ok!!Y+O!m?m+kXIvuqV|3 delta 2461 zcmZWqdr(tX8o%e}L7<4Jm8z{U6sZP9kd%sw*p`Ai+-}&C( zIp25g*-p}2m*AK)Cr*GTKc&ig>EW>iQ&PPamVbrUxd@<S%lYPC{~WRPN8H90~}tckmRRH)O{ z>K+k5JL@jsKxT0z)C39841|>kMu2Jtz;-X)UN65q6`pESybVgvQPq7|ZtqdLk1DOc zh;Kyo^~x<%>Ohk`>6eFEa_*Fmwky38vbz~j?HUgUPRs@e;;Ii4L5PbSIiUj8*L7B$Q?~``$=@@8vtWP$YyBSSxh^ibt4k>|!;%R{l757WvR`$LXKG`aFcpy2&H!imyiM)79 z9_<4@m@B8eK+N>|W?t!&J3TY6w$1b(oDI6er}xWm>{0sKl)lq)V|(QI_{E0CaD4!Z z391369BATB-D*d(I=XK*2>%{=azYLCL*SfDlQJ{{k(7OJMn*gkCLHXOJw0+@D&q59 zY}f;v5!j>toJ^Q0%>{o$$Jlj z0cvnR+sqGH9*a5w_wwKYb+k)qdsB8#Ky`W9+45*FxRRUqf|RwVmMt-AX)zvpF2bD% za{!F`ke1ofmdp+5tEj_Pim@WW zzxh`TI{h!KaoN`S2MH}jcZ3?EKkaNxw}8>~)OZ{LvB4lF+(JA3N7Ca+N;hz#ZP<1LT75JD_Vdb&dj~*F z!BU`@-;4&psH6X#b?Hr#)#)bgWD z3B_CN&T48YH#;k=R(5%HZc<45jMHHc>7;5~m6PUy52J>5v-Mdix_4k^iMSn6RVYyL>=Y<$fca?Y==DIsf7ovuS0 zowKUa>Y`_n!K1}c57Orm-s2ioX6kZ=$WHn`t2C`5O|0LviG0j1m?GQXoxMH`ZdWwppmzWufQL*T)DvqZs-L()=bXK~;@adM+CE44a>8B3@RUn?)}8|CIZ< zNRF@%*6tw3*s{E3#%Z)0L*P5W&xD`T1>|8^dKbbmwmUCpQAks1vC~DsFy6+Spj7j; z#X-kdATN!C*gx{Ti$la&YOq=Dvo0Tw?W1-3$qXxBFU|c98xp@H zI1%(~_Tl=c4AZcL`RV4Td;f-;=l%pr-$j)YL^|uI8$@yw9~FLg`&oT{(!vQ0a01~Z z!f}Lg|J(T~WV0LS5V#D*IIC?oGkxBeNT*TGYrcpS@2dZC;YUH#4^llWu^L4j(zxd< zUFB8OT48ZUOSxgs{oC*9Be}Q-^jn0>0LJ)e=+BYCI|kkSpD9Z&T(XR}12z;5BkV`` z9^nUsHiRE}1&+d{WCd$2+(<-rzVN-gf1~IZ1pKhl{~)AZgWHk10&qVT{VNbPCZ~&9 z%(l*ZGGpjf6bnpKv{MhCc!9dv?xGw6MuXP)o*gPmCgtp0(c=$ypyEpaFp_*bc_r5i z<0pH=cAz@HleN(sj912s(qbH*poY~H8_8DoTJg$s9dO~KHly9|r#6bww$Rs*Rfh1c z|6j$$gq&p?wj`HzA_p%4>P5iWq=yjxj=(R$X{7k9U7uk-YrKH3Q96r&t;1V^ikKUw T!m{Vx%+7Clklf0Ww`TkX9}Pz8 diff --git a/Util/LlmUtil.py b/Util/LlmUtil.py index 2d2f6df..46bbf8a 100644 --- a/Util/LlmUtil.py +++ b/Util/LlmUtil.py @@ -45,7 +45,7 @@ async def get_llm_response(query_text: str, stream: bool = True, system_prompt: request_params['temperature'] = temperature # 创建请求 - completion = await client.chat.completions.create(**request_params) + completion = await asyncio.wait_for(client.chat.completions.create(**request_params), timeout=60.0) if stream: # 流式输出模式,返回生成器 diff --git a/Util/__pycache__/LlmUtil.cpython-310.pyc b/Util/__pycache__/LlmUtil.cpython-310.pyc index 69b5a82d2467b0da253ad5ba549d2a91187f437f..36a4f515b20037a57584a7b58ac1b454dd98514f 100644 GIT binary patch delta 511 zcmYk2O;1xn6o$_kd%r(QY0*G*VS-A-hCm_r zI7wqskp^}Smqf*8hgeLdcWH4%5JXwVCwPq1R?+)tfCH$OzWpL^No6PhLV#B*Rm>cb zw41Ci^t|;P@9D%^o_6Z{hn+^kGIvl<#O5~RwL)XJE^7LVrI%}dgSES@rbt*Pvl_eo z&K{pNG+xT(v5^d$nb+i;@y+)B4xh>M8Qrzt;jI2?zp$z%sObmJ(v!KoyRgMri{HMbSpaDZn?=G7T^4aKofV|k zX67fORvtoMI>OO+Ck&)3Tx#y4?&h?!sYBgr9fEM@0bzG+5Eg)rm~$X3b|EYcz0^=W zf1y{YuR-gjCp>l|ynoR98b%$H_Hm8Tlnn5O!#4!!Ba|Dd#~X!T!5g?EGSCkQcvIL+ z{VGA-6F~(}k($y!@(=I;dU7wHWRa*TV;Ec1nK5K$3L7gtQ6GjHN}NKxwG$P2Dc(+u z!p8pgChO3@%NkPY)RJH^v5SnwY-ww6o#oR!uO7^0Y*i#XVRWY*HIp6a>PlT^HI5_J UmRe&iidQ)6t8M&=ddhZw17?PGzW@LL diff --git a/static/dashboard.html b/static/dashboard.html index 673f2c0..1d118fa 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -5,6 +5,7 @@ 分时电价分析 - 驿来特AI智能大脑 +
@@ -82,7 +83,7 @@ 🤖 AI 调价策略建议
-
+
AI 正在深度分析中
@@ -95,9 +96,9 @@ - - - + + + diff --git a/static/js/dashboard.js b/static/js/dashboard.js index cf112d0..329ccd6 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -283,11 +283,20 @@ createApp({ const reader = response.body.getReader(); const decoder = new TextDecoder(); + let fullText = ""; while (true) { const {done, value} = await reader.read(); if (done) break; const chunk = decoder.decode(value, {stream: true}); - aiText.value += chunk; + fullText += chunk; + + if (fullText.includes("---CLEAR_PREVIOUS_HINTS---")) { + const parts = fullText.split("---CLEAR_PREVIOUS_HINTS---"); + aiText.value = parts[parts.length - 1]; + } else { + aiText.value = fullText; + } + await Vue.nextTick(); if (aiBoxRef.value) { aiBoxRef.value.scrollTop = aiBoxRef.value.scrollHeight; @@ -325,7 +334,7 @@ createApp({ }); } - // 降级用的简易 Markdown 解析器 + // 降级用的简易 Markdown 解析器 (增强版) const simpleMarkdown = (text) => { if (!text) return ''; let lines = text.split('\n'); @@ -334,6 +343,7 @@ createApp({ const parseInline = (str) => { return str .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') .replace(/`(.*?)`/g, '$1'); }; for (let line of lines) { @@ -343,13 +353,28 @@ createApp({ html += '
'; continue; } - if (trimmed.startsWith('### ')) { + if (trimmed.startsWith('# ')) { + if (inList) { html += ''; inList = false; } + html += `

${parseInline(trimmed.substring(2))}

`; + } else if (trimmed.startsWith('## ')) { + if (inList) { html += ''; inList = false; } + html += `

${parseInline(trimmed.substring(3))}

`; + } else if (trimmed.startsWith('### ')) { if (inList) { html += ''; inList = false; } html += `

${parseInline(trimmed.substring(4))}

`; } else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { if (!inList) { html += '
    '; inList = true; } let content = trimmed.replace(/^(- |\d+\. )/, ''); html += `
  • ${parseInline(content)}
  • `; + } else if (trimmed.startsWith('|') && trimmed.endsWith('|')) { + // 简单的表格识别 (仅作示意,复杂表格仍需 marked) + if (inList) { html += '
'; inList = false; } + let cells = trimmed.split('|').filter(c => c.trim() || c === ''); + html += '
'; + cells.forEach(c => { + html += `
${parseInline(c.trim())}
`; + }); + html += '
'; } else { if (inList) { html += ''; inList = false; } html += `

${parseInline(trimmed)}

`; @@ -363,44 +388,45 @@ createApp({ const renderMarkdownAndLatex = (text) => { if (!text) return ''; + // 如果包含提示信息且还没被清除,先简单处理显示 + if (text.includes('正在收集各供应商价格数据') || text.includes('正在进行 AI 深度分析')) { + return `
${text.replace(/\n/g, '
')}
`; + } + try { - // 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $) let processedText = text; - // 处理块级 LaTeX: $$ ... $$ - processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => { - try { - if (typeof katex !== 'undefined') { + // 1. 处理 LaTeX (如果 katex 加载成功) + if (typeof katex !== 'undefined') { + // 处理块级 LaTeX: $$ ... $$ + processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => { + try { return '
' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '
'; - } - return match; - } catch (e) { - return match; - } - }); - - // 处理行内 LaTeX: $ ... $ - processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { - try { - if (typeof katex !== 'undefined') { + } catch (e) { return match; } + }); + + // 处理行内 LaTeX: $ ... $ + processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { + try { return katex.renderToString(formula, { displayMode: false, throwOnError: false }); - } - return match; - } catch (e) { - return match; - } - }); - - // 2. 使用 marked 解析 Markdown - if (typeof marked !== 'undefined') { - return marked.parse(processedText); - } else { - // 降级使用之前的 simpleMarkdown - return simpleMarkdown(processedText); + } catch (e) { return match; } + }); } + + // 2. 使用 marked 解析 Markdown (优先使用) + if (typeof marked !== 'undefined') { + // 兼容不同版本的 marked (UMD 版本中 marked 可能是函数或对象) + const parseFn = (typeof marked.parse === 'function') ? marked.parse.bind(marked) : (typeof marked === 'function' ? marked : null); + if (parseFn) { + return parseFn(processedText); + } + } + + // 3. 降级使用改进后的 simpleMarkdown + return simpleMarkdown(processedText); } catch (e) { - console.error('Markdown/LaTeX rendering error:', e); - return text; + console.error('Markdown rendering error:', e); + return simpleMarkdown(text); } };