feat:飞书文档补全

This commit is contained in:
Evilom
2026-06-08 10:04:12 +08:00
parent ab2ca1d836
commit 3b77605591
659 changed files with 51251 additions and 1340 deletions
+298
View File
@@ -0,0 +1,298 @@
import sys
import os
import argparse
from .tb_config import TBConfig
from .teambition_api import TeambitionAPI
from .task_analyzer import TaskAnalyzer
from .scheduler import TaskScheduler
from .dingtalk_sender import DingTalkSender, ImgBBUploader
from .task_manager import TaskManager
# Try to import table renderer (uses matplotlib, no external dependencies)
try:
from .table_renderer import TableImageRenderer
HAS_TABLE_RENDERER = True
print("[Info] table_renderer loaded successfully (matplotlib mode)")
except ImportError as e:
HAS_TABLE_RENDERER = False
print(f"[Warning] table_renderer not available: {e}. Will use markdown text instead.")
def fetch_and_analyze(target_robot=None, send_to_dingtalk=True, mode='daily'):
config = TBConfig()
api = TeambitionAPI(config)
analyzer = TaskAnalyzer(config)
task_manager = TaskManager(config.get('output.state_file', './data/task_state.json'))
print(f"\n{'='*60}")
print(f"开始获取 Teambition 任务数据 - {config.get('teambition.project_id')}")
print(f"{'='*60}\n")
csv_content = api.fetch_tasks()
if csv_content:
csv_path = api.save_csv(csv_content)
if csv_path:
df = analyzer.load_csv(csv_path)
if not df.empty:
# Fetch activities to credit users for reassigned tasks
# We only care about tasks that are NOT in "To Do" state (i.e. have been worked on)
# Filter for tasks that are "Done" or "In Progress" or "Testing"
# To be safe and simple, we fetch for all tasks that have an ID.
# Find ID column first
id_col = next((c for c in ['任务 ObjectId', '_id', 'id', 'taskId', '任务ID'] if c in df.columns), None)
activities = []
if id_col:
task_ids = df[id_col].dropna().astype(str).tolist()
# Filter out empty strings
task_ids = [t for t in task_ids if t and t != 'nan']
if len(task_ids) > 0:
print(f"准备获取 {len(task_ids)} 个任务的动态以校准贡献统计...")
try:
activities = api.fetch_tasks_activities(task_ids)
except Exception as e:
print(f"[Warning] 获取任务动态失败: {e}")
summary = analyzer.analyze_tasks(df, activities)
analyzer.save_summary(summary)
analyzer.print_summary(summary)
# State Tracking & Diffing
current_tasks_dict = analyzer.get_task_dict(df)
old_state = task_manager.load_state()
prev_tasks = old_state.get('tasks', {})
changes = task_manager.detect_changes(prev_tasks, current_tasks_dict)
if changes:
print("\n[变动检测] 发现以下变动:")
for c in changes:
print(f" - {c}")
else:
print("\n[变动检测] 无变动")
# Save new state
task_manager.save_state(current_tasks_dict)
# AI Analysis (if enabled)
ai_analysis = None
ai_conf = config.get('ai', {})
if ai_conf.get('enabled') and send_to_dingtalk and mode == 'daily':
try:
from .ai_analyzer import AIAnalyzer
print("\n正在执行 AI 项目分析...")
ai_analyzer = AIAnalyzer(config)
ai_analysis = ai_analyzer.run_full_analysis()
print(f"[AI] 分析完成: {ai_analysis.get('summary', 'N/A')}")
except Exception as e:
print(f"[Warning] AI 分析失败: {e}")
# Cleanup CSV
if csv_path and os.path.exists(csv_path):
try:
os.remove(csv_path)
print(f"\n[清理] 已删除 CSV 文件: {csv_path}")
except Exception as e:
print(f"\n[清理] 删除文件失败: {e}")
# Send DingTalk Notification
dingtalk_config = config.get('dingtalk', {})
if dingtalk_config.get('enabled') and send_to_dingtalk:
print("\n正在发送钉钉通知...")
dingtalk_switches = dingtalk_config.get('switches', {})
user_roles = config.get('user_roles', {})
user_mobiles = config.get('user_mobiles', {})
# Support multiple robots
robots = dingtalk_config.get('robots', {})
# Backward compatibility or direct webhook
if not robots and dingtalk_config.get('webhook'):
robots = {
'default': {
'webhook': dingtalk_config.get('webhook'),
'secret': dingtalk_config.get('secret')
}
}
if not robots:
print("未配置 DingTalk 机器人")
else:
# Prepare report content once
temp_sender = DingTalkSender("") # Helper to format
# format_task_report now returns (report_content, at_mobiles_list)
dashboard_url = config.get('dashboard_url')
if mode == 'interval':
# In interval mode, we only check for parallel tasks and alert independently
# We do NOT send the full report
if not dingtalk_switches.get('interval_alert', True):
print("Interval Alert disabled by switch.")
return True
print("Running in Interval Mode: Checking for parallel tasks...")
for name, bot_cfg in robots.items():
if target_robot and name != target_robot: continue
webhook = bot_cfg.get('webhook')
secret = bot_cfg.get('secret')
if not webhook: continue
sender = DingTalkSender(webhook)
sent = sender.send_parallel_task_alert(
summary, user_roles, user_mobiles, secret=secret
)
if sent:
print(f"Parallel Task Alert sent to {name}")
else:
print("No parallel tasks detected or alert not needed.")
else:
# Daily / Full Mode
# Try to render as image first
image_url = None
task_list = []
imgbb_key = config.get('imgbb.api_key')
if HAS_TABLE_RENDERER and imgbb_key:
print("[图片模式] 正在生成报表图片...")
try:
renderer = TableImageRenderer(config.get('output.csv_dir', './data'))
image_path, task_list = renderer.render_task_report(
summary,
user_roles,
dashboard_url=dashboard_url,
switches=dingtalk_switches
)
if image_path:
print(f"[图片模式] 上传到 ImgBB...")
uploader = ImgBBUploader(imgbb_key)
image_url = uploader.upload_image(image_path)
if image_url:
print(f"[图片模式] 上传成功: {image_url}")
else:
print("[图片模式] 上传失败,回退到 Markdown 模式")
else:
print("[图片模式] 渲染失败,回退到 Markdown 模式")
except Exception as e:
print(f"[图片模式] 错误: {e},回退到 Markdown 模式")
else:
if not HAS_TABLE_RENDERER:
print("[通知] 未安装 imgkit,使用 Markdown 模式")
if not imgbb_key:
print("[通知] 未配置 ImgBB API Key,使用 Markdown 模式")
# Prepare markdown report as fallback or for text content
report, at_mobiles_list = temp_sender.format_task_report(
summary,
user_roles,
user_mobiles,
dashboard_url=dashboard_url,
switches=dingtalk_switches
)
# Only add diff if enabled
if dingtalk_switches.get('daily_diff', True):
final_report = temp_sender.format_diff_report(changes, report)
else:
final_report = report
for name, bot_cfg in robots.items():
# Filter by target_robot if specified
if target_robot and name != target_robot:
continue
webhook = bot_cfg.get('webhook')
secret = bot_cfg.get('secret')
if not webhook:
continue
print(f"Sending to robot: {name}")
sender = DingTalkSender(webhook)
# Send image if available, otherwise markdown
if image_url:
# RPG Style summary from DingTalkSender
rpg_summary = temp_sender.get_rpg_summary(summary)
# Construction of the RPG Battle Report
brief_text = f"{rpg_summary}\n\n"
brief_text += f"![任务报表]({image_url})\n\n"
# Add task jump buttons (just numbers)
if task_list:
brief_text += "**🔗 快速跳转:**\n"
buttons = []
# Use target task list or summary's recent wins as fallback?
# renderer provides task_list which matches the image.
for i, task in enumerate(task_list[:20], 1):
task_id = task.get('task_id')
if task_id:
task_url = f"https://www.teambition.com/task/{task_id}"
buttons.append(f"[[{i:02}]]({task_url})")
for j in range(0, len(buttons), 5):
brief_text += " ".join(buttons[j:j+5]) + "\n"
brief_text += "\n"
if dashboard_url:
brief_text += f"👉 [**进入指挥部(看板)查看全局态势**]({dashboard_url})\n"
# Add anomaly alerts if any
if dingtalk_switches.get('daily_anomaly', True):
alert_text, alert_mobiles = temp_sender.generate_anomaly_alerts(
summary, user_roles, user_mobiles
)
if alert_text:
brief_text += "\n---\n" + alert_text
at_mobiles_list.extend(alert_mobiles)
# Add AI insight if available
if ai_analysis:
ai_insight = temp_sender.format_ai_insight(ai_analysis)
if ai_insight:
brief_text += ai_insight
sender.send_markdown(f"⚔️ 工作室今日战报 | {summary.get('updated_at', '').split('T')[0]}", brief_text, secret=secret, at_mobiles=at_mobiles_list)
else:
# Fallback to full markdown report
if ai_analysis:
ai_insight = temp_sender.format_ai_insight(ai_analysis)
if ai_insight:
final_report += ai_insight
sender.send_markdown("Teambition 任务日报", final_report, secret=secret, at_mobiles=at_mobiles_list)
return True
return True
print("获取或分析任务数据失败")
return False
def main():
parser = argparse.ArgumentParser(description='Teambition 任务数据获取和汇总工具')
parser.add_argument('--once', action='store_true', help='只执行一次,不启动定时任务')
parser.add_argument('--interval', type=int, default=30, help='定时任务间隔(分钟),默认30分钟')
parser.add_argument('--config', type=str, default='config.json', help='配置文件路径')
parser.add_argument('--robot', type=str, help='指定发送的机器人名称 (如 test 或 default)')
args = parser.parse_args()
config = TBConfig(args.config)
scheduler_config = config.get_scheduler_TBConfig()
if args.once or not scheduler_config.get('enabled', False):
fetch_and_analyze(args.robot)
else:
scheduler = TaskScheduler(config)
interval = args.interval if args.interval != 30 else scheduler_config.get('interval_minutes', 30)
scheduler.schedule_task(fetch_and_analyze, interval)
scheduler.start()
if __name__ == '__main__':
main()