|
|
@@ -7,6 +7,7 @@ InterTool 类 - AI 审查工具类
|
|
|
负责 AI 审查结果的辅助计算和结果处理功能。
|
|
|
"""
|
|
|
|
|
|
+import asyncio
|
|
|
import json
|
|
|
import re
|
|
|
from typing import Optional, Dict, Any, List, TypedDict
|
|
|
@@ -73,12 +74,14 @@ class InterTool:
|
|
|
logger.warning(f"风险等级计算异常: {str(e)},使用默认风险等级")
|
|
|
return DEFAULT_RISK_LEVEL
|
|
|
|
|
|
- def _aggregate_results(self, successful_results: List[List[Dict[str, Any]]]) -> Dict[str, Any]:
|
|
|
+ def _aggregate_results(self, successful_results: List) -> Dict[str, Any]:
|
|
|
"""
|
|
|
汇总审查结果(issues格式)
|
|
|
|
|
|
Args:
|
|
|
- successful_results: 成功的审查结果列表(issues格式),每个单元返回一个issues列表
|
|
|
+ successful_results: 成功的审查结果列表,支持两种格式:
|
|
|
+ - 嵌套列表格式:[[issue1, issue2], [issue3], ...],每个单元返回一个issues列表
|
|
|
+ - 扁平列表格式:[issue1, issue2, issue3, ...],所有issues合并为扁平列表
|
|
|
|
|
|
Returns:
|
|
|
Dict[str, Any]: 汇总后的统计信息,包含以下字段:
|
|
|
@@ -88,6 +91,7 @@ class InterTool:
|
|
|
|
|
|
Note:
|
|
|
当输入为空时返回空字典,异常时记录错误并返回空字典
|
|
|
+ 自动检测输入格式(嵌套列表 or 扁平列表)并分别处理
|
|
|
"""
|
|
|
try:
|
|
|
if not successful_results:
|
|
|
@@ -97,27 +101,62 @@ class InterTool:
|
|
|
risk_stats = {"low": 0, "medium": 0, "high": 0}
|
|
|
total_issues = 0
|
|
|
|
|
|
- for unit_issues in successful_results:
|
|
|
- # 每个unit_issues是一个issues列表
|
|
|
- if unit_issues and isinstance(unit_issues, list):
|
|
|
- total_issues += len(unit_issues)
|
|
|
+ # 检测输入格式(通过第一个元素判断是扁平列表还是嵌套列表)
|
|
|
+ is_flat_format = False
|
|
|
+ if successful_results and len(successful_results) > 0:
|
|
|
+ first_element = successful_results[0]
|
|
|
+ if isinstance(first_element, dict):
|
|
|
+ # 第一个元素是字典,说明是扁平列表格式
|
|
|
+ is_flat_format = True
|
|
|
+ logger.debug(f"检测到扁平列表格式,总元素数: {len(successful_results)}")
|
|
|
+ elif isinstance(first_element, list):
|
|
|
+ # 第一个元素是列表,说明是嵌套列表格式
|
|
|
+ logger.debug(f"检测到嵌套列表格式,总单元数: {len(successful_results)}")
|
|
|
+
|
|
|
+ if is_flat_format:
|
|
|
+ # 扁平列表格式: [issue1, issue2, issue3, ...]
|
|
|
+ # 每个 issue 是一个 dict: {issue_id: {risk_summary: {...}, ...}}
|
|
|
+ for issue in successful_results:
|
|
|
+ if isinstance(issue, dict):
|
|
|
+ for issue_data in issue.values():
|
|
|
+ risk_summary = issue_data.get('risk_summary', {})
|
|
|
+ max_risk = risk_summary.get('max_risk_level', '0')
|
|
|
+
|
|
|
+ if max_risk in risk_stats:
|
|
|
+ risk_stats[max_risk] += 1
|
|
|
+ elif max_risk == '0':
|
|
|
+ risk_stats['low'] += 1 # 无风险视为低风险
|
|
|
+
|
|
|
+ total_issues = len(successful_results)
|
|
|
+ total_reviewed = len(successful_results)
|
|
|
|
|
|
- # 统计每个issue中的风险等级
|
|
|
- for issue in unit_issues:
|
|
|
- if isinstance(issue, dict):
|
|
|
- # issue格式: {issue_id: {risk_summary: {...}}}
|
|
|
- for issue_data in issue.values():
|
|
|
- risk_summary = issue_data.get('risk_summary', {})
|
|
|
- max_risk = risk_summary.get('max_risk_level', '0')
|
|
|
+ else:
|
|
|
+ # 嵌套列表格式: [[issue1, issue2], [issue3], ...]
|
|
|
+ # 每个 unit_issues 是一个 issues 列表
|
|
|
+ for unit_issues in successful_results:
|
|
|
+ if unit_issues and isinstance(unit_issues, list):
|
|
|
+ total_issues += len(unit_issues)
|
|
|
+
|
|
|
+ # 统计每个issue中的风险等级
|
|
|
+ for issue in unit_issues:
|
|
|
+ if isinstance(issue, dict):
|
|
|
+ # issue格式: {issue_id: {risk_summary: {...}}}
|
|
|
+ for issue_data in issue.values():
|
|
|
+ risk_summary = issue_data.get('risk_summary', {})
|
|
|
+ max_risk = risk_summary.get('max_risk_level', '0')
|
|
|
+
|
|
|
+ if max_risk in risk_stats:
|
|
|
+ risk_stats[max_risk] += 1
|
|
|
+ elif max_risk == '0':
|
|
|
+ risk_stats['low'] += 1 # 无风险视为低风险
|
|
|
+
|
|
|
+ total_reviewed = len(successful_results)
|
|
|
|
|
|
- if max_risk in risk_stats:
|
|
|
- risk_stats[max_risk] += 1
|
|
|
- elif max_risk == '0':
|
|
|
- risk_stats['low'] += 1 # 无风险视为低风险
|
|
|
+ logger.info(f"结果汇总完成: total_issues={total_issues}, total_reviewed={total_reviewed}, risk_stats={risk_stats}")
|
|
|
|
|
|
return {
|
|
|
'risk_stats': risk_stats,
|
|
|
- 'total_reviewed': len(successful_results),
|
|
|
+ 'total_reviewed': total_reviewed,
|
|
|
'total_issues': total_issues
|
|
|
}
|
|
|
except (ZeroDivisionError, KeyError, TypeError) as e:
|
|
|
@@ -164,10 +203,30 @@ class InterTool:
|
|
|
# 合并所有审查结果
|
|
|
all_results = {}
|
|
|
|
|
|
- # check_item 模式:如果提供了 merged_results,直接使用
|
|
|
+ # check_item 模式:如果提供了 merged_results,需要特殊处理
|
|
|
if merged_results is not None:
|
|
|
logger.debug(f"使用 check_item 模式的合并结果: {list(merged_results.keys()) if isinstance(merged_results, dict) else 'N/A'}")
|
|
|
- all_results.update(merged_results)
|
|
|
+
|
|
|
+ # 检查是否是 entity_based 模式
|
|
|
+ if isinstance(merged_results, dict) and merged_results.get('review_mode') == 'entity_based' and 'entity_review_results' in merged_results:
|
|
|
+ # entity_based 模式:需要提取嵌套的审查结果
|
|
|
+ logger.debug(f"🔍 [DEBUG] merged_results 是 entity_based 模式,提取审查结果")
|
|
|
+ entity_review_results = merged_results.get('entity_review_results', [])
|
|
|
+ func_name = merged_results.get('func_name', '')
|
|
|
+
|
|
|
+ for idx, entity_item in enumerate(entity_review_results):
|
|
|
+ entity = entity_item.get('entity', f'entity_{idx}')
|
|
|
+
|
|
|
+ # ✅ 实际结构:result 字段包含 ReviewResult 对象
|
|
|
+ if 'result' in entity_item:
|
|
|
+ result_key = f'{func_name}_{entity}_{idx}' # 使用 func_name 作为键名前缀
|
|
|
+ all_results[result_key] = entity_item['result']
|
|
|
+ logger.debug(f"🔍 [DEBUG] 提取审查结果: {result_key}, 结果类型: {type(entity_item['result'])}")
|
|
|
+
|
|
|
+ logger.debug(f"🔍 [DEBUG] entity_based 模式处理完成,共提取 {len(entity_review_results)} 个实体的审查结果")
|
|
|
+ else:
|
|
|
+ # 普通模式:直接使用 merged_results
|
|
|
+ all_results.update(merged_results)
|
|
|
else:
|
|
|
# core_review 模式:合并 basic_result 和 technical_result
|
|
|
if basic_result:
|
|
|
@@ -176,44 +235,23 @@ class InterTool:
|
|
|
|
|
|
if technical_result:
|
|
|
logger.debug(f"🔍 [DEBUG] technical_result 类型: {type(technical_result)}, 键: {list(technical_result.keys()) if isinstance(technical_result, dict) else 'N/A'}")
|
|
|
-
|
|
|
- # 检查是否是 entity_based 模式
|
|
|
- if technical_result.get('review_mode') == 'entity_based' and 'entity_review_results' in technical_result:
|
|
|
- # entity_based 模式:从 entity_review_results 中提取实际审查结果
|
|
|
- logger.debug(f"🔍 [DEBUG] 检测到 entity_based 模式,从 entity_review_results 提取审查结果")
|
|
|
- entity_review_results = technical_result.get('entity_review_results', [])
|
|
|
- total_entities = technical_result.get('total_entities', 0)
|
|
|
-
|
|
|
- for idx, entity_item in enumerate(entity_review_results):
|
|
|
- entity = entity_item.get('entity', f'entity_{idx}')
|
|
|
- entity_info = f"{entity}_{idx}" # 使用 entity+索引 避免重复
|
|
|
-
|
|
|
- # 提取非参数性审查结果
|
|
|
- if 'non_parameter_compliance' in entity_item:
|
|
|
- result_key = f'non_parameter_compliance_{entity_info}'
|
|
|
- all_results[result_key] = entity_item['non_parameter_compliance']
|
|
|
- logger.debug(f"🔍 [DEBUG] 提取审查结果: {result_key}")
|
|
|
-
|
|
|
- # 提取参数性审查结果
|
|
|
- if 'parameter_compliance' in entity_item:
|
|
|
- result_key = f'parameter_compliance_{entity_info}'
|
|
|
- all_results[result_key] = entity_item['parameter_compliance']
|
|
|
- logger.debug(f"🔍 [DEBUG] 提取审查结果: {result_key}")
|
|
|
-
|
|
|
- logger.debug(f"🔍 [DEBUG] entity_based 模式处理完成,共提取 {len(entity_review_results)} 个实体的审查结果")
|
|
|
-
|
|
|
- else:
|
|
|
- # general 模式:过滤掉元数据字段,保留实际审查结果
|
|
|
- filtered_technical = {}
|
|
|
- metadata_keys = ['review_mode', 'entity_review_results', 'total_entities', 'total_results_processed']
|
|
|
- for key, value in technical_result.items():
|
|
|
- if key not in metadata_keys:
|
|
|
- filtered_technical[key] = value
|
|
|
- else:
|
|
|
- logger.debug(f"跳过技术审查元数据字段: {key} = {value} (类型: {type(value).__name__})")
|
|
|
-
|
|
|
- logger.debug(f"🔍 [DEBUG] 过滤后的 technical_result 键: {list(filtered_technical.keys())}")
|
|
|
- all_results.update(filtered_technical)
|
|
|
+ # 遍历 technical_result,处理可能的 entity_based 模式
|
|
|
+ for tech_key, tech_value in technical_result.items():
|
|
|
+ if isinstance(tech_value, dict) and tech_value.get('review_mode') == 'entity_based':
|
|
|
+ # entity_based 模式:提取嵌套的审查结果
|
|
|
+ logger.debug(f"🔍 [DEBUG] technical_result[{tech_key}] 是 entity_based 模式,提取审查结果")
|
|
|
+ entity_review_results = tech_value.get('entity_review_results', [])
|
|
|
+ func_name = tech_value.get('func_name', tech_key)
|
|
|
+
|
|
|
+ for idx, entity_item in enumerate(entity_review_results):
|
|
|
+ entity = entity_item.get('entity', f'entity_{idx}')
|
|
|
+ if 'result' in entity_item:
|
|
|
+ result_key = f'{func_name}_{entity}_{idx}'
|
|
|
+ all_results[result_key] = entity_item['result']
|
|
|
+ logger.debug(f"🔍 [DEBUG] 提取审查结果: {result_key}, 结果类型: {type(entity_item['result'])}")
|
|
|
+ else:
|
|
|
+ # 普通模式:直接添加
|
|
|
+ all_results[tech_key] = tech_value
|
|
|
|
|
|
logger.debug(f"开始格式化审查结果,合并后结果: {list(all_results.keys())}")
|
|
|
|
|
|
@@ -224,22 +262,32 @@ class InterTool:
|
|
|
logger.debug(f"跳过分数字段: {check_key}")
|
|
|
continue
|
|
|
|
|
|
- # 🔧 类型安全检查:确保 check_result 是字典类型
|
|
|
- if not isinstance(check_result, dict):
|
|
|
- logger.warning(f"⚠️ 检查项 {check_key} 的结果类型不是字典: {type(check_result).__name__}, 跳过")
|
|
|
+ # 🔧 类型安全检查:支持字典和 base_reviewer.ReviewResult 对象
|
|
|
+ is_dict = isinstance(check_result, dict)
|
|
|
+ is_review_result = hasattr(check_result, 'details') and hasattr(check_result, 'success')
|
|
|
+
|
|
|
+ if not is_dict and not is_review_result:
|
|
|
+ logger.warning(f"⚠️ 检查项 {check_key} 的结果类型不支持: {type(check_result).__name__}, 跳过")
|
|
|
continue
|
|
|
|
|
|
# 检查 check_result 是否为 None 或不包含 details
|
|
|
- if not check_result or "details" not in check_result:
|
|
|
+ if is_dict and (not check_result or "details" not in check_result):
|
|
|
logger.warning(f"检查项 {check_key} 结果为空或缺少details字段,跳过")
|
|
|
continue
|
|
|
|
|
|
- check_name = check_result["details"].get("name")
|
|
|
- if "response" in check_result["details"]:
|
|
|
- response = check_result["details"]["response"]
|
|
|
- check_name = check_result["details"].get("name")
|
|
|
- reference_source = check_result["details"].get("rag_reference_source")
|
|
|
- review_references = check_result["details"].get("rag_review_references")
|
|
|
+ if is_review_result and not check_result.details:
|
|
|
+ logger.warning(f"检查项 {check_key} 的 base_reviewer.ReviewResult 中 details 为空,跳过")
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 根据类型获取 details
|
|
|
+ details = check_result["details"] if is_dict else check_result.details
|
|
|
+
|
|
|
+ check_name = details.get("name")
|
|
|
+ if "response" in details:
|
|
|
+ response = details["response"]
|
|
|
+ check_name = details.get("name")
|
|
|
+ reference_source = details.get("rag_reference_source")
|
|
|
+ review_references = details.get("rag_review_references")
|
|
|
|
|
|
|
|
|
|
|
|
@@ -444,9 +492,26 @@ class InterTool:
|
|
|
|
|
|
return review_lists
|
|
|
|
|
|
- def _extract_json_data(self, response: str):
|
|
|
- """从响应中提取JSON数据,合并所有提取策略"""
|
|
|
+ def _extract_json_data(self, response):
|
|
|
+ """
|
|
|
+ 从响应中提取JSON数据,合并所有提取策略
|
|
|
+
|
|
|
+ Args:
|
|
|
+ response: 可以是字符串、列表或字典
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 提取的JSON数据(列表或字典),如果提取失败返回None
|
|
|
+ """
|
|
|
try:
|
|
|
+ # 如果 response 已经是列表或字典,直接返回
|
|
|
+ if isinstance(response, (list, dict)):
|
|
|
+ return response
|
|
|
+
|
|
|
+ # 确保 response 是字符串
|
|
|
+ if not isinstance(response, str):
|
|
|
+ logger.warning(f"响应类型错误: {type(response)}, 期望字符串")
|
|
|
+ return None
|
|
|
+
|
|
|
# 尝试直接解析整个响应
|
|
|
response_stripped = response.strip()
|
|
|
if ((response_stripped.startswith('{') and response_stripped.endswith('}')) or
|
|
|
@@ -467,10 +532,18 @@ class InterTool:
|
|
|
matches = re.findall(pattern, response, re.DOTALL)
|
|
|
if matches:
|
|
|
for match in matches:
|
|
|
- try:
|
|
|
- return json.loads(match.strip())
|
|
|
- except json.JSONDecodeError:
|
|
|
- continue
|
|
|
+ # 确保 match 是字符串
|
|
|
+ if isinstance(match, str):
|
|
|
+ try:
|
|
|
+ return json.loads(match.strip())
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ continue
|
|
|
+ # 如果 match 是列表/元组(多捕获组),取第一个元素
|
|
|
+ elif isinstance(match, (list, tuple)) and len(match) > 0:
|
|
|
+ try:
|
|
|
+ return json.loads(str(match[0]).strip())
|
|
|
+ except (json.JSONDecodeError, IndexError):
|
|
|
+ continue
|
|
|
|
|
|
# 尝试模式匹配提取JSON
|
|
|
json_patterns = [
|
|
|
@@ -482,10 +555,12 @@ class InterTool:
|
|
|
for pattern in json_patterns:
|
|
|
matches = re.findall(pattern, response, re.DOTALL)
|
|
|
for match in matches:
|
|
|
- try:
|
|
|
- return json.loads(match)
|
|
|
- except json.JSONDecodeError:
|
|
|
- continue
|
|
|
+ # 确保 match 是字符串
|
|
|
+ if isinstance(match, str):
|
|
|
+ try:
|
|
|
+ return json.loads(match)
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ continue
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"JSON提取失败: {str(e)}")
|
|
|
@@ -565,4 +640,143 @@ class InterTool:
|
|
|
"""
|
|
|
if state.get("error_message"):
|
|
|
return "error"
|
|
|
- return "success"
|
|
|
+ return "success"
|
|
|
+
|
|
|
+ async def _complete_format(self,result, state, unit_index, total_units, completeness_config):
|
|
|
+ section_label = completeness_config.get('section_label')
|
|
|
+ chapter_code = completeness_config.get('chapter_code', '')
|
|
|
+ logger.info(f"section_label: {section_label}")
|
|
|
+ # 格式化issues以获取问题数量
|
|
|
+ issues = self._format_review_results_to_issues(
|
|
|
+ state["callback_task_id"],
|
|
|
+ unit_index,
|
|
|
+ section_label,
|
|
|
+ chapter_code,
|
|
|
+ completeness_config,
|
|
|
+ result.basic_compliance,
|
|
|
+ result.technical_compliance
|
|
|
+ )
|
|
|
+
|
|
|
+ current = int(((unit_index + 1) / total_units) * 100)
|
|
|
+
|
|
|
+ # 立即发送单元审查详情(包含unit_review和processing_flag事件)
|
|
|
+ await self._send_unit_review_progress(state, unit_index, total_units, section_label, issues, current)
|
|
|
+
|
|
|
+ return issues
|
|
|
+
|
|
|
+ async def _send_unit_review_progress(self, state: AIReviewState, unit_index: int,
|
|
|
+ total_units: int, section_label: str,
|
|
|
+ issues: List[Dict], current: int) -> None:
|
|
|
+ """
|
|
|
+ 发送单元审查详细信息 - 强制串行并统一进度值
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 1. 计算问题数量
|
|
|
+ issues_count = 0
|
|
|
+ if isinstance(issues, list) and issues:
|
|
|
+ issues_count = sum(
|
|
|
+ 1 for issue in issues
|
|
|
+ for issue_data in issue.values()
|
|
|
+ for review_item in issue_data.get("review_lists", [])
|
|
|
+ if review_item.get("exist_issue", False)
|
|
|
+ )
|
|
|
+
|
|
|
+ real_current = await self._send_unit_overall_progress(
|
|
|
+ state, unit_index, total_units, section_label, issues_count
|
|
|
+ )
|
|
|
+
|
|
|
+ final_current = real_current if real_current is not None else current
|
|
|
+
|
|
|
+ await asyncio.sleep(0.05)
|
|
|
+
|
|
|
+ # 3. 发送单元详情 (Unit Review)
|
|
|
+ if isinstance(issues, list) and issues and state["progress_manager"]:
|
|
|
+ stage_name = f"AI审查:{section_label}"
|
|
|
+
|
|
|
+ await state["progress_manager"].update_stage_progress(
|
|
|
+ callback_task_id=state["callback_task_id"],
|
|
|
+ stage_name=stage_name,
|
|
|
+ current=final_current, # 【关键】使用与 Flag 事件完全一致的进度值
|
|
|
+ status="unit_review_update",
|
|
|
+ message=f"发现{issues_count}个问题: {section_label}",
|
|
|
+ issues=issues,
|
|
|
+ user_id=state.get("user_id", ""),
|
|
|
+ overall_task_status="processing",
|
|
|
+ event_type="unit_review"
|
|
|
+ )
|
|
|
+
|
|
|
+ # 再次微小延迟,确保 Clear 不会吞掉 Review
|
|
|
+ await asyncio.sleep(0.02)
|
|
|
+
|
|
|
+ # 清空当前issues
|
|
|
+ await state["progress_manager"].update_stage_progress(
|
|
|
+ callback_task_id=state["callback_task_id"],
|
|
|
+ issues=['clear']
|
|
|
+ )
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"发送单元审查详情失败: {str(e)}")
|
|
|
+
|
|
|
+ async def _send_unit_overall_progress(self, state: AIReviewState, unit_index: int,
|
|
|
+ total_units: int, section_label: str,
|
|
|
+ issues_count: int = None) -> Optional[int]:
|
|
|
+ """
|
|
|
+ 发送单元完成进度更新 - 返回计算出的实时进度
|
|
|
+ Returns:
|
|
|
+ int: 基于 Redis 统计的实时进度百分比
|
|
|
+ """
|
|
|
+ current_percent = None
|
|
|
+ try:
|
|
|
+ task_id = state.get("callback_task_id", "")
|
|
|
+ redis_client = None
|
|
|
+ try:
|
|
|
+ from foundation.infrastructure.cache.redis_connection import RedisConnectionFactory
|
|
|
+ redis_client = await RedisConnectionFactory.get_connection()
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Redis连接失败: {str(e)}")
|
|
|
+
|
|
|
+ completed_count = 0
|
|
|
+
|
|
|
+ if redis_client and task_id:
|
|
|
+ completed_key = f"ai_review:overall_task_progress:{task_id}:completed"
|
|
|
+ # 原子操作
|
|
|
+ await redis_client.sadd(completed_key, str(unit_index))
|
|
|
+ await redis_client.expire(completed_key, 3600)
|
|
|
+ completed_count = await redis_client.scard(completed_key)
|
|
|
+
|
|
|
+ # 计算进度
|
|
|
+ current_percent = int((completed_count / total_units) * 100)
|
|
|
+ else:
|
|
|
+ # 降级方案
|
|
|
+ completed_count = unit_index + 1
|
|
|
+ current_percent = int((completed_count / total_units) * 100)
|
|
|
+
|
|
|
+ # 构建消息
|
|
|
+ if issues_count is not None and issues_count > 0:
|
|
|
+ message = f"已完成第 {completed_count}/{total_units} 个单元: {section_label}(已发现{issues_count}个问题)"
|
|
|
+ else:
|
|
|
+ message = f"已完成第 {completed_count}/{total_units} 个单元: {section_label}"
|
|
|
+
|
|
|
+ logger.info(f"进度更新: {current_percent}% - {message}")
|
|
|
+
|
|
|
+ if state["progress_manager"]:
|
|
|
+ await state["progress_manager"].update_stage_progress(
|
|
|
+ callback_task_id=state["callback_task_id"],
|
|
|
+ stage_name="AI审查",
|
|
|
+ current=current_percent,
|
|
|
+ status="processing",
|
|
|
+ message=message,
|
|
|
+ user_id=state.get("user_id", ""),
|
|
|
+ overall_task_status="processing",
|
|
|
+ event_type="processing_flag"
|
|
|
+ )
|
|
|
+
|
|
|
+ return current_percent
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"发送单元完成进度更新失败: {str(e)}")
|
|
|
+ # 发生异常时,尝试返回一个基于 index 的估算值
|
|
|
+ try:
|
|
|
+ return int(((unit_index + 1) / total_units) * 100)
|
|
|
+ except:
|
|
|
+ return 0
|