Files
GX-gp-notify/app/services/pipeline.py
T
v6ole a51161b5f3 feat: 企微菜单新增 AI 分析开关 + 白名单控制
- 新增 ai_whitelist 配置项,仅白名单用户可操作 AI 开关
- 新增 app/services/ai_state.py 运行时开关模块
- 系统管理菜单新增「AI 分析」按钮 (toggle_ai)
- 监控配置、系统状态显示 AI 状态和 AI 标记统计
- pipeline 改用运行时开关 is_ai_enabled()
2026-05-26 16:02:26 +08:00

190 lines
6.9 KiB
Python

import logging
from typing import Any
from app.config import settings
from app.crawler.base import PipelineConfig, PipelineResult
from app.services.ai_state import is_ai_enabled
from app.services.filter_service import dedup_by_hash
logger = logging.getLogger(__name__)
class PostCrawlPipeline:
def __init__(self, db_session, notification_service):
self.db = db_session
self.notify = notification_service
async def process(self, announcements: list[dict[str, Any]],
config: PipelineConfig) -> PipelineResult:
result = PipelineResult()
if not announcements:
return result
# 1. Dedup
if config.dedup_enabled:
announcements = dedup_by_hash(announcements)
# 2. 先标记 keyword_matched,再存库
if config.filter_enabled and config.keywords:
for a in announcements:
a["keyword_matched"] = self._match_keywords(a, config.keywords)
# 3. Store to database
stored = await self._save_to_db(announcements)
result.stored = stored
to_notify = announcements
# 4. Filter
if config.filter_enabled and config.keywords:
before = len(to_notify)
to_notify = [a for a in to_notify if a.get("keyword_matched")]
result.filtered = before - len(to_notify)
# 5. Skip already-notified
if to_notify:
to_notify = await self._exclude_sent(to_notify)
# 5.5 AI 分析(可选,支持运行时开关)
if is_ai_enabled() and to_notify:
await self._ai_analyze(to_notify)
# 6. Notify
if config.notify_mode == "all":
result.notified = await self._send_notifications(to_notify)
elif config.notify_mode == "filtered":
if config.filter_enabled and config.keywords:
result.notified = await self._send_notifications(to_notify)
elif not config.filter_enabled:
result.notified = await self._send_notifications(to_notify)
# 7. Mark sent
if config.mark_sent and result.notified > 0:
await self._mark_sent(to_notify)
return result
async def _save_to_db(self, announcements: list[dict[str, Any]]) -> int:
from sqlalchemy.dialects.postgresql import insert
from app.models.announcement import Announcement
if not announcements:
return 0
values = [{
"title": a["title"],
"publish_date": a["publish_date"],
"purchase_name": a.get("purchase_name", ""),
"content_url": a.get("content_url", ""),
"source_code": a["source_code"],
"source_name": a["source_name"],
"announcement_type": a.get("announcement_type", "purchase"),
"content_hash": a["content_hash"],
"crawl_mode": a.get("crawl_mode", "auto"),
"is_new": a.get("is_new", True),
"is_sent": False,
"keyword_matched": a.get("keyword_matched", False),
} for a in announcements]
stmt = insert(Announcement).values(values)
stmt = stmt.on_conflict_do_nothing(index_elements=["content_hash"])
result_proxy = await self.db.execute(stmt)
await self.db.commit()
return result_proxy.rowcount if result_proxy.rowcount >= 0 else len(values)
async def _send_notifications(self, announcements: list[dict[str, Any]]) -> int:
return await self.notify.send(announcements)
async def _mark_sent(self, announcements: list[dict[str, Any]]) -> int:
from sqlalchemy import update
from app.models.announcement import Announcement
hashes = [a["content_hash"] for a in announcements if a.get("content_hash")]
if not hashes:
return 0
stmt = (
update(Announcement)
.where(Announcement.content_hash.in_(hashes))
.values(is_sent=True)
)
result = await self.db.execute(stmt)
await self.db.commit()
return result.rowcount
async def _exclude_sent(self, announcements: list[dict[str, Any]]) -> list[dict[str, Any]]:
from sqlalchemy import select
from app.models.announcement import Announcement
hashes = [a["content_hash"] for a in announcements if a.get("content_hash")]
if not hashes:
return announcements
stmt = select(Announcement.content_hash).where(
Announcement.content_hash.in_(hashes),
Announcement.is_sent == True, # noqa: E712
)
result = await self.db.execute(stmt)
sent_hashes = {row[0] for row in result.fetchall()}
return [a for a in announcements if a.get("content_hash") not in sent_hashes]
@staticmethod
def _match_keywords(announcement: dict[str, Any], keywords: list[str]) -> bool:
text = f"{announcement.get('title', '')} {announcement.get('purchase_name', '')}"
return any(kw in text for kw in keywords)
async def _ai_analyze(self, announcements: list[dict[str, Any]]) -> None:
"""对公告列表执行 AI 分析,将结果附加到每条公告的 ai_result 字段,并更新数据库"""
from app.services.ai_analyzer import AiAnalyzer
analyzer = AiAnalyzer()
logger.info("AI 分析开始:共 %d 条公告", len(announcements))
results = await analyzer.analyze_batch(announcements)
ai_updates = []
for ann, ai_result in zip(announcements, results):
ann["ai_result"] = {
"is_relevant": ai_result.is_relevant,
"reason": ai_result.reason,
"business_type": ai_result.business_type,
}
if ai_result.error:
logger.warning("AI 分析失败 [%s]: %s", ann.get("title", "")[:30], ai_result.error)
else:
ai_updates.append({
"content_hash": ann["content_hash"],
"ai_relevant": ai_result.is_relevant,
"ai_analysis": ai_result.reason,
})
if ai_result.is_relevant:
logger.info("AI 标记可承接项目: %s (%s)", ann.get("title", "")[:40], ai_result.business_type)
# 批量更新数据库中的 AI 分析结果
if ai_updates:
await self._update_ai_results(ai_updates)
async def _update_ai_results(self, updates: list[dict[str, Any]]) -> None:
"""批量更新公告的 AI 分析结果到数据库"""
from sqlalchemy import update
from app.models.announcement import Announcement
for u in updates:
stmt = (
update(Announcement)
.where(Announcement.content_hash == u["content_hash"])
.values(
ai_relevant=u["ai_relevant"],
ai_analysis=u["ai_analysis"],
)
)
await self.db.execute(stmt)
await self.db.commit()