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

@@ -9,10 +9,11 @@ Layer 4: Context 上下文记忆
Output: Reporter + Chart + Consolidator
"""
import os
import time
from typing import Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
from core.config import DB_PATH, MAX_EXPLORATION_ROUNDS, PLAYBOOK_DIR, CHARTS_DIR
from core.config import DB_PATH, MAX_EXPLORATION_ROUNDS, PLAYBOOK_DIR, CHARTS_DIR, PROJECT_ROOT
from core.schema import extract_schema, schema_to_text
from core.sandbox import SandboxExecutor
from layers.planner import Planner
@@ -47,6 +48,10 @@ class DataAnalysisAgent:
# 累积图表
self._all_charts: list[dict] = []
# 报告输出目录
self.reports_dir = os.path.join(PROJECT_ROOT, "reports")
os.makedirs(self.reports_dir, exist_ok=True)
# 自动生成 Playbook
if not self.playbook_mgr.playbooks:
print("\n🤖 [Playbook] 未发现预设剧本AI 自动生成中...")
@@ -145,6 +150,9 @@ class DataAnalysisAgent:
# Layer 4: 记录上下文
self.context.add_session(question=question, plan=plan, steps=steps, insights=insights, report=report)
# 自动保存报告
self.save_report(report, question, charts=charts)
return report
def full_report(self, question: str = "") -> str:
@@ -173,3 +181,54 @@ class DataAnalysisAgent:
def close(self):
"""释放资源"""
self.executor.close()
def save_report(self, report: str, question: str, charts: list[dict] | None = None) -> str:
"""将报告保存为 Markdown 文件,返回文件路径"""
ts = time.strftime("%Y%m%d_%H%M%S")
# 取问题前 20 字作为文件名
import re
safe_q = re.sub(r'[^\w\u4e00-\u9fff]', '_', question)[:20].strip('_')
fname = f"{ts}_{safe_q}.md"
fpath = os.path.join(self.reports_dir, fname)
with open(fpath, "w", encoding="utf-8") as f:
f.write(f"# 分析报告: {question}\n\n")
f.write(f"_生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}_\n\n")
f.write(report)
if charts:
f.write("\n\n---\n\n## 📊 图表索引\n\n")
for c in charts:
f.write(f"### {c['title']}\n![{c['title']}]({os.path.abspath(c['path'])})\n\n")
print(f" 💾 报告已保存: {fpath}")
return fpath
def export_data(self, steps: list, format: str = "csv") -> str | None:
"""导出探索结果为 CSV"""
import csv
import io
all_rows = []
all_cols = set()
for step in steps:
if step.success and step.rows:
for row in step.rows:
row["_query"] = step.purpose
all_rows.append(row)
all_cols.update(row.keys())
if not all_rows:
return None
ts = time.strftime("%Y%m%d_%H%M%S")
fname = f"export_{ts}.csv"
fpath = os.path.join(self.reports_dir, fname)
cols = sorted(all_cols)
with open(fpath, "w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=cols)
writer.writeheader()
writer.writerows(all_rows)
print(f" 📁 数据已导出: {fpath} ({len(all_rows)} 行)")
return fpath