配置全面迁移数据库:PromptConfig、TaskConfig动态调度、敏感词/清洗规则/趋势映射/平台标签/痛点模板全部可编辑

- 新增 PromptConfig 模型 + API,支持提示词在线编辑(16条默认)
- 调度器动态读取 TaskConfig.schedule,admin 可调执行时间
- 新增 KeywordDomainMap、SensitiveWord、ContentCleanRule、TrendFieldMapping 表
- DOMAINS、TREND_DOMAIN_MAP、PLATFORM_TAGS、china_pains、RSS关键词、priority_weights 全部迁移到 DB
- tasks.html 重构:卡片网格+配置/产出/历史/提示词四个Tab,折叠显示
- 清理冗余代码:DEFAULT_PROMPTS死代码、collector.py unreachable代码、compliance_checker bug
- strip_thinking_html 改用 DB 规则优先
This commit is contained in:
Yuzhiran Dev
2026-05-22 11:18:23 +08:00
parent a8e0a76e07
commit 1855f190f5
31 changed files with 2927 additions and 1127 deletions
+390
View File
@@ -0,0 +1,390 @@
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"<p[^>]*>(好的|好的,|好[的,]|我来|让我|我将|我这就)", "description": "AI思考-HTML模式", "sort_order": 16},
]
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))
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, enabled: bool = None, 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="未找到")
if enabled is not None:
item.is_active = enabled
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