Add platform config website_url, admin tab, fix writer DB config fallback, add trigger endpoints
This commit is contained in:
@@ -9,6 +9,7 @@ from ..database import get_db
|
||||
from ..models import User, Topic, SystemConfig
|
||||
from ..schemas import UserCreate, UserUpdate, UserResponse
|
||||
from ..core.audit_logger import audit_log
|
||||
from .auth import org_filter
|
||||
import json
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from ..core.nvidia_client import call_llm
|
||||
from .auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/assistant", tags=["assistant"])
|
||||
|
||||
PAGE_CONTEXTS = {
|
||||
"dashboard": "仪表盘页面:显示系统概览统计(选题总数、待处理、待审查、待发布、已发布、今日新增)、模块状态卡片(6个定时任务的运行状态)、近期计划列表",
|
||||
"topics": "选题管理页面:创建和管理内容选题,支持筛选、排序、评分、编辑、预览等功能",
|
||||
"metrics": "数据分析页面:展示各平台内容表现数据,包括浏览量、点赞、收藏、评论等指标的趋势图表",
|
||||
"calendar": "内容日历页面:以日历形式展示内容发布计划,支持创建、编辑、拖拽调整日期",
|
||||
"assets": "素材管理页面:管理图片、文件等媒体素材,支持上传、标签分类、关联选题",
|
||||
"tasks": "任务管理页面:查看内容创作任务的执行状态,包括自动采集、同步、生成、优化等任务",
|
||||
"platforms": "平台配置页面:管理各发布平台的配置,包括平台名称、API端点、发布格式等",
|
||||
"admin": "系统管理页面:管理用户、LLM配置、系统配置、采集类别、信息源、组织、查看运行日志等",
|
||||
}
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
page: str = "dashboard"
|
||||
history: List[dict] = []
|
||||
|
||||
@router.post("/chat")
|
||||
def chat(request: ChatRequest, current_user=Depends(get_current_user)):
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import SystemConfig
|
||||
db = SessionLocal()
|
||||
sc = db.query(SystemConfig).filter(SystemConfig.key == "assistant_system_prompt").first()
|
||||
base_prompt = sc.value if sc else None
|
||||
db.close()
|
||||
except Exception:
|
||||
base_prompt = None
|
||||
|
||||
page_name = request.page
|
||||
page_guide = PAGE_CONTEXTS.get(page_name, "未知页面")
|
||||
username = current_user.username if hasattr(current_user, 'username') else '用户'
|
||||
|
||||
system_prompt = base_prompt or f"""你是一个智能内容创作平台助手,帮助用户解答平台使用问题。
|
||||
|
||||
当前用户 {username} 正在查看:{page_guide}
|
||||
|
||||
你可以帮助用户:
|
||||
1. 解释各个页面的功能和操作方法
|
||||
2. 解答平台使用中的疑问
|
||||
3. 提供操作建议和最佳实践
|
||||
4. 指导用户完成具体的操作步骤
|
||||
|
||||
请用友好、简洁的中文回答。如果不确定,建议用户参考相关页面或联系管理员。一次回答控制在200字以内。"""
|
||||
|
||||
messages = request.history[-20:]
|
||||
conversation = "\n".join([f"{'用户' if m.get('role') == 'user' else '助手'}: {m.get('content', '')}" for m in messages])
|
||||
prompt_text = f"{conversation}\n用户: {request.message}\n助手:"
|
||||
|
||||
try:
|
||||
reply = call_llm(prompt_text, system_prompt=system_prompt, temperature=0.5, max_tokens=1000)
|
||||
return {"reply": reply}
|
||||
except Exception as e:
|
||||
return {"reply": f"抱歉,AI助手暂时无法响应,请稍后重试。"}
|
||||
@@ -142,6 +142,30 @@ def run_sync():
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/optimize-sources/run")
|
||||
def trigger_optimize_sources():
|
||||
try:
|
||||
from ..core.scheduler import scheduler
|
||||
scheduler._run_optimize_sources()
|
||||
log_file = LOGS_DIR / f"optimizer_sources_{date.today().isoformat()}.log"
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.write_text(f"{datetime.now().isoformat()} - 信息源优化完成\n")
|
||||
return {"message": "信息源优化已完成"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/metrics-sync/run")
|
||||
def trigger_metrics_sync():
|
||||
try:
|
||||
from ..core.scheduler import scheduler
|
||||
scheduler._run_metrics_sync()
|
||||
log_file = LOGS_DIR / f"metrics_sync_{date.today().isoformat()}.log"
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.write_text(f"{datetime.now().isoformat()} - 指标同步完成\n")
|
||||
return {"message": "指标同步已完成"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/automation/topics")
|
||||
def list_automation_topics(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
try:
|
||||
@@ -187,7 +211,6 @@ def get_modules_status():
|
||||
today_str = date.today().isoformat()
|
||||
log_based: dict = {
|
||||
"scheduled_collect": {"name": "📡 内容采集", "log": LOGS_DIR / f"collector_{today_str}.log"},
|
||||
"scheduled_sync": {"name": "🔄 数据同步", "log": LOGS_DIR / f"sync_{today_str}.log"},
|
||||
"scheduled_generate": {"name": "🤖 内容创作", "log": LOGS_DIR / f"creator_{today_str}.log"},
|
||||
"scheduled_optimize": {"name": "🔍 内容优化", "log": LOGS_DIR / f"optimizer_{today_str}.log"},
|
||||
"scheduled_optimize_sources": {"name": "📡 信息源优化", "log": LOGS_DIR / f"optimizer_sources_{today_str}.log"},
|
||||
@@ -208,12 +231,21 @@ def get_modules_status():
|
||||
total = task_count + content.count("失败") + content.count("failed") + content.count("ERROR")
|
||||
success_rate = round(task_count / total * 100) if total > 0 else None
|
||||
status = "running" if mod_id in jobs else "stopped"
|
||||
job = jobs.get(mod_id)
|
||||
next_run = None
|
||||
if job and job.get("next_run_time"):
|
||||
try:
|
||||
next_dt = datetime.fromisoformat(job["next_run_time"])
|
||||
next_run = next_dt.strftime("%Y-%m-%d %H:%M")
|
||||
except Exception:
|
||||
next_run = job["next_run_time"]
|
||||
modules.append({
|
||||
"id": mod_id,
|
||||
"title": cfg["name"],
|
||||
"status": status,
|
||||
"last_run": last_run or "从未运行",
|
||||
"next_run": next_run or "待计划",
|
||||
"task_count": task_count,
|
||||
"success_rate": success_rate,
|
||||
"success_rate": success_rate if success_rate is not None else 0,
|
||||
})
|
||||
return {"modules": modules, "scheduler": {"running": scheduler._started, "jobs": scheduler.get_jobs()}}
|
||||
|
||||
@@ -70,12 +70,26 @@ def _get_provider_config(provider: Optional[str] = None) -> dict:
|
||||
"base_url": base_url or fb.get("base_url", ""),
|
||||
}
|
||||
|
||||
def _get_db_defaults(provider: Optional[str] = None) -> dict:
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import LLMConfig
|
||||
db = SessionLocal()
|
||||
p = provider or _get_active_provider()
|
||||
cfg = db.query(LLMConfig).filter(LLMConfig.provider == p).order_by(LLMConfig.is_active.desc()).first()
|
||||
db.close()
|
||||
if cfg:
|
||||
return {"temperature": cfg.temperature, "max_tokens": cfg.max_tokens}
|
||||
except Exception:
|
||||
pass
|
||||
return {"temperature": 0.20, "max_tokens": 2048}
|
||||
|
||||
def call_llm(
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
system_prompt: str = "你是一个专业的内容创作助手。",
|
||||
temperature: float = 0.20,
|
||||
max_tokens: int = 2048,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
top_p: float = 0.70,
|
||||
frequency_penalty: float = 0.00,
|
||||
presence_penalty: float = 0.00,
|
||||
@@ -83,6 +97,9 @@ def call_llm(
|
||||
provider: Optional[str] = None,
|
||||
additional_params: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
defaults = _get_db_defaults(provider)
|
||||
temperature = temperature if temperature is not None else defaults["temperature"]
|
||||
max_tokens = max_tokens if max_tokens is not None else defaults["max_tokens"]
|
||||
cfg = _get_provider_config(provider)
|
||||
endpoint = f"{cfg['base_url'].rstrip('/')}/chat/completions"
|
||||
headers = {
|
||||
@@ -186,7 +203,7 @@ def expand_content_with_llm(
|
||||
prompt = f"# 参考资料\n{context}\n\n{prompt}"
|
||||
|
||||
try:
|
||||
result = call_llm(prompt, temperature=0.3, max_tokens=1500)
|
||||
result = call_llm(prompt, temperature=0.3)
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"## {section_title}\n\n(LLM 调用失败:{e},请手动补充)"
|
||||
|
||||
@@ -9,7 +9,6 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from .generator import run_creator
|
||||
from .optimizer import run_optimizer
|
||||
from .sync import sync_all_topics
|
||||
from .collector import run_collector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -28,17 +27,6 @@ class TaskScheduler:
|
||||
self._run_collect,
|
||||
CronTrigger(hour=1, minute=30),
|
||||
id='scheduled_collect',
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
self._run_sync,
|
||||
CronTrigger(hour=2, minute=30),
|
||||
id='scheduled_sync',
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
self._run_generate,
|
||||
@@ -176,14 +164,6 @@ class TaskScheduler:
|
||||
except Exception as e:
|
||||
logger.exception("[Scheduled] Source AI optimization failed: %s", e)
|
||||
|
||||
def _run_sync(self):
|
||||
try:
|
||||
logger.info("[Scheduled] Starting data sync...")
|
||||
sync_all_topics()
|
||||
logger.info("[Scheduled] Sync completed")
|
||||
except Exception as e:
|
||||
logger.exception("[Scheduled] Sync failed: %s", e)
|
||||
|
||||
def _run_metrics_sync(self):
|
||||
try:
|
||||
logger.info("[Scheduled] Starting metrics sync...")
|
||||
|
||||
@@ -60,6 +60,7 @@ def init_db():
|
||||
("platform_configs", "image_height", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "min_words", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "max_words", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "website_url", "VARCHAR"),
|
||||
]:
|
||||
try:
|
||||
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}"))
|
||||
|
||||
@@ -38,22 +38,20 @@ def import_initial_data():
|
||||
# 补充或更新 LLM 供应商配置(opencode-go 为主,nvidia 为备)
|
||||
expected = {
|
||||
"opencode-go": dict(provider="opencode-go", model="deepseek-v4-flash",
|
||||
base_url="https://opencode.ai/zen/go/v1", temperature=0.7, max_tokens=2000, is_active=True,
|
||||
base_url="https://opencode.ai/zen/go/v1", temperature=0.7, max_tokens=131072, is_active=True,
|
||||
user_prompt_template="将以下内容扩展为完整文章:\n{topic_title}\n{core_concept}"),
|
||||
"nvidia": dict(provider="nvidia", model="stepfun-ai/step-3.5-flash",
|
||||
base_url="https://integrate.api.nvidia.com/v1", temperature=0.5, max_tokens=2000, is_active=False,
|
||||
base_url="https://integrate.api.nvidia.com/v1", temperature=0.5, max_tokens=131072, is_active=False,
|
||||
user_prompt_template="将以下内容扩展为完整章节:\n{section_content}"),
|
||||
}
|
||||
existing = {c.name: c for c in db.query(LLMConfig).all()}
|
||||
# 删除完全无意义的旧残留
|
||||
for name in list(existing.keys()):
|
||||
if name not in expected:
|
||||
db.delete(existing[name]); existing.pop(name)
|
||||
for name, cfg in expected.items():
|
||||
if name in existing:
|
||||
c = existing[name]
|
||||
# 仅补缺失字段,不覆写用户已修改的值
|
||||
for k, v in cfg.items():
|
||||
setattr(c, k, v)
|
||||
if getattr(c, k, None) is None:
|
||||
setattr(c, k, v)
|
||||
else:
|
||||
db.add(LLMConfig(name=name, **cfg))
|
||||
db.commit()
|
||||
|
||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
|
||||
from .database import engine, get_db, init_db
|
||||
from .models import Base
|
||||
from .api import topics, system, articles, publishing, auth, admin, audit, optimizer_logs, cases, task_logs, llm_configs, system_configs, topic_config, calendar, metrics, assets, tasks, platform_config, collector_mgmt
|
||||
from .api import topics, system, articles, publishing, auth, admin, audit, optimizer_logs, cases, task_logs, llm_configs, system_configs, topic_config, calendar, metrics, assets, tasks, platform_config, collector_mgmt, assistant
|
||||
from .initial_data import import_initial_data
|
||||
from .core.scheduler import scheduler
|
||||
|
||||
@@ -93,6 +93,7 @@ app.include_router(assets.router)
|
||||
app.include_router(tasks.router)
|
||||
app.include_router(platform_config.router)
|
||||
app.include_router(collector_mgmt.router)
|
||||
app.include_router(assistant.router)
|
||||
|
||||
# 挂载前端
|
||||
FRONTEND_DIR = Path(__file__).parent.parent.parent / "frontend"
|
||||
|
||||
@@ -355,6 +355,7 @@ class PlatformConfig(Base):
|
||||
platform = Column(String, unique=True, nullable=False) # zhihu/wechat/xiaohongshu
|
||||
name = Column(String, nullable=False)
|
||||
icon = Column(String, nullable=True)
|
||||
website_url = Column(String, nullable=True)
|
||||
api_endpoint = Column(String, nullable=True)
|
||||
auth_config = Column(JSON, default=dict)
|
||||
format_template = Column(JSON, default=dict)
|
||||
@@ -377,6 +378,7 @@ class PlatformConfig(Base):
|
||||
"platform": self.platform,
|
||||
"name": self.name,
|
||||
"icon": self.icon,
|
||||
"website_url": self.website_url,
|
||||
"api_endpoint": self.api_endpoint,
|
||||
"format_template": self.format_template or {},
|
||||
"compliance_rules": self.compliance_rules or {},
|
||||
|
||||
@@ -257,6 +257,7 @@ class PlatformConfigBase(BaseModel):
|
||||
platform: str
|
||||
name: str
|
||||
icon: Optional[str] = None
|
||||
website_url: Optional[str] = None
|
||||
api_endpoint: Optional[str] = None
|
||||
auth_config: Dict[str, Any] = {}
|
||||
format_template: Dict[str, Any] = {}
|
||||
@@ -279,6 +280,7 @@ class PlatformConfigCreate(PlatformConfigBase):
|
||||
class PlatformConfigUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
website_url: Optional[str] = None
|
||||
api_endpoint: Optional[str] = None
|
||||
auth_config: Optional[Dict[str, Any]] = None
|
||||
format_template: Optional[Dict[str, Any]] = None
|
||||
@@ -476,7 +478,7 @@ class LLMConfigBase(BaseModel):
|
||||
system_prompt: Optional[str] = None
|
||||
user_prompt_template: str
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 2000
|
||||
max_tokens: int = 131072
|
||||
model: Optional[str] = None
|
||||
provider: str = "opencode-go"
|
||||
base_url: Optional[str] = None
|
||||
|
||||
Reference in New Issue
Block a user