feat: 四层架构全面增强

安全与稳定性:
- 移除硬编码 API Key,改用 .env + 环境变量
- LLM 调用统一重试机制(指数退避,3 次重试,处理 429/5xx/超时)
- 中文字体检测增强(CJK 关键词兜底 + 无字体时英文 fallback)
- 缺失 API Key 给出友好提示而非崩溃

分析能力提升:
- 异常检测新增 z-score 检测(标准差>2 标记异常)
- 新增变异系数 CV 检测(数据波动性)
- 新增零值/缺失检测
- 上下文管理器升级为关键词语义匹配(替代简单取最近 2 条)

用户体验:
- 报告自动保存为 Markdown(reports/ 目录)
- 新增 export 命令导出查询结果为 CSV
- 新增 reports 命令查看已保存报告
- CLI 支持 readline 命令历史(方向键翻阅)
- CSV 导入工具重写:自动列名映射、容错处理、dry-run 模式
- 新增 .env.example 配置模板
This commit is contained in:
openclaw
2026-03-31 14:39:17 +08:00
parent b7a27b12bd
commit e8f8e2f1ba
14 changed files with 588 additions and 115 deletions

View File

@@ -12,11 +12,12 @@ import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from core.config import LLM_CONFIG
from core.utils import get_llm_client, extract_json_array
from core.utils import get_llm_client, llm_chat, extract_json_array
from layers.explorer import ExplorationStep
def _setup_chinese_font():
"""尝试加载中文字体,找不到时用英文显示(不崩溃)"""
candidates = [
"SimHei", "Microsoft YaHei", "STHeiti", "WenQuanYi Micro Hei",
"Noto Sans CJK SC", "PingFang SC", "Source Han Sans CN",
@@ -27,10 +28,27 @@ def _setup_chinese_font():
plt.rcParams["font.sans-serif"] = [font]
plt.rcParams["axes.unicode_minus"] = False
return font
# 兜底:尝试找任何 CJK 字体
for f in fm.fontManager.ttflist:
if any(kw in f.name.lower() for kw in ("cjk", "chinese", "hei", "song", "ming", "fang")):
plt.rcParams["font.sans-serif"] = [f.name]
plt.rcParams["axes.unicode_minus"] = False
return f.name
plt.rcParams["axes.unicode_minus"] = False
return None
return None # 后续图表标题会用英文 fallback
_setup_chinese_font()
_CN_FONT = _setup_chinese_font()
def _safe_title(title: str) -> str:
"""无中文字体时将标题转为安全显示文本"""
if _CN_FONT:
return title
# 简单映射:中文→拼音首字母摘要,保留英文和数字
import re
clean = re.sub(r'[^\w\s.,;:!?%/()\-+]', '', title)
return clean if clean.strip() else "Chart"
CHART_PLAN_PROMPT = """你是一个数据可视化专家。根据以下分析结果,规划需要生成的图表。
@@ -97,15 +115,15 @@ class ChartGenerator:
)
try:
response = self.client.chat.completions.create(
model=self.model,
content = llm_chat(
self.client, self.model,
messages=[
{"role": "system", "content": "你是数据可视化专家。只输出纯 JSON 数组,不要 markdown 代码块。"},
{"role": "user", "content": CHART_PLAN_PROMPT.format(exploration_summary="\n\n".join(summary_parts))},
],
temperature=0.1, max_tokens=1024,
)
plans = extract_json_array(response.choices[0].message.content.strip())
plans = extract_json_array(content)
return plans if plans else self._fallback_plan(valid_steps)
except Exception as e:
print(f" ⚠️ 图表规划失败: {e},使用 fallback")
@@ -206,11 +224,11 @@ class ChartGenerator:
ax.set_xticklabels(x_vals, rotation=45, ha="right", fontsize=9)
ax.legend()
ax.set_title(title, fontsize=13, fontweight="bold", pad=12)
ax.set_title(_safe_title(title), fontsize=13, fontweight="bold", pad=12)
if chart_type not in ("pie",):
ax.set_xlabel(x_col, fontsize=10)
ax.set_xlabel(_safe_title(x_col), fontsize=10)
if chart_type != "horizontal_bar":
ax.set_ylabel(y_col, fontsize=10)
ax.set_ylabel(_safe_title(y_col), fontsize=10)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()