52564ad036
topics.html:
- 今日新增筛选时全量拉取 allTopics 用于保持 stats 统计正确,todayTopics
存储服务端日期过滤结果
- filteredTopics 对 'today' 返回 todayTopics,不按不存在的 status 过滤
writer.py/research.py/outline.py:
- 所有 LLM 提示词注入今天的动态年月日(datetime.now)
- 禁用过时数据的年份改为动态 {当前年-1}-{当前年}
- 每个论点必须配真实最新案例+数据来源
writer.py WeChat 配图:
- 从 null placeholder 改为动态 SVG,展示选题标题+领域
- 标题自动换行(24字/行),附蓝色装饰条 + 渐变背景
167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
大纲阶段:基于选题和研究笔记,用 LLM 动态生成结构化大纲
|
|
"""
|
|
|
|
import json, datetime, logging, sys
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
sys.path.insert(0, str(PROJECT_ROOT / "platform" / "backend"))
|
|
|
|
from db_helper import get_topic_by_id
|
|
try:
|
|
from app.core.nvidia_client import call_llm
|
|
HAVE_LLM = True
|
|
except ImportError:
|
|
HAVE_LLM = False
|
|
|
|
DATA_DIR = PROJECT_ROOT / "automation" / "data"
|
|
RESEARCH_DIR = DATA_DIR / "research"
|
|
OUTPUT_DIR = DATA_DIR / "outlines"
|
|
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
|
|
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s',
|
|
handlers=[logging.FileHandler(LOGS_DIR / f"outline_{TODAY}.log"), logging.StreamHandler()])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Outliner:
|
|
def __init__(self, topic_id: str):
|
|
self.topic_id = topic_id
|
|
self.topic = self._load_topic()
|
|
research_file = RESEARCH_DIR / TODAY / f"{topic_id}_research.md"
|
|
self.research_notes = research_file.read_text(encoding='utf-8') if research_file.exists() else ""
|
|
self.output_dir = OUTPUT_DIR / TODAY
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _load_topic(self) -> Dict:
|
|
topic = get_topic_by_id(self.topic_id)
|
|
if not topic: raise ValueError(f"Topic {self.topic_id} not found")
|
|
return topic
|
|
|
|
def generate_outline(self) -> str:
|
|
title = self.topic['title']
|
|
field = self.topic.get('field', '')
|
|
core = self.topic.get('core_concept', '')
|
|
pain = self.topic.get('audience_pain', '')
|
|
angle = self.topic.get('unique_angle', '')
|
|
cases_summary = self.research_notes[:2000] if self.research_notes else "暂无研究笔记"
|
|
|
|
if HAVE_LLM:
|
|
_now = datetime.datetime.now()
|
|
prompt = f"""你是一个资深内容编辑,擅长设计读者爱看+搜索引擎友好+平台愿意推荐+有市场传播力的文章结构。
|
|
|
|
⚠️ 今天日期:{_now.strftime('%Y年%m月%d日')}。当前年份:{_now.year}年。
|
|
|
|
## 选题信息
|
|
标题:{title}
|
|
领域:{field}
|
|
核心观点:{core}
|
|
受众痛点:{pain}
|
|
独特视角:{angle}
|
|
|
|
## 研究笔记
|
|
{cases_summary}
|
|
|
|
## 大纲设计要求
|
|
### 结构
|
|
- 5-8章,每章2-4个要点
|
|
- 结构要有递进:要么认知升级型,要么问题解决型
|
|
- 把独特视角和受众痛点融入各章,不单独列
|
|
- 每章标题自带信息量+好奇心,不要「引言」「总结」这类通用标题
|
|
- 开头要有"钩子"(hook)抓住读者,结尾要有可转发/收藏的总结
|
|
|
|
### 数据与热点
|
|
- **全文必须使用{_now.year-1}-{_now.year}年最新数据**,禁用一切过时数据
|
|
- 每个观点必须配最新的国内外热点事件/数据/政策来佐证
|
|
- 体现当前行业正在讨论的核心议题,拒绝泛泛而谈
|
|
|
|
### 独特风格
|
|
- 有自己的判断和立场,不是搬运观点
|
|
- 每章至少一个"反常识"或"很少有人提"的洞察
|
|
- 避免同质化表达、老生常谈
|
|
|
|
### SEO
|
|
- H2/H3自然包含用户搜索时会用的短语
|
|
- 确保大纲覆盖2-3个高价值搜索词,包含1个长尾词
|
|
- 每章标题对用户搜索意图有回应
|
|
|
|
### 市场价值
|
|
- 读完每章读者能拿走一个实际有用的东西(方法/清单/思维框架/判断标准)
|
|
- 避免「信息增量为零」的空洞章节
|
|
- 思考:这篇文章对读者职业/生活/认知有什么用?
|
|
|
|
### 平台推荐优化
|
|
- 知乎:偏硬核数据分析和深度逻辑,结构要有论证链
|
|
- 小红书:偏实操步骤/清单/对比,结构要一目了然
|
|
- 公众号:偏故事化开头+情感共鸣+金句结尾,段落节奏快
|
|
- 一个框架适应三平台,各平台可裁剪侧重点
|
|
|
|
直接输出大纲,不要输出思考过程。"""
|
|
try:
|
|
outline = call_llm(prompt, temperature=0.6, max_tokens=2000, system_prompt="你是一个有经验的内容编辑,擅长为不同选题设计差异化的文章结构。")
|
|
logger.info(f"LLM 大纲生成成功,长度:{len(outline)}")
|
|
return f"# 文章大纲:{title}\n\n{outline}\n\n---\n*大纲生成时间:{TODAY}*"
|
|
except Exception as e:
|
|
logger.warning(f"LLM 大纲生成失败: {e},使用模板")
|
|
|
|
return self._template_outline(title, field, core, pain, angle)
|
|
|
|
def _template_outline(self, title, field, core, pain, angle) -> str:
|
|
case_count = self.research_notes.count('### 案例') if self.research_notes else 0
|
|
case_section = f"""## 四、全球/行业趋势与案例
|
|
- 引用研究笔记中的 {case_count} 个案例,精选 2-3 个详述
|
|
- 数据支撑:提取研究笔记中的关键数据
|
|
- 趋势分析""" if case_count > 0 else ""
|
|
return f"""# 文章大纲:{title}
|
|
|
|
## 一、引言
|
|
- 场景切入:{title}
|
|
- 点明文章价值
|
|
|
|
## 二、核心观点
|
|
{core}
|
|
|
|
## 三、受众痛点分析
|
|
{pain}
|
|
{case_section}
|
|
|
|
## {"五" if case_section else "四"}、本土落地建议
|
|
- 结合{field}领域特点
|
|
- 提供可执行的步骤
|
|
- 注意事项
|
|
|
|
## {"六" if case_section else "五"}、独特视角:{angle}
|
|
|
|
## {"七" if case_section else "六"}、行动指南
|
|
1. 了解现状 2. 制定方案 3. 小范围验证 4. 持续优化
|
|
|
|
## {"八" if case_section else "七"}、总结与鼓励
|
|
|
|
---
|
|
|
|
*大纲生成时间:{TODAY}*"""
|
|
|
|
def save(self):
|
|
outline_text = self.generate_outline()
|
|
out_path = self.output_dir / f"{self.topic_id}_outline.md"
|
|
out_path.write_text(outline_text, encoding='utf-8')
|
|
logger.info(f"大纲已保存: {out_path}")
|
|
return out_path
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--topic-id', required=True)
|
|
args = parser.parse_args()
|
|
o = Outliner(args.topic_id)
|
|
o.save()
|
|
print(f"SUCCESS: Outline created for {args.topic_id}")
|
|
sys.exit(0)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|