""" Proactive Signal Capture Service - 主动式信号捕获服务 核心功能: 1. 飞书群聊监控 - 自动捕获群内讨论的产品/技术相关内容 2. 浏览器标签监控 - 检测用户访问的技术文档、竞品页面 3. GitHub 仓库监控 - 跟踪上游仓库更新、竞品开源项目 4. HackerNews 自动抓取 - 定时抓取技术前沿信息 每个信号都会自动: - 评估战略重要性 - 提取关键技术栈 - 生成任务建议 - 推送给相关角色 """ 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 from ..database import AsyncSessionLocal from ..models import ( Signal, Project, ProjectDomain, User, ProjectSignalLink, Task ) from .llm import LLMService class ProactiveSignalCaptureService: """主动式信号捕获服务""" def __init__(self, db: AsyncSession): self.db = db self.llm = LLMService() async def capture_from_feishu( self, group_chat_id: str, message_content: str, sender: str, tenant_id: int = 1 ) -> Dict[str, Any]: """ 从飞书群聊捕获信号 触发条件: - 提到产品、竞品、技术关键词 - 讨论架构、技术选型 - 分享技术文章 """ # AI 判断是否为有价值信号 is_signal = await self._analyze_chat_message( message_content, tenant_id=tenant_id, ) if not is_signal['is_valuable']: return {'status': 'ignored', 'reason': is_signal['reason']} # 创建信号 from sqlalchemy import text next_signal_id = (await self.db.execute(text("SELECT COALESCE(MAX(id),0)+1 FROM signals"))).scalar() or 1 signal = Signal( id=next_signal_id, source='Feishu', content=f"[{sender}]: {message_content[:200]}", impact_score=is_signal['impact_score'], priority=is_signal['priority'], processed=True, strategic_analysis=json.dumps({ 'captured_at': datetime.now(timezone.utc).isoformat(), 'source_context': { 'group_id': group_chat_id, 'sender': sender }, 'ai_analysis': is_signal.get('analysis', {}) }), tasks_generated=json.dumps([]), tenant_id=tenant_id ) self.db.add(signal) await self.db.commit() await self.db.refresh(signal) # 自动关联到相关项目 await self._auto_link_to_projects(signal.id, is_signal, tenant_id) return { 'signal_id': str(signal.id), 'status': 'captured', 'impact_score': signal.impact_score, 'auto_tasks': is_signal.get('suggested_tasks', []) } async def capture_from_browser( self, user_id: int, url: str, title: str, content_preview: str, tenant_id: int = 1 ) -> Dict[str, Any]: """ 从浏览器标签页捕获信号 触发条件: - 访问技术文档、API 文档 - 浏览竞品网站、产品页面 - 查看 GitHub issues/PRs - 阅读技术博客 """ # AI 分析页面价值 analysis = await self._analyze_webpage( url, title, content_preview, tenant_id=tenant_id, ) if not analysis['is_valuable']: return {'status': 'ignored', 'reason': analysis['reason']} # 创建信号 from sqlalchemy import text next_signal_id = (await self.db.execute(text("SELECT COALESCE(MAX(id),0)+1 FROM signals"))).scalar() or 1 signal = Signal( id=next_signal_id, source='Browser', content=f"[浏览] {title}: {content_preview[:200]}", impact_score=analysis['impact_score'], priority=analysis['priority'], processed=True, strategic_analysis=json.dumps({ 'url': url, 'captured_at': datetime.now(timezone.utc).isoformat(), 'user_id': user_id, 'ai_analysis': analysis.get('analysis', {}) }), tasks_generated=json.dumps([]), tenant_id=tenant_id ) self.db.add(signal) await self.db.commit() await self.db.refresh(signal) # 自动关联到相关项目 await self._auto_link_to_projects(signal.id, analysis, tenant_id) return { 'signal_id': str(signal.id), 'status': 'captured', 'impact_score': signal.impact_score } async def capture_from_github( self, repo_url: str, event_type: str, event_data: Dict[str, Any], tenant_id: int = 1 ) -> Dict[str, Any]: """ 从 GitHub 事件捕获信号 监控事件: - 竞品仓库更新 - 上游依赖更新 - 技术趋势项目发布 """ # AI 分析 GitHub 事件 analysis = await self._analyze_github_event( repo_url, event_type, event_data, tenant_id=tenant_id, ) if not analysis['is_valuable']: return {'status': 'ignored', 'reason': analysis['reason']} # 创建信号 from sqlalchemy import text next_signal_id = (await self.db.execute(text("SELECT COALESCE(MAX(id),0)+1 FROM signals"))).scalar() or 1 signal = Signal( id=next_signal_id, source='GitHub', content=f"[{event_type}] {repo_url}: {event_data.get('title', event_data.get('message', ''))[:200]}", impact_score=analysis['impact_score'], priority=analysis['priority'], processed=True, strategic_analysis=json.dumps({ 'repo_url': repo_url, 'event_type': event_type, 'event_data': event_data, 'captured_at': datetime.now(timezone.utc).isoformat(), 'ai_analysis': analysis.get('analysis', {}) }), tasks_generated=json.dumps([]), tenant_id=tenant_id ) self.db.add(signal) await self.db.commit() await self.db.refresh(signal) return { 'signal_id': str(signal.id), 'status': 'captured', 'impact_score': signal.impact_score } async def start_hackernews_monitor( self, interval_minutes: int = 30, tenant_id: int = 1 ): """ 启动 HackerNews 定时监控 定期抓取 HackerNews 首页,分析技术趋势 """ while True: try: # 抓取 HackerNews 首页 from .fetcher import fetch_hn_stories stories = await asyncio.to_thread(fetch_hn_stories) for story in stories[:10]: # 只分析前10条 # AI 分析每条故事 analysis = await self._analyze_hackernews_story( story, tenant_id=tenant_id, ) if analysis['is_strategic']: # 创建信号 from sqlalchemy import text next_signal_id = (await self.db.execute(text("SELECT COALESCE(MAX(id),0)+1 FROM signals"))).scalar() or 1 signal = Signal( id=next_signal_id, source='HackerNews', content=f"HN: {story.get('title', '')[:200]}", impact_score=analysis['impact_score'], priority=analysis['priority'], processed=True, strategic_analysis=json.dumps({ 'hn_url': story.get('url'), 'hn_points': story.get('points'), 'captured_at': datetime.now(timezone.utc).isoformat(), 'ai_analysis': analysis.get('analysis', {}) }), tasks_generated=json.dumps(analysis.get('suggested_tasks', [])), tenant_id=tenant_id ) self.db.add(signal) await self.db.commit() print(f"[SignalCapture] Captured HN story: {story.get('title')[:50]}") # 等待下次抓取 await asyncio.sleep(interval_minutes * 60) except Exception as e: print(f"[SignalCapture] HackerNews monitor error: {e}") await asyncio.sleep(interval_minutes * 60) async def _analyze_chat_message(self, message: str, tenant_id: int) -> Dict[str, Any]: """AI 分析聊天消息是否有价值""" prompt = f"""分析以下群聊消息是否包含有价值的项目或技术信号: 消息:{message} 请判断: 1. 是否与项目、产品、技术相关? 2. 是否包含重要信息或决策? 3. 影响程度(0-1)? 以 JSON 格式返回: {{ "is_valuable": boolean, "reason": "原因说明", "impact_score": 0.8, "priority": "high/medium/low", "analysis": {{}}, "suggested_tasks": [{{"task_name": "任务", "type": "backend", "estimated_hours": 8}}] }} 如果不是有价值信号,is_valuable 设为 false。""" try: response = await self.llm.generate( prompt, db=self.db, tenant_id=tenant_id, ) # 清理可能的 markdown response = response.strip() if response.startswith('```'): response = response.split('```')[1] if response.startswith('json'): response = response[4:] return json.loads(response.strip()) except: return {'is_valuable': False, 'reason': 'AI 分析失败'} async def _analyze_webpage(self, url: str, title: str, content: str, tenant_id: int) -> Dict[str, Any]: """AI 分析网页价值""" prompt = f"""分析以下网页内容是否有价值: 标题:{title} URL: {url} 内容摘要:{content[:500]} 请判断: 1. 是否是技术文档、竞品信息、行业趋势? 2. 是否值得团队关注? 3. 影响程度(0-1)? 以 JSON 格式返回: {{ "is_valuable": boolean, "reason": "原因", "impact_score": 0.8, "priority": "high/medium/low", "analysis": {{}}, "suggested_tasks": [{{"task_name": "任务", "type": "backend", "estimated_hours": 8}}] }} 如果不是有价值内容,is_valuable 设为 false。""" try: response = await self.llm.generate( prompt, db=self.db, tenant_id=tenant_id, ) response = response.strip() if response.startswith('```'): response = response.split('```')[1] if response.startswith('json'): response = response[4:] return json.loads(response.strip()) except: return {'is_valuable': False, 'reason': 'AI 分析失败'} async def _analyze_github_event(self, repo_url: str, event_type: str, event_data: Dict, tenant_id: int) -> Dict[str, Any]: """AI 分析 GitHub 事件""" description = f"{event_type}: {event_data.get('title', event_data.get('message', ''))}" prompt = f"""分析以下 GitHub 事件是否有价值: 仓库:{repo_url} 事件:{description} 请判断: 1. 是否是技术栈更新、安全修复、重要功能? 2. 是否影响我们的项目? 3. 影响程度(0-1)? 以 JSON 格式返回: {{ "is_valuable": boolean, "reason": "原因", "impact_score": 0.8, "priority": "high/medium/low", "analysis": {{}}, "suggested_tasks": [{{"task_name": "任务", "type": "backend", "estimated_hours": 8}}] }} 如果不是有价值内容,is_valuable 设为 false。""" try: response = await self.llm.generate( prompt, db=self.db, tenant_id=tenant_id, ) response = response.strip() if response.startswith('```'): response = response.split('```')[1] if response.startswith('json'): response = response[4:] return json.loads(response.strip()) except: return {'is_valuable': False, 'reason': 'AI 分析失败'} async def _analyze_hackernews_story(self, story: Dict, tenant_id: int) -> Dict[str, Any]: """AI 分析 HackerNews 故事""" prompt = f"""分析以下 HackerNews 故事是否是重要技术趋势: 标题:{story.get('title', '')} 链接:{story.get('url', '')} 得分:{story.get('points', 0)} 请判断: 1. 是否是行业趋势、技术突破? 2. 是否值得我们关注? 3. 影响程度(0-1)? 以 JSON 格式返回: {{ "is_strategic": boolean, "impact_score": 0.8, "priority": "high/medium/low", "analysis": {{}}, "suggested_tasks": [{{"task_name": "任务", "type": "backend", "estimated_hours": 8}}] }} 如果不是战略性内容,is_strategic 设为 false。""" try: response = await self.llm.generate( prompt, db=self.db, tenant_id=tenant_id, ) response = response.strip() if response.startswith('```'): response = response.split('```')[1] if response.startswith('json'): response = response[4:] result = json.loads(response.strip()) # 重命名字段以匹配其他方法 return { 'is_valuable': result.get('is_strategic', False), 'reason': result.get('reason', ''), 'impact_score': result.get('impact_score', 0.5), 'priority': result.get('priority', 'low'), 'analysis': result.get('analysis', {}), 'suggested_tasks': result.get('suggested_tasks', []) } except: return {'is_valuable': False, 'reason': 'AI 分析失败'} async def _auto_link_to_projects( self, signal_id: int, analysis: Dict[str, Any], tenant_id: int ): """自动关联信号到相关项目""" # 根据信号内容中的关键词查找相关项目 # 这里简化处理:将信号关联到所有活跃项目 result = await self.db.execute( select(Project).where( Project.status == 'active', Project.tenant_id == tenant_id ) ) projects = result.scalars().all() from sqlalchemy import text for project in projects: # 检查是否已存在关联 existing = await self.db.execute( text("SELECT id FROM project_signal_links WHERE project_id = :project_id AND signal_id = :signal_id"), {"project_id": project.id, "signal_id": signal_id} ) if existing.first() is None: next_link_id = (await self.db.execute(text("SELECT COALESCE(MAX(id),0)+1 FROM project_signal_links"))).scalar() or 1 from ..models import ProjectSignalLink link = ProjectSignalLink( id=next_link_id, project_id=project.id, signal_id=signal_id, link_type='auto_captured', relevance_score=0.7, tenant_id=tenant_id ) self.db.add(link) await self.db.commit() # 全局服务实例 _signal_capture_service: Optional[ProactiveSignalCaptureService] = None def get_signal_capture_service() -> ProactiveSignalCaptureService: """获取信号捕获服务实例(需要在 async 上下文中使用)""" return ProactiveSignalCaptureService(None) # Placeholder