Compare commits
25 Commits
master
...
task3maste
| Author | SHA1 | Date | |
|---|---|---|---|
| 9df099942b | |||
| a81e6961b4 | |||
| 3dc4815e26 | |||
| 4e189d6f44 | |||
| 6806e002c7 | |||
| af7243cf4e | |||
| 704318077d | |||
| 0224c87f45 | |||
| c5f2c64e82 | |||
| 403b8fee0d | |||
| 0769cf4755 | |||
| 2d7fa8ea7a | |||
| 01d768b16a | |||
| a3099c51db | |||
| 21e0160b76 | |||
| 104198e918 | |||
| 93f12f8d5a | |||
| 9e809ddf88 | |||
| 7db5d87a94 | |||
| ecf9ccbc0b | |||
| f91dadffd4 | |||
| 7cdd3e9390 | |||
| d984828777 | |||
| 397c14faee | |||
| bf4f346096 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -132,6 +132,8 @@ cython_debug/
|
||||
|
||||
# Custom
|
||||
logs/
|
||||
logs2/
|
||||
logs3/
|
||||
.claude/
|
||||
|
||||
# Project documentation
|
||||
|
||||
94
TAPD接口文档.md
94
TAPD接口文档.md
@ -564,6 +564,98 @@ curl -u 'api_user:api_password' -d 'name=story_created_by_api&workspace_id=10158
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 获取需求
|
||||
|
||||
### url
|
||||
|
||||
```
|
||||
https://api.tapd.cn/stories
|
||||
```
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#支持格式)支持格式
|
||||
|
||||
JSON/XML(默认JSON格式)
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#http请求方式)HTTP请求方式
|
||||
|
||||
GET
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#请求数限制)请求数限制
|
||||
|
||||
默认返回 30 条。可通过传 limit 参数设置,最大取 200。也可以传 page 参数翻页
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#请求参数)请求参数
|
||||
|
||||
| 字段名 | 必选 | 类型及范围 | 说明 | 特殊规则 |
|
||||
| :-------------------: | :--: | :---------------: | :----------------------------------------------------------: | :------------------------------------: |
|
||||
| id | 否 | integer | ID | 支持多ID查询 |
|
||||
| name | 否 | string | 标题 | 支持模糊匹配 |
|
||||
| priority | 否 | string | 优先级。为了兼容自定义优先级,`请使用 priority_label 字段`,详情参考:[如何兼容自定义优先级](https://open.tapd.cn/document/api-doc/API文档/subject/custom_priority/) | |
|
||||
| priority_label | 否 | string | 优先级。推荐使用这个字段 | |
|
||||
| business_value | 否 | integer | 业务价值 | |
|
||||
| status | 否 | string | 状态 | 支持枚举查询 |
|
||||
| v_status | 否 | string | 状态(支持传入中文状态名称) | |
|
||||
| with_v_status | 否 | string | 值=1可以返回中文状态 | |
|
||||
| label | 否 | string | 标签查询 | 支持枚举查询 |
|
||||
| workitem_type_id | 否 | string | 需求类别ID | 支持枚举查询 |
|
||||
| version | 否 | string | 版本 | |
|
||||
| module | 否 | string | 模块 | |
|
||||
| feature | 否 | string | 特性 | |
|
||||
| test_focus | 否 | string | 测试重点 | |
|
||||
| size | 否 | integer | 规模 | |
|
||||
| tech_risk | 否 | string | 技术风险 | |
|
||||
| business_value | 否 | string | 业务价值 | |
|
||||
| owner | 否 | string | 处理人 | 支持模糊匹配 |
|
||||
| cc | 否 | string | 抄送人 | 支持模糊匹配 |
|
||||
| creator | 否 | string | 创建人 | 支持多人员查询 |
|
||||
| developer | 否 | string | 开发人员 | |
|
||||
| begin | 否 | date | 预计开始 | 支持时间查询 |
|
||||
| due | 否 | date | 预计结束 | 支持时间查询 |
|
||||
| created | 否 | datetime | 创建时间 | 支持时间查询 |
|
||||
| modified | 否 | datetime | 最后修改时间 | 支持时间查询 |
|
||||
| completed | 否 | datetime | 完成时间 | 支持时间查询 |
|
||||
| iteration_id | 否 | string | 迭代ID | 支持不等于查询或枚举查询 |
|
||||
| include_sub_iteration | 否 | string | 是否包含子迭代 | 取值 0或者1,默认取 0 |
|
||||
| effort | 否 | string | 预估工时 | |
|
||||
| effort_completed | 否 | string | 完成工时 | |
|
||||
| remain | 否 | float | 剩余工时 | |
|
||||
| exceed | 否 | float | 超出工时 | |
|
||||
| category_id | 否 | integer | 需求分类 | 支持枚举查询 |
|
||||
| include_sub_category | 否 | string | 是否包含子分类 | 取值 0或者1,默认取 0 |
|
||||
| release_id | 否 | integer | 发布计划 | |
|
||||
| source | 否 | string | 需求来源 | |
|
||||
| type | 否 | string | 需求类型 | |
|
||||
| ancestor_id | 否 | integer | 祖先需求,查询指定需求下所有子需求 | |
|
||||
| parent_id | 否 | integer | 父需求 | |
|
||||
| children_id | 否 | string | 子需求 | 为空查询传:丨 |
|
||||
| include_leaf_stories | 否 | string | 是否包含子需求 | 取值 0或者1,默认取 0 |
|
||||
| description | 否 | string | 详细描述 | 支持模糊匹配 |
|
||||
| workspace_id | `是` | integer | 项目ID | |
|
||||
| custom_field_* | 否 | string或者integer | 自定义字段参数,具体字段名通过接口 [获取需求自定义字段配置](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_story_custom_fields_settings.html) 获取 | 支持枚举查询 |
|
||||
| custom_plan_field_* | 否 | string或者integer | 自定义计划应用参数,具体字段名通过接口 [获取自定义计划应用](https://open.tapd.cn/document/api-doc/API文档/api_reference/iteration/get_plan_apps.html) 获取 | |
|
||||
| limit | 否 | integer | 设置返回数量限制,默认为30 | |
|
||||
| page | 否 | integer | 返回当前数量限制下第N页的数据,默认为1(第一页) | |
|
||||
| order | 否 | string | 排序规则,规则:字段名 ASC或者DESC,然后 urlencode | 如按创建时间逆序:order=created%20desc |
|
||||
| fields | 否 | string | 设置获取的字段,多个字段间以','逗号隔开 | |
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#调用示例及返回结果)调用示例及返回结果
|
||||
|
||||
#### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#获取项目下需求)获取项目下需求
|
||||
|
||||
#### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/story/get_stories.html#curl-使用-basic-auth-鉴权调用示例)curl 使用 Basic Auth 鉴权调用示例
|
||||
|
||||
```
|
||||
curl -u 'api_user:api_password' 'https://api.tapd.cn/stories?workspace_id=10158231'
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 缺陷
|
||||
|
||||
## 获取缺陷所有字段及候选值
|
||||
@ -599,7 +691,7 @@ GET
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#调用示例及返回结果)调用示例及返回结果
|
||||
|
||||
## [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#获取项目下的缺陷字段)获取项目下的缺陷字段
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#获取项目下的缺陷字段)获取项目下的缺陷字段
|
||||
|
||||
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#curl-使用-basic-auth-鉴权调用示例)curl 使用 Basic Auth 鉴权调用示例
|
||||
|
||||
|
||||
24
config/config_task2.ini
Normal file
24
config/config_task2.ini
Normal file
@ -0,0 +1,24 @@
|
||||
# 任务二配置文件:TAPD状态实时同步至腾讯智能表格
|
||||
|
||||
[TAPD]
|
||||
# TAPD项目ID
|
||||
workspace_id = 58335167
|
||||
|
||||
[SmartSheet]
|
||||
# 智能表格文档ID(任务二专用)
|
||||
# 支持配置多个docid,用逗号分隔,例如:
|
||||
# docid = doc1,doc2,doc3
|
||||
# 同步时会依次对所有表格进行同步
|
||||
# docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg
|
||||
docid =dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA
|
||||
|
||||
[Schedule]
|
||||
# 同步频率(分钟)
|
||||
sync_interval = 1
|
||||
|
||||
[wework]
|
||||
# 企业微信应用ID
|
||||
agentid = 1000615
|
||||
# 接收人列表(用户ID,多个用|分隔,@all表示全部成员)
|
||||
receivers = 046364
|
||||
# receivers = 114514
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"access_token": "SPFfB9jMxleGiJn76FQH6v5pploseAJXTCVkTVVxl1_PEmHZJiqXsNGpEyGAK4qmYe3WRxYJ57xgCQLRCHopVfeoDfP87IgxVCytCqQABGES5ndG05SVkrI-9evg8Z4kbstlsiRMmfPGGGoNUgL1kUoZc2No0FYytm8FTfulnAXfiTExzoF8OCTdEPc9mA0g8JKFhlkiS2F0agBESS_2_ewbcZvA0i44-ChTKRBdRa0",
|
||||
"fetch_time": 1767599732.4166858
|
||||
"access_token": "lFg2EG8MT_7cNhAK_huBHTiMoR3_GNR63rqlZm-LgL3cyn2hctqm3N65Wb-Z3Lki8fHDmWcutxtSqbA5PoVqUnyn8fs2RpJg7tom1DbDeKq--6qp3lBFvkHUDiCDzB5BklWoYYl9VxG22wCsjmE3Gep_mrNRSMWLZch5mhaSZwaWojSh3fXHY3q23oI-u-nElMKsKE0vk14V7j1W6MZIRkejhZV28R6Fd4zZPkiNha0",
|
||||
"fetch_time": 1780403671.4761236
|
||||
}
|
||||
31
config3/config.ini
Normal file
31
config3/config.ini
Normal file
@ -0,0 +1,31 @@
|
||||
[TAPD]
|
||||
workspace_id = 58335167
|
||||
|
||||
[Schedule]
|
||||
push_time = 15:57
|
||||
skip_weekend = true
|
||||
|
||||
[Smartsheet]
|
||||
# 成员配置文档ID(通过查询子表title自动匹配sheet_id)
|
||||
docid = dcidlinq5l8GWnXWpTVnp0zZDKki40d5fRf3y1Rhu8VXBbvDb2cqeypgtBegSmAFCsW5RwFF5DRl5DiiZXm2vJsA
|
||||
|
||||
[GroupPush]
|
||||
# 按顺序一一对应:技术 -> 美术 -> 策划
|
||||
group_count = 3
|
||||
|
||||
group1_name = 技术
|
||||
group1_sheet_title = 技术组成员配置
|
||||
group1_webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=026670e0-9e8d-4f61-9b3f-1f81365954ff
|
||||
# group1_webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ae0acb1f-0ea5-4560-abf7-d2d87adfd119
|
||||
|
||||
|
||||
group2_name = 美术
|
||||
group2_sheet_title = 美术组成员配置
|
||||
group2_webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=60f86f3d-c43b-4ebc-b2e1-ca7498d91586
|
||||
#group2_webhook_url =https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=67617aa6-203d-4072-97f5-8fd681a54651
|
||||
|
||||
|
||||
group3_name = 策划
|
||||
group3_sheet_title = 策划组成员配置
|
||||
group3_webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=e057b0ed-588c-4ad7-8811-3fa27eea8cd2
|
||||
#group3_webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=fd9171d1-6961-4701-a5f4-09e3fe1b8fab
|
||||
14
core/__init__.py
Normal file
14
core/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""全局核心模块导出"""
|
||||
|
||||
from core.global_log_system import (
|
||||
GlobalLogSystem,
|
||||
create_task1_log_system,
|
||||
create_task2_log_system,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"GlobalLogSystem",
|
||||
"create_task1_log_system",
|
||||
"create_task2_log_system",
|
||||
]
|
||||
|
||||
210
core/global_log_system.py
Normal file
210
core/global_log_system.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""
|
||||
全局日志系统内核
|
||||
|
||||
职责:
|
||||
1. 按天写入 jsonl 日志
|
||||
2. 统一记录 API 调用事件
|
||||
3. 记录同步开始/结束事件与统计
|
||||
4. 对 token 等敏感字段做脱敏
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class GlobalLogSystem:
|
||||
"""全局日志记录器"""
|
||||
|
||||
ALLOWED_MODULES = {"smartsheet", "tapd", "wework"}
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"access_token",
|
||||
"token",
|
||||
"authorization",
|
||||
"corpsecret",
|
||||
"api_password",
|
||||
"password",
|
||||
"secret",
|
||||
"auth",
|
||||
}
|
||||
|
||||
TOKEN_PATTERNS = [
|
||||
re.compile(r"(access_token=)([^&\s]+)", re.IGNORECASE),
|
||||
re.compile(r"(corpsecret=)([^&\s]+)", re.IGNORECASE),
|
||||
re.compile(r"(token=)([^&\s]+)", re.IGNORECASE),
|
||||
]
|
||||
|
||||
def __init__(self, task_name: str, log_dir: str | Path):
|
||||
self.task_name = task_name
|
||||
self.log_dir = Path(log_dir)
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._active_sync_id: Optional[str] = None
|
||||
|
||||
def start_sync(self,
|
||||
trigger: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
sync_id: Optional[str] = None) -> str:
|
||||
"""记录一次同步开始事件并返回 sync_id"""
|
||||
if sync_id is None:
|
||||
sync_id = self._generate_sync_id()
|
||||
|
||||
event = {
|
||||
"event_type": "start_sync",
|
||||
"timestamp": self._now_string(),
|
||||
"task": self.task_name,
|
||||
"sync_id": sync_id,
|
||||
"trigger": trigger,
|
||||
"metadata": self._sanitize(metadata or {}),
|
||||
}
|
||||
|
||||
self._active_sync_id = sync_id
|
||||
self._write_event_safely(event)
|
||||
return sync_id
|
||||
|
||||
def log_api(self,
|
||||
module: str,
|
||||
operation: str,
|
||||
request_data: Optional[Dict[str, Any]],
|
||||
response_data: Optional[Dict[str, Any]],
|
||||
success: bool,
|
||||
error_message: Optional[str] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
sync_id: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""记录单次 API 调用事件"""
|
||||
if module not in self.ALLOWED_MODULES:
|
||||
raise ValueError(
|
||||
f"module 不合法: {module},允许值: {sorted(self.ALLOWED_MODULES)}"
|
||||
)
|
||||
|
||||
resolved_sync_id = sync_id or self._active_sync_id
|
||||
event = {
|
||||
"event_type": "api_call",
|
||||
"timestamp": self._now_string(),
|
||||
"task": self.task_name,
|
||||
"sync_id": resolved_sync_id,
|
||||
"module": module,
|
||||
"operation": operation,
|
||||
"success": success,
|
||||
"request": self._sanitize(request_data or {}),
|
||||
"response": self._sanitize(response_data or {}),
|
||||
"error_message": self._sanitize_text(error_message),
|
||||
"duration_ms": duration_ms,
|
||||
"extra": self._sanitize(extra or {}),
|
||||
}
|
||||
|
||||
self._write_event_safely(event)
|
||||
|
||||
def end_sync_with_stats(self,
|
||||
stats: Dict[str, Any],
|
||||
success: bool,
|
||||
error_message: Optional[str] = None,
|
||||
sync_id: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""记录一次同步结束事件"""
|
||||
resolved_sync_id = sync_id or self._active_sync_id
|
||||
event = {
|
||||
"event_type": "end_sync",
|
||||
"timestamp": self._now_string(),
|
||||
"task": self.task_name,
|
||||
"sync_id": resolved_sync_id,
|
||||
"success": success,
|
||||
"stats": self._sanitize(stats),
|
||||
"error_message": self._sanitize_text(error_message),
|
||||
"extra": self._sanitize(extra or {}),
|
||||
}
|
||||
|
||||
self._write_event_safely(event)
|
||||
|
||||
if resolved_sync_id == self._active_sync_id:
|
||||
self._active_sync_id = None
|
||||
|
||||
def _write_event_safely(self, event: Dict[str, Any]) -> None:
|
||||
"""安全写入:日志失败不影响主流程"""
|
||||
try:
|
||||
self._write_event(event)
|
||||
except Exception as exc:
|
||||
print(f"⚠️ 全局日志写入失败: {exc}")
|
||||
|
||||
def _write_event(self, event: Dict[str, Any]) -> None:
|
||||
log_file = self._get_today_log_file()
|
||||
with open(log_file, "a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
|
||||
def _get_today_log_file(self) -> Path:
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
return self.log_dir / f"api_log_{today}.jsonl"
|
||||
|
||||
def _generate_sync_id(self) -> str:
|
||||
time_part = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
random_part = uuid.uuid4().hex[:8]
|
||||
return f"{self.task_name}_{time_part}_{random_part}"
|
||||
|
||||
def _now_string(self) -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def _sanitize(self, value: Any, parent_key: str = "") -> Any:
|
||||
if isinstance(value, dict):
|
||||
sanitized: Dict[str, Any] = {}
|
||||
for key, item in value.items():
|
||||
key_lower = str(key).lower()
|
||||
if key_lower in self.SENSITIVE_KEYS:
|
||||
sanitized[key] = self._mask_sensitive_value(item)
|
||||
else:
|
||||
sanitized[key] = self._sanitize(item, parent_key=key_lower)
|
||||
return sanitized
|
||||
|
||||
if isinstance(value, list):
|
||||
return [self._sanitize(item, parent_key=parent_key) for item in value]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
return tuple(self._sanitize(item, parent_key=parent_key) for item in value)
|
||||
|
||||
if isinstance(value, str):
|
||||
if parent_key in self.SENSITIVE_KEYS:
|
||||
return self._mask_sensitive_value(value)
|
||||
return self._sanitize_text(value)
|
||||
|
||||
return value
|
||||
|
||||
def _sanitize_text(self, text: Optional[str]) -> Optional[str]:
|
||||
if text is None:
|
||||
return None
|
||||
|
||||
masked = text
|
||||
for pattern in self.TOKEN_PATTERNS:
|
||||
masked = pattern.sub(r"\1***", masked)
|
||||
|
||||
return masked
|
||||
|
||||
def _mask_sensitive_value(self, value: Any) -> str:
|
||||
text = "" if value is None else str(value)
|
||||
if not text:
|
||||
return "***"
|
||||
|
||||
if text.lower().startswith("bearer "):
|
||||
return "Bearer ***"
|
||||
|
||||
if len(text) <= 6:
|
||||
return "***"
|
||||
|
||||
return f"{text[:3]}***{text[-2:]}"
|
||||
|
||||
|
||||
def create_task1_log_system(project_root: Optional[Path] = None) -> GlobalLogSystem:
|
||||
"""创建任务一日志系统(固定 logs/)"""
|
||||
root = project_root or Path(__file__).parent.parent
|
||||
return GlobalLogSystem(task_name="task1", log_dir=root / "logs")
|
||||
|
||||
|
||||
def create_task2_log_system(project_root: Optional[Path] = None) -> GlobalLogSystem:
|
||||
"""创建任务二日志系统(固定 logs2/)"""
|
||||
root = project_root or Path(__file__).parent.parent
|
||||
return GlobalLogSystem(task_name="task2", log_dir=root / "logs2")
|
||||
|
||||
36
docs/全局任务列表.md
Normal file
36
docs/全局任务列表.md
Normal file
@ -0,0 +1,36 @@
|
||||
# 全局任务列表
|
||||
|
||||
> 用途:按层级维护项目任务、阶段状态与新增需求,避免任务漂移。
|
||||
|
||||
---
|
||||
|
||||
## 任务一:智能表格自动开 TAPD Bug 单
|
||||
|
||||
- [x] 扫描智能表格待开单记录
|
||||
- [x] 校验必填字段并回写开单状态
|
||||
- [x] 创建 TAPD Bug 并回写 TAPD 单号、Bug 状态
|
||||
- [x] 校验失败企微通知
|
||||
- [x] 处理 TAPD 429 限速导致的通知缺失
|
||||
- [x] 识别 TAPD HTTP 429 与业务限速错误
|
||||
- [x] TAPD 开单限速时等待2分钟重试当前记录
|
||||
- [x] 重试仍限速时停止本轮继续开单,保留未处理记录等待下次调度
|
||||
- [x] TAPD 开单失败与限速事件纳入企微推送
|
||||
- [x] 任务一 Bug 状态同步改为 TAPD 多 ID 批量查询,每批最多 200 个 ID
|
||||
- [x] 任务一 Bug 状态同步触发 TAPD 429 时等待2分钟重试当前批次
|
||||
- [x] 重试仍限速时停止本轮后续状态查询,并将失败批次纳入企微推送
|
||||
|
||||
## 任务二:TAPD 状态实时同步至智能表格
|
||||
|
||||
- [x] 扫描智能表格中已同步且非终态记录
|
||||
- [x] 查询 TAPD 最新状态
|
||||
- [x] 回写智能表格 TAPD 状态与相关字段
|
||||
|
||||
## 后续建议任务
|
||||
|
||||
- [ ] TAPD 请求节流与退避策略
|
||||
- [ ] 配置化单次请求间隔
|
||||
- [ ] 支持每轮最大处理数量,避免数百条记录一次性打满 TAPD 限额
|
||||
- [ ] 支持按 `Retry-After` 动态覆盖固定2分钟等待
|
||||
- [ ] 任务二 TAPD Story 批量查询优化
|
||||
- [ ] 调研 `stories` 接口多 ID 查询的分隔符与返回格式
|
||||
- [ ] 将 `src2/sync_service.py` 的逐条 `get_story` 优化为批量查询并复用缓存
|
||||
162
docs/全局框架文档.md
Normal file
162
docs/全局框架文档.md
Normal file
@ -0,0 +1,162 @@
|
||||
# 全局框架文档
|
||||
|
||||
> 用途:统一维护项目模块、脚本职责与接口定义,避免调用不存在接口与跨任务耦合失控。
|
||||
> 范围:`src/`、`src2/`、`src3/`,以及新增的全局模块(如后续 `core/`)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 日志系统总览(重构目标态)
|
||||
|
||||
### 1.1 目标架构
|
||||
- **全局日志内核**:位于任务目录外,提供统一记录能力。
|
||||
- **任务一适配层**:写入 `logs/`。
|
||||
- **任务二适配层**:写入 `logs2/`。
|
||||
- **同步边界管理**:每次同步使用唯一 `sync_id` 分隔,结束时写统计。
|
||||
|
||||
### 1.2 统一日志字段(约定)
|
||||
- `event_type`:`start_sync` / `api_call` / `end_sync`
|
||||
- `timestamp`:事件时间
|
||||
- `task`:`task1` / `task2`
|
||||
- `sync_id`:单次同步唯一标识
|
||||
- `module`:`smartsheet` / `tapd` / `wework`
|
||||
- `operation`:接口操作名
|
||||
- `request`:请求快照(含脱敏)
|
||||
- `response`:响应快照(含脱敏)
|
||||
- `success`:调用是否成功
|
||||
- `error_message`:失败原因
|
||||
- `duration_ms`:调用耗时(毫秒)
|
||||
- `stats`:同步完成统计(仅 `end_sync`)
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块清单与职责(当前状态)
|
||||
|
||||
## 2.0 全局核心(`core/`)
|
||||
- `core/global_log_system.py`
|
||||
- **职责**:提供统一日志内核(jsonl 写入、同步分隔、API 事件、统计事件、脱敏)。
|
||||
- **接口(对内)**:`start_sync`、`log_api`、`end_sync_with_stats`。
|
||||
- **创建器**:`create_task1_log_system`(固定 `logs/`)、`create_task2_log_system`(固定 `logs2/`)。
|
||||
- `core/__init__.py`
|
||||
- **职责**:导出全局日志内核与创建器。
|
||||
|
||||
## 2.1 任务一(`src/`)
|
||||
- `src/scheduler.py`
|
||||
- **职责**:任务一调度入口,定时触发单次同步。
|
||||
- **关键依赖**:`src/main.py`。
|
||||
- `src/main.py`
|
||||
- **职责**:任务一主流程编排(扫描、校验、开单、回写、通知)。
|
||||
- **接口(对内)**:`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 封装(创建、查询、附件上传)。
|
||||
- **接口(对内)**:`RateLimitError`、`create_bug`、`get_bug`、`get_bugs_by_ids`、`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(...)`、`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()`。
|
||||
- **关键点**:任务一目录下的 Bug 状态同步模块;按最多 200 个 Bug ID 批量查询 TAPD,触发 429 时等待 120 秒重试当前批次一次,重试仍限速则停止本轮后续状态查询并进入企微异常通知。
|
||||
|
||||
## 2.2 任务二(`src2/`)
|
||||
- `src2/scheduler.py`
|
||||
- **职责**:任务二调度入口,定时触发同步并记录每次同步边界与统计。
|
||||
- **关键依赖**:`src2/sync_service.py`。
|
||||
- `src2/sync_service.py`
|
||||
- **职责**:任务二同步编排(读取、解析链接、查询 TAPD、回写、通知)。
|
||||
- **接口(对内)**:`sync_once()`、`run_once(...)`(无外层同步时自动兜底边界)。
|
||||
- `src2/smartsheet.py`
|
||||
- **职责**:任务二智能表格 API 适配层。
|
||||
- **关键点**:应固定写入 `logs2/`。
|
||||
- `src2/smartsheet_sync.py`
|
||||
- **职责**:任务二表格字段检查、记录提取与回写构造。
|
||||
- `src2/tapd_api.py`
|
||||
- **职责**:任务二 TAPD Story 查询与状态映射。
|
||||
- **关键点**:`_make_request` 统一处理 TAPD Story 查询;任务二当前不包含本阶段 429 改动。
|
||||
- `src2/notifier.py`
|
||||
- **职责**:任务二失败通知封装(复用任务一通知器并显式注入任务二 logger,避免串目录)。
|
||||
- `src2/logger.py`
|
||||
- **职责**:任务二日志实例入口(固定写入 `logs2/`,接入统一内核兼容层)。
|
||||
|
||||
## 2.3 任务三(`src3/`)
|
||||
- `src3/config.py`
|
||||
- **职责**:任务三配置入口;读取分组推送配置(组名/成员子表标题/Webhook),并通过子表标题自动解析 `sheet_id` 后加载成员白名单和 `TAPD用户名 -> 企微UserID` 映射。
|
||||
- **接口(对内)**:`get_workspace_id()`、`get_push_time()`、`get_group_push_configs()`、`get_group_team_configs(...)`。
|
||||
- `src3/main.py`
|
||||
- **职责**:任务三主流程编排;合并多组成员执行一次过期单拉取,再按组过滤并分发到各组 webhook。
|
||||
- **接口(对内)**:`run_once()`。
|
||||
- `src3/tapd_api.py`
|
||||
- **职责**:任务三 TAPD 查询封装(需求 + 缺陷),内置终态过滤、过期判定字段提取与重试。
|
||||
- `src3/overdue_fetcher.py`
|
||||
- **职责**:聚合 TAPD 结果并补齐过期天数、详情链接。
|
||||
- `src3/message_formatter.py`
|
||||
- **职责**:按处理人分组排序并生成企微 Markdown 内容;输出 `mentioned_list`。
|
||||
- `src3/webhook_sender.py`
|
||||
- **职责**:Webhook 推送与失败重试(重试2次,间隔30秒);超长消息拆分发送。
|
||||
- `src3/scheduler.py`
|
||||
- **职责**:任务三定时调度入口(工作日定时触发 `run_once`)。
|
||||
- `src3/logger.py`
|
||||
- **职责**:任务三日志实例入口(固定写入 `logs3/`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现存问题与待改造点(阶段3后)
|
||||
|
||||
- **串目录问题(生产链路)**:已修复,`src2` 生产入口统一显式注入任务二 logger。
|
||||
- **双记录矛盾(任务一)**:已修复;任务二当前未发现同请求双写路径。
|
||||
- **写入稳定性问题**:已由统一 `jsonl` 事件流替代旧数组拼接策略。
|
||||
- **阶段4待办**:日志查看工具尚未完成 `jsonl + sync_id` 体验优化。
|
||||
- **任务三V2注意点**:分组成员若存在交叉,当前会导致同一过期单在多个群重复推送,应通过配置治理。
|
||||
|
||||
---
|
||||
|
||||
## 4. 模块接口演进计划(摘要)
|
||||
|
||||
- 阶段1:新增全局日志内核模块,定义统一接口。(已完成)
|
||||
- 阶段2:`src/api_logger.py` 改造成兼容层,保证旧调用可用。(已完成)
|
||||
- 阶段3:`src2/logger.py` 与任务二编排层切换到统一内核,修复串目录。(已完成)
|
||||
- 阶段4:更新查看工具与文档,支持 `jsonl + sync_id`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
### 2026-02-28
|
||||
- 新建本框架文档。
|
||||
- 写入日志系统重构目标态、模块职责清单、问题清单与演进路线。
|
||||
|
||||
### 2026-02-28(更新)
|
||||
- 新增 `core/global_log_system.py` 与 `core/__init__.py` 模块说明。
|
||||
- 标记阶段1完成:统一日志内核已落地,待阶段2/3接线。
|
||||
|
||||
### 2026-02-28(更新2)
|
||||
- 标记阶段2完成:任务一已接入统一日志内核。
|
||||
- `src/scheduler.py` 的 `job/sync_job` 已接入同步边界与统计事件。
|
||||
- `src/main.py` 的 `run_once` 已接入手动兜底同步边界。
|
||||
- `src/smartsheet.py` 已消除同请求双记录矛盾。
|
||||
|
||||
### 2026-03-10(更新3)
|
||||
- 标记阶段3完成:任务二生产链路已接入同步边界与统计收尾。
|
||||
- TokenManager、WeWorkNotifier 已支持 logger 注入,用于任务二目录隔离。
|
||||
- src2/notifier.py、src2/sync_service.py、src2/main.py、src2/scheduler.py 已完成 logs2 链路闭环。
|
||||
|
||||
### 2026-04-01(更新4)
|
||||
- 新增任务三(`src3/`)模块清单与职责说明。
|
||||
- 任务三配置改为多群分组推送:按顺序维护“组名-成员子表标题-Webhook”一一对应关系。
|
||||
- 任务三成员配置读取由手填 `sheet_id` 调整为通过子表标题自动解析 `sheet_id`。
|
||||
|
||||
### 2026-06-02(更新5)
|
||||
- `src/tapd_api.py` 新增 `RateLimitError`,用于区分 TAPD 429 限速与普通运行错误。
|
||||
- `src/tapd_api.py` 新增 `get_bugs_by_ids(...)`,用于任务一 Bug 状态同步按 TAPD 缺陷接口多 ID 批量查询。
|
||||
- `src/main.py` 在 TAPD 开单触发限速时等待 120 秒重试当前记录;重试仍失败时停止本轮后续开单,并将限速事件纳入企微异常通知。
|
||||
- `src/sync_status.py` 将任务一 Bug 状态同步从逐条 `get_bug` 改为每批最多 200 个 ID 的批量查询,触发 429 时等待 120 秒重试当前批次一次。
|
||||
- `src/wework_notifier.py` 新增 `send_operation_failure_notification(...)`,用于运行异常类通知。
|
||||
316
docs/全局迭代日志.md
Normal file
316
docs/全局迭代日志.md
Normal file
@ -0,0 +1,316 @@
|
||||
# 全局迭代日志
|
||||
|
||||
> 用途:跨任务(`src` / `src2`)记录每个阶段的增改内容、验收结果与回滚点。
|
||||
> 约束:每次阶段验收通过后,必须追加一条日志记录。
|
||||
|
||||
---
|
||||
|
||||
## 记录模板
|
||||
|
||||
### 阶段:
|
||||
- **阶段名称**:
|
||||
- **日期**:
|
||||
- **目标**:
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:
|
||||
- **修改文件**:
|
||||
- **删除文件**:
|
||||
|
||||
### 关键改动说明
|
||||
- **日志结构变更**:
|
||||
- **接口/调用链变更**:
|
||||
- **兼容性说明**:
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- **未通过项**:
|
||||
- **遗留风险**:
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:
|
||||
- **关联文档**:
|
||||
- **备注**:
|
||||
|
||||
---
|
||||
|
||||
## 阶段日志
|
||||
|
||||
## 阶段0:文档与约定先行
|
||||
- **阶段名称**:日志系统重构 - 阶段0
|
||||
- **日期**:2026-02-28
|
||||
- **目标**:建立重构方案与全局文档骨架,为后续代码改造提供统一约束。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:
|
||||
- `docs/日志系统重构实施方案.md`
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- **修改文件**:无
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- **日志结构变更**:确定后续采用 `jsonl`,并以 `sync_id` 分隔每次同步。
|
||||
- **接口/调用链变更**:明确生产链路以 `src/scheduler.py`、`src2/scheduler.py` 为准。
|
||||
- **兼容性说明**:阶段0仅文档,不影响现网行为。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 分阶段路线、边界条件、验收清单已落文档。
|
||||
- 已建立全局迭代日志与框架文档容器。
|
||||
- **未通过项**:无
|
||||
- **遗留风险**:
|
||||
- 任务二存在复用模块导致串目录风险(待阶段3修复)。
|
||||
- 现有日志写入策略存在双记录与结构损坏风险(待阶段1/2修复)。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:当前为纯文档提交,可直接整提交回滚。
|
||||
- **关联文档**:`docs/日志系统重构实施方案.md`
|
||||
- **备注**:阶段1开始前需再次确认日志字段最终版。
|
||||
|
||||
## 阶段1:实现全局日志内核
|
||||
- **阶段名称**:日志系统重构 - 阶段1
|
||||
- **日期**:2026-02-28
|
||||
- **负责人**:Codex
|
||||
- **目标**:在任务目录外提供可复用的统一日志内核,支持 jsonl、sync_id、token 脱敏。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:
|
||||
- `core/global_log_system.py`
|
||||
- `core/__init__.py`
|
||||
- **修改文件**:
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- **日志结构变更**:新增 `start_sync` / `api_call` / `end_sync` 三类事件模型。
|
||||
- **接口/调用链变更**:提供 `start_sync`、`log_api`、`end_sync_with_stats` 三个核心接口。
|
||||
- **兼容性说明**:阶段1仅新增内核,未接入任务一/任务二业务调用,不影响现网逻辑。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 已支持按天写入 `api_log_YYYY-MM-DD.jsonl`。
|
||||
- 已支持 `sync_id` 生命周期记录。
|
||||
- 已支持 token/secret 脱敏。
|
||||
- 已提供任务一、任务二创建器(固定目录)。
|
||||
- **未通过项**:
|
||||
- 尚未接入 `src` / `src2`,串目录与双记录矛盾仍待后续阶段修复。
|
||||
- **遗留风险**:
|
||||
- 阶段2/3接线时若沿用旧 logger 分支逻辑,可能再次引入双记录。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:`core/` 新增为独立改动,可单独回滚。
|
||||
- **关联文档**:`docs/日志系统重构实施方案.md`
|
||||
- **备注**:下一阶段优先完成任务一接入并验证“单次调用单条记录”。
|
||||
|
||||
## 阶段2:接入任务一(src)
|
||||
- **阶段名称**:日志系统重构 - 阶段2
|
||||
- **日期**:2026-02-28
|
||||
- **负责人**:Codex
|
||||
- **目标**:将任务一接入全局日志内核,补齐同步分隔与统计,并修复双记录矛盾。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:无
|
||||
- **修改文件**:
|
||||
- `src/api_logger.py`
|
||||
- `src/main.py`
|
||||
- `src/scheduler.py`
|
||||
- `src/smartsheet.py`
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- **日志结构变更**:任务一日志由旧 JSON 数组切换为 jsonl 事件流(通过兼容层接入)。
|
||||
- **接口/调用链变更**:
|
||||
- `src/api_logger.py` 保留旧接口,内部转发到全局内核。
|
||||
- `src/scheduler.py` 的 `job/sync_job` 增加 `start_sync/end_sync_with_stats`。
|
||||
- `src/main.py` 的 `run_once` 增加“无外层同步时自动兜底”的同步边界。
|
||||
- **兼容性说明**:旧调用 `log_api_call(...)` 保持可用;历史 `api_type` 通过映射落入 `module` 三类。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 任务一生产链路(scheduler 触发)已具备每次同步分隔与统计写入。
|
||||
- 任务一 API 日志已接入统一内核。
|
||||
- `src/smartsheet.py` 已修复“同一次请求先 success 后 failure”的双记录问题。
|
||||
- **未通过项**:
|
||||
- 任务二串目录问题尚未处理(待阶段3)。
|
||||
- **遗留风险**:
|
||||
- `main.py` 直跑与 scheduler 路径都可触发同步边界,后续需确保运维使用一致入口。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:可按文件粒度回滚(`src/api_logger.py` 与 `src/scheduler.py` 为关键点)。
|
||||
- **关联文档**:`docs/日志系统重构实施方案.md`
|
||||
- **备注**:下一阶段优先处理任务二串目录与通知链路复用问题。
|
||||
|
||||
## 阶段3:接入任务二(src2)
|
||||
- **阶段名称**:日志系统重构 - 阶段3
|
||||
- **日期**:2026-03-10
|
||||
- **负责人**:Codex
|
||||
- **目标**:修复任务二串目录问题,补齐任务二同步分隔与统计收尾,确保生产链路日志只落 `logs2/`。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:无
|
||||
- **修改文件**:
|
||||
- `src/token_manager.py`
|
||||
- `src/wework_notifier.py`
|
||||
- `src2/scheduler.py`
|
||||
- `src2/main.py`
|
||||
- `src2/sync_service.py`
|
||||
- `src2/notifier.py`
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- **日志结构变更**:任务二调度链路与手动入口均补齐 `start_sync/end_sync_with_stats`,按 `sync_id` 分隔并写入统计。
|
||||
- **接口/调用链变更**:
|
||||
- `TokenManager` 与 `WeWorkNotifier` 新增可选 `logger` 注入参数。
|
||||
- `src2/scheduler.py`、`src2/main.py`、`src2/sync_service.py` 全部接入任务二 logger 边界控制。
|
||||
- `src2/notifier.py` 调用通知器时显式注入 `get_task2_logger()`。
|
||||
- **兼容性说明**:旧调用不传 `logger` 仍保持任务一默认行为,生产链路通过显式注入实现目录隔离。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 任务二生产链路(`src2/scheduler.py`)已具备每次同步开始/结束事件与统计写入。
|
||||
- 任务二手动入口(`src2/main.py`)与 `run_once` 兜底路径已具备同步边界。
|
||||
- 任务二 token 获取、通知链路、sync_service 自动取 token 全部使用任务二 logger,不再串写 `logs/`。
|
||||
- `api_type` 已明确映射为 `smartsheet/tapd/wework` 三类语义。
|
||||
- **未通过项**:
|
||||
- 未执行运行态联调(仅完成静态改造与代码级复核)。
|
||||
- **遗留风险**:
|
||||
- `src2/test_*` 中仍有 `TokenManager()` 默认实例,属于测试路径,不影响生产链路。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:可按文件粒度回滚,优先关注 `src2/scheduler.py`、`src2/main.py`、`src2/sync_service.py`。
|
||||
- **关联文档**:`docs/日志系统重构实施方案.md`
|
||||
- **备注**:阶段4建议优先补日志查看工具对 `jsonl + sync_id` 的检索能力。
|
||||
|
||||
## 阶段4:任务三V2多群分组推送(src3)
|
||||
- **阶段名称**:任务三迭代 - V2 多群分组推送
|
||||
- **日期**:2026-04-01
|
||||
- **负责人**:Codex
|
||||
- **目标**:在不改 `src/`、`src2/` 的前提下,将任务三由单群推送升级为技术/美术/策划三群分组推送,并按子表标题自动解析成员配置表 `sheet_id`。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:无
|
||||
- **修改文件**:
|
||||
- `src3/config.py`
|
||||
- `src3/main.py`
|
||||
- `config3/config.ini`
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- **日志结构变更**:无新增日志事件类型,沿用任务三现有同步边界和统计写入。
|
||||
- **接口/调用链变更**:
|
||||
- `src3/config.py` 新增 `get_group_push_configs` 与 `get_group_team_configs`,从配置读取“组名-子表标题-Webhook”顺序映射,并通过 `smartsheet/get_sheet` 自动按标题解析 `sheet_id`。
|
||||
- `src3/main.py` 从“单组白名单 + 单Webhook”改为“多组配置”,执行一次 TAPD 拉取后按组成员过滤并分别推送到对应群。
|
||||
- 空白名单由“全局失败”调整为“按组跳过并继续其他组”;当所有组均为空时整体跳过本次推送。
|
||||
- **兼容性说明**:
|
||||
- 仅改 `src3` 与 `config3`,未触碰 `src/`、`src2/`。
|
||||
- `config3/config.ini` 改为 `GroupPush` 分组配置,移除手填 `sheet_id`。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 支持三组(技术/美术/策划)Webhook 一一对应配置,并按顺序生效。
|
||||
- 支持通过子表 `title` 自动解析成员配置 `sheet_id`,无需手填 `sheet_id`。
|
||||
- TAPD 过期单拉取改为合并成员单次拉取,避免重复调用后再按组分发。
|
||||
- **未通过项**:
|
||||
- 尚未执行运行态联调(未在命令行执行 Python,自检留待人工验收)。
|
||||
- **遗留风险**:
|
||||
- 若同一 TAPD 用户同时存在于多个组,当前会在多个群重复推送(由配置侧避免)。
|
||||
- Webhook 当前为测试地址,合入生产前需替换并再次验收。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:可按文件粒度回滚,关键文件为 `src3/config.py`、`src3/main.py`、`config3/config.ini`。
|
||||
- **关联文档**:`需求文档.md`(任务三 V2 段落)
|
||||
- **备注**:建议下一阶段补“配置校验命令”与“跨组重复成员检测告警”。
|
||||
|
||||
## 阶段4-补丁1:任务三V2消息头增加组别名
|
||||
- **阶段名称**:任务三迭代 - V2 文案补丁1
|
||||
- **日期**:2026-04-01
|
||||
- **负责人**:Codex
|
||||
- **目标**:每个分组推送消息标题增加组别名,格式如“⏰ TAPD 过期单提醒 — 策划组(YYYY-MM-DD)”。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:无
|
||||
- **修改文件**:
|
||||
- `src3/message_formatter.py`
|
||||
- `src3/main.py`
|
||||
- `docs/全局迭代日志.md`
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- `MessageFormatter.format_message(...)` 新增 `group_name` 参数,消息头按组名渲染。
|
||||
- 若组名未包含“组”后缀,自动补齐“组”。
|
||||
- `src3/main.py` 在按组发送时将当前组名传入格式化器。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 分组消息头可显示组别名(技术组/美术组/策划组)。
|
||||
- **未通过项**:
|
||||
- 尚未执行运行态联调(未在命令行执行 Python,自检留待人工验收)。
|
||||
- **遗留风险**:
|
||||
- 若配置中的组名为空,消息头将退化为不带组别名(由配置校验保障)。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:`src3/message_formatter.py` 与 `src3/main.py` 可独立回滚。
|
||||
- **关联文档**:`需求文档.md`(任务三 V2 段落)
|
||||
- **备注**:可在后续补“消息模板配置化”减少文案硬编码。
|
||||
|
||||
## 阶段5:任务一 TAPD 429 限速通知与状态批量查询修复
|
||||
- **阶段名称**:任务一 TAPD 429 限速与企微通知补丁
|
||||
- **日期**:2026-06-02
|
||||
- **负责人**:Codex
|
||||
- **目标**:定位并修复智能表格存在数百条记录时,任务一开单和任务一 Bug 状态同步触发 TAPD 429 后不会触发企微推送、状态查询请求过多或未等待重试的问题。
|
||||
|
||||
### 根因
|
||||
- **开单链路**:`src/main.py` 只把字段校验失败加入企微通知集合;TAPD 创建失败虽然会写回 `❌`,但不会触发企微推送。
|
||||
- **任务归属修正**:本阶段 429 问题归属任务一;任务二 `src2/` 改动已回滚,不纳入本阶段。
|
||||
- **任务一状态同步链路**:`src/sync_status.py` 原先逐条调用 `self.tapd_api.get_bug(bug_id)` 查询状态,数百条记录会放大 TAPD 限速风险。
|
||||
- **状态查询结果处理**:单条查询失败需要进入失败统计和企微异常通知,避免被误判为“状态无变化”。
|
||||
- **接口能力未利用**:TAPD `bugs` 查询接口支持多 ID 查询,默认返回 30 条,可通过 `limit` 设置到最大 200。
|
||||
|
||||
### 变更清单
|
||||
- **新增文件**:无
|
||||
- **修改文件**:
|
||||
- `src/tapd_api.py`
|
||||
- `src/sync_status.py`
|
||||
- `src/main.py`
|
||||
- `src/wework_notifier.py`
|
||||
- `docs/全局任务列表.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- `docs/全局迭代日志.md`
|
||||
- **删除文件**:无
|
||||
|
||||
### 关键改动说明
|
||||
- `src/tapd_api.py` 新增 `RateLimitError`,识别 HTTP 429 与业务错误文本中的限速信息。
|
||||
- `src/tapd_api.py` 新增 `get_bugs_by_ids(...)`,通过 `GET /bugs` 传 `id`、`limit`、`page`、`fields` 批量获取 Bug 信息,默认只取 `id,status`。
|
||||
- `src/main.py` 在开单触发限速后等待 120 秒重试当前记录一次;重试仍触发限速时才停止本轮后续 TAPD 开单,未处理记录保持未开单状态,等待下次调度重试。
|
||||
- `src/main.py` 将 TAPD 开单失败与限速事件纳入企微异常通知。
|
||||
- `src/sync_status.py` 将任务一 Bug 状态同步从逐条 `get_bug` 改为每批最多 200 个 ID 的批量查询。
|
||||
- `src/sync_status.py` 在批量状态查询触发 TAPD 429 时等待 120 秒重试当前批次一次;重试仍限速则停止本轮后续状态查询,并将失败批次纳入企微异常通知。
|
||||
- `src/wework_notifier.py` 的运行异常通知文案补充“当前记录或批次”,覆盖开单和状态查询两类限速场景。
|
||||
|
||||
### 验收结果
|
||||
- **通过项**:
|
||||
- 已完成静态链路复查:限速错误从 TAPD API 层到任务一开单/状态同步编排层都有专用分支。
|
||||
- 已确认开单链路触发 429 后会先等待 120 秒重试当前记录。
|
||||
- 已确认任务一状态同步不再逐条调用 `get_bug`,而是通过 `get_bugs_by_ids` 按最多 200 个 ID 批量查询。
|
||||
- 已确认任务一状态同步触发 429 后会等待 120 秒重试当前批次一次。
|
||||
- 已确认开单链路和状态同步链路的 TAPD 异常都会进入企微运行异常通知。
|
||||
- 已确认 `src2/tapd_api.py` 的任务二 429 改动已回滚。
|
||||
- **未通过项**:
|
||||
- 尚未执行运行态联调(按项目约束未在命令行执行 Python)。
|
||||
- **遗留风险**:
|
||||
- 当前批量查询的多 ID 分隔符按英文逗号处理,需要运行态联调确认与 TAPD 实际返回一致。
|
||||
- 当前补丁是“限速后固定等待 120 秒重试一次”,尚未实现主动节流、按 `Retry-After` 动态等待或单轮处理上限。
|
||||
- 若 TAPD 限速信息不包含 429、限速、频繁等关键词,仍可能被识别为普通运行错误。
|
||||
|
||||
### 回滚与追踪
|
||||
- **可回滚点**:优先按文件粒度回滚 `src/tapd_api.py`、`src/sync_status.py`、`src/main.py`、`src/wework_notifier.py`。
|
||||
- **关联文档**:`docs/全局框架文档.md`、`docs/全局任务列表.md`
|
||||
- **备注**:下一阶段建议做“请求节流 + 单轮最大处理数 + Retry-After 动态退避”,这是从根上降低 429 的方案。
|
||||
119
docs/日志系统重构实施方案.md
Normal file
119
docs/日志系统重构实施方案.md
Normal file
@ -0,0 +1,119 @@
|
||||
# 日志系统重构实施方案(任务一 + 任务二)
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
### 1.1 重构目标
|
||||
- 在任务一和任务二之外,提供统一的全局日志记录系统。
|
||||
- 覆盖生产链路:`src/scheduler.py` 与 `src2/scheduler.py` 触发的同步流程。
|
||||
- 在每一个发起 API 调用的地方记录请求与结果(成功/失败都记录)。
|
||||
- 任务一日志落在 `logs/`,任务二日志落在 `logs2/`。
|
||||
- 每条日志必须包含 `module` 字段,允许值:`smartsheet` / `tapd` / `wework`。
|
||||
- 通过 `sync_id` 分隔每次同步,并在同步完成后写入当次统计信息。
|
||||
|
||||
### 1.2 明确要解决的问题
|
||||
- 解决“串目录”问题:任务二不得写入 `logs/`,任务一不得写入 `logs2/`。
|
||||
- 解决“双记录矛盾”问题:同一次 API 调用只能有一条最终记录(不能先 success 再 failure)。
|
||||
- 日志格式改为 `jsonl`(每行一条完整 JSON 记录)。
|
||||
- 对 token 做脱敏,不做响应内容截断。
|
||||
|
||||
### 1.3 非目标
|
||||
- 本次不改业务流程语义(只改日志系统与接线方式)。
|
||||
- 本次不对历史 JSON 文件做离线迁移,仅保证新写入生效。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体设计
|
||||
|
||||
### 2.1 新增全局日志内核(任务外)
|
||||
- 新增独立模块(建议路径:`core/global_log_system.py`)。
|
||||
- 提供统一接口:
|
||||
- `start_sync(...)`:开始一次同步,生成 `sync_id`。
|
||||
- `log_api(...)`:记录单次 API 调用结果(成功/失败统一出口)。
|
||||
- `end_sync_with_stats(...)`:写入同步统计并结束。
|
||||
- 通过构造参数确定任务上下文:`task_name`、`log_dir`、默认 `module`。
|
||||
|
||||
### 2.2 日志存储格式
|
||||
- 文件命名:`api_log_YYYY-MM-DD.jsonl`。
|
||||
- 存储目录:任务一 `logs/`,任务二 `logs2/`。
|
||||
- 记录模型:
|
||||
- 通用字段:`event_type`、`timestamp`、`task`、`sync_id`、`module`。
|
||||
- API 事件字段:`operation`、`request`、`response`、`success`、`error_message`、`duration_ms`。
|
||||
- 同步边界字段:`start_sync` / `end_sync` 事件及统计 `stats`。
|
||||
|
||||
### 2.3 脱敏规则
|
||||
- `request/response` 中出现 token 字段统一脱敏(如 `access_token`、`Authorization`、`corpsecret`、`api_password`)。
|
||||
- 脱敏策略:保留前后少量字符,中间替换为 `***`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分阶段实施路线(小步走)
|
||||
|
||||
## 阶段0:文档与约定先行
|
||||
### 工作内容
|
||||
- 新建并维护两份全局文档:
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- 在文档中建立日志重构专属章节、字段定义、阶段验收标准。
|
||||
|
||||
### 验收标准
|
||||
- 文档结构可用于后续持续更新。
|
||||
- 明确字段与职责边界(尤其是 `sync_id`、`task`、`module`)。
|
||||
|
||||
## 阶段1:实现全局日志内核
|
||||
### 工作内容
|
||||
- 新增 `core/global_log_system.py`,实现 `jsonl` 写入、脱敏、同步分隔和统计写入。
|
||||
- 新增兼容层,避免一次性改动过大。
|
||||
|
||||
### 验收标准
|
||||
- 能独立写入 `logs/` / `logs2/`。
|
||||
- `start_sync -> 多条 log_api -> end_sync_with_stats` 链路完整。
|
||||
|
||||
## 阶段2:接入任务一(src)
|
||||
### 工作内容
|
||||
- 改造 `src/api_logger.py` 为新内核兼容封装。
|
||||
- 在任务一同步主流程增加 `sync_id` 生命周期。
|
||||
- 覆盖所有 API 调用记录点,统一单次调用单条记录。
|
||||
|
||||
### 验收标准
|
||||
- 任务一生产链路日志全部位于 `logs/`。
|
||||
- 无“双记录矛盾”。
|
||||
|
||||
## 阶段3:接入任务二(src2)
|
||||
### 工作内容
|
||||
- 改造 `src2/logger.py` 接入同一内核并固定 `logs2/`。
|
||||
- 在任务二同步主流程增加 `sync_id` 生命周期。
|
||||
- 修复复用模块导致的串目录问题(含 token 获取、wework 通知链路)。
|
||||
|
||||
### 验收标准
|
||||
- 任务二生产链路日志全部位于 `logs2/`。
|
||||
- 与任务一日志完全隔离。
|
||||
|
||||
## 阶段4:查看工具与收尾
|
||||
### 工作内容
|
||||
- 更新 `src/log_viewer.py` 支持读取 `jsonl`。
|
||||
- 更新 `logs/README.md`(新增 jsonl 规范与排障说明)。
|
||||
|
||||
### 验收标准
|
||||
- 可按日期与 `sync_id` 追踪一次完整同步。
|
||||
- 文档、代码、日志格式三者一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心风险与规避
|
||||
- 风险1:兼容层改造影响原调用。
|
||||
- 规避:先保留 `log_api_call(...)` 旧接口,再逐步替换调用。
|
||||
- 风险2:任务二通过复用模块写错目录。
|
||||
- 规避:统一使用可注入 logger/context,禁止隐式全局默认目录。
|
||||
- 风险3:单次请求出现多状态记录。
|
||||
- 规避:采用“单出口记录”,业务错误也只写一次最终状态。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收清单(最终)
|
||||
- [ ] 生产链路(两个 scheduler)所有 API 调用均有日志。
|
||||
- [ ] 每次同步都有 `start_sync` 与 `end_sync`,并可通过 `sync_id` 聚合。
|
||||
- [ ] 任务一只写 `logs/`,任务二只写 `logs2/`。
|
||||
- [ ] 无同一次 API 调用 success/failure 双写冲突。
|
||||
- [ ] token 已脱敏。
|
||||
- [ ] `jsonl` 可被查看工具读取。
|
||||
|
||||
2462
install.cmd
Normal file
2462
install.cmd
Normal file
File diff suppressed because one or more lines are too long
13
logs3/api_log_2026-03-11.jsonl
Normal file
13
logs3/api_log_2026-03-11.jsonl
Normal file
File diff suppressed because one or more lines are too long
121
logs3/api_log_2026-03-12.jsonl
Normal file
121
logs3/api_log_2026-03-12.jsonl
Normal file
@ -0,0 +1,121 @@
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 11:45:30", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:45:31", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:45:31", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:45:31", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:45:31", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:45:32", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n@BardLin(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n@星渊(13 条过期)\n13.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n14.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n15.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n16.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n17.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n18.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n19.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n20.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n21.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n22.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n23.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n24.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n25.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n共 25 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 11:45:32", "task": "task3", "sync_id": "task3_20260312_114530_bed15d44", "success": true, "stats": {"overdue_count": 25}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 11:48:56", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:56", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:56", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:57", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:57", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:57", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:57", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:48:58", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 11:48:58", "task": "task3", "sync_id": "task3_20260312_114856_2aa09865", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 11:51:37", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:38", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:38", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:38", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:39", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:39", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:39", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:51:39", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 11:51:39", "task": "task3", "sync_id": "task3_20260312_115137_176b8282", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 11:55:21", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:21", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:22", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:22", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:22", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:22", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:22", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:55:23", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 11:55:23", "task": "task3", "sync_id": "task3_20260312_115521_87933704", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 11:56:48", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:48", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:48", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:48", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:49", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:49", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:49", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:56:50", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n=======\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n=======\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n=======\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 11:56:50", "task": "task3", "sync_id": "task3_20260312_115648_2838ddca", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 11:57:28", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:29", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:29", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:29", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:29", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:29", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:29", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 11:57:30", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n---\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n---\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n---\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 11:57:30", "task": "task3", "sync_id": "task3_20260312_115728_e7f36b8f", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 12:03:15", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:16", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:16", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:16", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:16", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:16", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:16", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:03:17", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n========================\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n========================\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n========================\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 12:03:17", "task": "task3", "sync_id": "task3_20260312_120315_576256a6", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 12:05:37", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:38", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:38", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:38", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-04-04", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-11", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-10", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-10", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:38", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:38", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:38", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 12:05:39", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n========================\n\n\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n========================\n\n\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n========================\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 12:05:39", "task": "task3", "sync_id": "task3_20260312_120537_b6645a54", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 14:34:39", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:39", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:39", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:40", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-02", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-02", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-02", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:40", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:40", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:40", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:34:41", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n\n\n========================\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 14:34:41", "task": "task3", "sync_id": "task3_20260312_143439_6e21b546", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 14:35:41", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:41", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "BardLin", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004834011", "name": "【动画】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828079", "name": "飞遁动画", "due": "2026-01-30", "status": "status_9"}}, {"Story": {"id": "1158335167004828030", "name": "大啼魂-初绑", "due": "2026-03-02", "status": "status_8"}}, {"Story": {"id": "1158335167004827998", "name": "血色披风预研", "due": "2026-02-14", "status": "status_8"}}, {"Story": {"id": "1158335167004827996", "name": "青凝镜-回收Layout", "due": "2026-02-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827994", "name": "青凝镜-待机", "due": "2026-03-20", "status": "status_9"}}, {"Story": {"id": "1158335167004827991", "name": "青凝镜-检视表演Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827989", "name": "青凝镜-召唤Layout", "due": "2026-02-28", "status": "status_9"}}, {"Story": {"id": "1158335167004827987", "name": "青凝镜-初绑", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827946", "name": "玄铁飞天盾-初绑", "due": "2026-01-23", "status": "status_8"}}, {"Story": {"id": "1158335167004827929", "name": "噬金虫召唤动画Layout", "due": "2026-03-09", "status": "status_9"}}, {"Story": {"id": "1158335167004827926", "name": "噬金虫召-初绑", "due": "2026-01-26", "status": "status_8"}}, {"Story": {"id": "1158335167004827921", "name": "飞剑-解控", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827920", "name": "飞剑-检视表演", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004827919", "name": "飞剑-检视表演Layout", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004827914", "name": "剑影分光·闪回", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827912", "name": "剑影分光·上挑", "due": "2026-03-27", "status": "status_9"}}, {"Story": {"id": "1158335167004827910", "name": "空中右键下劈", "due": "2026-02-28", "status": "status_5"}}, {"Story": {"id": "1158335167004827909", "name": "右键垫步突进", "due": "2026-02-06", "status": "status_5"}}, {"Story": {"id": "1158335167004827852", "name": "动画制作 - 【法宝】青竹蜂云剑", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004816915", "name": "3C-飞遁 - 状态机重构", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004815661", "name": "3C-飞遁 - W输入HighSpeed状态", "due": "2026-01-09", "status": "status_8"}}, {"Story": {"id": "1158335167004787978", "name": "通用受击 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787977", "name": "海王兽 - 动作资源", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004787973", "name": "主角战斗 - 动作资源", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774665", "name": "地面待机", "due": "2025-12-30", "status": "status_8"}}, {"Story": {"id": "1158335167004774664", "name": "空中待机", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774629", "name": "雷属性受击 - 起始", "due": "2025-12-30", "status": "status_7"}}, {"Story": {"id": "1158335167004774628", "name": "雷属性受击 - 循环", "due": "2025-12-30", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:41", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:41", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "mithril", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004865833", "name": "客户端审核 - 噬金虫相关功能", "due": "2026-03-13", "status": "status_7"}}, {"Story": {"id": "1158335167004840448", "name": "技能触发条件接入及联调。", "due": "2026-01-31", "status": "status_8"}}, {"Story": {"id": "1158335167004838325", "name": "AI Codereview", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004838322", "name": "界面调整迭代", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838321", "name": "重构MainMenu", "due": "2026-02-28", "status": "status_8"}}, {"Story": {"id": "1158335167004838320", "name": "common ui调研,手机端测试", "due": "2026-03-20", "status": "status_7"}}, {"Story": {"id": "1158335167004836875", "name": "- 技能槽位系统重构-迭代", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004834469", "name": "Horde编译失败群通知", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004834468", "name": "Horde 自动触发逻辑", "due": "2026-05-30", "status": "status_9"}}, {"Story": {"id": "1158335167004829003", "name": "补全运行时日志", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004828993", "name": "接入xiandebug,可预览运行时子弹及相关数据", "due": "2026-03-27", "status": "status_7"}}, {"Story": {"id": "1158335167004828719", "name": "客户端审核 - 武器装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828716", "name": "客户端审核 - 法宝装备-UI逻辑", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004828709", "name": "客户端审核 - 武器装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828708", "name": "客户端审核 - 法宝装备界面逻辑", "due": "2026-03-13", "status": "status_12"}}, {"Story": {"id": "1158335167004828679", "name": "客户端审核 - 消耗品使用逻辑", "due": "2026-03-06", "status": "status_5"}}, {"Story": {"id": "1158335167004828673", "name": "客户端审核 - SkillSlotMgr新逻辑迭代", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828672", "name": "客户端审核 - 武器槽位映射逻辑重构", "due": "2026-01-30", "status": "status_8"}}, {"Story": {"id": "1158335167004827634", "name": "客户端审核 - 绑定Ability——左右键、派生技、武器战技和武器通用技能", "due": "2026-03-22", "status": "status_5"}}, {"Story": {"id": "1158335167004827625", "name": "客户端审核 - 可追踪角度、追踪速度等参数可靠性优化", "due": "2026-03-28", "status": "status_7"}}, {"Story": {"id": "1158335167004827584", "name": "客户端审核 - 根据血量裂纹和破碎效果", "due": "2026-04-03", "status": "status_7"}}, {"Story": {"id": "1158335167004827583", "name": "客户端审核 - 表现优化,接入动画资源", "due": "2026-04-02", "status": "status_7"}}, {"Story": {"id": "1158335167004806717", "name": "分支上增加显示玩家名称的功能", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800802", "name": "PSO收集", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004794635", "name": "客户端审核 - 格挡功能-子弹奉还", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794632", "name": "客户端审核 - 弹道系统现有配置优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794631", "name": "客户端审核 - 弹道系统优化- 激光类bullet", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004794629", "name": "客户端审核 - 弹道系统优化- AOE伤害&子弹爆炸", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004792154", "name": "玄铁飞天盾 - 格挡成功超出弧度生成多个盾后,若当前盾X秒内未再次受击则消散,直到仅剩下一个盾时,回到1倍大小", "due": "2026-04-02", "status": "status_7"}}, {"Story": {"id": "1158335167004792153", "name": "玄铁飞天盾 - 格挡成功后,X秒内未再次受击,则平滑往环绕半径轨道上移动", "due": "2026-04-02", "status": "status_7"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:42", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "BardLin", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002283694", "title": "受击动画中,root质心需要跟随人物模型运动(如击飞、击浮空时root需要向上跟随),否则浮空连击时,敌人位置会频繁上下跳动。", "deadline": "2025-12-05", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:42", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:42", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "mithril", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002280921", "title": "【UI】【单机】血条UI歪了,不在屏幕正中间了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280929", "title": "【联机】【弹道】左键第三段,第四段,飞剑子弹数量会丢失几根@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002280930", "title": "【联机】【弹道】子弹受击特效丢失了@mithril(hanjingchao)", "deadline": "2025-12-01", "status": "closed"}}, {"Bug": {"id": "1158335167002280931", "title": "【联机】【UI】对方的体力条会显示得特别大@mithril(hanjingchao)", "deadline": "2025-12-02", "status": "closed"}}, {"Bug": {"id": "1158335167002283432", "title": "更新之后,放左键,没目标时弹道坏了,不会正确追踪地形", "deadline": "2025-12-09", "status": "closed"}}, {"Bug": {"id": "1158335167002287668", "title": "格挡成功后,延迟时间内盾也要保证和角色相对移动,(当前是会静止在原地,不跟随角色移动)", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002287671", "title": "格挡成功后,延迟时间内再次格挡,则要刷新当前延迟时间(当前延迟时间内再次受击,延迟时间感觉并没有被刷新)", "deadline": "2025-12-16", "status": "closed"}}, {"Bug": {"id": "1158335167002287675", "title": "在新生成盾时,盾的内外表现不对,且旋转方向不对", "deadline": "2025-12-12", "status": "closed"}}, {"Bug": {"id": "1158335167002294091", "title": "【联机】【符箓】土牢符会把自己困住", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002294096", "title": "【联机】【法宝】飞天盾会挡住自己的子弹", "deadline": "2025-12-18", "status": "closed"}}, {"Bug": {"id": "1158335167002295354", "title": "飞天盾相关bug - 子弹会穿盾,对角色造成伤害", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295358", "title": "限制生成初始总数量为1,避免多次使用生成多个盾(再次使用时刷新盾血量和持续时间)", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002295360", "title": "飞天盾相关bug - 持续时间配置,避免盾一直不消失", "deadline": "2025-12-20", "status": "closed"}}, {"Bug": {"id": "1158335167002296688", "title": "【BUG】包里能不能默认 DisableAllScreenMessages", "deadline": "2025-12-22", "status": "closed"}}, {"Bug": {"id": "1158335167002297733", "title": "【高-BUG】左键普攻,第四段飞剑命中后会延迟一会才消失,希望和前三段一样快速消失", "deadline": "2025-12-25", "status": "closed"}}, {"Bug": {"id": "1158335167002299602", "title": "【高-BUG】土牢符的符纸,自己能锁定", "deadline": "2025-12-27", "status": "closed"}}, {"Bug": {"id": "1158335167002300378", "title": "【bug】土牢符陷阱现在不开神识探查也能看见", "deadline": "2025-12-30", "status": "in_progress"}}, {"Bug": {"id": "1158335167002300531", "title": "【致命-BUG】按G会Crash,复现较为频繁", "deadline": "2025-12-28", "status": "closed"}}, {"Bug": {"id": "1158335167002300717", "title": "【高-BUG】巨剑术诀子弹,命中海王兽场景海面底下的地面时没有正确触发overlap事件,停在表面并播特效", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002300837", "title": "【高-bug】MoveToTargetEX触发的时候,能直接从土牢符里穿出来,需要正常阻挡自身和目标穿出土牢符", "deadline": "2025-12-31", "status": "closed"}}, {"Bug": {"id": "1158335167002304061", "title": "【需Merge分支】【高-BUG】【联机】现在破定期间Q环绕飞剑,只环绕一次就消失了。", "deadline": "2026-01-07", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 14:35:42", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@BardLin>(12 条过期)\n1.【需求】主角战斗 - 动作资源 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004787973)\n2.【需求】空中待机 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774664)\n3.【需求】雷属性受击 - 起始 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774629)\n4.【需求】雷属性受击 - 循环 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004774628)\n5.【需求】飞遁动画 | 过期 41 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828079)\n6.【需求】右键垫步突进 | 过期 34 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827909)\n7.【需求】青凝镜-回收Layout | 过期 13 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827996)\n8.【需求】青凝镜-检视表演Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827991)\n9.【需求】青凝镜-召唤Layout | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827989)\n10.【需求】空中右键下劈 | 过期 12 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827910)\n11.【需求】噬金虫召唤动画Layout | 过期 3 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004827929)\n12.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n<@mi>(2 条过期)\n13.【缺陷】【bug】土牢符陷阱现在不开神识探查也能看见 | 过期 72 天 | [查看](https://www.tapd.cn/58335167/bugtrace/bugs/view?bug_id=1158335167002300378)\n14.【需求】客户端审核 - 消耗品使用逻辑 | 过期 6 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004828679)\n\n\n========================\n<@xingyuan>(13 条过期)\n15.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n16.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n17.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n18.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n19.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n20.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n21.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n22.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n23.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n24.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n25.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n26.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n27.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 27 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["BardLin", "mi", "xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 14:35:42", "task": "task3", "sync_id": "task3_20260312_143541_36dab0b7", "success": true, "stats": {"overdue_count": 27}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 15:22:00", "task": "task3", "sync_id": "task3_20260312_152200_38d7444c", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 15:22:02", "task": "task3", "sync_id": "task3_20260312_152200_38d7444c", "success": false, "stats": {}, "error_message": "'str' object has no attribute 'get'", "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 15:24:00", "task": "task3", "sync_id": "task3_20260312_152400_1b9bb4ae", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 15:24:01", "task": "task3", "sync_id": "task3_20260312_152400_1b9bb4ae", "success": false, "stats": {}, "error_message": "'str' object has no attribute 'get'", "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 15:35:00", "task": "task3", "sync_id": "task3_20260312_153500_8f56d85c", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 15:35:03", "task": "task3", "sync_id": "task3_20260312_153500_8f56d85c", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 15:35:03", "task": "task3", "sync_id": "task3_20260312_153500_8f56d85c", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 15:35:03", "task": "task3", "sync_id": "task3_20260312_153500_8f56d85c", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@xingyuan>(13 条过期)\n1.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n2.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n3.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n4.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n5.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n6.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n7.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n8.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n9.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n10.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n11.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n12.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n13.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 13 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 15:35:03", "task": "task3", "sync_id": "task3_20260312_153500_8f56d85c", "success": true, "stats": {"overdue_count": 13}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 16:07:51", "task": "task3", "sync_id": "task3_20260312_160751_c8a45221", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:07:53", "task": "task3", "sync_id": "task3_20260312_160751_c8a45221", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:07:54", "task": "task3", "sync_id": "task3_20260312_160751_c8a45221", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:07:54", "task": "task3", "sync_id": "task3_20260312_160751_c8a45221", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@xingyuan>(13 条过期)\n1.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n2.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n3.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n4.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n5.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n6.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n7.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n8.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n9.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n10.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n11.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n12.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n13.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 13 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 16:07:54", "task": "task3", "sync_id": "task3_20260312_160751_c8a45221", "success": true, "stats": {"overdue_count": 13}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 16:22:57", "task": "task3", "sync_id": "task3_20260312_162257_935477a8", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:23:00", "task": "task3", "sync_id": "task3_20260312_162257_935477a8", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:23:00", "task": "task3", "sync_id": "task3_20260312_162257_935477a8", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:23:01", "task": "task3", "sync_id": "task3_20260312_162257_935477a8", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@xingyuan>(13 条过期)\n1.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n2.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n3.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n4.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n5.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n6.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n7.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n8.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n9.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n10.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n11.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n12.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n13.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 13 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 16:23:01", "task": "task3", "sync_id": "task3_20260312_162257_935477a8", "success": true, "stats": {"overdue_count": 13}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 16:23:23", "task": "task3", "sync_id": "task3_20260312_162323_eea7a0d6", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:23:25", "task": "task3", "sync_id": "task3_20260312_162323_eea7a0d6", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:23:25", "task": "task3", "sync_id": "task3_20260312_162323_eea7a0d6", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:23:26", "task": "task3", "sync_id": "task3_20260312_162323_eea7a0d6", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@xingyuan>(13 条过期)\n1.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n2.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n3.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n4.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n5.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n6.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n7.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n8.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n9.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n10.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n11.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n12.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n13.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 13 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 16:23:26", "task": "task3", "sync_id": "task3_20260312_162323_eea7a0d6", "success": true, "stats": {"overdue_count": 13}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 16:27:11", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:27:14", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "星渊", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004866823", "name": "3C-基础移动-Bug-Dodge卡帧", "due": "2026-03-10", "status": "status_9"}}, {"Story": {"id": "1158335167004839323", "name": "材质Mask模式不受motion blur影响", "due": "2026-02-05", "status": "status_9"}}, {"Story": {"id": "1158335167004839302", "name": "AS脚本Bind扩展", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839301", "name": "尝试AS写Validator", "due": "2026-02-03", "status": "status_10"}}, {"Story": {"id": "1158335167004839299", "name": "简单Sample跑下流程", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839298", "name": "文档阅读", "due": "2026-02-03", "status": "status_8"}}, {"Story": {"id": "1158335167004839272", "name": "Kawaii,Magic,AnimVelet调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839270", "name": "Chaos布料算法调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839267", "name": "编译Android版", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839266", "name": "对GPU解算进行改近", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839263", "name": "育碧GPU布料结算的GDC Paper", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839259", "name": "理解GPU算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839251", "name": "增加Profiler", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839246", "name": "调试和理解NvCloth算法", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839242", "name": "编译NvCloth", "due": "2026-02-13", "status": "status_8"}}, {"Story": {"id": "1158335167004839241", "name": "NvCloth调研", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839239", "name": "布料解算基建", "due": "2026-02-13", "status": "status_5"}}, {"Story": {"id": "1158335167004839234", "name": "Data Validator框架", "due": "2026-02-03", "status": "status_5"}}, {"Story": {"id": "1158335167004834466", "name": "Horde相关-持续开发项", "due": "2026-05-30", "status": "status_5"}}, {"Story": {"id": "1158335167004834018", "name": "【引擎/TA】2026年1月 - 凡人各组月度产出梳理", "due": "2026-01-28", "status": "status_8"}}, {"Story": {"id": "1158335167004828309", "name": "技术基建-基础可过滤编辑器控件开发", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004802628", "name": "技术基建-XianDebugger-法术场Debugger", "due": null, "status": "status_9"}}, {"Story": {"id": "1158335167004802626", "name": "技术基建-XianDebugger", "due": null, "status": "status_5"}}, {"Story": {"id": "1158335167004800837", "name": "VAT贴图Foramt以及各种选项的意义", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800834", "name": "VAT贴图尺寸和种类优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800832", "name": "VAT内存优化", "due": null, "status": "status_7"}}, {"Story": {"id": "1158335167004800829", "name": "内存Profiling", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800828", "name": "内存优化", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800819", "name": "Async优化Lumen反射", "due": null, "status": "status_8"}}, {"Story": {"id": "1158335167004800816", "name": "优化Lumen反射,包括水的反射性能", "due": null, "status": "status_8"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:27:14", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "module": "tapd", "operation": "stories", "success": true, "request": {"workspace_id": "58335167", "owner": "v_linzelong", "fields": "id,name,due,status"}, "response": {"status": 1, "data": [{"Story": {"id": "1158335167004820451", "name": "自动化测试单", "due": null, "status": "status_12"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:27:14", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "星渊", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296797", "title": "优化性能,分辨率提升为1080P,静态帧率基本稳定60fps", "deadline": "2025-12-22", "status": "closed"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:27:14", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "module": "tapd", "operation": "bugs", "success": true, "request": {"workspace_id": "58335167", "current_owner": "v_linzelong", "fields": "id,title,deadline,status"}, "response": {"status": 1, "data": [{"Bug": {"id": "1158335167002296876", "title": "测试状态回写", "deadline": null, "status": "closed"}}, {"Bug": {"id": "1158335167002299210", "title": "测试状态回写", "deadline": null, "status": "rejected"}}, {"Bug": {"id": "1158335167002299743", "title": "多子表测试", "deadline": "2025-12-29", "status": "closed"}}, {"Bug": {"id": "1158335167002300470", "title": "测试描述", "deadline": "2025-12-30", "status": "rejected"}}], "info": "success"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "api_call", "timestamp": "2026-03-12 16:27:15", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "module": "wework", "operation": "webhook/send", "success": true, "request": {"msgtype": "markdown", "markdown": {"content": "⏰ TAPD 过期单提醒(2026-03-12)\n\n\n<@xingyuan>(13 条过期)\n1.【需求】AS脚本Bind扩展 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839302)\n2.【需求】尝试AS写Validator | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839301)\n3.【需求】Data Validator框架 | 过期 37 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839234)\n4.【需求】材质Mask模式不受motion blur影响 | 过期 35 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839323)\n5.【需求】Kawaii,Magic,AnimVelet调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839272)\n6.【需求】Chaos布料算法调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839270)\n7.【需求】编译Android版 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839267)\n8.【需求】对GPU解算进行改近 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839266)\n9.【需求】理解GPU算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839259)\n10.【需求】调试和理解NvCloth算法 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839246)\n11.【需求】NvCloth调研 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839241)\n12.【需求】布料解算基建 | 过期 27 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004839239)\n13.【需求】3C-基础移动-Bug-Dodge卡帧 | 过期 2 天 | [查看](https://www.tapd.cn/58335167/prong/stories/view/1158335167004866823)\n\n\n========================\n共 13 条过期单,请今日内更新状态 🙏"}, "mentioned_list": ["xingyuan"]}, "response": {"errcode": 0, "errmsg": "ok"}, "error_message": null, "duration_ms": null, "extra": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 16:27:15", "task": "task3", "sync_id": "task3_20260312_162711_4f28fc16", "success": true, "stats": {"overdue_count": 13}, "error_message": null, "extra": {}}
|
||||
{"event_type": "start_sync", "timestamp": "2026-03-12 16:41:15", "task": "task3", "sync_id": "task3_20260312_164115_81d6cac5", "trigger": "manual", "metadata": {}}
|
||||
{"event_type": "end_sync", "timestamp": "2026-03-12 16:41:16", "task": "task3", "sync_id": "task3_20260312_164115_81d6cac5", "success": false, "stats": {}, "error_message": "白名单错误: 智能表格中没有启用的技术组成员", "extra": {}}
|
||||
@ -1,170 +1,152 @@
|
||||
"""
|
||||
API调用日志记录模块
|
||||
负责记录所有API调用的请求和响应到JSON文件
|
||||
任务一日志兼容层
|
||||
|
||||
说明:
|
||||
1. 对外保持 APILogger / get_logger 接口不变
|
||||
2. 内部接入 core.global_log_system 的 jsonl 日志内核
|
||||
3. 支持同步边界(start_sync / end_sync_with_stats)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from core.global_log_system import GlobalLogSystem
|
||||
|
||||
|
||||
class APILogger:
|
||||
"""API调用日志记录器"""
|
||||
"""API 日志记录器(兼容旧接口)"""
|
||||
|
||||
def __init__(self, log_dir: Optional[str] = None):
|
||||
"""
|
||||
初始化日志记录器
|
||||
|
||||
Args:
|
||||
log_dir: 日志目录路径,如果为None则使用默认路径
|
||||
"""
|
||||
if log_dir is None:
|
||||
# 默认路径:项目根目录/logs/
|
||||
def __init__(self,
|
||||
log_dir: Optional[str] = None,
|
||||
task_name: Optional[str] = None):
|
||||
project_root = Path(__file__).parent.parent
|
||||
self.log_dir = project_root / "logs"
|
||||
else:
|
||||
self.log_dir = Path(log_dir)
|
||||
|
||||
# 确保日志目录存在
|
||||
self.log_dir.mkdir(exist_ok=True)
|
||||
if log_dir is None:
|
||||
resolved_log_dir = project_root / "logs"
|
||||
else:
|
||||
resolved_log_dir = Path(log_dir)
|
||||
|
||||
inferred_task_name = self._infer_task_name(task_name, resolved_log_dir)
|
||||
self.core = GlobalLogSystem(task_name=inferred_task_name, log_dir=resolved_log_dir)
|
||||
|
||||
@property
|
||||
def task_name(self) -> str:
|
||||
return self.core.task_name
|
||||
|
||||
@property
|
||||
def log_dir(self) -> Path:
|
||||
return self.core.log_dir
|
||||
|
||||
def _get_today_log_file(self) -> Path:
|
||||
"""
|
||||
获取今天的日志文件路径
|
||||
"""兼容旧方法:返回当天 jsonl 文件路径"""
|
||||
return self.core._get_today_log_file()
|
||||
|
||||
Returns:
|
||||
Path: 今天的日志文件路径(格式:api_log_YYYY-MM-DD.json)
|
||||
"""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
log_file = self.log_dir / f"api_log_{today}.json"
|
||||
def start_sync(self,
|
||||
trigger: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
sync_id: Optional[str] = None) -> str:
|
||||
return self.core.start_sync(trigger=trigger, metadata=metadata, sync_id=sync_id)
|
||||
|
||||
# 如果文件不存在,初始化
|
||||
if not log_file.exists():
|
||||
with open(log_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({"records": []}, f, ensure_ascii=False, indent=2)
|
||||
def end_sync_with_stats(self,
|
||||
stats: Dict[str, Any],
|
||||
success: bool,
|
||||
error_message: Optional[str] = None,
|
||||
sync_id: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None) -> None:
|
||||
self.core.end_sync_with_stats(
|
||||
stats=stats,
|
||||
success=success,
|
||||
error_message=error_message,
|
||||
sync_id=sync_id,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
return log_file
|
||||
def get_active_sync_id(self) -> Optional[str]:
|
||||
return self.core._active_sync_id
|
||||
|
||||
def log_api_call(self, api_type: str, operation: str,
|
||||
def log_api_call(self,
|
||||
api_type: str,
|
||||
operation: str,
|
||||
request_data: Dict[str, Any],
|
||||
response_data: Dict[str, Any],
|
||||
success: bool = True,
|
||||
error_message: Optional[str] = None):
|
||||
error_message: Optional[str] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
extra: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""
|
||||
记录API调用(使用追加式写入,避免读取整个文件)
|
||||
|
||||
Args:
|
||||
api_type: API类型(如 "smartsheet", "tapd", "wework")
|
||||
operation: 操作名称(如 "get_records", "create_bug")
|
||||
request_data: 请求数据(包含url、method、params等)
|
||||
response_data: 响应数据
|
||||
success: 是否成功
|
||||
error_message: 错误信息(如果失败)
|
||||
兼容旧接口:
|
||||
- api_type 允许历史值(如 task1/task2/test)
|
||||
- 内部统一映射为 module: smartsheet/tapd/wework
|
||||
"""
|
||||
try:
|
||||
# 获取今天的日志文件
|
||||
log_file = self._get_today_log_file()
|
||||
module = self._resolve_module(api_type, operation, request_data)
|
||||
|
||||
# 构造新记录
|
||||
record = {
|
||||
"api_type": api_type,
|
||||
"operation": operation,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"success": success,
|
||||
"request": request_data,
|
||||
"response": response_data
|
||||
}
|
||||
payload_extra = dict(extra or {})
|
||||
if api_type not in GlobalLogSystem.ALLOWED_MODULES:
|
||||
payload_extra["original_api_type"] = api_type
|
||||
|
||||
if error_message:
|
||||
record["error_message"] = error_message
|
||||
self.core.log_api(
|
||||
module=module,
|
||||
operation=operation,
|
||||
request_data=request_data,
|
||||
response_data=response_data,
|
||||
success=success,
|
||||
error_message=error_message,
|
||||
duration_ms=duration_ms,
|
||||
extra=payload_extra,
|
||||
)
|
||||
|
||||
# 使用追加式写入
|
||||
with open(log_file, 'r+', encoding='utf-8') as f:
|
||||
# 定位到文件末尾
|
||||
f.seek(0, 2)
|
||||
file_size = f.tell()
|
||||
def _resolve_module(self,
|
||||
api_type: str,
|
||||
operation: str,
|
||||
request_data: Optional[Dict[str, Any]]) -> str:
|
||||
normalized_type = (api_type or "").strip().lower()
|
||||
if normalized_type in GlobalLogSystem.ALLOWED_MODULES:
|
||||
return normalized_type
|
||||
|
||||
if file_size == 0:
|
||||
# 空文件,写入初始结构
|
||||
f.write('{"records": [\n')
|
||||
f.write(json.dumps(record, ensure_ascii=False, indent=2))
|
||||
f.write('\n]}')
|
||||
else:
|
||||
# 回退到最后的 ]}
|
||||
f.seek(file_size - 3)
|
||||
# 添加逗号和新记录
|
||||
f.write(',\n')
|
||||
f.write(json.dumps(record, ensure_ascii=False, indent=2))
|
||||
f.write('\n]}')
|
||||
request_url = ""
|
||||
if isinstance(request_data, dict):
|
||||
request_url = str(request_data.get("url", "")).lower()
|
||||
|
||||
except Exception as e:
|
||||
# 日志记录失败不应该影响主流程
|
||||
print(f"⚠ API日志记录失败: {str(e)}")
|
||||
operation_lower = (operation or "").lower()
|
||||
|
||||
if "tapd" in request_url:
|
||||
return "tapd"
|
||||
if "wedoc" in request_url or "smartsheet" in request_url:
|
||||
return "smartsheet"
|
||||
if "qyapi.weixin.qq.com/cgi-bin/message" in request_url:
|
||||
return "wework"
|
||||
if "qyapi.weixin.qq.com/cgi-bin/gettoken" in request_url:
|
||||
return "wework"
|
||||
|
||||
if any(key in operation_lower for key in ["tapd", "bug", "story", "attachment"]):
|
||||
return "tapd"
|
||||
if any(key in operation_lower for key in ["wework", "token", "message", "notify"]):
|
||||
return "wework"
|
||||
if any(key in operation_lower for key in ["sheet", "record", "field", "validation"]):
|
||||
return "smartsheet"
|
||||
|
||||
return "smartsheet"
|
||||
|
||||
def _infer_task_name(self, task_name: Optional[str], log_dir: Path) -> str:
|
||||
if task_name:
|
||||
return task_name
|
||||
|
||||
dir_name = log_dir.name.lower()
|
||||
if dir_name == "logs2":
|
||||
return "task2"
|
||||
|
||||
return "task1"
|
||||
|
||||
|
||||
# 全局单例
|
||||
_global_logger = None
|
||||
_global_logger: Optional[APILogger] = None
|
||||
|
||||
|
||||
def get_logger() -> APILogger:
|
||||
"""
|
||||
获取全局API日志记录器单例
|
||||
|
||||
Returns:
|
||||
APILogger: 日志记录器实例
|
||||
"""
|
||||
"""获取任务一默认日志记录器(单例)"""
|
||||
global _global_logger
|
||||
if _global_logger is None:
|
||||
_global_logger = APILogger()
|
||||
return _global_logger
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("=== API日志记录器测试 ===\n")
|
||||
|
||||
logger = APILogger()
|
||||
|
||||
# 测试记录一个成功的API调用
|
||||
print("测试1: 记录成功的API调用...")
|
||||
logger.log_api_call(
|
||||
api_type="smartsheet",
|
||||
operation="get_records",
|
||||
request_data={
|
||||
"url": "https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/get_records",
|
||||
"method": "POST",
|
||||
"params": {"docid": "test123", "sheet_id": "sheet456"}
|
||||
},
|
||||
response_data={
|
||||
"errcode": 0,
|
||||
"errmsg": "ok",
|
||||
"records": []
|
||||
},
|
||||
success=True
|
||||
)
|
||||
print("✓ 成功记录API调用")
|
||||
|
||||
# 测试记录一个失败的API调用
|
||||
print("\n测试2: 记录失败的API调用...")
|
||||
logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation="create_bug",
|
||||
request_data={
|
||||
"url": "https://api.tapd.cn/bugs",
|
||||
"method": "POST",
|
||||
"data": {"title": "测试bug"}
|
||||
},
|
||||
response_data={
|
||||
"status": 0,
|
||||
"info": "参数错误"
|
||||
},
|
||||
success=False,
|
||||
error_message="缺少必填参数workspace_id"
|
||||
)
|
||||
print("✓ 成功记录失败的API调用")
|
||||
|
||||
log_file = logger._get_today_log_file()
|
||||
print(f"\n日志文件: {log_file}")
|
||||
print(f"日志目录: {logger.log_dir}")
|
||||
|
||||
495
src/api_test.py
495
src/api_test.py
@ -22,7 +22,13 @@ from src.config import ConfigManager
|
||||
class WeWorkAPITester:
|
||||
"""企业微信API测试类"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, auto_load_token=True):
|
||||
"""
|
||||
初始化企业微信API测试类
|
||||
|
||||
Args:
|
||||
auto_load_token: 是否自动加载token,默认为True
|
||||
"""
|
||||
self.access_token = None
|
||||
self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs', 'api_test_log.json')
|
||||
self.base_url = "https://qyapi.weixin.qq.com/cgi-bin"
|
||||
@ -30,12 +36,92 @@ class WeWorkAPITester:
|
||||
# 确保日志文件存在
|
||||
self._init_log_file()
|
||||
|
||||
# 自动加载token
|
||||
if auto_load_token:
|
||||
self._auto_load_token()
|
||||
|
||||
def _init_log_file(self):
|
||||
"""初始化日志文件"""
|
||||
if not os.path.exists(self.log_file):
|
||||
with open(self.log_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({"records": []}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def _auto_load_token(self):
|
||||
"""
|
||||
自动加载access_token
|
||||
|
||||
优先从缓存读取,如果缓存不存在或已过期,则尝试从API获取新token
|
||||
"""
|
||||
print("\n=== 自动加载access_token ===")
|
||||
|
||||
# 先尝试从缓存读取
|
||||
if self._load_token_from_cache_silent():
|
||||
return
|
||||
|
||||
# 缓存无效,尝试从API获取
|
||||
print(" 尝试从API获取新token...")
|
||||
try:
|
||||
from src.token_manager import TokenManager
|
||||
token_manager = TokenManager()
|
||||
self.access_token = token_manager.get_token()
|
||||
print(f" ✓ 成功获取access_token")
|
||||
print(f" Token: {self.access_token[:20]}...")
|
||||
except ValueError as e:
|
||||
print(f" ⚠ 环境变量未配置: {e}")
|
||||
print(" 请手动选择菜单选项1获取token")
|
||||
except Exception as e:
|
||||
print(f" ⚠ 自动获取token失败: {e}")
|
||||
print(" 请手动选择菜单选项1获取token")
|
||||
|
||||
def _load_token_from_cache_silent(self):
|
||||
"""
|
||||
静默从缓存读取token(不打印标题)
|
||||
|
||||
Returns:
|
||||
bool: 是否成功读取有效token
|
||||
"""
|
||||
cache_file = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
'config',
|
||||
'token_cache.json'
|
||||
)
|
||||
|
||||
try:
|
||||
if not os.path.exists(cache_file):
|
||||
print(" 缓存文件不存在")
|
||||
return False
|
||||
|
||||
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
|
||||
access_token = cache_data.get('access_token')
|
||||
fetch_time = cache_data.get('fetch_time')
|
||||
|
||||
if not access_token:
|
||||
print(" 缓存文件中没有access_token")
|
||||
return False
|
||||
|
||||
# 检查token是否过期(7200秒 = 2小时,提前5分钟刷新)
|
||||
import time
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - fetch_time
|
||||
remaining_time = 7200 - elapsed_time
|
||||
|
||||
if remaining_time <= 300: # 剩余不足5分钟视为过期
|
||||
print(f" 缓存的token已过期或即将过期")
|
||||
return False
|
||||
|
||||
# token有效
|
||||
self.access_token = access_token
|
||||
print(f" ✓ 从缓存读取access_token成功")
|
||||
print(f" Token: {self.access_token[:20]}...")
|
||||
print(f" 剩余有效期: {int(remaining_time)}秒 ({int(remaining_time//60)}分钟)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" 读取缓存失败: {e}")
|
||||
return False
|
||||
|
||||
def _log_api_call(self, operation, request_data, response_data):
|
||||
"""记录API调用到JSON文件"""
|
||||
try:
|
||||
@ -335,6 +421,128 @@ class WeWorkAPITester:
|
||||
print(f"✗ 请求异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_sheet_list(self, docid):
|
||||
"""
|
||||
获取智能表格的子表列表
|
||||
|
||||
Args:
|
||||
docid: 文档ID
|
||||
|
||||
Returns:
|
||||
list: 子表列表,失败返回None
|
||||
"""
|
||||
print("\n=== 获取智能表格子表列表 ===")
|
||||
|
||||
if not self.access_token:
|
||||
print("✗ 请先获取access_token")
|
||||
return None
|
||||
|
||||
url = f"{self.base_url}/wedoc/smartsheet/get_sheet"
|
||||
params = {"access_token": self.access_token}
|
||||
data = {"docid": docid}
|
||||
|
||||
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("get_sheet_list", request_data, response_data)
|
||||
|
||||
if response_data.get("errcode") == 0:
|
||||
sheets = response_data.get("sheet_list", [])
|
||||
print(f"✓ 成功获取子表列表")
|
||||
print(f" 文档ID: {docid}")
|
||||
print(f" 子表数量: {len(sheets)}")
|
||||
|
||||
if sheets:
|
||||
print("\n子表列表:")
|
||||
print("-" * 60)
|
||||
for idx, sheet in enumerate(sheets, 1):
|
||||
sheet_id = sheet.get('sheet_id', '(无ID)')
|
||||
title = sheet.get('title', '(无标题)')
|
||||
print(f" {idx}. {title}")
|
||||
print(f" sheet_id: {sheet_id}")
|
||||
print("-" * 60)
|
||||
|
||||
return sheets
|
||||
else:
|
||||
print(f"✗ 获取失败")
|
||||
print(f" 错误码: {response_data.get('errcode')}")
|
||||
print(f" 错误信息: {response_data.get('errmsg')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
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):
|
||||
"""
|
||||
发送应用消息
|
||||
@ -481,6 +689,110 @@ class TAPDAPITester:
|
||||
except Exception as e:
|
||||
print(f"✗ 记录日志失败: {str(e)}")
|
||||
|
||||
def get_story_fields_info(self):
|
||||
"""
|
||||
获取TAPD需求的所有字段配置及候选值
|
||||
|
||||
Returns:
|
||||
dict: 字段配置信息,失败返回None
|
||||
"""
|
||||
print("\n=== 获取TAPD需求字段配置 ===")
|
||||
|
||||
# 初始化认证信息
|
||||
if not self._init_auth():
|
||||
return None
|
||||
|
||||
# 初始化workspace_id
|
||||
if not self._init_workspace_id():
|
||||
return None
|
||||
|
||||
url = f"{self.base_url}/stories/get_fields_info"
|
||||
params = {
|
||||
"workspace_id": self.workspace_id
|
||||
}
|
||||
|
||||
try:
|
||||
from requests.auth import HTTPBasicAuth
|
||||
auth = HTTPBasicAuth(self.api_user, self.api_password)
|
||||
|
||||
print(f"\n正在请求TAPD API...")
|
||||
print(f" URL: {url}")
|
||||
print(f" workspace_id: {self.workspace_id}")
|
||||
|
||||
response = requests.get(url, params=params, auth=auth, timeout=30)
|
||||
response_data = response.json()
|
||||
|
||||
# 记录API调用
|
||||
request_data = {
|
||||
"url": url,
|
||||
"method": "GET",
|
||||
"params": params,
|
||||
"auth_user": self.api_user
|
||||
}
|
||||
self._log_api_call("get_story_fields_info", request_data, response_data)
|
||||
|
||||
# 检查返回结果
|
||||
if response_data.get("status") == 1:
|
||||
data = response_data.get("data", {})
|
||||
print(f"\n✓ 成功获取需求字段配置")
|
||||
|
||||
# 显示字段统计
|
||||
if isinstance(data, dict):
|
||||
field_count = len(data)
|
||||
print(f" 共 {field_count} 个字段配置")
|
||||
|
||||
# 保存到单独的文件
|
||||
output_file = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
'logs',
|
||||
'tapd_story_fields.json'
|
||||
)
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ 字段配置已保存到: {output_file}")
|
||||
|
||||
# 显示部分字段信息
|
||||
print(f"\n需求字段列表预览:")
|
||||
print("-" * 80)
|
||||
for idx, (field_name, field_info) in enumerate(list(data.items())[:10], 1):
|
||||
field_label = field_info.get('label', '(无标签)')
|
||||
html_type = field_info.get('html_type', '(未知类型)')
|
||||
print(f" {idx}. {field_name} ({field_label}) - 类型: {html_type}")
|
||||
|
||||
# 如果有候选值,显示
|
||||
options = field_info.get('options', [])
|
||||
if options:
|
||||
if isinstance(options, dict):
|
||||
option_list = list(options.values())[:5]
|
||||
total_count = len(options)
|
||||
elif isinstance(options, list):
|
||||
option_list = options[:5]
|
||||
total_count = len(options)
|
||||
else:
|
||||
option_list = []
|
||||
total_count = 0
|
||||
|
||||
if option_list:
|
||||
print(f" 候选值: {', '.join(str(o) for o in option_list)}" +
|
||||
(f" ...等{total_count}个" if total_count > 5 else ""))
|
||||
|
||||
if field_count > 10:
|
||||
print(f" ... 还有 {field_count - 10} 个字段,详见输出文件")
|
||||
print("-" * 80)
|
||||
|
||||
return data
|
||||
else:
|
||||
print(f"\n✗ 获取失败")
|
||||
print(f" 状态码: {response_data.get('status')}")
|
||||
print(f" 错误信息: {response_data.get('info', '未知错误')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 请求异常: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def get_bug_custom_fields(self):
|
||||
"""
|
||||
获取TAPD缺陷的所有字段配置及候选值
|
||||
@ -586,6 +898,121 @@ class TAPDAPITester:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def get_story(self, story_id):
|
||||
"""
|
||||
根据需求ID获取需求详情
|
||||
|
||||
Args:
|
||||
story_id: 需求ID
|
||||
|
||||
Returns:
|
||||
dict: 需求信息,失败返回None
|
||||
"""
|
||||
print("\n=== 获取TAPD需求 ===")
|
||||
|
||||
# 初始化认证信息
|
||||
if not self._init_auth():
|
||||
return None
|
||||
|
||||
# 初始化workspace_id
|
||||
if not self._init_workspace_id():
|
||||
return None
|
||||
|
||||
# 验证story_id
|
||||
if not story_id:
|
||||
print("✗ 需求ID不能为空")
|
||||
return None
|
||||
|
||||
url = f"{self.base_url}/stories"
|
||||
params = {
|
||||
"workspace_id": self.workspace_id,
|
||||
"id": story_id
|
||||
}
|
||||
|
||||
try:
|
||||
from requests.auth import HTTPBasicAuth
|
||||
auth = HTTPBasicAuth(self.api_user, self.api_password)
|
||||
|
||||
print(f"\n正在请求TAPD API...")
|
||||
print(f" URL: {url}")
|
||||
print(f" workspace_id: {self.workspace_id}")
|
||||
print(f" story_id: {story_id}")
|
||||
|
||||
response = requests.get(url, params=params, auth=auth, timeout=30)
|
||||
response_data = response.json()
|
||||
|
||||
# 记录API调用
|
||||
request_data = {
|
||||
"url": url,
|
||||
"method": "GET",
|
||||
"params": params,
|
||||
"auth_user": self.api_user
|
||||
}
|
||||
self._log_api_call("get_story", request_data, response_data)
|
||||
|
||||
# 检查返回结果
|
||||
if response_data.get("status") == 1:
|
||||
data = response_data.get("data", [])
|
||||
|
||||
if not data:
|
||||
print(f"\n✗ 未找到需求ID为 {story_id} 的需求")
|
||||
return None
|
||||
|
||||
# TAPD返回的是列表,取第一个
|
||||
story_data = data[0] if isinstance(data, list) else data
|
||||
story_info = story_data.get("Story", {})
|
||||
|
||||
print(f"\n✓ 成功获取需求信息")
|
||||
print(f"\n需求详情:")
|
||||
print("=" * 80)
|
||||
|
||||
# 显示关键字段
|
||||
key_fields = [
|
||||
("id", "ID"),
|
||||
("name", "标题"),
|
||||
("status", "状态"),
|
||||
("priority", "优先级"),
|
||||
("owner", "处理人"),
|
||||
("creator", "创建人"),
|
||||
("created", "创建时间"),
|
||||
("modified", "最后修改时间"),
|
||||
("iteration_id", "迭代ID"),
|
||||
("description", "详细描述")
|
||||
]
|
||||
|
||||
for field_name, field_label in key_fields:
|
||||
value = story_info.get(field_name, '')
|
||||
if field_name == "description" and value:
|
||||
# 描述字段可能很长,只显示前100个字符
|
||||
if len(str(value)) > 100:
|
||||
value = str(value)[:100] + "..."
|
||||
print(f" {field_label}: {value}")
|
||||
|
||||
print("=" * 80)
|
||||
|
||||
# 保存完整数据到文件
|
||||
output_file = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
'logs',
|
||||
f'tapd_story_{story_id}.json'
|
||||
)
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(story_info, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n✓ 完整需求信息已保存到: {output_file}")
|
||||
|
||||
return story_info
|
||||
else:
|
||||
print(f"\n✗ 获取失败")
|
||||
print(f" 状态码: {response_data.get('status')}")
|
||||
print(f" 错误信息: {response_data.get('info', '未知错误')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 请求异常: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def upload_attachment(self, file_path, entry_type, entry_id, owner=None, overwrite=False):
|
||||
"""
|
||||
上传附件到TAPD
|
||||
@ -925,12 +1352,16 @@ def print_menu():
|
||||
print("3. 重命名文档")
|
||||
print("4. 删除文档")
|
||||
print("5. 发送应用消息")
|
||||
print("6. 查询智能表格子表列表")
|
||||
print("7. 修改文档成员权限")
|
||||
print("\n【TAPD API】")
|
||||
print("6. 获取缺陷字段配置")
|
||||
print("7. 获取附件列表")
|
||||
print("8. 上传附件")
|
||||
print("8. 获取缺陷字段配置")
|
||||
print("9. 获取需求字段配置")
|
||||
print("10. 获取需求")
|
||||
print("11. 获取附件列表")
|
||||
print("12. 上传附件")
|
||||
print("\n【其他】")
|
||||
print("9. 查看日志文件")
|
||||
print("13. 查看日志文件")
|
||||
print("0. 退出")
|
||||
print("="*50)
|
||||
|
||||
@ -942,7 +1373,7 @@ def main():
|
||||
|
||||
while True:
|
||||
print_menu()
|
||||
choice = input("\n请选择操作 (0-9): ").strip()
|
||||
choice = input("\n请选择操作 (0-13): ").strip()
|
||||
|
||||
if choice == "0":
|
||||
print("\n感谢使用,再见!")
|
||||
@ -1014,10 +1445,56 @@ def main():
|
||||
wework_tester.send_message(content)
|
||||
|
||||
elif choice == "6":
|
||||
# 查询智能表格子表列表
|
||||
docid = input("\n请输入文档ID: ").strip()
|
||||
if docid:
|
||||
wework_tester.get_sheet_list(docid)
|
||||
else:
|
||||
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 == "7":
|
||||
elif choice == "9":
|
||||
# 获取TAPD需求字段配置
|
||||
tapd_tester.get_story_fields_info()
|
||||
|
||||
elif choice == "10":
|
||||
# 获取TAPD需求
|
||||
story_id = input("\n请输入需求ID: ").strip()
|
||||
if not story_id:
|
||||
print("✗ 需求ID不能为空")
|
||||
continue
|
||||
tapd_tester.get_story(story_id)
|
||||
|
||||
elif choice == "11":
|
||||
# 获取TAPD附件列表
|
||||
print("\n=== 获取附件列表 ===")
|
||||
print("是否需要添加筛选条件?")
|
||||
@ -1055,7 +1532,7 @@ def main():
|
||||
limit=limit
|
||||
)
|
||||
|
||||
elif choice == "8":
|
||||
elif choice == "12":
|
||||
# 上传附件到TAPD
|
||||
print("\n=== 上传附件 ===")
|
||||
file_path = input("请输入文件路径: ").strip()
|
||||
@ -1099,7 +1576,7 @@ def main():
|
||||
overwrite=overwrite
|
||||
)
|
||||
|
||||
elif choice == "9":
|
||||
elif choice == "13":
|
||||
print("\n=== 查看日志文件 ===")
|
||||
try:
|
||||
with open(wework_tester.log_file, 'r', encoding='utf-8') as f:
|
||||
|
||||
205
src/main.py
205
src/main.py
@ -5,6 +5,7 @@ Debug阶段自动开单工具
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
@ -15,11 +16,16 @@ 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
|
||||
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():
|
||||
@ -295,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单
|
||||
@ -338,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', '未知')
|
||||
@ -353,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')
|
||||
@ -388,23 +429,73 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
|
||||
error_msg = f"字段映射失败: {e}"
|
||||
print(f" ✗ {error_msg}")
|
||||
|
||||
# 记录失败日志
|
||||
logger = get_logger()
|
||||
logger.log_api_call(
|
||||
api_type="task1",
|
||||
operation="create_bug_failure",
|
||||
request_data={"record_id": record_id, "title": title},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
result = BugCreationResult(
|
||||
record_id=record_id,
|
||||
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}"
|
||||
print(f" ✗ {error_msg}")
|
||||
|
||||
# 记录失败日志
|
||||
logger = get_logger()
|
||||
logger.log_api_call(
|
||||
api_type="task1",
|
||||
operation="create_bug_failure",
|
||||
request_data={"record_id": record_id, "title": title},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
result = BugCreationResult(
|
||||
record_id=record_id,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
result.title = title
|
||||
failed_results.append(result)
|
||||
|
||||
except Exception as e:
|
||||
@ -412,11 +503,23 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
|
||||
error_msg = f"未预期的错误: {type(e).__name__}: {e}"
|
||||
print(f" ✗ {error_msg}")
|
||||
|
||||
# 记录失败日志
|
||||
logger = get_logger()
|
||||
logger.log_api_call(
|
||||
api_type="task1",
|
||||
operation="create_bug_failure",
|
||||
request_data={"record_id": record_id, "title": title},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
result = BugCreationResult(
|
||||
record_id=record_id,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
result.title = title
|
||||
failed_results.append(result)
|
||||
|
||||
# 显示汇总结果
|
||||
@ -439,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
|
||||
}
|
||||
|
||||
|
||||
@ -574,6 +678,35 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
|
||||
'error_message': None
|
||||
}
|
||||
|
||||
logger = get_logger()
|
||||
started_sync_here = False
|
||||
if not logger.get_active_sync_id():
|
||||
logger.start_sync(
|
||||
trigger="task1_run_once_manual",
|
||||
metadata={
|
||||
"entry": "src/main.py:run_once",
|
||||
"test_mode": test_mode,
|
||||
},
|
||||
)
|
||||
started_sync_here = True
|
||||
|
||||
def _finalize_sync_and_return() -> Dict:
|
||||
if started_sync_here:
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"scanned_count": result.get('scanned_count', 0),
|
||||
"valid_count": result.get('valid_count', 0),
|
||||
"invalid_count": result.get('invalid_count', 0),
|
||||
"bugs_created": result.get('bugs_created', 0),
|
||||
"bugs_failed": result.get('bugs_failed', 0),
|
||||
"writeback_success": result.get('writeback_success', 0),
|
||||
},
|
||||
success=result.get('success', False),
|
||||
error_message=result.get('error_message'),
|
||||
extra={"source": "run_once"},
|
||||
)
|
||||
return result
|
||||
|
||||
try:
|
||||
# 获取配置信息
|
||||
all_config = config_manager.get_all_config()
|
||||
@ -588,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']
|
||||
@ -623,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'],
|
||||
@ -632,17 +770,45 @@ 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 = []
|
||||
for invalid_record in validation_result['invalid_records']:
|
||||
record_data = invalid_record['record_data']
|
||||
missing_fields = invalid_record['missing_fields']
|
||||
error_msg = f"校验失败,缺失字段: {', '.join(missing_fields)}"
|
||||
|
||||
# 记录校验失败日志
|
||||
logger = get_logger()
|
||||
logger.log_api_call(
|
||||
api_type="task1",
|
||||
operation="validation_failure",
|
||||
request_data={
|
||||
"record_id": record_data.get('record_id', '未知'),
|
||||
"title": record_data.get('标题', '(无标题)')
|
||||
},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
validation_failed_result = BugCreationResult(
|
||||
record_id=record_data.get('record_id', '未知'),
|
||||
success=False,
|
||||
error_message=f"校验失败,缺失字段: {', '.join(missing_fields)}"
|
||||
error_message=error_msg
|
||||
)
|
||||
validation_failed_results.append(validation_failed_result)
|
||||
|
||||
@ -695,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("执行完成 - 分表统计")
|
||||
@ -740,7 +927,7 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
|
||||
print("=" * 60)
|
||||
|
||||
result['success'] = True
|
||||
return result
|
||||
return _finalize_sync_and_return()
|
||||
|
||||
except FileNotFoundError as e:
|
||||
result['error_message'] = f"文件未找到: {e}"
|
||||
@ -748,7 +935,7 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
|
||||
print("\n解决方案:")
|
||||
print(" 1. 检查配置文件是否存在")
|
||||
print(" 2. 使用 --config 参数指定正确的配置文件路径")
|
||||
return result
|
||||
return _finalize_sync_and_return()
|
||||
|
||||
except ValueError as e:
|
||||
result['error_message'] = f"参数错误: {e}"
|
||||
@ -757,7 +944,7 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
|
||||
print(" 1. 检查配置文件中的配置项是否完整")
|
||||
print(" 2. 确保所有必填项都已填写")
|
||||
print(" 3. 检查access_token是否正确")
|
||||
return result
|
||||
return _finalize_sync_and_return()
|
||||
|
||||
except Exception as e:
|
||||
result['error_message'] = f"未预期的错误: {type(e).__name__}: {e}"
|
||||
@ -767,7 +954,7 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
|
||||
import traceback
|
||||
print("\n详细错误信息:")
|
||||
traceback.print_exc()
|
||||
return result
|
||||
return _finalize_sync_and_return()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -202,7 +202,7 @@ class FieldMapper:
|
||||
tapd_data = {}
|
||||
|
||||
# 1. 标题(必填)
|
||||
title = record_data.get('标题', '').strip()
|
||||
title = self._convert_multiline_text(record_data.get('标题', ''))
|
||||
if not title:
|
||||
raise ValueError("标题不能为空")
|
||||
tapd_data['title'] = title
|
||||
|
||||
@ -26,6 +26,7 @@ from src.config import ConfigManager
|
||||
from src.token_manager import TokenManager
|
||||
from src.main import run_once
|
||||
from src.sync_status import BugStatusSyncer
|
||||
from src.api_logger import get_logger
|
||||
|
||||
|
||||
class AutoTAPDScheduler:
|
||||
@ -90,6 +91,14 @@ class AutoTAPDScheduler:
|
||||
print(f"开始执行开单任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 80)
|
||||
|
||||
logger = get_logger()
|
||||
logger.start_sync(
|
||||
trigger="task1_scheduler_job",
|
||||
metadata={"entry": "src/scheduler.py:job"}
|
||||
)
|
||||
|
||||
result = None
|
||||
|
||||
try:
|
||||
# 获取access_token
|
||||
access_token = self.token_manager.get_token()
|
||||
@ -127,6 +136,20 @@ class AutoTAPDScheduler:
|
||||
print(f" 错误信息: {result.get('error_message', '未知错误')}")
|
||||
print("-" * 80)
|
||||
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"scanned_count": result.get('scanned_count', 0),
|
||||
"valid_count": result.get('valid_count', 0),
|
||||
"invalid_count": result.get('invalid_count', 0),
|
||||
"bugs_created": result.get('bugs_created', 0),
|
||||
"bugs_failed": result.get('bugs_failed', 0),
|
||||
"writeback_success": result.get('writeback_success', 0),
|
||||
},
|
||||
success=result.get('success', False),
|
||||
error_message=result.get('error_message'),
|
||||
extra={"source": "scheduler.job"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 捕获所有异常,确保不影响后续执行
|
||||
self.stats['total_runs'] += 1
|
||||
@ -144,6 +167,20 @@ class AutoTAPDScheduler:
|
||||
print("\n详细错误信息:")
|
||||
traceback.print_exc()
|
||||
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"scanned_count": 0,
|
||||
"valid_count": 0,
|
||||
"invalid_count": 0,
|
||||
"bugs_created": 0,
|
||||
"bugs_failed": 0,
|
||||
"writeback_success": 0,
|
||||
},
|
||||
success=False,
|
||||
error_message=f"{type(e).__name__}: {e}",
|
||||
extra={"source": "scheduler.job"}
|
||||
)
|
||||
|
||||
# 显示下次执行时间
|
||||
next_run = schedule.idle_seconds()
|
||||
if next_run is not None:
|
||||
@ -157,16 +194,27 @@ class AutoTAPDScheduler:
|
||||
print(f"开始执行bug状态同步 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 80)
|
||||
|
||||
logger = get_logger()
|
||||
logger.start_sync(
|
||||
trigger="task1_scheduler_sync_job",
|
||||
metadata={"entry": "src/scheduler.py:sync_job"}
|
||||
)
|
||||
|
||||
result = None
|
||||
|
||||
try:
|
||||
# 获取access_token
|
||||
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')
|
||||
)
|
||||
|
||||
# 执行一次状态同步
|
||||
@ -185,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
|
||||
@ -193,6 +242,18 @@ class AutoTAPDScheduler:
|
||||
print(f" 错误信息: {result.get('error_message', '未知错误')}")
|
||||
print("-" * 80)
|
||||
|
||||
logger.end_sync_with_stats(
|
||||
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'),
|
||||
extra={"source": "scheduler.sync_job"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 捕获所有异常,确保不影响后续执行
|
||||
self.stats['total_sync_runs'] += 1
|
||||
@ -210,6 +271,18 @@ class AutoTAPDScheduler:
|
||||
print("\n详细错误信息:")
|
||||
traceback.print_exc()
|
||||
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"checked_count": 0,
|
||||
"updated_count": 0,
|
||||
"failed_count": 0,
|
||||
"rate_limited": False,
|
||||
},
|
||||
success=False,
|
||||
error_message=f"{type(e).__name__}: {e}",
|
||||
extra={"source": "scheduler.sync_job"}
|
||||
)
|
||||
|
||||
# 显示下次执行时间
|
||||
next_run = schedule.idle_seconds()
|
||||
if next_run is not None:
|
||||
|
||||
@ -60,6 +60,7 @@ class SmartSheetAPI:
|
||||
print("=" * 80)
|
||||
print(f"请求方法: {method}")
|
||||
print(f"请求URL: {self.BASE_URL}/{endpoint}")
|
||||
print(f"完整URL(含参数): {url}") # 显示完整URL
|
||||
if data:
|
||||
import json
|
||||
print(f"请求数据:")
|
||||
@ -74,6 +75,21 @@ class SmartSheetAPI:
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# 先判断业务错误,避免同一次请求出现 success/failure 双记录
|
||||
if result.get('errcode', 0) != 0:
|
||||
error_msg = result.get('errmsg', '未知错误')
|
||||
self.logger.log_api_call(
|
||||
api_type="smartsheet",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data=result,
|
||||
success=False,
|
||||
error_message=f"errcode={result['errcode']}, errmsg={error_msg}"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"API调用失败: errcode={result['errcode']}, errmsg={error_msg}"
|
||||
)
|
||||
|
||||
# 记录API调用日志(成功)
|
||||
self.logger.log_api_call(
|
||||
api_type="smartsheet",
|
||||
@ -91,11 +107,6 @@ class SmartSheetAPI:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
print("=" * 80)
|
||||
|
||||
# 检查企业微信API返回的错误码
|
||||
if result.get('errcode', 0) != 0:
|
||||
error_msg = result.get('errmsg', '未知错误')
|
||||
raise RuntimeError(f"API调用失败: errcode={result['errcode']}, errmsg={error_msg}")
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
|
||||
@ -4,6 +4,7 @@ bug状态同步模块
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
@ -12,14 +13,20 @@ 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):
|
||||
TAPD_BATCH_LIMIT = 200
|
||||
TAPD_RATE_LIMIT_RETRY_WAIT_SECONDS = 120
|
||||
TAPD_RATE_LIMIT_MAX_RETRIES = 1
|
||||
|
||||
def __init__(self, access_token: str, docid: str, workspace_id: str, test_mode: bool = False,
|
||||
agentid: str = None, receivers: str = None):
|
||||
"""
|
||||
初始化状态同步器
|
||||
|
||||
@ -33,10 +40,40 @@ 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 = []
|
||||
|
||||
@staticmethod
|
||||
def _chunk_records(records: List[Dict], chunk_size: int):
|
||||
"""按固定大小拆分记录列表"""
|
||||
for index in range(0, len(records), chunk_size):
|
||||
yield records[index:index + chunk_size]
|
||||
|
||||
def _get_bugs_by_ids_with_rate_limit_retry(self, bug_ids: List[str]) -> Dict[str, Dict]:
|
||||
"""
|
||||
批量查询 TAPD Bug 状态;触发限速时等待 120 秒重试当前批次一次。
|
||||
"""
|
||||
max_retries = self.TAPD_RATE_LIMIT_MAX_RETRIES
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return self.tapd_api.get_bugs_by_ids(
|
||||
bug_ids,
|
||||
limit=self.TAPD_BATCH_LIMIT,
|
||||
fields="id,status"
|
||||
)
|
||||
except RateLimitError:
|
||||
if attempt >= max_retries:
|
||||
raise
|
||||
|
||||
wait_seconds = self.TAPD_RATE_LIMIT_RETRY_WAIT_SECONDS
|
||||
print(f" ⚠ TAPD 批量查询触发限速,等待 {wait_seconds} 秒后重试当前批次...")
|
||||
time.sleep(wait_seconds)
|
||||
print(f" → 继续重试当前批次状态查询")
|
||||
|
||||
def _sync_sheet_status(self, sheet_id: str, sheet_title: str) -> Dict:
|
||||
"""
|
||||
@ -47,7 +84,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,26 +110,97 @@ 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状态并对比
|
||||
# 3. 批量查询TAPD bug状态并对比
|
||||
updates = [] # 需要更新的记录列表
|
||||
failed_count = 0
|
||||
checked_count = 0
|
||||
rate_limited = False
|
||||
|
||||
prepared_records = []
|
||||
for record in records:
|
||||
record_id = record.get('record_id', '未知')
|
||||
|
||||
# 提取TAPD单号
|
||||
tapd_bug_field = self.smartsheet_api.get_field_value_by_title(record, 'TAPD单号')
|
||||
bug_id = str(tapd_bug_field) if tapd_bug_field else ''
|
||||
bug_id = str(tapd_bug_field).strip() if tapd_bug_field else ''
|
||||
|
||||
# 获取当前智能表格中的bug状态
|
||||
current_status = self.smartsheet_api.get_field_value_by_title(record, 'bug状态')
|
||||
|
||||
if not bug_id:
|
||||
failed_count += 1
|
||||
error_msg = "TAPD单号为空,无法查询状态"
|
||||
print(f" ✗ 记录 {record_id} 查询失败: {error_msg}")
|
||||
self.failure_records.append({
|
||||
"sheet_title": sheet_title,
|
||||
"record_id": record_id,
|
||||
"title": "TAPD单号为空",
|
||||
"error_message": error_msg
|
||||
})
|
||||
continue
|
||||
|
||||
prepared_records.append({
|
||||
"record_id": record_id,
|
||||
"bug_id": bug_id,
|
||||
"current_status": current_status
|
||||
})
|
||||
|
||||
for batch_records in self._chunk_records(prepared_records, self.TAPD_BATCH_LIMIT):
|
||||
batch_bug_ids = list(dict.fromkeys(item["bug_id"] for item in batch_records))
|
||||
checked_count_before_batch = checked_count
|
||||
|
||||
try:
|
||||
# 查询TAPD的最新状态
|
||||
bug_info = self.tapd_api.get_bug(bug_id)
|
||||
bug_map = self._get_bugs_by_ids_with_rate_limit_retry(batch_bug_ids)
|
||||
checked_count += len(batch_records)
|
||||
except RateLimitError as e:
|
||||
failed_count += len(batch_records)
|
||||
rate_limited = True
|
||||
error_msg = f"TAPD限速,暂停本轮状态同步: {e}"
|
||||
print(f" ✗ 批次查询失败: {error_msg}")
|
||||
self.failure_records.append({
|
||||
"sheet_title": sheet_title,
|
||||
"record_id": "批量查询",
|
||||
"title": f"TAPD批量查询 {len(batch_bug_ids)} 个Bug",
|
||||
"error_message": error_msg,
|
||||
"remaining_count": len(records) - checked_count_before_batch
|
||||
})
|
||||
break
|
||||
except Exception as e:
|
||||
checked_count += len(batch_records)
|
||||
failed_count += len(batch_records)
|
||||
error_msg = f"{type(e).__name__}: {e}"
|
||||
print(f" ✗ 批次查询失败: {error_msg}")
|
||||
for item in batch_records:
|
||||
self.failure_records.append({
|
||||
"sheet_title": sheet_title,
|
||||
"record_id": item["record_id"],
|
||||
"title": f"TAPD单号 {item['bug_id']}",
|
||||
"error_message": error_msg
|
||||
})
|
||||
continue
|
||||
|
||||
for item in batch_records:
|
||||
record_id = item["record_id"]
|
||||
bug_id = item["bug_id"]
|
||||
current_status = item["current_status"]
|
||||
bug_info = bug_map.get(bug_id)
|
||||
|
||||
if not bug_info:
|
||||
failed_count += 1
|
||||
error_msg = "TAPD批量查询结果中未返回该Bug"
|
||||
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
|
||||
|
||||
latest_status_en = bug_info.get('status', '')
|
||||
latest_status_cn = BugStatusMapper.to_chinese(latest_status_en)
|
||||
|
||||
@ -106,10 +214,6 @@ class BugStatusSyncer:
|
||||
}
|
||||
updates.append(update_record)
|
||||
|
||||
except Exception:
|
||||
# 单个bug查询失败不影响其他bug的同步
|
||||
continue
|
||||
|
||||
# 4. 批量更新智能表格
|
||||
if len(updates) > 0:
|
||||
self.smartsheet_api.update_records(sheet_id, updates)
|
||||
@ -117,7 +221,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 +237,7 @@ class BugStatusSyncer:
|
||||
'success': bool,
|
||||
'checked_count': int, # 检查的bug数量
|
||||
'updated_count': int, # 更新的bug数量
|
||||
'failed_count': int, # 查询失败的bug数量
|
||||
'error_message': str
|
||||
}
|
||||
"""
|
||||
@ -135,10 +245,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 +272,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 +294,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 +311,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 +320,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 +345,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 +366,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:
|
||||
|
||||
126
src/tapd_api.py
126
src/tapd_api.py
@ -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",
|
||||
@ -299,6 +363,64 @@ class TAPDApi:
|
||||
|
||||
return bug_info
|
||||
|
||||
def get_bugs_by_ids(self, bug_ids: List[str], limit: int = 200, fields: str = "id,status") -> Dict[str, Dict]:
|
||||
"""
|
||||
按多个 bug ID 批量获取 bug 信息
|
||||
|
||||
Args:
|
||||
bug_ids: bug ID 列表
|
||||
limit: 单页返回数量,TAPD 最大支持 200
|
||||
fields: 需要返回的字段,默认只取状态同步需要的字段
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict]: 以 bug ID 为 key 的 Bug 信息映射
|
||||
|
||||
Raises:
|
||||
RuntimeError: 获取失败时抛出
|
||||
"""
|
||||
clean_bug_ids = []
|
||||
seen_bug_ids = set()
|
||||
for bug_id in bug_ids:
|
||||
clean_bug_id = str(bug_id or "").strip()
|
||||
if not clean_bug_id or clean_bug_id in seen_bug_ids:
|
||||
continue
|
||||
clean_bug_ids.append(clean_bug_id)
|
||||
seen_bug_ids.add(clean_bug_id)
|
||||
|
||||
if not clean_bug_ids:
|
||||
return {}
|
||||
|
||||
bounded_limit = max(1, min(int(limit), 200))
|
||||
params = {
|
||||
'workspace_id': self.workspace_id,
|
||||
'id': ",".join(clean_bug_ids),
|
||||
'limit': bounded_limit,
|
||||
'page': 1,
|
||||
'fields': fields
|
||||
}
|
||||
|
||||
result = self._make_request("bugs", method="GET", params=params)
|
||||
|
||||
# TAPD API返回格式: {"status": 1, "data": [{"Bug": {...}}]}
|
||||
data = result.get('data', [])
|
||||
if not isinstance(data, list):
|
||||
raise RuntimeError(f"API返回数据格式异常: {data}")
|
||||
|
||||
bug_map = {}
|
||||
for item in data:
|
||||
if not isinstance(item, dict) or 'Bug' not in item:
|
||||
raise RuntimeError(f"API返回数据格式异常: {item}")
|
||||
|
||||
bug_info = item['Bug']
|
||||
if not bug_info:
|
||||
continue
|
||||
|
||||
returned_bug_id = str(bug_info.get('id', '')).strip()
|
||||
if returned_bug_id:
|
||||
bug_map[returned_bug_id] = bug_info
|
||||
|
||||
return bug_map
|
||||
|
||||
def get_bug_url(self, bug_id: str) -> str:
|
||||
"""
|
||||
生成bug的访问URL
|
||||
|
||||
@ -26,12 +26,13 @@ class TokenManager:
|
||||
# 提前刷新时间(秒),在token过期前5分钟就刷新
|
||||
REFRESH_BEFORE_EXPIRE = 300
|
||||
|
||||
def __init__(self, cache_file_path: Optional[str] = None):
|
||||
def __init__(self, cache_file_path: Optional[str] = None, logger=None):
|
||||
"""
|
||||
初始化Token管理器
|
||||
|
||||
Args:
|
||||
cache_file_path: token缓存文件路径,如果为None则使用默认路径
|
||||
logger: 可选日志实例,不传则使用默认任务一日志器
|
||||
"""
|
||||
# 确定缓存文件路径
|
||||
if cache_file_path is None:
|
||||
@ -46,7 +47,7 @@ class TokenManager:
|
||||
self.corpsecret = os.environ.get('CORPSECRET')
|
||||
|
||||
# 初始化日志记录器
|
||||
self.logger = get_logger()
|
||||
self.logger = logger if logger is not None else get_logger()
|
||||
|
||||
def _validate_env_config(self):
|
||||
"""
|
||||
|
||||
@ -14,7 +14,7 @@ from src.api_logger import get_logger
|
||||
class WeWorkNotifier:
|
||||
"""企业微信消息推送类"""
|
||||
|
||||
def __init__(self, access_token: str, agentid: str, receivers: str):
|
||||
def __init__(self, access_token: str, agentid: str, receivers: str, logger=None):
|
||||
"""
|
||||
初始化企业微信消息推送器
|
||||
|
||||
@ -22,12 +22,13 @@ class WeWorkNotifier:
|
||||
access_token: 企业微信access_token
|
||||
agentid: 应用ID
|
||||
receivers: 接收人列表(用户ID,多个用|分隔,@all表示全部成员)
|
||||
logger: 可选日志实例,不传则使用默认任务一日志器
|
||||
"""
|
||||
self.access_token = access_token
|
||||
self.agentid = agentid
|
||||
self.receivers = receivers
|
||||
self.base_url = "https://qyapi.weixin.qq.com/cgi-bin"
|
||||
self.logger = get_logger()
|
||||
self.logger = logger if logger is not None else get_logger()
|
||||
|
||||
def send_validation_failure_notification(self, invalid_records: List[Dict]) -> bool:
|
||||
"""
|
||||
@ -50,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_title、record_id、title、error_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:
|
||||
"""
|
||||
构造校验失败消息内容(支持多子表分组)
|
||||
@ -108,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:
|
||||
"""
|
||||
发送文本消息
|
||||
|
||||
3
src2/__init__.py
Normal file
3
src2/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
任务二:TAPD状态实时同步至腾讯智能表格
|
||||
"""
|
||||
196
src2/config.py
Normal file
196
src2/config.py
Normal file
@ -0,0 +1,196 @@
|
||||
"""
|
||||
任务二配置管理模块
|
||||
复用任务一的ConfigManager,读取任务二专用配置文件
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径,以便导入 src 模块
|
||||
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):
|
||||
"""任务二配置管理器,继承自任务一的ConfigManager"""
|
||||
|
||||
def __init__(self, config_path=None):
|
||||
"""
|
||||
初始化任务二配置管理器
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径,如果为None则使用任务二默认路径
|
||||
"""
|
||||
if config_path is None:
|
||||
# 默认路径:项目根目录/config/config_task2.ini
|
||||
config_path = project_root / "config" / "config_task2.ini"
|
||||
|
||||
super().__init__(config_path)
|
||||
|
||||
def get_tapd_config(self):
|
||||
"""
|
||||
获取TAPD配置(任务二版本,不需要reporter)
|
||||
|
||||
Returns:
|
||||
dict: 包含workspace_id的字典
|
||||
"""
|
||||
if not self.config.has_section('TAPD'):
|
||||
raise ValueError("配置文件缺少[TAPD]节")
|
||||
|
||||
if not self.config.has_option('TAPD', 'workspace_id'):
|
||||
raise ValueError("配置文件[TAPD]节缺少workspace_id配置项")
|
||||
|
||||
workspace_id = self.config.get('TAPD', 'workspace_id').strip()
|
||||
if not workspace_id:
|
||||
raise ValueError("workspace_id配置项不能为空")
|
||||
|
||||
return {
|
||||
'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)
|
||||
|
||||
Returns:
|
||||
dict: 包含sync_interval的字典
|
||||
"""
|
||||
default_sync_interval = 15
|
||||
|
||||
if not self.config.has_section('Schedule'):
|
||||
return {'sync_interval': default_sync_interval}
|
||||
|
||||
if not self.config.has_option('Schedule', 'sync_interval'):
|
||||
return {'sync_interval': default_sync_interval}
|
||||
|
||||
sync_interval_str = self.config.get('Schedule', 'sync_interval').strip()
|
||||
try:
|
||||
sync_interval = int(sync_interval_str)
|
||||
except ValueError:
|
||||
raise ValueError(f"sync_interval必须为整数,当前值: {sync_interval_str}")
|
||||
|
||||
if sync_interval <= 0:
|
||||
raise ValueError(f"sync_interval必须为正整数,当前值: {sync_interval}")
|
||||
|
||||
return {'sync_interval': sync_interval}
|
||||
|
||||
def get_wework_config(self):
|
||||
"""
|
||||
获取企业微信推送配置
|
||||
|
||||
Returns:
|
||||
dict: 包含agentid和receivers的字典,如果配置不存在则返回None
|
||||
"""
|
||||
if not self.config.has_section('wework'):
|
||||
return None
|
||||
|
||||
agentid = self.config.get('wework', 'agentid', fallback='').strip()
|
||||
receivers = self.config.get('wework', 'receivers', fallback='').strip()
|
||||
|
||||
# 如果配置不完整,返回None
|
||||
if not agentid or not receivers:
|
||||
return None
|
||||
|
||||
return {
|
||||
'agentid': agentid,
|
||||
'receivers': receivers
|
||||
}
|
||||
|
||||
def get_all_config(self):
|
||||
"""获取所有配置"""
|
||||
return {
|
||||
'tapd': self.get_tapd_config(),
|
||||
'smartsheet': self.get_smartsheet_config(),
|
||||
'schedule': self.get_schedule_config(),
|
||||
'wework': self.get_wework_config()
|
||||
}
|
||||
|
||||
def print_config(self):
|
||||
"""打印当前配置信息"""
|
||||
print("\n=== 任务二配置信息 ===")
|
||||
try:
|
||||
tapd_config = self.get_tapd_config()
|
||||
print(f"[TAPD]")
|
||||
print(f" workspace_id: {tapd_config['workspace_id']}")
|
||||
except ValueError as e:
|
||||
print(f"[TAPD] 配置错误: {e}")
|
||||
|
||||
try:
|
||||
smartsheet_config = self.get_smartsheet_config()
|
||||
print(f"[SmartSheet]")
|
||||
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}")
|
||||
|
||||
try:
|
||||
schedule_config = self.get_schedule_config()
|
||||
print(f"[Schedule]")
|
||||
print(f" sync_interval: {schedule_config['sync_interval']} 分钟")
|
||||
except ValueError as e:
|
||||
print(f"[Schedule] 配置错误: {e}")
|
||||
|
||||
wework_config = self.get_wework_config()
|
||||
if wework_config:
|
||||
print(f"[wework]")
|
||||
print(f" agentid: {wework_config['agentid']}")
|
||||
print(f" receivers: {wework_config['receivers']}")
|
||||
else:
|
||||
print(f"[wework] 未配置(推送功能将被禁用)")
|
||||
|
||||
print("======================\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
config = Task2ConfigManager()
|
||||
config.print_config()
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
115
src2/link_parser.py
Normal file
115
src2/link_parser.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""
|
||||
TAPD链接解析模块
|
||||
负责从TAPD链接中提取需求单号
|
||||
|
||||
支持的链接格式:
|
||||
1. 列表页弹窗链接: https://www.tapd.cn/tapd_fe/{workspace_id}/story/list?...dialog_preview_id=story_{单号}
|
||||
2. 详情页链接: https://www.tapd.cn/{workspace_id}/prong/stories/view/{单号}
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
# 链接类型常量
|
||||
LINK_TYPE_DIALOG = "dialog" # 列表页弹窗链接
|
||||
LINK_TYPE_VIEW = "view" # 详情页链接
|
||||
LINK_TYPE_UNKNOWN = "unknown"
|
||||
|
||||
|
||||
def parse_tapd_link(url: str) -> Tuple[bool, str, str]:
|
||||
"""
|
||||
解析TAPD链接,提取需求单号
|
||||
|
||||
Args:
|
||||
url: TAPD链接字符串
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str, str]: (是否成功, 单号或错误信息, 链接类型)
|
||||
- 成功时: (True, "1234567890", "dialog" 或 "view")
|
||||
- 失败时: (False, "错误信息", "unknown")
|
||||
"""
|
||||
if not url:
|
||||
return (False, "链接为空", LINK_TYPE_UNKNOWN)
|
||||
|
||||
# 确保是字符串类型
|
||||
if not isinstance(url, str):
|
||||
url = str(url)
|
||||
|
||||
url = url.strip()
|
||||
|
||||
if not url:
|
||||
return (False, "链接为空", LINK_TYPE_UNKNOWN)
|
||||
|
||||
# 格式一:列表页弹窗链接
|
||||
# 匹配 dialog_preview_id=story_(\d+)
|
||||
pattern_dialog = r'dialog_preview_id=story_(\d+)'
|
||||
match_dialog = re.search(pattern_dialog, url)
|
||||
if match_dialog:
|
||||
story_id = match_dialog.group(1)
|
||||
return (True, story_id, LINK_TYPE_DIALOG)
|
||||
|
||||
# 格式二:详情页链接
|
||||
# 匹配 /stories/view/(\d+)
|
||||
pattern_view = r'/stories/view/(\d+)'
|
||||
match_view = re.search(pattern_view, url)
|
||||
if match_view:
|
||||
story_id = match_view.group(1)
|
||||
return (True, story_id, LINK_TYPE_VIEW)
|
||||
|
||||
# 未匹配到任何格式
|
||||
return (False, f"无法识别的链接格式: {url[:100]}", LINK_TYPE_UNKNOWN)
|
||||
|
||||
|
||||
def extract_story_id(url: str) -> Optional[str]:
|
||||
"""
|
||||
从TAPD链接中提取需求单号(简化接口)
|
||||
|
||||
Args:
|
||||
url: TAPD链接字符串
|
||||
|
||||
Returns:
|
||||
Optional[str]: 成功返回单号,失败返回None
|
||||
"""
|
||||
success, result, _ = parse_tapd_link(url)
|
||||
return result if success else None
|
||||
|
||||
|
||||
def is_valid_tapd_link(url: str) -> bool:
|
||||
"""
|
||||
检查是否为有效的TAPD链接
|
||||
|
||||
Args:
|
||||
url: TAPD链接字符串
|
||||
|
||||
Returns:
|
||||
bool: 是否为有效链接
|
||||
"""
|
||||
success, _, _ = parse_tapd_link(url)
|
||||
return success
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== TAPD链接解析器测试 ===\n")
|
||||
|
||||
# 测试用例
|
||||
test_cases = [
|
||||
# 格式一:列表页弹窗链接
|
||||
"https://www.tapd.cn/tapd_fe/58335167/story/list?dialog_preview_id=story_1158335167001044388",
|
||||
# 格式二:详情页链接
|
||||
"https://www.tapd.cn/58335167/prong/stories/view/1158335167001044388",
|
||||
# 无效链接
|
||||
"https://www.tapd.cn/58335167/bugtrace/bugs/view/123456",
|
||||
"https://www.google.com",
|
||||
"",
|
||||
None,
|
||||
]
|
||||
|
||||
for i, url in enumerate(test_cases, 1):
|
||||
print(f"测试 {i}: {url}")
|
||||
success, result, link_type = parse_tapd_link(url)
|
||||
if success:
|
||||
print(f" ✓ 解析成功: 单号={result}, 类型={link_type}")
|
||||
else:
|
||||
print(f" ✗ 解析失败: {result}")
|
||||
print()
|
||||
56
src2/logger.py
Normal file
56
src2/logger.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""
|
||||
任务二日志模块
|
||||
创建独立的 APILogger 实例,日志写入 logs2/ 目录
|
||||
|
||||
设计说明:
|
||||
- 不修改 src/api_logger.py 的 get_logger() 全局单例
|
||||
- 任务二使用独立的 logger 实例,避免与任务一冲突
|
||||
- 两个任务可以同时运行,日志互不干扰
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.api_logger import APILogger
|
||||
|
||||
# 任务二日志目录
|
||||
TASK2_LOG_DIR = project_root / "logs2"
|
||||
|
||||
# 任务二专用的 logger 实例(模块级单例)
|
||||
_task2_logger = None
|
||||
|
||||
|
||||
def get_task2_logger() -> APILogger:
|
||||
"""
|
||||
获取任务二专用的日志记录器
|
||||
|
||||
Returns:
|
||||
APILogger: 任务二专用的日志记录器实例
|
||||
"""
|
||||
global _task2_logger
|
||||
if _task2_logger is None:
|
||||
_task2_logger = APILogger(log_dir=str(TASK2_LOG_DIR))
|
||||
return _task2_logger
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 任务二日志模块测试 ===\n")
|
||||
|
||||
logger = get_task2_logger()
|
||||
|
||||
# 测试记录一条日志
|
||||
logger.log_api_call(
|
||||
api_type="test",
|
||||
operation="task2/test_log",
|
||||
request_data={"test": "任务二日志测试"},
|
||||
response_data={"status": "ok"},
|
||||
success=True
|
||||
)
|
||||
|
||||
print(f"日志目录: {TASK2_LOG_DIR}")
|
||||
print(f"日志文件: {logger._get_today_log_file()}")
|
||||
print("\n测试完成,请检查 logs2/ 目录")
|
||||
203
src2/main.py
Normal file
203
src2/main.py
Normal file
@ -0,0 +1,203 @@
|
||||
"""
|
||||
任务二主程序入口
|
||||
TAPD状态实时同步至腾讯智能表格
|
||||
|
||||
功能:
|
||||
1. 命令行参数解析
|
||||
2. 单次执行模式
|
||||
3. 执行结果统计
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.token_manager import TokenManager
|
||||
from src2.config import Task2ConfigManager
|
||||
from src2.sync_service import run_once
|
||||
from src2.logger import get_task2_logger
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='TAPD状态同步工具 - 将TAPD需求状态同步到腾讯智能表格',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例用法:
|
||||
# 执行一次同步
|
||||
python src2/main.py
|
||||
|
||||
# 指定配置文件
|
||||
python src2/main.py --config /path/to/config.ini
|
||||
|
||||
# 测试模式(显示详细信息)
|
||||
python src2/main.py --test
|
||||
|
||||
# 手动传入access_token
|
||||
python src2/main.py --token YOUR_ACCESS_TOKEN
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
default=None,
|
||||
help='配置文件路径(默认: config/config_task2.ini)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-t', '--token',
|
||||
default=None,
|
||||
help='手动传入access_token(默认自动获取)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--test',
|
||||
action='store_true',
|
||||
help='启用测试模式,显示详细的API调用信息'
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def print_result_summary(result: dict):
|
||||
"""打印执行结果摘要"""
|
||||
print("\n" + "=" * 60)
|
||||
print("执行结果摘要")
|
||||
print("=" * 60)
|
||||
|
||||
if result["success"]:
|
||||
print("状态: ✓ 成功")
|
||||
else:
|
||||
print("状态: ✗ 失败")
|
||||
if result.get("error_message"):
|
||||
print(f"错误: {result['error_message']}")
|
||||
|
||||
print(f"\n子表统计:")
|
||||
print(f" 处理子表: {result['sheets_processed']} 个")
|
||||
print(f" 跳过子表: {result['sheets_skipped']} 个")
|
||||
|
||||
print(f"\n记录统计:")
|
||||
print(f" 包含链接: {result['records_with_link']} 条")
|
||||
print(f" 同步成功: {result['records_synced']} 条")
|
||||
print(f" 需要更新: {result['records_updated']} 条")
|
||||
print(f" 同步失败: {result['records_failed']} 条")
|
||||
|
||||
# 显示各子表详情
|
||||
if result.get("sheet_results"):
|
||||
print(f"\n子表详情:")
|
||||
for sheet in result["sheet_results"]:
|
||||
status = "跳过" if sheet["skipped"] else "完成"
|
||||
print(f" - {sheet['sheet_title']}: {status}")
|
||||
if sheet["skipped"]:
|
||||
print(f" 原因: {sheet['skip_reason']}")
|
||||
else:
|
||||
print(f" 链接: {sheet['records_with_link']} | "
|
||||
f"更新: {sheet['records_updated']} | "
|
||||
f"失败: {sheet['records_failed']}")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TAPD状态同步工具 (任务二)")
|
||||
print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 60)
|
||||
|
||||
logger = None
|
||||
|
||||
try:
|
||||
# 解析命令行参数
|
||||
args = parse_arguments()
|
||||
|
||||
logger = get_task2_logger()
|
||||
logger.start_sync(
|
||||
trigger="task2_main_manual",
|
||||
metadata={
|
||||
"entry": "src2/main.py:main",
|
||||
"test_mode": args.test,
|
||||
"manual_token": bool(args.token),
|
||||
},
|
||||
)
|
||||
|
||||
# 初始化配置管理器
|
||||
print("\n正在加载配置...")
|
||||
config_manager = Task2ConfigManager(config_path=args.config)
|
||||
config_manager.print_config()
|
||||
|
||||
# 获取access_token
|
||||
access_token = args.token
|
||||
if access_token is None:
|
||||
print("正在获取access_token...")
|
||||
token_manager = TokenManager(logger=logger)
|
||||
access_token = token_manager.get_token()
|
||||
print(f" ✓ access_token获取成功")
|
||||
|
||||
# 执行同步
|
||||
print("\n开始执行同步...")
|
||||
result = run_once(
|
||||
config_manager=config_manager,
|
||||
access_token=access_token,
|
||||
test_mode=args.test
|
||||
)
|
||||
|
||||
# 打印结果摘要
|
||||
print_result_summary(result)
|
||||
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"docs_total": result.get("docs_total", 0),
|
||||
"docs_success": result.get("docs_success", 0),
|
||||
"docs_failed": result.get("docs_failed", 0),
|
||||
"sheets_processed": result.get("sheets_processed", 0),
|
||||
"sheets_skipped": result.get("sheets_skipped", 0),
|
||||
"total_records": result.get("total_records", 0),
|
||||
"records_with_link": result.get("records_with_link", 0),
|
||||
"records_synced": result.get("records_synced", 0),
|
||||
"records_updated": result.get("records_updated", 0),
|
||||
"records_failed": result.get("records_failed", 0),
|
||||
},
|
||||
success=result.get("success", False),
|
||||
error_message=result.get("error_message"),
|
||||
extra={"source": "task2_main_manual"},
|
||||
)
|
||||
|
||||
# 返回状态码
|
||||
return 0 if result["success"] else 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n用户中断执行")
|
||||
if logger and logger.get_active_sync_id():
|
||||
logger.end_sync_with_stats(
|
||||
stats={},
|
||||
success=False,
|
||||
error_message="KeyboardInterrupt",
|
||||
extra={"source": "task2_main_manual"},
|
||||
)
|
||||
return 130
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if logger and logger.get_active_sync_id():
|
||||
logger.end_sync_with_stats(
|
||||
stats={},
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
extra={
|
||||
"source": "task2_main_manual",
|
||||
"exception_type": type(e).__name__,
|
||||
},
|
||||
)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
137
src2/notifier.py
Normal file
137
src2/notifier.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""
|
||||
任务二企业微信消息推送模块
|
||||
用于发送同步失败通知
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.wework_notifier import WeWorkNotifier
|
||||
from src2.logger import get_task2_logger
|
||||
|
||||
|
||||
def send_sync_failure_notification(access_token: str, agentid: str,
|
||||
receivers: str, failed_records: List[Dict]) -> bool:
|
||||
"""
|
||||
发送同步失败通知
|
||||
|
||||
Args:
|
||||
access_token: 企业微信access_token
|
||||
agentid: 应用ID
|
||||
receivers: 接收人列表
|
||||
failed_records: 失败记录列表,每条记录包含:
|
||||
- sheet_title: 子表标题
|
||||
- record_id: 记录ID
|
||||
- tapd_link: TAPD链接
|
||||
- error_message: 失败原因
|
||||
|
||||
Returns:
|
||||
bool: 是否发送成功
|
||||
"""
|
||||
if not failed_records:
|
||||
return True
|
||||
|
||||
# 构造消息内容
|
||||
content = _build_sync_failure_message(failed_records)
|
||||
|
||||
# 使用任务一的推送器发送消息
|
||||
notifier = WeWorkNotifier(access_token, agentid, receivers, logger=get_task2_logger())
|
||||
return notifier._send_text_message(content)
|
||||
|
||||
|
||||
def _build_sync_failure_message(failed_records: List[Dict]) -> str:
|
||||
"""
|
||||
构造同步失败消息内容(支持多表格、多子表分组)
|
||||
|
||||
Args:
|
||||
failed_records: 失败记录列表,每条记录可包含:
|
||||
- doc_index: 表格序号(可选)
|
||||
- docid_short: 表格ID简写(可选)
|
||||
- sheet_title: 子表标题
|
||||
- record_id: 记录ID
|
||||
- tapd_link: TAPD链接
|
||||
- error_message: 失败原因
|
||||
|
||||
Returns:
|
||||
str: 格式化的消息内容
|
||||
"""
|
||||
# 按表格和子表分组
|
||||
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_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)
|
||||
doc_count = len(records_by_doc)
|
||||
|
||||
lines = [
|
||||
"【autoTAPD 同步失败通知】",
|
||||
f"时间: {timestamp}",
|
||||
f"失败数量: {total_count} 条",
|
||||
]
|
||||
|
||||
# 如果有多个表格,显示表格数量
|
||||
if doc_count > 1:
|
||||
lines.append(f"涉及表格: {doc_count} 个")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"以下记录同步失败,请检查:",
|
||||
"=" * 40
|
||||
])
|
||||
|
||||
# 按表格和子表分组显示失败记录
|
||||
global_idx = 1
|
||||
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 sheet_title, sheet_records in sheets.items():
|
||||
lines.append(f"\n【子表:{sheet_title}】")
|
||||
lines.append("")
|
||||
|
||||
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("系统将在下次同步时自动重试失败记录。")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 任务二推送模块 ===")
|
||||
print("此模块提供同步失败通知功能")
|
||||
print("请通过 sync_service 或 main.py 调用")
|
||||
419
src2/scheduler.py
Normal file
419
src2/scheduler.py
Normal file
@ -0,0 +1,419 @@
|
||||
"""
|
||||
任务二调度器模块
|
||||
负责定时执行TAPD状态同步任务
|
||||
|
||||
功能:
|
||||
1. 按配置频率定时执行
|
||||
2. 优雅退出(Ctrl+C)
|
||||
3. 运行统计
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# 导入schedule库
|
||||
try:
|
||||
import schedule
|
||||
except ImportError:
|
||||
print("错误: 缺少schedule库")
|
||||
print("请运行: pip install schedule")
|
||||
sys.exit(1)
|
||||
|
||||
from src.token_manager import TokenManager
|
||||
from src2.config import Task2ConfigManager
|
||||
from src2.sync_service import run_once
|
||||
from src2.logger import get_task2_logger
|
||||
|
||||
|
||||
class Task2Scheduler:
|
||||
"""任务二调度器"""
|
||||
|
||||
def __init__(self, config_path=None, verbose=False):
|
||||
"""
|
||||
初始化调度器
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径
|
||||
verbose: 是否显示详细信息
|
||||
"""
|
||||
self.config_path = config_path
|
||||
self.verbose = verbose
|
||||
self.running = True
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
'start_time': None,
|
||||
'total_runs': 0,
|
||||
'success_runs': 0,
|
||||
'failed_runs': 0,
|
||||
'total_records_synced': 0,
|
||||
'total_records_updated': 0,
|
||||
'last_run_time': None
|
||||
}
|
||||
|
||||
# 初始化配置管理器
|
||||
self._init_config()
|
||||
|
||||
# 初始化TokenManager
|
||||
self._init_token_manager()
|
||||
|
||||
# 获取调度配置
|
||||
self.sync_interval = self.config['schedule']['sync_interval']
|
||||
|
||||
def _init_config(self):
|
||||
"""初始化配置"""
|
||||
try:
|
||||
print("正在加载配置文件...")
|
||||
self.config_manager = Task2ConfigManager(config_path=self.config_path)
|
||||
self.config = self.config_manager.get_all_config()
|
||||
print("✓ 配置文件加载成功")
|
||||
except Exception as e:
|
||||
print(f"✗ 配置文件加载失败: {e}")
|
||||
raise
|
||||
|
||||
def _init_token_manager(self):
|
||||
"""初始化TokenManager"""
|
||||
try:
|
||||
print("正在初始化TokenManager...")
|
||||
self.token_manager = TokenManager(logger=get_task2_logger())
|
||||
print("✓ TokenManager初始化成功")
|
||||
except Exception as e:
|
||||
print(f"✗ TokenManager初始化失败: {e}")
|
||||
raise
|
||||
|
||||
def job(self):
|
||||
"""执行一次同步任务"""
|
||||
logger = get_task2_logger()
|
||||
logger.start_sync(
|
||||
trigger="task2_scheduler_job",
|
||||
metadata={
|
||||
"entry": "src2/scheduler.py:Task2Scheduler.job",
|
||||
"verbose": self.verbose,
|
||||
}
|
||||
)
|
||||
|
||||
result = {
|
||||
"success": False,
|
||||
"docs_total": 0,
|
||||
"docs_success": 0,
|
||||
"docs_failed": 0,
|
||||
"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,
|
||||
}
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"开始执行同步任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 80)
|
||||
|
||||
try:
|
||||
# 获取access_token
|
||||
access_token = self.token_manager.get_token()
|
||||
|
||||
# 执行一次同步流程
|
||||
result = run_once(
|
||||
config_manager=self.config_manager,
|
||||
access_token=access_token,
|
||||
test_mode=self.verbose
|
||||
)
|
||||
|
||||
# 更新统计信息
|
||||
self.stats['total_runs'] += 1
|
||||
self.stats['last_run_time'] = datetime.now()
|
||||
|
||||
if result['success']:
|
||||
self.stats['success_runs'] += 1
|
||||
self.stats['total_records_synced'] += result['records_synced']
|
||||
self.stats['total_records_updated'] += result['records_updated']
|
||||
|
||||
print("\n" + "-" * 80)
|
||||
print("本次执行统计:")
|
||||
print(f" 处理子表: {result['sheets_processed']} 个")
|
||||
print(f" 跳过子表: {result['sheets_skipped']} 个")
|
||||
print(f" 包含链接: {result['records_with_link']} 条")
|
||||
print(f" 同步成功: {result['records_synced']} 条")
|
||||
print(f" 需要更新: {result['records_updated']} 条")
|
||||
print(f" 同步失败: {result['records_failed']} 条")
|
||||
print("-" * 80)
|
||||
else:
|
||||
self.stats['failed_runs'] += 1
|
||||
print("\n" + "-" * 80)
|
||||
print("本次执行失败:")
|
||||
print(f" 错误信息: {result.get('error_message', '未知错误')}")
|
||||
print("-" * 80)
|
||||
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"docs_total": result.get("docs_total", 0),
|
||||
"docs_success": result.get("docs_success", 0),
|
||||
"docs_failed": result.get("docs_failed", 0),
|
||||
"sheets_processed": result.get("sheets_processed", 0),
|
||||
"sheets_skipped": result.get("sheets_skipped", 0),
|
||||
"total_records": result.get("total_records", 0),
|
||||
"records_with_link": result.get("records_with_link", 0),
|
||||
"records_synced": result.get("records_synced", 0),
|
||||
"records_updated": result.get("records_updated", 0),
|
||||
"records_failed": result.get("records_failed", 0),
|
||||
},
|
||||
success=result.get("success", False),
|
||||
error_message=result.get("error_message"),
|
||||
extra={"source": "task2_scheduler_job"},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.stats['total_runs'] += 1
|
||||
self.stats['failed_runs'] += 1
|
||||
self.stats['last_run_time'] = datetime.now()
|
||||
|
||||
print("\n" + "-" * 80)
|
||||
print("本次执行异常:")
|
||||
print(f" 错误类型: {type(e).__name__}")
|
||||
print(f" 错误信息: {e}")
|
||||
print("-" * 80)
|
||||
|
||||
if self.verbose:
|
||||
import traceback
|
||||
print("\n详细错误信息:")
|
||||
traceback.print_exc()
|
||||
|
||||
if logger.get_active_sync_id():
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"docs_total": result.get("docs_total", 0),
|
||||
"docs_success": result.get("docs_success", 0),
|
||||
"docs_failed": result.get("docs_failed", 0),
|
||||
"sheets_processed": result.get("sheets_processed", 0),
|
||||
"sheets_skipped": result.get("sheets_skipped", 0),
|
||||
"total_records": result.get("total_records", 0),
|
||||
"records_with_link": result.get("records_with_link", 0),
|
||||
"records_synced": result.get("records_synced", 0),
|
||||
"records_updated": result.get("records_updated", 0),
|
||||
"records_failed": result.get("records_failed", 0),
|
||||
},
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
extra={
|
||||
"source": "task2_scheduler_job",
|
||||
"exception_type": type(e).__name__,
|
||||
},
|
||||
)
|
||||
|
||||
# 显示下次执行时间
|
||||
self._show_next_run_time()
|
||||
|
||||
def _show_next_run_time(self):
|
||||
"""显示下次执行时间"""
|
||||
next_run = schedule.idle_seconds()
|
||||
if next_run is not None:
|
||||
next_run_time = datetime.now().timestamp() + next_run
|
||||
next_run_str = datetime.fromtimestamp(next_run_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"\n下次执行时间: {next_run_str} (约 {int(next_run / 60)} 分钟后)")
|
||||
|
||||
def _startup_check(self):
|
||||
"""启动检查"""
|
||||
print("\n" + "=" * 80)
|
||||
print("启动检查")
|
||||
print("=" * 80)
|
||||
|
||||
checks_passed = True
|
||||
|
||||
# 1. 检查配置文件
|
||||
print("\n[1/3] 检查配置文件...")
|
||||
try:
|
||||
print(f" ✓ 配置文件已加载: {self.config_path or '默认路径'}")
|
||||
print(f" ✓ TAPD workspace_id: {self.config['tapd']['workspace_id']}")
|
||||
print(f" ✓ SmartSheet docid: {self.config['smartsheet']['docid'][:20]}...")
|
||||
print(f" ✓ 同步间隔: {self.sync_interval} 分钟")
|
||||
except Exception as e:
|
||||
print(f" ✗ 配置检查失败: {e}")
|
||||
checks_passed = False
|
||||
|
||||
# 2. 检查环境变量
|
||||
print("\n[2/3] 检查环境变量...")
|
||||
import os
|
||||
required_env_vars = {
|
||||
'CORPID': '企业微信CorpID',
|
||||
'CORPSECRET': '企业微信CorpSecret',
|
||||
'TAPD_API_USER': 'TAPD API用户名',
|
||||
'TAPD_API_PASSWORD': 'TAPD API密码'
|
||||
}
|
||||
|
||||
for env_var, description in required_env_vars.items():
|
||||
if os.environ.get(env_var):
|
||||
print(f" ✓ {description} ({env_var}): 已设置")
|
||||
else:
|
||||
print(f" ✗ {description} ({env_var}): 未设置")
|
||||
checks_passed = False
|
||||
|
||||
# 3. 测试access_token获取
|
||||
print("\n[3/3] 测试access_token获取...")
|
||||
try:
|
||||
access_token = self.token_manager.get_token()
|
||||
print(f" ✓ access_token获取成功: {access_token[:10]}...(已隐藏)")
|
||||
except Exception as e:
|
||||
print(f" ✗ access_token获取失败: {e}")
|
||||
checks_passed = False
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
if checks_passed:
|
||||
print("启动检查通过 ✓")
|
||||
else:
|
||||
print("启动检查失败 ✗")
|
||||
print("\n请检查上述错误并修复后重试")
|
||||
return False
|
||||
print("=" * 80)
|
||||
|
||||
return True
|
||||
|
||||
def start(self):
|
||||
"""启动调度器"""
|
||||
print("\n" + "=" * 80)
|
||||
print("TAPD状态同步调度器 (任务二)")
|
||||
print("版本: 1.0.0 (第四阶段)")
|
||||
print("=" * 80)
|
||||
|
||||
# 执行启动检查
|
||||
if not self._startup_check():
|
||||
print("\n调度器启动失败")
|
||||
sys.exit(1)
|
||||
|
||||
# 记录启动时间
|
||||
self.stats['start_time'] = datetime.now()
|
||||
|
||||
# 注册信号处理
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
# 配置定时任务
|
||||
schedule.every(self.sync_interval).minutes.do(self.job)
|
||||
|
||||
print(f"\n调度器已启动:")
|
||||
print(f" - 同步任务: 立即执行一次,然后每 {self.sync_interval} 分钟执行一次")
|
||||
print("按 Ctrl+C 停止调度器")
|
||||
print()
|
||||
|
||||
# 立即执行一次同步任务
|
||||
self.job()
|
||||
|
||||
# 进入调度循环
|
||||
while self.running:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
"""信号处理函数"""
|
||||
print("\n\n" + "=" * 80)
|
||||
print("收到停止信号,正在优雅退出...")
|
||||
print("=" * 80)
|
||||
self.running = False
|
||||
self._print_final_stats()
|
||||
|
||||
def _print_final_stats(self):
|
||||
"""打印最终统计信息"""
|
||||
print("\n" + "=" * 80)
|
||||
print("运行统计")
|
||||
print("=" * 80)
|
||||
|
||||
if self.stats['start_time']:
|
||||
start_time_str = self.stats['start_time'].strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"启动时间: {start_time_str}")
|
||||
|
||||
if self.stats['last_run_time']:
|
||||
last_run_str = self.stats['last_run_time'].strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"最后执行: {last_run_str}")
|
||||
|
||||
if self.stats['start_time']:
|
||||
running_time = datetime.now() - self.stats['start_time']
|
||||
hours = int(running_time.total_seconds() // 3600)
|
||||
minutes = int((running_time.total_seconds() % 3600) // 60)
|
||||
print(f"运行时长: {hours} 小时 {minutes} 分钟")
|
||||
|
||||
print(f"\n同步任务统计:")
|
||||
print(f" 总执行次数: {self.stats['total_runs']}")
|
||||
print(f" 成功次数: {self.stats['success_runs']}")
|
||||
print(f" 失败次数: {self.stats['failed_runs']}")
|
||||
print(f" 总同步记录: {self.stats['total_records_synced']}")
|
||||
print(f" 总更新记录: {self.stats['total_records_updated']}")
|
||||
|
||||
if self.stats['total_runs'] > 0:
|
||||
success_rate = (self.stats['success_runs'] / self.stats['total_runs']) * 100
|
||||
print(f" 成功率: {success_rate:.1f}%")
|
||||
|
||||
print("=" * 80)
|
||||
print("调度器已停止")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='TAPD状态同步调度器 - 定时执行同步任务',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例用法:
|
||||
# 使用默认配置启动调度器
|
||||
python src2/scheduler.py
|
||||
|
||||
# 指定配置文件路径
|
||||
python src2/scheduler.py --config /path/to/config.ini
|
||||
|
||||
# 显示详细信息
|
||||
python src2/scheduler.py --verbose
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
default=None,
|
||||
help='配置文件路径(默认: config/config_task2.ini)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='显示详细输出信息'
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
# 解析命令行参数
|
||||
args = parse_arguments()
|
||||
|
||||
# 创建并启动调度器
|
||||
scheduler = Task2Scheduler(
|
||||
config_path=args.config,
|
||||
verbose=args.verbose
|
||||
)
|
||||
scheduler.start()
|
||||
|
||||
return 0
|
||||
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 调度器启动失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
45
src2/smartsheet.py
Normal file
45
src2/smartsheet.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
任务二专用的智能表格API模块
|
||||
继承自任务一的 SmartSheetAPI,使用任务二专用的日志记录器
|
||||
|
||||
设计说明:
|
||||
- 继承 src.smartsheet.SmartSheetAPI 的所有功能
|
||||
- 重写 __init__ 方法,使用 get_task2_logger() 替代 get_logger()
|
||||
- 确保所有智能表格 API 调用日志写入 logs2/ 目录
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.smartsheet import SmartSheetAPI
|
||||
from src2.logger import get_task2_logger
|
||||
|
||||
|
||||
class SmartSheetAPITask2(SmartSheetAPI):
|
||||
"""任务二专用的智能表格API类"""
|
||||
|
||||
def __init__(self, access_token: str, docid: str, test_mode: bool = False):
|
||||
"""
|
||||
初始化智能表格API(任务二专用)
|
||||
|
||||
Args:
|
||||
access_token: 企业微信access_token
|
||||
docid: 智能表格文档ID
|
||||
test_mode: 是否启用测试模式(显示API返回结果)
|
||||
"""
|
||||
# 调用父类初始化
|
||||
super().__init__(access_token, docid, test_mode)
|
||||
|
||||
# 替换为任务二专用的日志记录器
|
||||
self.logger = get_task2_logger()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 任务二智能表格API模块 ===\n")
|
||||
print("此模块继承自 src.smartsheet.SmartSheetAPI")
|
||||
print("使用任务二专用的日志记录器,日志写入 logs2/ 目录")
|
||||
print("\n请通过 SmartSheetSync 类使用此模块")
|
||||
461
src2/smartsheet_sync.py
Normal file
461
src2/smartsheet_sync.py
Normal file
@ -0,0 +1,461 @@
|
||||
"""
|
||||
任务二智能表格同步模块
|
||||
负责智能表格的数据读取和回写
|
||||
|
||||
功能:
|
||||
1. 检测必要字段是否存在
|
||||
2. 读取所有记录
|
||||
3. 提取TAPD链接
|
||||
4. 构造更新记录
|
||||
5. 批量回写状态信息
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src2.smartsheet import SmartSheetAPITask2
|
||||
from src2.link_parser import parse_tapd_link, extract_story_id
|
||||
from src2.logger import get_task2_logger
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 字段名称常量(与智能表格列名完全一致)
|
||||
# ============================================================
|
||||
FIELD_TAPD_LINK = "TAPD链接" # 用户填写,解析单号
|
||||
FIELD_TAPD_STATUS = "TAPD状态(🈲勿手改)" # 工具回写
|
||||
FIELD_OWNER = "处理人(🈲勿手改)" # 工具回写
|
||||
FIELD_BEGIN_DATE = "TAPD预计开始日期(🈲勿手改)" # 工具回写
|
||||
FIELD_DUE_DATE = "TAPD预计完成日期(🈲勿手改)" # 工具回写
|
||||
FIELD_PLAN = "发布计划(🈲勿手改)" # 工具回写,TAPD发布计划字段
|
||||
FIELD_SYNC_STATUS = "同步状态(🈲勿手改)" # 工具回写,标记同步结果
|
||||
|
||||
# 必要字段列表
|
||||
REQUIRED_FIELDS = [
|
||||
FIELD_TAPD_LINK,
|
||||
FIELD_TAPD_STATUS,
|
||||
FIELD_OWNER,
|
||||
FIELD_BEGIN_DATE,
|
||||
FIELD_DUE_DATE,
|
||||
FIELD_PLAN,
|
||||
FIELD_SYNC_STATUS,
|
||||
]
|
||||
|
||||
|
||||
class SmartSheetSync:
|
||||
"""智能表格同步类"""
|
||||
|
||||
def __init__(self, access_token: str, docid: str, test_mode: bool = False):
|
||||
"""
|
||||
初始化智能表格同步模块
|
||||
|
||||
Args:
|
||||
access_token: 企业微信access_token
|
||||
docid: 智能表格文档ID
|
||||
test_mode: 是否启用测试模式
|
||||
"""
|
||||
self.api = SmartSheetAPITask2(access_token, docid, test_mode)
|
||||
self.logger = get_task2_logger()
|
||||
self.test_mode = test_mode
|
||||
|
||||
def check_required_fields(self, fields: List[Dict]) -> Tuple[bool, List[str], Dict[str, str]]:
|
||||
"""
|
||||
检测必要字段是否存在
|
||||
|
||||
Args:
|
||||
fields: 字段列表(从get_fields获取)
|
||||
|
||||
Returns:
|
||||
Tuple[bool, List[str], Dict[str, str]]:
|
||||
- 是否所有必要字段都存在
|
||||
- 缺失的字段列表
|
||||
- 字段名称到字段ID的映射
|
||||
"""
|
||||
# 构建字段映射
|
||||
field_mapping = {}
|
||||
for field in fields:
|
||||
field_title = field.get('field_title', '')
|
||||
field_id = field.get('field_id', '')
|
||||
if field_title and field_id:
|
||||
field_mapping[field_title] = field_id
|
||||
|
||||
# 检查必要字段
|
||||
missing_fields = []
|
||||
for required_field in REQUIRED_FIELDS:
|
||||
if required_field not in field_mapping:
|
||||
missing_fields.append(required_field)
|
||||
|
||||
all_present = len(missing_fields) == 0
|
||||
|
||||
if all_present:
|
||||
print(f" ✓ 所有必要字段都存在")
|
||||
else:
|
||||
print(f" ⚠ 缺少必要字段: {', '.join(missing_fields)}")
|
||||
|
||||
return (all_present, missing_fields, field_mapping)
|
||||
|
||||
def get_all_records(self, sheet_id: str) -> List[Dict]:
|
||||
"""
|
||||
获取子表的所有记录(支持分页)
|
||||
|
||||
Args:
|
||||
sheet_id: 子表ID
|
||||
|
||||
Returns:
|
||||
List[Dict]: 所有记录列表
|
||||
"""
|
||||
print(f"正在获取所有记录...")
|
||||
|
||||
all_records = []
|
||||
offset = 0
|
||||
limit = 100
|
||||
|
||||
while True:
|
||||
result = self.api.get_records(sheet_id, limit=limit, offset=offset)
|
||||
records = result['records']
|
||||
total = result['total']
|
||||
|
||||
all_records.extend(records)
|
||||
|
||||
print(f" - 已获取 {len(all_records)}/{total} 条记录")
|
||||
|
||||
if len(all_records) >= total:
|
||||
break
|
||||
|
||||
offset += limit
|
||||
|
||||
print(f" ✓ 共获取 {len(all_records)} 条记录")
|
||||
return all_records
|
||||
|
||||
def extract_tapd_link(self, record: Dict) -> Optional[str]:
|
||||
"""
|
||||
从记录中提取TAPD链接
|
||||
|
||||
Args:
|
||||
record: 记录对象
|
||||
|
||||
Returns:
|
||||
Optional[str]: TAPD链接字符串,如果不存在则返回None
|
||||
"""
|
||||
link_value = self.api.get_field_value_by_title(record, FIELD_TAPD_LINK)
|
||||
|
||||
if not link_value:
|
||||
return None
|
||||
|
||||
# 链接字段可能是字符串或包含url的对象
|
||||
if isinstance(link_value, str):
|
||||
return link_value
|
||||
elif isinstance(link_value, dict):
|
||||
# 可能是 {url: "...", text: "..."} 格式
|
||||
return link_value.get('url') or link_value.get('text')
|
||||
elif isinstance(link_value, list):
|
||||
# 可能是列表格式
|
||||
if len(link_value) > 0:
|
||||
first_item = link_value[0]
|
||||
if isinstance(first_item, dict):
|
||||
return first_item.get('url') or first_item.get('text')
|
||||
elif isinstance(first_item, str):
|
||||
return first_item
|
||||
|
||||
return None
|
||||
|
||||
def build_update_record(self, record_id: str, status: str = None,
|
||||
owner: str = None, begin_date: str = None,
|
||||
due_date: str = None, plan: str = None,
|
||||
sync_status: str = None) -> Dict:
|
||||
"""
|
||||
构造更新记录的数据结构
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
status: TAPD状态(中文)
|
||||
owner: 处理人
|
||||
begin_date: 预计开始日期
|
||||
due_date: 预计完成日期
|
||||
plan: 计划(中文名称)
|
||||
sync_status: 同步状态("成功" 或 "失败")
|
||||
|
||||
Returns:
|
||||
Dict: 更新记录的数据结构
|
||||
"""
|
||||
values = {}
|
||||
|
||||
# 处理字段值:
|
||||
# - None: 不更新该字段(跳过)
|
||||
# - 空字符串 "": 清空该字段(传入空数组)
|
||||
# - 非空字符串: 更新为新值
|
||||
|
||||
if status is not None:
|
||||
if status == "":
|
||||
values[FIELD_TAPD_STATUS] = []
|
||||
else:
|
||||
values[FIELD_TAPD_STATUS] = [{"type": "text", "text": status}]
|
||||
|
||||
if owner is not None:
|
||||
if owner == "":
|
||||
values[FIELD_OWNER] = []
|
||||
else:
|
||||
values[FIELD_OWNER] = [{"type": "text", "text": owner}]
|
||||
|
||||
if begin_date is not None:
|
||||
if begin_date == "":
|
||||
values[FIELD_BEGIN_DATE] = []
|
||||
else:
|
||||
values[FIELD_BEGIN_DATE] = [{"type": "text", "text": begin_date}]
|
||||
|
||||
if due_date is not None:
|
||||
if due_date == "":
|
||||
values[FIELD_DUE_DATE] = []
|
||||
else:
|
||||
values[FIELD_DUE_DATE] = [{"type": "text", "text": due_date}]
|
||||
|
||||
if plan is not None:
|
||||
if plan == "":
|
||||
values[FIELD_PLAN] = []
|
||||
else:
|
||||
values[FIELD_PLAN] = [{"type": "text", "text": plan}]
|
||||
|
||||
if sync_status is not None:
|
||||
if sync_status == "":
|
||||
values[FIELD_SYNC_STATUS] = []
|
||||
else:
|
||||
values[FIELD_SYNC_STATUS] = [{"type": "text", "text": sync_status}]
|
||||
|
||||
return {
|
||||
"record_id": record_id,
|
||||
"values": values
|
||||
}
|
||||
|
||||
def batch_update_records(self, sheet_id: str, update_records: List[Dict]) -> Dict:
|
||||
"""
|
||||
批量回写状态信息(使用任务一的API,带debug参数)
|
||||
|
||||
Args:
|
||||
sheet_id: 子表ID
|
||||
update_records: 需要更新的记录列表
|
||||
|
||||
Returns:
|
||||
Dict: 更新结果
|
||||
"""
|
||||
if not update_records:
|
||||
print(" ⚠ 没有需要更新的记录")
|
||||
return {"records": []}
|
||||
|
||||
# 直接使用任务一的 update_records 方法(已添加debug=1)
|
||||
return self.api.update_records(sheet_id, update_records)
|
||||
|
||||
def get_records_with_tapd_link(self, sheet_id: str,
|
||||
all_records: List[Dict] = None) -> List[Dict]:
|
||||
"""
|
||||
获取所有包含TAPD链接的新记录(同步状态为空)
|
||||
|
||||
Args:
|
||||
sheet_id: 子表ID
|
||||
all_records: 可选,已获取的所有记录列表,避免重复获取
|
||||
|
||||
Returns:
|
||||
List[Dict]: 包含TAPD链接的记录列表
|
||||
"""
|
||||
print(f"正在获取包含TAPD链接的新记录...")
|
||||
|
||||
if all_records is None:
|
||||
all_records = self.get_all_records(sheet_id)
|
||||
|
||||
records_with_link = []
|
||||
skipped_synced_count = 0
|
||||
|
||||
for record in all_records:
|
||||
tapd_link = self.extract_tapd_link(record)
|
||||
|
||||
if not tapd_link:
|
||||
continue
|
||||
|
||||
# 检查同步状态字段,如果不为空则跳过
|
||||
sync_status = self.api.get_field_value_by_title(record, FIELD_SYNC_STATUS)
|
||||
if sync_status is not None and sync_status != "":
|
||||
skipped_synced_count += 1
|
||||
continue
|
||||
|
||||
record_id = record.get('record_id', '')
|
||||
|
||||
# 解析链接
|
||||
success, result, link_type = parse_tapd_link(tapd_link)
|
||||
|
||||
record_info = {
|
||||
"record": record,
|
||||
"record_id": record_id,
|
||||
"tapd_link": tapd_link,
|
||||
"parse_success": success,
|
||||
}
|
||||
|
||||
if success:
|
||||
record_info["story_id"] = result
|
||||
record_info["link_type"] = link_type
|
||||
else:
|
||||
record_info["story_id"] = None
|
||||
record_info["parse_error"] = result
|
||||
|
||||
records_with_link.append(record_info)
|
||||
|
||||
# 统计
|
||||
success_count = sum(1 for r in records_with_link if r["parse_success"])
|
||||
fail_count = len(records_with_link) - success_count
|
||||
|
||||
print(f" ✓ 找到 {len(records_with_link)} 条包含TAPD链接的记录")
|
||||
if skipped_synced_count > 0:
|
||||
print(f" - 跳过已同步记录: {skipped_synced_count} 条")
|
||||
print(f" - 链接解析成功: {success_count} 条")
|
||||
if fail_count > 0:
|
||||
print(f" - 链接解析失败: {fail_count} 条")
|
||||
|
||||
return records_with_link
|
||||
|
||||
def get_current_field_values(self, record: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
获取记录当前的字段值
|
||||
|
||||
Args:
|
||||
record: 记录对象
|
||||
|
||||
Returns:
|
||||
Dict: 当前字段值
|
||||
"""
|
||||
return {
|
||||
FIELD_TAPD_STATUS: self.api.get_field_value_by_title(record, FIELD_TAPD_STATUS),
|
||||
FIELD_OWNER: self.api.get_field_value_by_title(record, FIELD_OWNER),
|
||||
FIELD_BEGIN_DATE: self.api.get_field_value_by_title(record, FIELD_BEGIN_DATE),
|
||||
FIELD_DUE_DATE: self.api.get_field_value_by_title(record, FIELD_DUE_DATE),
|
||||
FIELD_PLAN: self.api.get_field_value_by_title(record, FIELD_PLAN),
|
||||
}
|
||||
|
||||
def get_synced_records_for_update(self, sheet_id: str,
|
||||
terminal_statuses: List[str],
|
||||
all_records: List[Dict] = None) -> List[Dict]:
|
||||
"""
|
||||
获取需要持续同步的已同步记录
|
||||
|
||||
筛选条件:
|
||||
- 同步状态 = "成功"
|
||||
- TAPD状态 不在终态列表中
|
||||
|
||||
Args:
|
||||
sheet_id: 子表ID
|
||||
terminal_statuses: 终态列表(如 ['已完成', '取消'])
|
||||
all_records: 可选,已获取的所有记录列表,避免重复获取
|
||||
|
||||
Returns:
|
||||
List[Dict]: 需要持续同步的记录列表
|
||||
"""
|
||||
print(f"正在获取需要持续同步的记录...")
|
||||
|
||||
if all_records is None:
|
||||
all_records = self.get_all_records(sheet_id)
|
||||
records_for_update = []
|
||||
skipped_terminal_count = 0
|
||||
|
||||
for record in all_records:
|
||||
# 检查同步状态是否为成功(兼容新旧格式)
|
||||
# 旧格式: "成功"
|
||||
# 新格式: "✅ 同步成功 01-14 15:30"
|
||||
sync_status = self.api.get_field_value_by_title(record, FIELD_SYNC_STATUS)
|
||||
sync_status_str = str(sync_status) if sync_status else ""
|
||||
if not (sync_status == "成功" or "同步成功" in sync_status_str):
|
||||
continue
|
||||
|
||||
# 检查TAPD链接是否存在
|
||||
tapd_link = self.extract_tapd_link(record)
|
||||
if not tapd_link:
|
||||
continue
|
||||
|
||||
# 检查TAPD状态是否为终态
|
||||
tapd_status = self.api.get_field_value_by_title(record, FIELD_TAPD_STATUS)
|
||||
if tapd_status in terminal_statuses:
|
||||
skipped_terminal_count += 1
|
||||
continue
|
||||
|
||||
# 解析链接获取story_id
|
||||
success, result, link_type = parse_tapd_link(tapd_link)
|
||||
if not success:
|
||||
continue
|
||||
|
||||
record_info = {
|
||||
"record": record,
|
||||
"record_id": record.get('record_id', ''),
|
||||
"tapd_link": tapd_link,
|
||||
"story_id": result,
|
||||
"current_status": tapd_status,
|
||||
}
|
||||
records_for_update.append(record_info)
|
||||
|
||||
print(f" ✓ 找到 {len(records_for_update)} 条需要持续同步的记录")
|
||||
if skipped_terminal_count > 0:
|
||||
print(f" - 跳过终态记录: {skipped_terminal_count} 条")
|
||||
|
||||
return records_for_update
|
||||
|
||||
|
||||
def process_sheet(api: SmartSheetSync, sheet_id: str, sheet_title: str) -> Dict:
|
||||
"""
|
||||
处理单个子表的同步流程
|
||||
|
||||
Args:
|
||||
api: SmartSheetSync实例
|
||||
sheet_id: 子表ID
|
||||
sheet_title: 子表标题
|
||||
|
||||
Returns:
|
||||
Dict: 处理结果统计
|
||||
"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"处理子表: {sheet_title}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
result = {
|
||||
"sheet_id": sheet_id,
|
||||
"sheet_title": sheet_title,
|
||||
"success": False,
|
||||
"skipped": False,
|
||||
"skip_reason": None,
|
||||
"total_records": 0,
|
||||
"records_with_link": 0,
|
||||
"parse_success": 0,
|
||||
"parse_fail": 0,
|
||||
}
|
||||
|
||||
# 1. 获取字段信息
|
||||
fields = api.api.get_fields(sheet_id)
|
||||
|
||||
# 2. 检查必要字段
|
||||
all_present, missing_fields, field_mapping = api.check_required_fields(fields)
|
||||
|
||||
if not all_present:
|
||||
result["skipped"] = True
|
||||
result["skip_reason"] = f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
print(f" ⚠ 跳过此子表: {result['skip_reason']}")
|
||||
return result
|
||||
|
||||
# 3. 获取包含TAPD链接的记录
|
||||
records_with_link = api.get_records_with_tapd_link(sheet_id)
|
||||
|
||||
result["records_with_link"] = len(records_with_link)
|
||||
result["parse_success"] = sum(1 for r in records_with_link if r["parse_success"])
|
||||
result["parse_fail"] = result["records_with_link"] - result["parse_success"]
|
||||
result["success"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 智能表格同步模块测试 ===\n")
|
||||
print("此模块提供以下功能:")
|
||||
print("1. check_required_fields() - 检测必要字段")
|
||||
print("2. get_all_records() - 获取所有记录")
|
||||
print("3. extract_tapd_link() - 提取TAPD链接")
|
||||
print("4. build_update_record() - 构造更新记录")
|
||||
print("5. batch_update_records() - 批量回写")
|
||||
print("6. get_records_with_tapd_link() - 获取包含链接的记录")
|
||||
print("\n请运行 test_phase3.py 进行完整测试")
|
||||
856
src2/sync_service.py
Normal file
856
src2/sync_service.py
Normal file
@ -0,0 +1,856 @@
|
||||
"""
|
||||
任务二同步服务模块
|
||||
整合链接解析、TAPD查询、表格回写,实现完整的同步流程
|
||||
|
||||
功能:
|
||||
1. 单次同步流程
|
||||
2. 执行统计
|
||||
3. 错误处理
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.token_manager import TokenManager
|
||||
from src2.config import Task2ConfigManager
|
||||
from src2.logger import get_task2_logger
|
||||
from src2.tapd_api import TAPDStoryApi, TERMINAL_STATUSES, StoryNotFoundException
|
||||
from src2.smartsheet_sync import SmartSheetSync, REQUIRED_FIELDS
|
||||
|
||||
|
||||
class CacheEntry:
|
||||
"""TAPD查询缓存条目"""
|
||||
def __init__(self, success: bool, data: Optional[Dict] = None,
|
||||
error: Optional[Exception] = None):
|
||||
self.success = success
|
||||
self.data = data
|
||||
self.error = error
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
|
||||
class SyncService:
|
||||
"""TAPD状态同步服务(支持多表格同步)"""
|
||||
|
||||
def __init__(self, config_manager: Task2ConfigManager = None,
|
||||
access_token: str = None, test_mode: bool = False):
|
||||
"""
|
||||
初始化同步服务
|
||||
|
||||
Args:
|
||||
config_manager: 配置管理器,如果为None则自动创建
|
||||
access_token: 企业微信access_token,如果为None则自动获取
|
||||
test_mode: 是否启用测试模式
|
||||
"""
|
||||
self.test_mode = test_mode
|
||||
self.logger = get_task2_logger()
|
||||
|
||||
# 初始化配置管理器
|
||||
if config_manager is None:
|
||||
self.config_manager = Task2ConfigManager()
|
||||
else:
|
||||
self.config_manager = config_manager
|
||||
|
||||
# 获取配置
|
||||
self.config = self.config_manager.get_all_config()
|
||||
self.workspace_id = self.config['tapd']['workspace_id']
|
||||
|
||||
# 获取所有docid列表
|
||||
self.docid_list = self.config['smartsheet']['docid_list']
|
||||
print(f" 配置了 {len(self.docid_list)} 个智能表格")
|
||||
|
||||
# 获取access_token
|
||||
if access_token is None:
|
||||
token_manager = TokenManager(logger=self.logger)
|
||||
self.access_token = token_manager.get_token()
|
||||
else:
|
||||
self.access_token = access_token
|
||||
|
||||
# 初始化TAPD API(所有表格共用)
|
||||
self.tapd_api = TAPDStoryApi(self.workspace_id, test_mode=test_mode)
|
||||
|
||||
# 获取计划字段映射(每次同步时实时获取,所有表格共用)
|
||||
print(f" 正在获取计划字段映射...")
|
||||
try:
|
||||
self.plan_mapping = self.tapd_api.get_plan_mapping()
|
||||
print(f" ✓ 计划字段映射获取完成,共 {len(self.plan_mapping)} 个选项")
|
||||
except Exception as e:
|
||||
print(f" ⚠ 计划字段映射获取失败: {e},将使用空映射")
|
||||
self.plan_mapping = {}
|
||||
|
||||
# TAPD查询缓存(在sync_once中初始化)
|
||||
self._story_cache: Dict[str, CacheEntry] = {}
|
||||
self._cache_stats = {
|
||||
"total_queries": 0,
|
||||
"cache_hits": 0,
|
||||
"cache_misses": 0,
|
||||
"api_calls": 0,
|
||||
"cached_failures": 0
|
||||
}
|
||||
|
||||
print(f" ✓ 同步服务初始化完成")
|
||||
|
||||
def _get_beijing_time_str(self) -> str:
|
||||
"""
|
||||
获取北京时间格式字符串(MM-dd HH:mm)
|
||||
|
||||
Returns:
|
||||
str: 格式化的北京时间字符串,例如 "01-14 15:30"
|
||||
"""
|
||||
# 北京时间是 UTC+8
|
||||
beijing_tz = timezone(timedelta(hours=8))
|
||||
beijing_time = datetime.now(beijing_tz)
|
||||
return beijing_time.strftime("%m-%d %H:%M")
|
||||
|
||||
def sync_once(self) -> Dict[str, Any]:
|
||||
"""
|
||||
执行一次完整的同步流程(遍历所有配置的智能表格)
|
||||
|
||||
Returns:
|
||||
Dict: 同步结果统计
|
||||
"""
|
||||
# 初始化本次同步的缓存
|
||||
self._story_cache = {}
|
||||
self._cache_stats = {
|
||||
"total_queries": 0,
|
||||
"cache_hits": 0,
|
||||
"cache_misses": 0,
|
||||
"api_calls": 0,
|
||||
"cached_failures": 0
|
||||
}
|
||||
|
||||
result = {
|
||||
"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,
|
||||
"records_with_link": 0,
|
||||
"records_synced": 0,
|
||||
"records_updated": 0,
|
||||
"records_failed": 0,
|
||||
"error_message": None,
|
||||
"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)
|
||||
|
||||
# 记录缓存统计
|
||||
self._log_cache_statistics()
|
||||
result["cache_stats"] = self._cache_stats.copy()
|
||||
|
||||
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:
|
||||
# 为当前表格创建SmartSheetSync实例
|
||||
smartsheet = SmartSheetSync(self.access_token, docid, test_mode=self.test_mode)
|
||||
self.current_smartsheet = smartsheet # 供_process_sheet等方法使用
|
||||
|
||||
# 获取所有子表
|
||||
print("\n正在获取子表列表...")
|
||||
sheets = smartsheet.api.get_sheet_list()
|
||||
print(f" ✓ 找到 {len(sheets)} 个子表")
|
||||
|
||||
# 处理每个子表
|
||||
for sheet in sheets:
|
||||
sheet_id = sheet.get('sheet_id', '')
|
||||
sheet_title = sheet.get('title', '未命名')
|
||||
|
||||
sheet_result = self._process_sheet(sheet_id, sheet_title, doc_index)
|
||||
doc_result["sheet_results"].append(sheet_result)
|
||||
|
||||
if sheet_result["skipped"]:
|
||||
doc_result["sheets_skipped"] += 1
|
||||
else:
|
||||
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"]
|
||||
|
||||
# 收集失败记录(添加表格标识)
|
||||
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)
|
||||
|
||||
doc_result["success"] = True
|
||||
print(f"\n✓ [表格 {doc_index}/{total_docs}] 同步完成")
|
||||
|
||||
except Exception as e:
|
||||
doc_result["error_message"] = str(e)
|
||||
print(f"\n✗ [表格 {doc_index}/{total_docs}] 同步失败: {e}")
|
||||
|
||||
if self.test_mode:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return doc_result
|
||||
|
||||
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} (表格{doc_index})")
|
||||
print(f"{'='*60}")
|
||||
|
||||
sheet_result = {
|
||||
"sheet_id": sheet_id,
|
||||
"sheet_title": sheet_title,
|
||||
"skipped": False,
|
||||
"skip_reason": None,
|
||||
"total_records": 0,
|
||||
"records_with_link": 0,
|
||||
"records_synced": 0,
|
||||
"records_updated": 0,
|
||||
"records_failed": 0,
|
||||
"details": [],
|
||||
"failed_records": [] # 收集失败记录的详细信息
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. 获取字段信息并检查必要字段
|
||||
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
|
||||
sheet_result["skip_reason"] = f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
print(f" ⚠ 跳过此子表: {sheet_result['skip_reason']}")
|
||||
return sheet_result
|
||||
|
||||
# 2. 获取所有记录(只获取一次,供新记录同步和持续同步共用)
|
||||
print(f"正在获取所有记录...")
|
||||
all_records = self.current_smartsheet.get_all_records(sheet_id)
|
||||
|
||||
# 3. 获取包含TAPD链接的新记录(同步状态为空)
|
||||
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)
|
||||
|
||||
# 4. 处理新记录
|
||||
if records_with_link:
|
||||
print(f"\n--- 新记录同步 ---")
|
||||
success_records = [] # 成功同步的记录
|
||||
failed_records_info = [] # 失败的记录信息列表(包含ID和状态)
|
||||
|
||||
for record_info in records_with_link:
|
||||
record_result = self._process_record(record_info)
|
||||
sheet_result["details"].append(record_result)
|
||||
|
||||
if record_result["success"]:
|
||||
sheet_result["records_synced"] += 1
|
||||
|
||||
if record_result["update_record"]:
|
||||
success_records.append(record_result["update_record"])
|
||||
sheet_result["records_updated"] += 1
|
||||
else:
|
||||
sheet_result["records_failed"] += 1
|
||||
|
||||
# 收集失败记录的ID和对应的同步状态
|
||||
failed_records_info.append({
|
||||
"record_id": record_info["record_id"],
|
||||
"sync_status": record_result.get("sync_status", "⚠️ 同步失败,请联系PM")
|
||||
})
|
||||
|
||||
# 收集失败记录的详细信息用于推送
|
||||
failed_record = {
|
||||
"sheet_title": sheet_title,
|
||||
"record_id": record_info["record_id"],
|
||||
"tapd_link": record_info.get("tapd_link", "(无链接)"),
|
||||
"error_message": record_result.get("error_message", "未知错误")
|
||||
}
|
||||
sheet_result["failed_records"].append(failed_record)
|
||||
|
||||
# 4. 批量回写成功记录
|
||||
if success_records:
|
||||
print(f"\n正在回写 {len(success_records)} 条成功记录...")
|
||||
try:
|
||||
self.current_smartsheet.batch_update_records(sheet_id, success_records)
|
||||
print(f" ✓ 成功记录回写完成")
|
||||
except Exception as e:
|
||||
print(f" ✗ 成功记录回写失败: {e}")
|
||||
|
||||
# 5. 批量回写失败记录(根据不同失败原因回写不同状态)
|
||||
if failed_records_info:
|
||||
print(f"\n正在回写 {len(failed_records_info)} 条失败记录的状态...")
|
||||
try:
|
||||
failed_updates = [
|
||||
self.current_smartsheet.build_update_record(
|
||||
record_id=info["record_id"],
|
||||
sync_status=info["sync_status"]
|
||||
)
|
||||
for info in failed_records_info
|
||||
]
|
||||
self.current_smartsheet.batch_update_records(sheet_id, failed_updates)
|
||||
print(f" ✓ 失败记录状态回写完成")
|
||||
except Exception as e:
|
||||
print(f" ✗ 失败记录状态回写失败: {e}")
|
||||
else:
|
||||
print(f" ℹ 没有新记录需要同步")
|
||||
|
||||
# 7. 持续同步:更新已同步记录的最新状态
|
||||
print(f"\n--- 持续同步 ---")
|
||||
update_result = self._process_synced_records_update(sheet_id, all_records=all_records)
|
||||
sheet_result["continuous_sync"] = update_result
|
||||
|
||||
sheet_result["total_records"] = len(records_with_link)
|
||||
|
||||
except Exception as e:
|
||||
sheet_result["skipped"] = True
|
||||
sheet_result["skip_reason"] = f"处理异常: {str(e)}"
|
||||
print(f" ✗ 处理子表异常: {e}")
|
||||
|
||||
return sheet_result
|
||||
|
||||
def _get_story_with_cache(self, story_id: str) -> Dict:
|
||||
"""
|
||||
带缓存的获取需求详情
|
||||
|
||||
Args:
|
||||
story_id: 需求ID
|
||||
|
||||
Returns:
|
||||
Dict: 需求详细信息
|
||||
|
||||
Raises:
|
||||
StoryNotFoundException: 需求不存在
|
||||
Exception: 其他API错误
|
||||
"""
|
||||
self._cache_stats["total_queries"] += 1
|
||||
|
||||
# 检查缓存
|
||||
if story_id in self._story_cache:
|
||||
cache_entry = self._story_cache[story_id]
|
||||
self._cache_stats["cache_hits"] += 1
|
||||
|
||||
if cache_entry.success:
|
||||
# 缓存命中 - 成功记录
|
||||
if self.test_mode:
|
||||
print(f" [缓存命中] story_id={story_id}")
|
||||
return cache_entry.data
|
||||
else:
|
||||
# 缓存命中 - 失败记录
|
||||
self._cache_stats["cached_failures"] += 1
|
||||
if self.test_mode:
|
||||
print(f" [缓存命中-失败] story_id={story_id}")
|
||||
raise cache_entry.error
|
||||
|
||||
# 缓存未命中,调用API
|
||||
self._cache_stats["cache_misses"] += 1
|
||||
self._cache_stats["api_calls"] += 1
|
||||
|
||||
if self.test_mode:
|
||||
print(f" [API调用] story_id={story_id}")
|
||||
|
||||
try:
|
||||
story_info = self.tapd_api.get_story(story_id)
|
||||
# 缓存成功结果
|
||||
self._story_cache[story_id] = CacheEntry(success=True, data=story_info)
|
||||
return story_info
|
||||
except StoryNotFoundException as e:
|
||||
# 缓存确定性失败(单号无效)
|
||||
self._story_cache[story_id] = CacheEntry(success=False, error=e)
|
||||
raise
|
||||
except Exception as e:
|
||||
# 不缓存非确定性失败(网络错误、API限流等)
|
||||
raise
|
||||
|
||||
def _process_record(self, record_info: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
处理单条记录的同步
|
||||
|
||||
Args:
|
||||
record_info: 记录信息(包含解析结果)
|
||||
|
||||
Returns:
|
||||
Dict: 记录处理结果
|
||||
"""
|
||||
record_result = {
|
||||
"record_id": record_info["record_id"],
|
||||
"tapd_link": record_info["tapd_link"],
|
||||
"success": False,
|
||||
"update_record": None,
|
||||
"error_message": None,
|
||||
"story_info": None,
|
||||
"sync_status": None # 用于失败记录的状态回写
|
||||
}
|
||||
|
||||
# 检查链接解析结果
|
||||
if not record_info["parse_success"]:
|
||||
record_result["error_message"] = record_info.get("parse_error", "链接解析失败")
|
||||
record_result["sync_status"] = "❌ 单号无效"
|
||||
# 记录链接解析失败日志
|
||||
self.logger.log_api_call(
|
||||
api_type="smartsheet",
|
||||
operation="link_parse_failure",
|
||||
request_data={
|
||||
"record_id": record_info["record_id"],
|
||||
"tapd_link": record_info["tapd_link"]
|
||||
},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=record_result["error_message"]
|
||||
)
|
||||
return record_result
|
||||
|
||||
story_id = record_info["story_id"]
|
||||
|
||||
try:
|
||||
# 查询TAPD获取需求信息(使用缓存)
|
||||
story_info = self._get_story_with_cache(story_id)
|
||||
record_result["story_info"] = story_info
|
||||
|
||||
# 提取需要同步的字段(None 转为空字符串)
|
||||
status = story_info.get('status') or ''
|
||||
owner = story_info.get('owner') or ''
|
||||
begin_date = story_info.get('begin') or ''
|
||||
due_date = story_info.get('due') or ''
|
||||
|
||||
# 提取发布计划字段并转换为中文名称
|
||||
plan_id = story_info.get('release_id') or ''
|
||||
plan_name = self.tapd_api.map_plan_id_to_name(plan_id)
|
||||
|
||||
# 获取当前字段值,判断是否需要更新
|
||||
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
|
||||
)
|
||||
|
||||
# 生成同步成功状态(包含时间戳)
|
||||
time_str = self._get_beijing_time_str()
|
||||
sync_status_success = f"✅ 同步成功 {time_str}"
|
||||
|
||||
# 构造更新记录(包含业务字段 + 同步状态=成功+时间戳)
|
||||
# 即使业务字段没有变化,也要写入同步状态
|
||||
update_record = self.current_smartsheet.build_update_record(
|
||||
record_id=record_info["record_id"],
|
||||
status=status,
|
||||
owner=owner,
|
||||
begin_date=begin_date,
|
||||
due_date=due_date,
|
||||
plan=plan_name,
|
||||
sync_status=sync_status_success
|
||||
)
|
||||
record_result["update_record"] = update_record
|
||||
|
||||
record_result["success"] = True
|
||||
|
||||
except StoryNotFoundException as e:
|
||||
# 单号无效(TAPD中未找到该需求)
|
||||
record_result["error_message"] = str(e)
|
||||
record_result["sync_status"] = "❌ 单号无效"
|
||||
# 记录TAPD查询失败日志
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation="sync_record_failure",
|
||||
request_data={
|
||||
"record_id": record_info["record_id"],
|
||||
"story_id": story_id
|
||||
},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=record_result["error_message"]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 其他异常(API调用失败等)
|
||||
record_result["error_message"] = str(e)
|
||||
record_result["sync_status"] = "⚠️ 同步失败,请联系PM"
|
||||
# 记录TAPD查询失败日志
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation="sync_record_failure",
|
||||
request_data={
|
||||
"record_id": record_info["record_id"],
|
||||
"story_id": story_id
|
||||
},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=record_result["error_message"]
|
||||
)
|
||||
|
||||
return record_result
|
||||
|
||||
def _check_needs_update(self, current_values: Dict,
|
||||
status: str, owner: str,
|
||||
begin_date: str, due_date: str,
|
||||
plan: str = "") -> bool:
|
||||
"""
|
||||
检查是否需要更新记录
|
||||
|
||||
Args:
|
||||
current_values: 当前字段值
|
||||
status: 新状态
|
||||
owner: 新处理人
|
||||
begin_date: 新开始日期
|
||||
due_date: 新结束日期
|
||||
plan: 新计划
|
||||
|
||||
Returns:
|
||||
bool: 是否需要更新
|
||||
"""
|
||||
# 提取当前值的文本内容
|
||||
def extract_text(value):
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, list) and len(value) > 0:
|
||||
first = value[0]
|
||||
if isinstance(first, dict):
|
||||
return first.get('text', '')
|
||||
return str(first)
|
||||
if isinstance(value, dict):
|
||||
return value.get('text', '')
|
||||
return str(value)
|
||||
|
||||
current_status = extract_text(current_values.get('TAPD状态'))
|
||||
current_owner = extract_text(current_values.get('处理人'))
|
||||
current_begin = extract_text(current_values.get('TAPD预计开始日期'))
|
||||
current_due = extract_text(current_values.get('TAPD预计完成日期'))
|
||||
current_plan = extract_text(current_values.get('计划'))
|
||||
|
||||
# 比较是否有变化
|
||||
if current_status != status:
|
||||
return True
|
||||
if current_owner != owner:
|
||||
return True
|
||||
if current_begin != begin_date:
|
||||
return True
|
||||
if current_due != due_date:
|
||||
return True
|
||||
if current_plan != plan:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _process_synced_records_update(self, sheet_id: str,
|
||||
all_records: List[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
处理已同步记录的状态更新(持续同步)
|
||||
|
||||
Args:
|
||||
sheet_id: 子表ID
|
||||
all_records: 可选,已获取的所有记录列表
|
||||
|
||||
Returns:
|
||||
Dict: 更新结果统计
|
||||
"""
|
||||
result = {
|
||||
"checked": 0,
|
||||
"updated": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
|
||||
# 获取需要持续同步的记录
|
||||
records = self.current_smartsheet.get_synced_records_for_update(
|
||||
sheet_id, TERMINAL_STATUSES, all_records=all_records
|
||||
)
|
||||
|
||||
if not records:
|
||||
print(f" ℹ 没有需要持续同步的记录")
|
||||
return result
|
||||
|
||||
result["checked"] = len(records)
|
||||
updates = []
|
||||
|
||||
for record_info in records:
|
||||
try:
|
||||
# 查询TAPD最新状态(使用缓存)
|
||||
story_info = self._get_story_with_cache(record_info["story_id"])
|
||||
|
||||
# 提取最新字段值(None 转为空字符串)
|
||||
new_status = story_info.get('status') or ''
|
||||
new_owner = story_info.get('owner') or ''
|
||||
new_begin = story_info.get('begin') or ''
|
||||
new_due = story_info.get('due') or ''
|
||||
plan_id = story_info.get('release_id') or ''
|
||||
new_plan = self.tapd_api.map_plan_id_to_name(plan_id)
|
||||
|
||||
# 获取当前值并比较
|
||||
current = self.current_smartsheet.get_current_field_values(record_info["record"])
|
||||
|
||||
# 调试:打印当前值和新值(含类型)
|
||||
print(f"\n [DEBUG] 记录 {record_info['record_id']} 字段比较:")
|
||||
print(f" 结束日期: 当前='{current.get('TAPD预计完成日期')}' (type={type(current.get('TAPD预计完成日期'))}) vs 新='{new_due}' (type={type(new_due)})")
|
||||
|
||||
needs_update = self._check_needs_update(
|
||||
current, new_status, new_owner, new_begin, new_due, new_plan
|
||||
)
|
||||
print(f" needs_update = {needs_update}")
|
||||
|
||||
if needs_update:
|
||||
# 生成同步成功状态(包含时间戳)
|
||||
time_str = self._get_beijing_time_str()
|
||||
sync_status_success = f"✅ 同步成功 {time_str}"
|
||||
|
||||
update_record = self.current_smartsheet.build_update_record(
|
||||
record_id=record_info["record_id"],
|
||||
status=new_status,
|
||||
owner=new_owner,
|
||||
begin_date=new_begin,
|
||||
due_date=new_due,
|
||||
plan=new_plan,
|
||||
sync_status=sync_status_success
|
||||
)
|
||||
updates.append(update_record)
|
||||
|
||||
except Exception as e:
|
||||
result["failed"] += 1
|
||||
error_msg = str(e)
|
||||
# 记录持续同步失败日志
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation="continuous_sync_failure",
|
||||
request_data={
|
||||
"record_id": record_info["record_id"],
|
||||
"story_id": record_info["story_id"]
|
||||
},
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
if self.test_mode:
|
||||
print(f" ✗ 记录 {record_info['record_id']} 更新失败: {e}")
|
||||
|
||||
# 批量更新
|
||||
if updates:
|
||||
print(f" 正在更新 {len(updates)} 条记录...")
|
||||
self.current_smartsheet.batch_update_records(sheet_id, updates)
|
||||
result["updated"] = len(updates)
|
||||
print(f" ✓ 持续同步更新完成")
|
||||
else:
|
||||
print(f" ✓ 所有记录状态均未变化")
|
||||
|
||||
return result
|
||||
|
||||
def _log_cache_statistics(self):
|
||||
"""记录缓存统计信息到日志"""
|
||||
stats = self._cache_stats
|
||||
|
||||
if stats["total_queries"] == 0:
|
||||
return
|
||||
|
||||
hit_rate = (stats["cache_hits"] / stats["total_queries"]) * 100
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"TAPD查询缓存统计")
|
||||
print(f"{'='*60}")
|
||||
print(f" 总查询次数: {stats['total_queries']}")
|
||||
print(f" 缓存命中: {stats['cache_hits']} ({hit_rate:.1f}%)")
|
||||
print(f" 缓存未命中: {stats['cache_misses']}")
|
||||
print(f" 实际API调用: {stats['api_calls']}")
|
||||
print(f" 缓存失败记录命中: {stats['cached_failures']}")
|
||||
print(f" 缓存条目数: {len(self._story_cache)}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# 记录到日志文件
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation="cache_statistics",
|
||||
request_data={},
|
||||
response_data=stats,
|
||||
success=True,
|
||||
error_message=None
|
||||
)
|
||||
|
||||
def _send_failure_notification(self, failed_records: List[Dict]) -> None:
|
||||
"""
|
||||
发送同步失败通知
|
||||
|
||||
Args:
|
||||
failed_records: 失败记录列表
|
||||
"""
|
||||
try:
|
||||
# 获取企业微信配置
|
||||
wework_config = self.config.get('wework')
|
||||
|
||||
if not wework_config:
|
||||
print(" ℹ 未配置企业微信推送,跳过失败通知")
|
||||
return
|
||||
|
||||
agentid = wework_config.get('agentid')
|
||||
receivers = wework_config.get('receivers')
|
||||
|
||||
if not agentid or not receivers:
|
||||
print(" ⚠ 企业微信推送配置不完整,跳过失败通知")
|
||||
return
|
||||
|
||||
# 发送推送通知
|
||||
print(f"\n正在发送同步失败通知...")
|
||||
from src2.notifier import send_sync_failure_notification
|
||||
|
||||
success = send_sync_failure_notification(
|
||||
self.access_token,
|
||||
agentid,
|
||||
receivers,
|
||||
failed_records
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f" ✓ 失败通知已发送")
|
||||
else:
|
||||
print(f" ✗ 失败通知发送失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 发送失败通知时出错: {e}")
|
||||
if self.test_mode:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def run_once(config_manager: Task2ConfigManager = None,
|
||||
access_token: str = None,
|
||||
test_mode: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
执行一次同步流程(供调度器调用)
|
||||
|
||||
Args:
|
||||
config_manager: 配置管理器
|
||||
access_token: access_token
|
||||
test_mode: 测试模式
|
||||
|
||||
Returns:
|
||||
Dict: 同步结果
|
||||
"""
|
||||
service = SyncService(
|
||||
config_manager=config_manager,
|
||||
access_token=access_token,
|
||||
test_mode=test_mode
|
||||
)
|
||||
|
||||
logger = service.logger
|
||||
started_sync_here = False
|
||||
if not logger.get_active_sync_id():
|
||||
logger.start_sync(
|
||||
trigger="task2_run_once_manual",
|
||||
metadata={
|
||||
"entry": "src2/sync_service.py:run_once",
|
||||
"test_mode": test_mode,
|
||||
},
|
||||
)
|
||||
started_sync_here = True
|
||||
|
||||
try:
|
||||
result = service.sync_once()
|
||||
if started_sync_here:
|
||||
logger.end_sync_with_stats(
|
||||
stats={
|
||||
"docs_total": result.get("docs_total", 0),
|
||||
"docs_success": result.get("docs_success", 0),
|
||||
"docs_failed": result.get("docs_failed", 0),
|
||||
"sheets_processed": result.get("sheets_processed", 0),
|
||||
"sheets_skipped": result.get("sheets_skipped", 0),
|
||||
"total_records": result.get("total_records", 0),
|
||||
"records_with_link": result.get("records_with_link", 0),
|
||||
"records_synced": result.get("records_synced", 0),
|
||||
"records_updated": result.get("records_updated", 0),
|
||||
"records_failed": result.get("records_failed", 0),
|
||||
},
|
||||
success=result.get("success", False),
|
||||
error_message=result.get("error_message"),
|
||||
extra={"source": "task2_run_once_manual"},
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
if started_sync_here and logger.get_active_sync_id():
|
||||
logger.end_sync_with_stats(
|
||||
stats={},
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
extra={
|
||||
"source": "task2_run_once_manual",
|
||||
"exception_type": type(e).__name__,
|
||||
},
|
||||
)
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 任务二同步服务测试 ===\n")
|
||||
print("请使用 main.py 或 scheduler.py 运行同步服务")
|
||||
print("或使用 test_phase4.py 进行测试")
|
||||
381
src2/tapd_api.py
Normal file
381
src2/tapd_api.py
Normal file
@ -0,0 +1,381 @@
|
||||
"""
|
||||
TAPD API调用模块(任务二专用)
|
||||
负责与TAPD Open API交互,查询需求(Story)信息
|
||||
|
||||
与任务一的区别:
|
||||
- 任务一:创建和管理Bug单
|
||||
- 任务二:查询需求(Story)状态信息
|
||||
"""
|
||||
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
from typing import Dict, Optional, Any
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
# 导入任务二专用的日志模块
|
||||
from src2.logger import get_task2_logger
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 自定义异常类
|
||||
# ============================================================
|
||||
class StoryNotFoundException(Exception):
|
||||
"""当TAPD需求(Story)不存在时抛出的异常"""
|
||||
pass
|
||||
|
||||
|
||||
# TAPD状态值映射表
|
||||
# 将API返回的状态代码转换为中文显示文本
|
||||
STATUS_MAPPING = {
|
||||
"status_5": "进行中",
|
||||
"status_7": "未开始",
|
||||
"status_8": "已完成",
|
||||
"status_9": "待验收",
|
||||
"status_10": "联调",
|
||||
"status_12": "取消",
|
||||
"status_13": "待评审",
|
||||
}
|
||||
|
||||
# 需求终态列表(这些状态不需要持续同步)
|
||||
TERMINAL_STATUSES = ['已完成', '取消']
|
||||
|
||||
|
||||
def map_status(status_code: str) -> str:
|
||||
"""
|
||||
将TAPD状态代码转换为中文显示文本
|
||||
|
||||
Args:
|
||||
status_code: TAPD API返回的状态代码(如 "status_5")
|
||||
|
||||
Returns:
|
||||
str: 中文显示文本(如 "进行中"),未知状态返回原始值
|
||||
"""
|
||||
if not status_code:
|
||||
return "未知"
|
||||
return STATUS_MAPPING.get(status_code, status_code)
|
||||
|
||||
|
||||
class TAPDStoryApi:
|
||||
"""TAPD需求API封装类(任务二专用)"""
|
||||
|
||||
# TAPD API基础URL(与任务一相同)
|
||||
BASE_URL = "https://tapd-api.bilibili.co/tapd"
|
||||
|
||||
# 发布计划字段名称
|
||||
PLAN_FIELD_NAME = "release_id"
|
||||
|
||||
def __init__(self, workspace_id: str, test_mode: bool = False):
|
||||
"""
|
||||
初始化TAPD Story API
|
||||
|
||||
Args:
|
||||
workspace_id: TAPD项目ID
|
||||
test_mode: 是否启用测试模式(显示API请求和响应)
|
||||
|
||||
Raises:
|
||||
ValueError: 环境变量未设置时抛出
|
||||
"""
|
||||
self.workspace_id = workspace_id
|
||||
self.test_mode = test_mode
|
||||
self.session = requests.Session()
|
||||
|
||||
# 从环境变量读取认证信息(与任务一共用)
|
||||
self.api_user = os.environ.get('TAPD_API_USER')
|
||||
self.api_password = os.environ.get('TAPD_API_PASSWORD')
|
||||
|
||||
if not self.api_user or not self.api_password:
|
||||
raise ValueError(
|
||||
"TAPD认证信息未设置。请设置环境变量:\n"
|
||||
" - TAPD_API_USER\n"
|
||||
" - TAPD_API_PASSWORD"
|
||||
)
|
||||
|
||||
# 设置Basic Auth
|
||||
self.auth = HTTPBasicAuth(self.api_user, self.api_password)
|
||||
|
||||
# 初始化任务二专用的日志记录器
|
||||
self.logger = get_task2_logger()
|
||||
|
||||
# 计划字段映射缓存(ID -> 中文名称)
|
||||
self._plan_mapping = None
|
||||
|
||||
print(f" ✓ TAPD Story API初始化完成 (workspace_id: {workspace_id})")
|
||||
if test_mode:
|
||||
print(f" ⚠ 测试模式已启用:将显示所有API调用的详细信息")
|
||||
|
||||
def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Dict:
|
||||
"""
|
||||
发起TAPD API GET请求的通用方法(支持429错误重试)
|
||||
|
||||
Args:
|
||||
endpoint: API端点(如 "stories")
|
||||
params: URL查询参数
|
||||
|
||||
Returns:
|
||||
Dict: API响应数据
|
||||
|
||||
Raises:
|
||||
RuntimeError: API调用失败时抛出
|
||||
"""
|
||||
url = f"{self.BASE_URL}/{endpoint}"
|
||||
|
||||
# 准备日志记录的请求数据
|
||||
log_request_data = {
|
||||
"url": url,
|
||||
"method": "GET",
|
||||
"params": params,
|
||||
"auth_user": self.api_user
|
||||
}
|
||||
|
||||
# 测试模式:显示请求信息
|
||||
if self.test_mode:
|
||||
print("\n" + "=" * 60)
|
||||
print(f"【测试模式】TAPD API调用: {endpoint}")
|
||||
print("=" * 60)
|
||||
print(f"请求URL: {url}")
|
||||
if params:
|
||||
print(f"URL参数:")
|
||||
for key, value in params.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 429错误重试逻辑:最多重试1次
|
||||
max_retries = 1
|
||||
retry_count = 0
|
||||
|
||||
while retry_count <= max_retries:
|
||||
try:
|
||||
response = self.session.get(
|
||||
url,
|
||||
params=params,
|
||||
auth=self.auth,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# 测试模式:显示响应信息
|
||||
if self.test_mode:
|
||||
print(f"\n响应状态码: {response.status_code}")
|
||||
try:
|
||||
import json
|
||||
result = response.json()
|
||||
print(f"响应数据:")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
except:
|
||||
print(f"响应内容: {response.text[:500]}")
|
||||
print("=" * 60)
|
||||
|
||||
# 检查是否是429错误(在raise_for_status之前检查)
|
||||
if response.status_code == 429:
|
||||
if retry_count < max_retries:
|
||||
retry_count += 1
|
||||
wait_seconds = 60
|
||||
print(f"\n⚠️ 触发TAPD API限流 (429 Too Many Requests)")
|
||||
print(f" [开始等待] 等待 {wait_seconds} 秒后重试... (第 {retry_count}/{max_retries} 次重试)")
|
||||
print(f" [重要] 在等待期间,代码会阻塞在这里,不会继续处理其他记录")
|
||||
|
||||
# 记录429错误日志
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data={"status_code": 429, "retry_count": retry_count},
|
||||
success=False,
|
||||
error_message=f"429 Too Many Requests, 等待{wait_seconds}秒后重试"
|
||||
)
|
||||
|
||||
time.sleep(wait_seconds)
|
||||
print(f" [等待结束] 开始重试请求...")
|
||||
continue # 重试
|
||||
else:
|
||||
# 已达到最大重试次数,抛出异常
|
||||
error_msg = "TAPD API限流 (429 Too Many Requests),重试后仍然失败"
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data={"status_code": 429, "retry_count": retry_count},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# 对于非429错误,调用raise_for_status检查HTTP状态
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# 检查TAPD API返回的状态
|
||||
if result.get('status') != 1:
|
||||
error_msg = result.get('info', '未知错误')
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data=result,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
raise RuntimeError(f"TAPD API调用失败: {error_msg}")
|
||||
|
||||
# 记录成功日志
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data=result,
|
||||
success=True
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = f"TAPD API请求超时: {endpoint}"
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
# 如果是HTTPError且状态码是429,由上面的status_code检查处理
|
||||
# 这里不应该到达,因为429在response.status_code检查时已处理
|
||||
error_msg = f"TAPD API请求失败: {e}"
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data={},
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# 理论上不会到达这里
|
||||
raise RuntimeError("TAPD API请求失败:未知错误")
|
||||
|
||||
def get_story(self, story_id: str) -> Dict:
|
||||
"""
|
||||
获取需求详情
|
||||
|
||||
Args:
|
||||
story_id: 需求ID
|
||||
|
||||
Returns:
|
||||
Dict: 需求详细信息
|
||||
|
||||
Raises:
|
||||
RuntimeError: 获取失败时抛出
|
||||
"""
|
||||
params = {
|
||||
'workspace_id': self.workspace_id,
|
||||
'id': story_id
|
||||
}
|
||||
|
||||
result = self._make_request("stories", params=params)
|
||||
|
||||
# TAPD API返回格式: {"status": 1, "data": [{"Story": {...}}]}
|
||||
data = result.get('data', [])
|
||||
|
||||
if not isinstance(data, list) or len(data) == 0:
|
||||
raise StoryNotFoundException(f"未找到需求: {story_id}")
|
||||
|
||||
# 取第一个元素
|
||||
first_item = data[0]
|
||||
|
||||
# 提取Story对象
|
||||
if isinstance(first_item, dict) and 'Story' in first_item:
|
||||
story_info = first_item['Story']
|
||||
else:
|
||||
raise RuntimeError(f"API返回数据格式异常: {first_item}")
|
||||
|
||||
if not story_info:
|
||||
raise StoryNotFoundException(f"未找到需求: {story_id}")
|
||||
|
||||
# 转换状态为中文
|
||||
raw_status = story_info.get('status', '')
|
||||
story_info['raw_status'] = raw_status
|
||||
story_info['status'] = map_status(raw_status)
|
||||
|
||||
return story_info
|
||||
|
||||
def get_story_url(self, story_id: str) -> str:
|
||||
"""
|
||||
生成需求的访问URL
|
||||
|
||||
Args:
|
||||
story_id: 需求ID
|
||||
|
||||
Returns:
|
||||
str: 需求的访问URL
|
||||
"""
|
||||
return f"https://www.tapd.cn/{self.workspace_id}/prong/stories/view/{story_id}"
|
||||
|
||||
def get_story_fields_info(self) -> Dict:
|
||||
"""
|
||||
获取需求所有字段及候选值
|
||||
|
||||
Returns:
|
||||
Dict: 字段信息,包含各字段的名称、选项等
|
||||
|
||||
Raises:
|
||||
RuntimeError: 获取失败时抛出
|
||||
"""
|
||||
params = {
|
||||
'workspace_id': self.workspace_id
|
||||
}
|
||||
|
||||
result = self._make_request("stories/get_fields_info", params=params)
|
||||
return result.get('data', {})
|
||||
|
||||
def get_plan_mapping(self) -> Dict[str, str]:
|
||||
"""
|
||||
获取发布计划字段的ID到中文名称映射
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 发布计划ID到中文名称的映射
|
||||
例如: {"1010104801000069739": "v2test", ...}
|
||||
"""
|
||||
# 获取字段信息
|
||||
fields_info = self.get_story_fields_info()
|
||||
|
||||
# 提取计划字段的options
|
||||
plan_field = fields_info.get(self.PLAN_FIELD_NAME, {})
|
||||
options = plan_field.get('options', {})
|
||||
|
||||
# 缓存映射
|
||||
self._plan_mapping = options
|
||||
|
||||
if self.test_mode:
|
||||
print(f"\n【测试模式】计划字段映射:")
|
||||
for plan_id, plan_name in options.items():
|
||||
print(f" {plan_id} -> {plan_name}")
|
||||
|
||||
return options
|
||||
|
||||
def map_plan_id_to_name(self, plan_id: str) -> str:
|
||||
"""
|
||||
将发布计划ID转换为中文名称
|
||||
|
||||
Args:
|
||||
plan_id: 发布计划ID(如 "1010104801000069739")
|
||||
|
||||
Returns:
|
||||
str: 中文名称(如 "v2test"),未找到则返回空字符串
|
||||
"""
|
||||
if not plan_id or plan_id == "0":
|
||||
return ""
|
||||
|
||||
# 如果映射未初始化,先获取
|
||||
if self._plan_mapping is None:
|
||||
self.get_plan_mapping()
|
||||
|
||||
return self._plan_mapping.get(plan_id, "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== TAPD Story API 测试 ===\n")
|
||||
print("请使用 test_phase2.py 进行完整测试")
|
||||
|
||||
210
src2/test_phase2.py
Normal file
210
src2/test_phase2.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""
|
||||
任务二第二阶段验证脚本
|
||||
测试链接解析和TAPD API功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src2.link_parser import parse_tapd_link, extract_story_id, is_valid_tapd_link
|
||||
from src2.tapd_api import TAPDStoryApi, map_status, STATUS_MAPPING
|
||||
|
||||
|
||||
def test_link_parser():
|
||||
"""测试链接解析功能"""
|
||||
print("=" * 60)
|
||||
print("测试1: 链接解析器")
|
||||
print("=" * 60)
|
||||
|
||||
test_cases = [
|
||||
# 格式一:列表页弹窗链接
|
||||
(
|
||||
"https://www.tapd.cn/tapd_fe/58335167/story/list?dialog_preview_id=story_1158335167001044388",
|
||||
True,
|
||||
"1158335167001044388",
|
||||
"dialog"
|
||||
),
|
||||
# 格式二:详情页链接
|
||||
(
|
||||
"https://www.tapd.cn/58335167/prong/stories/view/1158335167001044388",
|
||||
True,
|
||||
"1158335167001044388",
|
||||
"view"
|
||||
),
|
||||
# 无效链接:Bug链接
|
||||
(
|
||||
"https://www.tapd.cn/58335167/bugtrace/bugs/view/123456",
|
||||
False,
|
||||
None,
|
||||
"unknown"
|
||||
),
|
||||
# 无效链接:其他网站
|
||||
(
|
||||
"https://www.google.com",
|
||||
False,
|
||||
None,
|
||||
"unknown"
|
||||
),
|
||||
# 空链接
|
||||
(
|
||||
"",
|
||||
False,
|
||||
None,
|
||||
"unknown"
|
||||
),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for i, (url, expected_success, expected_id, expected_type) in enumerate(test_cases, 1):
|
||||
success, result, link_type = parse_tapd_link(url)
|
||||
|
||||
# 检查结果
|
||||
if success == expected_success and link_type == expected_type:
|
||||
if success and result == expected_id:
|
||||
print(f" [{i}] PASS: {url[:50]}...")
|
||||
passed += 1
|
||||
elif not success:
|
||||
print(f" [{i}] PASS: 正确识别无效链接")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" [{i}] FAIL: 单号不匹配 (期望={expected_id}, 实际={result})")
|
||||
failed += 1
|
||||
else:
|
||||
print(f" [{i}] FAIL: {url[:50]}...")
|
||||
print(f" 期望: success={expected_success}, type={expected_type}")
|
||||
print(f" 实际: success={success}, type={link_type}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n链接解析测试结果: {passed} 通过, {failed} 失败")
|
||||
return failed == 0
|
||||
|
||||
|
||||
def test_status_mapping():
|
||||
"""测试状态映射功能"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试2: 状态映射")
|
||||
print("=" * 60)
|
||||
|
||||
test_cases = [
|
||||
("status_5", "进行中"),
|
||||
("status_7", "未开始"),
|
||||
("status_8", "已完成"),
|
||||
("status_9", "待验收"),
|
||||
("status_10", "联调"),
|
||||
("status_12", "取消"),
|
||||
("status_13", "待评审"),
|
||||
("status_99", "status_99"), # 未知状态返回原值
|
||||
("", "未知"),
|
||||
(None, "未知"),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for status_code, expected in test_cases:
|
||||
result = map_status(status_code)
|
||||
if result == expected:
|
||||
print(f" PASS: {status_code} -> {result}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL: {status_code} -> {result} (期望: {expected})")
|
||||
failed += 1
|
||||
|
||||
print(f"\n状态映射测试结果: {passed} 通过, {failed} 失败")
|
||||
return failed == 0
|
||||
|
||||
|
||||
def test_tapd_api(story_id: str = None):
|
||||
"""测试TAPD API功能"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试3: TAPD API")
|
||||
print("=" * 60)
|
||||
|
||||
# 从配置读取workspace_id
|
||||
from src2.config import Task2ConfigManager
|
||||
config = Task2ConfigManager()
|
||||
tapd_config = config.get_tapd_config()
|
||||
workspace_id = tapd_config['workspace_id']
|
||||
|
||||
print(f" workspace_id: {workspace_id}")
|
||||
|
||||
try:
|
||||
# 初始化API
|
||||
api = TAPDStoryApi(workspace_id, test_mode=True)
|
||||
print(" ✓ API初始化成功")
|
||||
except ValueError as e:
|
||||
print(f" ✗ API初始化失败: {e}")
|
||||
return False
|
||||
|
||||
if not story_id:
|
||||
print("\n 跳过需求查询测试(未提供story_id)")
|
||||
print(" 用法: python test_phase2.py <story_id>")
|
||||
return True
|
||||
|
||||
# 测试获取需求详情
|
||||
print(f"\n 测试获取需求: {story_id}")
|
||||
try:
|
||||
story = api.get_story(story_id)
|
||||
print(f" ✓ 获取成功")
|
||||
print(f" - ID: {story.get('id')}")
|
||||
print(f" - 名称: {story.get('name')}")
|
||||
print(f" - 状态: {story.get('status')}")
|
||||
print(f" - 处理人: {story.get('owner')}")
|
||||
print(f" - 预计开始: {story.get('begin')}")
|
||||
print(f" - 预计结束: {story.get('due')}")
|
||||
return True
|
||||
except RuntimeError as e:
|
||||
print(f" ✗ 获取失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=" * 60)
|
||||
print("任务二第二阶段验证")
|
||||
print("=" * 60)
|
||||
|
||||
# 获取命令行参数
|
||||
story_id = None
|
||||
if len(sys.argv) > 1:
|
||||
story_id = sys.argv[1]
|
||||
|
||||
results = []
|
||||
|
||||
# 测试1: 链接解析
|
||||
results.append(("链接解析", test_link_parser()))
|
||||
|
||||
# 测试2: 状态映射
|
||||
results.append(("状态映射", test_status_mapping()))
|
||||
|
||||
# 测试3: TAPD API
|
||||
results.append(("TAPD API", test_tapd_api(story_id)))
|
||||
|
||||
# 汇总结果
|
||||
print("\n" + "=" * 60)
|
||||
print("验收结果汇总")
|
||||
print("=" * 60)
|
||||
|
||||
all_passed = True
|
||||
for name, passed in results:
|
||||
status = "✓ PASS" if passed else "✗ FAIL"
|
||||
print(f" {status}: {name}")
|
||||
if not passed:
|
||||
all_passed = False
|
||||
|
||||
if all_passed:
|
||||
print("\n所有测试通过!第二阶段验收完成。")
|
||||
else:
|
||||
print("\n部分测试失败,请检查。")
|
||||
|
||||
return 0 if all_passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
330
src2/test_phase3.py
Normal file
330
src2/test_phase3.py
Normal file
@ -0,0 +1,330 @@
|
||||
"""
|
||||
第三阶段验证脚本:智能表格读写功能测试
|
||||
|
||||
验证项:
|
||||
1. 字段检测 - 检查必要字段是否存在
|
||||
2. 记录读取 - 获取所有记录
|
||||
3. TAPD链接提取 - 从记录中提取链接并解析
|
||||
4. 数据回写测试 - 构造更新记录(可选执行)
|
||||
|
||||
用法:
|
||||
python src2/test_phase3.py # 只读测试(不修改数据)
|
||||
python src2/test_phase3.py --write # 包含回写测试(会修改一条记录)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.token_manager import TokenManager
|
||||
from src2.config import Task2ConfigManager
|
||||
from src2.smartsheet_sync import (
|
||||
SmartSheetSync,
|
||||
REQUIRED_FIELDS,
|
||||
FIELD_TAPD_LINK,
|
||||
FIELD_TAPD_STATUS,
|
||||
FIELD_OWNER,
|
||||
FIELD_BEGIN_DATE,
|
||||
FIELD_DUE_DATE,
|
||||
)
|
||||
|
||||
|
||||
def print_separator(title: str = ""):
|
||||
"""打印分隔线"""
|
||||
print("\n" + "=" * 60)
|
||||
if title:
|
||||
print(f" {title}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def test_field_detection(sync: SmartSheetSync, sheet_id: str) -> bool:
|
||||
"""
|
||||
测试1:字段检测
|
||||
|
||||
Returns:
|
||||
bool: 测试是否通过
|
||||
"""
|
||||
print_separator("测试1:字段检测")
|
||||
|
||||
print(f"必要字段列表:")
|
||||
for field in REQUIRED_FIELDS:
|
||||
print(f" - {field}")
|
||||
print()
|
||||
|
||||
# 获取字段信息
|
||||
fields = sync.api.get_fields(sheet_id)
|
||||
|
||||
# 检查必要字段
|
||||
all_present, missing_fields, field_mapping = sync.check_required_fields(fields)
|
||||
|
||||
print(f"\n字段映射结果:")
|
||||
for field_name in REQUIRED_FIELDS:
|
||||
field_id = field_mapping.get(field_name, "未找到")
|
||||
status = "✓" if field_name in field_mapping else "✗"
|
||||
print(f" {status} {field_name}: {field_id}")
|
||||
|
||||
if all_present:
|
||||
print(f"\n✓ 测试通过:所有必要字段都存在")
|
||||
return True
|
||||
else:
|
||||
print(f"\n✗ 测试失败:缺少字段 {missing_fields}")
|
||||
return False
|
||||
|
||||
|
||||
def test_record_reading(sync: SmartSheetSync, sheet_id: str) -> bool:
|
||||
"""
|
||||
测试2:记录读取
|
||||
|
||||
Returns:
|
||||
bool: 测试是否通过
|
||||
"""
|
||||
print_separator("测试2:记录读取")
|
||||
|
||||
try:
|
||||
records = sync.get_all_records(sheet_id)
|
||||
|
||||
print(f"\n记录读取结果:")
|
||||
print(f" - 总记录数: {len(records)}")
|
||||
|
||||
if len(records) > 0:
|
||||
print(f"\n第一条记录示例:")
|
||||
first_record = records[0]
|
||||
record_id = first_record.get('record_id', 'N/A')
|
||||
print(f" - record_id: {record_id}")
|
||||
|
||||
# 显示部分字段值
|
||||
values = first_record.get('values', {})
|
||||
print(f" - 字段数量: {len(values)}")
|
||||
|
||||
# 显示前5个字段
|
||||
for i, (key, value) in enumerate(values.items()):
|
||||
if i >= 5:
|
||||
print(f" - ... 还有 {len(values) - 5} 个字段")
|
||||
break
|
||||
print(f" - {key}: {str(value)[:50]}...")
|
||||
|
||||
print(f"\n✓ 测试通过:成功读取 {len(records)} 条记录")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败:{e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_tapd_link_extraction(sync: SmartSheetSync, sheet_id: str) -> bool:
|
||||
"""
|
||||
测试3:TAPD链接提取
|
||||
|
||||
Returns:
|
||||
bool: 测试是否通过
|
||||
"""
|
||||
print_separator("测试3:TAPD链接提取")
|
||||
|
||||
try:
|
||||
records_with_link = sync.get_records_with_tapd_link(sheet_id)
|
||||
|
||||
print(f"\n链接提取结果:")
|
||||
print(f" - 包含链接的记录数: {len(records_with_link)}")
|
||||
|
||||
# 统计解析结果
|
||||
success_count = sum(1 for r in records_with_link if r["parse_success"])
|
||||
fail_count = len(records_with_link) - success_count
|
||||
|
||||
print(f" - 解析成功: {success_count}")
|
||||
print(f" - 解析失败: {fail_count}")
|
||||
|
||||
# 显示前3条成功解析的记录
|
||||
success_records = [r for r in records_with_link if r["parse_success"]]
|
||||
if success_records:
|
||||
print(f"\n成功解析的记录示例(最多3条):")
|
||||
for i, record_info in enumerate(success_records[:3]):
|
||||
print(f"\n [{i+1}] record_id: {record_info['record_id']}")
|
||||
print(f" 链接: {record_info['tapd_link'][:60]}...")
|
||||
print(f" 单号: {record_info['story_id']}")
|
||||
print(f" 类型: {record_info.get('link_type', 'N/A')}")
|
||||
|
||||
# 显示解析失败的记录
|
||||
fail_records = [r for r in records_with_link if not r["parse_success"]]
|
||||
if fail_records:
|
||||
print(f"\n解析失败的记录(最多3条):")
|
||||
for i, record_info in enumerate(fail_records[:3]):
|
||||
print(f"\n [{i+1}] record_id: {record_info['record_id']}")
|
||||
print(f" 链接: {record_info['tapd_link'][:60]}...")
|
||||
print(f" 错误: {record_info.get('parse_error', 'N/A')}")
|
||||
|
||||
print(f"\n✓ 测试通过:成功提取并解析TAPD链接")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败:{e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_update_record_structure(sync: SmartSheetSync) -> bool:
|
||||
"""
|
||||
测试4:更新记录结构构造(不实际写入)
|
||||
|
||||
Returns:
|
||||
bool: 测试是否通过
|
||||
"""
|
||||
print_separator("测试4:更新记录结构构造")
|
||||
|
||||
try:
|
||||
# 构造一个测试更新记录
|
||||
test_record = sync.build_update_record(
|
||||
record_id="test_record_id_123",
|
||||
status="进行中",
|
||||
owner="张三",
|
||||
begin_date="2025-01-01",
|
||||
due_date="2025-01-15"
|
||||
)
|
||||
|
||||
print(f"构造的更新记录结构:")
|
||||
print(f" record_id: {test_record['record_id']}")
|
||||
print(f" values:")
|
||||
for key, value in test_record['values'].items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 验证结构
|
||||
assert 'record_id' in test_record
|
||||
assert 'values' in test_record
|
||||
assert FIELD_TAPD_STATUS in test_record['values']
|
||||
assert FIELD_OWNER in test_record['values']
|
||||
assert FIELD_BEGIN_DATE in test_record['values']
|
||||
assert FIELD_DUE_DATE in test_record['values']
|
||||
|
||||
print(f"\n✓ 测试通过:更新记录结构正确")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败:{e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_multi_sheet_support(sync: SmartSheetSync) -> bool:
|
||||
"""
|
||||
测试5:多子表支持
|
||||
|
||||
Returns:
|
||||
bool: 测试是否通过
|
||||
"""
|
||||
print_separator("测试5:多子表支持")
|
||||
|
||||
try:
|
||||
# 获取所有子表
|
||||
sheet_list = sync.api.get_sheet_list()
|
||||
|
||||
print(f"子表列表:")
|
||||
for i, sheet in enumerate(sheet_list):
|
||||
sheet_id = sheet.get('sheet_id', 'N/A')
|
||||
title = sheet.get('title', 'N/A')
|
||||
print(f" [{i+1}] {title} (ID: {sheet_id})")
|
||||
|
||||
print(f"\n共找到 {len(sheet_list)} 个子表")
|
||||
|
||||
if len(sheet_list) == 0:
|
||||
print(f"\n⚠ 警告:没有找到子表")
|
||||
return False
|
||||
|
||||
print(f"\n✓ 测试通过:成功获取子表列表")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败:{e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description="第三阶段验证脚本")
|
||||
parser.add_argument("--write", action="store_true",
|
||||
help="执行回写测试(会修改数据)")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print(" 任务二 第三阶段验证:智能表格读写功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 加载配置
|
||||
print("\n[初始化] 加载配置...")
|
||||
config = Task2ConfigManager()
|
||||
smartsheet_config = config.get_smartsheet_config()
|
||||
docid = smartsheet_config.get('docid')
|
||||
print(f" docid: {docid[:20]}...")
|
||||
|
||||
# 2. 获取token
|
||||
print("\n[初始化] 获取access_token...")
|
||||
token_manager = TokenManager()
|
||||
access_token = token_manager.get_token()
|
||||
print(f" token: {access_token[:20]}...")
|
||||
|
||||
# 3. 初始化同步模块
|
||||
print("\n[初始化] 初始化SmartSheetSync...")
|
||||
sync = SmartSheetSync(access_token, docid, test_mode=False)
|
||||
print(" ✓ 初始化完成")
|
||||
|
||||
# 4. 获取子表列表
|
||||
sheet_list = sync.api.get_sheet_list()
|
||||
if not sheet_list:
|
||||
print("\n✗ 错误:没有找到子表")
|
||||
return 1
|
||||
|
||||
# 使用第一个子表进行测试
|
||||
first_sheet = sheet_list[0]
|
||||
sheet_id = first_sheet.get('sheet_id')
|
||||
sheet_title = first_sheet.get('title')
|
||||
print(f"\n使用子表进行测试: {sheet_title}")
|
||||
|
||||
# 5. 运行测试
|
||||
results = []
|
||||
|
||||
# 测试5先执行(多子表支持)
|
||||
results.append(("多子表支持", test_multi_sheet_support(sync)))
|
||||
|
||||
# 测试1:字段检测
|
||||
results.append(("字段检测", test_field_detection(sync, sheet_id)))
|
||||
|
||||
# 测试2:记录读取
|
||||
results.append(("记录读取", test_record_reading(sync, sheet_id)))
|
||||
|
||||
# 测试3:TAPD链接提取
|
||||
results.append(("TAPD链接提取", test_tapd_link_extraction(sync, sheet_id)))
|
||||
|
||||
# 测试4:更新记录结构
|
||||
results.append(("更新记录结构", test_update_record_structure(sync)))
|
||||
|
||||
# 6. 输出测试结果汇总
|
||||
print_separator("测试结果汇总")
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
for name, result in results:
|
||||
status = "✓ 通过" if result else "✗ 失败"
|
||||
print(f" {status}: {name}")
|
||||
if result:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print(f"\n总计: {passed} 通过, {failed} 失败")
|
||||
|
||||
if failed == 0:
|
||||
print("\n" + "=" * 60)
|
||||
print(" ✓ 第三阶段验证全部通过!")
|
||||
print("=" * 60)
|
||||
return 0
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print(" ✗ 部分测试失败,请检查")
|
||||
print("=" * 60)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
160
src2/test_phase4.py
Normal file
160
src2/test_phase4.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""
|
||||
任务二第四阶段验证脚本
|
||||
验证同步服务与主程序的完整功能
|
||||
|
||||
验证项:
|
||||
1. 同步服务初始化
|
||||
2. 单次同步流程
|
||||
3. 调度器初始化
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def test_sync_service_init():
|
||||
"""测试1: 同步服务初始化"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试1: 同步服务初始化")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from src2.sync_service import SyncService
|
||||
from src2.config import Task2ConfigManager
|
||||
|
||||
# 初始化配置
|
||||
config_manager = Task2ConfigManager()
|
||||
print("✓ 配置管理器初始化成功")
|
||||
|
||||
# 初始化同步服务(测试模式)
|
||||
service = SyncService(
|
||||
config_manager=config_manager,
|
||||
test_mode=True
|
||||
)
|
||||
print("✓ 同步服务初始化成功")
|
||||
|
||||
# 验证属性
|
||||
assert service.workspace_id is not None
|
||||
assert service.docid is not None
|
||||
assert service.access_token is not None
|
||||
print("✓ 服务属性验证通过")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_sync_once():
|
||||
"""测试2: 单次同步流程"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试2: 单次同步流程")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from src2.sync_service import run_once
|
||||
|
||||
# 执行一次同步
|
||||
print("正在执行同步...")
|
||||
result = run_once(test_mode=False)
|
||||
|
||||
# 验证结果结构
|
||||
assert 'success' in result
|
||||
assert 'sheets_processed' in result
|
||||
assert 'records_with_link' in result
|
||||
assert 'records_synced' in result
|
||||
print("✓ 结果结构验证通过")
|
||||
|
||||
# 打印结果摘要
|
||||
print(f"\n同步结果:")
|
||||
print(f" 成功: {result['success']}")
|
||||
print(f" 处理子表: {result['sheets_processed']} 个")
|
||||
print(f" 跳过子表: {result['sheets_skipped']} 个")
|
||||
print(f" 包含链接: {result['records_with_link']} 条")
|
||||
print(f" 同步成功: {result['records_synced']} 条")
|
||||
print(f" 需要更新: {result['records_updated']} 条")
|
||||
|
||||
return result['success']
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_scheduler_init():
|
||||
"""测试3: 调度器初始化"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试3: 调度器初始化")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from src2.scheduler import Task2Scheduler
|
||||
|
||||
# 初始化调度器(不启动)
|
||||
scheduler = Task2Scheduler(verbose=False)
|
||||
print("✓ 调度器初始化成功")
|
||||
|
||||
# 验证属性
|
||||
assert scheduler.config is not None
|
||||
assert scheduler.sync_interval > 0
|
||||
print(f"✓ 同步间隔: {scheduler.sync_interval} 分钟")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("\n" + "=" * 60)
|
||||
print("任务二第四阶段验证")
|
||||
print("=" * 60)
|
||||
|
||||
results = {}
|
||||
|
||||
# 测试1: 同步服务初始化
|
||||
results['sync_service_init'] = test_sync_service_init()
|
||||
|
||||
# 测试2: 单次同步流程
|
||||
results['sync_once'] = test_sync_once()
|
||||
|
||||
# 测试3: 调度器初始化
|
||||
results['scheduler_init'] = test_scheduler_init()
|
||||
|
||||
# 打印总结
|
||||
print("\n" + "=" * 60)
|
||||
print("验证结果总结")
|
||||
print("=" * 60)
|
||||
|
||||
all_passed = True
|
||||
for test_name, passed in results.items():
|
||||
status = "✓ 通过" if passed else "✗ 失败"
|
||||
print(f" {test_name}: {status}")
|
||||
if not passed:
|
||||
all_passed = False
|
||||
|
||||
print("=" * 60)
|
||||
if all_passed:
|
||||
print("所有测试通过!第四阶段验收完成。")
|
||||
else:
|
||||
print("部分测试失败,请检查错误信息。")
|
||||
print("=" * 60)
|
||||
|
||||
return 0 if all_passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
220
src2/test_setup.py
Normal file
220
src2/test_setup.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""
|
||||
任务二第一阶段验证脚本
|
||||
验证基础框架搭建是否正确
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
|
||||
def test_directory_structure():
|
||||
"""测试1: 验证目录结构"""
|
||||
print("=" * 50)
|
||||
print("测试1: 验证目录结构")
|
||||
print("=" * 50)
|
||||
|
||||
src2_dir = project_root / "src2"
|
||||
logs2_dir = project_root / "logs2"
|
||||
config_file = project_root / "config" / "config_task2.ini"
|
||||
|
||||
results = []
|
||||
|
||||
# 检查 src2 目录
|
||||
if src2_dir.exists() and src2_dir.is_dir():
|
||||
print(f" [OK] src2/ 目录存在")
|
||||
results.append(True)
|
||||
else:
|
||||
print(f" [FAIL] src2/ 目录不存在")
|
||||
results.append(False)
|
||||
|
||||
# 检查 logs2 目录
|
||||
if logs2_dir.exists() and logs2_dir.is_dir():
|
||||
print(f" [OK] logs2/ 目录存在")
|
||||
results.append(True)
|
||||
else:
|
||||
print(f" [FAIL] logs2/ 目录不存在")
|
||||
results.append(False)
|
||||
|
||||
# 检查配置文件
|
||||
if config_file.exists():
|
||||
print(f" [OK] config/config_task2.ini 存在")
|
||||
results.append(True)
|
||||
else:
|
||||
print(f" [FAIL] config/config_task2.ini 不存在")
|
||||
results.append(False)
|
||||
|
||||
return all(results)
|
||||
|
||||
|
||||
def test_config_read():
|
||||
"""测试2: 验证配置文件读取"""
|
||||
print("\n" + "=" * 50)
|
||||
print("测试2: 验证配置文件读取")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
from src2.config import Task2ConfigManager
|
||||
config = Task2ConfigManager()
|
||||
|
||||
# 读取TAPD配置
|
||||
tapd_config = config.get_tapd_config()
|
||||
print(f" [OK] TAPD配置读取成功")
|
||||
print(f" workspace_id: {tapd_config['workspace_id']}")
|
||||
|
||||
# 读取SmartSheet配置
|
||||
smartsheet_config = config.get_smartsheet_config()
|
||||
print(f" [OK] SmartSheet配置读取成功")
|
||||
print(f" docid: {smartsheet_config['docid']}")
|
||||
|
||||
# 读取Schedule配置
|
||||
schedule_config = config.get_schedule_config()
|
||||
print(f" [OK] Schedule配置读取成功")
|
||||
print(f" sync_interval: {schedule_config['sync_interval']} 分钟")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" [FAIL] 配置读取失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_logger():
|
||||
"""测试3: 验证日志写入"""
|
||||
print("\n" + "=" * 50)
|
||||
print("测试3: 验证日志写入到 logs2/")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
from src2.logger import get_task2_logger, TASK2_LOG_DIR
|
||||
|
||||
logger = get_task2_logger()
|
||||
|
||||
# 写入测试日志
|
||||
logger.log_api_call(
|
||||
api_type="test",
|
||||
operation="task2/setup_test",
|
||||
request_data={"test": "验证脚本测试"},
|
||||
response_data={"status": "success"},
|
||||
success=True
|
||||
)
|
||||
|
||||
# 检查日志文件是否创建
|
||||
log_file = logger._get_today_log_file()
|
||||
if log_file.exists():
|
||||
print(f" [OK] 日志写入成功")
|
||||
print(f" 日志目录: {TASK2_LOG_DIR}")
|
||||
print(f" 日志文件: {log_file.name}")
|
||||
return True
|
||||
else:
|
||||
print(f" [FAIL] 日志文件未创建")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" [FAIL] 日志测试失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_token_manager():
|
||||
"""测试4: 验证Token管理器复用"""
|
||||
print("\n" + "=" * 50)
|
||||
print("测试4: 验证Token管理器复用")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
from src.token_manager import TokenManager
|
||||
|
||||
# 创建TokenManager实例(使用默认缓存路径)
|
||||
token_manager = TokenManager()
|
||||
print(f" [OK] TokenManager导入成功")
|
||||
print(f" 缓存文件: {token_manager.cache_file_path}")
|
||||
|
||||
# 尝试获取token
|
||||
token = token_manager.get_token()
|
||||
print(f" [OK] Token获取成功")
|
||||
print(f" Token前20字符: {token[:20]}...")
|
||||
|
||||
return True
|
||||
except ValueError as e:
|
||||
print(f" [WARN] 环境变量未设置: {e}")
|
||||
print(f" 这不影响框架搭建,后续运行时需要设置")
|
||||
return True # 环境变量未设置不算失败
|
||||
except Exception as e:
|
||||
print(f" [FAIL] Token测试失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_smartsheet_api():
|
||||
"""测试5: 验证SmartSheetAPI复用"""
|
||||
print("\n" + "=" * 50)
|
||||
print("测试5: 验证SmartSheetAPI复用")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
from src.smartsheet import SmartSheetAPI
|
||||
from src.token_manager import TokenManager
|
||||
from src2.config import Task2ConfigManager
|
||||
|
||||
print(f" [OK] SmartSheetAPI导入成功")
|
||||
|
||||
# 获取配置
|
||||
config = Task2ConfigManager()
|
||||
docid = config.get_smartsheet_config()['docid']
|
||||
|
||||
# 获取token
|
||||
token_manager = TokenManager()
|
||||
token = token_manager.get_token()
|
||||
|
||||
# 创建SmartSheetAPI实例
|
||||
api = SmartSheetAPI(token, docid)
|
||||
print(f" [OK] SmartSheetAPI实例创建成功")
|
||||
print(f" docid: {docid}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" [FAIL] SmartSheetAPI测试失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""运行所有测试"""
|
||||
print("\n" + "=" * 50)
|
||||
print("任务二第一阶段验证")
|
||||
print("=" * 50)
|
||||
|
||||
results = {
|
||||
"目录结构": test_directory_structure(),
|
||||
"配置读取": test_config_read(),
|
||||
"日志写入": test_logger(),
|
||||
"Token管理": test_token_manager(),
|
||||
"SmartSheetAPI": test_smartsheet_api()
|
||||
}
|
||||
|
||||
# 汇总结果
|
||||
print("\n" + "=" * 50)
|
||||
print("验证结果汇总")
|
||||
print("=" * 50)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
for name, result in results.items():
|
||||
status = "[OK]" if result else "[FAIL]"
|
||||
print(f" {status} {name}")
|
||||
if result:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print(f"\n总计: {passed} 通过, {failed} 失败")
|
||||
|
||||
if failed == 0:
|
||||
print("\n第一阶段验收通过!")
|
||||
else:
|
||||
print("\n请检查失败项并修复")
|
||||
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
130
src2/test_update_records.py
Normal file
130
src2/test_update_records.py
Normal file
@ -0,0 +1,130 @@
|
||||
"""
|
||||
测试 update_records 功能的独立脚本
|
||||
用于调试智能表格的记录更新功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.token_manager import TokenManager
|
||||
|
||||
|
||||
def test_update_records():
|
||||
"""测试更新记录功能"""
|
||||
|
||||
print("=" * 60)
|
||||
print("测试 update_records 功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 获取 access_token
|
||||
print("\n[1/3] 获取 access_token...")
|
||||
try:
|
||||
token_manager = TokenManager()
|
||||
access_token = token_manager.get_token()
|
||||
print(f" ✓ access_token: {access_token[:20]}...(已隐藏)")
|
||||
except Exception as e:
|
||||
print(f" ✗ 获取失败: {e}")
|
||||
return False
|
||||
|
||||
# 2. 准备测试数据
|
||||
print("\n[2/3] 准备测试数据...")
|
||||
|
||||
BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/wedoc"
|
||||
|
||||
# 测试数据
|
||||
data = {
|
||||
"docid": "dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ",
|
||||
"sheet_id": "56YEeR",
|
||||
"key_type": "CELL_VALUE_KEY_TYPE_FIELD_TITLE",
|
||||
"records": [
|
||||
{
|
||||
"record_id": "rNrk6o",
|
||||
"values": {
|
||||
"TAPD状态": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "已完成"
|
||||
}
|
||||
],
|
||||
"处理人": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": ""
|
||||
}
|
||||
],
|
||||
"TAPD预计完成日期": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "2025-11-28"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
print(" ✓ 测试数据已准备")
|
||||
print(f" - docid: {data['docid'][:20]}...")
|
||||
print(f" - sheet_id: {data['sheet_id']}")
|
||||
print(f" - 记录数: {len(data['records'])}")
|
||||
print(f" - record_id: {data['records'][0]['record_id']}")
|
||||
|
||||
# 3. 发送请求
|
||||
print("\n[3/3] 发送 update_records 请求...")
|
||||
|
||||
# 构造完整URL(带debug=1)
|
||||
url = f"{BASE_URL}/smartsheet/update_records?access_token={access_token}&debug=1"
|
||||
|
||||
print(f"\n请求信息:")
|
||||
print(f" URL: {BASE_URL}/smartsheet/update_records?access_token=***&debug=1")
|
||||
print(f" Method: POST")
|
||||
print(f"\n请求数据:")
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
try:
|
||||
# 发送POST请求
|
||||
print("\n正在发送请求...")
|
||||
response = requests.post(url, json=data, timeout=30)
|
||||
|
||||
# 打印响应信息
|
||||
print(f"\n响应信息:")
|
||||
print(f" 状态码: {response.status_code}")
|
||||
print(f"\n响应数据:")
|
||||
result = response.json()
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
# 检查结果
|
||||
print("\n" + "=" * 60)
|
||||
if result.get('errcode', 0) == 0:
|
||||
print("✓ 测试成功!")
|
||||
updated_records = result.get('records', [])
|
||||
print(f" 更新记录数: {len(updated_records)}")
|
||||
return True
|
||||
else:
|
||||
print("✗ 测试失败!")
|
||||
print(f" 错误码: {result.get('errcode')}")
|
||||
print(f" 错误信息: {result.get('errmsg')}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 请求异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
success = test_update_records()
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
1
src3/__init__.py
Normal file
1
src3/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# 任务三:TAPD过期单推送
|
||||
171
src3/config.py
Normal file
171
src3/config.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""任务三配置管理"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.config import ConfigManager
|
||||
|
||||
|
||||
class Task3ConfigManager(ConfigManager):
|
||||
"""任务三配置管理器"""
|
||||
|
||||
GROUP_SECTION = "GroupPush"
|
||||
DEFAULT_GROUP_COUNT = 3
|
||||
|
||||
def __init__(self):
|
||||
project_root = Path(__file__).parent.parent
|
||||
config_path = project_root / "config3" / "config.ini"
|
||||
super().__init__(config_path)
|
||||
|
||||
def get_workspace_id(self) -> str:
|
||||
"""获取TAPD工作空间ID"""
|
||||
if not self.config.has_section("TAPD"):
|
||||
raise ValueError("配置文件缺少[TAPD]节")
|
||||
if not self.config.has_option("TAPD", "workspace_id"):
|
||||
raise ValueError("配置文件[TAPD]节缺少workspace_id配置项")
|
||||
workspace_id = self.config.get("TAPD", "workspace_id").strip()
|
||||
if not workspace_id:
|
||||
raise ValueError("workspace_id配置项不能为空")
|
||||
return workspace_id
|
||||
|
||||
def get_push_time(self) -> str:
|
||||
"""获取推送时间"""
|
||||
if not self.config.has_section("Schedule"):
|
||||
return "09:30"
|
||||
return self.config.get("Schedule", "push_time", fallback="09:30").strip()
|
||||
|
||||
def get_skip_weekend(self) -> bool:
|
||||
"""是否跳过周末"""
|
||||
if not self.config.has_section("Schedule"):
|
||||
return True
|
||||
return self.config.getboolean("Schedule", "skip_weekend", fallback=True)
|
||||
|
||||
def get_smartsheet_config(self) -> Dict[str, str]:
|
||||
"""获取智能表格配置(仅docid)"""
|
||||
if not self.config.has_section("Smartsheet"):
|
||||
raise ValueError("配置文件缺少[Smartsheet]节")
|
||||
if not self.config.has_option("Smartsheet", "docid"):
|
||||
raise ValueError("配置文件[Smartsheet]节缺少docid配置项")
|
||||
|
||||
docid = self.config.get("Smartsheet", "docid").strip()
|
||||
if not docid:
|
||||
raise ValueError("docid配置项不能为空")
|
||||
|
||||
return {"docid": docid}
|
||||
|
||||
def get_group_push_configs(self) -> List[Dict[str, str]]:
|
||||
"""读取多群配置(按顺序一一对应)"""
|
||||
section = self.GROUP_SECTION
|
||||
if not self.config.has_section(section):
|
||||
raise ValueError("配置文件缺少[GroupPush]节")
|
||||
|
||||
group_count = self.config.getint(section, "group_count", fallback=self.DEFAULT_GROUP_COUNT)
|
||||
if group_count <= 0:
|
||||
raise ValueError("group_count必须为正整数")
|
||||
|
||||
group_configs: List[Dict[str, str]] = []
|
||||
for idx in range(1, group_count + 1):
|
||||
name_key = f"group{idx}_name"
|
||||
title_key = f"group{idx}_sheet_title"
|
||||
webhook_key = f"group{idx}_webhook_url"
|
||||
|
||||
if not self.config.has_option(section, name_key):
|
||||
raise ValueError(f"[GroupPush]缺少{name_key}配置项")
|
||||
if not self.config.has_option(section, title_key):
|
||||
raise ValueError(f"[GroupPush]缺少{title_key}配置项")
|
||||
if not self.config.has_option(section, webhook_key):
|
||||
raise ValueError(f"[GroupPush]缺少{webhook_key}配置项")
|
||||
|
||||
group_name = self.config.get(section, name_key).strip()
|
||||
sheet_title = self.config.get(section, title_key).strip()
|
||||
webhook_url = self.config.get(section, webhook_key).strip()
|
||||
|
||||
if not group_name or not sheet_title or not webhook_url:
|
||||
raise ValueError(f"[GroupPush]第{idx}组配置不能为空")
|
||||
|
||||
group_configs.append(
|
||||
{
|
||||
"group_name": group_name,
|
||||
"sheet_title": sheet_title,
|
||||
"webhook_url": webhook_url,
|
||||
}
|
||||
)
|
||||
|
||||
return group_configs
|
||||
|
||||
def get_group_team_configs(self, logger=None) -> List[Dict]:
|
||||
"""加载多组成员配置并解析子表ID"""
|
||||
from src.token_manager import TokenManager
|
||||
from src.smartsheet import SmartSheetAPI
|
||||
|
||||
smartsheet_config = self.get_smartsheet_config()
|
||||
group_push_configs = self.get_group_push_configs()
|
||||
docid = smartsheet_config["docid"]
|
||||
|
||||
token_manager = TokenManager(logger=logger)
|
||||
access_token = token_manager.get_token()
|
||||
|
||||
api = SmartSheetAPI(access_token, docid)
|
||||
sheet_list = api.get_sheet_list()
|
||||
title_to_sheet_id = self._build_title_to_sheet_id(sheet_list)
|
||||
|
||||
group_team_configs: List[Dict] = []
|
||||
for group_config in group_push_configs:
|
||||
sheet_title = group_config["sheet_title"]
|
||||
sheet_id = title_to_sheet_id.get(sheet_title)
|
||||
if not sheet_id:
|
||||
raise ValueError(f"未找到成员配置子表: {sheet_title}")
|
||||
|
||||
records_result = api.get_records(sheet_id)
|
||||
records = records_result.get("records", [])
|
||||
|
||||
member_list: List[str] = []
|
||||
user_mapping: Dict[str, str] = {}
|
||||
|
||||
for record in records:
|
||||
tapd_name = api.get_field_value_by_title(record, "TAPD用户名")
|
||||
wework_id = api.get_field_value_by_title(record, "企微UserID")
|
||||
enabled = api.get_field_value_by_title(record, "是否启用")
|
||||
|
||||
tapd_name = str(tapd_name).strip() if tapd_name else ""
|
||||
wework_id = str(wework_id).strip() if wework_id else ""
|
||||
|
||||
if not tapd_name or not self._is_enabled(enabled):
|
||||
continue
|
||||
|
||||
if tapd_name not in member_list:
|
||||
member_list.append(tapd_name)
|
||||
if wework_id:
|
||||
user_mapping[tapd_name] = wework_id
|
||||
|
||||
group_team_configs.append(
|
||||
{
|
||||
"group_name": group_config["group_name"],
|
||||
"sheet_title": sheet_title,
|
||||
"sheet_id": sheet_id,
|
||||
"webhook_url": group_config["webhook_url"],
|
||||
"member_list": member_list,
|
||||
"user_mapping": user_mapping,
|
||||
}
|
||||
)
|
||||
|
||||
return group_team_configs
|
||||
|
||||
def _build_title_to_sheet_id(self, sheet_list: List[Dict]) -> Dict[str, str]:
|
||||
"""构建子表标题到sheet_id映射"""
|
||||
mapping: Dict[str, str] = {}
|
||||
for sheet in sheet_list:
|
||||
title = str(sheet.get("title", "")).strip()
|
||||
sheet_id = str(sheet.get("sheet_id", "")).strip()
|
||||
if title and sheet_id:
|
||||
mapping[title] = sheet_id
|
||||
return mapping
|
||||
|
||||
def _is_enabled(self, value) -> bool:
|
||||
"""判断成员是否启用"""
|
||||
normalized = str(value).strip().lower()
|
||||
return normalized in {"是", "true", "1", "yes", "y"}
|
||||
14
src3/logger.py
Normal file
14
src3/logger.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""任务三日志系统"""
|
||||
from src.api_logger import APILogger
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
TASK3_LOG_DIR = project_root / "logs3"
|
||||
_task3_logger = None
|
||||
|
||||
def get_task3_logger() -> APILogger:
|
||||
"""获取任务三日志实例"""
|
||||
global _task3_logger
|
||||
if _task3_logger is None:
|
||||
_task3_logger = APILogger(log_dir=str(TASK3_LOG_DIR), task_name="task3")
|
||||
return _task3_logger
|
||||
193
src3/main.py
Normal file
193
src3/main.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""任务三主流程"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src3.logger import get_task3_logger
|
||||
from src3.config import Task3ConfigManager
|
||||
from src3.overdue_fetcher import OverdueFetcher
|
||||
from src3.message_formatter import MessageFormatter
|
||||
from src3.webhook_sender import WebhookSender
|
||||
|
||||
|
||||
def _post_simple_markdown(webhook_url: str, content: str):
|
||||
"""发送简单markdown消息(轻量通知)"""
|
||||
import requests
|
||||
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
try:
|
||||
requests.post(webhook_url, json=payload, timeout=10)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _send_error_notification(webhook_url: str, group_name: str):
|
||||
"""发送错误通知"""
|
||||
content = f"⚠️ 今日{group_name}组过期单提醒获取失败,请人工检查"
|
||||
_post_simple_markdown(webhook_url, content)
|
||||
|
||||
|
||||
def _send_no_overdue_message(webhook_url: str, group_name: str):
|
||||
"""发送无过期单消息"""
|
||||
content = f"✅ 今日{group_name}组无过期单,大家保持!"
|
||||
_post_simple_markdown(webhook_url, content)
|
||||
|
||||
|
||||
def run_once():
|
||||
"""执行一次过期单推送"""
|
||||
logger = get_task3_logger()
|
||||
sync_id = logger.start_sync("manual")
|
||||
|
||||
try:
|
||||
print("任务三:TAPD过期单推送")
|
||||
|
||||
# 1. 加载基础配置
|
||||
try:
|
||||
config = Task3ConfigManager()
|
||||
workspace_id = config.get_workspace_id()
|
||||
except ValueError as e:
|
||||
print(f"✗ 配置错误: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"配置错误: {e}", sync_id=sync_id)
|
||||
return
|
||||
|
||||
# 2. 加载多组配置(组名/成员子表/Webhook)
|
||||
try:
|
||||
group_team_configs = config.get_group_team_configs(logger=logger)
|
||||
except ValueError as e:
|
||||
print(f"✗ 组配置错误: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"组配置错误: {e}", sync_id=sync_id)
|
||||
return
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f"✗ 获取组配置失败: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"配置加载失败: {e}", sync_id=sync_id)
|
||||
return
|
||||
|
||||
for group in group_team_configs:
|
||||
group_name = group["group_name"]
|
||||
member_count = len(group["member_list"])
|
||||
print(f"{group_name}组成员数: {member_count}")
|
||||
|
||||
# 3. 合并成员后统一拉取过期单(避免重复调用TAPD)
|
||||
all_members = []
|
||||
for group in group_team_configs:
|
||||
for member in group["member_list"]:
|
||||
if member not in all_members:
|
||||
all_members.append(member)
|
||||
|
||||
if not all_members:
|
||||
print("⚠️ 所有组白名单均为空,跳过本次推送")
|
||||
logger.end_sync_with_stats(
|
||||
{"group_count": len(group_team_configs), "overdue_count": 0},
|
||||
True,
|
||||
sync_id=sync_id,
|
||||
)
|
||||
return
|
||||
|
||||
# 4. 获取过期单
|
||||
try:
|
||||
fetcher = OverdueFetcher(workspace_id, logger)
|
||||
items = fetcher.fetch_all_overdue(all_members)
|
||||
except Exception as e:
|
||||
print(f"✗ TAPD API调用失败: {e}")
|
||||
for group in group_team_configs:
|
||||
_send_error_notification(group["webhook_url"], group["group_name"])
|
||||
logger.end_sync_with_stats({}, False, f"TAPD API失败: {e}", sync_id=sync_id)
|
||||
return
|
||||
|
||||
print(f"获取到 {len(items)} 条过期单")
|
||||
|
||||
# 5. 按组分发推送
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
group_stats: List[Dict] = []
|
||||
failed_groups: List[str] = []
|
||||
skipped_empty_groups: List[str] = []
|
||||
|
||||
for group in group_team_configs:
|
||||
group_name = group["group_name"]
|
||||
webhook_url = group["webhook_url"]
|
||||
member_list = group["member_list"]
|
||||
user_mapping = group["user_mapping"]
|
||||
|
||||
if not member_list:
|
||||
print(f"⚠️ {group_name}组白名单为空,跳过该组推送")
|
||||
skipped_empty_groups.append(group_name)
|
||||
group_stats.append(
|
||||
{"group_name": group_name, "overdue_count": 0, "push_success": True, "skipped": True}
|
||||
)
|
||||
continue
|
||||
|
||||
member_set = set(member_list)
|
||||
group_items = [item for item in items if item["owner"] in member_set]
|
||||
|
||||
if not group_items:
|
||||
print(f"✅ {group_name}组无过期单")
|
||||
_send_no_overdue_message(webhook_url, group_name)
|
||||
group_stats.append(
|
||||
{"group_name": group_name, "overdue_count": 0, "push_success": True, "skipped": False}
|
||||
)
|
||||
continue
|
||||
|
||||
formatter = MessageFormatter(logger)
|
||||
content, mentioned_list = formatter.format_message(
|
||||
group_items,
|
||||
user_mapping,
|
||||
today,
|
||||
group_name=group_name
|
||||
)
|
||||
|
||||
if formatter.unmapped_users:
|
||||
unmapped = ", ".join(sorted(formatter.unmapped_users))
|
||||
print(f"⚠️ {group_name}组未映射用户: {unmapped}")
|
||||
|
||||
sender = WebhookSender(webhook_url, logger)
|
||||
success = sender.send_markdown(content, mentioned_list)
|
||||
if success:
|
||||
print(f"✓ {group_name}组推送成功({len(group_items)}条)")
|
||||
else:
|
||||
print(f"✗ {group_name}组推送失败")
|
||||
failed_groups.append(group_name)
|
||||
|
||||
group_stats.append(
|
||||
{
|
||||
"group_name": group_name,
|
||||
"overdue_count": len(group_items),
|
||||
"push_success": success,
|
||||
"skipped": False,
|
||||
}
|
||||
)
|
||||
|
||||
overall_success = len(failed_groups) == 0
|
||||
stats = {
|
||||
"group_count": len(group_team_configs),
|
||||
"overdue_count": len(items),
|
||||
"group_stats": group_stats,
|
||||
"skipped_empty_groups": skipped_empty_groups,
|
||||
}
|
||||
if overall_success:
|
||||
logger.end_sync_with_stats(stats, True, sync_id=sync_id)
|
||||
else:
|
||||
logger.end_sync_with_stats(
|
||||
stats,
|
||||
False,
|
||||
f"推送失败分组: {', '.join(failed_groups)}",
|
||||
sync_id=sync_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 未知错误: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"未知错误: {e}", sync_id=sync_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_once()
|
||||
104
src3/message_formatter.py
Normal file
104
src3/message_formatter.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""消息格式化器"""
|
||||
from typing import List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MessageFormatter:
|
||||
"""消息格式化器"""
|
||||
|
||||
def __init__(self, logger=None):
|
||||
self.logger = logger
|
||||
self.unmapped_users = set()
|
||||
|
||||
def format_message(
|
||||
self,
|
||||
items: List[dict],
|
||||
user_mapping: Dict[str, str],
|
||||
date: str = None,
|
||||
group_name: str = None
|
||||
) -> tuple:
|
||||
"""格式化消息
|
||||
返回: (markdown_content, mentioned_list)
|
||||
"""
|
||||
if not items:
|
||||
return None, []
|
||||
|
||||
if date is None:
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
# 按处理人分组
|
||||
grouped = self._group_by_owner(items)
|
||||
|
||||
# 排序
|
||||
sorted_groups = self._sort_groups(grouped)
|
||||
|
||||
# 生成Markdown
|
||||
group_label = self._format_group_label(group_name)
|
||||
if group_label:
|
||||
lines = [f"⏰ TAPD 过期单提醒 — {group_label}({date})\n\n"]
|
||||
else:
|
||||
lines = [f"⏰ TAPD 过期单提醒({date})\n\n"]
|
||||
mentioned_list = []
|
||||
total_count = 0
|
||||
item_index = 1
|
||||
|
||||
for owner, owner_items in sorted_groups:
|
||||
wework_id = user_mapping.get(owner)
|
||||
|
||||
if wework_id:
|
||||
mentioned_list.append(wework_id)
|
||||
lines.append(f"<@{wework_id}>({len(owner_items)} 条过期)")
|
||||
else:
|
||||
# 处理人不在映射表中
|
||||
self.unmapped_users.add(owner)
|
||||
lines.append(f"@{owner}({len(owner_items)} 条过期)")
|
||||
if self.logger:
|
||||
self.logger.log_api_call("formatter", "unmapped_user", {"owner": owner}, {}, False, "用户未在映射表中")
|
||||
|
||||
for item in owner_items:
|
||||
type_label = "需求" if item['type'] == 'story' else "缺陷"
|
||||
title = item.get('name') or item.get('title', '未命名')
|
||||
lines.append(f"{item_index}.【{type_label}】{title} | 过期 {item['overdue_days']} 天 | [查看]({item['url']})")
|
||||
item_index += 1
|
||||
total_count += 1
|
||||
|
||||
lines.append("\n\n========================")
|
||||
|
||||
lines.append(f"共 {total_count} 条过期单,请今日内更新状态 🙏")
|
||||
|
||||
return "\n".join(lines), mentioned_list
|
||||
|
||||
def _format_group_label(self, group_name: str) -> str:
|
||||
"""格式化组别显示文本"""
|
||||
if not group_name:
|
||||
return ""
|
||||
normalized = str(group_name).strip()
|
||||
if not normalized:
|
||||
return ""
|
||||
if normalized.endswith("组"):
|
||||
return normalized
|
||||
return f"{normalized}组"
|
||||
|
||||
def _group_by_owner(self, items: List[dict]) -> Dict[str, List[dict]]:
|
||||
"""按处理人分组"""
|
||||
grouped = {}
|
||||
for item in items:
|
||||
owner = item['owner']
|
||||
if owner not in grouped:
|
||||
grouped[owner] = []
|
||||
grouped[owner].append(item)
|
||||
return grouped
|
||||
|
||||
def _sort_groups(self, grouped: Dict[str, List[dict]]) -> List[tuple]:
|
||||
"""排序:组内按过期天数降序,组间按最大过期天数降序"""
|
||||
sorted_groups = []
|
||||
|
||||
for owner, items in grouped.items():
|
||||
# 组内排序
|
||||
items.sort(key=lambda x: x['overdue_days'], reverse=True)
|
||||
sorted_groups.append((owner, items))
|
||||
|
||||
# 组间排序
|
||||
sorted_groups.sort(key=lambda x: max(item['overdue_days'] for item in x[1]), reverse=True)
|
||||
|
||||
return sorted_groups
|
||||
39
src3/overdue_fetcher.py
Normal file
39
src3/overdue_fetcher.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""过期单获取器"""
|
||||
from typing import List, Dict
|
||||
from datetime import datetime
|
||||
from src3.tapd_api import TAPDUnifiedApi
|
||||
|
||||
|
||||
class OverdueFetcher:
|
||||
"""过期单获取器"""
|
||||
|
||||
def __init__(self, workspace_id: str, logger):
|
||||
self.workspace_id = workspace_id
|
||||
self.logger = logger
|
||||
self.api = TAPDUnifiedApi(workspace_id, logger)
|
||||
|
||||
def fetch_all_overdue(self, owner_list: List[str]) -> List[dict]:
|
||||
"""获取所有过期单(需求+缺陷)"""
|
||||
stories = self.api.get_overdue_stories(owner_list)
|
||||
bugs = self.api.get_overdue_bugs(owner_list)
|
||||
|
||||
# 合并并计算过期天数
|
||||
all_items = stories + bugs
|
||||
for item in all_items:
|
||||
item['overdue_days'] = self.calculate_overdue_days(item['due'])
|
||||
item['url'] = self._get_url(item)
|
||||
|
||||
return all_items
|
||||
|
||||
def calculate_overdue_days(self, due_date: str) -> int:
|
||||
"""计算过期天数"""
|
||||
today = datetime.now().date()
|
||||
due = datetime.strptime(due_date, '%Y-%m-%d').date()
|
||||
return (today - due).days
|
||||
|
||||
def _get_url(self, item: dict) -> str:
|
||||
"""获取单据URL"""
|
||||
if item['type'] == 'story':
|
||||
return self.api.get_story_url(item['id'])
|
||||
else:
|
||||
return self.api.get_bug_url(item['id'])
|
||||
105
src3/scheduler.py
Normal file
105
src3/scheduler.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""任务三定时调度器"""
|
||||
import sys
|
||||
import signal
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import schedule
|
||||
from src3.main import run_once
|
||||
|
||||
|
||||
class Task3Scheduler:
|
||||
"""任务三定时调度器"""
|
||||
|
||||
def __init__(self):
|
||||
self.running = True
|
||||
self.total_runs = 0
|
||||
self.success_count = 0
|
||||
self.fail_count = 0
|
||||
self.start_time = datetime.now()
|
||||
|
||||
# 注册信号处理
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
"""处理退出信号"""
|
||||
print("\n收到退出信号,等待当前任务完成...")
|
||||
self.running = False
|
||||
|
||||
def _is_workday(self):
|
||||
"""判断是否为工作日(周一到周五)"""
|
||||
return datetime.now().weekday() < 5
|
||||
|
||||
def _job(self):
|
||||
"""定时任务"""
|
||||
if not self._is_workday():
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 今天是周末,跳过推送")
|
||||
return
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始执行过期单推送")
|
||||
print(f"{'='*50}")
|
||||
|
||||
self.total_runs += 1
|
||||
|
||||
try:
|
||||
run_once()
|
||||
self.success_count += 1
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ✓ 执行成功")
|
||||
except Exception as e:
|
||||
self.fail_count += 1
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ✗ 执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def start(self):
|
||||
"""启动调度器"""
|
||||
from src3.config import Task3ConfigManager
|
||||
|
||||
try:
|
||||
config = Task3ConfigManager()
|
||||
push_time = config.get_push_time()
|
||||
except Exception as e:
|
||||
print(f"✗ 配置加载失败: {e}")
|
||||
return
|
||||
|
||||
print("="*50)
|
||||
print("任务三:TAPD过期单推送调度器")
|
||||
print("="*50)
|
||||
print(f"推送时间: 每个工作日 {push_time}")
|
||||
print(f"启动时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("="*50)
|
||||
|
||||
# 设置定时任务
|
||||
schedule.every().day.at(push_time).do(self._job)
|
||||
|
||||
# 循环执行
|
||||
while self.running:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
# 退出统计
|
||||
self._print_stats()
|
||||
|
||||
def _print_stats(self):
|
||||
"""打印运行统计"""
|
||||
print("\n" + "="*50)
|
||||
print("运行统计")
|
||||
print("="*50)
|
||||
print(f"启动时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"总执行次数: {self.total_runs}")
|
||||
print(f"成功次数: {self.success_count}")
|
||||
print(f"失败次数: {self.fail_count}")
|
||||
print("="*50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
scheduler = Task3Scheduler()
|
||||
scheduler.start()
|
||||
136
src3/tapd_api.py
Normal file
136
src3/tapd_api.py
Normal file
@ -0,0 +1,136 @@
|
||||
"""任务三TAPD API - 整合Story和Bug查询"""
|
||||
import os
|
||||
import requests
|
||||
from typing import List, Dict
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TAPDUnifiedApi:
|
||||
"""统一的TAPD API,整合Story和Bug查询"""
|
||||
|
||||
BASE_URL = "https://tapd-api.bilibili.co/tapd"
|
||||
|
||||
# 需求终态
|
||||
STORY_TERMINAL_STATUSES = ['status_8', 'status_12'] # 已完成、取消
|
||||
# 缺陷终态
|
||||
BUG_TERMINAL_STATUSES = ['rejected', 'closed'] # 取消、验证通过
|
||||
|
||||
def __init__(self, workspace_id: str, logger):
|
||||
self.workspace_id = workspace_id
|
||||
self.logger = logger
|
||||
|
||||
self.api_user = os.environ.get('TAPD_API_USER')
|
||||
self.api_password = os.environ.get('TAPD_API_PASSWORD')
|
||||
|
||||
if not self.api_user or not self.api_password:
|
||||
raise ValueError("TAPD认证信息未设置")
|
||||
|
||||
self.auth = HTTPBasicAuth(self.api_user, self.api_password)
|
||||
self.session = requests.Session()
|
||||
|
||||
def _make_request(self, endpoint: str, params: Dict) -> Dict:
|
||||
"""发起TAPD API请求"""
|
||||
url = f"{self.BASE_URL}/{endpoint}"
|
||||
|
||||
try:
|
||||
response = self.session.get(url, params=params, auth=self.auth, timeout=30)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('status') != 1:
|
||||
raise RuntimeError(f"TAPD API返回错误: {result}")
|
||||
|
||||
self.logger.log_api_call("tapd", endpoint, params, result, True)
|
||||
return result
|
||||
except Exception as e:
|
||||
self.logger.log_api_call("tapd", endpoint, params, {}, False, str(e))
|
||||
raise
|
||||
|
||||
def _make_request_with_retry(self, endpoint: str, params: Dict, retries=2, delay=30) -> Dict:
|
||||
"""带重试的TAPD API请求"""
|
||||
import time
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
return self._make_request(endpoint, params)
|
||||
except Exception as e:
|
||||
if attempt < retries:
|
||||
time.sleep(delay)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_overdue_stories(self, owner_list: List[str]) -> List[dict]:
|
||||
"""获取过期需求"""
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
all_stories = []
|
||||
|
||||
for owner in owner_list:
|
||||
params = {
|
||||
'workspace_id': self.workspace_id,
|
||||
'owner': owner,
|
||||
'fields': 'id,name,due,status'
|
||||
}
|
||||
|
||||
result = self._make_request_with_retry("stories", params)
|
||||
if not result:
|
||||
continue
|
||||
|
||||
data = result.get('data', [])
|
||||
for item in data:
|
||||
story = item.get('Story', {})
|
||||
due_date = story.get('due', '')
|
||||
status = story.get('status', '')
|
||||
|
||||
if due_date and due_date < today and status not in self.STORY_TERMINAL_STATUSES:
|
||||
all_stories.append({
|
||||
'id': story.get('id'),
|
||||
'name': story.get('name'),
|
||||
'owner': owner,
|
||||
'due': due_date,
|
||||
'status': status,
|
||||
'type': 'story'
|
||||
})
|
||||
|
||||
return all_stories
|
||||
|
||||
def get_overdue_bugs(self, owner_list: List[str]) -> List[dict]:
|
||||
"""获取过期缺陷"""
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
all_bugs = []
|
||||
|
||||
for owner in owner_list:
|
||||
params = {
|
||||
'workspace_id': self.workspace_id,
|
||||
'current_owner': owner,
|
||||
'fields': 'id,title,deadline,status'
|
||||
}
|
||||
|
||||
result = self._make_request_with_retry("bugs", params)
|
||||
if not result:
|
||||
continue
|
||||
|
||||
data = result.get('data', [])
|
||||
for item in data:
|
||||
bug = item.get('Bug', {})
|
||||
deadline = bug.get('deadline', '')
|
||||
status = bug.get('status', '')
|
||||
|
||||
if deadline and deadline < today and status not in self.BUG_TERMINAL_STATUSES:
|
||||
all_bugs.append({
|
||||
'id': bug.get('id'),
|
||||
'title': bug.get('title'),
|
||||
'owner': owner,
|
||||
'due': deadline,
|
||||
'status': status,
|
||||
'type': 'bug'
|
||||
})
|
||||
|
||||
return all_bugs
|
||||
|
||||
def get_story_url(self, story_id: str) -> str:
|
||||
"""生成需求URL"""
|
||||
return f"https://www.tapd.cn/{self.workspace_id}/prong/stories/view/{story_id}"
|
||||
|
||||
def get_bug_url(self, bug_id: str) -> str:
|
||||
"""生成缺陷URL"""
|
||||
return f"https://www.tapd.cn/{self.workspace_id}/bugtrace/bugs/view?bug_id={bug_id}"
|
||||
76
src3/webhook_sender.py
Normal file
76
src3/webhook_sender.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Webhook推送器"""
|
||||
import requests
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
|
||||
class WebhookSender:
|
||||
"""企微Webhook推送器"""
|
||||
|
||||
MAX_BYTES = 4096 # 企微消息最大字节数
|
||||
|
||||
def __init__(self, webhook_url: str, logger):
|
||||
self.webhook_url = webhook_url
|
||||
self.logger = logger
|
||||
|
||||
def send_markdown(self, content: str, mentioned_list: List[str]) -> bool:
|
||||
"""发送Markdown消息,自动分段"""
|
||||
messages = self._split_by_bytes(content)
|
||||
|
||||
for msg in messages:
|
||||
if not self._send_single(msg, mentioned_list):
|
||||
return False
|
||||
time.sleep(1) # 避免频率限制
|
||||
|
||||
return True
|
||||
|
||||
def _split_by_bytes(self, content: str) -> List[str]:
|
||||
"""按字节长度分段,不切断单个用户的过期单"""
|
||||
lines = content.split('\n')
|
||||
messages = []
|
||||
current = []
|
||||
current_bytes = 0
|
||||
|
||||
for line in lines:
|
||||
line_bytes = len(line.encode('utf-8')) + 1 # +1 for \n
|
||||
|
||||
if current_bytes + line_bytes > self.MAX_BYTES and current:
|
||||
messages.append('\n'.join(current))
|
||||
current = [line]
|
||||
current_bytes = line_bytes
|
||||
else:
|
||||
current.append(line)
|
||||
current_bytes += line_bytes
|
||||
|
||||
if current:
|
||||
messages.append('\n'.join(current))
|
||||
|
||||
return messages
|
||||
|
||||
def _send_single(self, content: str, mentioned_list: List[str]) -> bool:
|
||||
"""发送单条消息"""
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": content
|
||||
},
|
||||
"mentioned_list": mentioned_list
|
||||
}
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.post(self.webhook_url, json=payload, timeout=10)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
self.logger.log_api_call("wework", "webhook/send", payload, result, result.get('errcode') == 0)
|
||||
|
||||
if result.get('errcode') == 0:
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.log_api_call("wework", "webhook/send", payload, {}, False, str(e))
|
||||
|
||||
if attempt < 2:
|
||||
time.sleep(30)
|
||||
|
||||
return False
|
||||
69
智能表格接口文档.md
69
智能表格接口文档.md
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 查询智能表格子表权限
|
||||
|
||||
该接口用于查询智能表格子表权限详情
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user