fix: 三平台内容差异化 + admin敏感词管理表格化
- writer.py: _expand_section() 去除 <100字阈值,始终调用 LLM 平台专属扩写
- prompt_loader.py: 新增 section_expansion_zhihu/wechat/xiaohongshu 三个独立 prompt
- admin.html: 配置管理标签页 + 敏感词/清理规则子标签 + 敏感词表格化管理(编辑/删除)
- config_items.py: PUT /sensitive-words/{id} 支持更新 word/category
- compliance_checker.py: AI 套话从 DB 加载 + 人称规则修正
- initial_data.py: PlatformConfig 字数迁移 + 新种子
- 各前端页面: LLM 配置 rate_limit 字段 + 供应商列表排序
This commit is contained in:
@@ -65,6 +65,7 @@ def run_creator_blocking(topic_id: str = None, timeout: int = 1800):
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if topic:
|
||||
topic.generated_at = datetime.now(timezone.utc)
|
||||
topic.reviewed_at = datetime.now(timezone.utc)
|
||||
if topic.status in ('pending', '待处理'):
|
||||
topic.status = 'review'
|
||||
db.commit()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
Unified LLM Client
|
||||
支持 NVIDIA / opencode-go / 兼容 OpenAI 格式的 API,配置从环境变量读取
|
||||
支持 NVIDIA / sensenova / 兼容 OpenAI 格式的 API,配置从环境变量读取
|
||||
支持模型级 rate limit 和任务级模型选择(LLM_TASK_MODEL)
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
@@ -30,10 +32,28 @@ _API_KEYS = {
|
||||
# 代码级回退默认值(实际配置优先从 DB 读取)
|
||||
_FALLBACK = {
|
||||
"opencode-go": {"base_url": "https://opencode.ai/zen/go/v1", "model": "deepseek-v4-flash"},
|
||||
"nvidia": {"base_url": "https://integrate.api.nvidia.com/v1", "model": "stepfun-ai/step-3.7-flash"},
|
||||
"nvidia": {"base_url": "https://integrate.api.nvidia.com/v1", "model": "stepfun-ai/step-3.5-flash"},
|
||||
"sensenova": {"base_url": "https://token.sensenova.cn/v1", "model": "deepseek-v4-flash"},
|
||||
}
|
||||
|
||||
# 模型级 rate limiter(进程内,重启重置)
|
||||
_RATE_LIMITER: Dict[str, List[float]] = {}
|
||||
|
||||
def _check_rate_limit(provider: str, model: str, limit: int, window_minutes: int) -> bool:
|
||||
if limit <= 0:
|
||||
return True
|
||||
key = f"{provider}/{model}"
|
||||
now = time.time()
|
||||
window_sec = window_minutes * 60
|
||||
timestamps = _RATE_LIMITER.get(key, [])
|
||||
timestamps = [t for t in timestamps if now - t < window_sec]
|
||||
_RATE_LIMITER[key] = timestamps
|
||||
return len(timestamps) < limit
|
||||
|
||||
def _record_usage(provider: str, model: str):
|
||||
key = f"{provider}/{model}"
|
||||
_RATE_LIMITER.setdefault(key, []).append(time.time())
|
||||
|
||||
def _get_active_provider() -> str:
|
||||
"""从 DB 读取活跃供应商,优先取 is_default=True;DB 不可用时回退环境变量"""
|
||||
try:
|
||||
@@ -61,6 +81,8 @@ def _get_provider_config(provider: Optional[str] = None) -> dict:
|
||||
db_model = None
|
||||
db_base_url = None
|
||||
db_api_key = None
|
||||
db_rate_limit = 0
|
||||
db_rate_window = 300
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import LLMConfig
|
||||
@@ -70,6 +92,8 @@ def _get_provider_config(provider: Optional[str] = None) -> dict:
|
||||
db_model = cfg.model
|
||||
db_base_url = cfg.base_url
|
||||
db_api_key = cfg.api_key
|
||||
db_rate_limit = cfg.rate_limit or 0
|
||||
db_rate_window = cfg.rate_limit_window_minutes or 300
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -82,6 +106,8 @@ def _get_provider_config(provider: Optional[str] = None) -> dict:
|
||||
"api_key": api_key,
|
||||
"model": db_model or fb.get("model", ""),
|
||||
"base_url": db_base_url or fb.get("base_url", ""),
|
||||
"rate_limit": db_rate_limit,
|
||||
"rate_limit_window_minutes": db_rate_window,
|
||||
}
|
||||
|
||||
def _get_db_defaults(provider: Optional[str] = None) -> dict:
|
||||
@@ -139,14 +165,25 @@ def call_llm(
|
||||
if not provider and os.getenv("LLM_TASK_PROVIDER"):
|
||||
task_provider = os.getenv("LLM_TASK_PROVIDER")
|
||||
providers_to_try = [task_provider] + [p for p in providers_to_try if p != task_provider]
|
||||
# LLM_TASK_MODEL 环境变量可覆盖任务级别的模型选择
|
||||
if not model and os.getenv("LLM_TASK_MODEL"):
|
||||
model = os.getenv("LLM_TASK_MODEL")
|
||||
last_error = None
|
||||
for p in providers_to_try:
|
||||
try:
|
||||
cfg = _get_provider_config(p)
|
||||
actual_model = model or cfg["model"]
|
||||
# Rate limit check
|
||||
rl = cfg.get("rate_limit", 0)
|
||||
rw = cfg.get("rate_limit_window_minutes", 300)
|
||||
if not _check_rate_limit(p, actual_model, rl, rw):
|
||||
logger.warning(f"[LLM] {p}/{actual_model} rate limit reached ({rl}/{rw}min) → 尝试下一个")
|
||||
last_error = LLMError(f"{p}/{actual_model} rate limit reached")
|
||||
continue
|
||||
endpoint = f"{cfg['base_url'].rstrip('/')}/chat/completions"
|
||||
headers = {"Authorization": f"Bearer {cfg['api_key']}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"model": model or cfg["model"],
|
||||
"model": actual_model,
|
||||
"messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}],
|
||||
"temperature": temperature, "max_tokens": max_tokens, "top_p": top_p,
|
||||
"frequency_penalty": frequency_penalty, "presence_penalty": presence_penalty,
|
||||
@@ -176,6 +213,7 @@ def call_llm(
|
||||
if delta.get('content'): content_parts.append(delta['content'])
|
||||
if delta.get('reasoning_content'): reasoning_parts.append(delta['reasoning_content'])
|
||||
except Exception: continue
|
||||
_record_usage(p, actual_model)
|
||||
return "".join(content_parts) or "".join(reasoning_parts)
|
||||
else:
|
||||
data = resp.json()
|
||||
@@ -186,6 +224,7 @@ def call_llm(
|
||||
if rc:
|
||||
parts = [p.strip() for p in rc.replace('\n', '。').split('。') if p.strip()]
|
||||
content = parts[-1] if parts else rc
|
||||
_record_usage(p, actual_model)
|
||||
return content.strip() or ''
|
||||
except LLMError as e:
|
||||
if '429' in str(e):
|
||||
|
||||
@@ -20,7 +20,7 @@ from .collector import run_collector_blocking
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _set_task_llm_provider(module_id: str):
|
||||
"""从 TaskConfig 读取 llm_provider 并设为环境变量,供子进程和 call_llm 读取"""
|
||||
"""从 TaskConfig 读取 llm_provider 和 llm_model 并设为环境变量,供子进程和 call_llm 读取"""
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import TaskConfig
|
||||
@@ -32,10 +32,20 @@ def _set_task_llm_provider(module_id: str):
|
||||
if provider:
|
||||
os.environ["LLM_TASK_PROVIDER"] = provider
|
||||
logger.debug("[%s] LLM provider set to %s", module_id, provider)
|
||||
return
|
||||
else:
|
||||
os.environ.pop("LLM_TASK_PROVIDER", None)
|
||||
|
||||
model = cfg.params.get("llm_model")
|
||||
if model:
|
||||
os.environ["LLM_TASK_MODEL"] = model
|
||||
logger.debug("[%s] LLM model set to %s", module_id, model)
|
||||
else:
|
||||
os.environ.pop("LLM_TASK_MODEL", None)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
os.environ.pop("LLM_TASK_PROVIDER", None)
|
||||
os.environ.pop("LLM_TASK_MODEL", None)
|
||||
|
||||
MODULES = {
|
||||
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "cron": "01:10"},
|
||||
|
||||
Reference in New Issue
Block a user