2025-12-18 16:23:16 +08:00

608 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
autoTAPD 主程序
Debug阶段自动开单工具
"""
import sys
import argparse
from pathlib import Path
from typing import Dict
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.config import ConfigManager
from src.smartsheet import SmartSheetAPI
from src.validator import RecordValidator
from src.tapd_api import TAPDApi
from src.mapper import FieldMapper, BugCreationResult
from src.token_manager import TokenManager
def parse_arguments():
"""
解析命令行参数
Returns:
argparse.Namespace: 解析后的参数对象
"""
parser = argparse.ArgumentParser(
description='autoTAPD - Debug阶段自动开单工具',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例用法:
# 手动提供access_token
python main.py --access-token YOUR_ACCESS_TOKEN
# 自动从环境变量获取access_token需要设置CORPID和CORPSECRET
python main.py
# 指定配置文件路径
python main.py --config /path/to/config.ini
"""
)
# 可选参数access_token如果不提供将自动从环境变量获取
parser.add_argument(
'-t', '--access-token',
required=False,
default=None,
help='企业微信access_token可选如不提供则自动从环境变量获取'
)
# 可选参数
parser.add_argument(
'-c', '--config',
default=None,
help='配置文件路径(默认: config/config.ini'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='显示详细输出信息'
)
parser.add_argument(
'--test',
action='store_true',
help='测试模式显示所有字段的ID和标题'
)
return parser.parse_args()
def get_or_validate_access_token(access_token_arg):
"""
获取或验证access_token
如果命令行提供了token则验证并使用
如果未提供则通过TokenManager自动获取
Args:
access_token_arg: 命令行传入的token可能为None
Returns:
str: 有效的access_token
Raises:
ValueError: token无效或环境变量未设置时抛出
RuntimeError: 自动获取token失败时抛出
"""
if access_token_arg:
# 命令行提供了token进行验证
print("使用命令行提供的access_token")
if not access_token_arg.strip():
raise ValueError("access_token不能为空")
return access_token_arg.strip()
else:
# 命令行未提供token使用TokenManager自动获取
print("命令行未提供access_token将自动从环境变量获取")
print()
try:
token_manager = TokenManager()
access_token = token_manager.get_token()
return access_token
except ValueError as e:
# 环境变量未设置
raise ValueError(f"自动获取access_token失败: {e}")
except RuntimeError as e:
# API调用失败
raise RuntimeError(f"自动获取access_token失败: {e}")
def scan_and_validate_records(access_token: str, docid: str, verbose: bool = False, test_mode: bool = False):
"""
扫描智能表格并校验记录
Args:
access_token: 企业微信access_token
docid: 智能表格文档ID
verbose: 是否显示详细信息
test_mode: 是否启用测试模式显示所有字段ID和标题
Returns:
Dict: 包含valid_records、invalid_records、fields和sheet_id的字典
"""
print("\n" + "=" * 60)
print("开始扫描智能表格")
print("=" * 60)
# 1. 初始化API
print("\n[1/5] 初始化智能表格API...")
api = SmartSheetAPI(access_token, docid, test_mode=test_mode)
print(" ✓ API初始化完成")
if test_mode:
print(" ⚠ 测试模式已启用将显示所有API调用的详细信息")
# 2. 获取子表列表
print("\n[2/5] 获取子表列表...")
sheet_list = api.get_sheet_list()
if not sheet_list:
raise RuntimeError("未找到任何子表")
# 使用第一个子表
sheet = sheet_list[0]
sheet_id = sheet['sheet_id']
sheet_title = sheet.get('title', '未命名')
print(f" ✓ 使用子表: {sheet_title} (ID: {sheet_id})")
# 3. 获取字段信息
print("\n[3/5] 获取字段信息...")
fields = api.get_fields(sheet_id)
field_mapping = api.build_field_mapping(fields)
# 测试模式显示所有字段的ID和标题
if test_mode:
print("\n" + "=" * 60)
print("【测试模式】智能表格字段信息")
print("=" * 60)
print(f"{'序号':<6} {'字段标题':<20} {'字段ID':<30} {'字段类型':<15}")
print("-" * 60)
for idx, field in enumerate(fields, 1):
field_title = field.get('field_title', '(无标题)')
field_id = field.get('field_id', '(无ID)')
field_type = field.get('field_type', '(未知)')
print(f"{idx:<6} {field_title:<20} {field_id:<30} {field_type:<15}")
print("=" * 60)
print(f"{len(fields)} 个字段\n")
if verbose:
print(f" 字段列表:")
for field_name in field_mapping.keys():
print(f" - {field_name}")
# 检查必需的字段是否存在
required_fields = ['标题', '详细描述', '优先级', '严重程度', '处理人', '验证人', '发现版本', '模块', '开单状态']
missing_fields = [f for f in required_fields if f not in field_mapping]
if missing_fields:
raise RuntimeError(f"智能表格缺少必需字段: {', '.join(missing_fields)}")
# 4. 获取"开单状态"为空的记录
print("\n[4/5] 查询待开单记录...")
status_field_id = field_mapping['开单状态']
records = api.get_empty_status_records(sheet_id, status_field_id)
if len(records) == 0:
print(" ✓ 没有待开单的记录")
return {
'valid_records': [],
'invalid_records': []
}
# 5. 提取记录数据并校验
print("\n[5/5] 提取并校验记录数据...")
records_data = []
for record in records:
# 现在直接根据字段标题提取数据不再需要field_mapping
record_data = api.extract_record_data(record)
records_data.append(record_data)
print(f" ✓ 成功提取 {len(records_data)} 条记录数据")
# 校验记录
print("\n开始校验记录...")
validator = RecordValidator()
validation_result = validator.validate_records(records_data)
# 返回结果包含字段信息和sheet_id
return {
'validation_result': validation_result,
'fields': fields,
'field_mapping': field_mapping,
'sheet_id': sheet_id
}
def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test_mode: bool = False) -> Dict:
"""
批量创建TAPD bug单
Args:
valid_records: 校验通过的记录列表
workspace_id: TAPD项目ID
reporter: Bug报告人
test_mode: 是否启用测试模式
Returns:
Dict: 包含成功和失败结果的字典
"""
print("\n" + "=" * 60)
print("开始创建TAPD bug单")
print("=" * 60)
if len(valid_records) == 0:
print("没有需要创建的bug单")
return {
'success_results': [],
'failed_results': []
}
# 1. 初始化TAPD API
print("\n[1/3] 初始化TAPD API...")
try:
tapd_api = TAPDApi(workspace_id, test_mode=test_mode)
except ValueError as e:
print(f"{e}")
raise
# 2. 初始化字段映射器
print("\n[2/3] 初始化字段映射器...")
mapper = FieldMapper(reporter=reporter)
print(f" ✓ 字段映射器初始化完成 (reporter: {reporter})")
# 3. 逐条创建bug单
print(f"\n[3/3] 开始创建bug单{len(valid_records)} 条)...")
print("-" * 60)
success_results = []
failed_results = []
for idx, record_data in enumerate(valid_records, 1):
record_id = record_data.get('record_id', '未知')
title = record_data.get('标题', '(无标题)')
print(f"\n[{idx}/{len(valid_records)}] 处理记录: {title}")
print(f" 记录ID: {record_id}")
try:
# 映射字段
tapd_data = mapper.map_record_to_tapd(record_data)
print(f" ✓ 字段映射完成")
# 创建bug
print(f" → 正在创建TAPD bug...")
bug_info = tapd_api.create_bug(tapd_data)
# 提取bug ID
bug_id = bug_info.get('id')
if not bug_id:
raise RuntimeError("API返回的bug信息中未找到ID")
# 生成bug URL
bug_url = tapd_api.get_bug_url(bug_id)
# 获取bug状态
bug_status = bug_info.get('status', '新建')
print(f" ✓ 创建成功!")
print(f" Bug ID: {bug_id}")
print(f" Bug URL: {bug_url}")
print(f" Bug 状态: {bug_status}")
# 记录成功结果
result = BugCreationResult(
record_id=record_id,
success=True,
bug_id=bug_id,
bug_url=bug_url
)
# 添加bug状态信息
result.bug_status = bug_status
success_results.append(result)
except ValueError as e:
# 字段映射错误
error_msg = f"字段映射失败: {e}"
print(f"{error_msg}")
result = BugCreationResult(
record_id=record_id,
success=False,
error_message=error_msg
)
failed_results.append(result)
except RuntimeError as e:
# TAPD API调用错误
error_msg = f"TAPD API调用失败: {e}"
print(f"{error_msg}")
result = BugCreationResult(
record_id=record_id,
success=False,
error_message=error_msg
)
failed_results.append(result)
except Exception as e:
# 其他未预期的错误
error_msg = f"未预期的错误: {type(e).__name__}: {e}"
print(f"{error_msg}")
result = BugCreationResult(
record_id=record_id,
success=False,
error_message=error_msg
)
failed_results.append(result)
# 显示汇总结果
print("\n" + "=" * 60)
print("TAPD开单结果汇总")
print("=" * 60)
print(f"总计: {len(valid_records)}")
print(f" ✓ 成功: {len(success_results)}")
print(f" ✗ 失败: {len(failed_results)}")
print("=" * 60)
# 显示失败详情
if len(failed_results) > 0:
print("\n失败记录详情:")
print("-" * 60)
for idx, result in enumerate(failed_results, 1):
print(f"\n[{idx}] 记录ID: {result.record_id}")
print(f" 错误: {result.error_message}")
print("-" * 60)
return {
'success_results': success_results,
'failed_results': failed_results
}
def write_back_results(access_token: str, docid: str, sheet_id: str,
success_results: list, failed_results: list, test_mode: bool = False) -> Dict:
"""
将开单结果回写到智能表格
Args:
access_token: 企业微信access_token
docid: 智能表格文档ID
sheet_id: 子表ID
success_results: 成功的结果列表
failed_results: 失败的结果列表
test_mode: 是否启用测试模式
Returns:
Dict: 包含回写结果的字典
"""
print("\n" + "=" * 60)
print("开始回写结果到智能表格")
print("=" * 60)
total_count = len(success_results) + len(failed_results)
if total_count == 0:
print("没有需要回写的记录")
return {
'success_count': 0,
'failed_count': 0
}
# 初始化智能表格API
print("\n[1/2] 初始化智能表格API...")
api = SmartSheetAPI(access_token, docid, test_mode=test_mode)
print(" ✓ API初始化完成")
# 准备更新记录
print(f"\n[2/2] 准备回写数据(共 {total_count} 条)...")
update_records = []
# 处理成功的记录
for result in success_results:
record_update = {
"record_id": result.record_id,
"values": {
"开单状态": [{"type": "text", "text": ""}],
"TAPD单号": [{
"type": "url",
"text": result.bug_id,
"link": result.bug_url
}],
"bug状态": [{"type": "text", "text": getattr(result, 'bug_status', '新建')}]
}
}
update_records.append(record_update)
# 处理失败的记录
for result in failed_results:
record_update = {
"record_id": result.record_id,
"values": {
"开单状态": [{"type": "text", "text": ""}]
}
}
update_records.append(record_update)
print(f" ✓ 准备完成: 成功 {len(success_results)} 条, 失败 {len(failed_results)}")
# 批量更新记录
try:
print(f"\n正在批量更新记录...")
api.update_records(sheet_id, update_records)
print("\n" + "=" * 60)
print("回写结果汇总")
print("=" * 60)
print(f"总计: {total_count}")
print(f" ✓ 成功回写: {len(success_results)} 条(开单成功)")
print(f" ✓ 成功回写: {len(failed_results)} 条(开单失败)")
print("=" * 60)
return {
'success_count': len(success_results),
'failed_count': len(failed_results)
}
except RuntimeError as e:
print(f"\n✗ 回写失败: {e}")
print("\n" + "=" * 60)
print("回写结果汇总")
print("=" * 60)
print(f"总计: {total_count}")
print(f" ✗ 回写失败: 所有记录")
print("=" * 60)
return {
'success_count': 0,
'failed_count': total_count,
'error': str(e)
}
def main():
"""主函数"""
print("=" * 60)
print("autoTAPD - Debug阶段自动开单工具")
print("版本: 0.4.0 (第四阶段 - 支持自动获取access_token)")
print("=" * 60)
print()
try:
# 1. 解析命令行参数
print("[1/3] 解析命令行参数...")
args = parse_arguments()
if args.verbose:
print(f" - access_token: {args.access_token[:10]}...(已隐藏)")
print(f" - config: {args.config or '使用默认路径'}")
print(f" - verbose: {args.verbose}")
print(f" - test: {args.test}")
# 2. 获取或验证access_token
print("[2/3] 获取access_token...")
access_token = get_or_validate_access_token(args.access_token)
print(" ✓ access_token准备就绪")
print()
# 3. 加载配置文件
print("[3/3] 加载配置文件...")
config_manager = ConfigManager(config_path=args.config)
# 获取并显示配置信息
all_config = config_manager.get_all_config()
print(" ✓ 配置文件加载成功")
print()
# 显示配置摘要
print("=" * 60)
print("配置摘要:")
print("-" * 60)
print(f"TAPD workspace_id: {all_config['tapd']['workspace_id']}")
print(f"SmartSheet docid: {all_config['smartsheet']['docid'][:20]}...")
print(f"Access Token: {access_token[:10]}...(已隐藏)")
if args.test:
print(f"测试模式: 已启用")
print("=" * 60)
# 4. 扫描智能表格并校验记录
result = scan_and_validate_records(
access_token,
all_config['smartsheet']['docid'],
args.verbose,
args.test
)
validation_result = result['validation_result']
# 5. 显示校验结果
validator = RecordValidator()
validator.print_validation_summary(validation_result)
# 显示校验通过的记录详情
if len(validation_result['valid_records']) > 0:
validator.print_valid_records_summary(validation_result['valid_records'])
# 6. 创建TAPD bug单
if len(validation_result['valid_records']) > 0:
creation_result = create_tapd_bugs(
validation_result['valid_records'],
all_config['tapd']['workspace_id'],
all_config['tapd']['reporter'],
args.test
)
# 7. 回写结果到智能表格
# 从result中获取sheet_id
sheet_id = result.get('sheet_id')
if sheet_id:
writeback_result = write_back_results(
access_token,
all_config['smartsheet']['docid'],
sheet_id,
creation_result['success_results'],
creation_result['failed_results'],
args.test
)
# 显示最终统计
print("\n" + "=" * 60)
print("执行完成")
print("=" * 60)
print(f"✓ 成功连接智能表格")
print(f"✓ 成功获取字段信息")
print(f"✓ 成功筛选待开单记录")
print(f"✓ 成功校验必填项")
print(f"✓ 成功创建 {len(creation_result['success_results'])} 个TAPD bug单")
if len(creation_result['failed_results']) > 0:
print(f"{len(creation_result['failed_results'])} 个bug单创建失败")
if 'writeback_result' in locals():
print(f"✓ 成功回写 {writeback_result.get('success_count', 0) + writeback_result.get('failed_count', 0)} 条记录到智能表格")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("执行完成")
print("=" * 60)
print(f"✓ 成功连接智能表格")
print(f"✓ 成功获取字段信息")
print(f"✓ 成功筛选待开单记录")
print(f"✓ 成功校验必填项")
print(f" 没有需要创建的bug单")
print("=" * 60)
return 0
except FileNotFoundError as e:
print(f"\n✗ 错误: {e}")
print("\n解决方案:")
print(" 1. 检查配置文件是否存在")
print(" 2. 使用 --config 参数指定正确的配置文件路径")
return 1
except ValueError as e:
print(f"\n✗ 错误: {e}")
print("\n解决方案:")
print(" 1. 检查配置文件中的配置项是否完整")
print(" 2. 确保所有必填项都已填写")
print(" 3. 检查access_token是否正确")
return 1
except Exception as e:
print(f"\n✗ 未预期的错误: {e}")
print(f"错误类型: {type(e).__name__}")
if args.verbose:
import traceback
print("\n详细错误信息:")
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())