Files
yu-zhi-ran/scripts/outline.py
T
Yuzhiran Dev 9c37c9a574 feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列
测试: tests/test_phase_upgrades.py 97项全覆盖
CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式
修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
2026-05-17 06:56:53 +08:00

155 lines
5.2 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:
prompt = f"""你是一个资深内容编辑,擅长设计读者爱看+搜索引擎友好+平台愿意推荐的文章结构。
## 选题信息
标题:{title}
领域:{field}
核心观点:{core}
受众痛点:{pain}
独特视角:{angle}
## 研究笔记
{cases_summary}
## 大纲设计要求
### 结构
- 5-8章,每章2-4个要点
- 结构要有递进:要么认知升级型,要么问题解决型
- 把独特视角和受众痛点融入各章,不单独列
- 每章标题自带信息量,不要「引言」「总结」这类通用标题
### 数据要求
- **全文使用的数据必须为2025-2026年最新数据**,禁用2024年及之前过时数据
- 每个观点尽量配最新的数据或案例支撑
### SEO
- H2/H3标题自然包含用户搜索时会用的短语
- 确保大纲覆盖2-3个高价值搜索词
### 价值
- 每章回答读者「所以呢?」——为什么对他有用
- 避免空洞章节,尽量包含可操作内容(步骤/清单/方法)
### 平台预适配提示
- 知乎方向偏数据分析和深度逻辑
- 小红书方向偏实操步骤和清单
- 公众号方向偏故事和情感共鸣
- 同一大纲应能适应不同平台侧重点
直接输出大纲,不要输出思考过程。"""
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()