from __future__ import annotations from .shared import * class SignalCaptureExternalSourcesMixin: 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']} # 创建信号 await ensure_signal_id_sequence(self.db) signal = Signal( 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': now_cn_aware().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']} # 创建信号 await ensure_signal_id_sequence(self.db) signal = Signal( 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': now_cn_aware().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 # 兼容三种实现: # 1) async def -> 直接 await # 2) sync def -> to_thread 防阻塞 # 3) sync def 返回 coroutine -> 对返回值再 await if inspect.iscoroutinefunction(fetch_hn_stories): stories = await fetch_hn_stories(limit=30) else: result = await asyncio.to_thread(fetch_hn_stories, 30) stories = await result if inspect.isawaitable(result) else result for story in stories[:10]: # 只分析前10条 # AI 分析每条故事 analysis = await self._analyze_hackernews_story( story, tenant_id=tenant_id, ) if analysis.get('is_valuable'): # 创建信号 await ensure_signal_id_sequence(self.db) signal = Signal( 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': now_cn_aware().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() self._emit_ai_think( "hn_story_captured", title=str(story.get("title") or "")[:50], ) # 等待下次抓取 await asyncio.sleep(interval_minutes * 60) except Exception as e: self._emit_ai_think("hn_monitor_error", error=str(e)) await asyncio.sleep(interval_minutes * 60) async def _load_active_projects_context(self, tenant_id: int) -> List[Dict[str, Any]]: result = await self.db.execute( select(Project).where( Project.status == "active", Project.tenant_id == tenant_id, ) ) projects = result.scalars().all() rows: List[Dict[str, Any]] = [] for project in projects: rows.append( { "id": int(project.id), "title": (project.title or project.name or "").strip(), "description": (project.description or "").strip()[:300], "status": project.status, } ) return rows def _emit_ai_think(self, stage: str, **payload: Any) -> None: safe_payload: Dict[str, Any] = {} for key, value in payload.items(): safe_payload[key] = self._safe_log_value(value) logger.info("signal_capture_ai_think", stage=stage, **safe_payload) try: stage_zh = AI_THINK_STAGE_ZH.get(stage, stage) now_text = self._format_cn_time(now_cn_aware()) print( f"[{now_text}][AI思考][SignalCapture] {stage_zh} " + json.dumps({"stage": stage, **safe_payload}, ensure_ascii=False) ) except Exception: pass @staticmethod def _safe_log_value(value: Any, max_len: int = 600) -> Any: if isinstance(value, (str, int, float, bool)) or value is None: text = str(value) if value is not None else None if text is None: return None return text if len(text) <= max_len else f"{text[:max_len]}...(truncated)" try: text = json.dumps(value, ensure_ascii=False, default=str) except Exception: text = str(value) return text if len(text) <= max_len else f"{text[:max_len]}...(truncated)"