import os import time import threading from flask import Flask, render_template, send_from_directory, request, jsonify from flask_cors import CORS import chat_splitter import analyzer # --- 1. 配置区域 --- # 定义项目根目录 (F:\工作\项目\G36\官网) ROOT_PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) app = Flask(__name__, static_folder=ROOT_PROJECT_DIR, static_url_path='/') CORS(app) # 启用CORS,允许来自所有源的请求 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) UPLOADS_FOLDER = os.path.join(BASE_DIR, 'uploads') SPLIT_LOGS_FOLDER = os.path.join(BASE_DIR, '聊天记录_按天拆分') REPORTS_FOLDER = os.path.join(BASE_DIR, '分析报告') LOG_FILE = os.path.join(BASE_DIR, 'app_activity.log') # -- 新增 --: 用于持久化存储上一次分析时间戳的状态文件 STATE_FILE = os.path.join(BASE_DIR, '.app_state') CHECK_INTERVAL_SECONDS = 300 APP_CONFIG = { "OPENROUTER_API_KEY": "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", "YOUR_SITE_URL": "https://your-site.com", "YOUR_SITE_NAME": "My Game Analysis", "MODEL_NAME": "google/gemini-2.5-pro" } # 全局变量,将在启动时从状态文件加载 last_known_mtime = 0 # --- 2. 日志与状态管理 (核心修改区域) --- def log_activity(message: str): """记录活动到日志文件并打印到控制台。""" timestamp = time.strftime("%Y-%m-%d %H:%M:%S") log_message = f"[{timestamp}] {message}" print(log_message) try: with open(LOG_FILE, 'a', encoding='utf-8') as f: f.write(log_message + '\\n') except IOError as e: print(f"[{timestamp}] CRITICAL: Unable to write to log file '{LOG_FILE}'. Error: {e}") # -- 新增 --: 从状态文件加载上一次的修改时间 def load_last_mtime() -> float: """从.app_state文件加载上次记录的mtime,如果文件不存在或无效则返回0。""" try: if os.path.exists(STATE_FILE): with open(STATE_FILE, 'r') as f: return float(f.read().strip()) except (IOError, ValueError) as e: log_activity(f"Warning: Could not read state from '{STATE_FILE}', defaulting to 0. Error: {e}") return 0.0 # -- 新增 --: 将最新的修改时间保存到状态文件 def save_last_mtime(mtime: float): """将新的mtime保存到.app_state文件。""" try: with open(STATE_FILE, 'w') as f: f.write(str(mtime)) except IOError as e: log_activity(f"Warning: Could not save state to '{STATE_FILE}'. Error: {e}") # --- 3. 后台自动分析任务 --- def get_latest_mtime_in_folder(folder_path: str) -> float: """获取指定文件夹内所有.txt文件的最新修改时间。""" latest_mtime = 0 try: txt_files = [f for f in os.listdir(folder_path) if f.endswith('.txt')] if not txt_files: return 0 for filename in txt_files: file_path = os.path.join(folder_path, filename) try: mtime = os.path.getmtime(file_path) if mtime > latest_mtime: latest_mtime = mtime except FileNotFoundError: continue except FileNotFoundError: return 0 return latest_mtime def run_the_analysis(): """封装核心分析逻辑,以便可以被手动调用。""" global last_known_mtime log_activity("--- Triggering analysis manually. ---") current_latest_mtime = get_latest_mtime_in_folder(UPLOADS_FOLDER) log_activity("Step 1/2: Merging and splitting all chat logs by day...") chat_splitter.split_log_file(UPLOADS_FOLDER, SPLIT_LOGS_FOLDER) log_activity("Step 2/2: Analyzing new daily log files...") new_reports_count = analyzer.run_analysis_on_new_files( input_folder=SPLIT_LOGS_FOLDER, output_folder=REPORTS_FOLDER, config=APP_CONFIG ) if new_reports_count > 0: log_activity(f"--- SUCCESS: Analysis complete. Generated/Updated {new_reports_count} report(s). ---") else: log_activity("--- Analysis complete. No new reports were generated. ---") last_known_mtime = current_latest_mtime save_last_mtime(last_known_mtime) log_activity(f"State updated. New last_known_mtime is {last_known_mtime}.") return new_reports_count def background_analysis_task(): """在后台运行的守护线程,定时检查uploads文件夹中是否有文件更新并触发分析。""" global last_known_mtime log_activity(f"Background monitoring started. Checking '{UPLOADS_FOLDER}' for updates...") while True: try: current_latest_mtime = get_latest_mtime_in_folder(UPLOADS_FOLDER) if current_latest_mtime > last_known_mtime: run_the_analysis() except Exception as e: log_activity(f"CRITICAL ERROR in background task: {e}") time.sleep(CHECK_INTERVAL_SECONDS) # --- 4. API 路由 --- @app.route('/') def index(): return app.send_static_file('index.html') # 为报告文件夹提供一个专门的路由,以便前端可以访问 @app.route('/网页服务/分析报告/') def serve_report_file(filename): return send_from_directory(REPORTS_FOLDER, filename) @app.route('/reports', methods=['GET']) def list_reports(): """提供一个API端点,用于列出所有报告(包含修改时间)。""" reports = [] if not os.path.exists(REPORTS_FOLDER): return jsonify([]) for filename in sorted(os.listdir(REPORTS_FOLDER), reverse=True): if filename.endswith(".html"): filepath = os.path.join(REPORTS_FOLDER, filename) reports.append({ "name": filename, "mtime": os.path.getmtime(filepath) }) return jsonify(reports) @app.route('/analyze', methods=['POST']) def handle_analysis_request(): """处理来自前端的文件上传和分析请求。""" if 'file' not in request.files: return jsonify({"error": "No file part"}), 400 file = request.files['file'] if file.filename == '': return jsonify({"error": "No selected file"}), 400 if file and file.filename.endswith('.txt'): try: filepath = os.path.join(UPLOADS_FOLDER, file.filename) file.save(filepath) log_activity(f"Received new file from web: '{file.filename}'. Saved to uploads folder.") # 在一个新线程中运行分析,避免阻塞网页请求 analysis_thread = threading.Thread(target=run_the_analysis) analysis_thread.start() return jsonify({"message": f"File '{file.filename}' uploaded successfully. Analysis started in background."}), 202 except Exception as e: log_activity(f"Error processing uploaded file: {e}") return jsonify({"error": "Server error during file processing"}), 500 return jsonify({"error": "Invalid file type, only .txt is accepted"}), 400 # --- 5. 启动应用 (核心修改区域) --- if __name__ == '__main__': for folder in [UPLOADS_FOLDER, SPLIT_LOGS_FOLDER, REPORTS_FOLDER]: if not os.path.exists(folder): os.makedirs(folder) # -- 修改 --: 在启动时从文件加载“记忆”,而不是实时获取 last_known_mtime = load_last_mtime() log_activity(f"Application starting... Loaded last known mtime from state file: {last_known_mtime}.") daemon = threading.Thread(target=background_analysis_task, daemon=True, name='Monitor') daemon.start() log_activity("Web service started. Background task is now monitoring for file changes.") log_activity(f"To begin, please place your chat log files (.txt) into the '{UPLOADS_FOLDER}' folder.") app.run(host='0.0.0.0', port=5000, use_reloader=False)