PMFlow/backend/app/services/signal_capture_service/mixin_external_sources.py

233 lines
8.0 KiB
Python

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