Fix calendar filter, module triggers, API cleanup, consolidate get_current_admin, fix LLM schema

This commit is contained in:
Yuzhiran Dev
2026-05-20 14:15:00 +08:00
parent 498165440f
commit 0a8fbda4fd
23 changed files with 118 additions and 155 deletions
+5 -4
View File
@@ -86,10 +86,11 @@
| 步骤 | 触发方式 | 说明 | | 步骤 | 触发方式 | 说明 |
|------|---------|------| |------|---------|------|
| 采集 | 定时 01:30 | 热点趋势采集→生成选题建议→存入选题库 | | 内容采集 | 定时 01:30 | 热点趋势采集→生成选题建议→存入选题库 |
| 同步 | 定时 02:30 | DB → JSON 备份同步 | | 内容创作 | 手动点击 / 定时 03:30 | 研究→大纲→撰写文章→合规审查→存入 articles 表 |
| 创作 | 手动点击 / 定时 03:30 | 生成三平台文章 → 存入 articles 表 → 状态→待审查 | | 合规审查 | 手动点击 / 定时 04:30 | 从 articles 表读取 draft→合规检查→LLM迭代修复(最多3次)→状态→待发布 |
| 审查 | 手动点击 / 创作后自动 | 从 articles 表读取 → 合规检查 → LLM迭代修复(最多3次)→合规分回写→状态→待发布 | | 信息源优化 | 定时 05:00 | AI评估采集类别与信息源配置,给出调整建议 |
| 指标同步 | 定时 06:00 | 同步统计数据 |
| 发布 | 手动点击 | 仅待发布状态可选 | | 发布 | 手动点击 | 仅待发布状态可选 |
--- ---
+1 -13
View File
@@ -9,23 +9,11 @@ from ..database import get_db
from ..models import User, Topic, SystemConfig from ..models import User, Topic, SystemConfig
from ..schemas import UserCreate, UserUpdate, UserResponse from ..schemas import UserCreate, UserUpdate, UserResponse
from ..core.audit_logger import audit_log from ..core.audit_logger import audit_log
from .auth import org_filter from .auth import get_current_admin, org_filter
import json import json
router = APIRouter(prefix="/api/admin", tags=["admin"]) router = APIRouter(prefix="/api/admin", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token, org_filter
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("/users", response_model=List[UserResponse]) @router.get("/users", response_model=List[UserResponse])
def list_users( def list_users(
request: Request, request: Request,
+4 -15
View File
@@ -4,28 +4,17 @@ from typing import List, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ..database import get_db from ..database import get_db
from ..models import AuditLog from ..models import AuditLog, User
from ..schemas import AuditLogResponse from ..schemas import AuditLogResponse
from .auth import verify_token from .auth import get_current_admin
router = APIRouter(prefix="/api/audit", tags=["audit"]) router = APIRouter(prefix="/api/audit", tags=["audit"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("/logs", response_model=List[AuditLogResponse]) @router.get("/logs", response_model=List[AuditLogResponse])
def list_audit_logs( def list_audit_logs(
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
admin_user: bool = Depends(get_current_admin), admin_user: User = Depends(get_current_admin),
username: Optional[str] = Query(None, description="按用户名筛选"), username: Optional[str] = Query(None, description="按用户名筛选"),
action: Optional[str] = Query(None, description="按操作类型筛选"), action: Optional[str] = Query(None, description="按操作类型筛选"),
resource_type: Optional[str] = Query(None, description="按资源类型筛选"), resource_type: Optional[str] = Query(None, description="按资源类型筛选"),
@@ -64,7 +53,7 @@ def list_audit_actions(
def list_audit_users( def list_audit_users(
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
admin_user: bool = Depends(get_current_admin), admin_user: User = Depends(get_current_admin),
limit: int = Query(50, ge=1, le=200) limit: int = Query(50, ge=1, le=200)
): ):
"""获取最近产生审计记录的用户列表""" """获取最近产生审计记录的用户列表"""
+1 -12
View File
@@ -5,21 +5,10 @@ from typing import List
from ..database import get_db from ..database import get_db
from ..models import Case from ..models import Case
from ..schemas import CaseBase, CaseResponse from ..schemas import CaseBase, CaseResponse
from .auth import get_current_admin
router = APIRouter(prefix="/api/admin/cases", tags=["admin"]) router = APIRouter(prefix="/api/admin/cases", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("", response_model=List[CaseResponse]) @router.get("", response_model=List[CaseResponse])
def list_cases( def list_cases(
request: Request, request: Request,
+1 -12
View File
@@ -5,21 +5,10 @@ from typing import List
from ..database import get_db from ..database import get_db
from ..models import LLMConfig from ..models import LLMConfig
from ..schemas import LLMConfigBase, LLMConfigResponse from ..schemas import LLMConfigBase, LLMConfigResponse
from .auth import get_current_admin
router = APIRouter(prefix="/api/admin/llmconfigs", tags=["admin"]) router = APIRouter(prefix="/api/admin/llmconfigs", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("", response_model=List[LLMConfigResponse]) @router.get("", response_model=List[LLMConfigResponse])
def list_llm_configs( def list_llm_configs(
request: Request, request: Request,
+11 -1
View File
@@ -11,6 +11,7 @@ from ..database import get_db
from ..models import Topic, Article from ..models import Topic, Article
from ..core.generator import run_creator from ..core.generator import run_creator
from ..core.optimizer import run_optimizer from ..core.optimizer import run_optimizer
from ..core.collector import run_collector
from ..core.sync import sync_all_topics from ..core.sync import sync_all_topics
from ..core.scheduler import scheduler from ..core.scheduler import scheduler
from .auth import get_current_user, org_filter from .auth import get_current_user, org_filter
@@ -69,6 +70,15 @@ def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Dep
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/collect/run", dependencies=[Depends(get_current_user)])
def trigger_collection(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
logger.info(f"Manual collection triggered by {current_user.username}")
try:
result = run_collector()
return {"message": "内容采集已完成", "result": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/review/run", dependencies=[Depends(get_current_user)]) @router.post("/review/run", dependencies=[Depends(get_current_user)])
def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)): def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)):
try: try:
@@ -212,7 +222,7 @@ def get_modules_status():
log_based: dict = { log_based: dict = {
"scheduled_collect": {"name": "📡 内容采集", "log": LOGS_DIR / f"collector_{today_str}.log"}, "scheduled_collect": {"name": "📡 内容采集", "log": LOGS_DIR / f"collector_{today_str}.log"},
"scheduled_generate": {"name": "🤖 内容创作", "log": LOGS_DIR / f"creator_{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": {"name": "🔍 合规审查", "log": LOGS_DIR / f"optimizer_{today_str}.log"},
"scheduled_optimize_sources": {"name": "📡 信息源优化", "log": LOGS_DIR / f"optimizer_sources_{today_str}.log"}, "scheduled_optimize_sources": {"name": "📡 信息源优化", "log": LOGS_DIR / f"optimizer_sources_{today_str}.log"},
"scheduled_metrics_sync": {"name": "📊 指标同步", "log": LOGS_DIR / f"metrics_sync_{today_str}.log"}, "scheduled_metrics_sync": {"name": "📊 指标同步", "log": LOGS_DIR / f"metrics_sync_{today_str}.log"},
} }
+1 -12
View File
@@ -5,21 +5,10 @@ from typing import List, Dict, Any
from ..database import get_db from ..database import get_db
from ..models import SystemConfig from ..models import SystemConfig
from ..schemas import SystemConfigBase, SystemConfigResponse from ..schemas import SystemConfigBase, SystemConfigResponse
from .auth import get_current_admin
router = APIRouter(prefix="/api/admin/systemconfigs", tags=["admin"]) router = APIRouter(prefix="/api/admin/systemconfigs", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("", response_model=List[SystemConfigResponse]) @router.get("", response_model=List[SystemConfigResponse])
def list_system_configs( def list_system_configs(
request: Request, request: Request,
+1 -12
View File
@@ -5,21 +5,10 @@ from typing import List, Optional
from ..database import get_db from ..database import get_db
from ..models import TaskLog from ..models import TaskLog
from ..schemas import TaskLogBase, TaskLogResponse from ..schemas import TaskLogBase, TaskLogResponse
from .auth import get_current_admin
router = APIRouter(prefix="/api/admin/tasklogs", tags=["admin"]) router = APIRouter(prefix="/api/admin/tasklogs", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("", response_model=List[TaskLogResponse]) @router.get("", response_model=List[TaskLogResponse])
def list_task_logs( def list_task_logs(
request: Request, request: Request,
+1 -28
View File
@@ -6,10 +6,9 @@ from datetime import datetime, date
from pathlib import Path from pathlib import Path
from ..database import get_db from ..database import get_db
from ..models import Topic, TopicField, TopicConfigField, Article, PublishRecord, ContentMetrics from ..models import Topic, TopicField, TopicConfigField, Article, ContentMetrics
from ..schemas import ( from ..schemas import (
TopicCreate, TopicUpdate, TopicResponse, TopicScoreRequest, TopicCreate, TopicUpdate, TopicResponse, TopicScoreRequest,
PublishRequest, PublishActionRequest, PublishRecordResponse
) )
from .auth import get_current_user, org_filter from .auth import get_current_user, org_filter
@@ -241,32 +240,6 @@ def score_topic(
return {"priority_score": topic.priority_score, "total_score": topic.total_score} return {"priority_score": topic.priority_score, "total_score": topic.total_score}
@router.post("/{topic_id}/publish")
def publish_topic(topic_id: str, req: PublishRequest, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="Topic not found")
_check_org(topic, current_user, db)
if topic.status not in ("pending", "ready", "draft"):
raise HTTPException(status_code=400, detail=f"选题状态({topic.status})不允许发布")
topic.status = "published"
topic.published_at = date.today()
topic.updated_at = datetime.now()
topic.platform_urls = req.platform_urls
record = PublishRecord(
topic_id=topic_id,
platform="all",
action="publish",
status="success",
description=f"选题 {topic_id} 已发布"
)
db.add(record)
db.commit()
return {"ok": True, "topic_id": topic_id}
@router.get("/{topic_id}/articles", response_model=List[Dict[str, Any]]) @router.get("/{topic_id}/articles", response_model=List[Dict[str, Any]])
def get_topic_articles(topic_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user)): def get_topic_articles(topic_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
topic = db.query(Topic).filter(Topic.id == topic_id).first() topic = db.query(Topic).filter(Topic.id == topic_id).first()
+6 -1
View File
@@ -19,8 +19,13 @@ def run_collector():
script_path = PROJECT_ROOT / "scripts" / "collector.py" script_path = PROJECT_ROOT / "scripts" / "collector.py"
if not script_path.exists(): if not script_path.exists():
raise FileNotFoundError(f"Collector script not found: {script_path}") raise FileNotFoundError(f"Collector script not found: {script_path}")
venv_python = PROJECT_ROOT / "platform" / "backend" / "venv" / "bin" / "python"
if venv_python.exists():
cmd = [str(venv_python), str(script_path)]
else:
cmd = ["python3", str(script_path)]
result = subprocess.run( result = subprocess.run(
["python", str(script_path)], cmd,
capture_output=True, capture_output=True,
text=True, text=True,
cwd=PROJECT_ROOT, cwd=PROJECT_ROOT,
+4 -4
View File
@@ -62,7 +62,7 @@ class TaskScheduler:
) )
self.scheduler.start() self.scheduler.start()
self._started = True self._started = True
logger.info("Scheduler started with daily cron triggers (01:30 collect, 02:30 sync, 03:30 generate, 04:30 optimize, 05:00 optimize_sources, 06:00 metrics_sync)") logger.info("Scheduler started: 01:30 collect, 03:30 generate, 04:30 review, 05:00 optimize_sources, 06:00 metrics_sync")
def shutdown(self): def shutdown(self):
if self.scheduler.running: if self.scheduler.running:
self.scheduler.shutdown() self.scheduler.shutdown()
@@ -86,11 +86,11 @@ class TaskScheduler:
def _run_optimize(self): def _run_optimize(self):
try: try:
logger.info("[Scheduled] Starting compliance optimization...") logger.info("[Scheduled] Starting compliance review...")
result = run_optimizer() result = run_optimizer()
logger.info("[Scheduled] Optimization completed: %s", result) logger.info("[Scheduled] Review completed: %s", result)
except Exception as e: except Exception as e:
logger.exception("[Scheduled] Optimization failed: %s", e) logger.exception("[Scheduled] Review failed: %s", e)
def _run_collect(self): def _run_collect(self):
try: try:
-8
View File
@@ -490,8 +490,6 @@ class LLMConfigResponse(LLMConfigBase):
id: int id: int
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -519,9 +517,3 @@ class SystemStatus(BaseModel):
execution_time: Optional[float] = None execution_time: Optional[float] = None
class OptimizationRequest(BaseModel):
topic_ids: Optional[List[str]] = None
class BatchPublishRequest(BaseModel):
date: str
+12 -5
View File
@@ -106,8 +106,7 @@
<div><span>今日任务</span><span>{{ mod.task_count }} 个</span></div> <div><span>今日任务</span><span>{{ mod.task_count }} 个</span></div>
<div><span>成功率</span><span>{{ mod.success_rate > 0 ? mod.success_rate + '%' : '暂无' }}</span></div> <div><span>成功率</span><span>{{ mod.success_rate > 0 ? mod.success_rate + '%' : '暂无' }}</span></div>
<div style="margin-top:10px; border-bottom:none;"> <div style="margin-top:10px; border-bottom:none;">
<el-button v-if="mod.id === 'scheduled_optimize_sources'" size="small" type="primary" @click="triggerModule(mod.id)" :loading="runningModule === mod.id">立即运行</el-button> <el-button size="small" type="primary" @click="triggerModule(mod.id)" :loading="runningModule === mod.id">立即运行</el-button>
<el-button v-if="mod.id === 'scheduled_metrics_sync'" size="small" type="primary" @click="triggerModule(mod.id)" :loading="runningModule === mod.id">立即运行</el-button>
</div> </div>
</div> </div>
</div> </div>
@@ -271,8 +270,8 @@
const end = new Date(today); const end = new Date(today);
end.setDate(end.getDate() + 7); end.setDate(end.getDate() + 7);
this.upcomingEntries = (all || []) this.upcomingEntries = (all || [])
.filter(e => { const d = new Date(e.date); return d >= today && d < end; }) .filter(e => { const d = new Date(e.planned_date); return d >= today && d < end; })
.sort((a, b) => a.date.localeCompare(b.date)); .sort((a, b) => a.planned_date.localeCompare(b.planned_date));
} }
} catch (e) { } catch (e) {
console.error('获取近期计划失败:', e); console.error('获取近期计划失败:', e);
@@ -284,9 +283,17 @@
}, },
async triggerModule(modId) { async triggerModule(modId) {
this.runningModule = modId; this.runningModule = modId;
const endpoints = {
scheduled_collect: '/api/system/collect/run',
scheduled_generate: '/api/system/generate/run',
scheduled_optimize: '/api/system/review/run',
scheduled_optimize_sources: '/api/system/optimize-sources/run',
scheduled_metrics_sync: '/api/system/metrics-sync/run',
};
const endpoint = endpoints[modId];
if (!endpoint) { this.$message.error('未知模块'); this.runningModule = null; return; }
try { try {
const token = localStorage.getItem('authToken'); const token = localStorage.getItem('authToken');
const endpoint = modId === 'scheduled_optimize_sources' ? '/api/system/optimize-sources/run' : '/api/system/metrics-sync/run';
const resp = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); const resp = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } });
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
-1
View File
@@ -174,7 +174,6 @@ const TasksApp = {
data() { data() {
const SCHEDULER_JOBS = { const SCHEDULER_JOBS = {
'scheduled_collect': { icon: 'IconRefresh', name: '内容采集', defaultTime: '01:30' }, 'scheduled_collect': { icon: 'IconRefresh', name: '内容采集', defaultTime: '01:30' },
'scheduled_sync': { icon: 'IconRefresh', name: '数据同步', defaultTime: '02:30' },
'scheduled_generate': { icon: 'IconDocument', name: '内容创作', defaultTime: '03:30' }, 'scheduled_generate': { icon: 'IconDocument', name: '内容创作', defaultTime: '03:30' },
'scheduled_optimize': { icon: 'IconSearch', name: '合规审查', defaultTime: '04:30' }, 'scheduled_optimize': { icon: 'IconSearch', name: '合规审查', defaultTime: '04:30' },
'scheduled_optimize_sources': { icon: 'IconSetting', name: '信息源优化', defaultTime: '05:00' }, 'scheduled_optimize_sources': { icon: 'IconSetting', name: '信息源优化', defaultTime: '05:00' },
+1 -1
View File
@@ -138,7 +138,7 @@
<el-button type="primary" @click="confirmPublish" :loading="publishing" :disabled="!publishPlatforms.zhihu && !publishPlatforms.wechat && !publishPlatforms.xiaohongshu">确认发布</el-button> <el-button type="primary" @click="confirmPublish" :loading="publishing" :disabled="!publishPlatforms.zhihu && !publishPlatforms.wechat && !publishPlatforms.xiaohongshu">确认发布</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="previewVisible" title="选题预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen" close-on-press-escape> <el-dialog v-model="previewVisible" title="选题预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen" close-on-press-escape :lock-scroll="false">
<div v-if="previewTopic"> <div v-if="previewTopic">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;"> <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;">
<h2 style="margin:0; font-size:16px;">{{ previewTopic.title }}</h2> <h2 style="margin:0; font-size:16px;">{{ previewTopic.title }}</h2>
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
可持续性内容收集脚本 内容采集:趋势抓取 → 选题生成 → 存入选题库
每天凌晨5:00运行,收集全球可持续性趋势信息,提炼选题和案例 收集热点趋势信息,经LLM分析后生成选题建议并存入数据库
""" """
import os import os
+4
View File
@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
合规审查:文章合规检查 → LLM迭代修复
从articles表读取待审文章,进行合规评分;不合格文章由LLM修复(最多3次),通过后更新选题状态为待发布
"""
import json, datetime, logging, sys, re import json, datetime, logging, sys, re
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
+2 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
宇之然内容创作流水线研究 → 大纲 → 撰写 → 合规优化)v3 - DB version 内容创作流水线研究 → 大纲 → 撰写 → 合规审查
基于选题ID,依次执行research/outline/writer各阶段,创作三平台文章并存入articles表
""" """
import json, datetime, logging, sys, subprocess import json, datetime, logging, sys, subprocess
+7
View File
@@ -4,10 +4,17 @@
""" """
import sys import sys
import os
from pathlib import Path from pathlib import Path
from datetime import datetime, date from datetime import datetime, date
from typing import Optional, Dict, List from typing import Optional, Dict, List
# 加载 .env(在 scripts/ 目录下运行时需要)
_env_path = Path(__file__).parent.parent / 'platform' / 'backend' / '.env'
if _env_path.exists():
from dotenv import load_dotenv
load_dotenv(_env_path)
# 添加项目根和 backend 路径 # 添加项目根和 backend 路径
PROJECT_ROOT = Path(__file__).parent.parent PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
+4
View File
@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
选题研究:信息收集与结构化整理
基于选题方向,收集相关数据/案例/趋势,输出结构化研究笔记供大纲生成使用
"""
import json, datetime, logging, sys, re import json, datetime, logging, sys, re
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List
+2 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
选题引擎 v2:多趋势加权匹配 + 趋势缺口检测 + 新选题生成 选题引擎:多趋势加权匹配 + 趋势缺口检测 + 新选题生成
结合采集到的热点趋势和当前选题库,推荐最优选题并检测趋势缺口
""" """
import sys, json, logging import sys, json, logging
+39 -14
View File
@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
平台适配文章撰写
根据大纲和平台配置(字数/格式/配图要求),为知乎/公众号/小红书各平台生成适配内容
"""
import json, datetime, logging, sys, re import json, datetime, logging, sys, re
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List
@@ -92,15 +96,17 @@ class Writer:
if line.startswith("# "): if line.startswith("# "):
if current: if current:
sections.append(current) sections.append(current)
current = {"level": 1, "title": line[2:].strip(), "content": ""} current = {"level": 1, "title": line[2:].strip(), "content": "", "section_type": "normal"}
elif line.startswith("## "): elif line.startswith("## "):
if current: if current:
sections.append(current) sections.append(current)
current = {"level": 2, "title": line[3:].strip(), "content": ""} title = line[3:].strip()
stype = "noise" if title in ("文章大纲", "大纲", "文章结构", "结构") else "normal"
current = {"level": 2, "title": title, "content": "", "section_type": stype}
elif line.startswith("### "): elif line.startswith("### "):
if current: if current:
sections.append(current) sections.append(current)
current = {"level": 3, "title": line[4:].strip(), "content": ""} current = {"level": 3, "title": line[4:].strip(), "content": "", "section_type": "normal"}
else: else:
if current and line.strip(): if current and line.strip():
current['content'] = current.get('content', '') + line + "\n" current['content'] = current.get('content', '') + line + "\n"
@@ -112,7 +118,13 @@ class Writer:
def _clean_markdown(text: str) -> str: def _clean_markdown(text: str) -> str:
lines = text.split('\n') lines = text.split('\n')
cleaned = [] cleaned = []
in_code_fence = False
for line in lines: for line in lines:
if line.strip().startswith('```'):
in_code_fence = not in_code_fence
continue
if in_code_fence:
continue
line = re.sub(r'^#{1,6}\s+', '', line) line = re.sub(r'^#{1,6}\s+', '', line)
line = re.sub(r'^[\-\*\+]\s+', '', line) line = re.sub(r'^[\-\*\+]\s+', '', line)
line = re.sub(r'^\d+[\.\)]\s+', '', line) line = re.sub(r'^\d+[\.\)]\s+', '', line)
@@ -133,12 +145,19 @@ class Writer:
return True return True
return False return False
def _is_bullet_only(self, text: str) -> bool:
"""检查内容是否主要是要点列表(大纲格式),需要 LLM 展开"""
lines = [l.strip() for l in text.split('\n') if l.strip()]
if not lines:
return False
bullet_count = sum(1 for l in lines if l.startswith(('- ', '* ', '**', '+ ')))
return bullet_count / len(lines) > 0.4
def _expand_section(self, section: Dict) -> str: def _expand_section(self, section: Dict) -> str:
content = section.get('content', '').strip() content = section.get('content', '').strip()
if len(content) > 200: # 大纲要点格式(>40% 行以 -/*/** 开头)应始终由 LLM 展开为连贯段落
return content if HAVE_LLM and self._is_bullet_only(content):
if HAVE_LLM and len(content) < 150: logger.info(f"使用 LLM 扩写章节(要点→段落): {section['title']}")
logger.info(f"使用 LLM 扩写章节: {section['title']}")
prompt = f"""你是一个资深作者,正在写一篇关于「{self.topic['title']}」的文章。请写「{section['title']}」这一节。 prompt = f"""你是一个资深作者,正在写一篇关于「{self.topic['title']}」的文章。请写「{section['title']}」这一节。
今天日期:{datetime.datetime.now().strftime('%Y年%m月%d')} 今天日期:{datetime.datetime.now().strftime('%Y年%m月%d')}
@@ -147,7 +166,7 @@ class Writer:
{content} {content}
【输出要求】 【输出要求】
输出3-5段纯粹、流畅的段落文字,共400-800字。 输出3-6段纯粹、流畅的段落文字,每节内容根据平台需求控制在200-800字之间
格式: 格式:
- 禁止任何标题/列表/格式标记(#、-、*、1.、**等) - 禁止任何标题/列表/格式标记(#、-、*、1.、**等)
@@ -187,7 +206,8 @@ class Writer:
text += '' text += ''
sentences.append(text) sentences.append(text)
if sentences: if sentences:
return ' '.join(sentences) result = ' '.join(sentences)
return self._clean_markdown(result)
return '' return ''
def generate_full_markdown(self) -> str: def generate_full_markdown(self) -> str:
@@ -200,6 +220,12 @@ class Writer:
if expanded: if expanded:
parts.append(expanded + "\n") parts.append(expanded + "\n")
continue continue
# 跳过大纲结构噪音节点
title_stripped = sec['title'].strip()
if title_stripped in ('文章大纲', '大纲', '文章结构', '结构'):
continue
if sec.get('section_type') == 'noise':
continue
heading = f"{'#' * sec['level']} {sec['title']}" heading = f"{'#' * sec['level']} {sec['title']}"
parts.append(heading) parts.append(heading)
if sec.get('content'): if sec.get('content'):
@@ -246,11 +272,6 @@ class Writer:
else: else:
result.append(line) result.append(line)
adapted = '\n'.join(result) adapted = '\n'.join(result)
if len(adapted) > max_c:
adapted = adapted[:max_c]
last = max(adapted.rfind(''), adapted.rfind('\n'), adapted.rfind(''))
if last > max_c // 2:
adapted = adapted[:last + 1]
return adapted return adapted
return markdown return markdown
@@ -429,6 +450,10 @@ class Writer:
html = html.replace("<!-- CONTENT -->", html_content) html = html.replace("<!-- CONTENT -->", html_content)
# 防御:清理可能在 LLM 输出中混入的 markdown 代码围栏和文件头
html = re.sub(r'^```+\w*\s*\n?', '', html)
html = html.strip()
tags_html = self._get_platform_tags(platform) tags_html = self._get_platform_tags(platform)
if tags_html: if tags_html:
html = html.replace("<!-- TAGS -->", tags_html) html = html.replace("<!-- TAGS -->", tags_html)
+9 -8
View File
@@ -115,14 +115,15 @@ test("DELETE 删除源", r.status_code == 200)
# 7. LLM provider config # 7. LLM provider config
print("\n=== 7. LLM多供应商 ===") print("\n=== 7. LLM多供应商 ===")
sys.path.insert(0, str(root / "platform" / "backend")) sys.path.insert(0, str(root / "platform" / "backend"))
from app.core.nvidia_client import PROVIDERS, _ACTIVE_PROVIDER, _get_provider_config from app.core.nvidia_client import _get_active_provider, _get_provider_config
test("默认供应商", _ACTIVE_PROVIDER == "opencode-go") test("默认供应商", _get_active_provider() == "opencode-go")
test("opencode-go已配置", "opencode-go" in PROVIDERS) cfg = _get_provider_config("opencode-go")
test("nvidia备用存在", "nvidia" in PROVIDERS) test("opencode-go已配置", cfg is not None)
cfg = PROVIDERS["opencode-go"] cfg_nv = _get_provider_config("nvidia")
test("opencode-go模型", cfg["model"] == "deepseek-v4-flash") test("nvidia备用存在", cfg_nv is not None)
test("opencode-go URL非空", bool(cfg["base_url"])) test("opencode-go模型", cfg and cfg.get("model") == "deepseek-v4-flash")
test("opencode-go Key非空", bool(cfg["api_key"])) test("opencode-go URL非空", bool(cfg and cfg.get("base_url")))
test("opencode-go Key非空", bool(cfg and cfg.get("api_key")))
# 8. Collector DB loading # 8. Collector DB loading
print("\n=== 8. 采集器DB加载 ===") print("\n=== 8. 采集器DB加载 ===")