'task3v2'

This commit is contained in:
zelong 2026-04-01 16:01:58 +08:00
parent af7243cf4e
commit 6806e002c7
7 changed files with 395 additions and 134 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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`

View File

@ -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 段落)
- **备注**:可在后续补“消息模板配置化”减少文案硬编码。

View File

@ -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"}

View File

@ -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}")

View File

@ -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 = {}