修复任务一429时不推送企微报错

This commit is contained in:
zelong 2026-06-02 18:56:48 +08:00
parent 3dc4815e26
commit a81e6961b4
11 changed files with 619 additions and 35 deletions

View File

@ -1,4 +1,4 @@
{
"access_token": "MzN3pnuz4rhX4CUzHCEgLONP_tzovqGquHfGlKRli6uZmZfdMUlnx6q4FqK6jTOF_xFfE0HSz4H2OhKI8FPS1eo8-EQJgjxLycRMhOFKwqKtEE9oMwyPfi9mwMaeXJUoZfBhlXQOPfOvCN0sL6LD6atthEdmj0VVSyDU_fPJ1XpSkqzWv8cosuG8tbrssX8EZgw90vKT46N0ySi5Mnhe45yY2_V-PH0TtuSHd-D6EgM",
"fetch_time": 1775029801.2061243
"access_token": "uUoS08xX0tunEpZHP5HD2bDQgSeozq4bf8fuyuNNWKqBXY5-Ygjglv6dqeyYDkPigiuhAQaRpEnWa625ImTxNQYzETWip57YpmI2YRWZW2b6fNSxc7GtcpcgfZO8ACETZjVz6iO0E9szj5CgCqf5M6V_ndJGkZU4rjtcmCt9Hn4jAPfhc00T2VPNQCtToZkLv62RFELABgqQ61PJaKp9dEG8yexEkaYoiS9W6O387qU",
"fetch_time": 1777019153.1190348
}

View File

@ -0,0 +1,38 @@
# 全局任务列表
> 用途:按层级维护项目任务、阶段状态与新增需求,避免任务漂移。
---
## 任务一:智能表格自动开 TAPD Bug 单
- [x] 扫描智能表格待开单记录
- [x] 校验必填字段并回写开单状态
- [x] 创建 TAPD Bug 并回写 TAPD 单号、Bug 状态
- [x] 校验失败企微通知
- [x] 处理 TAPD 429 限速导致的通知缺失
- [x] 识别 TAPD HTTP 429 与业务限速错误
- [x] TAPD 开单限速时等待2分钟重试当前记录
- [x] 重试仍限速时停止本轮继续开单,保留未处理记录等待下次调度
- [x] TAPD 开单失败与限速事件纳入企微推送
## 任务一TAPD Bug 状态同步
- [x] 扫描已开单且非终态记录
- [x] 查询 TAPD 最新 Bug 状态
- [x] 回写智能表格 Bug 状态
- [x] 处理 TAPD 429 限速导致的通知缺失
- [x] 单条查询失败不再静默吞掉
- [x] TAPD 限速时停止本轮后续状态查询
- [x] 查询失败与限速事件纳入企微推送
- [x] 调度统计补充 `failed_count``rate_limited`
## 后续建议任务
- [ ] TAPD 请求节流与退避策略
- [ ] 配置化单次请求间隔
- [ ] 支持按 `Retry-After` 延迟重试
- [ ] 支持每轮最大处理数量,避免数百条记录一次性打满 TAPD 限额
- [ ] 状态同步批量化优化
- [ ] 调研 TAPD Bug 列表接口是否支持批量按 ID 查询
- [ ] 减少逐条 `get_bug` 调用

View File

@ -45,21 +45,27 @@
- **关键依赖**`src/main.py`
- `src/main.py`
- **职责**:任务一主流程编排(扫描、校验、开单、回写、通知)。
- **接口(对内)**`run_once(...)`
- **接口(对内)**`run_once(...)``create_bug_with_rate_limit_retry(...)`
- **关键点**:开单触发 TAPD 429 时等待 120 秒重试当前记录一次;重试仍限速才停止本轮后续开单。
- `src/smartsheet.py`
- **职责**:智能表格 API 封装。
- **接口(对内)**`get_sheet_list``get_fields``get_records``update_records` 等。
- `src/tapd_api.py`
- **职责**TAPD Bug API 封装(创建、查询、附件上传)。
- **接口(对内)**`create_bug``get_bug``upload_attachment` 等。
- **接口(对内)**`RateLimitError``create_bug``get_bug``upload_attachment` 等。
- **关键点**HTTP 429 或业务错误中的限速信息会抛出 `RateLimitError`,上层应停止本轮继续打 TAPD。
- `src/token_manager.py`
- **职责**:企业微信 `access_token` 获取与缓存。
- **接口(对内)**`__init__(cache_file_path=None, logger=None)``get_token()`
- `src/wework_notifier.py`
- **职责**:企业微信消息通知。
- **接口(对内)**`__init__(access_token, agentid, receivers, logger=None)``send_validation_failure_notification(...)`
- **接口(对内)**`__init__(access_token, agentid, receivers, logger=None)``send_validation_failure_notification(...)``send_operation_failure_notification(...)`
- `src/api_logger.py`
- **职责**:现有日志记录器(后续将作为兼容层)。
- `src/sync_status.py`
- **职责**:任务一 Bug 状态同步编排。
- **接口(对内)**`BugStatusSyncer(access_token, docid, workspace_id, test_mode=False, agentid=None, receivers=None)``sync_bug_status()`
- **关键点**TAPD 查询失败会计入 `failed_count`;触发 429 时停止本轮后续查询并推送企微异常通知。
## 2.2 任务二(`src2/`
- `src2/scheduler.py`
@ -146,3 +152,10 @@
- 新增任务三(`src3/`)模块清单与职责说明。
- 任务三配置改为多群分组推送:按顺序维护“组名-成员子表标题-Webhook”一一对应关系。
- 任务三成员配置读取由手填 `sheet_id` 调整为通过子表标题自动解析 `sheet_id`
### 2026-06-02更新5
- `src/tapd_api.py` 新增 `RateLimitError`,用于区分 TAPD 429 限速与普通运行错误。
- `src/main.py` 在 TAPD 开单触发限速时等待 120 秒重试当前记录;重试仍失败时停止本轮后续开单,并将限速事件纳入企微异常通知。
- `src/sync_status.py` 不再静默吞掉单条 TAPD 查询失败;触发限速时停止本轮状态同步并推送异常通知。
- `src/wework_notifier.py` 新增 `send_operation_failure_notification(...)`,用于运行异常类通知。
- `src/scheduler.py` 状态同步统计新增 `failed_count``rate_limited`

View File

@ -260,3 +260,52 @@
- **可回滚点**`src3/message_formatter.py``src3/main.py` 可独立回滚。
- **关联文档**`需求文档.md`(任务三 V2 段落)
- **备注**:可在后续补“消息模板配置化”减少文案硬编码。
## 阶段5任务一 TAPD 429 限速通知修复
- **阶段名称**:任务一迭代 - TAPD 429 限速与企微通知补丁
- **日期**2026-06-02
- **负责人**Codex
- **目标**:定位并修复智能表格存在数百条记录时,开单和状态同步触发 TAPD 429 后不会触发企微推送的问题。
### 根因
- **开单链路**`src/main.py` 只把字段校验失败加入企微通知集合TAPD 创建失败虽然会写回 `❌`,但不会触发企微推送。
- **状态同步链路**`src/sync_status.py` 对单条 `get_bug` 查询异常使用 `except Exception: continue` 静默吞掉,导致 429 被误判为“状态无变化/同步成功”。
- **限速放大**TAPD 调用层没有区分 429批量记录触发限速后仍可能继续逐条请求 TAPD开单链路还缺少用户期望的“等待2分钟后重试当前记录”步骤。
### 变更清单
- **新增文件**
- `docs/全局任务列表.md`
- **修改文件**
- `src/tapd_api.py`
- `src/main.py`
- `src/sync_status.py`
- `src/scheduler.py`
- `src/wework_notifier.py`
- `docs/全局框架文档.md`
- `docs/全局迭代日志.md`
- **删除文件**:无
### 关键改动说明
- `src/tapd_api.py` 新增 `RateLimitError`,识别 HTTP 429 与业务错误文本中的限速信息。
- `src/main.py` 在开单触发限速后等待 120 秒重试当前记录一次;重试仍触发限速时才停止本轮后续 TAPD 开单,未处理记录保持未开单状态,等待下次调度重试。
- `src/main.py` 将 TAPD 开单失败与限速事件纳入企微异常通知。
- `src/sync_status.py` 将 TAPD 查询失败计入 `failed_count`,触发限速时停止本轮后续查询并推送企微异常通知。
- `src/scheduler.py` 状态同步日志统计新增 `failed_count``rate_limited`
- `src/wework_notifier.py` 新增运行异常通知接口,复用企业微信应用消息发送链路。
### 验收结果
- **通过项**
- 已完成静态链路复查:限速错误从 TAPD API 层到开单/同步编排层都有专用分支。
- 已确认开单链路触发 429 后会先等待 120 秒重试当前记录。
- 已确认开单链路不再只通知字段校验失败TAPD 开单异常也会进入企微通知。
- 已确认状态同步不再静默吞掉单条 TAPD 查询失败。
- **未通过项**
- 尚未执行运行态联调(按项目约束未在命令行执行 Python
- **遗留风险**
- 当前补丁是“开单限速后固定等待 120 秒重试一次”,尚未实现主动节流、按 `Retry-After` 动态等待或单轮处理上限。
- 若 TAPD 限速信息不包含 429、限速、频繁等关键词仍可能被识别为普通运行错误。
### 回滚与追踪
- **可回滚点**:优先按文件粒度回滚 `src/tapd_api.py``src/main.py``src/sync_status.py``src/wework_notifier.py``src/scheduler.py`
- **关联文档**`docs/全局框架文档.md``docs/全局任务列表.md`
- **备注**:下一阶段建议做“请求节流 + 单轮最大处理数 + Retry-After 动态退避”,这是从根上降低 429 的方案。

View File

@ -480,6 +480,69 @@ class WeWorkAPITester:
print(f"✗ 请求异常: {str(e)}")
return None
def mod_doc_member(self, docid, userid, auth=7):
"""
修改文档通知范围及权限添加管理员等
Args:
docid: 文档ID
userid: 企业成员的userid
auth: 权限类型 1:只读 2:读写 7:管理员
Returns:
bool: 是否成功
"""
print("\n=== 修改文档成员权限 ===")
if not self.access_token:
print("✗ 请先获取access_token")
return False
auth_name_map = {1: "只读", 2: "读写", 7: "管理员"}
auth_name = auth_name_map.get(auth, f"未知({auth})")
url = f"{self.base_url}/wedoc/mod_doc_member"
params = {"access_token": self.access_token}
data = {
"docid": docid,
"update_file_member_list": [
{
"type": 1,
"auth": auth,
"userid": userid
}
]
}
try:
response = requests.post(url, params=params, json=data, timeout=10)
response_data = response.json()
# 记录API调用
request_data = {
"url": url,
"params": {"access_token": "***"},
"body": data
}
self._log_api_call("mod_doc_member", request_data, response_data)
# 检查返回结果
if response_data.get("errcode") == 0:
print(f"✓ 权限修改成功")
print(f" 文档ID: {docid}")
print(f" 用户ID: {userid}")
print(f" 权限: {auth_name}")
return True
else:
print(f"✗ 权限修改失败")
print(f" 错误码: {response_data.get('errcode')}")
print(f" 错误信息: {response_data.get('errmsg')}")
return False
except Exception as e:
print(f"✗ 请求异常: {str(e)}")
return False
def send_message(self, content):
"""
发送应用消息
@ -1290,14 +1353,15 @@ def print_menu():
print("4. 删除文档")
print("5. 发送应用消息")
print("6. 查询智能表格子表列表")
print("7. 修改文档成员权限")
print("\n【TAPD API】")
print("7. 获取缺陷字段配置")
print("8. 获取需求字段配置")
print("9. 获取需求")
print("10. 获取附件列表")
print("11. 上传附件")
print("8. 获取缺陷字段配置")
print("9. 获取需求字段配置")
print("10. 获取需求")
print("11. 获取附件列表")
print("12. 上传附件")
print("\n【其他】")
print("12. 查看日志文件")
print("13. 查看日志文件")
print("0. 退出")
print("="*50)
@ -1309,7 +1373,7 @@ def main():
while True:
print_menu()
choice = input("\n请选择操作 (0-12): ").strip()
choice = input("\n请选择操作 (0-13): ").strip()
if choice == "0":
print("\n感谢使用,再见!")
@ -1389,14 +1453,40 @@ def main():
print("✗ 文档ID不能为空")
elif choice == "7":
# 修改文档成员权限
print("\n=== 修改文档成员权限 ===")
docid = input("请输入文档ID (docid): ").strip()
if not docid:
print("✗ 文档ID不能为空")
continue
userid = input("请输入用户ID (userid): ").strip()
if not userid:
print("✗ 用户ID不能为空")
continue
print("\n权限类型:")
print(" 1 - 只读")
print(" 2 - 读写(仅智能表格支持)")
print(" 7 - 管理员 (默认)")
auth_input = input("请选择权限类型 (直接回车默认为7-管理员): ").strip()
auth = int(auth_input) if auth_input else 7
if auth not in [1, 2, 7]:
print("✗ 无效的权限类型,必须是 1/2/7 之一")
continue
wework_tester.mod_doc_member(docid, userid, auth)
elif choice == "8":
# 获取TAPD缺陷字段配置
tapd_tester.get_bug_custom_fields()
elif choice == "8":
elif choice == "9":
# 获取TAPD需求字段配置
tapd_tester.get_story_fields_info()
elif choice == "9":
elif choice == "10":
# 获取TAPD需求
story_id = input("\n请输入需求ID: ").strip()
if not story_id:
@ -1404,7 +1494,7 @@ def main():
continue
tapd_tester.get_story(story_id)
elif choice == "10":
elif choice == "11":
# 获取TAPD附件列表
print("\n=== 获取附件列表 ===")
print("是否需要添加筛选条件?")
@ -1442,7 +1532,7 @@ def main():
limit=limit
)
elif choice == "11":
elif choice == "12":
# 上传附件到TAPD
print("\n=== 上传附件 ===")
file_path = input("请输入文件路径: ").strip()
@ -1486,7 +1576,7 @@ def main():
overwrite=overwrite
)
elif choice == "12":
elif choice == "13":
print("\n=== 查看日志文件 ===")
try:
with open(wework_tester.log_file, 'r', encoding='utf-8') as f:

View File

@ -5,6 +5,7 @@ Debug阶段自动开单工具
import sys
import argparse
import time
from pathlib import Path
from typing import Dict
@ -15,7 +16,7 @@ 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.tapd_api import RateLimitError, TAPDApi
from src.mapper import FieldMapper, BugCreationResult
from src.token_manager import TokenManager
from src.status_mapper import BugStatusMapper
@ -23,6 +24,10 @@ from src.wework_notifier import WeWorkNotifier
from src.api_logger import get_logger
TAPD_RATE_LIMIT_RETRY_WAIT_SECONDS = 120
TAPD_RATE_LIMIT_MAX_RETRIES = 1
def parse_arguments():
"""
解析命令行参数
@ -296,6 +301,40 @@ def scan_all_sheets(access_token: str, docid: str, verbose: bool = False, test_m
return all_results
def create_bug_with_rate_limit_retry(tapd_api: TAPDApi, tapd_data: Dict,
wait_seconds: int = TAPD_RATE_LIMIT_RETRY_WAIT_SECONDS,
max_retries: int = TAPD_RATE_LIMIT_MAX_RETRIES) -> Dict:
"""
创建TAPD bug触发限速时等待后重试当前记录
Args:
tapd_api: TAPD API实例
tapd_data: TAPD开单数据
wait_seconds: 限速后等待秒数
max_retries: 限速后的最大重试次数
Returns:
Dict: 创建成功的bug信息
Raises:
RateLimitError: 等待重试后仍触发限速
RuntimeError: TAPD API其他错误
"""
retry_count = 0
while True:
try:
return tapd_api.create_bug(tapd_data)
except RateLimitError:
if retry_count >= max_retries:
raise
retry_count += 1
print(f" ⚠ TAPD触发限速等待 {wait_seconds} 秒后重试当前记录 ({retry_count}/{max_retries})")
time.sleep(wait_seconds)
print(f" → 正在重试创建TAPD bug...")
def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test_mode: bool = False) -> Dict:
"""
批量创建TAPD bug单
@ -339,6 +378,7 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
success_results = []
failed_results = []
rate_limit_info = None
for idx, record_data in enumerate(valid_records, 1):
record_id = record_data.get('record_id', '未知')
@ -354,7 +394,7 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
# 创建bug
print(f" → 正在创建TAPD bug...")
bug_info = tapd_api.create_bug(tapd_data)
bug_info = create_bug_with_rate_limit_retry(tapd_api, tapd_data)
# 提取bug ID
bug_id = bug_info.get('id')
@ -405,8 +445,35 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
success=False,
error_message=error_msg
)
result.title = title
failed_results.append(result)
except RateLimitError as e:
# 等待2分钟重试后仍限速继续逐条请求只会扩大失败面。
error_msg = f"TAPD限速已等待2分钟重试仍失败暂停本轮开单: {e}"
remaining_count = len(valid_records) - idx + 1
print(f"{error_msg}")
print(f" ⚠ 本轮剩余 {remaining_count} 条记录将保持未开单状态,等待下次调度重试")
logger = get_logger()
logger.log_api_call(
api_type="task1",
operation="create_bug_rate_limited",
request_data={"record_id": record_id, "title": title},
response_data={},
success=False,
error_message=error_msg,
extra={"remaining_count": remaining_count}
)
rate_limit_info = {
"record_id": record_id,
"title": title,
"error_message": error_msg,
"remaining_count": remaining_count
}
break
except RuntimeError as e:
# TAPD API调用错误
error_msg = f"TAPD API调用失败: {e}"
@ -428,6 +495,7 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
success=False,
error_message=error_msg
)
result.title = title
failed_results.append(result)
except Exception as e:
@ -451,6 +519,7 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
success=False,
error_message=error_msg
)
result.title = title
failed_results.append(result)
# 显示汇总结果
@ -473,7 +542,8 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
return {
'success_results': success_results,
'failed_results': failed_results
'failed_results': failed_results,
'rate_limit_info': rate_limit_info
}
@ -651,7 +721,10 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
# 2. 遍历每个子表,分别处理
all_invalid_records = [] # 收集所有子表的失败记录(用于推送)
all_creation_failure_records = [] # 收集TAPD开单失败记录用于推送
all_rate_limit_records = [] # 收集TAPD限速事件用于推送
sheet_summaries = [] # 收集每个子表的统计摘要
hit_tapd_rate_limit = False
for sheet_result in all_sheet_results:
sheet_id = sheet_result['sheet_id']
@ -686,7 +759,9 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
creation_success_results = []
creation_failed_results = []
if valid_count > 0:
if valid_count > 0 and hit_tapd_rate_limit:
print(f"\n[{sheet_title}] 已触发TAPD限速本轮跳过该子表开单待下次调度重试")
elif valid_count > 0:
creation_result = create_tapd_bugs(
validation_result['valid_records'],
all_config['tapd']['workspace_id'],
@ -695,6 +770,19 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
)
creation_success_results = creation_result['success_results']
creation_failed_results = creation_result['failed_results']
rate_limit_info = creation_result.get('rate_limit_info')
if rate_limit_info:
rate_limit_info['sheet_title'] = sheet_title
all_rate_limit_records.append(rate_limit_info)
hit_tapd_rate_limit = True
for failed_result in creation_failed_results:
all_creation_failure_records.append({
"sheet_title": sheet_title,
"record_id": failed_result.record_id,
"title": getattr(failed_result, "title", "未记录标题"),
"error_message": failed_result.error_message
})
# 4. 处理校验失败的记录转换为BugCreationResult格式
validation_failed_results = []
@ -773,6 +861,27 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
except Exception as e:
print(f" ✗ 企业微信推送失败: {e}")
if len(all_creation_failure_records) > 0 or len(all_rate_limit_records) > 0:
print("\n" + "=" * 60)
print("发送TAPD开单异常企业微信推送")
print("=" * 60)
try:
wework_config = all_config.get('wework', {})
agentid = wework_config.get('agentid')
receivers = wework_config.get('receivers')
if agentid and receivers:
notifier = WeWorkNotifier(access_token, agentid, receivers)
failure_records = all_rate_limit_records + all_creation_failure_records
notifier.send_operation_failure_notification(
"autoTAPD TAPD开单异常通知",
failure_records
)
else:
print(" ⚠ 企业微信推送配置不完整,跳过推送")
except Exception as e:
print(f" ✗ 企业微信推送失败: {e}")
# 7. 显示最终统计(分表统计 + 总体统计)
print("\n" + "=" * 60)
print("执行完成 - 分表统计")

View File

@ -207,11 +207,14 @@ class AutoTAPDScheduler:
access_token = self.token_manager.get_token()
# 创建状态同步器
wework_config = self.config.get('wework', {})
syncer = BugStatusSyncer(
access_token=access_token,
docid=self.config['smartsheet']['docid'],
workspace_id=self.config['tapd']['workspace_id'],
test_mode=False
test_mode=False,
agentid=wework_config.get('agentid'),
receivers=wework_config.get('receivers')
)
# 执行一次状态同步
@ -230,6 +233,7 @@ class AutoTAPDScheduler:
print("本次同步统计:")
print(f" 检查bug: {result['checked_count']}")
print(f" 更新bug: {result['updated_count']}")
print(f" 查询失败: {result.get('failed_count', 0)}")
print("-" * 80)
else:
self.stats['failed_sync_runs'] += 1
@ -242,6 +246,8 @@ class AutoTAPDScheduler:
stats={
"checked_count": result.get('checked_count', 0),
"updated_count": result.get('updated_count', 0),
"failed_count": result.get('failed_count', 0),
"rate_limited": result.get('rate_limited', False),
},
success=result.get('success', False),
error_message=result.get('error_message'),
@ -269,6 +275,8 @@ class AutoTAPDScheduler:
stats={
"checked_count": 0,
"updated_count": 0,
"failed_count": 0,
"rate_limited": False,
},
success=False,
error_message=f"{type(e).__name__}: {e}",

View File

@ -12,14 +12,16 @@ project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.smartsheet import SmartSheetAPI
from src.tapd_api import TAPDApi
from src.tapd_api import RateLimitError, TAPDApi
from src.status_mapper import BugStatusMapper
from src.wework_notifier import WeWorkNotifier
class BugStatusSyncer:
"""Bug状态同步器"""
def __init__(self, access_token: str, docid: str, workspace_id: str, test_mode: bool = False):
def __init__(self, access_token: str, docid: str, workspace_id: str, test_mode: bool = False,
agentid: str = None, receivers: str = None):
"""
初始化状态同步器
@ -33,10 +35,13 @@ class BugStatusSyncer:
self.docid = docid
self.workspace_id = workspace_id
self.test_mode = test_mode
self.agentid = agentid
self.receivers = receivers
# 初始化API
self.smartsheet_api = None
self.tapd_api = None
self.failure_records = []
def _sync_sheet_status(self, sheet_id: str, sheet_title: str) -> Dict:
"""
@ -47,7 +52,7 @@ class BugStatusSyncer:
sheet_title: 子表标题
Returns:
Dict: {'checked': int, 'updated': int}
Dict: {'checked': int, 'updated': int, 'failed': int, 'rate_limited': bool}
"""
# 1. 获取字段信息
fields = self.smartsheet_api.get_fields(sheet_id)
@ -73,12 +78,15 @@ class BugStatusSyncer:
if len(records) == 0:
print(f" ✓ 没有需要同步状态的记录")
return {'checked': 0, 'updated': 0}
return {'checked': 0, 'updated': 0, 'failed': 0, 'rate_limited': False}
print(f" → 检查 {len(records)} 个bug的状态...")
# 3. 逐个查询TAPD bug状态并对比
updates = [] # 需要更新的记录列表
failed_count = 0
checked_count = 0
rate_limited = False
for record in records:
record_id = record.get('record_id', '未知')
@ -91,6 +99,7 @@ class BugStatusSyncer:
current_status = self.smartsheet_api.get_field_value_by_title(record, 'bug状态')
try:
checked_count += 1
# 查询TAPD的最新状态
bug_info = self.tapd_api.get_bug(bug_id)
latest_status_en = bug_info.get('status', '')
@ -106,8 +115,31 @@ class BugStatusSyncer:
}
updates.append(update_record)
except Exception:
# 单个bug查询失败不影响其他bug的同步
except RateLimitError as e:
failed_count += 1
rate_limited = True
error_msg = f"TAPD限速暂停本轮状态同步: {e}"
print(f" ✗ 记录 {record_id} 查询失败: {error_msg}")
self.failure_records.append({
"sheet_title": sheet_title,
"record_id": record_id,
"title": f"TAPD单号 {bug_id}",
"error_message": error_msg,
"remaining_count": len(records) - checked_count + 1
})
break
except Exception as e:
# 单个bug查询失败不影响其他bug的同步但必须记录避免失败被误判为状态无变化。
failed_count += 1
error_msg = f"{type(e).__name__}: {e}"
print(f" ✗ 记录 {record_id} 查询失败: {error_msg}")
self.failure_records.append({
"sheet_title": sheet_title,
"record_id": record_id,
"title": f"TAPD单号 {bug_id}",
"error_message": error_msg
})
continue
# 4. 批量更新智能表格
@ -117,7 +149,12 @@ class BugStatusSyncer:
else:
print(f" ✓ 所有bug状态均未变化")
return {'checked': len(records), 'updated': len(updates)}
return {
'checked': checked_count,
'updated': len(updates),
'failed': failed_count,
'rate_limited': rate_limited
}
def sync_bug_status(self) -> Dict:
"""
@ -128,6 +165,7 @@ class BugStatusSyncer:
'success': bool,
'checked_count': int, # 检查的bug数量
'updated_count': int, # 更新的bug数量
'failed_count': int, # 查询失败的bug数量
'error_message': str
}
"""
@ -135,10 +173,14 @@ class BugStatusSyncer:
'success': False,
'checked_count': 0,
'updated_count': 0,
'failed_count': 0,
'rate_limited': False,
'error_message': None
}
try:
self.failure_records = []
# 1. 初始化API
print("\n[1/5] 初始化API...")
self.smartsheet_api = SmartSheetAPI(self.access_token, self.docid, test_mode=self.test_mode)
@ -158,8 +200,14 @@ class BugStatusSyncer:
sheet_summaries = [] # 收集每个子表的统计摘要
total_checked = 0
total_updated = 0
total_failed = 0
hit_rate_limit = False
for idx, sheet in enumerate(sheet_list, 1):
if hit_rate_limit:
print("\n已触发TAPD限速停止处理后续子表")
break
sheet_id = sheet['sheet_id']
sheet_title = sheet.get('title', '未命名')
@ -174,12 +222,16 @@ class BugStatusSyncer:
'sheet_title': sheet_title,
'error': None,
'checked': sheet_result['checked'],
'updated': sheet_result['updated']
'updated': sheet_result['updated'],
'failed': sheet_result['failed']
})
# 累加总计
total_checked += sheet_result['checked']
total_updated += sheet_result['updated']
total_failed += sheet_result['failed']
if sheet_result.get('rate_limited'):
hit_rate_limit = True
except RuntimeError as e:
print(f" ✗ 处理失败: {e}")
@ -187,7 +239,8 @@ class BugStatusSyncer:
'sheet_title': sheet_title,
'error': str(e),
'checked': 0,
'updated': 0
'updated': 0,
'failed': 0
})
except Exception as e:
print(f" ✗ 未预期的错误: {type(e).__name__}: {e}")
@ -195,12 +248,15 @@ class BugStatusSyncer:
'sheet_title': sheet_title,
'error': f"{type(e).__name__}: {e}",
'checked': 0,
'updated': 0
'updated': 0,
'failed': 0
})
# 4. 显示最终统计(分表统计 + 总体统计)
result['checked_count'] = total_checked
result['updated_count'] = total_updated
result['failed_count'] = total_failed
result['rate_limited'] = hit_rate_limit
print("\n" + "=" * 60)
print("状态同步完成 - 分表统计")
@ -217,6 +273,8 @@ class BugStatusSyncer:
print(f" ✓ 更新: {summary['updated']} 个bug")
else:
print(f" ✓ 所有bug状态均未变化")
if summary['failed'] > 0:
print(f" ⚠ 查询失败: {summary['failed']} 个bug")
print("\n" + "=" * 60)
print("状态同步完成 - 总体统计")
@ -236,9 +294,30 @@ class BugStatusSyncer:
print(f" ✓ 更新: {total_updated} 个bug")
else:
print(f" ✓ 所有bug状态均未变化")
if total_failed > 0:
print(f" ⚠ 查询失败: {total_failed} 个bug")
print("=" * 60)
result['success'] = True
if self.failure_records and self.agentid and self.receivers:
print("\n" + "=" * 60)
print("发送bug状态同步异常企业微信推送")
print("=" * 60)
try:
notifier = WeWorkNotifier(self.access_token, self.agentid, self.receivers)
notifier.send_operation_failure_notification(
"autoTAPD bug状态同步异常通知",
self.failure_records
)
except Exception as e:
print(f" ✗ 企业微信推送失败: {e}")
elif self.failure_records:
print(" ⚠ 企业微信推送配置不完整,跳过同步异常推送")
result['success'] = total_failed == 0 and not hit_rate_limit
if hit_rate_limit:
result['error_message'] = "TAPD触发429限速本轮状态同步已提前停止"
elif total_failed > 0:
result['error_message'] = f"状态同步存在 {total_failed} 个TAPD查询失败"
return result
except FileNotFoundError as e:

View File

@ -10,6 +10,16 @@ from requests.auth import HTTPBasicAuth
from src.api_logger import get_logger
class RateLimitError(RuntimeError):
"""TAPD 触发限速时抛出的专用异常"""
def __init__(self, message: str, endpoint: str = "", retry_after: str = None, status_code: int = 429):
super().__init__(message)
self.endpoint = endpoint
self.retry_after = retry_after
self.status_code = status_code
class TAPDApi:
"""TAPD API封装类"""
@ -52,6 +62,21 @@ class TAPDApi:
if test_mode:
print(f" ⚠ 测试模式已启用将显示所有TAPD API调用的详细信息")
@staticmethod
def _looks_like_rate_limit(error_msg: str) -> bool:
"""识别 TAPD 以业务错误形式返回的限速信息"""
normalized_msg = str(error_msg or "").lower()
rate_limit_keywords = [
"429",
"too many requests",
"rate limit",
"ratelimit",
"限速",
"频繁",
"请求过多",
]
return any(keyword in normalized_msg for keyword in rate_limit_keywords)
def _make_request(self, endpoint: str, method: str = "POST",
data: Optional[Dict] = None, params: Optional[Dict] = None) -> Dict:
"""
@ -151,6 +176,12 @@ class TAPDApi:
success=False,
error_message=error_msg
)
if self._looks_like_rate_limit(error_msg):
raise RateLimitError(
f"TAPD API触发限速: {error_msg}",
endpoint=endpoint,
status_code=429
)
raise RuntimeError(f"TAPD API调用失败: {error_msg}")
# 记录API调用日志成功
@ -177,9 +208,42 @@ class TAPDApi:
)
raise RuntimeError(error_msg)
except requests.exceptions.HTTPError as e:
status_code = getattr(e.response, 'status_code', None)
retry_after = None
response_text = ""
if e.response is not None:
retry_after = e.response.headers.get("Retry-After")
response_text = e.response.text[:200]
if status_code == 429:
error_msg = f"TAPD API触发429限速: {endpoint}"
if retry_after:
error_msg += f",建议等待 {retry_after} 秒后重试"
if response_text:
error_msg += f"\n响应内容: {response_text}"
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data={
"status_code": status_code,
"retry_after": retry_after,
"body": response_text
},
success=False,
error_message=error_msg
)
raise RateLimitError(
error_msg,
endpoint=endpoint,
retry_after=retry_after,
status_code=status_code
)
error_msg = f"TAPD API HTTP错误: {e}"
if hasattr(e.response, 'text'):
error_msg += f"\n响应内容: {e.response.text[:200]}"
if response_text:
error_msg += f"\n响应内容: {response_text}"
# 记录API调用日志失败
self.logger.log_api_call(
api_type="tapd",

View File

@ -51,6 +51,23 @@ class WeWorkNotifier:
# 发送消息
return self._send_text_message(content)
def send_operation_failure_notification(self, title: str, failure_records: List[Dict]) -> bool:
"""
发送运行异常通知
Args:
title: 通知标题
failure_records: 异常记录列表每条记录建议包含 sheet_titlerecord_idtitleerror_message
Returns:
bool: 是否发送成功
"""
if not failure_records:
return True
content = self._build_operation_failure_message(title, failure_records)
return self._send_text_message(content)
def _build_failure_message(self, invalid_records: List[Dict]) -> str:
"""
构造校验失败消息内容支持多子表分组
@ -109,6 +126,54 @@ class WeWorkNotifier:
return "\n".join(lines)
def _build_operation_failure_message(self, title: str, failure_records: List[Dict]) -> str:
"""
构造运行异常消息内容
Args:
title: 通知标题
failure_records: 异常记录列表
Returns:
str: 格式化后的消息内容
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
max_display_count = 20
display_records = failure_records[:max_display_count]
lines = [
f"{title}",
f"时间: {timestamp}",
f"异常数量: {len(failure_records)}",
"",
"以下记录在本轮任务中发生运行异常,请查看日志并等待下次调度或人工处理。",
"=" * 40
]
for idx, record in enumerate(display_records, 1):
sheet_title = record.get("sheet_title", "未知子表")
record_title = record.get("title", "(无标题)")
record_id = record.get("record_id", "未知")
error_message = record.get("error_message", "未知错误")
lines.append(f"\n[{idx}] {record_title}")
lines.append(f"子表: {sheet_title}")
lines.append(f"记录ID: {record_id}")
lines.append(f"错误: {error_message}")
remaining_count = record.get("remaining_count")
if remaining_count:
lines.append(f"本轮暂停处理数量: {remaining_count}")
if len(failure_records) > max_display_count:
lines.append("")
lines.append(f"仅展示前 {max_display_count} 条,其余请查看日志。")
lines.append("=" * 40)
lines.append("若开单触发TAPD 429限速系统会等待2分钟重试当前记录重试仍失败时才停止本轮后续请求。")
return "\n".join(lines)
def _send_text_message(self, content: str) -> bool:
"""
发送文本消息

View File

@ -3893,6 +3893,75 @@ Smartsheet 的某个表格中记录相关的参数:
对于全员权限,生效成员即是全部文档成员。
对于成员额外权限,可以配置生效的成员范围。
## 修改文档成员与权限
最后更新2026/03/12
该接口用于管理文档、表格、智能表格的文档成员,支持增加或删除成员、修改成员的权限。
**请求方式**POST**HTTPS**
**请求地址**: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_member?access_token=ACCESS_TOKEN
**请求包体**
```json
{
"docid":"DOCID",
"update_file_member_list":[
{
"type":1,
"auth":7,
"userid":"USERID1"
}
],
"del_file_member_list":[
{
"type":1,
"userid":"USERID2"
},
{
"type":1,
"tmp_external_userid":"TMP_EXTERNAL_USERID2"
}
]
}
```
**参数说明**
| 参数 | 类型 | 是否必须 | 说明 |
| ----------------------- | ------ | -------- | ----------------------------------------------------------- |
| docid | string | 是 | 操作的文档id |
| update_file_member_list | obj[] | 否 | 更新文档成员的列表, 批次大小最大100 |
| type | uint32 | 是 | 文档成员的类型 1:用户。文档成员仅支持按人配置 |
| auth | uint32 | 是 | 文档成员内人员获得的权限 1:只读权限 2:读写权限 7:管理员权限 |
| userid | string | 否 | 企业内成员的ID |
| tmp_external_userid | string | 否 | 外部用户临时id。同一个用户在不同的文档中返回的该id不一致。 |
| del_file_member_list | obj[] | 否 | 删除的文档成员列表,批次大小最大一百 |
| type | uint32 | 是 | 文档成员的类型 1:用户。文档成员仅支持按人配置 |
| userid | string | 否 | 企业内成员的ID |
| tmp_external_userid | string | 否 | 外部用户临时id。同一个用户在不同的文档中返回的该id不一致。 |
**权限说明**
- 自建应用需配置到“[可调用应用](https://developer.work.weixin.qq.com/document/path/97795#43883)”列表中的应用secret所获取的accesstoken来调用[accesstoken如何获取](https://developer.work.weixin.qq.com/document/path/97795#10013/第三步获取access_token)
- 第三方应用需具有“文档”权限
- 代开发自建应用需具有“文档”权限
- 只能操作该应用创建的文档
**返回示例**
```json
{
"errcode": 0,
"errmsg": "ok"
}
```
## 查询智能表格子表权限
该接口用于查询智能表格子表权限详情