From a51161b5f384edfae80eea317c40b550c81f2cc5 Mon Sep 17 00:00:00 2001 From: v6ole Date: Tue, 26 May 2026 16:02:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=81=E5=BE=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AI=20=E5=88=86=E6=9E=90=E5=BC=80=E5=85=B3?= =?UTF-8?q?=20+=20=E7=99=BD=E5=90=8D=E5=8D=95=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ai_whitelist 配置项,仅白名单用户可操作 AI 开关 - 新增 app/services/ai_state.py 运行时开关模块 - 系统管理菜单新增「AI 分析」按钮 (toggle_ai) - 监控配置、系统状态显示 AI 状态和 AI 标记统计 - pipeline 改用运行时开关 is_ai_enabled() --- .env.example | 2 ++ app/config.py | 1 + app/services/ai_state.py | 51 +++++++++++++++++++++++++++++++++ app/services/pipeline.py | 5 ++-- app/wechat/handler.py | 62 ++++++++++++++++++++++++++++++++++++++-- app/wechat/menu.py | 5 ++++ 6 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 app/services/ai_state.py diff --git a/.env.example b/.env.example index 8950fee..5e096bf 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,8 @@ LOGHIVE_API_KEY= # AI 分析 (DeepSeek) AI_ENABLED=false +# AI 管理白名单(企微用户ID,逗号分隔),留空表示所有人可操作 +AI_WHITELIST= AI_API_KEY=sk-your-deepseek-api-key AI_BASE_URL=https://api.deepseek.com/v1 AI_MODEL=deepseek-chat diff --git a/app/config.py b/app/config.py index 65c86f1..48ac65a 100644 --- a/app/config.py +++ b/app/config.py @@ -41,6 +41,7 @@ class Settings(BaseSettings): # AI 分析 (DeepSeek) ai_enabled: bool = False + ai_whitelist: str = "" ai_api_key: str = "" ai_base_url: str = "https://api.deepseek.com/v1" ai_model: str = "deepseek-chat" diff --git a/app/services/ai_state.py b/app/services/ai_state.py new file mode 100644 index 0000000..94ce460 --- /dev/null +++ b/app/services/ai_state.py @@ -0,0 +1,51 @@ +"""AI 运行时状态 — 支持企微菜单动态开关""" + +import logging + +from app.config import settings + +logger = logging.getLogger(__name__) + +# 运行时覆盖值,None 表示使用 settings.ai_enabled +_runtime_override: bool | None = None + + +def is_ai_enabled() -> bool: + """获取 AI 分析当前是否启用(考虑运行时覆盖)""" + if _runtime_override is not None: + return _runtime_override + return settings.ai_enabled + + +def set_ai_enabled(enabled: bool) -> bool: + """设置 AI 分析运行时开关,返回是否真的发生了变化""" + global _runtime_override + current = is_ai_enabled() + if enabled == current: + return False + _runtime_override = enabled + status = "启用" if enabled else "禁用" + logger.info("AI 分析已通过企微菜单%s", status) + return True + + +def get_ai_status_text() -> str: + """获取 AI 状态文本""" + return "已启用" if is_ai_enabled() else "已禁用" + + +def get_whitelist() -> list[str]: + """获取 AI 白名单用户列表""" + raw = settings.ai_whitelist + if not raw: + return [] + return [u.strip() for u in raw.split(",") if u.strip()] + + +def is_whitelisted(user_id: str) -> bool: + """检查用户是否在 AI 白名单中""" + whitelist = get_whitelist() + if not whitelist: + # 白名单为空则所有人都可以操作 + return True + return user_id in whitelist diff --git a/app/services/pipeline.py b/app/services/pipeline.py index 0168a5c..7fa8a42 100644 --- a/app/services/pipeline.py +++ b/app/services/pipeline.py @@ -3,6 +3,7 @@ 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__) @@ -45,8 +46,8 @@ class PostCrawlPipeline: if to_notify: to_notify = await self._exclude_sent(to_notify) - # 5.5 AI 分析(可选) - if settings.ai_enabled and to_notify: + # 5.5 AI 分析(可选,支持运行时开关) + if is_ai_enabled() and to_notify: await self._ai_analyze(to_notify) # 6. Notify diff --git a/app/wechat/handler.py b/app/wechat/handler.py index 31f201c..efb4257 100644 --- a/app/wechat/handler.py +++ b/app/wechat/handler.py @@ -87,6 +87,8 @@ class WeChatMessageHandler: return await self._handle_pause_scheduler(from_user) elif event_key == "resume_scheduler": return await self._handle_resume_scheduler(from_user) + elif event_key == "toggle_ai": + return await self._handle_toggle_ai(from_user) return None async def handle_text(self, content: str, from_user: str) -> str | None: @@ -170,15 +172,22 @@ class WeChatMessageHandler: async def _handle_monitor_config(self, from_user: str) -> str | None: import json + from app.services.ai_state import get_ai_status_text try: keywords = settings.crawler_keywords sources = json.loads(settings.announcement_sources) source_names = "、".join(v["name"] for v in sources.values()) text = ( + f"📋 监控配置\n" + f"---\n" f"监控关键词: {', '.join(keywords)}\n" f"爬取页数: {settings.crawler_max_pages} 页\n" f"定时规则: {settings.scheduler_cron}\n" - f"公告来源: {source_names}" + f"公告来源: {source_names}\n" + f"---\n" + f"🤖 AI 分析: {get_ai_status_text()}\n" + f"AI 模型: {settings.ai_model}\n" + f"重点标记: {settings.ai_analysis_title}" ) await self.client.send_text(text, from_user) except Exception as e: @@ -189,6 +198,7 @@ class WeChatMessageHandler: from app.api.deps import get_db from sqlalchemy import func, select from app.models.announcement import Announcement + from app.services.ai_state import get_ai_status_text try: async for db in get_db(): @@ -212,13 +222,27 @@ class WeChatMessageHandler: ) unsent = unsent_result.scalar() or 0 + # AI 标记统计 + ai_relevant_result = await db.execute( + select(func.count()).where( + Announcement.ai_relevant == True, # noqa: E712 + ).select_from(Announcement) + ) + ai_relevant = ai_relevant_result.scalar() or 0 + scheduler_status = "已启用" if settings.scheduler_enabled else "已禁用" text = ( + f"📊 系统状态\n" + f"---\n" f"累计公告: {total} 条\n" f"今日新增: {today} 条\n" f"待推送: {unsent} 条\n" + f"---\n" f"定时任务: {scheduler_status}\n" - f"定时规则: {settings.scheduler_cron}" + f"定时规则: {settings.scheduler_cron}\n" + f"---\n" + f"🤖 AI 分析: {get_ai_status_text()}\n" + f"AI 标记项目: {ai_relevant} 条" ) await self.client.send_text(text, from_user) except Exception as e: @@ -304,6 +328,40 @@ class WeChatMessageHandler: logger.error(f"暂停定时任务失败: {e}") await self.client.send_text(f"操作失败: {e}", from_user) + async def _handle_toggle_ai(self, from_user: str) -> str | None: + from app.services.ai_state import ( + get_ai_status_text, + get_whitelist, + is_ai_enabled, + is_whitelisted, + set_ai_enabled, + ) + + # 白名单校验 + whitelist = get_whitelist() + if whitelist and not is_whitelisted(from_user): + await self.client.send_text( + f"⚠️ 你没有权限操作 AI 分析开关\n" + f"当前仅以下用户可操作:\n{', '.join(whitelist)}", + from_user, + ) + return None + + current = is_ai_enabled() + changed = set_ai_enabled(not current) + if not changed: + await self.client.send_text( + f"AI 分析当前已是「{get_ai_status_text()}」状态,无需切换", + from_user, + ) + return None + + await self.client.send_text( + f"✅ AI 分析已切换为「{get_ai_status_text()}」\n" + f"下次爬取触发时生效", + from_user, + ) + async def _handle_resume_scheduler(self, from_user: str) -> str | None: try: from app.scheduler.jobs import scheduler diff --git a/app/wechat/menu.py b/app/wechat/menu.py index e2cb9d6..9280637 100644 --- a/app/wechat/menu.py +++ b/app/wechat/menu.py @@ -46,6 +46,11 @@ MENU = { "type": "click", "key": "sync_holidays", }, + { + "name": "AI 分析", + "type": "click", + "key": "toggle_ai", + }, ], }, ]