Compare commits

...

25 Commits

Author SHA1 Message Date
9df099942b 任务一 TAPD 429 限速通知与状态批量查询 2026-06-02 20:35:12 +08:00
a81e6961b4 修复任务一429时不推送企微报错 2026-06-02 18:56:48 +08:00
3dc4815e26 '修复bug提单工具【标题】字段包含跨行文本时报错' 2026-04-22 14:07:12 +08:00
4e189d6f44 'update-webhook' 2026-04-01 18:40:32 +08:00
6806e002c7 'task3v2' 2026-04-01 16:01:58 +08:00
af7243cf4e 'task3TAPD过期单推送' 2026-03-13 14:21:45 +08:00
704318077d 'task3phase2调度器和智能表格获取名单' 2026-03-12 15:36:51 +08:00
0224c87f45 'task3phase1单次拉取过期单并推送企微完成' 2026-03-12 14:13:50 +08:00
c5f2c64e82 '日志系统迭代' 2026-03-10 20:53:32 +08:00
403b8fee0d '日志迭代phase2' 2026-03-10 14:23:17 +08:00
0769cf4755 '修复任务2单次同步记录过多触发429限流,会暂停1分钟' 2026-02-28 17:16:30 +08:00
2d7fa8ea7a 'fix:task2-多条记录填入同一tapd链接时从缓存读取已拉取内容' 2026-02-04 14:14:03 +08:00
01d768b16a tapd链接字段去掉标识 2026-01-22 16:53:27 +08:00
a3099c51db 【计划】字段改为【发布计划】 2026-01-22 16:47:57 +08:00
21e0160b76 列名统一增加后缀(🈲勿手改)+优化空值覆盖逻辑 2026-01-20 17:09:41 +08:00
104198e918 '表格api调用失败日志写入,失败原因写入' 2026-01-14 16:52:18 +08:00
93f12f8d5a '表格api调用失败日志写入' 2026-01-14 14:25:04 +08:00
9e809ddf88 'task2多docid' 2026-01-13 15:59:40 +08:00
7db5d87a94 '新增计划字段+持续同步功能' 2026-01-12 19:38:33 +08:00
ecf9ccbc0b 新增计划字段及映射同步 2026-01-09 20:07:33 +08:00
f91dadffd4 同步失败消息推送 2026-01-09 19:01:34 +08:00
7cdd3e9390 task2-phase4: 定时同步回写 2026-01-09 11:45:03 +08:00
d984828777 task2-phase3: 表格记录获取 2026-01-08 16:26:57 +08:00
397c14faee task2-phase2: 链接解析与TAPD API 2026-01-08 15:27:19 +08:00
bf4f346096 task2-phase1: 基础框架搭建 2026-01-07 20:15:02 +08:00
50 changed files with 9685 additions and 183 deletions

2
.gitignore vendored
View File

@ -132,6 +132,8 @@ cython_debug/
# Custom # Custom
logs/ logs/
logs2/
logs3/
.claude/ .claude/
# Project documentation # Project documentation

View File

@ -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#获取项目下的缺陷字段)获取项目下的缺陷字段
### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#curl-使用-basic-auth-鉴权调用示例)curl 使用 Basic Auth 鉴权调用示例 ### [#](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
View 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

View File

@ -1,4 +1,4 @@
{ {
"access_token": "SPFfB9jMxleGiJn76FQH6v5pploseAJXTCVkTVVxl1_PEmHZJiqXsNGpEyGAK4qmYe3WRxYJ57xgCQLRCHopVfeoDfP87IgxVCytCqQABGES5ndG05SVkrI-9evg8Z4kbstlsiRMmfPGGGoNUgL1kUoZc2No0FYytm8FTfulnAXfiTExzoF8OCTdEPc9mA0g8JKFhlkiS2F0agBESS_2_ewbcZvA0i44-ChTKRBdRa0", "access_token": "lFg2EG8MT_7cNhAK_huBHTiMoR3_GNR63rqlZm-LgL3cyn2hctqm3N65Wb-Z3Lki8fHDmWcutxtSqbA5PoVqUnyn8fs2RpJg7tom1DbDeKq--6qp3lBFvkHUDiCDzB5BklWoYYl9VxG22wCsjmE3Gep_mrNRSMWLZch5mhaSZwaWojSh3fXHY3q23oI-u-nElMKsKE0vk14V7j1W6MZIRkejhZV28R6Fd4zZPkiNha0",
"fetch_time": 1767599732.4166858 "fetch_time": 1780403671.4761236
} }

31
config3/config.ini Normal file
View 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
View 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
View 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")

View 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
View 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
View 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 的方案。

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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": "KawaiiMagicAnimVelet调研", "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@BardLin12 条过期)\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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": "KawaiiMagicAnimVelet调研", "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.【需求】KawaiiMagicAnimVelet调研 | 过期 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": {}}

View File

@ -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 from __future__ import annotations
import os
from datetime import datetime
from typing import Dict, Any, Optional
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional
from core.global_log_system import GlobalLogSystem
class APILogger: class APILogger:
"""API调用日志记录器""" """API 日志记录器(兼容旧接口)"""
def __init__(self, log_dir: Optional[str] = None): def __init__(self,
""" log_dir: Optional[str] = None,
初始化日志记录器 task_name: Optional[str] = None):
project_root = Path(__file__).parent.parent
Args:
log_dir: 日志目录路径如果为None则使用默认路径
"""
if log_dir is None: if log_dir is None:
# 默认路径:项目根目录/logs/ resolved_log_dir = project_root / "logs"
project_root = Path(__file__).parent.parent
self.log_dir = project_root / "logs"
else: else:
self.log_dir = Path(log_dir) resolved_log_dir = Path(log_dir)
# 确保日志目录存在 inferred_task_name = self._infer_task_name(task_name, resolved_log_dir)
self.log_dir.mkdir(exist_ok=True) 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: def _get_today_log_file(self) -> Path:
""" """兼容旧方法:返回当天 jsonl 文件路径"""
获取今天的日志文件路径 return self.core._get_today_log_file()
Returns: def start_sync(self,
Path: 今天的日志文件路径格式api_log_YYYY-MM-DD.json trigger: str,
""" metadata: Optional[Dict[str, Any]] = None,
today = datetime.now().strftime("%Y-%m-%d") sync_id: Optional[str] = None) -> str:
log_file = self.log_dir / f"api_log_{today}.json" return self.core.start_sync(trigger=trigger, metadata=metadata, sync_id=sync_id)
# 如果文件不存在,初始化 def end_sync_with_stats(self,
if not log_file.exists(): stats: Dict[str, Any],
with open(log_file, 'w', encoding='utf-8') as f: success: bool,
json.dump({"records": []}, f, ensure_ascii=False, indent=2) 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], request_data: Dict[str, Any],
response_data: Dict[str, Any], response_data: Dict[str, Any],
success: bool = True, 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调用使用追加式写入避免读取整个文件 兼容旧接口
- api_type 允许历史值 task1/task2/test
Args: - 内部统一映射为 module: smartsheet/tapd/wework
api_type: API类型 "smartsheet", "tapd", "wework"
operation: 操作名称 "get_records", "create_bug"
request_data: 请求数据包含urlmethodparams等
response_data: 响应数据
success: 是否成功
error_message: 错误信息如果失败
""" """
try: module = self._resolve_module(api_type, operation, request_data)
# 获取今天的日志文件
log_file = self._get_today_log_file()
# 构造新记录 payload_extra = dict(extra or {})
record = { if api_type not in GlobalLogSystem.ALLOWED_MODULES:
"api_type": api_type, payload_extra["original_api_type"] = api_type
"operation": operation,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"success": success,
"request": request_data,
"response": response_data
}
if error_message: self.core.log_api(
record["error_message"] = error_message 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,
)
# 使用追加式写入 def _resolve_module(self,
with open(log_file, 'r+', encoding='utf-8') as f: api_type: str,
# 定位到文件末尾 operation: str,
f.seek(0, 2) request_data: Optional[Dict[str, Any]]) -> str:
file_size = f.tell() normalized_type = (api_type or "").strip().lower()
if normalized_type in GlobalLogSystem.ALLOWED_MODULES:
return normalized_type
if file_size == 0: request_url = ""
# 空文件,写入初始结构 if isinstance(request_data, dict):
f.write('{"records": [\n') request_url = str(request_data.get("url", "")).lower()
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]}')
except Exception as e: operation_lower = (operation or "").lower()
# 日志记录失败不应该影响主流程
print(f"⚠ API日志记录失败: {str(e)}") 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: Optional[APILogger] = None
_global_logger = None
def get_logger() -> APILogger: def get_logger() -> APILogger:
""" """获取任务一默认日志记录器(单例)"""
获取全局API日志记录器单例
Returns:
APILogger: 日志记录器实例
"""
global _global_logger global _global_logger
if _global_logger is None: if _global_logger is None:
_global_logger = APILogger() _global_logger = APILogger()
return _global_logger 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}")

View File

@ -22,7 +22,13 @@ from src.config import ConfigManager
class WeWorkAPITester: class WeWorkAPITester:
"""企业微信API测试类""" """企业微信API测试类"""
def __init__(self): def __init__(self, auto_load_token=True):
"""
初始化企业微信API测试类
Args:
auto_load_token: 是否自动加载token默认为True
"""
self.access_token = None self.access_token = None
self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs', 'api_test_log.json') 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" self.base_url = "https://qyapi.weixin.qq.com/cgi-bin"
@ -30,12 +36,92 @@ class WeWorkAPITester:
# 确保日志文件存在 # 确保日志文件存在
self._init_log_file() self._init_log_file()
# 自动加载token
if auto_load_token:
self._auto_load_token()
def _init_log_file(self): def _init_log_file(self):
"""初始化日志文件""" """初始化日志文件"""
if not os.path.exists(self.log_file): if not os.path.exists(self.log_file):
with open(self.log_file, 'w', encoding='utf-8') as f: with open(self.log_file, 'w', encoding='utf-8') as f:
json.dump({"records": []}, f, ensure_ascii=False, indent=2) 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): def _log_api_call(self, operation, request_data, response_data):
"""记录API调用到JSON文件""" """记录API调用到JSON文件"""
try: try:
@ -335,6 +421,128 @@ class WeWorkAPITester:
print(f"✗ 请求异常: {str(e)}") print(f"✗ 请求异常: {str(e)}")
return False 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): def send_message(self, content):
""" """
发送应用消息 发送应用消息
@ -481,6 +689,110 @@ class TAPDAPITester:
except Exception as e: except Exception as e:
print(f"✗ 记录日志失败: {str(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): def get_bug_custom_fields(self):
""" """
获取TAPD缺陷的所有字段配置及候选值 获取TAPD缺陷的所有字段配置及候选值
@ -586,6 +898,121 @@ class TAPDAPITester:
traceback.print_exc() traceback.print_exc()
return None 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): def upload_attachment(self, file_path, entry_type, entry_id, owner=None, overwrite=False):
""" """
上传附件到TAPD 上传附件到TAPD
@ -925,12 +1352,16 @@ def print_menu():
print("3. 重命名文档") print("3. 重命名文档")
print("4. 删除文档") print("4. 删除文档")
print("5. 发送应用消息") print("5. 发送应用消息")
print("6. 查询智能表格子表列表")
print("7. 修改文档成员权限")
print("\n【TAPD API】") print("\n【TAPD API】")
print("6. 获取缺陷字段配置") print("8. 获取缺陷字段配置")
print("7. 获取附件列表") print("9. 获取需求字段配置")
print("8. 上传附件") print("10. 获取需求")
print("11. 获取附件列表")
print("12. 上传附件")
print("\n【其他】") print("\n【其他】")
print("9. 查看日志文件") print("13. 查看日志文件")
print("0. 退出") print("0. 退出")
print("="*50) print("="*50)
@ -942,7 +1373,7 @@ def main():
while True: while True:
print_menu() print_menu()
choice = input("\n请选择操作 (0-9): ").strip() choice = input("\n请选择操作 (0-13): ").strip()
if choice == "0": if choice == "0":
print("\n感谢使用,再见!") print("\n感谢使用,再见!")
@ -1014,10 +1445,56 @@ def main():
wework_tester.send_message(content) wework_tester.send_message(content)
elif choice == "6": 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缺陷字段配置
tapd_tester.get_bug_custom_fields() 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附件列表 # 获取TAPD附件列表
print("\n=== 获取附件列表 ===") print("\n=== 获取附件列表 ===")
print("是否需要添加筛选条件?") print("是否需要添加筛选条件?")
@ -1055,7 +1532,7 @@ def main():
limit=limit limit=limit
) )
elif choice == "8": elif choice == "12":
# 上传附件到TAPD # 上传附件到TAPD
print("\n=== 上传附件 ===") print("\n=== 上传附件 ===")
file_path = input("请输入文件路径: ").strip() file_path = input("请输入文件路径: ").strip()
@ -1099,7 +1576,7 @@ def main():
overwrite=overwrite overwrite=overwrite
) )
elif choice == "9": elif choice == "13":
print("\n=== 查看日志文件 ===") print("\n=== 查看日志文件 ===")
try: try:
with open(wework_tester.log_file, 'r', encoding='utf-8') as f: with open(wework_tester.log_file, 'r', encoding='utf-8') as f:

View File

@ -5,6 +5,7 @@ Debug阶段自动开单工具
import sys import sys
import argparse import argparse
import time
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
@ -15,11 +16,16 @@ sys.path.insert(0, str(project_root))
from src.config import ConfigManager from src.config import ConfigManager
from src.smartsheet import SmartSheetAPI from src.smartsheet import SmartSheetAPI
from src.validator import RecordValidator 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.mapper import FieldMapper, BugCreationResult
from src.token_manager import TokenManager from src.token_manager import TokenManager
from src.status_mapper import BugStatusMapper from src.status_mapper import BugStatusMapper
from src.wework_notifier import WeWorkNotifier 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(): def parse_arguments():
@ -295,6 +301,40 @@ def scan_all_sheets(access_token: str, docid: str, verbose: bool = False, test_m
return all_results 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: def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test_mode: bool = False) -> Dict:
""" """
批量创建TAPD bug单 批量创建TAPD bug单
@ -338,6 +378,7 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
success_results = [] success_results = []
failed_results = [] failed_results = []
rate_limit_info = None
for idx, record_data in enumerate(valid_records, 1): for idx, record_data in enumerate(valid_records, 1):
record_id = record_data.get('record_id', '未知') 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 # 创建bug
print(f" → 正在创建TAPD 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_id = bug_info.get('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}" error_msg = f"字段映射失败: {e}"
print(f"{error_msg}") 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( result = BugCreationResult(
record_id=record_id, record_id=record_id,
success=False, success=False,
error_message=error_msg error_message=error_msg
) )
result.title = title
failed_results.append(result) 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: except RuntimeError as e:
# TAPD API调用错误 # TAPD API调用错误
error_msg = f"TAPD API调用失败: {e}" error_msg = f"TAPD API调用失败: {e}"
print(f"{error_msg}") 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( result = BugCreationResult(
record_id=record_id, record_id=record_id,
success=False, success=False,
error_message=error_msg error_message=error_msg
) )
result.title = title
failed_results.append(result) failed_results.append(result)
except Exception as e: 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}" error_msg = f"未预期的错误: {type(e).__name__}: {e}"
print(f"{error_msg}") 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( result = BugCreationResult(
record_id=record_id, record_id=record_id,
success=False, success=False,
error_message=error_msg error_message=error_msg
) )
result.title = title
failed_results.append(result) failed_results.append(result)
# 显示汇总结果 # 显示汇总结果
@ -439,7 +542,8 @@ def create_tapd_bugs(valid_records: list, workspace_id: str, reporter: str, test
return { return {
'success_results': success_results, '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 '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: try:
# 获取配置信息 # 获取配置信息
all_config = config_manager.get_all_config() all_config = config_manager.get_all_config()
@ -588,7 +721,10 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
# 2. 遍历每个子表,分别处理 # 2. 遍历每个子表,分别处理
all_invalid_records = [] # 收集所有子表的失败记录(用于推送) all_invalid_records = [] # 收集所有子表的失败记录(用于推送)
all_creation_failure_records = [] # 收集TAPD开单失败记录用于推送
all_rate_limit_records = [] # 收集TAPD限速事件用于推送
sheet_summaries = [] # 收集每个子表的统计摘要 sheet_summaries = [] # 收集每个子表的统计摘要
hit_tapd_rate_limit = False
for sheet_result in all_sheet_results: for sheet_result in all_sheet_results:
sheet_id = sheet_result['sheet_id'] 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_success_results = []
creation_failed_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( creation_result = create_tapd_bugs(
validation_result['valid_records'], validation_result['valid_records'],
all_config['tapd']['workspace_id'], 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_success_results = creation_result['success_results']
creation_failed_results = creation_result['failed_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格式 # 4. 处理校验失败的记录转换为BugCreationResult格式
validation_failed_results = [] validation_failed_results = []
for invalid_record in validation_result['invalid_records']: for invalid_record in validation_result['invalid_records']:
record_data = invalid_record['record_data'] record_data = invalid_record['record_data']
missing_fields = invalid_record['missing_fields'] 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( validation_failed_result = BugCreationResult(
record_id=record_data.get('record_id', '未知'), record_id=record_data.get('record_id', '未知'),
success=False, success=False,
error_message=f"校验失败,缺失字段: {', '.join(missing_fields)}" error_message=error_msg
) )
validation_failed_results.append(validation_failed_result) 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: except Exception as e:
print(f" ✗ 企业微信推送失败: {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. 显示最终统计(分表统计 + 总体统计) # 7. 显示最终统计(分表统计 + 总体统计)
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("执行完成 - 分表统计") print("执行完成 - 分表统计")
@ -740,7 +927,7 @@ def run_once(config_manager: ConfigManager, access_token: str, verbose: bool = F
print("=" * 60) print("=" * 60)
result['success'] = True result['success'] = True
return result return _finalize_sync_and_return()
except FileNotFoundError as e: except FileNotFoundError as e:
result['error_message'] = f"文件未找到: {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("\n解决方案:")
print(" 1. 检查配置文件是否存在") print(" 1. 检查配置文件是否存在")
print(" 2. 使用 --config 参数指定正确的配置文件路径") print(" 2. 使用 --config 参数指定正确的配置文件路径")
return result return _finalize_sync_and_return()
except ValueError as e: except ValueError as e:
result['error_message'] = f"参数错误: {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(" 1. 检查配置文件中的配置项是否完整")
print(" 2. 确保所有必填项都已填写") print(" 2. 确保所有必填项都已填写")
print(" 3. 检查access_token是否正确") print(" 3. 检查access_token是否正确")
return result return _finalize_sync_and_return()
except Exception as e: except Exception as e:
result['error_message'] = f"未预期的错误: {type(e).__name__}: {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 import traceback
print("\n详细错误信息:") print("\n详细错误信息:")
traceback.print_exc() traceback.print_exc()
return result return _finalize_sync_and_return()
def main(): def main():

View File

@ -202,7 +202,7 @@ class FieldMapper:
tapd_data = {} tapd_data = {}
# 1. 标题(必填) # 1. 标题(必填)
title = record_data.get('标题', '').strip() title = self._convert_multiline_text(record_data.get('标题', ''))
if not title: if not title:
raise ValueError("标题不能为空") raise ValueError("标题不能为空")
tapd_data['title'] = title tapd_data['title'] = title

View File

@ -26,6 +26,7 @@ from src.config import ConfigManager
from src.token_manager import TokenManager from src.token_manager import TokenManager
from src.main import run_once from src.main import run_once
from src.sync_status import BugStatusSyncer from src.sync_status import BugStatusSyncer
from src.api_logger import get_logger
class AutoTAPDScheduler: class AutoTAPDScheduler:
@ -90,6 +91,14 @@ class AutoTAPDScheduler:
print(f"开始执行开单任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"开始执行开单任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 80) print("=" * 80)
logger = get_logger()
logger.start_sync(
trigger="task1_scheduler_job",
metadata={"entry": "src/scheduler.py:job"}
)
result = None
try: try:
# 获取access_token # 获取access_token
access_token = self.token_manager.get_token() access_token = self.token_manager.get_token()
@ -127,6 +136,20 @@ class AutoTAPDScheduler:
print(f" 错误信息: {result.get('error_message', '未知错误')}") print(f" 错误信息: {result.get('error_message', '未知错误')}")
print("-" * 80) 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: except Exception as e:
# 捕获所有异常,确保不影响后续执行 # 捕获所有异常,确保不影响后续执行
self.stats['total_runs'] += 1 self.stats['total_runs'] += 1
@ -144,6 +167,20 @@ class AutoTAPDScheduler:
print("\n详细错误信息:") print("\n详细错误信息:")
traceback.print_exc() 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() next_run = schedule.idle_seconds()
if next_run is not None: 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(f"开始执行bug状态同步 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 80) print("=" * 80)
logger = get_logger()
logger.start_sync(
trigger="task1_scheduler_sync_job",
metadata={"entry": "src/scheduler.py:sync_job"}
)
result = None
try: try:
# 获取access_token # 获取access_token
access_token = self.token_manager.get_token() access_token = self.token_manager.get_token()
# 创建状态同步器 # 创建状态同步器
wework_config = self.config.get('wework', {})
syncer = BugStatusSyncer( syncer = BugStatusSyncer(
access_token=access_token, access_token=access_token,
docid=self.config['smartsheet']['docid'], docid=self.config['smartsheet']['docid'],
workspace_id=self.config['tapd']['workspace_id'], 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("本次同步统计:")
print(f" 检查bug: {result['checked_count']}") print(f" 检查bug: {result['checked_count']}")
print(f" 更新bug: {result['updated_count']}") print(f" 更新bug: {result['updated_count']}")
print(f" 查询失败: {result.get('failed_count', 0)}")
print("-" * 80) print("-" * 80)
else: else:
self.stats['failed_sync_runs'] += 1 self.stats['failed_sync_runs'] += 1
@ -193,6 +242,18 @@ class AutoTAPDScheduler:
print(f" 错误信息: {result.get('error_message', '未知错误')}") print(f" 错误信息: {result.get('error_message', '未知错误')}")
print("-" * 80) 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: except Exception as e:
# 捕获所有异常,确保不影响后续执行 # 捕获所有异常,确保不影响后续执行
self.stats['total_sync_runs'] += 1 self.stats['total_sync_runs'] += 1
@ -210,6 +271,18 @@ class AutoTAPDScheduler:
print("\n详细错误信息:") print("\n详细错误信息:")
traceback.print_exc() 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() next_run = schedule.idle_seconds()
if next_run is not None: if next_run is not None:

View File

@ -60,6 +60,7 @@ class SmartSheetAPI:
print("=" * 80) print("=" * 80)
print(f"请求方法: {method}") print(f"请求方法: {method}")
print(f"请求URL: {self.BASE_URL}/{endpoint}") print(f"请求URL: {self.BASE_URL}/{endpoint}")
print(f"完整URL含参数: {url}") # 显示完整URL
if data: if data:
import json import json
print(f"请求数据:") print(f"请求数据:")
@ -74,6 +75,21 @@ class SmartSheetAPI:
response.raise_for_status() response.raise_for_status()
result = response.json() 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调用日志成功 # 记录API调用日志成功
self.logger.log_api_call( self.logger.log_api_call(
api_type="smartsheet", api_type="smartsheet",
@ -91,11 +107,6 @@ class SmartSheetAPI:
print(json.dumps(result, ensure_ascii=False, indent=2)) print(json.dumps(result, ensure_ascii=False, indent=2))
print("=" * 80) 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 return result
except requests.exceptions.Timeout: except requests.exceptions.Timeout:

View File

@ -4,6 +4,7 @@ bug状态同步模块
""" """
import sys import sys
import time
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List
@ -12,14 +13,20 @@ project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
from src.smartsheet import SmartSheetAPI 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.status_mapper import BugStatusMapper
from src.wework_notifier import WeWorkNotifier
class BugStatusSyncer: class BugStatusSyncer:
"""Bug状态同步器""" """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.docid = docid
self.workspace_id = workspace_id self.workspace_id = workspace_id
self.test_mode = test_mode self.test_mode = test_mode
self.agentid = agentid
self.receivers = receivers
# 初始化API # 初始化API
self.smartsheet_api = None self.smartsheet_api = None
self.tapd_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: def _sync_sheet_status(self, sheet_id: str, sheet_title: str) -> Dict:
""" """
@ -47,7 +84,7 @@ class BugStatusSyncer:
sheet_title: 子表标题 sheet_title: 子表标题
Returns: Returns:
Dict: {'checked': int, 'updated': int} Dict: {'checked': int, 'updated': int, 'failed': int, 'rate_limited': bool}
""" """
# 1. 获取字段信息 # 1. 获取字段信息
fields = self.smartsheet_api.get_fields(sheet_id) fields = self.smartsheet_api.get_fields(sheet_id)
@ -73,26 +110,97 @@ class BugStatusSyncer:
if len(records) == 0: if len(records) == 0:
print(f" ✓ 没有需要同步状态的记录") print(f" ✓ 没有需要同步状态的记录")
return {'checked': 0, 'updated': 0} return {'checked': 0, 'updated': 0, 'failed': 0, 'rate_limited': False}
print(f" → 检查 {len(records)} 个bug的状态...") print(f" → 检查 {len(records)} 个bug的状态...")
# 3. 逐个查询TAPD bug状态并对比 # 3. 批量查询TAPD bug状态并对比
updates = [] # 需要更新的记录列表 updates = [] # 需要更新的记录列表
failed_count = 0
checked_count = 0
rate_limited = False
prepared_records = []
for record in records: for record in records:
record_id = record.get('record_id', '未知') record_id = record.get('record_id', '未知')
# 提取TAPD单号 # 提取TAPD单号
tapd_bug_field = self.smartsheet_api.get_field_value_by_title(record, '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状态 # 获取当前智能表格中的bug状态
current_status = self.smartsheet_api.get_field_value_by_title(record, '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: try:
# 查询TAPD的最新状态 bug_map = self._get_bugs_by_ids_with_rate_limit_retry(batch_bug_ids)
bug_info = self.tapd_api.get_bug(bug_id) 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_en = bug_info.get('status', '')
latest_status_cn = BugStatusMapper.to_chinese(latest_status_en) latest_status_cn = BugStatusMapper.to_chinese(latest_status_en)
@ -106,10 +214,6 @@ class BugStatusSyncer:
} }
updates.append(update_record) updates.append(update_record)
except Exception:
# 单个bug查询失败不影响其他bug的同步
continue
# 4. 批量更新智能表格 # 4. 批量更新智能表格
if len(updates) > 0: if len(updates) > 0:
self.smartsheet_api.update_records(sheet_id, updates) self.smartsheet_api.update_records(sheet_id, updates)
@ -117,7 +221,12 @@ class BugStatusSyncer:
else: else:
print(f" ✓ 所有bug状态均未变化") 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: def sync_bug_status(self) -> Dict:
""" """
@ -128,6 +237,7 @@ class BugStatusSyncer:
'success': bool, 'success': bool,
'checked_count': int, # 检查的bug数量 'checked_count': int, # 检查的bug数量
'updated_count': int, # 更新的bug数量 'updated_count': int, # 更新的bug数量
'failed_count': int, # 查询失败的bug数量
'error_message': str 'error_message': str
} }
""" """
@ -135,10 +245,14 @@ class BugStatusSyncer:
'success': False, 'success': False,
'checked_count': 0, 'checked_count': 0,
'updated_count': 0, 'updated_count': 0,
'failed_count': 0,
'rate_limited': False,
'error_message': None 'error_message': None
} }
try: try:
self.failure_records = []
# 1. 初始化API # 1. 初始化API
print("\n[1/5] 初始化API...") print("\n[1/5] 初始化API...")
self.smartsheet_api = SmartSheetAPI(self.access_token, self.docid, test_mode=self.test_mode) self.smartsheet_api = SmartSheetAPI(self.access_token, self.docid, test_mode=self.test_mode)
@ -158,8 +272,14 @@ class BugStatusSyncer:
sheet_summaries = [] # 收集每个子表的统计摘要 sheet_summaries = [] # 收集每个子表的统计摘要
total_checked = 0 total_checked = 0
total_updated = 0 total_updated = 0
total_failed = 0
hit_rate_limit = False
for idx, sheet in enumerate(sheet_list, 1): for idx, sheet in enumerate(sheet_list, 1):
if hit_rate_limit:
print("\n已触发TAPD限速停止处理后续子表")
break
sheet_id = sheet['sheet_id'] sheet_id = sheet['sheet_id']
sheet_title = sheet.get('title', '未命名') sheet_title = sheet.get('title', '未命名')
@ -174,12 +294,16 @@ class BugStatusSyncer:
'sheet_title': sheet_title, 'sheet_title': sheet_title,
'error': None, 'error': None,
'checked': sheet_result['checked'], 'checked': sheet_result['checked'],
'updated': sheet_result['updated'] 'updated': sheet_result['updated'],
'failed': sheet_result['failed']
}) })
# 累加总计 # 累加总计
total_checked += sheet_result['checked'] total_checked += sheet_result['checked']
total_updated += sheet_result['updated'] total_updated += sheet_result['updated']
total_failed += sheet_result['failed']
if sheet_result.get('rate_limited'):
hit_rate_limit = True
except RuntimeError as e: except RuntimeError as e:
print(f" ✗ 处理失败: {e}") print(f" ✗ 处理失败: {e}")
@ -187,7 +311,8 @@ class BugStatusSyncer:
'sheet_title': sheet_title, 'sheet_title': sheet_title,
'error': str(e), 'error': str(e),
'checked': 0, 'checked': 0,
'updated': 0 'updated': 0,
'failed': 0
}) })
except Exception as e: except Exception as e:
print(f" ✗ 未预期的错误: {type(e).__name__}: {e}") print(f" ✗ 未预期的错误: {type(e).__name__}: {e}")
@ -195,12 +320,15 @@ class BugStatusSyncer:
'sheet_title': sheet_title, 'sheet_title': sheet_title,
'error': f"{type(e).__name__}: {e}", 'error': f"{type(e).__name__}: {e}",
'checked': 0, 'checked': 0,
'updated': 0 'updated': 0,
'failed': 0
}) })
# 4. 显示最终统计(分表统计 + 总体统计) # 4. 显示最终统计(分表统计 + 总体统计)
result['checked_count'] = total_checked result['checked_count'] = total_checked
result['updated_count'] = total_updated result['updated_count'] = total_updated
result['failed_count'] = total_failed
result['rate_limited'] = hit_rate_limit
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("状态同步完成 - 分表统计") print("状态同步完成 - 分表统计")
@ -217,6 +345,8 @@ class BugStatusSyncer:
print(f" ✓ 更新: {summary['updated']} 个bug") print(f" ✓ 更新: {summary['updated']} 个bug")
else: else:
print(f" ✓ 所有bug状态均未变化") print(f" ✓ 所有bug状态均未变化")
if summary['failed'] > 0:
print(f" ⚠ 查询失败: {summary['failed']} 个bug")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("状态同步完成 - 总体统计") print("状态同步完成 - 总体统计")
@ -236,9 +366,30 @@ class BugStatusSyncer:
print(f" ✓ 更新: {total_updated} 个bug") print(f" ✓ 更新: {total_updated} 个bug")
else: else:
print(f" ✓ 所有bug状态均未变化") print(f" ✓ 所有bug状态均未变化")
if total_failed > 0:
print(f" ⚠ 查询失败: {total_failed} 个bug")
print("=" * 60) 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 return result
except FileNotFoundError as e: except FileNotFoundError as e:

View File

@ -10,6 +10,16 @@ from requests.auth import HTTPBasicAuth
from src.api_logger import get_logger 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: class TAPDApi:
"""TAPD API封装类""" """TAPD API封装类"""
@ -52,6 +62,21 @@ class TAPDApi:
if test_mode: if test_mode:
print(f" ⚠ 测试模式已启用将显示所有TAPD API调用的详细信息") 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", def _make_request(self, endpoint: str, method: str = "POST",
data: Optional[Dict] = None, params: Optional[Dict] = None) -> Dict: data: Optional[Dict] = None, params: Optional[Dict] = None) -> Dict:
""" """
@ -151,6 +176,12 @@ class TAPDApi:
success=False, success=False,
error_message=error_msg 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}") raise RuntimeError(f"TAPD API调用失败: {error_msg}")
# 记录API调用日志成功 # 记录API调用日志成功
@ -177,9 +208,42 @@ class TAPDApi:
) )
raise RuntimeError(error_msg) raise RuntimeError(error_msg)
except requests.exceptions.HTTPError as e: 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}" error_msg = f"TAPD API HTTP错误: {e}"
if hasattr(e.response, 'text'): if response_text:
error_msg += f"\n响应内容: {e.response.text[:200]}" error_msg += f"\n响应内容: {response_text}"
# 记录API调用日志失败 # 记录API调用日志失败
self.logger.log_api_call( self.logger.log_api_call(
api_type="tapd", api_type="tapd",
@ -299,6 +363,64 @@ class TAPDApi:
return bug_info 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: def get_bug_url(self, bug_id: str) -> str:
""" """
生成bug的访问URL 生成bug的访问URL

View File

@ -26,12 +26,13 @@ class TokenManager:
# 提前刷新时间在token过期前5分钟就刷新 # 提前刷新时间在token过期前5分钟就刷新
REFRESH_BEFORE_EXPIRE = 300 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管理器 初始化Token管理器
Args: Args:
cache_file_path: token缓存文件路径如果为None则使用默认路径 cache_file_path: token缓存文件路径如果为None则使用默认路径
logger: 可选日志实例不传则使用默认任务一日志器
""" """
# 确定缓存文件路径 # 确定缓存文件路径
if cache_file_path is None: if cache_file_path is None:
@ -46,7 +47,7 @@ class TokenManager:
self.corpsecret = os.environ.get('CORPSECRET') 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): def _validate_env_config(self):
""" """

View File

@ -14,7 +14,7 @@ from src.api_logger import get_logger
class WeWorkNotifier: 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 access_token: 企业微信access_token
agentid: 应用ID agentid: 应用ID
receivers: 接收人列表用户ID多个用|分隔@all表示全部成员 receivers: 接收人列表用户ID多个用|分隔@all表示全部成员
logger: 可选日志实例不传则使用默认任务一日志器
""" """
self.access_token = access_token self.access_token = access_token
self.agentid = agentid self.agentid = agentid
self.receivers = receivers self.receivers = receivers
self.base_url = "https://qyapi.weixin.qq.com/cgi-bin" 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: def send_validation_failure_notification(self, invalid_records: List[Dict]) -> bool:
""" """
@ -50,6 +51,23 @@ class WeWorkNotifier:
# 发送消息 # 发送消息
return self._send_text_message(content) return self._send_text_message(content)
def send_operation_failure_notification(self, title: str, failure_records: List[Dict]) -> bool:
"""
发送运行异常通知
Args:
title: 通知标题
failure_records: 异常记录列表每条记录建议包含 sheet_titlerecord_idtitleerror_message
Returns:
bool: 是否发送成功
"""
if not failure_records:
return True
content = self._build_operation_failure_message(title, failure_records)
return self._send_text_message(content)
def _build_failure_message(self, invalid_records: List[Dict]) -> str: def _build_failure_message(self, invalid_records: List[Dict]) -> str:
""" """
构造校验失败消息内容支持多子表分组 构造校验失败消息内容支持多子表分组
@ -108,6 +126,54 @@ class WeWorkNotifier:
return "\n".join(lines) 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: def _send_text_message(self, content: str) -> bool:
""" """
发送文本消息 发送文本消息

3
src2/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
任务二TAPD状态实时同步至腾讯智能表格
"""

196
src2/config.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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:
"""
测试3TAPD链接提取
Returns:
bool: 测试是否通过
"""
print_separator("测试3TAPD链接提取")
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)))
# 测试3TAPD链接提取
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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
# 任务三TAPD过期单推送

171
src3/config.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

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