diff --git a/config/config_task2.ini b/config/config_task2.ini index cda961e..f6d1cee 100644 --- a/config/config_task2.ini +++ b/config/config_task2.ini @@ -6,7 +6,10 @@ workspace_id = 58335167 [SmartSheet] # 智能表格文档ID(任务二专用) -docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ +# 支持配置多个docid,用逗号分隔,例如: +# docid = doc1,doc2,doc3 +# 同步时会依次对所有表格进行同步 +docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg [Schedule] # 同步频率(分钟) diff --git a/config/token_cache.json b/config/token_cache.json index 12cb6ce..225817f 100644 --- a/config/token_cache.json +++ b/config/token_cache.json @@ -1,4 +1,4 @@ { - "access_token": "9-OFdr2nbGo-1sSAYBujyvTtP5v88W0MjLSy3BzHayMEw0tX0tQ8qsbYXshNdHURsLqjXh4dVaRs2HIxvy2gXbWglBg90YtJU_3Yxfz-EXkCUT6Yyt1U1Z6ojUzWkwG6esUX9rWndHWK7hdXusZj5vpHBqVyQpY1Pi3eVKpB28puC_7IXsgbUaW5-JenDP3C7AnAs_x7jbsECXD8Y2XjZymhBKEwf_jZH-iK1Ojnvxk", - "fetch_time": 1768214302.441097 + "access_token": "zM0aHsLt58v2YLVMa5YUiLys9JxCUbcS9bFV-gBAi1DrD6b3sG80ym5XPEJRqQYIffqy5XwrwbTmCbfd9OZtlhGS99D6pRtWl0xrX2QPlpV4DsPTrt4XoDHej47dqd59Z0CcpuZR2Ue8tsH-Vfe3b6f0a6JzKAAeK1mIA0PiAeCT9kSJIruNSlmnQHBYK_6GW0CsR9swct36YGTy5pp7sCIz7iYH91fHCDalobsaoF0", + "fetch_time": 1768286609.6865482 } \ No newline at end of file diff --git a/src2/config.py b/src2/config.py index 0dd1c46..48428ca 100644 --- a/src2/config.py +++ b/src2/config.py @@ -11,6 +11,7 @@ project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from src.config import ConfigManager as BaseConfigManager +from typing import List class Task2ConfigManager(BaseConfigManager): @@ -50,6 +51,46 @@ class Task2ConfigManager(BaseConfigManager): 'workspace_id': workspace_id } + def get_smartsheet_config(self): + """ + 获取智能表格配置(任务二版本,支持多个docid) + + Returns: + dict: 包含docid_list的字典 + + Raises: + ValueError: 配置项缺失时抛出 + """ + if not self.config.has_section('SmartSheet'): + raise ValueError("配置文件缺少[SmartSheet]节") + + if not self.config.has_option('SmartSheet', 'docid'): + raise ValueError("配置文件[SmartSheet]节缺少docid配置项") + + docid_raw = self.config.get('SmartSheet', 'docid').strip() + if not docid_raw: + raise ValueError("docid配置项不能为空") + + # 解析逗号分隔的多个docid + docid_list = [d.strip() for d in docid_raw.split(',') if d.strip()] + + if not docid_list: + raise ValueError("docid配置项解析后为空") + + return { + 'docid': docid_list[0], # 保持向后兼容,返回第一个docid + 'docid_list': docid_list # 新增:返回所有docid列表 + } + + def get_docid_list(self) -> List[str]: + """ + 获取所有配置的docid列表 + + Returns: + List[str]: docid列表 + """ + return self.get_smartsheet_config()['docid_list'] + def get_schedule_config(self): """ 获取调度配置(任务二版本,只需要sync_interval) @@ -120,7 +161,12 @@ class Task2ConfigManager(BaseConfigManager): try: smartsheet_config = self.get_smartsheet_config() print(f"[SmartSheet]") - print(f" docid: {smartsheet_config['docid']}") + docid_list = smartsheet_config['docid_list'] + print(f" docid数量: {len(docid_list)}") + for i, docid in enumerate(docid_list, 1): + # 显示docid的前20个字符,便于识别 + display_id = docid[:20] + "..." if len(docid) > 20 else docid + print(f" docid_{i}: {display_id}") except ValueError as e: print(f"[SmartSheet] 配置错误: {e}") diff --git a/src2/notifier.py b/src2/notifier.py index fe9b85e..b5c9158 100644 --- a/src2/notifier.py +++ b/src2/notifier.py @@ -46,53 +46,83 @@ def send_sync_failure_notification(access_token: str, agentid: str, def _build_sync_failure_message(failed_records: List[Dict]) -> str: """ - 构造同步失败消息内容(支持多子表分组) + 构造同步失败消息内容(支持多表格、多子表分组) Args: - failed_records: 失败记录列表 + failed_records: 失败记录列表,每条记录可包含: + - doc_index: 表格序号(可选) + - docid_short: 表格ID简写(可选) + - sheet_title: 子表标题 + - record_id: 记录ID + - tapd_link: TAPD链接 + - error_message: 失败原因 Returns: str: 格式化的消息内容 """ - # 按子表分组 - records_by_sheet = {} + # 按表格和子表分组 + records_by_doc = {} for record in failed_records: + doc_index = record.get('doc_index', 1) + docid_short = record.get('docid_short', '') + doc_key = (doc_index, docid_short) + + if doc_key not in records_by_doc: + records_by_doc[doc_key] = {} + sheet_title = record.get('sheet_title', '未知子表') - if sheet_title not in records_by_sheet: - records_by_sheet[sheet_title] = [] - records_by_sheet[sheet_title].append(record) + if sheet_title not in records_by_doc[doc_key]: + records_by_doc[doc_key][sheet_title] = [] + records_by_doc[doc_key][sheet_title].append(record) # 消息头部 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") total_count = len(failed_records) - sheet_count = len(records_by_sheet) + doc_count = len(records_by_doc) lines = [ "【autoTAPD 同步失败通知】", f"时间: {timestamp}", - f"失败数量: {total_count} 条(来自 {sheet_count} 个子表)", + f"失败数量: {total_count} 条", + ] + + # 如果有多个表格,显示表格数量 + if doc_count > 1: + lines.append(f"涉及表格: {doc_count} 个") + + lines.extend([ "", "以下记录同步失败,请检查:", "=" * 40 - ] + ]) - # 按子表分组显示失败记录 + # 按表格和子表分组显示失败记录 global_idx = 1 - for sheet_title, sheet_records in records_by_sheet.items(): - lines.append(f"\n【子表:{sheet_title}】") - lines.append("") + for (doc_index, docid_short), sheets in sorted(records_by_doc.items()): + # 如果有多个表格,显示表格标识 + if doc_count > 1: + doc_label = f"表格{doc_index}" + if docid_short: + doc_label += f" ({docid_short})" + lines.append(f"\n{'#'*20}") + lines.append(f"# {doc_label}") + lines.append(f"{'#'*20}") - for record in sheet_records: - record_id = record.get('record_id', '未知') - tapd_link = record.get('tapd_link', '(无链接)') - error_message = record.get('error_message', '未知错误') - - lines.append(f"[{global_idx}] 记录ID: {record_id}") - lines.append(f"TAPD链接: {tapd_link}") - lines.append(f"失败原因: {error_message}") + for sheet_title, sheet_records in sheets.items(): + lines.append(f"\n【子表:{sheet_title}】") lines.append("") - global_idx += 1 + for record in sheet_records: + record_id = record.get('record_id', '未知') + tapd_link = record.get('tapd_link', '(无链接)') + error_message = record.get('error_message', '未知错误') + + lines.append(f"[{global_idx}] 记录ID: {record_id}") + lines.append(f"TAPD链接: {tapd_link}") + lines.append(f"失败原因: {error_message}") + lines.append("") + + global_idx += 1 lines.append("=" * 40) lines.append("系统将在下次同步时自动重试失败记录。") diff --git a/src2/sync_service.py b/src2/sync_service.py index fd7f64e..9303a4b 100644 --- a/src2/sync_service.py +++ b/src2/sync_service.py @@ -25,7 +25,7 @@ from src2.smartsheet_sync import SmartSheetSync, REQUIRED_FIELDS class SyncService: - """TAPD状态同步服务""" + """TAPD状态同步服务(支持多表格同步)""" def __init__(self, config_manager: Task2ConfigManager = None, access_token: str = None, test_mode: bool = False): @@ -49,7 +49,10 @@ class SyncService: # 获取配置 self.config = self.config_manager.get_all_config() self.workspace_id = self.config['tapd']['workspace_id'] - self.docid = self.config['smartsheet']['docid'] + + # 获取所有docid列表 + self.docid_list = self.config['smartsheet']['docid_list'] + print(f" 配置了 {len(self.docid_list)} 个智能表格") # 获取access_token if access_token is None: @@ -58,11 +61,10 @@ class SyncService: else: self.access_token = access_token - # 初始化API模块 + # 初始化TAPD API(所有表格共用) self.tapd_api = TAPDStoryApi(self.workspace_id, test_mode=test_mode) - self.smartsheet = SmartSheetSync(self.access_token, self.docid, test_mode=test_mode) - # 获取计划字段映射(每次同步时实时获取) + # 获取计划字段映射(每次同步时实时获取,所有表格共用) print(f" 正在获取计划字段映射...") try: self.plan_mapping = self.tapd_api.get_plan_mapping() @@ -75,7 +77,7 @@ class SyncService: def sync_once(self) -> Dict[str, Any]: """ - 执行一次完整的同步流程 + 执行一次完整的同步流程(遍历所有配置的智能表格) Returns: Dict: 同步结果统计 @@ -84,6 +86,9 @@ class SyncService: "success": False, "start_time": datetime.now().isoformat(), "end_time": None, + "docs_total": len(self.docid_list), + "docs_success": 0, + "docs_failed": 0, "sheets_processed": 0, "sheets_skipped": 0, "total_records": 0, @@ -92,68 +97,137 @@ class SyncService: "records_updated": 0, "records_failed": 0, "error_message": None, - "sheet_results": [] + "doc_results": [] # 每个表格的详细结果 + } + + all_failed_records = [] # 汇总所有表格的失败记录 + + # 遍历所有配置的智能表格 + for doc_index, docid in enumerate(self.docid_list, 1): + doc_result = self._sync_single_doc(docid, doc_index, len(self.docid_list)) + result["doc_results"].append(doc_result) + + if doc_result["success"]: + result["docs_success"] += 1 + # 累加统计数据 + result["sheets_processed"] += doc_result["sheets_processed"] + result["sheets_skipped"] += doc_result["sheets_skipped"] + result["total_records"] += doc_result["total_records"] + result["records_with_link"] += doc_result["records_with_link"] + result["records_synced"] += doc_result["records_synced"] + result["records_updated"] += doc_result["records_updated"] + result["records_failed"] += doc_result["records_failed"] + # 收集失败记录 + all_failed_records.extend(doc_result.get("all_failed_records", [])) + else: + result["docs_failed"] += 1 + + # 判断整体是否成功(至少有一个表格成功) + result["success"] = result["docs_success"] > 0 + + # 发送失败通知(汇总所有表格的失败记录) + if all_failed_records: + self._send_failure_notification(all_failed_records) + + result["end_time"] = datetime.now().isoformat() + return result + + def _sync_single_doc(self, docid: str, doc_index: int, total_docs: int) -> Dict[str, Any]: + """ + 同步单个智能表格 + + Args: + docid: 智能表格文档ID + doc_index: 当前表格序号(从1开始) + total_docs: 表格总数 + + Returns: + Dict: 单个表格的同步结果 + """ + # 显示docid的前16个字符便于识别 + display_id = docid[:16] + "..." if len(docid) > 16 else docid + + print(f"\n{'#'*70}") + print(f"# [表格 {doc_index}/{total_docs}] docid: {display_id}") + print(f"{'#'*70}") + + doc_result = { + "docid": docid, + "doc_index": doc_index, + "success": False, + "sheets_processed": 0, + "sheets_skipped": 0, + "total_records": 0, + "records_with_link": 0, + "records_synced": 0, + "records_updated": 0, + "records_failed": 0, + "error_message": None, + "sheet_results": [], + "all_failed_records": [] } try: - # 1. 获取所有子表 + # 为当前表格创建SmartSheetSync实例 + smartsheet = SmartSheetSync(self.access_token, docid, test_mode=self.test_mode) + self.current_smartsheet = smartsheet # 供_process_sheet等方法使用 + + # 获取所有子表 print("\n正在获取子表列表...") - sheets = self.smartsheet.api.get_sheet_list() + sheets = smartsheet.api.get_sheet_list() print(f" ✓ 找到 {len(sheets)} 个子表") - # 2. 处理每个子表 + # 处理每个子表 for sheet in sheets: sheet_id = sheet.get('sheet_id', '') sheet_title = sheet.get('title', '未命名') - sheet_result = self._process_sheet(sheet_id, sheet_title) - result["sheet_results"].append(sheet_result) + sheet_result = self._process_sheet(sheet_id, sheet_title, doc_index) + doc_result["sheet_results"].append(sheet_result) if sheet_result["skipped"]: - result["sheets_skipped"] += 1 + doc_result["sheets_skipped"] += 1 else: - result["sheets_processed"] += 1 - result["total_records"] += sheet_result["total_records"] - result["records_with_link"] += sheet_result["records_with_link"] - result["records_synced"] += sheet_result["records_synced"] - result["records_updated"] += sheet_result["records_updated"] - result["records_failed"] += sheet_result["records_failed"] + doc_result["sheets_processed"] += 1 + doc_result["total_records"] += sheet_result["total_records"] + doc_result["records_with_link"] += sheet_result["records_with_link"] + doc_result["records_synced"] += sheet_result["records_synced"] + doc_result["records_updated"] += sheet_result["records_updated"] + doc_result["records_failed"] += sheet_result["records_failed"] - result["success"] = True + # 收集失败记录(添加表格标识) + for failed in sheet_result.get("failed_records", []): + failed["doc_index"] = doc_index + failed["docid_short"] = display_id + doc_result["all_failed_records"].append(failed) - # 3. 汇总所有失败记录并发送推送通知 - all_failed_records = [] - for sheet_result in result["sheet_results"]: - all_failed_records.extend(sheet_result.get("failed_records", [])) - - # 如果有失败记录,尝试发送推送通知 - if all_failed_records: - self._send_failure_notification(all_failed_records) + doc_result["success"] = True + print(f"\n✓ [表格 {doc_index}/{total_docs}] 同步完成") except Exception as e: - result["error_message"] = str(e) - print(f"\n✗ 同步失败: {e}") + doc_result["error_message"] = str(e) + print(f"\n✗ [表格 {doc_index}/{total_docs}] 同步失败: {e}") if self.test_mode: import traceback traceback.print_exc() - result["end_time"] = datetime.now().isoformat() - return result + return doc_result - def _process_sheet(self, sheet_id: str, sheet_title: str) -> Dict[str, Any]: + def _process_sheet(self, sheet_id: str, sheet_title: str, doc_index: int = 1) -> Dict[str, Any]: """ 处理单个子表的同步 Args: sheet_id: 子表ID sheet_title: 子表标题 + doc_index: 表格序号(用于日志显示) Returns: Dict: 子表处理结果 """ print(f"\n{'='*60}") - print(f"处理子表: {sheet_title}") + print(f"处理子表: {sheet_title} (表格{doc_index})") print(f"{'='*60}") sheet_result = { @@ -172,8 +246,8 @@ class SyncService: try: # 1. 获取字段信息并检查必要字段 - fields = self.smartsheet.api.get_fields(sheet_id) - all_present, missing_fields, field_mapping = self.smartsheet.check_required_fields(fields) + fields = self.current_smartsheet.api.get_fields(sheet_id) + all_present, missing_fields, field_mapping = self.current_smartsheet.check_required_fields(fields) if not all_present: sheet_result["skipped"] = True @@ -183,10 +257,10 @@ class SyncService: # 2. 获取所有记录(只获取一次,供新记录同步和持续同步共用) print(f"正在获取所有记录...") - all_records = self.smartsheet.get_all_records(sheet_id) + all_records = self.current_smartsheet.get_all_records(sheet_id) # 3. 获取包含TAPD链接的新记录(同步状态为空) - records_with_link = self.smartsheet.get_records_with_tapd_link( + records_with_link = self.current_smartsheet.get_records_with_tapd_link( sheet_id, all_records=all_records ) sheet_result["records_with_link"] = len(records_with_link) @@ -224,7 +298,7 @@ class SyncService: if success_records: print(f"\n正在回写 {len(success_records)} 条成功记录...") try: - self.smartsheet.batch_update_records(sheet_id, success_records) + self.current_smartsheet.batch_update_records(sheet_id, success_records) print(f" ✓ 成功记录回写完成") except Exception as e: print(f" ✗ 成功记录回写失败: {e}") @@ -234,13 +308,13 @@ class SyncService: print(f"\n正在回写 {len(failed_record_ids)} 条失败记录的状态...") try: failed_updates = [ - self.smartsheet.build_update_record( + self.current_smartsheet.build_update_record( record_id=record_id, sync_status="失败" ) for record_id in failed_record_ids ] - self.smartsheet.batch_update_records(sheet_id, failed_updates) + self.current_smartsheet.batch_update_records(sheet_id, failed_updates) print(f" ✓ 失败记录状态回写完成") except Exception as e: print(f" ✗ 失败记录状态回写失败: {e}") @@ -303,7 +377,7 @@ class SyncService: plan_name = self.tapd_api.map_plan_id_to_name(plan_id) # 获取当前字段值,判断是否需要更新 - current_values = self.smartsheet.get_current_field_values(record_info["record"]) + current_values = self.current_smartsheet.get_current_field_values(record_info["record"]) needs_update = self._check_needs_update( current_values, status, owner, begin_date, due_date, plan_name @@ -311,7 +385,7 @@ class SyncService: # 构造更新记录(包含业务字段 + 同步状态=成功) # 即使业务字段没有变化,也要写入同步状态 - update_record = self.smartsheet.build_update_record( + update_record = self.current_smartsheet.build_update_record( record_id=record_info["record_id"], status=status, owner=owner, @@ -401,7 +475,7 @@ class SyncService: } # 获取需要持续同步的记录 - records = self.smartsheet.get_synced_records_for_update( + records = self.current_smartsheet.get_synced_records_for_update( sheet_id, TERMINAL_STATUSES, all_records=all_records ) @@ -426,13 +500,13 @@ class SyncService: new_plan = self.tapd_api.map_plan_id_to_name(plan_id) # 获取当前值并比较 - current = self.smartsheet.get_current_field_values(record_info["record"]) + current = self.current_smartsheet.get_current_field_values(record_info["record"]) needs_update = self._check_needs_update( current, new_status, new_owner, new_begin, new_due, new_plan ) if needs_update: - update_record = self.smartsheet.build_update_record( + update_record = self.current_smartsheet.build_update_record( record_id=record_info["record_id"], status=new_status, owner=new_owner, @@ -450,7 +524,7 @@ class SyncService: # 批量更新 if updates: print(f" 正在更新 {len(updates)} 条记录...") - self.smartsheet.batch_update_records(sheet_id, updates) + self.current_smartsheet.batch_update_records(sheet_id, updates) result["updated"] = len(updates) print(f" ✓ 持续同步更新完成") else: