""" Daily Progress Briefing Service - 每日进度简报服务 核心功能: 1. 为每个角色生成个性化简报 2. 智能过滤无关信息 3. 根据角色优先级排序内容 4. 替代传统日报/周报 个性化规则: - AI工程师:关注模型更新、技术信号、实验结果 - 后端开发:关注API变更、任务阻塞、依赖更新 - 前端开发:关注UI需求、设计变更、接口更新 - BOSS/ADMIN:关注整体进度、风险、决策点 """ import asyncio import json from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy import text, func, and_ from ..models import ( User, Project, Task, Signal, CognitiveConflict, TruthNode, UserRole, ProjectSignalLink ) from .llm import LLMService def _parse_involved_user_ids(raw_value: Any) -> List[int]: """兼容 involved_user_ids 的 list / json-string / 单值字符串。""" if raw_value is None: return [] data = raw_value if isinstance(data, (bytes, bytearray)): data = data.decode("utf-8", errors="ignore") if isinstance(data, str): text = data.strip() if not text: return [] try: data = json.loads(text) except (TypeError, ValueError, json.JSONDecodeError): data = [v.strip() for v in text.split(",") if v.strip()] if not isinstance(data, list): data = [data] parsed: List[int] = [] for item in data: if item is None or isinstance(item, bool): continue try: parsed.append(int(item)) except (TypeError, ValueError): continue return parsed class DailyBriefingService: """每日进度简报服务""" def __init__(self, db: AsyncSession): self.db = db self.llm = LLMService() async def generate_personalized_briefing( self, user_id: int, tenant_id: int = 1 ) -> Dict[str, Any]: """ 生成个性化每日简报 简报内容根据用户角色自动过滤: - 只显示相关项目的内容 - 按优先级排序 - 突出风险和需要行动的事项 """ # 获取用户信息 user_result = await self.db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: raise ValueError(f"User {user_id} not found") # 计算时间范围 now = datetime.now(timezone.utc) yesterday = now - timedelta(days=1) # 根据角色收集相关信息 relevant_data = await self._collect_relevant_data( user=user, since=yesterday, tenant_id=tenant_id ) # AI 生成个性化摘要 summary = await self._generate_ai_summary( user=user, data=relevant_data, tenant_id=tenant_id, ) return { 'user_id': user_id, 'user_role': self._role_code(user.role), 'date': now.strftime('%Y-%m-%d'), 'summary': summary, 'highlights': relevant_data.get('highlights', []), 'your_tasks': relevant_data.get('your_tasks', []), 'risks': relevant_data.get('risks', []), 'signals': relevant_data.get('signals', []), 'conflicts': relevant_data.get('conflicts', []), 'action_items': relevant_data.get('action_items', []), 'team_updates': relevant_data.get('team_updates', []) } async def _collect_relevant_data( self, user: User, since: datetime, tenant_id: int ) -> Dict[str, Any]: """收集与用户相关的数据""" data = { 'highlights': [], 'your_tasks': [], 'risks': [], 'signals': [], 'conflicts': [], 'action_items': [], 'team_updates': [] } # 获取用户相关的项目 user_projects = await self._get_user_projects(user.id, tenant_id) if not user_projects: return data project_ids = [p.id for p in user_projects] role_code = self._role_code(user.role) # 1. 获取用户的任务更新 task_result = await self.db.execute( select(Task, Project).join( Project, and_(Project.id == Task.project_id, Project.tenant_id == tenant_id), ).where( Task.tenant_id == tenant_id, Task.project_id.in_(project_ids), Task.updated_at >= since ) ) task_rows = task_result.all() for task, project in task_rows: # 我的任务 if task.assignee_id == user.id: data['your_tasks'].append({ 'id': task.id, 'title': task.title, 'status': task.status, 'priority': task.priority, 'project': project.title, 'updated_at': task.updated_at.isoformat() if hasattr(task, 'updated_at') else None }) # 高优先级或阻塞的任务加入 highlights if task.priority == 'HIGH' or task.status == 'blocked': data['highlights'].append({ 'type': 'task', 'priority': 'high', 'content': f'任务"{task.title}"状态: {task.status}', 'project': project.title }) # 2. 获取风险(阻塞的任务) for task, project in task_rows: if task.status == 'blocked': data['risks'].append({ 'type': 'task_blocked', 'task': task.title, 'project': project.title, 'severity': 'high' }) # 3. 获取新信号(根据角色过滤) signal_result = await self.db.execute( select(Signal).join( ProjectSignalLink, ProjectSignalLink.signal_id == Signal.id ).where( Signal.tenant_id == tenant_id, ProjectSignalLink.project_id.in_(project_ids), Signal.created_at >= since, Signal.impact_score >= 0.7 # 只显示高影响信号 ).order_by( Signal.impact_score.desc() ).limit(10) ) signals = signal_result.scalars().all() for signal in signals: # 根据角色过滤信号内容 if self._is_signal_relevant_to_role(signal, role_code): data['signals'].append({ 'id': signal.id, 'source': signal.source, 'content': signal.content[:200], 'impact_score': signal.impact_score, 'priority': signal.priority }) # 高影响信号加入 highlights if signal.impact_score >= 0.9: data['highlights'].append({ 'type': 'signal', 'priority': 'high', 'content': f'高影响信号: {signal.content[:100]}', 'source': signal.source }) # 4. 获取认知冲突 conflict_result = await self.db.execute( select(CognitiveConflict).join( TruthNode, TruthNode.id == CognitiveConflict.truth_node_id ).where( CognitiveConflict.tenant_id == tenant_id, TruthNode.project_id.in_(project_ids), CognitiveConflict.status == 'active', CognitiveConflict.created_at >= since ) ) conflicts = conflict_result.scalars().all() for conflict in conflicts: involved_users = _parse_involved_user_ids(conflict.involved_user_ids) # 如果用户参与了冲突,或是管理员 if user.id in involved_users or role_code in [UserRole.ADMIN.value, UserRole.BOSS.value]: data['conflicts'].append({ 'id': conflict.id, 'type': conflict.conflict_type, 'description': conflict.description[:200], 'severity': 'high' if 'critical' in conflict.description.lower() else 'medium' }) # 冲突加入 highlights data['highlights'].append({ 'type': 'conflict', 'priority': 'high', 'content': f'认知冲突: {conflict.conflict_type}', 'project_id': conflict.truth_node_id }) # 5. 生成行动项 if data['your_tasks']: for task in data['your_tasks'][:3]: if task['status'] in ['pending', 'in_progress']: data['action_items'].append({ 'type': 'task', 'content': f'继续推进任务: {task["title"]}', 'priority': task['priority'] }) if data['conflicts']: data['action_items'].append({ 'type': 'conflict', 'content': f'解决 {len(data["conflicts"])} 个认知冲突', 'priority': 'high' }) # 6. 团队更新(其他用户的相关任务) team_task_result = await self.db.execute( select(Task, Project).join( Project, and_(Project.id == Task.project_id, Project.tenant_id == tenant_id), ).where( Task.tenant_id == tenant_id, Task.project_id.in_(project_ids), Task.assignee_id != user.id, Task.updated_at >= since ) ) team_tasks = team_task_result.all() for task, project in team_tasks[:5]: data['team_updates'].append({ 'assignee_id': task.assignee_id, 'task': task.title, 'status': task.status, 'project': project.title }) return data async def _get_user_projects( self, user_id: int, tenant_id: int ) -> List[Project]: """获取用户相关的项目""" # 获取用户参与的所有项目 result = await self.db.execute( select(Project).where( Project.tenant_id == tenant_id, Project.status == 'active' ) ) return result.scalars().all() def _is_signal_relevant_to_role( self, signal: Signal, user_role: Any ) -> bool: """判断信号是否与用户角色相关""" content = signal.content.lower() role_code = self._role_code(user_role) _ = signal.source.lower() # AI工程师关注的内容 if role_code == UserRole.AI.value: ai_keywords = ['model', 'llm', 'gpt', 'embedding', 'vector', '训练', '推理', 'ollama', 'langchain'] if any(kw in content for kw in ai_keywords): return True # 后端开发关注的内容 elif role_code in {UserRole.BE.value, "BACKEND"}: backend_keywords = ['api', 'backend', 'server', 'database', 'sql', '后端', '接口', '服务'] if any(kw in content for kw in backend_keywords): return True # 前端开发关注的内容 elif role_code in {UserRole.FE.value, "FRONTEND"}: frontend_keywords = ['ui', 'frontend', 'react', 'vue', 'component', '前端', '界面', '交互'] if any(kw in content for kw in frontend_keywords): return True # 管理员看到所有高影响信号 elif role_code in {UserRole.ADMIN.value, UserRole.BOSS.value}: return signal.impact_score >= 0.8 # 默认返回中等影响以上的信号 return signal.impact_score >= 0.7 async def _generate_ai_summary( self, user: User, data: Dict[str, Any], tenant_id: int, ) -> str: """AI 生成个性化摘要""" role_code = self._role_code(user.role) prompt = f"""为以下用户生成每日工作摘要: 用户角色:{role_code} 用户名:{user.username} 今日概况: - 你的任务数:{len(data.get('your_tasks', []))} - 新信号数:{len(data.get('signals', []))} - 活跃冲突数:{len(data.get('conflicts', []))} - 风险数:{len(data.get('risks', []))} 重点事项: {json.dumps(data.get('highlights', []), ensure_ascii=False, indent=2)[:500]} 请生成一段简洁的每日摘要(100字以内),突出最重要的信息和行动项。""" try: summary = await self.llm.generate( prompt, db=self.db, tenant_id=tenant_id, ) return summary.strip() except: # 降级:生成简单摘要 parts = [] if data.get('your_tasks'): parts.append(f"你有 {len(data['your_tasks'])} 个任务需要关注") if data.get('risks'): parts.append(f"注意 {len(data['risks'])} 个风险点") if data.get('conflicts'): parts.append(f"需要解决 {len(data['conflicts'])} 个认知冲突") return ";".join(parts) if parts else "今日无重要更新" @staticmethod def _role_code(raw_role: Any) -> str: if raw_role is None: return "" value = getattr(raw_role, "value", raw_role) return str(value or "").strip().upper() async def generate_team_briefing( self, project_id: int, tenant_id: int = 1 ) -> Dict[str, Any]: """ 生成团队简报(给管理者) 包含: - 整体进度 - 每个成员的工作量 - 风险和阻塞 - 需要决策的事项 """ # 获取项目信息 project_result = await self.db.execute( select(Project).where( Project.id == project_id, Project.tenant_id == tenant_id ) ) project = project_result.scalar_one_or_none() if not project: raise ValueError(f"Project {project_id} not found") # 获取项目所有任务 task_result = await self.db.execute( select(Task).where( Task.project_id == project_id, Task.tenant_id == tenant_id ) ) tasks = task_result.scalars().all() # 统计任务状态 status_counts = {} for task in tasks: status_counts[task.status] = status_counts.get(task.status, 0) + 1 # 获取阻塞的任务 blocked_tasks = [t for t in tasks if t.status == 'blocked'] # 获取项目相关信号 signal_result = await self.db.execute( select(Signal).join( ProjectSignalLink, ProjectSignalLink.signal_id == Signal.id ).where( Signal.tenant_id == tenant_id, ProjectSignalLink.project_id == project_id ).order_by( Signal.created_at.desc() ).limit(10) ) signals = signal_result.scalars().all() return { 'project_id': project_id, 'project_name': project.title, 'generated_at': datetime.now(timezone.utc).isoformat(), 'task_summary': { 'total': len(tasks), 'by_status': status_counts, 'completion_rate': status_counts.get('done', 0) / len(tasks) if tasks else 0 }, 'blocked_tasks': [ {'id': t.id, 'title': t.title, 'assignee_id': t.assignee_id} for t in blocked_tasks ], 'recent_signals': [ {'id': s.id, 'content': s.content[:100], 'impact_score': s.impact_score} for s in signals[:5] ], 'needs_attention': len(blocked_tasks) > 0 } # 全局服务实例 def get_daily_briefing_service(db: AsyncSession) -> DailyBriefingService: """获取每日简报服务实例""" return DailyBriefingService(db)