feat: 企微菜单新增 AI 分析开关 + 白名单控制

- 新增 ai_whitelist 配置项,仅白名单用户可操作 AI 开关
- 新增 app/services/ai_state.py 运行时开关模块
- 系统管理菜单新增「AI 分析」按钮 (toggle_ai)
- 监控配置、系统状态显示 AI 状态和 AI 标记统计
- pipeline 改用运行时开关 is_ai_enabled()
This commit is contained in:
2026-05-26 16:02:26 +08:00
parent 68215aa804
commit a51161b5f3
6 changed files with 122 additions and 4 deletions
+2
View File
@@ -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
+1
View File
@@ -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"
+51
View File
@@ -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
+3 -2
View File
@@ -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
+60 -2
View File
@@ -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
+5
View File
@@ -46,6 +46,11 @@ MENU = {
"type": "click",
"key": "sync_holidays",
},
{
"name": "AI 分析",
"type": "click",
"key": "toggle_ai",
},
],
},
]