diff --git a/alas.py b/alas.py index a8f03eabe..241fe36d2 100644 --- a/alas.py +++ b/alas.py @@ -298,39 +298,50 @@ class AzurLaneAutoScript: from module.base.utils import save_image from module.handler.sensitive_info import (handle_sensitive_image, handle_sensitive_logs) - if self.config.Error_SaveError: + + # LLM Error Analysis - 放到最前面,防止后面截图保存时二次崩溃导致分析没跑 + try: + if hasattr(self, 'config') and getattr(self.config, 'Error_LlmAnalysis', False): + from module.llm import analyze_exception + import sys + _, exc_value, _ = sys.exc_info() + if exc_value is not None: + analyze_exception(self.config, exc_value) + except Exception as e: + logger.error(f'LLM Analysis failed: {e}') + + if getattr(self.config, 'Error_SaveError', False): config_folder = pathlib.Path(f"./log/error/{self.config_name}") folder = config_folder.joinpath(str(int(time.time() * 1000))) folder.mkdir(parents=True, exist_ok=True) logger.warning(f'保存错误日志: {folder}') - for data in self.device.screenshot_deque: - image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') - image = handle_sensitive_image(data['image']) - save_image(image, f'{folder}/{image_time}.png') - with open(logger.log_file, 'r', encoding='utf-8') as f: - lines = f.readlines() - start = 0 - for index, line in enumerate(lines): - line = line.strip(' \r\t\n') - if re.match('^═{15,}$', line): - start = index - lines = lines[start - 2:] - lines = handle_sensitive_logs(lines) - with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f: - f.writelines(lines) - self.keep_last_errlog(config_folder, self.config.Error_SaveErrorCount) - - # LLM Error Analysis - if self.config.Error_LlmAnalysis: - try: - from module.llm import analyze_exception - import sys - _, exc_value, _ = sys.exc_info() - if exc_value is not None: - analyze_exception(self.config, exc_value) - except Exception as e: - logger.error(f'LLM Analysis failed: {e}') + try: + # 只在已经初始化了设备时才尝试保存截图,避免按需初始化时二次崩溃 + if 'device' in self.__dict__: + for data in self.device.screenshot_deque: + image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f') + image = handle_sensitive_image(data['image']) + save_image(image, f'{folder}/{image_time}.png') + except Exception as e: + logger.error(f"Save error screenshot failed: {e}") + + try: + with open(logger.log_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + start = 0 + for index, line in enumerate(lines): + line = line.strip(' \r\t\n') + if re.match('^═{15,}$', line): + start = index + lines = lines[start - 2:] + lines = handle_sensitive_logs(lines) + with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f: + f.writelines(lines) + except Exception as e: + logger.error(f"Save error logs failed: {e}") + + self.keep_last_errlog(config_folder, getattr(self.config, 'Error_SaveErrorCount', 0)) def restart(self): from module.handler.login import LoginHandler @@ -844,6 +855,15 @@ class AzurLaneAutoScript: logger.error("调度器循环中发生意外的全局异常!") import traceback logger.error(traceback.format_exc()) # 打印完整的错误堆栈 + + # 即使没有达到重启或失败上限,也第一时间自动请求分析崩溃原因 + try: + if hasattr(self, 'config') and getattr(self.config, 'Error_LlmAnalysis', False): + from module.llm import analyze_exception + analyze_exception(self.config, e) + except Exception as ex: + logger.error(f'LLM Analysis failed: {ex}') + logger.warning( f">>> 这是第 {consecutive_global_failures} 次连续全局失败,共 {MAX_GLOBAL_FAILURES} 次。" ) diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 8613c0ad7..e22e2fc1d 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -456,7 +456,7 @@ }, "LlmModel": { "type": "input", - "value": "Nvidia/deepseek-ai/deepseek-v3.1", + "value": "Nvidia/qwen/qwen2.5-coder-32b-instruct", "valuetype": "str" } }, diff --git a/module/llm.py b/module/llm.py index eba6ead8f..8c6022e9e 100644 --- a/module/llm.py +++ b/module/llm.py @@ -2,6 +2,10 @@ import traceback import os from module.logger import logger +import hashlib + +_analyzed_errors_cache = {} + def analyze_exception(config, e): """ Analyze the exception using LLM. @@ -12,6 +16,26 @@ def analyze_exception(config, e): """ if not hasattr(config, 'Error_LlmAnalysis') or not config.Error_LlmAnalysis: return + + tb = ''.join(traceback.format_exception(type(e), e, e.__traceback__)) + + # 错误去重:计算当前堆栈报错的哈希值,如果最近已经分析过,直接跳过,防止浪费 API 和时间 + error_hash = hashlib.md5(tb.encode('utf-8')).hexdigest() + if error_hash in _analyzed_errors_cache: + cached_result = _analyzed_errors_cache[error_hash] + model = getattr(config, 'Error_LlmModel', 'gpt-4o-mini') + logger.hr('LLM 错误分析', level=1) + logger.info('该错误已被 LLM 分析过,直接复用上次的分析结果以节省 API...') + logger.info(f"\n[LLM 分析报告 (由 {model} 提供, 复用缓存)]\n{cached_result}\n") + logger.hr('LLM 分析结束', level=1) + return + + # 加入缓存占位符,防止等待期间如果又疯狂连续报错导致重复发送请求 + _analyzed_errors_cache[error_hash] = "该错误正在被 LLM 分析中或上次分析失败,暂无结果。" + # 控制缓存大小,防止内存泄漏(保留最近 50 个报错记录) + if len(_analyzed_errors_cache) > 50: + _analyzed_errors_cache.clear() + _analyzed_errors_cache[error_hash] = "该错误正在被 LLM 分析中或上次分析失败,暂无结果。" api_key = getattr(config, 'Error_LlmApiKey', '') api_base = getattr(config, 'Error_LlmApiBase', 'https://api.openai.com/v1') @@ -75,7 +99,9 @@ def analyze_exception(config, e): ) analysis = response.choices[0].message.content.strip() - logger.info(f"\n[LLM 分析报告]\n{analysis}\n") + # 覆写真正的成果进字典 + _analyzed_errors_cache[error_hash] = analysis + logger.info(f"\n[LLM 分析报告 (由 {model} 提供)]\n{analysis}\n") logger.hr('LLM 分析结束', level=1) except ImportError: