608 lines
19 KiB
Python
608 lines
19 KiB
Python
"""
|
||
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())
|