Fix calendar filter, module triggers, API cleanup, consolidate get_current_admin, fix LLM schema
This commit is contained in:
+5
-4
@@ -86,10 +86,11 @@
|
||||
|
||||
| 步骤 | 触发方式 | 说明 |
|
||||
|------|---------|------|
|
||||
| 采集 | 定时 01:30 | 热点趋势采集→生成选题建议→存入选题库 |
|
||||
| 同步 | 定时 02:30 | DB → JSON 备份同步 |
|
||||
| 创作 | 手动点击 / 定时 03:30 | 生成三平台文章 → 存入 articles 表 → 状态→待审查 |
|
||||
| 审查 | 手动点击 / 创作后自动 | 从 articles 表读取 → 合规检查 → LLM迭代修复(最多3次)→合规分回写→状态→待发布 |
|
||||
| 内容采集 | 定时 01:30 | 热点趋势采集→生成选题建议→存入选题库 |
|
||||
| 内容创作 | 手动点击 / 定时 03:30 | 研究→大纲→撰写文章→合规审查→存入 articles 表 |
|
||||
| 合规审查 | 手动点击 / 定时 04:30 | 从 articles 表读取 draft→合规检查→LLM迭代修复(最多3次)→状态→待发布 |
|
||||
| 信息源优化 | 定时 05:00 | AI评估采集类别与信息源配置,给出调整建议 |
|
||||
| 指标同步 | 定时 06:00 | 同步统计数据 |
|
||||
| 发布 | 手动点击 | 仅待发布状态可选 |
|
||||
|
||||
---
|
||||
|
||||
@@ -9,23 +9,11 @@ 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
|
||||
from .auth import get_current_admin, org_filter
|
||||
import json
|
||||
|
||||
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])
|
||||
def list_users(
|
||||
request: Request,
|
||||
|
||||
@@ -4,28 +4,17 @@ from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import AuditLog
|
||||
from ..models import AuditLog, User
|
||||
from ..schemas import AuditLogResponse
|
||||
from .auth import verify_token
|
||||
from .auth import get_current_admin
|
||||
|
||||
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])
|
||||
def list_audit_logs(
|
||||
request: Request,
|
||||
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="按用户名筛选"),
|
||||
action: Optional[str] = Query(None, description="按操作类型筛选"),
|
||||
resource_type: Optional[str] = Query(None, description="按资源类型筛选"),
|
||||
@@ -64,7 +53,7 @@ def list_audit_actions(
|
||||
def list_audit_users(
|
||||
request: Request,
|
||||
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)
|
||||
):
|
||||
"""获取最近产生审计记录的用户列表"""
|
||||
|
||||
@@ -5,21 +5,10 @@ from typing import List
|
||||
from ..database import get_db
|
||||
from ..models import Case
|
||||
from ..schemas import CaseBase, CaseResponse
|
||||
from .auth import get_current_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])
|
||||
def list_cases(
|
||||
request: Request,
|
||||
|
||||
@@ -5,21 +5,10 @@ from typing import List
|
||||
from ..database import get_db
|
||||
from ..models import LLMConfig
|
||||
from ..schemas import LLMConfigBase, LLMConfigResponse
|
||||
from .auth import get_current_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])
|
||||
def list_llm_configs(
|
||||
request: Request,
|
||||
|
||||
@@ -11,6 +11,7 @@ from ..database import get_db
|
||||
from ..models import Topic, Article
|
||||
from ..core.generator import run_creator
|
||||
from ..core.optimizer import run_optimizer
|
||||
from ..core.collector import run_collector
|
||||
from ..core.sync import sync_all_topics
|
||||
from ..core.scheduler import scheduler
|
||||
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:
|
||||
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)])
|
||||
def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
try:
|
||||
@@ -212,7 +222,7 @@ def get_modules_status():
|
||||
log_based: dict = {
|
||||
"scheduled_collect": {"name": "📡 内容采集", "log": LOGS_DIR / f"collector_{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_metrics_sync": {"name": "📊 指标同步", "log": LOGS_DIR / f"metrics_sync_{today_str}.log"},
|
||||
}
|
||||
|
||||
@@ -5,21 +5,10 @@ from typing import List, Dict, Any
|
||||
from ..database import get_db
|
||||
from ..models import SystemConfig
|
||||
from ..schemas import SystemConfigBase, SystemConfigResponse
|
||||
from .auth import get_current_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])
|
||||
def list_system_configs(
|
||||
request: Request,
|
||||
|
||||
@@ -5,21 +5,10 @@ from typing import List, Optional
|
||||
from ..database import get_db
|
||||
from ..models import TaskLog
|
||||
from ..schemas import TaskLogBase, TaskLogResponse
|
||||
from .auth import get_current_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])
|
||||
def list_task_logs(
|
||||
request: Request,
|
||||
|
||||
@@ -6,10 +6,9 @@ from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
|
||||
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 (
|
||||
TopicCreate, TopicUpdate, TopicResponse, TopicScoreRequest,
|
||||
PublishRequest, PublishActionRequest, PublishRecordResponse
|
||||
)
|
||||
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}
|
||||
|
||||
|
||||
@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]])
|
||||
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()
|
||||
|
||||
@@ -19,8 +19,13 @@ def run_collector():
|
||||
script_path = PROJECT_ROOT / "scripts" / "collector.py"
|
||||
if not script_path.exists():
|
||||
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(
|
||||
["python", str(script_path)],
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=PROJECT_ROOT,
|
||||
|
||||
@@ -62,7 +62,7 @@ class TaskScheduler:
|
||||
)
|
||||
self.scheduler.start()
|
||||
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):
|
||||
if self.scheduler.running:
|
||||
self.scheduler.shutdown()
|
||||
@@ -86,11 +86,11 @@ class TaskScheduler:
|
||||
|
||||
def _run_optimize(self):
|
||||
try:
|
||||
logger.info("[Scheduled] Starting compliance optimization...")
|
||||
logger.info("[Scheduled] Starting compliance review...")
|
||||
result = run_optimizer()
|
||||
logger.info("[Scheduled] Optimization completed: %s", result)
|
||||
logger.info("[Scheduled] Review completed: %s", result)
|
||||
except Exception as e:
|
||||
logger.exception("[Scheduled] Optimization failed: %s", e)
|
||||
logger.exception("[Scheduled] Review failed: %s", e)
|
||||
|
||||
def _run_collect(self):
|
||||
try:
|
||||
|
||||
@@ -490,8 +490,6 @@ class LLMConfigResponse(LLMConfigBase):
|
||||
id: int
|
||||
created_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)
|
||||
|
||||
@@ -519,9 +517,3 @@ class SystemStatus(BaseModel):
|
||||
execution_time: Optional[float] = None
|
||||
|
||||
|
||||
class OptimizationRequest(BaseModel):
|
||||
topic_ids: Optional[List[str]] = None
|
||||
|
||||
|
||||
class BatchPublishRequest(BaseModel):
|
||||
date: str
|
||||
@@ -106,8 +106,7 @@
|
||||
<div><span>今日任务</span><span>{{ mod.task_count }} 个</span></div>
|
||||
<div><span>成功率</span><span>{{ mod.success_rate > 0 ? mod.success_rate + '%' : '暂无' }}</span></div>
|
||||
<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 v-if="mod.id === 'scheduled_metrics_sync'" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,8 +270,8 @@
|
||||
const end = new Date(today);
|
||||
end.setDate(end.getDate() + 7);
|
||||
this.upcomingEntries = (all || [])
|
||||
.filter(e => { const d = new Date(e.date); return d >= today && d < end; })
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
.filter(e => { const d = new Date(e.planned_date); return d >= today && d < end; })
|
||||
.sort((a, b) => a.planned_date.localeCompare(b.planned_date));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取近期计划失败:', e);
|
||||
@@ -284,9 +283,17 @@
|
||||
},
|
||||
async triggerModule(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 {
|
||||
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 } });
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -174,7 +174,6 @@ const TasksApp = {
|
||||
data() {
|
||||
const SCHEDULER_JOBS = {
|
||||
'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_optimize': { icon: 'IconSearch', name: '合规审查', defaultTime: '04:30' },
|
||||
'scheduled_optimize_sources': { icon: 'IconSetting', name: '信息源优化', defaultTime: '05:00' },
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<el-button type="primary" @click="confirmPublish" :loading="publishing" :disabled="!publishPlatforms.zhihu && !publishPlatforms.wechat && !publishPlatforms.xiaohongshu">确认发布</el-button>
|
||||
</template>
|
||||
</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 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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
可持续性内容收集脚本
|
||||
每天凌晨5:00运行,收集全球可持续性趋势信息,提炼选题和案例
|
||||
内容采集:趋势抓取 → 选题生成 → 存入选题库
|
||||
收集热点趋势信息,经LLM分析后生成选题建议并存入数据库
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
合规审查:文章合规检查 → LLM迭代修复
|
||||
从articles表读取待审文章,进行合规评分;不合格文章由LLM修复(最多3次),通过后更新选题状态为待发布
|
||||
"""
|
||||
import json, datetime, logging, sys, re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
宇之然内容创作流水线(研究 → 大纲 → 撰写 → 合规优化)v3 - DB version
|
||||
内容创作流水线:研究 → 大纲 → 撰写 → 合规审查
|
||||
基于选题ID,依次执行research/outline/writer各阶段,创作三平台文章并存入articles表
|
||||
"""
|
||||
|
||||
import json, datetime, logging, sys, subprocess
|
||||
|
||||
@@ -4,10 +4,17 @@
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime, date
|
||||
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 路径
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
选题研究:信息收集与结构化整理
|
||||
基于选题方向,收集相关数据/案例/趋势,输出结构化研究笔记供大纲生成使用
|
||||
"""
|
||||
import json, datetime, logging, sys, re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
选题引擎 v2:多趋势加权匹配 + 趋势缺口检测 + 新选题生成
|
||||
选题引擎:多趋势加权匹配 + 趋势缺口检测 + 新选题生成
|
||||
结合采集到的热点趋势和当前选题库,推荐最优选题并检测趋势缺口
|
||||
"""
|
||||
|
||||
import sys, json, logging
|
||||
|
||||
+39
-14
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
平台适配文章撰写
|
||||
根据大纲和平台配置(字数/格式/配图要求),为知乎/公众号/小红书各平台生成适配内容
|
||||
"""
|
||||
import json, datetime, logging, sys, re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
@@ -92,15 +96,17 @@ class Writer:
|
||||
if line.startswith("# "):
|
||||
if 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("## "):
|
||||
if 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("### "):
|
||||
if current:
|
||||
sections.append(current)
|
||||
current = {"level": 3, "title": line[4:].strip(), "content": ""}
|
||||
current = {"level": 3, "title": line[4:].strip(), "content": "", "section_type": "normal"}
|
||||
else:
|
||||
if current and line.strip():
|
||||
current['content'] = current.get('content', '') + line + "\n"
|
||||
@@ -112,7 +118,13 @@ class Writer:
|
||||
def _clean_markdown(text: str) -> str:
|
||||
lines = text.split('\n')
|
||||
cleaned = []
|
||||
in_code_fence = False
|
||||
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'^[\-\*\+]\s+', '', line)
|
||||
line = re.sub(r'^\d+[\.\)]\s+', '', line)
|
||||
@@ -133,12 +145,19 @@ class Writer:
|
||||
return True
|
||||
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:
|
||||
content = section.get('content', '').strip()
|
||||
if len(content) > 200:
|
||||
return content
|
||||
if HAVE_LLM and len(content) < 150:
|
||||
logger.info(f"使用 LLM 扩写章节: {section['title']}")
|
||||
# 大纲要点格式(>40% 行以 -/*/** 开头)应始终由 LLM 展开为连贯段落
|
||||
if HAVE_LLM and self._is_bullet_only(content):
|
||||
logger.info(f"使用 LLM 扩写章节(要点→段落): {section['title']}")
|
||||
prompt = f"""你是一个资深作者,正在写一篇关于「{self.topic['title']}」的文章。请写「{section['title']}」这一节。
|
||||
|
||||
今天日期:{datetime.datetime.now().strftime('%Y年%m月%d日')}。
|
||||
@@ -147,7 +166,7 @@ class Writer:
|
||||
{content}
|
||||
|
||||
【输出要求】
|
||||
输出3-5段纯粹、流畅的段落文字,共400-800字。
|
||||
输出3-6段纯粹、流畅的段落文字,每节内容根据平台需求控制在200-800字之间。
|
||||
|
||||
格式:
|
||||
- 禁止任何标题/列表/格式标记(#、-、*、1.、**等)
|
||||
@@ -187,7 +206,8 @@ class Writer:
|
||||
text += '。'
|
||||
sentences.append(text)
|
||||
if sentences:
|
||||
return ' '.join(sentences)
|
||||
result = ' '.join(sentences)
|
||||
return self._clean_markdown(result)
|
||||
return ''
|
||||
|
||||
def generate_full_markdown(self) -> str:
|
||||
@@ -200,6 +220,12 @@ class Writer:
|
||||
if expanded:
|
||||
parts.append(expanded + "\n")
|
||||
continue
|
||||
# 跳过大纲结构噪音节点
|
||||
title_stripped = sec['title'].strip()
|
||||
if title_stripped in ('文章大纲', '大纲', '文章结构', '结构'):
|
||||
continue
|
||||
if sec.get('section_type') == 'noise':
|
||||
continue
|
||||
heading = f"{'#' * sec['level']} {sec['title']}"
|
||||
parts.append(heading)
|
||||
if sec.get('content'):
|
||||
@@ -246,11 +272,6 @@ class Writer:
|
||||
else:
|
||||
result.append(line)
|
||||
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 markdown
|
||||
@@ -429,6 +450,10 @@ class Writer:
|
||||
|
||||
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)
|
||||
if tags_html:
|
||||
html = html.replace("<!-- TAGS -->", tags_html)
|
||||
|
||||
@@ -115,14 +115,15 @@ test("DELETE 删除源", r.status_code == 200)
|
||||
# 7. LLM provider config
|
||||
print("\n=== 7. LLM多供应商 ===")
|
||||
sys.path.insert(0, str(root / "platform" / "backend"))
|
||||
from app.core.nvidia_client import PROVIDERS, _ACTIVE_PROVIDER, _get_provider_config
|
||||
test("默认供应商", _ACTIVE_PROVIDER == "opencode-go")
|
||||
test("opencode-go已配置", "opencode-go" in PROVIDERS)
|
||||
test("nvidia备用存在", "nvidia" in PROVIDERS)
|
||||
cfg = PROVIDERS["opencode-go"]
|
||||
test("opencode-go模型", cfg["model"] == "deepseek-v4-flash")
|
||||
test("opencode-go URL非空", bool(cfg["base_url"]))
|
||||
test("opencode-go Key非空", bool(cfg["api_key"]))
|
||||
from app.core.nvidia_client import _get_active_provider, _get_provider_config
|
||||
test("默认供应商", _get_active_provider() == "opencode-go")
|
||||
cfg = _get_provider_config("opencode-go")
|
||||
test("opencode-go已配置", cfg is not None)
|
||||
cfg_nv = _get_provider_config("nvidia")
|
||||
test("nvidia备用存在", cfg_nv is not None)
|
||||
test("opencode-go模型", cfg and cfg.get("model") == "deepseek-v4-flash")
|
||||
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
|
||||
print("\n=== 8. 采集器DB加载 ===")
|
||||
|
||||
Reference in New Issue
Block a user