Merge branch 'pr-1'
# Conflicts: # stock_analyzer.py # templates/index.html # web_server.py
This commit is contained in:
60
logger.py
60
logger.py
@@ -2,9 +2,7 @@ from loguru import logger
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import shutil
|
||||||
# 获取当前时间作为日志文件名的一部分
|
|
||||||
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
|
|
||||||
# 创建日志目录
|
# 创建日志目录
|
||||||
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
||||||
@@ -17,42 +15,54 @@ logger.remove() # 移除默认的处理器
|
|||||||
logger.add(
|
logger.add(
|
||||||
sys.stdout,
|
sys.stdout,
|
||||||
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||||
level="DEBUG"
|
level="INFO", # 同时显示在控制台和写入到日志文件中
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加文件处理器(debug级别)
|
# 添加统一的日志文件处理器,按日期自动轮转
|
||||||
logger.add(
|
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}",
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}",
|
||||||
level="DEBUG",
|
level="DEBUG",
|
||||||
rotation="100 MB",
|
rotation="00:00", # 每天午夜轮转
|
||||||
retention="1 week"
|
retention="7 days", # 保留7天的日志
|
||||||
|
compression="zip", # 压缩旧日志文件
|
||||||
|
enqueue=True # 使用队列写入,提高性能
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加文件处理器(error级别)
|
# 添加错误日志文件处理器,专门记录错误信息
|
||||||
logger.add(
|
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}",
|
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}",
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
rotation="100 MB",
|
rotation="00:00", # 每天午夜轮转
|
||||||
retention="1 month"
|
retention="7 days", # 保留7天的错误日志
|
||||||
|
compression="zip", # 压缩旧日志文件
|
||||||
|
enqueue=True # 使用队列写入,提高性能
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加流处理器(用于记录流式输出)
|
def clean_old_logs(max_days=7):
|
||||||
logger.add(
|
"""清理超过指定天数的日志文件"""
|
||||||
os.path.join(log_dir, f"stream_{current_time}.log"),
|
try:
|
||||||
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {message}",
|
today = datetime.now()
|
||||||
filter=lambda record: "STREAM" in record["extra"],
|
for filename in os.listdir(log_dir):
|
||||||
level="INFO"
|
file_path = os.path.join(log_dir, filename)
|
||||||
)
|
# 跳过目录
|
||||||
|
if os.path.isdir(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
# 创建专用于流式输出的日志器
|
# 检查文件修改时间
|
||||||
stream_logger = logger.bind(STREAM=True)
|
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():
|
def get_logger():
|
||||||
"""获取通用日志器"""
|
"""获取通用日志器"""
|
||||||
|
# 启动时清理旧日志
|
||||||
|
clean_old_logs()
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
def get_stream_logger():
|
|
||||||
"""获取流式输出专用日志器"""
|
|
||||||
return stream_logger
|
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import requests
|
|||||||
from typing import Dict, List, Optional, Tuple, Generator
|
from typing import Dict, List, Optional, Tuple, Generator
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import json
|
import json
|
||||||
from logger import get_logger, get_stream_logger
|
from logger import get_logger
|
||||||
|
|
||||||
# 获取日志器
|
# 获取日志器
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
stream_logger = get_stream_logger()
|
|
||||||
|
|
||||||
class StockAnalyzer:
|
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()
|
load_dotenv()
|
||||||
@@ -21,10 +20,10 @@ class StockAnalyzer:
|
|||||||
# 设置 API 配置,优先使用自定义配置,否则使用环境变量
|
# 设置 API 配置,优先使用自定义配置,否则使用环境变量
|
||||||
self.API_URL = custom_api_url or os.getenv('API_URL')
|
self.API_URL = custom_api_url or os.getenv('API_URL')
|
||||||
self.API_KEY = custom_api_key or os.getenv('API_KEY')
|
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_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 = {
|
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配置
|
# 检查API配置
|
||||||
if not self.API_URL:
|
if not self.API_URL:
|
||||||
@@ -261,19 +260,14 @@ class StockAnalyzer:
|
|||||||
return error_msg if not stream else (yield json.dumps({"error": error_msg}))
|
return error_msg if not stream else (yield json.dumps({"error": error_msg}))
|
||||||
|
|
||||||
# 标准化API URL
|
# 标准化API URL
|
||||||
if self.API_URL.endswith('/'):
|
api_url = self.API_URL
|
||||||
api_url = f"{self.API_URL}chat/completions"
|
if not (api_url.endswith('/chat/completions') or api_url.endswith('/v1/chat/completions')):
|
||||||
else:
|
if api_url.endswith('/v1'):
|
||||||
api_url = f"{self.API_URL}/v1/chat/completions"
|
api_url = f"{api_url}/chat/completions"
|
||||||
# 标准化API URL
|
elif api_url.endswith('/'):
|
||||||
# api_url = self.API_URL
|
api_url = f"{api_url}v1/chat/completions"
|
||||||
# if not (api_url.endswith('/chat/completions') or api_url.endswith('/v1/chat/completions')):
|
else:
|
||||||
# if api_url.endswith('/v1'):
|
api_url = f"{api_url}/v1/chat/completions"
|
||||||
# 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}")
|
logger.debug(f"标准化后的API URL: {api_url}")
|
||||||
|
|
||||||
@@ -295,13 +289,13 @@ class StockAnalyzer:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"发起流式API请求: {api_url}")
|
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(
|
response = requests.post(
|
||||||
api_url,
|
api_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=60, # 增加超时时间
|
timeout=self.API_TIMEOUT, # 增加超时时间
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -313,7 +307,7 @@ class StockAnalyzer:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
error_response = response.json()
|
error_response = response.json()
|
||||||
error_text = json.dumps(error_response, indent=2)
|
error_text = self._truncate_json_for_logging(error_response)
|
||||||
except:
|
except:
|
||||||
error_text = response.text[:500] if response.text else "无响应内容"
|
error_text = response.text[:500] if response.text else "无响应内容"
|
||||||
|
|
||||||
@@ -335,7 +329,7 @@ class StockAnalyzer:
|
|||||||
api_url,
|
api_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=60
|
timeout=self.API_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"API非流式响应状态码: {response.status_code}")
|
logger.debug(f"API非流式响应状态码: {response.status_code}")
|
||||||
@@ -349,7 +343,7 @@ class StockAnalyzer:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
error_response = response.json()
|
error_response = response.json()
|
||||||
error_text = json.dumps(error_response, indent=2)
|
error_text = self._truncate_json_for_logging(error_response)
|
||||||
except:
|
except:
|
||||||
error_text = response.text[:500] if response.text else "无响应内容"
|
error_text = response.text[:500] if response.text else "无响应内容"
|
||||||
|
|
||||||
@@ -371,11 +365,26 @@ class StockAnalyzer:
|
|||||||
if stream:
|
if stream:
|
||||||
logger.debug("在流式模式下返回异常信息")
|
logger.debug("在流式模式下返回异常信息")
|
||||||
error_json = json.dumps({"stock_code": stock_code, "error": error_msg})
|
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
|
yield error_json
|
||||||
else:
|
else:
|
||||||
return error_msg
|
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]:
|
def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]:
|
||||||
"""处理AI流式响应"""
|
"""处理AI流式响应"""
|
||||||
logger.info(f"开始处理股票 {stock_code} 的AI流式响应")
|
logger.info(f"开始处理股票 {stock_code} 的AI流式响应")
|
||||||
@@ -386,7 +395,6 @@ class StockAnalyzer:
|
|||||||
for line in response.iter_lines():
|
for line in response.iter_lines():
|
||||||
if line:
|
if line:
|
||||||
line = line.decode('utf-8')
|
line = line.decode('utf-8')
|
||||||
stream_logger.info(f"原始流式行: {line}")
|
|
||||||
|
|
||||||
# 跳过保持连接的空行
|
# 跳过保持连接的空行
|
||||||
if line.strip() == '':
|
if line.strip() == '':
|
||||||
@@ -396,7 +404,6 @@ class StockAnalyzer:
|
|||||||
# 数据行通常以"data: "开头
|
# 数据行通常以"data: "开头
|
||||||
if line.startswith('data: '):
|
if line.startswith('data: '):
|
||||||
data_content = line[6:] # 移除 "data: " 前缀
|
data_content = line[6:] # 移除 "data: " 前缀
|
||||||
stream_logger.info(f"数据内容: {data_content}")
|
|
||||||
|
|
||||||
# 检查是否为流的结束
|
# 检查是否为流的结束
|
||||||
if data_content.strip() == '[DONE]':
|
if data_content.strip() == '[DONE]':
|
||||||
@@ -405,7 +412,6 @@ class StockAnalyzer:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
json_data = json.loads(data_content)
|
json_data = json.loads(data_content)
|
||||||
logger.debug(f"解析的JSON数据: {json.dumps(json_data)[:100]}...")
|
|
||||||
|
|
||||||
if 'choices' in json_data:
|
if 'choices' in json_data:
|
||||||
delta = json_data['choices'][0].get('delta', {})
|
delta = json_data['choices'][0].get('delta', {})
|
||||||
@@ -414,15 +420,12 @@ class StockAnalyzer:
|
|||||||
if content:
|
if content:
|
||||||
chunk_count += 1
|
chunk_count += 1
|
||||||
buffer += content
|
buffer += content
|
||||||
logger.debug(f"收到内容片段 #{chunk_count}: {content}")
|
|
||||||
stream_logger.info(f"发送内容片段: {content}")
|
|
||||||
|
|
||||||
# 创建包含AI分析片段的JSON
|
# 创建包含AI分析片段的JSON
|
||||||
chunk_json = json.dumps({
|
chunk_json = json.dumps({
|
||||||
"stock_code": stock_code,
|
"stock_code": stock_code,
|
||||||
"ai_analysis_chunk": content
|
"ai_analysis_chunk": content
|
||||||
})
|
})
|
||||||
stream_logger.info(f"流式输出JSON: {chunk_json}")
|
|
||||||
yield chunk_json
|
yield chunk_json
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"JSON解析错误: {str(e)}, 行内容: {data_content}")
|
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',
|
'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
|
||||||
'recommendation': self.get_recommendation(score)
|
'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:
|
if stream:
|
||||||
logger.info(f"以流式模式返回股票 {stock_code} 分析结果")
|
logger.info(f"以流式模式返回股票 {stock_code} 分析结果")
|
||||||
@@ -502,8 +505,8 @@ class StockAnalyzer:
|
|||||||
base_report = dict(report)
|
base_report = dict(report)
|
||||||
base_report['ai_analysis'] = ''
|
base_report['ai_analysis'] = ''
|
||||||
base_report_json = json.dumps(base_report)
|
base_report_json = json.dumps(base_report)
|
||||||
logger.debug(f"基础报告JSON: {base_report_json[:100]}...")
|
logger.debug(f"基础报告JSON: {self._truncate_json_for_logging(base_report_json, 100)}...")
|
||||||
stream_logger.info(f"发送基础报告: {base_report_json}")
|
logger.info(f"发送基础报告: {base_report_json}")
|
||||||
yield base_report_json
|
yield base_report_json
|
||||||
|
|
||||||
# 然后流式返回AI分析部分
|
# 然后流式返回AI分析部分
|
||||||
@@ -511,7 +514,6 @@ class StockAnalyzer:
|
|||||||
ai_chunks_count = 0
|
ai_chunks_count = 0
|
||||||
for ai_chunk in self.get_ai_analysis(df, stock_code, stream=True):
|
for ai_chunk in self.get_ai_analysis(df, stock_code, stream=True):
|
||||||
ai_chunks_count += 1
|
ai_chunks_count += 1
|
||||||
stream_logger.info(f"股票 {stock_code} 流式块 #{ai_chunks_count}: {ai_chunk}")
|
|
||||||
yield ai_chunk
|
yield ai_chunk
|
||||||
logger.info(f"股票 {stock_code} 流式AI分析完成,共发送 {ai_chunks_count} 个块")
|
logger.info(f"股票 {stock_code} 流式AI分析完成,共发送 {ai_chunks_count} 个块")
|
||||||
else:
|
else:
|
||||||
@@ -528,7 +530,7 @@ class StockAnalyzer:
|
|||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
error_json = json.dumps({'stock_code': stock_code, 'error': error_msg})
|
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
|
yield error_json
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@@ -570,7 +572,6 @@ class StockAnalyzer:
|
|||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
for chunk in self.analyze_stock(stock_code, market_type, stream=True):
|
for chunk in self.analyze_stock(stock_code, market_type, stream=True):
|
||||||
chunk_count += 1
|
chunk_count += 1
|
||||||
stream_logger.info(f"股票 {stock_code} 流式块 #{chunk_count}: {chunk}")
|
|
||||||
yield chunk
|
yield chunk
|
||||||
logger.debug(f"股票 {stock_code} 流式分析完成,共 {chunk_count} 个块")
|
logger.debug(f"股票 {stock_code} 流式分析完成,共 {chunk_count} 个块")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -578,6 +579,6 @@ class StockAnalyzer:
|
|||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
error_json = json.dumps({'stock_code': stock_code, 'error': error_msg})
|
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
|
yield error_json
|
||||||
logger.info(f"流式扫描完成,处理了 {stock_count} 只股票")
|
logger.info(f"流式扫描完成,处理了 {stock_count} 只股票")
|
||||||
|
|||||||
@@ -63,28 +63,37 @@
|
|||||||
placeholder="例如: gpt-3.5-turbo"
|
placeholder="例如: gpt-3.5-turbo"
|
||||||
value="{{ default_api_model }}">
|
value="{{ default_api_model }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="apiTimeout" class="block text-sm font-medium text-gray-700 mb-1">API 超时时间</label>
|
<label for="apiKey" class="block text-sm font-medium text-gray-700 mb-1">API Key</label>
|
||||||
<input type="text" id="apiTimeout"
|
<input type="password" id="apiKey"
|
||||||
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="输入您的API Key">
|
||||||
|
<p class="mt-1 text-sm text-gray-500">如不填写,将使用系统默认配置</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="apiTimeout" class="block text-sm font-medium text-gray-700 mb-1">API 超时时间 (秒)</label>
|
||||||
|
<input type="number" id="apiTimeout"
|
||||||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
placeholder="例如: 60"
|
placeholder="例如: 60"
|
||||||
value="{{ default_api_timeout }}">
|
value="{{ default_api_timeout }}" min="1" max="300">
|
||||||
|
<p class="mt-1 text-sm text-gray-500">请求超时时间,默认60秒</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex justify-between">
|
||||||
<label for="apiKey" class="block text-sm font-medium text-gray-700 mb-1">API Key</label>
|
<div class="flex items-center">
|
||||||
<input type="password" id="apiKey"
|
<input type="checkbox" id="saveApiConfig" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
<label for="saveApiConfig" class="ml-2 block text-sm text-gray-700">保存配置到本地</label>
|
||||||
placeholder="输入您的API Key">
|
</div>
|
||||||
<p class="mt-1 text-sm text-gray-500">如不填写,将使用系统默认配置</p>
|
<div>
|
||||||
</div>
|
<button id="resetApiConfig" class="text-gray-600 hover:text-gray-800 text-sm mr-3">
|
||||||
<div class="flex justify-end">
|
重置为默认
|
||||||
<button id="resetApiConfig" class="text-gray-600 hover:text-gray-800 text-sm mr-3">
|
</button>
|
||||||
重置为默认
|
<button id="testApiConfig" class="bg-blue-100 text-blue-700 px-3 py-1 rounded hover:bg-blue-200 text-sm">
|
||||||
</button>
|
测试连接
|
||||||
<button id="testApiConfig" class="bg-blue-100 text-blue-700 px-3 py-1 rounded hover:bg-blue-200 text-sm">
|
</button>
|
||||||
测试连接
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -350,6 +359,7 @@
|
|||||||
const apiUrl = document.getElementById('apiUrl').value.trim();
|
const apiUrl = document.getElementById('apiUrl').value.trim();
|
||||||
const apiKey = document.getElementById('apiKey').value.trim();
|
const apiKey = document.getElementById('apiKey').value.trim();
|
||||||
const apiModel = document.getElementById('apiModel').value.trim();
|
const apiModel = document.getElementById('apiModel').value.trim();
|
||||||
|
const apiTimeout = document.getElementById('apiTimeout').value.trim();
|
||||||
|
|
||||||
if (!stockInput) {
|
if (!stockInput) {
|
||||||
alert('请输入代码');
|
alert('请输入代码');
|
||||||
@@ -386,7 +396,7 @@
|
|||||||
market_type: marketType,
|
market_type: marketType,
|
||||||
api_url: apiUrl,
|
api_url: apiUrl,
|
||||||
api_key: apiKey,
|
api_key: apiKey,
|
||||||
api_model: apiModel
|
api_model: apiModel,
|
||||||
api_timeout: apiTimeout
|
api_timeout: apiTimeout
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -438,7 +448,7 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('请求失败:', error);
|
console.error('请求失败:', error);
|
||||||
resultContent.innerHTML = `
|
resultContent.innerHTML = `
|
||||||
<div class="p-4 bg-red-50 text-red-600 rounded">
|
<div class="p-6 bg-yellow-50 text-yellow-600 rounded-lg text-center">
|
||||||
分析出错:${error.message}
|
分析出错:${error.message}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -713,73 +723,153 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// API配置面板切换
|
// API配置面板切换
|
||||||
const toggleBtn = document.getElementById('toggleApiConfig');
|
const toggleApiConfig = document.getElementById('toggleApiConfig');
|
||||||
const configPanel = document.getElementById('apiConfigPanel');
|
const apiConfigPanel = document.getElementById('apiConfigPanel');
|
||||||
const toggleText = document.getElementById('toggleApiConfigText');
|
const toggleApiConfigText = document.getElementById('toggleApiConfigText');
|
||||||
const toggleIcon = document.getElementById('toggleApiConfigIcon');
|
const toggleApiConfigIcon = document.getElementById('toggleApiConfigIcon');
|
||||||
|
const apiUrl = document.getElementById('apiUrl');
|
||||||
|
const apiKey = document.getElementById('apiKey');
|
||||||
|
const apiModel = document.getElementById('apiModel');
|
||||||
|
const apiTimeout = document.getElementById('apiTimeout');
|
||||||
|
const saveApiConfig = document.getElementById('saveApiConfig');
|
||||||
|
const resetApiConfig = document.getElementById('resetApiConfig');
|
||||||
|
const testApiConfig = document.getElementById('testApiConfig');
|
||||||
|
|
||||||
toggleBtn.addEventListener('click', function() {
|
// 从localStorage加载保存的配置
|
||||||
const isHidden = configPanel.classList.contains('hidden');
|
loadApiConfig();
|
||||||
configPanel.classList.toggle('hidden', !isHidden);
|
|
||||||
toggleText.textContent = isHidden ? '隐藏配置' : '显示配置';
|
// 切换API配置面板显示/隐藏
|
||||||
toggleIcon.style.transform = isHidden ? 'rotate(180deg)' : '';
|
toggleApiConfig.addEventListener('click', function() {
|
||||||
|
apiConfigPanel.classList.toggle('hidden');
|
||||||
|
|
||||||
|
if (apiConfigPanel.classList.contains('hidden')) {
|
||||||
|
toggleApiConfigText.textContent = '显示配置';
|
||||||
|
toggleApiConfigIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>';
|
||||||
|
} else {
|
||||||
|
toggleApiConfigText.textContent = '隐藏配置';
|
||||||
|
toggleApiConfigIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重置API配置
|
// 重置API配置
|
||||||
document.getElementById('resetApiConfig').addEventListener('click', function() {
|
resetApiConfig.addEventListener('click', function() {
|
||||||
document.getElementById('apiUrl').value = '{{ default_api_url }}';
|
apiUrl.value = '{{ default_api_url }}';
|
||||||
document.getElementById('apiModel').value = '{{ default_api_model }}';
|
apiKey.value = '';
|
||||||
document.getElementById('apiKey').value = '';
|
apiModel.value = '{{ default_api_model }}';
|
||||||
|
apiTimeout.value = '{{ default_api_timeout }}';
|
||||||
|
saveApiConfig.checked = false;
|
||||||
|
|
||||||
|
// 清除localStorage中的配置
|
||||||
|
localStorage.removeItem('apiConfig');
|
||||||
|
|
||||||
|
alert('已重置为默认配置');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 测试API连接
|
// 测试API连接
|
||||||
document.getElementById('testApiConfig').addEventListener('click', async function() {
|
testApiConfig.addEventListener('click', async function() {
|
||||||
const apiUrl = document.getElementById('apiUrl').value.trim();
|
const url = apiUrl.value.trim();
|
||||||
const apiKey = document.getElementById('apiKey').value.trim();
|
const key = apiKey.value.trim();
|
||||||
const apiModel = document.getElementById('apiModel').value.trim();
|
const model = apiModel.value.trim();
|
||||||
|
const timeout = apiTimeout.value.trim();
|
||||||
|
|
||||||
if (!apiUrl) {
|
if (!url) {
|
||||||
alert('请输入API URL');
|
alert('请输入API URL');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!key) {
|
||||||
alert('请输入API Key');
|
alert('请输入API Key');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.textContent = '测试中...';
|
|
||||||
this.disabled = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用后端代理进行API测试
|
testApiConfig.disabled = true;
|
||||||
|
testApiConfig.textContent = '测试中...';
|
||||||
|
|
||||||
const response = await fetch('/test_api_connection', {
|
const response = await fetch('/test_api_connection', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
api_url: apiUrl,
|
api_url: url,
|
||||||
api_key: apiKey,
|
api_key: key,
|
||||||
api_model: apiModel
|
api_model: model,
|
||||||
|
api_timeout: timeout
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
if (result.success) {
|
||||||
alert('API连接成功!');
|
alert(result.message);
|
||||||
|
|
||||||
|
// 如果勾选了保存配置,则保存到localStorage
|
||||||
|
if (saveApiConfig.checked) {
|
||||||
|
saveApiConfigToLocalStorage();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert(`API连接失败: ${data.message || '未知错误'}`);
|
alert(result.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`API连接测试失败: ${error.message}`);
|
alert(error.message);
|
||||||
} finally {
|
} finally {
|
||||||
this.textContent = '测试连接';
|
testApiConfig.disabled = false;
|
||||||
this.disabled = false;
|
testApiConfig.textContent = '测试连接';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听输入变化,自动保存配置
|
||||||
|
[apiUrl, apiKey, apiModel, apiTimeout].forEach(input => {
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
if (saveApiConfig.checked) {
|
||||||
|
saveApiConfigToLocalStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听保存配置复选框变化
|
||||||
|
saveApiConfig.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
saveApiConfigToLocalStorage();
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('apiConfig');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 保存API配置到localStorage
|
||||||
|
function saveApiConfigToLocalStorage() {
|
||||||
|
const apiConfig = {
|
||||||
|
url: document.getElementById('apiUrl').value.trim(),
|
||||||
|
model: document.getElementById('apiModel').value.trim(),
|
||||||
|
key: document.getElementById('apiKey').value.trim(),
|
||||||
|
timeout: document.getElementById('apiTimeout').value.trim(),
|
||||||
|
saveEnabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem('apiConfig', JSON.stringify(apiConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从localStorage加载API配置
|
||||||
|
function loadApiConfig() {
|
||||||
|
const savedConfig = localStorage.getItem('apiConfig');
|
||||||
|
|
||||||
|
if (savedConfig) {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
|
||||||
|
if (config.url) document.getElementById('apiUrl').value = config.url;
|
||||||
|
if (config.model) document.getElementById('apiModel').value = config.model;
|
||||||
|
if (config.key) document.getElementById('apiKey').value = config.key;
|
||||||
|
if (config.timeout) document.getElementById('apiTimeout').value = config.timeout;
|
||||||
|
|
||||||
|
document.getElementById('saveApiConfig').checked = config.saveEnabled || false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载API配置时出错:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// 添加 Markdown 样式
|
// 添加 Markdown 样式
|
||||||
|
|||||||
@@ -8,6 +8,25 @@ from dotenv import load_dotenv
|
|||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
stream_logger = get_stream_logger()
|
stream_logger = get_stream_logger()
|
||||||
|
|
||||||
|
def _truncate_json_for_logging(json_obj, max_length=500):
|
||||||
|
"""截断JSON对象用于日志记录,避免日志过大
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_obj: 要截断的JSON对象
|
||||||
|
max_length: 最大字符长度,默认500
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 截断后的JSON字符串
|
||||||
|
"""
|
||||||
|
if isinstance(json_obj, str):
|
||||||
|
json_str = json_obj
|
||||||
|
else:
|
||||||
|
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 test_api_stream():
|
def test_api_stream():
|
||||||
"""
|
"""
|
||||||
测试API流式响应功能
|
测试API流式响应功能
|
||||||
@@ -57,7 +76,7 @@ def test_api_stream():
|
|||||||
"stream": True # 明确设置stream参数为True
|
"stream": True # 明确设置stream参数为True
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(f"请求载荷: {json.dumps(payload, indent=2)}")
|
logger.debug(f"请求载荷: {_truncate_json_for_logging(payload)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"发起流式API请求: {api_url}")
|
logger.info(f"发起流式API请求: {api_url}")
|
||||||
@@ -66,7 +85,7 @@ def test_api_stream():
|
|||||||
api_url,
|
api_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=60,
|
timeout=int(os.getenv('API_TIMEOUT', 60)),
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,7 +121,7 @@ def test_api_stream():
|
|||||||
try:
|
try:
|
||||||
# 解析JSON数据
|
# 解析JSON数据
|
||||||
json_data = json.loads(data_content)
|
json_data = json.loads(data_content)
|
||||||
logger.debug(f"JSON结构: {json.dumps(json_data, indent=2)}")
|
logger.debug(f"JSON结构: {_truncate_json_for_logging(json_data)}")
|
||||||
|
|
||||||
if 'choices' in json_data:
|
if 'choices' in json_data:
|
||||||
delta = json_data['choices'][0].get('delta', {})
|
delta = json_data['choices'][0].get('delta', {})
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import threading
|
|||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import requests
|
import requests
|
||||||
from logger import get_logger, get_stream_logger
|
from logger import get_logger
|
||||||
|
|
||||||
# 获取日志器
|
# 获取日志器
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
stream_logger = get_stream_logger()
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
analyzer = StockAnalyzer()
|
analyzer = StockAnalyzer()
|
||||||
@@ -21,11 +20,13 @@ def index():
|
|||||||
# 获取默认API配置信息
|
# 获取默认API配置信息
|
||||||
default_api_url = os.getenv('API_URL', '')
|
default_api_url = os.getenv('API_URL', '')
|
||||||
default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo')
|
default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo')
|
||||||
|
default_api_timeout = os.getenv('API_TIMEOUT', '60')
|
||||||
# 不传递API_KEY到前端,出于安全考虑
|
# 不传递API_KEY到前端,出于安全考虑
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
announcement=announcement,
|
announcement=announcement,
|
||||||
default_api_url=default_api_url,
|
default_api_url=default_api_url,
|
||||||
default_api_model=default_api_model)
|
default_api_model=default_api_model,
|
||||||
|
default_api_timeout=default_api_timeout)
|
||||||
|
|
||||||
@app.route('/analyze', methods=['POST'])
|
@app.route('/analyze', methods=['POST'])
|
||||||
def analyze():
|
def analyze():
|
||||||
@@ -41,16 +42,16 @@ def analyze():
|
|||||||
custom_api_url = data.get('api_url')
|
custom_api_url = data.get('api_url')
|
||||||
custom_api_key = data.get('api_key')
|
custom_api_key = data.get('api_key')
|
||||||
custom_api_model = data.get('api_model')
|
custom_api_model = data.get('api_model')
|
||||||
custom_api_timeout = data.get('api_timeout', 60)
|
custom_api_timeout = data.get('api_timeout')
|
||||||
|
|
||||||
logger.debug(f"自定义API配置: URL={custom_api_url}, 模型={custom_api_model}, API Key={'已提供' if custom_api_key else '未提供'}")
|
logger.debug(f"自定义API配置: URL={custom_api_url}, 模型={custom_api_model}, API Key={'已提供' if custom_api_key else '未提供'}, Timeout={custom_api_timeout}")
|
||||||
|
|
||||||
# 创建新的分析器实例,使用自定义配置
|
# 创建新的分析器实例,使用自定义配置
|
||||||
custom_analyzer = StockAnalyzer(
|
custom_analyzer = StockAnalyzer(
|
||||||
custom_api_url=custom_api_url,
|
custom_api_url=custom_api_url,
|
||||||
custom_api_key=custom_api_key,
|
custom_api_key=custom_api_key,
|
||||||
custom_api_model=custom_api_model,
|
custom_api_model=custom_api_model,
|
||||||
custom_api_timeout= custom_api_timeout,
|
custom_api_timeout=custom_api_timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
if not stock_codes:
|
if not stock_codes:
|
||||||
@@ -64,31 +65,33 @@ def analyze():
|
|||||||
stock_code = stock_codes[0].strip()
|
stock_code = stock_codes[0].strip()
|
||||||
logger.info(f"开始单股流式分析: {stock_code}")
|
logger.info(f"开始单股流式分析: {stock_code}")
|
||||||
|
|
||||||
stream_logger.info(f"初始化单股分析流: {stock_code}")
|
|
||||||
init_message = f'{{"stream_type": "single", "stock_code": "{stock_code}"}}\n'
|
init_message = f'{{"stream_type": "single", "stock_code": "{stock_code}"}}\n'
|
||||||
stream_logger.info(f"发送初始化消息: {init_message}")
|
|
||||||
yield init_message
|
yield init_message
|
||||||
|
|
||||||
|
logger.debug(f"开始处理股票 {stock_code} 的流式响应")
|
||||||
|
chunk_count = 0
|
||||||
for chunk in custom_analyzer.analyze_stock(stock_code, market_type, stream=True):
|
for chunk in custom_analyzer.analyze_stock(stock_code, market_type, stream=True):
|
||||||
stream_logger.info(f"流式输出块: {chunk}")
|
chunk_count += 1
|
||||||
yield chunk + '\n'
|
yield chunk + '\n'
|
||||||
|
logger.info(f"股票 {stock_code} 流式分析完成,共发送 {chunk_count} 个块")
|
||||||
else:
|
else:
|
||||||
# 批量分析流式处理
|
# 批量分析流式处理
|
||||||
logger.info(f"开始批量流式分析: {stock_codes}")
|
logger.info(f"开始批量流式分析: {stock_codes}")
|
||||||
|
|
||||||
stream_logger.info(f"初始化批量分析流: {stock_codes}")
|
|
||||||
init_message = f'{{"stream_type": "batch", "stock_codes": {stock_codes}}}\n'
|
init_message = f'{{"stream_type": "batch", "stock_codes": {stock_codes}}}\n'
|
||||||
stream_logger.info(f"发送初始化消息: {init_message}")
|
|
||||||
yield init_message
|
yield init_message
|
||||||
|
|
||||||
|
logger.debug(f"开始处理批量股票的流式响应")
|
||||||
|
chunk_count = 0
|
||||||
for chunk in custom_analyzer.scan_market(
|
for chunk in custom_analyzer.scan_market(
|
||||||
[code.strip() for code in stock_codes],
|
[code.strip() for code in stock_codes],
|
||||||
min_score=0,
|
min_score=0,
|
||||||
market_type=market_type,
|
market_type=market_type,
|
||||||
stream=True
|
stream=True
|
||||||
):
|
):
|
||||||
stream_logger.info(f"流式输出块: {chunk}")
|
chunk_count += 1
|
||||||
yield chunk + '\n'
|
yield chunk + '\n'
|
||||||
|
logger.info(f"批量流式分析完成,共发送 {chunk_count} 个块")
|
||||||
|
|
||||||
logger.info("成功创建流式响应生成器")
|
logger.info("成功创建流式响应生成器")
|
||||||
return Response(stream_with_context(generate()), mimetype='application/json')
|
return Response(stream_with_context(generate()), mimetype='application/json')
|
||||||
@@ -122,8 +125,9 @@ def test_api_connection():
|
|||||||
api_url = data.get('api_url')
|
api_url = data.get('api_url')
|
||||||
api_key = data.get('api_key')
|
api_key = data.get('api_key')
|
||||||
api_model = data.get('api_model')
|
api_model = data.get('api_model')
|
||||||
|
api_timeout = data.get('api_timeout', 10) # 默认测试连接超时为10秒
|
||||||
|
|
||||||
logger.debug(f"测试API连接: URL={api_url}, 模型={api_model}, API Key={'已提供' if api_key else '未提供'}")
|
logger.debug(f"测试API连接: URL={api_url}, 模型={api_model}, API Key={'已提供' if api_key else '未提供'}, Timeout={api_timeout}")
|
||||||
|
|
||||||
if not api_url:
|
if not api_url:
|
||||||
logger.warning("未提供API URL")
|
logger.warning("未提供API URL")
|
||||||
@@ -159,25 +163,25 @@ def test_api_connection():
|
|||||||
],
|
],
|
||||||
"max_tokens": 20
|
"max_tokens": 20
|
||||||
},
|
},
|
||||||
timeout=10
|
timeout=int(api_timeout)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查响应
|
# 检查响应
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
logger.info(f"API连接测试成功: {response.status_code}")
|
logger.info(f"API 连接测试成功: {response.status_code}")
|
||||||
return jsonify({'success': True, 'message': '连接成功'})
|
return jsonify({'success': True, 'message': 'API 连接测试成功'})
|
||||||
else:
|
else:
|
||||||
error_message = response.json().get('error', {}).get('message', '未知错误')
|
error_message = response.json().get('error', {}).get('message', '未知错误')
|
||||||
logger.warning(f"API连接测试失败: {response.status_code} - {error_message}")
|
logger.warning(f"API连接测试失败: {response.status_code} - {error_message}")
|
||||||
return jsonify({'success': False, 'message': f'连接失败: {error_message}', 'status_code': response.status_code}), 400
|
return jsonify({'success': False, 'message': f'API 连接测试失败: {error_message}', 'status_code': response.status_code}), 400
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"API连接请求错误: {str(e)}")
|
logger.error(f"API 连接请求错误: {str(e)}")
|
||||||
return jsonify({'success': False, 'message': f'请求错误: {str(e)}'}), 400
|
return jsonify({'success': False, 'message': f'请求错误: {str(e)}'}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"测试API连接时出错: {str(e)}")
|
logger.error(f"测试 API 连接时出错: {str(e)}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return jsonify({'success': False, 'message': f'测试连接时出错: {str(e)}'}), 500
|
return jsonify({'success': False, 'message': f'API 测试连接时出错: {str(e)}'}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logger.info("股票分析系统启动")
|
logger.info("股票分析系统启动")
|
||||||
|
|||||||
Reference in New Issue
Block a user