'task3v2'
This commit is contained in:
parent
af7243cf4e
commit
6806e002c7
@ -1,4 +1,4 @@
|
||||
{
|
||||
"access_token": "28gECNTAJqbnb4w4V8SfXKq6EIBaamsIFwrqNy0KkABSCPzZz5BWERID2zg9JXGKnr48Ug3GDqAy46EdY047BQocXTUNdMgFCmzHZHgPJxfKtbqr_4pAq_tGvX5TfWVWbQxu-ZjnNsNLoGef7nLW4E-ykYzRQ-I7qqIZwePYevo6-Q_ezF0ImXDyD9I_LOXmQZ9vuGFWsjYGFddKBQ8bSfePtudJvio5ILiuRgwUsXc",
|
||||
"fetch_time": 1773373351.2013075
|
||||
"access_token": "MzN3pnuz4rhX4CUzHCEgLONP_tzovqGquHfGlKRli6uZmZfdMUlnx6q4FqK6jTOF_xFfE0HSz4H2OhKI8FPS1eo8-EQJgjxLycRMhOFKwqKtEE9oMwyPfi9mwMaeXJUoZfBhlXQOPfOvCN0sL6LD6atthEdmj0VVSyDU_fPJ1XpSkqzWv8cosuG8tbrssX8EZgw90vKT46N0ySi5Mnhe45yY2_V-PH0TtuSHd-D6EgM",
|
||||
"fetch_time": 1775029801.2061243
|
||||
}
|
||||
@ -2,17 +2,30 @@
|
||||
workspace_id = 58335167
|
||||
|
||||
[Schedule]
|
||||
push_time = 15:35
|
||||
push_time = 15:57
|
||||
skip_weekend = true
|
||||
|
||||
[WeWork]
|
||||
# 测试
|
||||
webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=026670e0-9e8d-4f61-9b3f-1f81365954ff
|
||||
# 实际
|
||||
# webhook_url = https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=20dd49fb-7663-4e16-98fe-f0e71d8126fb
|
||||
|
||||
|
||||
[Smartsheet]
|
||||
# 技术组成员配置表
|
||||
# 成员配置文档ID(通过查询子表title自动匹配sheet_id)
|
||||
docid = dcidlinq5l8GWnXWpTVnp0zZDKki40d5fRf3y1Rhu8VXBbvDb2cqeypgtBegSmAFCsW5RwFF5DRl5DiiZXm2vJsA
|
||||
sheet_id = q979lj
|
||||
|
||||
[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=20dd49fb-7663-4e16-98fe-f0e71d8126fb
|
||||
|
||||
|
||||
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=cdb14555-76d3-42e9-affb-0514a5afeafd
|
||||
|
||||
|
||||
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=b1b1e1ce-1129-48f3-bab7-fb211a875469
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 全局框架文档
|
||||
|
||||
> 用途:统一维护项目模块、脚本职责与接口定义,避免调用不存在接口与跨任务耦合失控。
|
||||
> 范围:`src/`、`src2/`,以及新增的全局模块(如后续 `core/`)。
|
||||
> 范围:`src/`、`src2/`、`src3/`,以及新增的全局模块(如后续 `core/`)。
|
||||
|
||||
---
|
||||
|
||||
@ -80,6 +80,26 @@
|
||||
- `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后)
|
||||
@ -88,6 +108,7 @@
|
||||
- **双记录矛盾(任务一)**:已修复;任务二当前未发现同请求双写路径。
|
||||
- **写入稳定性问题**:已由统一 `jsonl` 事件流替代旧数组拼接策略。
|
||||
- **阶段4待办**:日志查看工具尚未完成 `jsonl + sync_id` 体验优化。
|
||||
- **任务三V2注意点**:分组成员若存在交叉,当前会导致同一过期单在多个群重复推送,应通过配置治理。
|
||||
|
||||
---
|
||||
|
||||
@ -120,3 +141,8 @@
|
||||
- 标记阶段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`。
|
||||
|
||||
@ -186,3 +186,77 @@
|
||||
- **可回滚点**:可按文件粒度回滚,优先关注 `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 段落)
|
||||
- **备注**:可在后续补“消息模板配置化”减少文案硬编码。
|
||||
|
||||
176
src3/config.py
176
src3/config.py
@ -1,6 +1,7 @@
|
||||
"""任务三配置管理"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
@ -12,6 +13,9 @@ 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"
|
||||
@ -19,95 +23,149 @@ class Task3ConfigManager(ConfigManager):
|
||||
|
||||
def get_workspace_id(self) -> str:
|
||||
"""获取TAPD工作空间ID"""
|
||||
if not self.config.has_section('TAPD'):
|
||||
if not self.config.has_section("TAPD"):
|
||||
raise ValueError("配置文件缺少[TAPD]节")
|
||||
if not self.config.has_option('TAPD', 'workspace_id'):
|
||||
if not self.config.has_option("TAPD", "workspace_id"):
|
||||
raise ValueError("配置文件[TAPD]节缺少workspace_id配置项")
|
||||
workspace_id = self.config.get('TAPD', 'workspace_id').strip()
|
||||
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'):
|
||||
if not self.config.has_section("Schedule"):
|
||||
return "09:30"
|
||||
return self.config.get('Schedule', 'push_time', fallback='09:30').strip()
|
||||
return self.config.get("Schedule", "push_time", fallback="09:30").strip()
|
||||
|
||||
def get_skip_weekend(self) -> bool:
|
||||
"""是否跳过周末"""
|
||||
if not self.config.has_section('Schedule'):
|
||||
if not self.config.has_section("Schedule"):
|
||||
return True
|
||||
return self.config.getboolean('Schedule', 'skip_weekend', fallback=True)
|
||||
return self.config.getboolean("Schedule", "skip_weekend", fallback=True)
|
||||
|
||||
def get_webhook_url(self) -> str:
|
||||
"""获取企微Webhook URL"""
|
||||
if not self.config.has_section('WeWork'):
|
||||
raise ValueError("配置文件缺少[WeWork]节")
|
||||
if not self.config.has_option('WeWork', 'webhook_url'):
|
||||
raise ValueError("配置文件[WeWork]节缺少webhook_url配置项")
|
||||
webhook_url = self.config.get('WeWork', 'webhook_url').strip()
|
||||
if not webhook_url:
|
||||
raise ValueError("webhook_url配置项不能为空")
|
||||
return webhook_url
|
||||
|
||||
def get_smartsheet_config(self) -> dict:
|
||||
"""获取智能表格配置"""
|
||||
if not self.config.has_section('Smartsheet'):
|
||||
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'):
|
||||
if not self.config.has_option("Smartsheet", "docid"):
|
||||
raise ValueError("配置文件[Smartsheet]节缺少docid配置项")
|
||||
if not self.config.has_option('Smartsheet', 'sheet_id'):
|
||||
raise ValueError("配置文件[Smartsheet]节缺少sheet_id配置项")
|
||||
|
||||
docid = self.config.get('Smartsheet', 'docid').strip()
|
||||
sheet_id = self.config.get('Smartsheet', 'sheet_id').strip()
|
||||
docid = self.config.get("Smartsheet", "docid").strip()
|
||||
if not docid:
|
||||
raise ValueError("docid配置项不能为空")
|
||||
|
||||
if not docid or not sheet_id:
|
||||
raise ValueError("docid和sheet_id配置项不能为空")
|
||||
return {"docid": docid}
|
||||
|
||||
return {'docid': docid, 'sheet_id': sheet_id}
|
||||
def get_group_push_configs(self) -> List[Dict[str, str]]:
|
||||
"""读取多群配置(按顺序一一对应)"""
|
||||
section = self.GROUP_SECTION
|
||||
if not self.config.has_section(section):
|
||||
raise ValueError("配置文件缺少[GroupPush]节")
|
||||
|
||||
def get_tech_team_config(self) -> dict:
|
||||
"""从智能表格加载技术组成员配置
|
||||
返回: {
|
||||
'member_list': ['范宇', '杰割', '周浩'],
|
||||
'user_mapping': {'范宇': 'FanYu', '杰割': 'JieGe', '周浩': 'ZhouHao'}
|
||||
}
|
||||
"""
|
||||
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()
|
||||
docid = smartsheet_config['docid']
|
||||
sheet_id = smartsheet_config['sheet_id']
|
||||
group_push_configs = self.get_group_push_configs()
|
||||
docid = smartsheet_config["docid"]
|
||||
|
||||
# 获取access_token
|
||||
token_manager = TokenManager()
|
||||
token_manager = TokenManager(logger=logger)
|
||||
access_token = token_manager.get_token()
|
||||
|
||||
# 读取智能表格
|
||||
api = SmartSheetAPI(access_token, docid)
|
||||
records_result = api.get_records(sheet_id)
|
||||
records = records_result['records']
|
||||
sheet_list = api.get_sheet_list()
|
||||
title_to_sheet_id = self._build_title_to_sheet_id(sheet_list)
|
||||
|
||||
member_list = []
|
||||
user_mapping = {}
|
||||
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}")
|
||||
|
||||
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, '是否启用')
|
||||
records_result = api.get_records(sheet_id)
|
||||
records = records_result.get("records", [])
|
||||
|
||||
if enabled == '是' and tapd_name and wework_id:
|
||||
member_list.append(tapd_name)
|
||||
user_mapping[tapd_name] = wework_id
|
||||
member_list: List[str] = []
|
||||
user_mapping: Dict[str, str] = {}
|
||||
|
||||
if not member_list:
|
||||
raise ValueError("智能表格中没有启用的技术组成员")
|
||||
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, "是否启用")
|
||||
|
||||
return {
|
||||
'member_list': member_list,
|
||||
'user_mapping': user_mapping
|
||||
}
|
||||
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"}
|
||||
|
||||
189
src3/main.py
189
src3/main.py
@ -2,6 +2,7 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
@ -14,34 +15,33 @@ from src3.message_formatter import MessageFormatter
|
||||
from src3.webhook_sender import WebhookSender
|
||||
|
||||
|
||||
def _send_error_notification(webhook_url: str, logger):
|
||||
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):
|
||||
"""发送错误通知"""
|
||||
import requests
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": "⚠️ 今日过期单提醒获取失败,请人工检查"
|
||||
}
|
||||
}
|
||||
try:
|
||||
requests.post(webhook_url, json=payload, timeout=10)
|
||||
except:
|
||||
pass
|
||||
content = f"⚠️ 今日{group_name}组过期单提醒获取失败,请人工检查"
|
||||
_post_simple_markdown(webhook_url, content)
|
||||
|
||||
|
||||
def _send_no_overdue_message(webhook_url: str, logger):
|
||||
def _send_no_overdue_message(webhook_url: str, group_name: str):
|
||||
"""发送无过期单消息"""
|
||||
import requests
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": "✅ 今日程序组无过期单,大家保持!"
|
||||
}
|
||||
}
|
||||
try:
|
||||
requests.post(webhook_url, json=payload, timeout=10)
|
||||
except:
|
||||
pass
|
||||
content = f"✅ 今日{group_name}组无过期单,大家保持!"
|
||||
_post_simple_markdown(webhook_url, content)
|
||||
|
||||
|
||||
def run_once():
|
||||
@ -52,68 +52,137 @@ def run_once():
|
||||
try:
|
||||
print("任务三:TAPD过期单推送")
|
||||
|
||||
# 1. 加载配置
|
||||
# 1. 加载基础配置
|
||||
try:
|
||||
config = Task3ConfigManager()
|
||||
workspace_id = config.get_workspace_id()
|
||||
webhook_url = config.get_webhook_url()
|
||||
except ValueError as e:
|
||||
print(f"✗ 配置错误: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"配置错误: {e}", sync_id=sync_id)
|
||||
return
|
||||
|
||||
# 2. 加载技术组配置
|
||||
# 2. 加载多组配置(组名/成员子表/Webhook)
|
||||
try:
|
||||
tech_team = config.get_tech_team_config()
|
||||
member_list = tech_team['member_list']
|
||||
user_mapping = tech_team['user_mapping']
|
||||
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)
|
||||
print(f"✗ 组配置错误: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"组配置错误: {e}", sync_id=sync_id)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"✗ 获取技术组配置失败: {e}")
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f"✗ 获取组配置失败: {e}")
|
||||
logger.end_sync_with_stats({}, False, f"配置加载失败: {e}", sync_id=sync_id)
|
||||
return
|
||||
|
||||
print(f"技术组成员: {member_list}")
|
||||
for group in group_team_configs:
|
||||
group_name = group["group_name"]
|
||||
member_count = len(group["member_list"])
|
||||
print(f"{group_name}组成员数: {member_count}")
|
||||
|
||||
# 3. 获取过期单
|
||||
try:
|
||||
fetcher = OverdueFetcher(workspace_id, logger)
|
||||
items = fetcher.fetch_all_overdue(member_list)
|
||||
except Exception as e:
|
||||
print(f"✗ TAPD API调用失败: {e}")
|
||||
_send_error_notification(webhook_url, logger)
|
||||
logger.end_sync_with_stats({}, False, f"TAPD API失败: {e}", sync_id=sync_id)
|
||||
# 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
|
||||
|
||||
if not items:
|
||||
print("✅ 今日程序组无过期单")
|
||||
_send_no_overdue_message(webhook_url, logger)
|
||||
logger.end_sync_with_stats({"overdue_count": 0}, True, sync_id=sync_id)
|
||||
# 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)} 条过期单")
|
||||
|
||||
# 4. 格式化消息
|
||||
formatter = MessageFormatter(logger)
|
||||
# 5. 按组分发推送
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
content, mentioned_list = formatter.format_message(items, user_mapping, today)
|
||||
group_stats: List[Dict] = []
|
||||
failed_groups: List[str] = []
|
||||
skipped_empty_groups: List[str] = []
|
||||
|
||||
if formatter.unmapped_users:
|
||||
print(f"⚠️ 未映射用户: {', '.join(formatter.unmapped_users)}")
|
||||
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"]
|
||||
|
||||
# 5. 发送消息
|
||||
sender = WebhookSender(webhook_url, logger)
|
||||
success = sender.send_markdown(content, mentioned_list)
|
||||
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
|
||||
|
||||
if success:
|
||||
print(f"✓ 推送成功")
|
||||
logger.end_sync_with_stats({"overdue_count": len(items)}, True, sync_id=sync_id)
|
||||
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:
|
||||
print("✗ 推送失败")
|
||||
logger.end_sync_with_stats({"overdue_count": len(items)}, False, "推送失败", sync_id=sync_id)
|
||||
logger.end_sync_with_stats(
|
||||
stats,
|
||||
False,
|
||||
f"推送失败分组: {', '.join(failed_groups)}",
|
||||
sync_id=sync_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 未知错误: {e}")
|
||||
|
||||
@ -10,7 +10,13 @@ class MessageFormatter:
|
||||
self.logger = logger
|
||||
self.unmapped_users = set()
|
||||
|
||||
def format_message(self, items: List[dict], user_mapping: Dict[str, str], date: str = None) -> tuple:
|
||||
def format_message(
|
||||
self,
|
||||
items: List[dict],
|
||||
user_mapping: Dict[str, str],
|
||||
date: str = None,
|
||||
group_name: str = None
|
||||
) -> tuple:
|
||||
"""格式化消息
|
||||
返回: (markdown_content, mentioned_list)
|
||||
"""
|
||||
@ -27,7 +33,11 @@ class MessageFormatter:
|
||||
sorted_groups = self._sort_groups(grouped)
|
||||
|
||||
# 生成Markdown
|
||||
lines = [f"⏰ TAPD 过期单提醒({date})\n\n"]
|
||||
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
|
||||
@ -58,6 +68,17 @@ class MessageFormatter:
|
||||
|
||||
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 = {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user