feat(report): 支持 Minimax AI 后端 + 统一 AI 配置对话框,修复 figure_counter 返回值断链 Bug
This commit is contained in:
@ -9,14 +9,15 @@ import traceback
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
||||
QLabel, QCheckBox, QPushButton, QLineEdit, QSpinBox,
|
||||
QLabel, QCheckBox, QPushButton, QLineEdit,
|
||||
QMessageBox, QFileDialog,
|
||||
)
|
||||
|
||||
from src.gui.styles import ModernStylesheet
|
||||
from src.gui.dialogs import AISettingsDialog, AI_SETTINGS_ORG, AI_SETTINGS_APP
|
||||
|
||||
|
||||
class ReportGenerateThread(QThread):
|
||||
@ -25,35 +26,43 @@ class ReportGenerateThread(QThread):
|
||||
failed = pyqtSignal(str)
|
||||
log_message = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, options: dict):
|
||||
def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, enable_ai: bool):
|
||||
super().__init__()
|
||||
self.work_dir = work_dir
|
||||
self.output_dir = output_dir
|
||||
self.report_title = report_title
|
||||
self.options = options
|
||||
self.enable_ai = enable_ai
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
from src.postprocessing.report_word import WaterQualityReportGenerator, ReportGenerationConfig
|
||||
|
||||
url = (self.options.get("ollama_url") or "").strip() or None
|
||||
vision = (self.options.get("ollama_vision_model") or "").strip() or None
|
||||
text = (self.options.get("ollama_text_model") or "").strip() or None
|
||||
if self.options.get("text_same_as_vision"):
|
||||
text = vision
|
||||
timeout = self.options.get("ollama_timeout_s")
|
||||
enable_ai = self.options.get("enable_ai_analysis")
|
||||
# 唯一数据源:直接从 QSettings 读取 AI 配置
|
||||
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||
provider = s.value("ai_provider", "minimax", type=str)
|
||||
timeout = int(s.value("timeout_s", 120, type=int))
|
||||
|
||||
if provider == "ollama":
|
||||
ai_cfg = ReportGenerationConfig(
|
||||
ai_provider="ollama",
|
||||
ollama_base_url=s.value("api_base_url", "", type=str) or None,
|
||||
ollama_vision_model=s.value("vision_model", "", type=str) or None,
|
||||
ollama_text_model=s.value("text_model", "", type=str) or None,
|
||||
ollama_timeout_s=timeout,
|
||||
enable_ai_analysis=self.enable_ai,
|
||||
)
|
||||
else:
|
||||
ai_cfg = ReportGenerationConfig(
|
||||
ai_provider="minimax",
|
||||
minimax_api_key=s.value("api_key", "", type=str) or "",
|
||||
minimax_vision_model=s.value("vision_model", "", type=str) or None,
|
||||
minimax_text_model=s.value("text_model", "", type=str) or None,
|
||||
minimax_timeout_s=timeout,
|
||||
enable_ai_analysis=self.enable_ai,
|
||||
)
|
||||
|
||||
ai_cfg = ReportGenerationConfig(
|
||||
ollama_base_url=url,
|
||||
ollama_vision_model=vision,
|
||||
ollama_text_model=text,
|
||||
ollama_timeout_s=int(timeout) if timeout is not None else None,
|
||||
enable_ai_analysis=bool(enable_ai),
|
||||
)
|
||||
self.log_message.emit(
|
||||
f"报告生成:工作目录={self.work_dir},AI={'开' if enable_ai else '关'},"
|
||||
f"模型URL={url or '(环境变量 OLLAMA_URL)'}",
|
||||
f"报告生成:工作目录={self.work_dir},AI={'开' if self.enable_ai else '关'},Provider={provider}",
|
||||
"info",
|
||||
)
|
||||
gen = WaterQualityReportGenerator(
|
||||
@ -71,12 +80,13 @@ class ReportGenerateThread(QThread):
|
||||
|
||||
|
||||
class ReportGenerationPanel(QWidget):
|
||||
"""Word 报告生成:工作目录、输出目录、Ollama URL/模型、是否启用 AI 等。"""
|
||||
"""Word 报告生成面板。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。"""
|
||||
|
||||
def __init__(self, main_window=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.main_window = main_window
|
||||
self._report_thread = None
|
||||
self._ai_label = None
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
@ -86,7 +96,7 @@ class ReportGenerationPanel(QWidget):
|
||||
|
||||
intro = QLabel(
|
||||
"根据工作目录下的可视化结果(14_visualization 等)生成 Word 分析报告。"
|
||||
"需已存在可视化图表;AI 分析通过 Ollama /api/chat 调用本地或远程服务。"
|
||||
"需已存在可视化图表;AI 分析通过 Ollama 或 Minimax 调用云端/本地服务。"
|
||||
)
|
||||
intro.setWordWrap(True)
|
||||
intro.setStyleSheet(
|
||||
@ -94,6 +104,7 @@ class ReportGenerationPanel(QWidget):
|
||||
)
|
||||
layout.addWidget(intro)
|
||||
|
||||
# ── 路径 ──────────────────────────────────────────────────────────────
|
||||
path_group = QGroupBox("路径")
|
||||
path_form = QFormLayout()
|
||||
|
||||
@ -125,52 +136,32 @@ class ReportGenerationPanel(QWidget):
|
||||
path_group.setLayout(path_form)
|
||||
layout.addWidget(path_group)
|
||||
|
||||
ai_group = QGroupBox("AI 分析(Ollama)")
|
||||
ai_form = QFormLayout()
|
||||
# ── AI 分析 ───────────────────────────────────────────────────────────
|
||||
ai_group = QGroupBox("AI 分析")
|
||||
ai_layout = QVBoxLayout()
|
||||
|
||||
self.enable_ai_cb = QCheckBox("启用 AI 图表解读与综合总结")
|
||||
self.enable_ai_cb.setChecked(
|
||||
os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {"0", "false", "False"}
|
||||
)
|
||||
ai_form.addRow(self.enable_ai_cb)
|
||||
ai_layout.addWidget(self.enable_ai_cb)
|
||||
|
||||
self.ollama_url_edit = QLineEdit()
|
||||
self.ollama_url_edit.setText(
|
||||
os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/")
|
||||
)
|
||||
ai_form.addRow("服务 URL:", self.ollama_url_edit)
|
||||
# 只读提示行:当前引擎 + 配置按钮
|
||||
ai_status_row = QHBoxLayout()
|
||||
ai_status_row.addWidget(QLabel("当前 AI 引擎:"))
|
||||
self._ai_label = QLabel()
|
||||
self._ai_label.setStyleSheet("color: #0078d4; font-weight: bold;")
|
||||
ai_status_row.addWidget(self._ai_label)
|
||||
ai_status_row.addStretch(1)
|
||||
open_settings_btn = QPushButton("高级配置...")
|
||||
open_settings_btn.clicked.connect(self._open_ai_settings)
|
||||
ai_status_row.addWidget(open_settings_btn)
|
||||
ai_layout.addLayout(ai_status_row)
|
||||
|
||||
self.vision_model_edit = QLineEdit()
|
||||
self.vision_model_edit.setText(
|
||||
os.environ.get("OLLAMA_VISION_MODEL", "qwen3-vl:8b")
|
||||
)
|
||||
ai_form.addRow("视觉模型:", self.vision_model_edit)
|
||||
|
||||
self.same_text_model_cb = QCheckBox("文本总结与视觉使用同一模型")
|
||||
self.same_text_model_cb.setChecked(True)
|
||||
ai_form.addRow(self.same_text_model_cb)
|
||||
|
||||
self.text_model_edit = QLineEdit()
|
||||
self.text_model_edit.setText(
|
||||
os.environ.get(
|
||||
"OLLAMA_TEXT_MODEL",
|
||||
self.vision_model_edit.text() or "qwen3-vl:8b"
|
||||
)
|
||||
)
|
||||
self.text_model_edit.setEnabled(False)
|
||||
self.same_text_model_cb.toggled.connect(self._on_same_text_toggled)
|
||||
self.vision_model_edit.textChanged.connect(self._sync_text_model_if_linked)
|
||||
ai_form.addRow("文本模型:", self.text_model_edit)
|
||||
|
||||
self.timeout_spin = QSpinBox()
|
||||
self.timeout_spin.setRange(30, 3600)
|
||||
self.timeout_spin.setSingleStep(30)
|
||||
self.timeout_spin.setValue(int(os.environ.get("OLLAMA_TIMEOUT_S", "120")))
|
||||
ai_form.addRow("请求超时(秒):", self.timeout_spin)
|
||||
|
||||
ai_group.setLayout(ai_form)
|
||||
ai_group.setLayout(ai_layout)
|
||||
layout.addWidget(ai_group)
|
||||
|
||||
# ── 按钮 ──────────────────────────────────────────────────────────────
|
||||
btn_row = QHBoxLayout()
|
||||
self.generate_btn = QPushButton("生成 Word 报告")
|
||||
self.generate_btn.setStyleSheet(
|
||||
@ -184,16 +175,21 @@ class ReportGenerationPanel(QWidget):
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def _on_same_text_toggled(self, checked: bool):
|
||||
self.text_model_edit.setEnabled(not checked)
|
||||
if checked:
|
||||
self.text_model_edit.setText(self.vision_model_edit.text())
|
||||
# 刷新引擎提示文字
|
||||
self._refresh_ai_label()
|
||||
|
||||
def _sync_text_model_if_linked(self, _t=None):
|
||||
if self.same_text_model_cb.isChecked():
|
||||
self.text_model_edit.blockSignals(True)
|
||||
self.text_model_edit.setText(self.vision_model_edit.text())
|
||||
self.text_model_edit.blockSignals(False)
|
||||
def _refresh_ai_label(self):
|
||||
"""从 QSettings 读取当前 Provider 并更新只读标签。"""
|
||||
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||
provider = s.value("ai_provider", "minimax", type=str)
|
||||
label_map = {"ollama": "Ollama (本地)", "minimax": "Minimax (云端)"}
|
||||
self._ai_label.setText(label_map.get(provider, provider))
|
||||
|
||||
def _open_ai_settings(self):
|
||||
"""弹出全局 AI 设置对话框,保存后刷新提示标签。"""
|
||||
dlg = AISettingsDialog(self)
|
||||
if dlg.exec_() == dlg.Accepted:
|
||||
self._refresh_ai_label()
|
||||
|
||||
def _get_default_work_dir(self):
|
||||
"""获取 work_dir,优先用主窗口缓存的 work_dir"""
|
||||
@ -227,15 +223,11 @@ class ReportGenerationPanel(QWidget):
|
||||
self.work_dir_edit.setText(str(work_dir))
|
||||
|
||||
def get_config(self):
|
||||
"""返回路径和标题配置(AI 配置不由本面板持有)。"""
|
||||
return {
|
||||
"work_dir": self.work_dir_edit.text().strip() or None,
|
||||
"output_dir": self.output_dir_edit.text().strip() or None,
|
||||
"report_title": self.report_title_edit.text().strip() or "水质参数反演分析报告",
|
||||
"ollama_url": self.ollama_url_edit.text().strip(),
|
||||
"ollama_vision_model": self.vision_model_edit.text().strip(),
|
||||
"ollama_text_model": self.text_model_edit.text().strip(),
|
||||
"text_same_as_vision": self.same_text_model_cb.isChecked(),
|
||||
"ollama_timeout_s": self.timeout_spin.value(),
|
||||
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
||||
}
|
||||
|
||||
@ -248,16 +240,6 @@ class ReportGenerationPanel(QWidget):
|
||||
self.output_dir_edit.setText(str(config["output_dir"] or ""))
|
||||
if config.get("report_title"):
|
||||
self.report_title_edit.setText(str(config["report_title"]))
|
||||
if config.get("ollama_url"):
|
||||
self.ollama_url_edit.setText(str(config["ollama_url"]))
|
||||
if config.get("ollama_vision_model"):
|
||||
self.vision_model_edit.setText(str(config["ollama_vision_model"]))
|
||||
if "text_same_as_vision" in config:
|
||||
self.same_text_model_cb.setChecked(bool(config["text_same_as_vision"]))
|
||||
if config.get("ollama_text_model"):
|
||||
self.text_model_edit.setText(str(config["ollama_text_model"]))
|
||||
if config.get("ollama_timeout_s") is not None:
|
||||
self.timeout_spin.setValue(int(config["ollama_timeout_s"]))
|
||||
if "enable_ai_analysis" in config:
|
||||
self.enable_ai_cb.setChecked(bool(config["enable_ai_analysis"]))
|
||||
|
||||
@ -280,16 +262,10 @@ class ReportGenerationPanel(QWidget):
|
||||
|
||||
out = self.output_dir_edit.text().strip() or None
|
||||
title = self.report_title_edit.text().strip() or "水质参数反演分析报告"
|
||||
opts = {
|
||||
"ollama_url": self.ollama_url_edit.text().strip(),
|
||||
"ollama_vision_model": self.vision_model_edit.text().strip(),
|
||||
"ollama_text_model": self.text_model_edit.text().strip(),
|
||||
"text_same_as_vision": self.same_text_model_cb.isChecked(),
|
||||
"ollama_timeout_s": self.timeout_spin.value(),
|
||||
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
||||
}
|
||||
enable_ai = self.enable_ai_cb.isChecked()
|
||||
|
||||
self.generate_btn.setEnabled(False)
|
||||
self._report_thread = ReportGenerateThread(wd, out, title, opts)
|
||||
self._report_thread = ReportGenerateThread(wd, out, title, enable_ai)
|
||||
self._report_thread.log_message.connect(self._forward_log, Qt.QueuedConnection)
|
||||
self._report_thread.finished_ok.connect(self._on_report_ok, Qt.QueuedConnection)
|
||||
self._report_thread.failed.connect(self._on_report_fail, Qt.QueuedConnection)
|
||||
@ -312,4 +288,4 @@ class ReportGenerationPanel(QWidget):
|
||||
|
||||
def _on_report_fail(self, err: str):
|
||||
QMessageBox.critical(self, "失败", f"报告生成失败:\n{err[:800]}")
|
||||
self._forward_log(err, "error")
|
||||
self._forward_log(err, "error")
|
||||
Reference in New Issue
Block a user