'修复任务2单次同步记录过多触发429限流,会暂停1分钟'
This commit is contained in:
parent
2d7fa8ea7a
commit
0769cf4755
@ -9,8 +9,8 @@ workspace_id = 58335167
|
||||
# 支持配置多个docid,用逗号分隔,例如:
|
||||
# docid = doc1,doc2,doc3
|
||||
# 同步时会依次对所有表格进行同步
|
||||
#docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg
|
||||
docid =dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA
|
||||
docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg
|
||||
#docid =dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA
|
||||
|
||||
[Schedule]
|
||||
# 同步频率(分钟)
|
||||
@ -20,4 +20,5 @@ sync_interval = 30
|
||||
# 企业微信应用ID
|
||||
agentid = 1000615
|
||||
# 接收人列表(用户ID,多个用|分隔,@all表示全部成员)
|
||||
receivers = 046364
|
||||
# receivers = 046364
|
||||
receivers = 040005
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"access_token": "2LMUjFN9nR32QOSk-RYWTfpdYRL4CLtdgk10JWam9o4mvbfTnsLt8a0ODt-S_eu7MFJ4CFnC6fjLMbAUmfOufPXZrwa2sndigq7xoTWnUqeGqsu_2YcRW3VwilGMJuMG1_6_SYJgNwizMoS8BapKpGW1b37i1ITlQERhRR-iCyO-4DOezbQzb_07y_G1XNM1T3uyo09BNvfIIRx1yGAbmyv_63koAjW4LG3x_AbZnPE",
|
||||
"fetch_time": 1770177536.5388677
|
||||
"access_token": "GQKX61Nh9c5A4Mp0FDOGy4nZgnFok2gefTA0_X4Y5PEL7NkpD9UHWwji3lWkZLsHMJf3dpbJ_l-NdichZ5qSZuPhF7kJNU47Blf2yLQRqFctmXMU6m1cWU80iLiY0vrX2EPzvldaHMR-al3HgKK6PUSU9T2a5Xp-lCjh9StPzEnQJUnwickV4PiPegLLGcH5F6jcM-9pHztkJ6pSV6bfP5QFFATAldmj-71Occib9V0",
|
||||
"fetch_time": 1772269143.6670134
|
||||
}
|
||||
14
core/__init__.py
Normal file
14
core/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""全局核心模块导出"""
|
||||
|
||||
from core.global_log_system import (
|
||||
GlobalLogSystem,
|
||||
create_task1_log_system,
|
||||
create_task2_log_system,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"GlobalLogSystem",
|
||||
"create_task1_log_system",
|
||||
"create_task2_log_system",
|
||||
]
|
||||
|
||||
210
core/global_log_system.py
Normal file
210
core/global_log_system.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""
|
||||
全局日志系统内核
|
||||
|
||||
职责:
|
||||
1. 按天写入 jsonl 日志
|
||||
2. 统一记录 API 调用事件
|
||||
3. 记录同步开始/结束事件与统计
|
||||
4. 对 token 等敏感字段做脱敏
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class GlobalLogSystem:
|
||||
"""全局日志记录器"""
|
||||
|
||||
ALLOWED_MODULES = {"smartsheet", "tapd", "wework"}
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"access_token",
|
||||
"token",
|
||||
"authorization",
|
||||
"corpsecret",
|
||||
"api_password",
|
||||
"password",
|
||||
"secret",
|
||||
"auth",
|
||||
}
|
||||
|
||||
TOKEN_PATTERNS = [
|
||||
re.compile(r"(access_token=)([^&\s]+)", re.IGNORECASE),
|
||||
re.compile(r"(corpsecret=)([^&\s]+)", re.IGNORECASE),
|
||||
re.compile(r"(token=)([^&\s]+)", re.IGNORECASE),
|
||||
]
|
||||
|
||||
def __init__(self, task_name: str, log_dir: str | Path):
|
||||
self.task_name = task_name
|
||||
self.log_dir = Path(log_dir)
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._active_sync_id: Optional[str] = None
|
||||
|
||||
def start_sync(self,
|
||||
trigger: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
sync_id: Optional[str] = None) -> str:
|
||||
"""记录一次同步开始事件并返回 sync_id"""
|
||||
if sync_id is None:
|
||||
sync_id = self._generate_sync_id()
|
||||
|
||||
event = {
|
||||
"event_type": "start_sync",
|
||||
"timestamp": self._now_string(),
|
||||
"task": self.task_name,
|
||||
"sync_id": sync_id,
|
||||
"trigger": trigger,
|
||||
"metadata": self._sanitize(metadata or {}),
|
||||
}
|
||||
|
||||
self._active_sync_id = sync_id
|
||||
self._write_event_safely(event)
|
||||
return sync_id
|
||||
|
||||
def log_api(self,
|
||||
module: str,
|
||||
operation: str,
|
||||
request_data: Optional[Dict[str, Any]],
|
||||
response_data: Optional[Dict[str, Any]],
|
||||
success: bool,
|
||||
error_message: Optional[str] = None,
|
||||
duration_ms: Optional[int] = None,
|
||||
sync_id: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""记录单次 API 调用事件"""
|
||||
if module not in self.ALLOWED_MODULES:
|
||||
raise ValueError(
|
||||
f"module 不合法: {module},允许值: {sorted(self.ALLOWED_MODULES)}"
|
||||
)
|
||||
|
||||
resolved_sync_id = sync_id or self._active_sync_id
|
||||
event = {
|
||||
"event_type": "api_call",
|
||||
"timestamp": self._now_string(),
|
||||
"task": self.task_name,
|
||||
"sync_id": resolved_sync_id,
|
||||
"module": module,
|
||||
"operation": operation,
|
||||
"success": success,
|
||||
"request": self._sanitize(request_data or {}),
|
||||
"response": self._sanitize(response_data or {}),
|
||||
"error_message": self._sanitize_text(error_message),
|
||||
"duration_ms": duration_ms,
|
||||
"extra": self._sanitize(extra or {}),
|
||||
}
|
||||
|
||||
self._write_event_safely(event)
|
||||
|
||||
def end_sync_with_stats(self,
|
||||
stats: Dict[str, Any],
|
||||
success: bool,
|
||||
error_message: Optional[str] = None,
|
||||
sync_id: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""记录一次同步结束事件"""
|
||||
resolved_sync_id = sync_id or self._active_sync_id
|
||||
event = {
|
||||
"event_type": "end_sync",
|
||||
"timestamp": self._now_string(),
|
||||
"task": self.task_name,
|
||||
"sync_id": resolved_sync_id,
|
||||
"success": success,
|
||||
"stats": self._sanitize(stats),
|
||||
"error_message": self._sanitize_text(error_message),
|
||||
"extra": self._sanitize(extra or {}),
|
||||
}
|
||||
|
||||
self._write_event_safely(event)
|
||||
|
||||
if resolved_sync_id == self._active_sync_id:
|
||||
self._active_sync_id = None
|
||||
|
||||
def _write_event_safely(self, event: Dict[str, Any]) -> None:
|
||||
"""安全写入:日志失败不影响主流程"""
|
||||
try:
|
||||
self._write_event(event)
|
||||
except Exception as exc:
|
||||
print(f"⚠️ 全局日志写入失败: {exc}")
|
||||
|
||||
def _write_event(self, event: Dict[str, Any]) -> None:
|
||||
log_file = self._get_today_log_file()
|
||||
with open(log_file, "a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
|
||||
def _get_today_log_file(self) -> Path:
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
return self.log_dir / f"api_log_{today}.jsonl"
|
||||
|
||||
def _generate_sync_id(self) -> str:
|
||||
time_part = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
random_part = uuid.uuid4().hex[:8]
|
||||
return f"{self.task_name}_{time_part}_{random_part}"
|
||||
|
||||
def _now_string(self) -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def _sanitize(self, value: Any, parent_key: str = "") -> Any:
|
||||
if isinstance(value, dict):
|
||||
sanitized: Dict[str, Any] = {}
|
||||
for key, item in value.items():
|
||||
key_lower = str(key).lower()
|
||||
if key_lower in self.SENSITIVE_KEYS:
|
||||
sanitized[key] = self._mask_sensitive_value(item)
|
||||
else:
|
||||
sanitized[key] = self._sanitize(item, parent_key=key_lower)
|
||||
return sanitized
|
||||
|
||||
if isinstance(value, list):
|
||||
return [self._sanitize(item, parent_key=parent_key) for item in value]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
return tuple(self._sanitize(item, parent_key=parent_key) for item in value)
|
||||
|
||||
if isinstance(value, str):
|
||||
if parent_key in self.SENSITIVE_KEYS:
|
||||
return self._mask_sensitive_value(value)
|
||||
return self._sanitize_text(value)
|
||||
|
||||
return value
|
||||
|
||||
def _sanitize_text(self, text: Optional[str]) -> Optional[str]:
|
||||
if text is None:
|
||||
return None
|
||||
|
||||
masked = text
|
||||
for pattern in self.TOKEN_PATTERNS:
|
||||
masked = pattern.sub(r"\1***", masked)
|
||||
|
||||
return masked
|
||||
|
||||
def _mask_sensitive_value(self, value: Any) -> str:
|
||||
text = "" if value is None else str(value)
|
||||
if not text:
|
||||
return "***"
|
||||
|
||||
if text.lower().startswith("bearer "):
|
||||
return "Bearer ***"
|
||||
|
||||
if len(text) <= 6:
|
||||
return "***"
|
||||
|
||||
return f"{text[:3]}***{text[-2:]}"
|
||||
|
||||
|
||||
def create_task1_log_system(project_root: Optional[Path] = None) -> GlobalLogSystem:
|
||||
"""创建任务一日志系统(固定 logs/)"""
|
||||
root = project_root or Path(__file__).parent.parent
|
||||
return GlobalLogSystem(task_name="task1", log_dir=root / "logs")
|
||||
|
||||
|
||||
def create_task2_log_system(project_root: Optional[Path] = None) -> GlobalLogSystem:
|
||||
"""创建任务二日志系统(固定 logs2/)"""
|
||||
root = project_root or Path(__file__).parent.parent
|
||||
return GlobalLogSystem(task_name="task2", log_dir=root / "logs2")
|
||||
|
||||
111
docs/全局框架文档.md
Normal file
111
docs/全局框架文档.md
Normal file
@ -0,0 +1,111 @@
|
||||
# 全局框架文档
|
||||
|
||||
> 用途:统一维护项目模块、脚本职责与接口定义,避免调用不存在接口与跨任务耦合失控。
|
||||
> 范围:`src/`、`src2/`,以及新增的全局模块(如后续 `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(...)`。
|
||||
- `src/smartsheet.py`
|
||||
- **职责**:智能表格 API 封装。
|
||||
- **接口(对内)**:`get_sheet_list`、`get_fields`、`get_records`、`update_records` 等。
|
||||
- `src/tapd_api.py`
|
||||
- **职责**:TAPD Bug API 封装(创建、查询、附件上传)。
|
||||
- **接口(对内)**:`create_bug`、`get_bug`、`upload_attachment` 等。
|
||||
- `src/token_manager.py`
|
||||
- **职责**:企业微信 `access_token` 获取与缓存。
|
||||
- **接口(对内)**:`get_token()`。
|
||||
- `src/wework_notifier.py`
|
||||
- **职责**:企业微信消息通知。
|
||||
- **接口(对内)**:`send_validation_failure_notification(...)`。
|
||||
- `src/api_logger.py`
|
||||
- **职责**:现有日志记录器(后续将作为兼容层)。
|
||||
|
||||
## 2.2 任务二(`src2/`)
|
||||
- `src2/scheduler.py`
|
||||
- **职责**:任务二调度入口,定时触发同步。
|
||||
- **关键依赖**:`src2/sync_service.py`。
|
||||
- `src2/sync_service.py`
|
||||
- **职责**:任务二同步编排(读取、解析链接、查询 TAPD、回写、通知)。
|
||||
- **接口(对内)**:`sync_once()`。
|
||||
- `src2/smartsheet.py`
|
||||
- **职责**:任务二智能表格 API 适配层。
|
||||
- **关键点**:应固定写入 `logs2/`。
|
||||
- `src2/smartsheet_sync.py`
|
||||
- **职责**:任务二表格字段检查、记录提取与回写构造。
|
||||
- `src2/tapd_api.py`
|
||||
- **职责**:任务二 TAPD Story 查询与状态映射。
|
||||
- `src2/notifier.py`
|
||||
- **职责**:任务二失败通知封装(当前复用任务一通知器,存在串目录风险)。
|
||||
- `src2/logger.py`
|
||||
- **职责**:任务二日志实例入口(后续接入统一内核)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现存问题与待改造点
|
||||
|
||||
- **串目录问题**:任务二复用 `TokenManager`、`WeWorkNotifier` 可能写入 `logs/`。
|
||||
- **双记录矛盾**:同一次请求在部分分支可能出现先成功后失败两条记录。
|
||||
- **写入稳定性问题**:现有按 JSON 数组拼接的策略会造成结构损坏风险。
|
||||
- **同步边界缺失**:缺少标准化 `start_sync` / `end_sync` 分隔与统计记录。
|
||||
|
||||
---
|
||||
|
||||
## 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接线。
|
||||
105
docs/全局迭代日志.md
Normal file
105
docs/全局迭代日志.md
Normal file
@ -0,0 +1,105 @@
|
||||
# 全局迭代日志
|
||||
|
||||
> 用途:跨任务(`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`
|
||||
- **备注**:下一阶段优先完成任务一接入并验证“单次调用单条记录”。
|
||||
119
docs/日志系统重构实施方案.md
Normal file
119
docs/日志系统重构实施方案.md
Normal file
@ -0,0 +1,119 @@
|
||||
# 日志系统重构实施方案(任务一 + 任务二)
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
### 1.1 重构目标
|
||||
- 在任务一和任务二之外,提供统一的全局日志记录系统。
|
||||
- 覆盖生产链路:`src/scheduler.py` 与 `src2/scheduler.py` 触发的同步流程。
|
||||
- 在每一个发起 API 调用的地方记录请求与结果(成功/失败都记录)。
|
||||
- 任务一日志落在 `logs/`,任务二日志落在 `logs2/`。
|
||||
- 每条日志必须包含 `module` 字段,允许值:`smartsheet` / `tapd` / `wework`。
|
||||
- 通过 `sync_id` 分隔每次同步,并在同步完成后写入当次统计信息。
|
||||
|
||||
### 1.2 明确要解决的问题
|
||||
- 解决“串目录”问题:任务二不得写入 `logs/`,任务一不得写入 `logs2/`。
|
||||
- 解决“双记录矛盾”问题:同一次 API 调用只能有一条最终记录(不能先 success 再 failure)。
|
||||
- 日志格式改为 `jsonl`(每行一条完整 JSON 记录)。
|
||||
- 对 token 做脱敏,不做响应内容截断。
|
||||
|
||||
### 1.3 非目标
|
||||
- 本次不改业务流程语义(只改日志系统与接线方式)。
|
||||
- 本次不对历史 JSON 文件做离线迁移,仅保证新写入生效。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体设计
|
||||
|
||||
### 2.1 新增全局日志内核(任务外)
|
||||
- 新增独立模块(建议路径:`core/global_log_system.py`)。
|
||||
- 提供统一接口:
|
||||
- `start_sync(...)`:开始一次同步,生成 `sync_id`。
|
||||
- `log_api(...)`:记录单次 API 调用结果(成功/失败统一出口)。
|
||||
- `end_sync_with_stats(...)`:写入同步统计并结束。
|
||||
- 通过构造参数确定任务上下文:`task_name`、`log_dir`、默认 `module`。
|
||||
|
||||
### 2.2 日志存储格式
|
||||
- 文件命名:`api_log_YYYY-MM-DD.jsonl`。
|
||||
- 存储目录:任务一 `logs/`,任务二 `logs2/`。
|
||||
- 记录模型:
|
||||
- 通用字段:`event_type`、`timestamp`、`task`、`sync_id`、`module`。
|
||||
- API 事件字段:`operation`、`request`、`response`、`success`、`error_message`、`duration_ms`。
|
||||
- 同步边界字段:`start_sync` / `end_sync` 事件及统计 `stats`。
|
||||
|
||||
### 2.3 脱敏规则
|
||||
- `request/response` 中出现 token 字段统一脱敏(如 `access_token`、`Authorization`、`corpsecret`、`api_password`)。
|
||||
- 脱敏策略:保留前后少量字符,中间替换为 `***`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分阶段实施路线(小步走)
|
||||
|
||||
## 阶段0:文档与约定先行
|
||||
### 工作内容
|
||||
- 新建并维护两份全局文档:
|
||||
- `docs/全局迭代日志.md`
|
||||
- `docs/全局框架文档.md`
|
||||
- 在文档中建立日志重构专属章节、字段定义、阶段验收标准。
|
||||
|
||||
### 验收标准
|
||||
- 文档结构可用于后续持续更新。
|
||||
- 明确字段与职责边界(尤其是 `sync_id`、`task`、`module`)。
|
||||
|
||||
## 阶段1:实现全局日志内核
|
||||
### 工作内容
|
||||
- 新增 `core/global_log_system.py`,实现 `jsonl` 写入、脱敏、同步分隔和统计写入。
|
||||
- 新增兼容层,避免一次性改动过大。
|
||||
|
||||
### 验收标准
|
||||
- 能独立写入 `logs/` / `logs2/`。
|
||||
- `start_sync -> 多条 log_api -> end_sync_with_stats` 链路完整。
|
||||
|
||||
## 阶段2:接入任务一(src)
|
||||
### 工作内容
|
||||
- 改造 `src/api_logger.py` 为新内核兼容封装。
|
||||
- 在任务一同步主流程增加 `sync_id` 生命周期。
|
||||
- 覆盖所有 API 调用记录点,统一单次调用单条记录。
|
||||
|
||||
### 验收标准
|
||||
- 任务一生产链路日志全部位于 `logs/`。
|
||||
- 无“双记录矛盾”。
|
||||
|
||||
## 阶段3:接入任务二(src2)
|
||||
### 工作内容
|
||||
- 改造 `src2/logger.py` 接入同一内核并固定 `logs2/`。
|
||||
- 在任务二同步主流程增加 `sync_id` 生命周期。
|
||||
- 修复复用模块导致的串目录问题(含 token 获取、wework 通知链路)。
|
||||
|
||||
### 验收标准
|
||||
- 任务二生产链路日志全部位于 `logs2/`。
|
||||
- 与任务一日志完全隔离。
|
||||
|
||||
## 阶段4:查看工具与收尾
|
||||
### 工作内容
|
||||
- 更新 `src/log_viewer.py` 支持读取 `jsonl`。
|
||||
- 更新 `logs/README.md`(新增 jsonl 规范与排障说明)。
|
||||
|
||||
### 验收标准
|
||||
- 可按日期与 `sync_id` 追踪一次完整同步。
|
||||
- 文档、代码、日志格式三者一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心风险与规避
|
||||
- 风险1:兼容层改造影响原调用。
|
||||
- 规避:先保留 `log_api_call(...)` 旧接口,再逐步替换调用。
|
||||
- 风险2:任务二通过复用模块写错目录。
|
||||
- 规避:统一使用可注入 logger/context,禁止隐式全局默认目录。
|
||||
- 风险3:单次请求出现多状态记录。
|
||||
- 规避:采用“单出口记录”,业务错误也只写一次最终状态。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收清单(最终)
|
||||
- [ ] 生产链路(两个 scheduler)所有 API 调用均有日志。
|
||||
- [ ] 每次同步都有 `start_sync` 与 `end_sync`,并可通过 `sync_id` 聚合。
|
||||
- [ ] 任务一只写 `logs/`,任务二只写 `logs2/`。
|
||||
- [ ] 无同一次 API 调用 success/failure 双写冲突。
|
||||
- [ ] token 已脱敏。
|
||||
- [ ] `jsonl` 可被查看工具读取。
|
||||
|
||||
163
src2/tapd_api.py
163
src2/tapd_api.py
@ -9,6 +9,7 @@ TAPD API调用模块(任务二专用)
|
||||
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
from typing import Dict, Optional, Any
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
@ -105,7 +106,7 @@ class TAPDStoryApi:
|
||||
|
||||
def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Dict:
|
||||
"""
|
||||
发起TAPD API GET请求的通用方法
|
||||
发起TAPD API GET请求的通用方法(支持429错误重试)
|
||||
|
||||
Args:
|
||||
endpoint: API端点(如 "stories")
|
||||
@ -138,76 +139,122 @@ class TAPDStoryApi:
|
||||
for key, value in params.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
try:
|
||||
response = self.session.get(
|
||||
url,
|
||||
params=params,
|
||||
auth=self.auth,
|
||||
timeout=30
|
||||
)
|
||||
# 429错误重试逻辑:最多重试1次
|
||||
max_retries = 1
|
||||
retry_count = 0
|
||||
|
||||
# 测试模式:显示响应信息
|
||||
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)
|
||||
while retry_count <= max_retries:
|
||||
try:
|
||||
response = self.session.get(
|
||||
url,
|
||||
params=params,
|
||||
auth=self.auth,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
# 测试模式:显示响应信息
|
||||
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)
|
||||
|
||||
# 检查TAPD API返回的状态
|
||||
if result.get('status') != 1:
|
||||
error_msg = result.get('info', '未知错误')
|
||||
# 检查是否是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(f"TAPD API调用失败: {error_msg}")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# 记录成功日志
|
||||
self.logger.log_api_call(
|
||||
api_type="tapd",
|
||||
operation=endpoint,
|
||||
request_data=log_request_data,
|
||||
response_data=result,
|
||||
success=True
|
||||
)
|
||||
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)
|
||||
|
||||
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:
|
||||
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:
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user