from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List, Optional from pydantic import BaseModel, ConfigDict from datetime import datetime import json from ..database import get_db from ..models import KeywordDomainMap, SensitiveWord, ContentCleanRule, TrendFieldMapping, CollectorCategory from .auth import get_current_admin router = APIRouter(prefix="/api/admin/config", tags=["admin"]) DEFAULT_KEYWORD_DOMAIN_MAP = [ {"pattern": r"AI|人工智能|大模型|GPT|机器学习|深度学习|聊天机器人|LLM", "domain": "AI工具", "sort_order": 1}, {"pattern": r"远程|居家办公|自由职业|数字游民|远程协作", "domain": "远程工作", "sort_order": 2}, {"pattern": r"可持续|环保|低碳|绿色|碳中和|循环|零浪费|垃圾分类|节能", "domain": "可持续生活", "sort_order": 3}, {"pattern": r"知识管理|笔记|Obsidian|Notion|第二大脑|读书|阅读", "domain": "知识管理", "sort_order": 4}, {"pattern": r"数字生活|数码|手机|电脑|智能|APP|应用、软件", "domain": "数字生活", "sort_order": 5}, {"pattern": r"科技|人文|教育|心理|哲学|社会学", "domain": "科技人文", "sort_order": 6}, ] DEFAULT_TREND_FIELD_MAP = [ {"trend_keyword": "远程工作", "field_name": "未来工作方式", "sort_order": 1}, {"trend_keyword": "AI工具", "field_name": "AI与效率", "sort_order": 2}, {"trend_keyword": "可持续生活", "field_name": "可持续生活系统", "sort_order": 3}, {"trend_keyword": "知识管理", "field_name": "个人知识工厂", "sort_order": 4}, {"trend_keyword": "数字生活", "field_name": "科技人文交叉", "sort_order": 5}, {"trend_keyword": "科技人文", "field_name": "科技人文交叉", "sort_order": 6}, {"trend_keyword": "个人成长", "field_name": "个人成长", "sort_order": 7}, {"trend_keyword": "副业", "field_name": "个人成长", "sort_order": 8}, {"trend_keyword": "AI创作", "field_name": "AI与效率", "sort_order": 9}, {"trend_keyword": "未来工作", "field_name": "未来工作方式", "sort_order": 10}, {"trend_keyword": "效率工具", "field_name": "AI与效率", "sort_order": 11}, {"trend_keyword": "家庭教育", "field_name": "科技人文交叉", "sort_order": 12}, ] DEFAULT_CHINA_PAIN_TEMPLATES = { "循环消费": "以旧换新流程繁琐、二手商品信任缺失、租赁市场不规范", "低碳出行": "新能源车充电设施不足、城市规划不支持骑行、通勤距离长", "干净饮食": "有机食品价格高、真伪难辨、外卖为主的生活方式难以改变", "零浪费生活": "环保产品溢价高、可持续选择不便、漂绿营销难以分辨", "绿色家电与节能": "绿色家电初期投入高、节能效果难量化、老旧小区改造难", "碳普惠": "碳账户普及率低、减排量兑换吸引力不足、公众认知有限", "环保科技产品": "绿色产品溢价68%难以承受、缺乏统一认证标准、担心漂绿", "AI与效率": "AI工具选择困难、数据隐私担忧、学习成本高、实际效果难验证", } DEFAULT_GLOBAL_RSS_KEYWORDS = ['sustainable', 'green', 'eco', 'circular', 'climate', 'carbon', 'zero waste', 'renewable', 'recycle', '环保', '可持续', '碳中和', '循环经济', '零浪费', '低碳', '生态'] DEFAULT_PRIORITY_WEIGHTS = {"audience_match": 0.3, "data_availability": 0.25, "uniqueness": 0.2, "executability": 0.15, "brand_fit": 0.1} DEFAULT_DOMAINS = ["远程工作", "AI工具", "可持续生活", "知识管理", "数字生活", "科技人文"] DEFAULT_SENSITIVE_WORDS = [ {"word": "国家主席", "category": "political"}, {"word": "政治局", "category": "political"}, {"word": "常委", "category": "political"}, {"word": "军委", "category": "political"}, {"word": "统战部", "category": "political"}, {"word": "颠覆国家", "category": "political"}, {"word": "分裂主义", "category": "political"}, {"word": "台独", "category": "political"}, {"word": "疆独", "category": "political"}, {"word": "藏独", "category": "political"}, {"word": "赌博", "category": "prohibited"}, {"word": "毒品", "category": "prohibited"}, {"word": "迷药", "category": "prohibited"}, {"word": "枪支", "category": "prohibited"}, {"word": "炸药", "category": "prohibited"}, {"word": "色情", "category": "prohibited"}, {"word": "低俗", "category": "prohibited"}, {"word": "反动", "category": "prohibited"}, {"word": "邪教", "category": "prohibited"}, {"word": "保证赚钱", "category": "misleading"}, {"word": "一夜暴富", "category": "misleading"}, {"word": "100%有效", "category": "misleading"}, {"word": "包治百病", "category": "misleading"}, {"word": "绝对正确", "category": "misleading"}, {"word": "国家机密", "category": "legal"}, {"word": "军事秘密", "category": "legal"}, {"word": "绝密", "category": "legal"}, {"word": "迷信", "category": "legal"}, ] DEFAULT_CONTENT_CLEAN_RULES = [ {"rule_type": "thinking", "pattern": r"^(好的|好的,|好[的,]|我来|让我|我将|我这就).*?(?=\n|$)", "description": "AI思考模式1", "sort_order": 1}, {"rule_type": "thinking", "pattern": r"^(以下|下面是|这是|为您|根据).*?(?=\n|$)", "description": "AI思考模式2", "sort_order": 2}, {"rule_type": "preface", "pattern": r"^(基于|\u3010.*?\u3011|这里.*)", "description": "AI前缀模式", "sort_order": 3}, {"rule_type": "verbosity", "pattern": r"^首先|^其次|^最后", "description": "AI废话-首先其次", "sort_order": 4}, {"rule_type": "verbosity", "pattern": r"^总的来说$", "description": "AI废话-总的来说", "sort_order": 5}, {"rule_type": "verbosity", "pattern": r"^值得注意的是$", "description": "AI废话-值得注意的是", "sort_order": 6}, {"rule_type": "verbosity", "pattern": r"^换句话说$", "description": "AI废话-换句话说", "sort_order": 7}, {"rule_type": "verbosity", "pattern": r"^总而言之$", "description": "AI废话-总而言之", "sort_order": 8}, {"rule_type": "verbosity", "pattern": r"^简而言之$", "description": "AI废话-简而言之", "sort_order": 9}, {"rule_type": "verbosity", "pattern": r"^一言以蔽之$", "description": "AI废话-一言以蔽之", "sort_order": 10}, {"rule_type": "verbosity", "pattern": r"^可以说$", "description": "AI废话-可以说", "sort_order": 11}, {"rule_type": "verbosity", "pattern": r"^不难发现$", "description": "AI废话-不难发现", "sort_order": 12}, {"rule_type": "verbosity", "pattern": r"^由此可见$", "description": "AI废话-由此可见", "sort_order": 13}, {"rule_type": "verbosity", "pattern": r"^综上所述$", "description": "AI废话-综上所述", "sort_order": 14}, {"rule_type": "verbosity", "pattern": r"^通过以上", "description": "AI废话-通过以上", "sort_order": 15}, {"rule_type": "html_thinking", "pattern": r"
]*>(好的|好的,|好[的,]|我来|让我|我将|我这就)", "description": "AI思考-HTML模式", "sort_order": 16}, {"rule_type": "ai_telltale", "pattern": r"值得注意的是", "description": "AI套话—值得注意的是", "sort_order": 20}, {"rule_type": "ai_telltale", "pattern": r"首先.*其次.*最后", "description": "AI套话—首先其次最后", "sort_order": 21}, {"rule_type": "ai_telltale", "pattern": r"综上所述", "description": "AI套话—综上所述", "sort_order": 22}, {"rule_type": "ai_telltale", "pattern": r"总的来说", "description": "AI套话—总的来说", "sort_order": 23}, {"rule_type": "ai_telltale", "pattern": r"总而言之", "description": "AI套话—总而言之", "sort_order": 24}, {"rule_type": "ai_telltale", "pattern": r"无可否认", "description": "AI套话—无可否认", "sort_order": 25}, {"rule_type": "ai_telltale", "pattern": r"毋庸置疑", "description": "AI套话—毋庸置疑", "sort_order": 26}, {"rule_type": "ai_telltale", "pattern": r"众所周知", "description": "AI套话—众所周知", "sort_order": 27}, {"rule_type": "ai_telltale", "pattern": r"不可否认", "description": "AI套话—不可否认", "sort_order": 28}, {"rule_type": "ai_telltale", "pattern": r"从某种意义上", "description": "AI套话—从某种意义上", "sort_order": 29}, {"rule_type": "ai_telltale", "pattern": r"从某种程度上", "description": "AI套话—从某种程度上", "sort_order": 30}, {"rule_type": "ai_telltale", "pattern": r"在一定程度上", "description": "AI套话—在一定程度上", "sort_order": 31}, {"rule_type": "ai_telltale", "pattern": r"换而言之", "description": "AI套话—换而言之", "sort_order": 32}, {"rule_type": "ai_telltale", "pattern": r"换言之", "description": "AI套话—换言之", "sort_order": 33}, {"rule_type": "ai_telltale", "pattern": r"归根结底", "description": "AI套话—归根结底", "sort_order": 34}, {"rule_type": "ai_telltale", "pattern": r"说到底", "description": "AI套话—说到底", "sort_order": 35}, {"rule_type": "ai_telltale", "pattern": r"这为我们提供了", "description": "AI套话—这为我们提供了", "sort_order": 36}, {"rule_type": "ai_telltale", "pattern": r"引人深思", "description": "AI套话—引人深思", "sort_order": 37}, {"rule_type": "ai_telltale", "pattern": r"毫无悬念", "description": "AI套话—毫无悬念", "sort_order": 38}, {"rule_type": "ai_telltale", "pattern": r"不知大家", "description": "AI套话—不知大家", "sort_order": 39}, {"rule_type": "ai_telltale", "pattern": r"说回到", "description": "AI套话—说回到", "sort_order": 40}, {"rule_type": "ai_telltale", "pattern": r"如果你也", "description": "AI套话—如果你也", "sort_order": 41}, {"rule_type": "ai_telltale", "pattern": r"我们不难发现", "description": "AI套话—我们不难发现", "sort_order": 42}, {"rule_type": "ai_telltale", "pattern": r"我们可以看出", "description": "AI套话—我们可以看出", "sort_order": 43}, {"rule_type": "ai_telltale", "pattern": r"从以上分析可以看出", "description": "AI套话—从以上分析可以看出", "sort_order": 44}, {"rule_type": "ai_telltale", "pattern": r"来说说到底", "description": "AI套话—来说说到底", "sort_order": 45}, ] class KeywordDomainMapResponse(BaseModel): id: int pattern: str domain: str sort_order: int is_active: bool created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class KeywordDomainMapCreate(BaseModel): pattern: str domain: str sort_order: int = 0 class KeywordDomainMapUpdate(BaseModel): pattern: Optional[str] = None domain: Optional[str] = None sort_order: Optional[int] = None is_active: Optional[bool] = None class SensitiveWordResponse(BaseModel): id: int word: str category: str is_active: bool added_by: Optional[str] = None created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class SensitiveWordCreate(BaseModel): word: str category: str = "general" class ContentCleanRuleResponse(BaseModel): id: int rule_type: str pattern: str description: Optional[str] = None is_active: bool sort_order: int created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class ContentCleanRuleCreate(BaseModel): rule_type: str pattern: str description: Optional[str] = None sort_order: int = 0 class ContentCleanRuleUpdate(BaseModel): pattern: Optional[str] = None description: Optional[str] = None is_active: Optional[bool] = None sort_order: Optional[int] = None class TrendFieldMappingResponse(BaseModel): id: int trend_keyword: str field_name: str sort_order: int is_active: bool created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class TrendFieldMappingCreate(BaseModel): trend_keyword: str field_name: str sort_order: int = 0 class TrendFieldMappingUpdate(BaseModel): trend_keyword: Optional[str] = None field_name: Optional[str] = None sort_order: Optional[int] = None is_active: Optional[bool] = None class SystemConfigValueResponse(BaseModel): key: str value: Optional[str] = None description: Optional[str] = None def _ensure_defaults(db: Session): if db.query(KeywordDomainMap).count() == 0: for item in DEFAULT_KEYWORD_DOMAIN_MAP: db.add(KeywordDomainMap(**item)) if db.query(SensitiveWord).count() == 0: for item in DEFAULT_SENSITIVE_WORDS: db.add(SensitiveWord(**item)) if db.query(ContentCleanRule).count() == 0: for item in DEFAULT_CONTENT_CLEAN_RULES: db.add(ContentCleanRule(**item)) else: # 补入缺失的 ai_telltale 规则(不覆盖已存在的内容) existing_patterns = {r.pattern for r in db.query(ContentCleanRule).filter(ContentCleanRule.rule_type == 'ai_telltale').all()} for item in DEFAULT_CONTENT_CLEAN_RULES: if item['rule_type'] == 'ai_telltale' and item['pattern'] not in existing_patterns: db.add(ContentCleanRule(**item)) if any(item['rule_type'] == 'ai_telltale' and item['pattern'] not in existing_patterns for item in DEFAULT_CONTENT_CLEAN_RULES): db.flush() if db.query(TrendFieldMapping).count() == 0: for item in DEFAULT_TREND_FIELD_MAP: db.add(TrendFieldMapping(**item)) for name, pain in DEFAULT_CHINA_PAIN_TEMPLATES.items(): cat = db.query(CollectorCategory).filter(CollectorCategory.name == name).first() if cat and not cat.pain_template: cat.pain_template = pain db.commit() @router.get("/keyword-domain-map", response_model=List[KeywordDomainMapResponse]) def list_keyword_domain_map(db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): _ensure_defaults(db) return db.query(KeywordDomainMap).order_by(KeywordDomainMap.sort_order).all() @router.post("/keyword-domain-map", response_model=KeywordDomainMapResponse) def create_keyword_domain_map(data: KeywordDomainMapCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = KeywordDomainMap(**data.model_dump()) db.add(item) db.commit() db.refresh(item) return item @router.put("/keyword-domain-map/{item_id}", response_model=KeywordDomainMapResponse) def update_keyword_domain_map(item_id: int, data: KeywordDomainMapUpdate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(KeywordDomainMap).filter(KeywordDomainMap.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") for k, v in data.model_dump(exclude_unset=True).items(): if v is not None: setattr(item, k, v) db.commit() db.refresh(item) return item @router.delete("/keyword-domain-map/{item_id}") def delete_keyword_domain_map(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(KeywordDomainMap).filter(KeywordDomainMap.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") db.delete(item) db.commit() return {"message": "删除成功"} @router.get("/sensitive-words", response_model=List[SensitiveWordResponse]) def list_sensitive_words(category: Optional[str] = None, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): _ensure_defaults(db) q = db.query(SensitiveWord) if category: q = q.filter(SensitiveWord.category == category) return q.order_by(SensitiveWord.id).all() @router.post("/sensitive-words", response_model=SensitiveWordResponse) def create_sensitive_word(data: SensitiveWordCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = SensitiveWord(**data.model_dump(), added_by=admin_user.username) db.add(item) db.commit() db.refresh(item) return item @router.put("/sensitive-words/{item_id}", response_model=SensitiveWordResponse) def update_sensitive_word(item_id: int, body: SensitiveWordCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(SensitiveWord).filter(SensitiveWord.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") item.word = body.word item.category = body.category db.commit() db.refresh(item) return item @router.delete("/sensitive-words/{item_id}") def delete_sensitive_word(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(SensitiveWord).filter(SensitiveWord.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") db.delete(item) db.commit() return {"message": "删除成功"} @router.get("/content-clean-rules", response_model=List[ContentCleanRuleResponse]) def list_content_clean_rules(rule_type: Optional[str] = None, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): _ensure_defaults(db) q = db.query(ContentCleanRule) if rule_type: q = q.filter(ContentCleanRule.rule_type == rule_type) return q.order_by(ContentCleanRule.sort_order).all() @router.post("/content-clean-rules", response_model=ContentCleanRuleResponse) def create_content_clean_rule(data: ContentCleanRuleCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = ContentCleanRule(**data.model_dump()) db.add(item) db.commit() db.refresh(item) return item @router.put("/content-clean-rules/{item_id}", response_model=ContentCleanRuleResponse) def update_content_clean_rule(item_id: int, data: ContentCleanRuleUpdate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(ContentCleanRule).filter(ContentCleanRule.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") for k, v in data.model_dump(exclude_unset=True).items(): if v is not None: setattr(item, k, v) db.commit() db.refresh(item) return item @router.delete("/content-clean-rules/{item_id}") def delete_content_clean_rule(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(ContentCleanRule).filter(ContentCleanRule.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") db.delete(item) db.commit() return {"message": "删除成功"} @router.get("/trend-field-mapping", response_model=List[TrendFieldMappingResponse]) def list_trend_field_mapping(db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): _ensure_defaults(db) return db.query(TrendFieldMapping).filter(TrendFieldMapping.is_active == True).order_by(TrendFieldMapping.sort_order).all() @router.post("/trend-field-mapping", response_model=TrendFieldMappingResponse) def create_trend_field_mapping(data: TrendFieldMappingCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = TrendFieldMapping(**data.model_dump()) db.add(item) db.commit() db.refresh(item) return item @router.put("/trend-field-mapping/{item_id}", response_model=TrendFieldMappingResponse) def update_trend_field_mapping(item_id: int, data: TrendFieldMappingUpdate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(TrendFieldMapping).filter(TrendFieldMapping.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") for k, v in data.model_dump(exclude_unset=True).items(): if v is not None: setattr(item, k, v) db.commit() db.refresh(item) return item @router.delete("/trend-field-mapping/{item_id}") def delete_trend_field_mapping(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): item = db.query(TrendFieldMapping).filter(TrendFieldMapping.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="未找到") db.delete(item) db.commit() return {"message": "删除成功"} @router.get("/system-config/{key}", response_model=SystemConfigValueResponse) def get_system_config(key: str, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): from ..models import SystemConfig cfg = db.query(SystemConfig).filter(SystemConfig.key == key).first() if not cfg: default_map = { "trend_domains": json.dumps(DEFAULT_DOMAINS), "rss_default_keywords": json.dumps(DEFAULT_GLOBAL_RSS_KEYWORDS), "priority_weights": json.dumps(DEFAULT_PRIORITY_WEIGHTS), } if key in default_map: return SystemConfigValueResponse(key=key, value=default_map[key], description=f"系统默认配置 - {key}") raise HTTPException(status_code=404, detail="未找到") return SystemConfigValueResponse(key=cfg.key, value=cfg.value, description=cfg.description) @router.put("/system-config/{key}") def update_system_config(key: str, value: str, description: Optional[str] = None, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): from ..models import SystemConfig cfg = db.query(SystemConfig).filter(SystemConfig.key == key).first() if cfg: cfg.value = value if description is not None: cfg.description = description else: cfg = SystemConfig(key=key, value=value, description=description or key) db.add(cfg) db.commit() return {"message": "保存成功", "key": key, "value": value} @router.get("/system-configs") def list_system_configs(db: Session = Depends(get_db), admin_user=Depends(get_current_admin)): from ..models import SystemConfig configs = db.query(SystemConfig).order_by(SystemConfig.key).all() defaults = { "trend_domains": json.dumps(DEFAULT_DOMAINS), "rss_default_keywords": json.dumps(DEFAULT_GLOBAL_RSS_KEYWORDS), "priority_weights": json.dumps(DEFAULT_PRIORITY_WEIGHTS), } result = [] for c in configs: result.append({"key": c.key, "value": c.value, "description": c.description, "is_default": False}) for k, v in defaults.items(): if not any(x["key"] == k for x in result): result.append({"key": k, "value": v, "description": f"系统默认 - {k}", "is_default": True}) return result