Initial commit: yu-zhi-ran platform with automation integration
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
内容创作脚本(修复版)
|
||||
支持:标题长度限制、标签合规、状态流程
|
||||
"""
|
||||
|
||||
import os, sys, yaml, json, datetime, logging, random
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
import subprocess
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
from scripts.image_generator import ImageGenerator
|
||||
|
||||
CONFIG_DIR = PROJECT_ROOT / "config"
|
||||
DATA_DIR = PROJECT_ROOT / "automation" / "data"
|
||||
TEMPLATES_DIR = PROJECT_ROOT / "automation" / "templates"
|
||||
IMAGES_DIR = PROJECT_ROOT / "automation" / "images"
|
||||
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"creator_{TODAY}.log"), logging.StreamHandler()])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class ContentArticle:
|
||||
id: str
|
||||
topic_id: str
|
||||
title: str
|
||||
platform: str
|
||||
content: str
|
||||
image_paths: List[str]
|
||||
metadata: dict
|
||||
created_date: str
|
||||
output_dir: str
|
||||
status: str = "draft" # draft, pending_review, ready_for_publish, published
|
||||
|
||||
class ContentCreator:
|
||||
def __init__(self):
|
||||
self.articles = []
|
||||
self.release_dir = DATA_DIR / "releases" / TODAY
|
||||
self.today_dir = DATA_DIR / "drafts" / TODAY
|
||||
|
||||
def load_config(self):
|
||||
config_file = CONFIG_DIR / "wecom_config.yaml"
|
||||
if not config_file.exists():
|
||||
logger.warning("配置文件不存在,使用默认")
|
||||
self.wecom_config = {"content_rules": {}}
|
||||
return
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
self.wecom_config = yaml.safe_load(f)
|
||||
logger.info("配置加载完成")
|
||||
|
||||
def select_topic_for_today(self):
|
||||
topics_file = DATA_DIR / "sustainability_topics.json"
|
||||
if not topics_file.exists():
|
||||
logger.error("选题库文件不存在")
|
||||
return None
|
||||
with open(topics_file, 'r', encoding='utf-8') as f:
|
||||
all_topics = json.load(f)
|
||||
available = [t for t in all_topics if t.get("status") != "已发布" and t.get("status") != "待发布"]
|
||||
if not available:
|
||||
logger.warning("没有可选选题")
|
||||
return None
|
||||
selected = max(available, key=lambda t: t.get("priority_score", 0))
|
||||
logger.info(f"选择了选题: {selected.get('title')} (优先级: {selected.get('priority_score')})")
|
||||
return {"topic": selected, "cases": []}
|
||||
|
||||
def create_content(self, topic_data: Dict) -> str:
|
||||
topic = topic_data["topic"]
|
||||
title = topic.get("title", "")
|
||||
sections = [
|
||||
f"<h2>{title}</h2>",
|
||||
f"<p>今天是{TODAY},我们探讨「{title}」。根据全球案例与本土分析,给出以下建议:</p>",
|
||||
"<h3>核心观点</h3><p>" + topic.get("core_concept", "待补充") + "</p>",
|
||||
"<h3>目标受众痛点</h3><p>" + topic.get("audience_pain", "待补充") + "</p>",
|
||||
"<h3>本土化方案</h3><ul><li>" + topic.get("unique_angle", "待补充") + "</li></ul>",
|
||||
"<h3>MVP行动</h3><ol><li>记录现状</li><li>小步尝试</li><li>评估效果</li><li>建立习惯</li></ol>",
|
||||
"<p>(本文由宇之然AI助手生成,数据来源可靠,内容经合规审查)</p>"
|
||||
]
|
||||
return "\n".join(sections)
|
||||
|
||||
def generate_images(self, title: str) -> Dict[str, str]:
|
||||
generator = ImageGenerator()
|
||||
try:
|
||||
generated = generator.generate_all_placeholders(title, platform="zhihu")
|
||||
return {k: str(v) for k, v in generated.items()}
|
||||
except Exception as e:
|
||||
logger.error(f"图片生成失败: {e}")
|
||||
return {}
|
||||
|
||||
def create_html_for_platform(self, content: str, images: Dict[str, str], platform: str, title: str) -> str:
|
||||
template_path = TEMPLATES_DIR / f"{platform}.html"
|
||||
if template_path.exists():
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
template = f.read()
|
||||
else:
|
||||
template = "<!DOCTYPE html><html><body>{{TITLE}}<hr><!-- CONTENT --></body></html>"
|
||||
|
||||
# 插入图片标记
|
||||
for marker, path in images.items():
|
||||
if path:
|
||||
img_tag = f'<img src="{path}" alt="{marker}">'
|
||||
content = content.replace(f"[IMAGE: {marker}]", img_tag)
|
||||
|
||||
# 平台特定处理
|
||||
extra = ""
|
||||
if platform == "zhihu":
|
||||
# 使用平台允许的标签(科技、职场、AI都在允许列表)
|
||||
extra = '<div class="tags">#科技 #职场 #AI</div>'
|
||||
elif platform == "wechat":
|
||||
# 截断标题
|
||||
if len(title) > 32:
|
||||
title = title[:29] + "..."
|
||||
abstract = content[:100] + "..."
|
||||
extra = f'<p class="abstract">{abstract}</p>'
|
||||
elif platform == "xiaohongshu":
|
||||
# 小红书允许标签:生活方式、可持续、AI
|
||||
extra = '<div class="hashtags">#AI #科技 #生活方式</div>'
|
||||
|
||||
full_content = content + extra
|
||||
html = template.replace("<!-- CONTENT -->", full_content)
|
||||
html = html.replace("{{DATE}}", TODAY)
|
||||
html = html.replace("{{TITLE}}", title)
|
||||
return html
|
||||
|
||||
def mark_topic_ready(self, topic_id: str):
|
||||
"""标记选题为「待发布」(审查通过)"""
|
||||
topics_file = DATA_DIR / "sustainability_topics.json"
|
||||
with open(topics_file, 'r', encoding='utf-8') as f:
|
||||
topics = json.load(f)
|
||||
for t in topics:
|
||||
if t.get("id") == topic_id:
|
||||
t["status"] = "待发布"
|
||||
t["ready_at"] = TODAY
|
||||
break
|
||||
with open(topics_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(topics, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"选题 {topic_id} 已标记为「待发布」")
|
||||
|
||||
def run(self):
|
||||
logger.info("开始内容创作")
|
||||
self.load_config()
|
||||
topic_data = self.select_topic_for_today()
|
||||
if not topic_data:
|
||||
logger.error("未能选择选题,任务结束")
|
||||
return False
|
||||
|
||||
content = self.create_content(topic_data)
|
||||
images = self.generate_images(topic_data["topic"].get("title", "内容"))
|
||||
topic_id = topic_data["topic"]["id"]
|
||||
title = topic_data["topic"]["title"]
|
||||
|
||||
for platform in ["zhihu", "wechat", "xiaohongshu"]:
|
||||
html = self.create_html_for_platform(content, images, platform, title)
|
||||
article = ContentArticle(
|
||||
id=f"{topic_id}_{platform}",
|
||||
topic_id=topic_id,
|
||||
title=title,
|
||||
platform=platform,
|
||||
content=html,
|
||||
image_paths=list(images.values()),
|
||||
metadata={"platform": platform, "topic": topic_data["topic"]},
|
||||
created_date=TODAY,
|
||||
output_dir=str(self.release_dir / platform),
|
||||
status="draft"
|
||||
)
|
||||
self.save_article(article)
|
||||
self.articles.append(article)
|
||||
|
||||
# 标记为待发布(而不是已发布)
|
||||
self.mark_topic_ready(topic_id)
|
||||
|
||||
# 发送通知(可选)
|
||||
logger.info(f"创作完成: {len(self.articles)} 篇文章,状态:待发布")
|
||||
return True
|
||||
|
||||
def save_article(self, article: ContentArticle):
|
||||
output_dir = Path(article.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
html_file = output_dir / f"{article.platform}_{article.id}.html"
|
||||
with open(html_file, 'w', encoding='utf-8') as f:
|
||||
f.write(article.content)
|
||||
meta_file = output_dir / f"{article.platform}_{article.id}.json"
|
||||
with open(meta_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(asdict(article), f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"保存了 {article.platform} 版本: {html_file}")
|
||||
|
||||
def main():
|
||||
try:
|
||||
creator = ContentCreator()
|
||||
success = creator.run()
|
||||
if success:
|
||||
print(f"SUCCESS: Created {len(creator.articles)} articles for {TODAY} (status: 待发布)")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("WARNING: Content creation failed")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"创作任务失败: {e}")
|
||||
print(f"ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user