diff --git a/logger.py b/logger.py index 8313c52..4a941b2 100644 --- a/logger.py +++ b/logger.py @@ -2,9 +2,7 @@ from loguru import logger import sys import os from datetime import datetime - -# 获取当前时间作为日志文件名的一部分 -current_time = datetime.now().strftime("%Y%m%d_%H%M%S") +import shutil # 创建日志目录 log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") @@ -17,42 +15,54 @@ logger.remove() # 移除默认的处理器 logger.add( sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}", - level="DEBUG" + level="INFO", # 同时显示在控制台和写入到日志文件中 ) -# 添加文件处理器(debug级别) +# 添加统一的日志文件处理器,按日期自动轮转 logger.add( - os.path.join(log_dir, f"debug_{current_time}.log"), + os.path.join(log_dir, "stock_scanner_{time:YYYY-MM-DD}.log"), format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}", level="DEBUG", - rotation="100 MB", - retention="1 week" + rotation="00:00", # 每天午夜轮转 + retention="7 days", # 保留7天的日志 + compression="zip", # 压缩旧日志文件 + enqueue=True # 使用队列写入,提高性能 ) -# 添加文件处理器(error级别) +# 添加错误日志文件处理器,专门记录错误信息 logger.add( - os.path.join(log_dir, f"error_{current_time}.log"), + os.path.join(log_dir, "error_{time:YYYY-MM-DD}.log"), format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}", level="ERROR", - rotation="100 MB", - retention="1 month" + rotation="00:00", # 每天午夜轮转 + retention="7 days", # 保留7天的错误日志 + compression="zip", # 压缩旧日志文件 + enqueue=True # 使用队列写入,提高性能 ) -# 添加流处理器(用于记录流式输出) -logger.add( - os.path.join(log_dir, f"stream_{current_time}.log"), - format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {message}", - filter=lambda record: "STREAM" in record["extra"], - level="INFO" -) - -# 创建专用于流式输出的日志器 -stream_logger = logger.bind(STREAM=True) +def clean_old_logs(max_days=7): + """清理超过指定天数的日志文件""" + try: + today = datetime.now() + for filename in os.listdir(log_dir): + file_path = os.path.join(log_dir, filename) + # 跳过目录 + if os.path.isdir(file_path): + continue + + # 检查文件修改时间 + file_time = datetime.fromtimestamp(os.path.getmtime(file_path)) + days_old = (today - file_time).days + + # 如果文件超过指定天数,删除它 + if days_old > max_days: + os.remove(file_path) + logger.info(f"已删除过期日志文件: {filename}") + except Exception as e: + logger.error(f"清理日志文件时出错: {e}") def get_logger(): """获取通用日志器""" + # 启动时清理旧日志 + clean_old_logs() return logger - -def get_stream_logger(): - """获取流式输出专用日志器""" - return stream_logger diff --git a/stock_analyzer.py b/stock_analyzer.py index ea35826..4b8185a 100644 --- a/stock_analyzer.py +++ b/stock_analyzer.py @@ -6,14 +6,13 @@ import requests from typing import Dict, List, Optional, Tuple, Generator from dotenv import load_dotenv import json -from logger import get_logger, get_stream_logger +from logger import get_logger # 获取日志器 logger = get_logger() -stream_logger = get_stream_logger() class StockAnalyzer: - def __init__(self, initial_cash=1000000, custom_api_url=None, custom_api_key=None, custom_api_model=None, custom_api_timeout=60): + def __init__(self, initial_cash=1000000, custom_api_url=None, custom_api_key=None, custom_api_model=None, custom_api_timeout=None): # 加载环境变量 load_dotenv() @@ -21,10 +20,10 @@ class StockAnalyzer: # 设置 API 配置,优先使用自定义配置,否则使用环境变量 self.API_URL = custom_api_url or os.getenv('API_URL') self.API_KEY = custom_api_key or os.getenv('API_KEY') - self.API_TIMEOUT = custom_api_timeout or int(os.getenv('API_TIMEOUT', '60')) self.API_MODEL = custom_api_model or os.getenv('API_MODEL', 'gpt-3.5-turbo') + self.API_TIMEOUT = int(custom_api_timeout or os.getenv('API_TIMEOUT', 60)) - logger.debug(f"初始化StockAnalyzer: API_URL={self.API_URL}, API_MODEL={self.API_MODEL}, API_KEY={'已提供' if self.API_KEY else '未提供'}") + logger.debug(f"初始化StockAnalyzer: API_URL={self.API_URL}, API_MODEL={self.API_MODEL}, API_KEY={'已提供' if self.API_KEY else '未提供'}, API_TIMEOUT={self.API_TIMEOUT}") # 配置参数 self.params = { @@ -247,7 +246,7 @@ class StockAnalyzer: 请基于技术指标和市场动态进行分析,给出具体数据支持。 """ - logger.debug(f"生成的AI分析提示词: {prompt[:100]}...") + logger.debug(f"生成的AI分析提示词: {self._truncate_json_for_logging(prompt, 100)}...") # 检查API配置 if not self.API_URL: @@ -261,19 +260,14 @@ class StockAnalyzer: return error_msg if not stream else (yield json.dumps({"error": error_msg})) # 标准化API URL - if self.API_URL.endswith('/'): - api_url = f"{self.API_URL}chat/completions" - else: - api_url = f"{self.API_URL}/v1/chat/completions" - # 标准化API URL - # api_url = self.API_URL - # if not (api_url.endswith('/chat/completions') or api_url.endswith('/v1/chat/completions')): - # if api_url.endswith('/v1'): - # api_url = f"{api_url}/chat/completions" - # elif api_url.endswith('/'): - # api_url = f"{api_url}v1/chat/completions" - # else: - # api_url = f"{api_url}/v1/chat/completions" + api_url = self.API_URL + if not (api_url.endswith('/chat/completions') or api_url.endswith('/v1/chat/completions')): + if api_url.endswith('/v1'): + api_url = f"{api_url}/chat/completions" + elif api_url.endswith('/'): + api_url = f"{api_url}v1/chat/completions" + else: + api_url = f"{api_url}/v1/chat/completions" logger.debug(f"标准化后的API URL: {api_url}") @@ -295,13 +289,13 @@ class StockAnalyzer: try: logger.debug(f"发起流式API请求: {api_url}") - logger.debug(f"请求载荷: {json.dumps(payload, indent=2)}") + logger.debug(f"请求载荷: {self._truncate_json_for_logging(payload)}") response = requests.post( api_url, headers=headers, json=payload, - timeout=60, # 增加超时时间 + timeout=self.API_TIMEOUT, # 增加超时时间 stream=True ) @@ -313,7 +307,7 @@ class StockAnalyzer: else: try: error_response = response.json() - error_text = json.dumps(error_response, indent=2) + error_text = self._truncate_json_for_logging(error_response) except: error_text = response.text[:500] if response.text else "无响应内容" @@ -335,7 +329,7 @@ class StockAnalyzer: api_url, headers=headers, json=payload, - timeout=60 + timeout=self.API_TIMEOUT ) logger.debug(f"API非流式响应状态码: {response.status_code}") @@ -349,7 +343,7 @@ class StockAnalyzer: else: try: error_response = response.json() - error_text = json.dumps(error_response, indent=2) + error_text = self._truncate_json_for_logging(error_response) except: error_text = response.text[:500] if response.text else "无响应内容" @@ -371,11 +365,26 @@ class StockAnalyzer: if stream: logger.debug("在流式模式下返回异常信息") error_json = json.dumps({"stock_code": stock_code, "error": error_msg}) - stream_logger.info(f"流式异常输出: {error_json}") + logger.info(f"流式异常输出: {error_json}") yield error_json else: return error_msg + def _truncate_json_for_logging(self, json_obj, max_length=500): + """截断JSON对象用于日志记录,避免日志过大 + + Args: + json_obj: 要截断的JSON对象 + max_length: 最大字符长度,默认500 + + Returns: + str: 截断后的JSON字符串 + """ + json_str = json.dumps(json_obj, ensure_ascii=False) + if len(json_str) <= max_length: + return json_str + return json_str[:max_length] + f"... [截断,总长度: {len(json_str)}字符]" + def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]: """处理AI流式响应""" logger.info(f"开始处理股票 {stock_code} 的AI流式响应") @@ -386,7 +395,6 @@ class StockAnalyzer: for line in response.iter_lines(): if line: line = line.decode('utf-8') - stream_logger.info(f"原始流式行: {line}") # 跳过保持连接的空行 if line.strip() == '': @@ -396,7 +404,6 @@ class StockAnalyzer: # 数据行通常以"data: "开头 if line.startswith('data: '): data_content = line[6:] # 移除 "data: " 前缀 - stream_logger.info(f"数据内容: {data_content}") # 检查是否为流的结束 if data_content.strip() == '[DONE]': @@ -405,7 +412,6 @@ class StockAnalyzer: try: json_data = json.loads(data_content) - logger.debug(f"解析的JSON数据: {json.dumps(json_data)[:100]}...") if 'choices' in json_data: delta = json_data['choices'][0].get('delta', {}) @@ -414,15 +420,12 @@ class StockAnalyzer: if content: chunk_count += 1 buffer += content - logger.debug(f"收到内容片段 #{chunk_count}: {content}") - stream_logger.info(f"发送内容片段: {content}") - + # 创建包含AI分析片段的JSON chunk_json = json.dumps({ "stock_code": stock_code, "ai_analysis_chunk": content }) - stream_logger.info(f"流式输出JSON: {chunk_json}") yield chunk_json except json.JSONDecodeError as e: logger.error(f"JSON解析错误: {str(e)}, 行内容: {data_content}") @@ -494,7 +497,7 @@ class StockAnalyzer: 'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL', 'recommendation': self.get_recommendation(score) } - logger.debug(f"生成股票 {stock_code} 基础报告: {json.dumps(report)[:100]}...") + logger.debug(f"生成股票 {stock_code} 基础报告: {self._truncate_json_for_logging(report, 100)}...") if stream: logger.info(f"以流式模式返回股票 {stock_code} 分析结果") @@ -502,8 +505,8 @@ class StockAnalyzer: base_report = dict(report) base_report['ai_analysis'] = '' base_report_json = json.dumps(base_report) - logger.debug(f"基础报告JSON: {base_report_json[:100]}...") - stream_logger.info(f"发送基础报告: {base_report_json}") + logger.debug(f"基础报告JSON: {self._truncate_json_for_logging(base_report_json, 100)}...") + logger.info(f"发送基础报告: {base_report_json}") yield base_report_json # 然后流式返回AI分析部分 @@ -511,7 +514,6 @@ class StockAnalyzer: ai_chunks_count = 0 for ai_chunk in self.get_ai_analysis(df, stock_code, stream=True): ai_chunks_count += 1 - stream_logger.info(f"股票 {stock_code} 流式块 #{ai_chunks_count}: {ai_chunk}") yield ai_chunk logger.info(f"股票 {stock_code} 流式AI分析完成,共发送 {ai_chunks_count} 个块") else: @@ -528,7 +530,7 @@ class StockAnalyzer: if stream: error_json = json.dumps({'stock_code': stock_code, 'error': error_msg}) - stream_logger.info(f"流式错误输出: {error_json}") + logger.info(f"流式错误输出: {error_json}") yield error_json else: raise @@ -570,7 +572,6 @@ class StockAnalyzer: chunk_count = 0 for chunk in self.analyze_stock(stock_code, market_type, stream=True): chunk_count += 1 - stream_logger.info(f"股票 {stock_code} 流式块 #{chunk_count}: {chunk}") yield chunk logger.debug(f"股票 {stock_code} 流式分析完成,共 {chunk_count} 个块") except Exception as e: @@ -578,6 +579,6 @@ class StockAnalyzer: logger.error(error_msg) logger.exception(e) error_json = json.dumps({'stock_code': stock_code, 'error': error_msg}) - stream_logger.info(f"流式错误输出: {error_json}") + logger.info(f"流式错误输出: {error_json}") yield error_json logger.info(f"流式扫描完成,处理了 {stock_count} 只股票") diff --git a/templates/index.html b/templates/index.html index 11045cf..979f6bc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -63,28 +63,37 @@ placeholder="例如: gpt-3.5-turbo" value="{{ default_api_model }}"> + +
- - API Key + +

如不填写,将使用系统默认配置

+
+
+ + + value="{{ default_api_timeout }}" min="1" max="300"> +

请求超时时间,默认60秒

-
- - -

如不填写,将使用系统默认配置

-
-
- - +
+
+ + +
+
+ + +
@@ -350,6 +359,7 @@ const apiUrl = document.getElementById('apiUrl').value.trim(); const apiKey = document.getElementById('apiKey').value.trim(); const apiModel = document.getElementById('apiModel').value.trim(); + const apiTimeout = document.getElementById('apiTimeout').value.trim(); if (!stockInput) { alert('请输入代码'); @@ -386,7 +396,7 @@ market_type: marketType, api_url: apiUrl, api_key: apiKey, - api_model: apiModel + api_model: apiModel, api_timeout: apiTimeout }) }); @@ -438,7 +448,7 @@ } catch (error) { console.error('请求失败:', error); resultContent.innerHTML = ` -
+
分析出错:${error.message}
`; @@ -713,73 +723,153 @@