Committed by
GitHub
Add final report download button (#329)
* fix(app): 改进应用健康检查机制并更新默认配置 添加专用的健康检查路径和代理配置,重构健康检查URL构建逻辑 增加健康检查失败时的日志记录 延长应用启动等待时间至90秒 * style(templates): 统一CSS选择器缩进格式并修复空格问题 * feat(报告下载): 实现报告文件下载功能并增强任务状态管理 - 在ReportAgent中修改generate_report返回包含文件路径的字典 - 在ReportTask中添加文件路径相关字段 - 新增/download接口用于下载报告文件 - 在前端添加下载按钮及相关控制逻辑 - 完善任务状态显示,增加文件路径信息 * feat(report): 添加报告下载功能并优化状态管理 - 在ReportAgent中返回报告文件保存路径信息 - 新增Flask接口/download/<task_id>用于下载报告文件 - 在前端添加下载按钮及相关控制逻辑 - 修复报告生成状态重置问题 - 优化健康检查URL构建和代理设置 - 统一CSS样式中的空格和缩进 --------- Co-authored-by: HKLHaoBin <we3q@qq.com> Co-authored-by: Zhang Yuxiang <51037789+NTFago@users.noreply.github.com>
Showing
5 changed files
with
361 additions
and
72 deletions
BettaFish.bat
0 → 100644
| @@ -190,10 +190,16 @@ class ReportAgent: | @@ -190,10 +190,16 @@ class ReportAgent: | ||
| 190 | save_report: 是否保存报告到文件 | 190 | save_report: 是否保存报告到文件 |
| 191 | 191 | ||
| 192 | Returns: | 192 | Returns: |
| 193 | - 最终HTML报告内容 | 193 | + dict: 包含HTML内容与保存文件信息 |
| 194 | """ | 194 | """ |
| 195 | start_time = datetime.now() | 195 | start_time = datetime.now() |
| 196 | 196 | ||
| 197 | + # 为新的查询重置状态,确保文件命名信息完整 | ||
| 198 | + self.state = ReportState(query=query) | ||
| 199 | + self.state.metadata.query = query | ||
| 200 | + self.state.query = query | ||
| 201 | + self.state.mark_processing() | ||
| 202 | + | ||
| 197 | logger.info(f"开始生成报告: {query}") | 203 | logger.info(f"开始生成报告: {query}") |
| 198 | logger.info(f"输入数据 - 报告数量: {len(reports)}, 论坛日志长度: {len(forum_logs)}") | 204 | logger.info(f"输入数据 - 报告数量: {len(reports)}, 论坛日志长度: {len(forum_logs)}") |
| 199 | 205 | ||
| @@ -205,8 +211,9 @@ class ReportAgent: | @@ -205,8 +211,9 @@ class ReportAgent: | ||
| 205 | html_report = self._generate_html_report(query, reports, forum_logs, template_result) | 211 | html_report = self._generate_html_report(query, reports, forum_logs, template_result) |
| 206 | 212 | ||
| 207 | # Step 3: 保存报告 | 213 | # Step 3: 保存报告 |
| 214 | + saved_files = {} | ||
| 208 | if save_report: | 215 | if save_report: |
| 209 | - self._save_report(html_report) | 216 | + saved_files = self._save_report(html_report) |
| 210 | 217 | ||
| 211 | # 更新生成时间 | 218 | # 更新生成时间 |
| 212 | end_time = datetime.now() | 219 | end_time = datetime.now() |
| @@ -215,7 +222,10 @@ class ReportAgent: | @@ -215,7 +222,10 @@ class ReportAgent: | ||
| 215 | 222 | ||
| 216 | logger.info(f"报告生成完成,耗时: {generation_time:.2f} 秒") | 223 | logger.info(f"报告生成完成,耗时: {generation_time:.2f} 秒") |
| 217 | 224 | ||
| 218 | - return html_report | 225 | + return { |
| 226 | + 'html_content': html_report, | ||
| 227 | + **saved_files | ||
| 228 | + } | ||
| 219 | 229 | ||
| 220 | except Exception as e: | 230 | except Exception as e: |
| 221 | logger.exception(f"报告生成过程中发生错误: {str(e)}") | 231 | logger.exception(f"报告生成过程中发生错误: {str(e)}") |
| @@ -357,13 +367,26 @@ class ReportAgent: | @@ -357,13 +367,26 @@ class ReportAgent: | ||
| 357 | with open(filepath, 'w', encoding='utf-8') as f: | 367 | with open(filepath, 'w', encoding='utf-8') as f: |
| 358 | f.write(html_content) | 368 | f.write(html_content) |
| 359 | 369 | ||
| 360 | - logger.info(f"报告已保存到: {filepath}") | 370 | + abs_report_path = os.path.abspath(filepath) |
| 371 | + rel_report_path = os.path.relpath(abs_report_path, os.getcwd()) | ||
| 372 | + logger.info(f"报告已保存到: {abs_report_path}") | ||
| 361 | 373 | ||
| 362 | # 保存状态 | 374 | # 保存状态 |
| 363 | state_filename = f"report_state_{query_safe}_{timestamp}.json" | 375 | state_filename = f"report_state_{query_safe}_{timestamp}.json" |
| 364 | state_filepath = os.path.join(self.config.OUTPUT_DIR, state_filename) | 376 | state_filepath = os.path.join(self.config.OUTPUT_DIR, state_filename) |
| 365 | self.state.save_to_file(state_filepath) | 377 | self.state.save_to_file(state_filepath) |
| 366 | - logger.info(f"状态已保存到: {state_filepath}") | 378 | + abs_state_path = os.path.abspath(state_filepath) |
| 379 | + rel_state_path = os.path.relpath(abs_state_path, os.getcwd()) | ||
| 380 | + logger.info(f"状态已保存到: {abs_state_path}") | ||
| 381 | + | ||
| 382 | + return { | ||
| 383 | + 'report_filename': filename, | ||
| 384 | + 'report_filepath': abs_report_path, | ||
| 385 | + 'report_relative_path': rel_report_path, | ||
| 386 | + 'state_filename': state_filename, | ||
| 387 | + 'state_filepath': abs_state_path, | ||
| 388 | + 'state_relative_path': rel_state_path | ||
| 389 | + } | ||
| 367 | 390 | ||
| 368 | def get_progress_summary(self) -> Dict[str, Any]: | 391 | def get_progress_summary(self) -> Dict[str, Any]: |
| 369 | """获取进度摘要""" | 392 | """获取进度摘要""" |
| @@ -492,4 +515,4 @@ def create_agent(config_file: Optional[str] = None) -> ReportAgent: | @@ -492,4 +515,4 @@ def create_agent(config_file: Optional[str] = None) -> ReportAgent: | ||
| 492 | """ | 515 | """ |
| 493 | 516 | ||
| 494 | config = Settings() # 以空配置初始化,而从从环境变量初始化 | 517 | config = Settings() # 以空配置初始化,而从从环境变量初始化 |
| 495 | - return ReportAgent(config) | 518 | + return ReportAgent(config) |
| @@ -8,7 +8,7 @@ import json | @@ -8,7 +8,7 @@ import json | ||
| 8 | import threading | 8 | import threading |
| 9 | import time | 9 | import time |
| 10 | from datetime import datetime | 10 | from datetime import datetime |
| 11 | -from flask import Blueprint, request, jsonify, Response | 11 | +from flask import Blueprint, request, jsonify, Response, send_file |
| 12 | from typing import Dict, Any | 12 | from typing import Dict, Any |
| 13 | from loguru import logger | 13 | from loguru import logger |
| 14 | from .agent import ReportAgent, create_agent | 14 | from .agent import ReportAgent, create_agent |
| @@ -50,6 +50,11 @@ class ReportTask: | @@ -50,6 +50,11 @@ class ReportTask: | ||
| 50 | self.created_at = datetime.now() | 50 | self.created_at = datetime.now() |
| 51 | self.updated_at = datetime.now() | 51 | self.updated_at = datetime.now() |
| 52 | self.html_content = "" | 52 | self.html_content = "" |
| 53 | + self.report_file_path = "" | ||
| 54 | + self.report_file_relative_path = "" | ||
| 55 | + self.report_file_name = "" | ||
| 56 | + self.state_file_path = "" | ||
| 57 | + self.state_file_relative_path = "" | ||
| 53 | 58 | ||
| 54 | def update_status(self, status: str, progress: int = None, error_message: str = ""): | 59 | def update_status(self, status: str, progress: int = None, error_message: str = ""): |
| 55 | """更新任务状态""" | 60 | """更新任务状态""" |
| @@ -70,7 +75,10 @@ class ReportTask: | @@ -70,7 +75,10 @@ class ReportTask: | ||
| 70 | 'error_message': self.error_message, | 75 | 'error_message': self.error_message, |
| 71 | 'created_at': self.created_at.isoformat(), | 76 | 'created_at': self.created_at.isoformat(), |
| 72 | 'updated_at': self.updated_at.isoformat(), | 77 | 'updated_at': self.updated_at.isoformat(), |
| 73 | - 'has_result': bool(self.html_content) | 78 | + 'has_result': bool(self.html_content), |
| 79 | + 'report_file_ready': bool(self.report_file_path), | ||
| 80 | + 'report_file_name': self.report_file_name, | ||
| 81 | + 'report_file_path': self.report_file_relative_path | ||
| 74 | } | 82 | } |
| 75 | 83 | ||
| 76 | 84 | ||
| @@ -119,7 +127,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " | @@ -119,7 +127,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " | ||
| 119 | task.update_status("running", 50) | 127 | task.update_status("running", 50) |
| 120 | 128 | ||
| 121 | # 生成报告 | 129 | # 生成报告 |
| 122 | - html_report = report_agent.generate_report( | 130 | + generation_result = report_agent.generate_report( |
| 123 | query=query, | 131 | query=query, |
| 124 | reports=content['reports'], | 132 | reports=content['reports'], |
| 125 | forum_logs=content['forum_logs'], | 133 | forum_logs=content['forum_logs'], |
| @@ -127,10 +135,17 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " | @@ -127,10 +135,17 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " | ||
| 127 | save_report=True | 135 | save_report=True |
| 128 | ) | 136 | ) |
| 129 | 137 | ||
| 138 | + html_report = generation_result.get('html_content', '') | ||
| 139 | + | ||
| 130 | task.update_status("running", 90) | 140 | task.update_status("running", 90) |
| 131 | 141 | ||
| 132 | # 保存结果 | 142 | # 保存结果 |
| 133 | task.html_content = html_report | 143 | task.html_content = html_report |
| 144 | + task.report_file_path = generation_result.get('report_filepath', '') | ||
| 145 | + task.report_file_relative_path = generation_result.get('report_relative_path', '') | ||
| 146 | + task.report_file_name = generation_result.get('report_filename', '') | ||
| 147 | + task.state_file_path = generation_result.get('state_filepath', '') | ||
| 148 | + task.state_file_relative_path = generation_result.get('state_relative_path', '') | ||
| 134 | task.update_status("completed", 100) | 149 | task.update_status("completed", 100) |
| 135 | 150 | ||
| 136 | except Exception as e: | 151 | except Exception as e: |
| @@ -251,7 +266,10 @@ def get_progress(task_id: str): | @@ -251,7 +266,10 @@ def get_progress(task_id: str): | ||
| 251 | 'status': 'completed', | 266 | 'status': 'completed', |
| 252 | 'progress': 100, | 267 | 'progress': 100, |
| 253 | 'error_message': '', | 268 | 'error_message': '', |
| 254 | - 'has_result': True | 269 | + 'has_result': True, |
| 270 | + 'report_file_ready': False, | ||
| 271 | + 'report_file_name': '', | ||
| 272 | + 'report_file_path': '' | ||
| 255 | } | 273 | } |
| 256 | }) | 274 | }) |
| 257 | 275 | ||
| @@ -329,6 +347,44 @@ def get_result_json(task_id: str): | @@ -329,6 +347,44 @@ def get_result_json(task_id: str): | ||
| 329 | }), 500 | 347 | }), 500 |
| 330 | 348 | ||
| 331 | 349 | ||
| 350 | +@report_bp.route('/download/<task_id>', methods=['GET']) | ||
| 351 | +def download_report(task_id: str): | ||
| 352 | + """下载已生成的报告HTML文件""" | ||
| 353 | + try: | ||
| 354 | + if not current_task or current_task.task_id != task_id: | ||
| 355 | + return jsonify({ | ||
| 356 | + 'success': False, | ||
| 357 | + 'error': '任务不存在' | ||
| 358 | + }), 404 | ||
| 359 | + | ||
| 360 | + if current_task.status != "completed" or not current_task.report_file_path: | ||
| 361 | + return jsonify({ | ||
| 362 | + 'success': False, | ||
| 363 | + 'error': '报告尚未完成或尚未保存' | ||
| 364 | + }), 400 | ||
| 365 | + | ||
| 366 | + if not os.path.exists(current_task.report_file_path): | ||
| 367 | + return jsonify({ | ||
| 368 | + 'success': False, | ||
| 369 | + 'error': '报告文件不存在或已被删除' | ||
| 370 | + }), 404 | ||
| 371 | + | ||
| 372 | + download_name = current_task.report_file_name or os.path.basename(current_task.report_file_path) | ||
| 373 | + return send_file( | ||
| 374 | + current_task.report_file_path, | ||
| 375 | + mimetype='text/html', | ||
| 376 | + as_attachment=True, | ||
| 377 | + download_name=download_name | ||
| 378 | + ) | ||
| 379 | + | ||
| 380 | + except Exception as e: | ||
| 381 | + logger.exception(f"下载报告失败: {str(e)}") | ||
| 382 | + return jsonify({ | ||
| 383 | + 'success': False, | ||
| 384 | + 'error': str(e) | ||
| 385 | + }), 500 | ||
| 386 | + | ||
| 387 | + | ||
| 332 | @report_bp.route('/cancel/<task_id>', methods=['POST']) | 388 | @report_bp.route('/cancel/<task_id>', methods=['POST']) |
| 333 | def cancel_task(task_id: str): | 389 | def cancel_task(task_id: str): |
| 334 | """取消报告生成任务""" | 390 | """取消报告生成任务""" |
| @@ -478,4 +534,4 @@ def clear_log(): | @@ -478,4 +534,4 @@ def clear_log(): | ||
| 478 | return jsonify({ | 534 | return jsonify({ |
| 479 | 'success': False, | 535 | 'success': False, |
| 480 | 'error': f'清空日志失败: {str(e)}' | 536 | 'error': f'清空日志失败: {str(e)}' |
| 481 | - }), 500 | 537 | + }), 500 |
| @@ -656,6 +656,14 @@ def stop_streamlit_app(app_name): | @@ -656,6 +656,14 @@ def stop_streamlit_app(app_name): | ||
| 656 | except Exception as e: | 656 | except Exception as e: |
| 657 | return False, f"停止失败: {str(e)}" | 657 | return False, f"停止失败: {str(e)}" |
| 658 | 658 | ||
| 659 | +HEALTHCHECK_PATH = "/_stcore/health" | ||
| 660 | +HEALTHCHECK_PROXIES = {'http': None, 'https': None} | ||
| 661 | + | ||
| 662 | + | ||
| 663 | +def _build_healthcheck_url(port): | ||
| 664 | + return f"http://127.0.0.1:{port}{HEALTHCHECK_PATH}" | ||
| 665 | + | ||
| 666 | + | ||
| 659 | def check_app_status(): | 667 | def check_app_status(): |
| 660 | """检查应用状态""" | 668 | """检查应用状态""" |
| 661 | for app_name, info in processes.items(): | 669 | for app_name, info in processes.items(): |
| @@ -663,21 +671,24 @@ def check_app_status(): | @@ -663,21 +671,24 @@ def check_app_status(): | ||
| 663 | if info['process'].poll() is None: | 671 | if info['process'].poll() is None: |
| 664 | # 进程仍在运行,检查端口是否可访问 | 672 | # 进程仍在运行,检查端口是否可访问 |
| 665 | try: | 673 | try: |
| 666 | - response = requests.get(f"http://localhost:{info['port']}", timeout=2) | 674 | + response = requests.get( |
| 675 | + _build_healthcheck_url(info['port']), | ||
| 676 | + timeout=2, | ||
| 677 | + proxies=HEALTHCHECK_PROXIES | ||
| 678 | + ) | ||
| 667 | if response.status_code == 200: | 679 | if response.status_code == 200: |
| 668 | info['status'] = 'running' | 680 | info['status'] = 'running' |
| 669 | else: | 681 | else: |
| 670 | info['status'] = 'starting' | 682 | info['status'] = 'starting' |
| 671 | - except requests.exceptions.RequestException: | ||
| 672 | - info['status'] = 'starting' | ||
| 673 | - except Exception: | 683 | + except Exception as exc: |
| 684 | + logger.warning(f"{app_name} 健康检查失败: {exc}") | ||
| 674 | info['status'] = 'starting' | 685 | info['status'] = 'starting' |
| 675 | else: | 686 | else: |
| 676 | # 进程已结束 | 687 | # 进程已结束 |
| 677 | info['process'] = None | 688 | info['process'] = None |
| 678 | info['status'] = 'stopped' | 689 | info['status'] = 'stopped' |
| 679 | 690 | ||
| 680 | -def wait_for_app_startup(app_name, max_wait_time=30): | 691 | +def wait_for_app_startup(app_name, max_wait_time=90): |
| 681 | """等待应用启动完成""" | 692 | """等待应用启动完成""" |
| 682 | import time | 693 | import time |
| 683 | start_time = time.time() | 694 | start_time = time.time() |
| @@ -690,15 +701,19 @@ def wait_for_app_startup(app_name, max_wait_time=30): | @@ -690,15 +701,19 @@ def wait_for_app_startup(app_name, max_wait_time=30): | ||
| 690 | return False, "进程启动失败" | 701 | return False, "进程启动失败" |
| 691 | 702 | ||
| 692 | try: | 703 | try: |
| 693 | - response = requests.get(f"http://localhost:{info['port']}", timeout=2) | 704 | + response = requests.get( |
| 705 | + _build_healthcheck_url(info['port']), | ||
| 706 | + timeout=2, | ||
| 707 | + proxies=HEALTHCHECK_PROXIES | ||
| 708 | + ) | ||
| 694 | if response.status_code == 200: | 709 | if response.status_code == 200: |
| 695 | info['status'] = 'running' | 710 | info['status'] = 'running' |
| 696 | return True, "启动成功" | 711 | return True, "启动成功" |
| 697 | - except: | ||
| 698 | - pass | ||
| 699 | - | 712 | + except Exception as exc: |
| 713 | + logger.warning(f"{app_name} 健康检查失败: {exc}") | ||
| 714 | + | ||
| 700 | time.sleep(1) | 715 | time.sleep(1) |
| 701 | - | 716 | + |
| 702 | return False, "启动超时" | 717 | return False, "启动超时" |
| 703 | 718 | ||
| 704 | def cleanup_processes(): | 719 | def cleanup_processes(): |
| @@ -1042,4 +1057,4 @@ if __name__ == '__main__': | @@ -1042,4 +1057,4 @@ if __name__ == '__main__': | ||
| 1042 | logger.info("\n正在关闭应用...") | 1057 | logger.info("\n正在关闭应用...") |
| 1043 | cleanup_processes() | 1058 | cleanup_processes() |
| 1044 | 1059 | ||
| 1045 | - | 1060 | + |
| @@ -699,6 +699,13 @@ | @@ -699,6 +699,13 @@ | ||
| 699 | font-size: 13px; | 699 | font-size: 13px; |
| 700 | } | 700 | } |
| 701 | 701 | ||
| 702 | + .task-actions { | ||
| 703 | + margin-top: 15px; | ||
| 704 | + display: flex; | ||
| 705 | + gap: 12px; | ||
| 706 | + flex-wrap: wrap; | ||
| 707 | + } | ||
| 708 | + | ||
| 702 | @keyframes spin { | 709 | @keyframes spin { |
| 703 | to { transform: rotate(360deg); } | 710 | to { transform: rotate(360deg); } |
| 704 | } | 711 | } |
| @@ -801,51 +808,51 @@ | @@ -801,51 +808,51 @@ | ||
| 801 | } | 808 | } |
| 802 | 809 | ||
| 803 | /* 不同Engine的颜色区分 */ | 810 | /* 不同Engine的颜色区分 */ |
| 804 | - .forum-message.agent:has(.forum-message-header:contains("Query Engine")) { | ||
| 805 | - background-color: #eaf1f8; | ||
| 806 | - border-color: #608ab1; | ||
| 807 | - } | 811 | + .forum-message.agent:has(.forum-message-header:contains("Query Engine")) { |
| 812 | + background-color: #eaf1f8; | ||
| 813 | + border-color: #608ab1; | ||
| 814 | + } | ||
| 808 | 815 | ||
| 809 | .forum-message.agent:has(.forum-message-header:contains("QUERY Engine")) { | 816 | .forum-message.agent:has(.forum-message-header:contains("QUERY Engine")) { |
| 810 | - background-color: #eaf1f8; | ||
| 811 | - border-color: #608ab1; | ||
| 812 | - } | 817 | + background-color: #eaf1f8; |
| 818 | + border-color: #608ab1; | ||
| 819 | + } | ||
| 813 | 820 | ||
| 814 | - .forum-message.agent:has(.forum-message-header:contains("Insight Engine")) { | ||
| 815 | - background-color: #f2ebf3; | ||
| 816 | - border-color: #8e6a9f; | ||
| 817 | - } | 821 | + .forum-message.agent:has(.forum-message-header:contains("Insight Engine")) { |
| 822 | + background-color: #f2ebf3; | ||
| 823 | + border-color: #8e6a9f; | ||
| 824 | + } | ||
| 818 | 825 | ||
| 819 | .forum-message.agent:has(.forum-message-header:contains("INSIGHT Engine")) { | 826 | .forum-message.agent:has(.forum-message-header:contains("INSIGHT Engine")) { |
| 820 | - background-color: #f2ebf3; | ||
| 821 | - border-color: #8e6a9f; | ||
| 822 | - } | 827 | + background-color: #f2ebf3; |
| 828 | + border-color: #8e6a9f; | ||
| 829 | + } | ||
| 823 | 830 | ||
| 824 | - .forum-message.agent:has(.forum-message-header:contains("Media Engine")) { | ||
| 825 | - background-color: #ebf2ea; | ||
| 826 | - border-color: #6a9a6e; | ||
| 827 | - } | 831 | + .forum-message.agent:has(.forum-message-header:contains("Media Engine")) { |
| 832 | + background-color: #ebf2ea; | ||
| 833 | + border-color: #6a9a6e; | ||
| 834 | + } | ||
| 828 | 835 | ||
| 829 | .forum-message.agent:has(.forum-message-header:contains("MEDIA Engine")) { | 836 | .forum-message.agent:has(.forum-message-header:contains("MEDIA Engine")) { |
| 830 | - background-color: #ebf2ea; | ||
| 831 | - border-color: #6a9a6e; | ||
| 832 | - } | ||
| 833 | - | ||
| 834 | - /* 备用方案:通过JavaScript添加的类 */ | ||
| 835 | - .forum-message.query-engine { | ||
| 836 | - background-color: #eaf1f8; | ||
| 837 | - border-color: #608ab1; | ||
| 838 | - } | ||
| 839 | - | ||
| 840 | - .forum-message.insight-engine { | ||
| 841 | - background-color: #f2ebf3; | ||
| 842 | - border-color: #8e6a9f; | ||
| 843 | - } | ||
| 844 | - | ||
| 845 | - .forum-message.media-engine { | ||
| 846 | - background-color: #ebf2ea; | ||
| 847 | - border-color: #6a9a6e; | ||
| 848 | - } | 837 | + background-color: #ebf2ea; |
| 838 | + border-color: #6a9a6e; | ||
| 839 | + } | ||
| 840 | + | ||
| 841 | + /* 备用方案:通过JavaScript添加的类 */ | ||
| 842 | + .forum-message.query-engine { | ||
| 843 | + background-color: #eaf1f8; | ||
| 844 | + border-color: #608ab1; | ||
| 845 | + } | ||
| 846 | + | ||
| 847 | + .forum-message.insight-engine { | ||
| 848 | + background-color: #f2ebf3; | ||
| 849 | + border-color: #8e6a9f; | ||
| 850 | + } | ||
| 851 | + | ||
| 852 | + .forum-message.media-engine { | ||
| 853 | + background-color: #ebf2ea; | ||
| 854 | + border-color: #6a9a6e; | ||
| 855 | + } | ||
| 849 | 856 | ||
| 850 | .forum-message.agent.QUERY { | 857 | .forum-message.agent.QUERY { |
| 851 | background-color: #eaf1f8; | 858 | background-color: #eaf1f8; |
| @@ -857,10 +864,10 @@ | @@ -857,10 +864,10 @@ | ||
| 857 | border-color: #608ab1; | 864 | border-color: #608ab1; |
| 858 | } | 865 | } |
| 859 | 866 | ||
| 860 | - .forum-message.agent.MEDIA { | ||
| 861 | - background-color: #ebf2ea; | ||
| 862 | - border-color: #6a9a6e; | ||
| 863 | - } | 867 | + .forum-message.agent.MEDIA { |
| 868 | + background-color: #ebf2ea; | ||
| 869 | + border-color: #6a9a6e; | ||
| 870 | + } | ||
| 864 | 871 | ||
| 865 | .forum-message.agent.media-engine { | 872 | .forum-message.agent.media-engine { |
| 866 | background-color: #ebf2ea; | 873 | background-color: #ebf2ea; |
| @@ -2378,6 +2385,7 @@ | @@ -2378,6 +2385,7 @@ | ||
| 2378 | // Report Engine 相关函数 | 2385 | // Report Engine 相关函数 |
| 2379 | let reportLogLineCount = 0; | 2386 | let reportLogLineCount = 0; |
| 2380 | let reportLockCheckInterval = null; | 2387 | let reportLockCheckInterval = null; |
| 2388 | + let lastCompletedReportTask = null; | ||
| 2381 | 2389 | ||
| 2382 | // 实时刷新论坛消息(适用于所有页面) | 2390 | // 实时刷新论坛消息(适用于所有页面) |
| 2383 | function refreshForumMessages() { | 2391 | function refreshForumMessages() { |
| @@ -2783,6 +2791,12 @@ | @@ -2783,6 +2791,12 @@ | ||
| 2783 | <div>正在初始化...</div> | 2791 | <div>正在初始化...</div> |
| 2784 | </div> | 2792 | </div> |
| 2785 | </div> | 2793 | </div> |
| 2794 | + | ||
| 2795 | + <!-- 控制按钮区域 --> | ||
| 2796 | + <div class="report-controls"> | ||
| 2797 | + <button class="report-button primary" id="generateReportButton">生成最终报告</button> | ||
| 2798 | + <button class="report-button" id="downloadReportButton" disabled>下载HTML</button> | ||
| 2799 | + </div> | ||
| 2786 | 2800 | ||
| 2787 | <!-- 任务进度区域 --> | 2801 | <!-- 任务进度区域 --> |
| 2788 | <div id="taskProgressArea"></div> | 2802 | <div id="taskProgressArea"></div> |
| @@ -2796,19 +2810,146 @@ | @@ -2796,19 +2810,146 @@ | ||
| 2796 | `; | 2810 | `; |
| 2797 | 2811 | ||
| 2798 | reportContent.innerHTML = interfaceHTML; | 2812 | reportContent.innerHTML = interfaceHTML; |
| 2813 | + initializeReportControls(); | ||
| 2799 | 2814 | ||
| 2800 | // 立即更新状态信息 | 2815 | // 立即更新状态信息 |
| 2801 | updateEngineStatusDisplay(statusData); | 2816 | updateEngineStatusDisplay(statusData); |
| 2802 | 2817 | ||
| 2803 | // 如果有当前任务,显示任务状态 | 2818 | // 如果有当前任务,显示任务状态 |
| 2804 | if (statusData.current_task) { | 2819 | if (statusData.current_task) { |
| 2805 | - const taskArea = document.getElementById('taskProgressArea'); | ||
| 2806 | - if (taskArea) { | ||
| 2807 | - taskArea.innerHTML = renderTaskStatus(statusData.current_task); | 2820 | + updateTaskProgressStatus(statusData.current_task); |
| 2821 | + } else { | ||
| 2822 | + updateDownloadButtonState(null); | ||
| 2823 | + } | ||
| 2824 | + } | ||
| 2825 | + | ||
| 2826 | + function initializeReportControls() { | ||
| 2827 | + const generateButton = document.getElementById('generateReportButton'); | ||
| 2828 | + if (generateButton && !generateButton.dataset.bound) { | ||
| 2829 | + generateButton.dataset.bound = 'true'; | ||
| 2830 | + generateButton.addEventListener('click', () => { | ||
| 2831 | + if (reportTaskId) { | ||
| 2832 | + showMessage('已有报告生成任务在运行', 'info'); | ||
| 2833 | + return; | ||
| 2834 | + } | ||
| 2835 | + const reportButton = document.querySelector('[data-app="report"]'); | ||
| 2836 | + if (reportButton && reportButton.classList.contains('locked')) { | ||
| 2837 | + showMessage('需等待三个Agent完成最新分析后才能生成最终报告', 'error'); | ||
| 2838 | + return; | ||
| 2839 | + } | ||
| 2840 | + generateReport(); | ||
| 2841 | + }); | ||
| 2842 | + } | ||
| 2843 | + | ||
| 2844 | + const downloadButton = document.getElementById('downloadReportButton'); | ||
| 2845 | + if (downloadButton && !downloadButton.dataset.bound) { | ||
| 2846 | + downloadButton.dataset.bound = 'true'; | ||
| 2847 | + downloadButton.addEventListener('click', () => downloadReport()); | ||
| 2848 | + } | ||
| 2849 | + | ||
| 2850 | + if (reportTaskId) { | ||
| 2851 | + setGenerateButtonState(true); | ||
| 2852 | + } else { | ||
| 2853 | + setGenerateButtonState(false); | ||
| 2854 | + } | ||
| 2855 | + | ||
| 2856 | + if (lastCompletedReportTask) { | ||
| 2857 | + updateDownloadButtonState(lastCompletedReportTask); | ||
| 2858 | + } | ||
| 2859 | + } | ||
| 2860 | + | ||
| 2861 | + function setGenerateButtonState(forceLoading = false) { | ||
| 2862 | + const generateButton = document.getElementById('generateReportButton'); | ||
| 2863 | + if (!generateButton) return; | ||
| 2864 | + | ||
| 2865 | + if (forceLoading || reportTaskId) { | ||
| 2866 | + if (!generateButton.dataset.originalText) { | ||
| 2867 | + generateButton.dataset.originalText = generateButton.textContent || '生成最终报告'; | ||
| 2868 | + } | ||
| 2869 | + generateButton.disabled = true; | ||
| 2870 | + generateButton.textContent = '生成中...'; | ||
| 2871 | + } else { | ||
| 2872 | + const originalText = generateButton.dataset.originalText || '生成最终报告'; | ||
| 2873 | + generateButton.disabled = false; | ||
| 2874 | + generateButton.textContent = originalText; | ||
| 2875 | + } | ||
| 2876 | + } | ||
| 2877 | + | ||
| 2878 | + function updateDownloadButtonState(task) { | ||
| 2879 | + const downloadButton = document.getElementById('downloadReportButton'); | ||
| 2880 | + if (!downloadButton) return; | ||
| 2881 | + | ||
| 2882 | + if (task && task.status === 'completed' && (task.report_file_ready || task.report_file_path)) { | ||
| 2883 | + downloadButton.disabled = false; | ||
| 2884 | + downloadButton.dataset.taskId = task.task_id; | ||
| 2885 | + downloadButton.dataset.filename = task.report_file_name || ''; | ||
| 2886 | + const label = task.report_file_name ? `下载HTML (${task.report_file_name})` : '下载HTML'; | ||
| 2887 | + downloadButton.textContent = label; | ||
| 2888 | + lastCompletedReportTask = task; | ||
| 2889 | + } else if (!lastCompletedReportTask || (task && task.status !== 'completed')) { | ||
| 2890 | + downloadButton.disabled = true; | ||
| 2891 | + downloadButton.dataset.taskId = ''; | ||
| 2892 | + downloadButton.dataset.filename = ''; | ||
| 2893 | + downloadButton.textContent = '下载HTML'; | ||
| 2894 | + if (!reportTaskId) { | ||
| 2895 | + lastCompletedReportTask = null; | ||
| 2808 | } | 2896 | } |
| 2809 | } | 2897 | } |
| 2810 | } | 2898 | } |
| 2811 | 2899 | ||
| 2900 | + function downloadReport(taskId = null) { | ||
| 2901 | + const downloadButton = document.getElementById('downloadReportButton'); | ||
| 2902 | + const targetTaskId = taskId || (downloadButton ? downloadButton.dataset.taskId : ''); | ||
| 2903 | + | ||
| 2904 | + if (!targetTaskId) { | ||
| 2905 | + showMessage('暂无可下载的报告,请先生成最终报告', 'error'); | ||
| 2906 | + return; | ||
| 2907 | + } | ||
| 2908 | + | ||
| 2909 | + let preferredFileName = ''; | ||
| 2910 | + if (downloadButton && downloadButton.dataset.filename) { | ||
| 2911 | + preferredFileName = downloadButton.dataset.filename; | ||
| 2912 | + } else if (lastCompletedReportTask && lastCompletedReportTask.task_id === targetTaskId) { | ||
| 2913 | + preferredFileName = lastCompletedReportTask.report_file_name || ''; | ||
| 2914 | + } | ||
| 2915 | + | ||
| 2916 | + fetch(`/api/report/download/${targetTaskId}`) | ||
| 2917 | + .then(response => { | ||
| 2918 | + if (!response.ok) { | ||
| 2919 | + const contentType = response.headers.get('Content-Type') || ''; | ||
| 2920 | + if (contentType.includes('application/json')) { | ||
| 2921 | + return response.json().then(err => { | ||
| 2922 | + throw new Error(err.error || '下载失败'); | ||
| 2923 | + }); | ||
| 2924 | + } | ||
| 2925 | + throw new Error('下载失败'); | ||
| 2926 | + } | ||
| 2927 | + const disposition = response.headers.get('Content-Disposition') || ''; | ||
| 2928 | + return response.blob().then(blob => ({ blob, disposition })); | ||
| 2929 | + }) | ||
| 2930 | + .then(({ blob, disposition }) => { | ||
| 2931 | + let filename = preferredFileName; | ||
| 2932 | + if (!filename) { | ||
| 2933 | + const match = disposition.match(/filename="?([^";]+)"?/i); | ||
| 2934 | + filename = match ? match[1] : `final_report_${targetTaskId}.html`; | ||
| 2935 | + } | ||
| 2936 | + | ||
| 2937 | + const url = window.URL.createObjectURL(blob); | ||
| 2938 | + const link = document.createElement('a'); | ||
| 2939 | + link.href = url; | ||
| 2940 | + link.download = filename || 'final_report.html'; | ||
| 2941 | + document.body.appendChild(link); | ||
| 2942 | + link.click(); | ||
| 2943 | + document.body.removeChild(link); | ||
| 2944 | + window.URL.revokeObjectURL(url); | ||
| 2945 | + showMessage('报告文件已开始下载', 'success'); | ||
| 2946 | + }) | ||
| 2947 | + .catch(error => { | ||
| 2948 | + console.error('下载报告失败:', error); | ||
| 2949 | + showMessage('下载报告失败: ' + error.message, 'error'); | ||
| 2950 | + }); | ||
| 2951 | + } | ||
| 2952 | + | ||
| 2812 | // 渲染任务状态(使用新的进度条样式) | 2953 | // 渲染任务状态(使用新的进度条样式) |
| 2813 | function renderTaskStatus(task) { | 2954 | function renderTaskStatus(task) { |
| 2814 | // 状态文本的中文映射 | 2955 | // 状态文本的中文映射 |
| @@ -2863,7 +3004,18 @@ | @@ -2863,7 +3004,18 @@ | ||
| 2863 | </div> | 3004 | </div> |
| 2864 | </div> | 3005 | </div> |
| 2865 | `; | 3006 | `; |
| 2866 | - | 3007 | + |
| 3008 | + if (task.report_file_path) { | ||
| 3009 | + statusHTML += ` | ||
| 3010 | + <div class="task-info-line"> | ||
| 3011 | + <div class="task-info-item"> | ||
| 3012 | + <span class="task-info-label">保存路径:</span> | ||
| 3013 | + <span class="task-info-value">${task.report_file_path}</span> | ||
| 3014 | + </div> | ||
| 3015 | + </div> | ||
| 3016 | + `; | ||
| 3017 | + } | ||
| 3018 | + | ||
| 2867 | if (task.error_message) { | 3019 | if (task.error_message) { |
| 2868 | statusHTML += ` | 3020 | statusHTML += ` |
| 2869 | <div class="task-error-message"> | 3021 | <div class="task-error-message"> |
| @@ -2871,13 +3023,33 @@ | @@ -2871,13 +3023,33 @@ | ||
| 2871 | </div> | 3023 | </div> |
| 2872 | `; | 3024 | `; |
| 2873 | } | 3025 | } |
| 2874 | - | 3026 | + |
| 3027 | + if (task.status === 'completed') { | ||
| 3028 | + statusHTML += ` | ||
| 3029 | + <div class="task-actions"> | ||
| 3030 | + <button class="report-button primary" onclick="viewReport('${task.task_id}')">重新加载</button> | ||
| 3031 | + ${task.report_file_ready ? `<button class="report-button" onclick="downloadReport('${task.task_id}')">下载HTML</button>` : ''} | ||
| 3032 | + </div> | ||
| 3033 | + `; | ||
| 3034 | + } | ||
| 3035 | + | ||
| 2875 | statusHTML += '</div>'; | 3036 | statusHTML += '</div>'; |
| 2876 | return statusHTML; | 3037 | return statusHTML; |
| 2877 | } | 3038 | } |
| 2878 | 3039 | ||
| 2879 | // 生成报告 | 3040 | // 生成报告 |
| 2880 | function generateReport() { | 3041 | function generateReport() { |
| 3042 | + if (reportTaskId) { | ||
| 3043 | + showMessage('已有报告生成任务在运行', 'info'); | ||
| 3044 | + return; | ||
| 3045 | + } | ||
| 3046 | + | ||
| 3047 | + const reportButton = document.querySelector('[data-app="report"]'); | ||
| 3048 | + if (reportButton && reportButton.classList.contains('locked')) { | ||
| 3049 | + showMessage('需等待三个Agent完成最新分析后才能生成最终报告', 'error'); | ||
| 3050 | + return; | ||
| 3051 | + } | ||
| 3052 | + | ||
| 2881 | const query = document.getElementById('searchInput').value.trim() || '智能舆情分析报告'; | 3053 | const query = document.getElementById('searchInput').value.trim() || '智能舆情分析报告'; |
| 2882 | 3054 | ||
| 2883 | // 重置日志计数器,因为后台会清空日志文件 | 3055 | // 重置日志计数器,因为后台会清空日志文件 |
| @@ -2887,8 +3059,8 @@ | @@ -2887,8 +3059,8 @@ | ||
| 2887 | const consoleOutput = document.getElementById('consoleOutput'); | 3059 | const consoleOutput = document.getElementById('consoleOutput'); |
| 2888 | consoleOutput.innerHTML = '<div class="console-line">[系统] 开始生成报告,日志已重置</div>'; | 3060 | consoleOutput.innerHTML = '<div class="console-line">[系统] 开始生成报告,日志已重置</div>'; |
| 2889 | 3061 | ||
| 2890 | - // 按钮已移除,无需操作按钮状态 | ||
| 2891 | - | 3062 | + setGenerateButtonState(true); |
| 3063 | + | ||
| 2892 | // 在现有状态信息后添加任务进度状态,而不是替换 | 3064 | // 在现有状态信息后添加任务进度状态,而不是替换 |
| 2893 | addTaskProgressStatus('正在启动报告生成任务...', 'loading'); | 3065 | addTaskProgressStatus('正在启动报告生成任务...', 'loading'); |
| 2894 | 3066 | ||
| @@ -2934,6 +3106,7 @@ | @@ -2934,6 +3106,7 @@ | ||
| 2934 | // 重置标志允许重新尝试 | 3106 | // 重置标志允许重新尝试 |
| 2935 | autoGenerateTriggered = false; | 3107 | autoGenerateTriggered = false; |
| 2936 | reportTaskId = null; | 3108 | reportTaskId = null; |
| 3109 | + setGenerateButtonState(false); | ||
| 2937 | } | 3110 | } |
| 2938 | }) | 3111 | }) |
| 2939 | .catch(error => { | 3112 | .catch(error => { |
| @@ -2942,6 +3115,7 @@ | @@ -2942,6 +3115,7 @@ | ||
| 2942 | // 重置标志允许重新尝试 | 3115 | // 重置标志允许重新尝试 |
| 2943 | autoGenerateTriggered = false; | 3116 | autoGenerateTriggered = false; |
| 2944 | reportTaskId = null; | 3117 | reportTaskId = null; |
| 3118 | + setGenerateButtonState(false); | ||
| 2945 | }); | 3119 | }); |
| 2946 | } | 3120 | } |
| 2947 | 3121 | ||
| @@ -2977,6 +3151,7 @@ | @@ -2977,6 +3151,7 @@ | ||
| 2977 | // 重置自动生成标志,允许下次有新内容时自动生成 | 3151 | // 重置自动生成标志,允许下次有新内容时自动生成 |
| 2978 | autoGenerateTriggered = false; | 3152 | autoGenerateTriggered = false; |
| 2979 | reportTaskId = null; | 3153 | reportTaskId = null; |
| 3154 | + setGenerateButtonState(false); | ||
| 2980 | } else if (data.task.status === 'error') { | 3155 | } else if (data.task.status === 'error') { |
| 2981 | clearInterval(reportPollingInterval); | 3156 | clearInterval(reportPollingInterval); |
| 2982 | showMessage('报告生成失败: ' + data.task.error_message, 'error'); | 3157 | showMessage('报告生成失败: ' + data.task.error_message, 'error'); |
| @@ -2984,6 +3159,7 @@ | @@ -2984,6 +3159,7 @@ | ||
| 2984 | // 重置自动生成标志,允许重新尝试 | 3159 | // 重置自动生成标志,允许重新尝试 |
| 2985 | autoGenerateTriggered = false; | 3160 | autoGenerateTriggered = false; |
| 2986 | reportTaskId = null; | 3161 | reportTaskId = null; |
| 3162 | + setGenerateButtonState(false); | ||
| 2987 | } | 3163 | } |
| 2988 | } | 3164 | } |
| 2989 | }) | 3165 | }) |
| @@ -3020,11 +3196,17 @@ | @@ -3020,11 +3196,17 @@ | ||
| 3020 | 3196 | ||
| 3021 | if (task) { | 3197 | if (task) { |
| 3022 | taskArea.innerHTML = renderTaskStatus(task); | 3198 | taskArea.innerHTML = renderTaskStatus(task); |
| 3199 | + if (task.status === 'completed') { | ||
| 3200 | + lastCompletedReportTask = task; | ||
| 3201 | + } else if (task.status === 'running') { | ||
| 3202 | + lastCompletedReportTask = null; | ||
| 3203 | + } | ||
| 3204 | + updateDownloadButtonState(task); | ||
| 3023 | } else if (status && errorMessage) { | 3205 | } else if (status && errorMessage) { |
| 3024 | const loadingIndicator = status === 'loading' ? '<span class="report-loading-spinner"></span>' : ''; | 3206 | const loadingIndicator = status === 'loading' ? '<span class="report-loading-spinner"></span>' : ''; |
| 3025 | const statusBadgeClass = status === 'error' ? 'task-status-error' : 'task-status-running'; | 3207 | const statusBadgeClass = status === 'error' ? 'task-status-error' : 'task-status-running'; |
| 3026 | const statusText = status === 'error' ? '错误' : '处理中'; | 3208 | const statusText = status === 'error' ? '错误' : '处理中'; |
| 3027 | - | 3209 | + |
| 3028 | taskArea.innerHTML = ` | 3210 | taskArea.innerHTML = ` |
| 3029 | <div class="task-progress-container"> | 3211 | <div class="task-progress-container"> |
| 3030 | <div class="task-progress-header"> | 3212 | <div class="task-progress-header"> |
| @@ -3253,4 +3435,4 @@ | @@ -3253,4 +3435,4 @@ | ||
| 3253 | } | 3435 | } |
| 3254 | </script> | 3436 | </script> |
| 3255 | </body> | 3437 | </body> |
| 3256 | -</html> | 3438 | +</html> |
-
Please register or login to post a comment