From 0769cf475537d0e97fb91996b48a530532a57050 Mon Sep 17 00:00:00 2001 From: zelong <2895587166@qq.com> Date: Sat, 28 Feb 2026 17:16:30 +0800 Subject: [PATCH] =?UTF-8?q?'=E4=BF=AE=E5=A4=8D=E4=BB=BB=E5=8A=A12=E5=8D=95?= =?UTF-8?q?=E6=AC=A1=E5=90=8C=E6=AD=A5=E8=AE=B0=E5=BD=95=E8=BF=87=E5=A4=9A?= =?UTF-8?q?=E8=A7=A6=E5=8F=91429=E9=99=90=E6=B5=81=EF=BC=8C=E4=BC=9A?= =?UTF-8?q?=E6=9A=82=E5=81=9C1=E5=88=86=E9=92=9F'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config_task2.ini | 7 +- config/token_cache.json | 4 +- core/__init__.py | 14 +++ core/global_log_system.py | 210 +++++++++++++++++++++++++++++++++++ docs/全局框架文档.md | 111 ++++++++++++++++++ docs/全局迭代日志.md | 105 ++++++++++++++++++ docs/日志系统重构实施方案.md | 119 ++++++++++++++++++++ src2/tapd_api.py | 163 +++++++++++++++++---------- 8 files changed, 670 insertions(+), 63 deletions(-) create mode 100644 core/__init__.py create mode 100644 core/global_log_system.py create mode 100644 docs/全局框架文档.md create mode 100644 docs/全局迭代日志.md create mode 100644 docs/日志系统重构实施方案.md diff --git a/config/config_task2.ini b/config/config_task2.ini index cade10c..2944b46 100644 --- a/config/config_task2.ini +++ b/config/config_task2.ini @@ -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 diff --git a/config/token_cache.json b/config/token_cache.json index 6ed3d05..fa6f0ce 100644 --- a/config/token_cache.json +++ b/config/token_cache.json @@ -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 } \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..bb34427 --- /dev/null +++ b/core/__init__.py @@ -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", +] + diff --git a/core/global_log_system.py b/core/global_log_system.py new file mode 100644 index 0000000..428b91e --- /dev/null +++ b/core/global_log_system.py @@ -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") + diff --git a/docs/全局框架文档.md b/docs/全局框架文档.md new file mode 100644 index 0000000..25a03c1 --- /dev/null +++ b/docs/全局框架文档.md @@ -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接线。 diff --git a/docs/全局迭代日志.md b/docs/全局迭代日志.md new file mode 100644 index 0000000..3772cc6 --- /dev/null +++ b/docs/全局迭代日志.md @@ -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` +- **备注**:下一阶段优先完成任务一接入并验证“单次调用单条记录”。 diff --git a/docs/日志系统重构实施方案.md b/docs/日志系统重构实施方案.md new file mode 100644 index 0000000..df4e7c6 --- /dev/null +++ b/docs/日志系统重构实施方案.md @@ -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` 可被查看工具读取。 + diff --git a/src2/tapd_api.py b/src2/tapd_api.py index d122e59..e388016 100644 --- a/src2/tapd_api.py +++ b/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: """