From 0a8fbda4fd7486cb9b68850499bc34b6f799ecce Mon Sep 17 00:00:00 2001 From: Yuzhiran Dev Date: Wed, 20 May 2026 14:15:00 +0800 Subject: [PATCH] Fix calendar filter, module triggers, API cleanup, consolidate get_current_admin, fix LLM schema --- PROGRESS.md | 9 ++-- platform/backend/app/api/admin.py | 14 +----- platform/backend/app/api/audit.py | 19 ++------ platform/backend/app/api/cases.py | 13 +----- platform/backend/app/api/llm_configs.py | 13 +----- platform/backend/app/api/system.py | 12 ++++- platform/backend/app/api/system_configs.py | 13 +----- platform/backend/app/api/task_logs.py | 13 +----- platform/backend/app/api/topics.py | 29 +----------- platform/backend/app/core/collector.py | 7 ++- platform/backend/app/core/scheduler.py | 8 ++-- platform/backend/app/schemas.py | 8 ---- platform/frontend/index.html | 17 +++++-- platform/frontend/tasks.html | 1 - platform/frontend/topics.html | 2 +- scripts/collector.py | 4 +- scripts/compliance_optimizer.py | 4 ++ scripts/creator.py | 3 +- scripts/db_helper.py | 7 +++ scripts/research.py | 4 ++ scripts/topic_selector.py | 3 +- scripts/writer.py | 53 ++++++++++++++++------ tests/test_new_features.py | 17 +++---- 23 files changed, 118 insertions(+), 155 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index e379a2f..c684eac 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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 | 同步统计数据 | | 发布 | 手动点击 | 仅待发布状态可选 | --- diff --git a/platform/backend/app/api/admin.py b/platform/backend/app/api/admin.py index 64cb69e..79d4ae0 100644 --- a/platform/backend/app/api/admin.py +++ b/platform/backend/app/api/admin.py @@ -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, diff --git a/platform/backend/app/api/audit.py b/platform/backend/app/api/audit.py index ed96480..799eae2 100644 --- a/platform/backend/app/api/audit.py +++ b/platform/backend/app/api/audit.py @@ -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) ): """获取最近产生审计记录的用户列表""" diff --git a/platform/backend/app/api/cases.py b/platform/backend/app/api/cases.py index 49d4a9f..bd1f10f 100644 --- a/platform/backend/app/api/cases.py +++ b/platform/backend/app/api/cases.py @@ -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, diff --git a/platform/backend/app/api/llm_configs.py b/platform/backend/app/api/llm_configs.py index eca6c85..b6e42b3 100644 --- a/platform/backend/app/api/llm_configs.py +++ b/platform/backend/app/api/llm_configs.py @@ -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, diff --git a/platform/backend/app/api/system.py b/platform/backend/app/api/system.py index 14a509f..e4ad49c 100644 --- a/platform/backend/app/api/system.py +++ b/platform/backend/app/api/system.py @@ -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"}, } diff --git a/platform/backend/app/api/system_configs.py b/platform/backend/app/api/system_configs.py index a6e4f0b..2f72391 100644 --- a/platform/backend/app/api/system_configs.py +++ b/platform/backend/app/api/system_configs.py @@ -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, diff --git a/platform/backend/app/api/task_logs.py b/platform/backend/app/api/task_logs.py index aad6c2e..7dacd58 100644 --- a/platform/backend/app/api/task_logs.py +++ b/platform/backend/app/api/task_logs.py @@ -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, diff --git a/platform/backend/app/api/topics.py b/platform/backend/app/api/topics.py index da52cdb..50c4067 100644 --- a/platform/backend/app/api/topics.py +++ b/platform/backend/app/api/topics.py @@ -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() diff --git a/platform/backend/app/core/collector.py b/platform/backend/app/core/collector.py index 97c3e1d..5d80062 100644 --- a/platform/backend/app/core/collector.py +++ b/platform/backend/app/core/collector.py @@ -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, diff --git a/platform/backend/app/core/scheduler.py b/platform/backend/app/core/scheduler.py index 18f92b9..c9b9766 100644 --- a/platform/backend/app/core/scheduler.py +++ b/platform/backend/app/core/scheduler.py @@ -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: diff --git a/platform/backend/app/schemas.py b/platform/backend/app/schemas.py index 913fd93..d302fca 100644 --- a/platform/backend/app/schemas.py +++ b/platform/backend/app/schemas.py @@ -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 \ No newline at end of file diff --git a/platform/frontend/index.html b/platform/frontend/index.html index 9def6be..f73464a 100644 --- a/platform/frontend/index.html +++ b/platform/frontend/index.html @@ -106,8 +106,7 @@
今日任务{{ mod.task_count }} 个
成功率{{ mod.success_rate > 0 ? mod.success_rate + '%' : '暂无' }}
- 立即运行 - 立即运行 + 立即运行
@@ -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(); diff --git a/platform/frontend/tasks.html b/platform/frontend/tasks.html index 3e90be0..68449ba 100644 --- a/platform/frontend/tasks.html +++ b/platform/frontend/tasks.html @@ -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' }, diff --git a/platform/frontend/topics.html b/platform/frontend/topics.html index e9fbbc0..9974cdb 100644 --- a/platform/frontend/topics.html +++ b/platform/frontend/topics.html @@ -138,7 +138,7 @@ 确认发布 - +

{{ previewTopic.title }}

diff --git a/scripts/collector.py b/scripts/collector.py index a5e0169..82f1dc4 100644 --- a/scripts/collector.py +++ b/scripts/collector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -可持续性内容收集脚本 -每天凌晨5:00运行,收集全球可持续性趋势信息,提炼选题和案例 +内容采集:趋势抓取 → 选题生成 → 存入选题库 +收集热点趋势信息,经LLM分析后生成选题建议并存入数据库 """ import os diff --git a/scripts/compliance_optimizer.py b/scripts/compliance_optimizer.py index 23dfd05..fa2f17c 100644 --- a/scripts/compliance_optimizer.py +++ b/scripts/compliance_optimizer.py @@ -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 diff --git a/scripts/creator.py b/scripts/creator.py index 7dfb957..0b3a2ee 100755 --- a/scripts/creator.py +++ b/scripts/creator.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ -宇之然内容创作流水线(研究 → 大纲 → 撰写 → 合规优化)v3 - DB version +内容创作流水线:研究 → 大纲 → 撰写 → 合规审查 +基于选题ID,依次执行research/outline/writer各阶段,创作三平台文章并存入articles表 """ import json, datetime, logging, sys, subprocess diff --git a/scripts/db_helper.py b/scripts/db_helper.py index 7890b4f..13dd5b0 100644 --- a/scripts/db_helper.py +++ b/scripts/db_helper.py @@ -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)) diff --git a/scripts/research.py b/scripts/research.py index b404423..fda27b7 100644 --- a/scripts/research.py +++ b/scripts/research.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +""" +选题研究:信息收集与结构化整理 +基于选题方向,收集相关数据/案例/趋势,输出结构化研究笔记供大纲生成使用 +""" import json, datetime, logging, sys, re from pathlib import Path from typing import Dict, List diff --git a/scripts/topic_selector.py b/scripts/topic_selector.py index 7164b4a..8f5683a 100644 --- a/scripts/topic_selector.py +++ b/scripts/topic_selector.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ -选题引擎 v2:多趋势加权匹配 + 趋势缺口检测 + 新选题生成 +选题引擎:多趋势加权匹配 + 趋势缺口检测 + 新选题生成 +结合采集到的热点趋势和当前选题库,推荐最优选题并检测趋势缺口 """ import sys, json, logging diff --git a/scripts/writer.py b/scripts/writer.py index ebad0b3..ffa3229 100644 --- a/scripts/writer.py +++ b/scripts/writer.py @@ -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("", 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_html) diff --git a/tests/test_new_features.py b/tests/test_new_features.py index f430d18..4fab3b8 100644 --- a/tests/test_new_features.py +++ b/tests/test_new_features.py @@ -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加载 ===")