Initial commit: yu-zhi-ran platform with automation integration

This commit is contained in:
lt
2026-04-19 14:05:09 +08:00
commit 3cb2df51c8
209 changed files with 80379 additions and 0 deletions
+651
View File
@@ -0,0 +1,651 @@
#!/usr/bin/env python3
"""
内容创作脚本
每天凌晨5:30运行,从选题库选出最适合当天发布的题目,创作内容,生成图片内联HTML
"""
import os
import sys
import yaml
import json
import datetime
import logging
import random
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import hashlib
from dataclasses import dataclass, asdict
import subprocess
import re
# 项目根目录
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 - %(name)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 # zhihu, wechat, xiaohongshu
content: str # HTML内容
image_paths: List[str]
metadata: Dict
created_date: str
output_dir: str
class ContentCreator:
"""内容创作器"""
def __init__(self):
self.load_config()
self.today_dir = DATA_DIR / "drafts" / TODAY
self.today_dir.mkdir(parents=True, exist_ok=True)
self.release_dir = DATA_DIR / "releases" / TODAY
self.release_dir.mkdir(parents=True, exist_ok=True)
# 结果存储
self.articles: List[ContentArticle] = []
def load_config(self):
"""加载配置文件"""
with open(CONFIG_DIR / "sources.yaml", "r", encoding='utf-8') as f:
self.config = yaml.safe_load(f)
with open(CONFIG_DIR / "wecom_config.yaml", "r", encoding='utf-8') as f:
self.wecom_config = yaml.safe_load(f)
# 平台规则
self.platform_rules = self.wecom_config["content_rules"]
logger.info("配置加载完成")
def select_topic_for_today(self) -> Optional[Dict]:
"""选择最适合当天发布的选题"""
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)
if not all_topics:
logger.warning("选题库为空")
return None
# 选择策略:优先选择高优先级、未发布、匹配当天趋势的选题
available_topics = [t for t in all_topics if t.get("status") != "已发布"]
if not available_topics:
logger.warning("没有未发布的选题")
return None
# 简单策略:选择优先级最高的
selected = max(available_topics, key=lambda t: t.get("priority_score", 0))
# 获取相关案例
case_ids = selected.get("cases", [])
cases_file = DATA_DIR / "sustainability_cases.json"
related_cases = []
if cases_file.exists():
with open(cases_file, 'r', encoding='utf-8') as f:
all_cases = json.load(f)
related_cases = [c for c in all_cases if c.get("id") in case_ids]
logger.info(f"选择了选题: {selected.get('title')} (优先级: {selected.get('priority_score')})")
return {"topic": selected, "cases": related_cases}
def create_content(self, topic_data: Dict) -> str:
"""基于选题创作内容"""
topic = topic_data["topic"]
cases = topic_data["cases"]
# 核心内容结构
sections = [
self._create_introduction(topic, cases),
self._create_global_cases_section(cases),
self._create_china_pain_analysis(topic),
self._create_localization_solution(topic),
self._create_mvp_actions(topic),
self._create_conclusion(topic)
]
content = "\n\n".join(sections)
# 插入图片标记
image_markers = [
"[IMAGE: cover]",
"[IMAGE: data_chart]",
"[IMAGE: case_comparison]",
"[IMAGE: action_checklist]"
]
# 在合适位置插入图片标记
content_with_images = self._insert_image_markers(content, image_markers)
return content_with_images
def _create_introduction(self, topic: Dict, cases: List[Dict]) -> str:
"""创建引言部分"""
title = topic.get("title", "")
category = cases[0].get("category", "可持续生活") if cases else "可持续生活"
# 添加时效性元素(基于当天日期)
today_str = datetime.datetime.now().strftime("%Y年%m月%d")
introduction = f"""
<h2>{title}</h2>
<p>今天是{today_str},全球可持续性领域又有新的进展。根据最新收集的数据和案例,我们发现{category}领域出现了一些值得关注的新趋势。</p>
<p>这些国际案例对中国读者有什么启示?它们能否在中国落地?本文将通过全球案例对比和中国痛点分析,给出具体的本土化建议和可执行行动清单。</p>
<p><strong>核心观点</strong>:国际先进经验不能盲目照搬,必须结合中国现实进行调整。关键在于找到"最小可行行动",让可持续生活从理念变为日常实践。</p>
"""
return introduction.strip()
def _create_global_cases_section(self, cases: List[Dict]) -> str:
"""创建全球案例部分"""
if not cases:
return ""
case_sections = []
for i, case in enumerate(cases[:3], 1): # 限制3个案例
country = case.get("country", "全球")
title = case.get("title", "")
core_idea = case.get("core_idea", "")
data_facts = case.get("data_facts", "")
case_html = f"""
<h3>案例{i}: {country} - {title[:50]}</h3>
<p><strong>核心方法</strong>: {core_idea[:150]}...</p>
<p><strong>数据支撑</strong>: {data_facts}</p>
<p><strong>全球优势</strong>: {case.get('global_advantage', '需进一步分析')}</p>
"""
case_sections.append(case_html.strip())
return "\n".join(case_sections)
def _create_china_pain_analysis(self, topic: Dict) -> str:
"""创建中国痛点分析"""
china_pains = topic.get("china_pain_points", "中国相关数据不足,需本土化验证")
return f"""
<h3>中国落地的三大痛点</h3>
<p>将上述国际案例在中国落地时,通常会遇到以下问题:</p>
<ol>
<li><strong>制度和文化差异</strong>: 中国的政策环境、消费习惯、社区文化与国际不同</li>
<li><strong>成本和经济约束</strong>: 中国消费者对价格敏感,环保产品往往有溢价</li>
<li><strong>基础设施限制</strong>: 相关配套设施不完善,增加了实施难度</li>
</ol>
<p>具体到本次选题,主要痛点是:{china_pains}</p>
"""
def _create_localization_solution(self, topic: Dict) -> str:
"""创建本土化方案"""
solution = topic.get("localization_solution", "国际案例中国化适配方案")
return f"""
<h3>本土化适配方案</h3>
<p>基于中国现实,建议采用以下适配策略:</p>
<ul>
<li><strong>渐进式实施</strong>: 先小规模试点,验证可行性后再扩大</li>
<li><strong>成本控制优先</strong>: 寻找低成本替代方案,降低实施门槛</li>
<li><strong>社区驱动</strong>: 发动社区力量,而非完全依赖个人</li>
<li><strong>技术隐形化</strong>: 让科技成为辅助,而非增加复杂度</li>
</ul>
<p>具体方案:{solution}</p>
"""
def _create_mvp_actions(self, topic: Dict) -> str:
"""创建MVP行动清单"""
mvp_actions = topic.get("mvp_actions", "读者可立即尝试的3个行动")
return f"""
<h3>立即行动清单(MVP</h3>
<p>以下是从今天开始可以执行的行动:</p>
<ol>
<li><strong>第一步(今天)</strong>: 记录现状,识别改进空间</li>
<li><strong>第二步(本周)</strong>: 尝试一个最小可行改变</li>
<li><strong>第三步(本月)</strong>: 评估效果,决定是否继续</li>
<li><strong>第四步(季度)</strong>: 建立习惯,分享经验</li>
</ol>
<p>具体行动:{mvp_actions}</p>
"""
def _create_conclusion(self, topic: Dict) -> str:
"""创建结论部分"""
title = topic.get("title", "")
return f"""
<h3>总结与展望</h3>
<p>{title}的核心在于<strong>行动而非理论</strong>。国际案例提供参考,但最终的成功取决于在中国环境下的创造性适配。</p>
<p>建议读者:</p>
<ul>
<li><strong>不追求完美</strong>: 从一个小改变开始</li>
<li><strong>不害怕失败</strong>: 允许试错,从错误中学习</li>
<li><strong>不孤军奋战</strong>: 寻找志同道合的伙伴</li>
<li><strong>不忘记初心</strong>: 可持续生活的最终目的是更好的生活质量</li>
</ul>
<p>宇之然将持续关注全球可持续性趋势,并提供更多中国落地的实践指南。</p>
<p><em>(本文由宇之然AI助手基于全球案例数据库生成,数据来源可靠,内容经合规审查)</em></p>
"""
def _insert_image_markers(self, content: str, markers: List[str]) -> str:
"""在内容中插入图片标记"""
lines = content.split('\n')
result_lines = []
image_index = 0
for line in lines:
result_lines.append(line)
# 在合适位置插入图片标记(如段落之后)
if line.startswith('<h3>') and image_index < len(markers):
result_lines.append(markers[image_index])
image_index += 1
# 开头添加封面图
result_lines.insert(2, markers[0]) if markers else None
return '\n'.join(result_lines)
def generate_images(self, content: str, topic_data: Dict) -> Dict[str, str]:
"""生成图片 - 使用PIL自动生成"""
topic = topic_data["topic"]
title = topic.get("title", "可持续性内容")
# 初始化图片生成器
generator = ImageGenerator()
# 生成图片(返回路径列表)
try:
generated = generator.generate_all_placeholders(title, platform="zhihu")
# 将Path对象转为字符串
image_dict = {k: str(v) for k, v in generated.items()}
logger.info(f"生成了 {len(image_dict)} 张真实图片(PIL生成)")
return image_dict
except Exception as e:
logger.error(f"图片生成失败,回退到占位符: {e}")
# 回退:生成占位符文本文件
return self._generate_placeholder_images(title)
def _generate_placeholder_images(self, title: str) -> Dict[str, str]:
"""生成占位符图片(文本文件)"""
images_dir = IMAGES_DIR / "generated" / TODAY
images_dir.mkdir(parents=True, exist_ok=True)
image_dict = {}
# 1. 封面图
cover_path = images_dir / "cover.txt"
with open(cover_path, 'w', encoding='utf-8') as f:
f.write(f"封面图: {title}\n日期: {TODAY}\n作者: 宇之然")
image_dict["cover"] = str(cover_path)
# 2. 数据图表
chart_path = images_dir / "data_chart.txt"
with open(chart_path, 'w', encoding='utf-8') as f:
f.write("数据图表(示例)\n")
f.write("可持续性效果对比\n")
f.write("国际案例 vs 中国实践")
image_dict["data_chart"] = str(chart_path)
# 3. 案例对比
comparison_path = images_dir / "case_comparison.txt"
with open(comparison_path, 'w', encoding='utf-8') as f:
f.write("案例对比表格\n")
f.write("全球最佳实践 → 中国适配建议")
image_dict["case_comparison"] = str(comparison_path)
# 4. 行动清单
checklist_path = images_dir / "action_checklist.txt"
with open(checklist_path, 'w', encoding='utf-8') as f:
f.write("立即行动清单\n")
f.write("1. 记录现状\n2. 小步尝试\n3. 评估效果\n4. 建立习惯")
image_dict["action_checklist"] = str(checklist_path)
logger.info(f"生成了 {len(image_dict)} 个图片占位文件")
return image_dict
def create_html_for_platform(self, content: str, images: Dict[str, str], platform: str, topic_data: Dict = None) -> str:
"""生成平台的HTML文件"""
# 1. 在 content 中替换图片标记
image_markers = {
"[IMAGE: cover]": images.get("cover", ""),
"[IMAGE: data_chart]": images.get("data_chart", ""),
"[IMAGE: case_comparison]": images.get("case_comparison", ""),
"[IMAGE: action_checklist]": images.get("action_checklist", ""),
"[IMAGE: equipment]": images.get("equipment", ""),
}
for marker, image_path in image_markers.items():
if image_path:
img_tag = f'<img src="{image_path}" alt="{marker}">'
content = content.replace(marker, img_tag)
# 2. 替换索引标记
ordered_keys = ["cover", "data_chart", "case_comparison", "action_checklist", "equipment"]
for i, key in enumerate(ordered_keys, 1):
if key in images:
marker = f"[IMAGE: image_{i}]"
img_tag = f'<img src="{images[key]}" alt="图片{i}" class="platform-{platform}">'
content = content.replace(marker, img_tag)
# 3. 填充模板(将内容放入)
template = self._get_base_template()
html = template.replace("<!-- CONTENT -->", content)
# 4. 平台特定占位符替换(在 html 上进行)
if platform == "zhihu":
# 确定选题领域
field = "可持续生活"
if topic_data and isinstance(topic_data, dict):
topic_field = topic_data.get("topic", {}).get("field", "")
if topic_field:
field = topic_field
# 领域映射到标签
tag_map = {
"未来工作方式": ["科技", "职场", "AI"],
"可持续生活系统": ["可持续", "生活", "环保"],
"个人知识工厂": ["知识管理", "个人成长", "效率"],
"科技人文交叉": ["科技", "人文", "AI伦理"],
}
tags = tag_map.get(field, ["科技", "生活", "可持续"])[:4]
tags.append(TODAY[:4]+"年趋势")
tags_section = '<div class="tags">' + " ".join(f'#{tag}' for tag in tags) + '</div>'
logger.info(f"[DEBUG] Replacing TAGS with: {tags_section}")
if "<!-- TAGS -->" in html:
html = html.replace("<!-- TAGS -->", tags_section)
logger.info(f"[DEBUG] After TAGS replace, length: {len(html)}")
else:
logger.warning("[DEBUG] TAGS placeholder not found in HTML! Template may be missing.")
# Fallback: append tags at end of content
html = html.replace("</body>", tags_section + "\n</body>")
elif platform == "wechat":
abstract = content[:100] + "..."
html = html.replace("<!-- ABSTRACT -->", f'<p class="abstract">{abstract}</p>')
elif platform == "xiaohongshu":
field = "可持续生活"
if topic_data and isinstance(topic_data, dict):
topic_field = topic_data.get("topic", {}).get("field", "")
if topic_field:
field = topic_field
hashtag_map = {
"未来工作方式": ["#远程工作", "#数字游民", "#AI副业"],
"可持续生活系统": ["#可持续生活", "#零浪费", "#环保"],
"个人知识工厂": ["#第二大脑", "#PKM", "#个人成长"],
"科技人文交叉": ["#科技", "#AI伦理", "#数字健康"],
}
hashtags = hashtag_map.get(field, ["#可持续生活", "#全球视野", "#宇之然"])[:5]
hashtags_section = '<div class="hashtags">' + " ".join(hashtags) + '</div>'
html = html.replace("<!-- HASHTAGS -->", hashtags_section)
# 5. 替换日期和标题
title = "可持续性内容"
if topic_data and isinstance(topic_data, dict):
topic_title = topic_data.get("topic", {}).get("title")
if topic_title:
title = topic_title
html = html.replace("{{DATE}}", TODAY)
html = html.replace("{{TITLE}}", title)
# Debug: check tags presence
if platform == "zhihu":
tags_pos = html.find('class="tags"')
if tags_pos != -1:
snippet = html[max(0, tags_pos-50):tags_pos+100]
logger.info(f"[DEBUG] Tags found: ...{snippet}...")
else:
logger.info("[DEBUG] Tags section not found")
return html
def _get_base_template(self) -> str:
"""获取基础HTML模板"""
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - {{DATE}}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding:66px; }
h1, h2, h3 { color: #333; margin-top: 1.5em; }
p { margin: 1em 0; color: #555; }
ul, ol { padding-left: 1.5em; }
img { max-width: 100%; height: auto; margin: 1em 0; border: 1px solid #eee; }
.tags, .hashtags { margin: 1em 0; color: #666; }
.abstract { background: #f9f9f9; padding: 1em; border-left: 4px solid #4CAF50; }
footer { margin-top: 2em; padding-top: 1em; border-top: 1px solid #eee; color: #888; font-size: 0.9em; }
</style>
</head>
<body>
<!-- CONTENT -->
<footer>
<p>本文由宇之然AI助手生成 | 数据来源:全球可持续性信息源 | 生成日期:{{DATE}}</p>
</footer>
</body>
</html>
"""
def save_article(self, article: ContentArticle):
"""保存文章"""
output_dir = Path(article.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# HTML文件
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 send_wecom_notification(self):
"""发送企业微信通知"""
try:
notification_script = PROJECT_ROOT / "scripts" / "wecom_notifier.py"
if not notification_script.exists():
logger.warning("企业微信通知脚本不存在")
return
# 准备通知数据
notification_data = {
"task": "content_creation",
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
"topic_title": self.articles[0].title if self.articles else "",
"image_count": len(self.articles[0].image_paths) if self.articles else 0,
"output_dir": str(self.release_dir.relative_to(PROJECT_ROOT)),
"status": "完成" if self.articles else "失败"
}
data_file = self.today_dir / "creator_notification.json"
with open(data_file, 'w', encoding='utf-8') as f:
json.dump(notification_data, f, ensure_ascii=False)
# 运行通知脚本
result = subprocess.run(
[sys.executable, str(notification_script), str(data_file)],
capture_output=True,
text=True,
cwd=PROJECT_ROOT
)
if result.returncode == 0:
logger.info("企业微信通知发送成功")
else:
logger.error(f"通知发送失败: {result.stderr}")
except Exception as e:
logger.error(f"发送通知失败: {e}")
def run(self):
"""主运行流程"""
logger.info("开始内容创作")
# 1. 选择选题
topic_data = self.select_topic_for_today()
if not topic_data:
logger.error("未能选择选题,任务结束")
return False
# 2. 创作内容
content = self.create_content(topic_data)
# 3. 生成图片 (返回字典)
images_dict = self.generate_images(content, topic_data)
# 4. 为每个平台生成HTML
platforms = ["zhihu", "wechat", "xiaohongshu"]
topic_id = topic_data["topic"].get("id", "unknown")
# 确定图片顺序
ordered_keys = ["cover", "data_chart", "case_comparison", "action_checklist", "equipment"]
images_list = [images_dict[k] for k in ordered_keys if k in images_dict]
for platform in platforms:
html = self.create_html_for_platform(content, images_dict, platform, topic_data)
article = ContentArticle(
id=f"{topic_id}_{platform}",
topic_id=topic_id,
title=topic_data["topic"].get("title", ""),
platform=platform,
content=html,
image_paths=images_list, # 有序列表
metadata={
"platform": platform,
"topic": topic_data["topic"],
"cases": topic_data["cases"],
"word_count": len(content)
},
created_date=TODAY,
output_dir=str(self.release_dir / platform)
)
self.save_article(article)
self.articles.append(article)
# 5. 更新选题状态
self.update_topic_status(topic_id)
# 6. 发送通知
self.send_wecom_notification()
# 7. 合规审查(如有生成文章)
if self.articles:
self.run_compliance_check(self.articles[0])
logger.info(f"创作完成: {len(self.articles)} 篇文章")
return True
def update_topic_status(self, topic_id: str):
"""更新选题状态为已发布"""
topics_file = DATA_DIR / "sustainability_topics.json"
if not topics_file.exists():
return
with open(topics_file, 'r', encoding='utf-8') as f:
all_topics = json.load(f)
for topic in all_topics:
if topic.get("id") == topic_id:
topic["status"] = "已发布"
topic["published_date"] = TODAY
break
with open(topics_file, 'w', encoding='utf-8') as f:
json.dump(all_topics, f, ensure_ascii=False, indent=2)
logger.info(f"更新选题 {topic_id} 状态为已发布")
def run_compliance_check(self, article):
"""运行合规审查"""
try:
from scripts.compliance_checker import check_article
result = check_article(
article.content,
article.platform,
article.metadata.get("topic") if hasattr(article, 'metadata') else None
)
# 记录审查结果
log_msg = f"合规审查: {article.platform} - {article.title[:30]} - 得分: {result['score']} - 问题数: {len(result['issues'])}"
if result['passed']:
logger.info(log_msg)
else:
logger.warning(log_msg)
for issue in result['issues']:
logger.warning(f" [合规问题] {issue['type']}/{issue.get('category','')}: {issue.get('suggestion','')}")
# 保存审查报告
compliance_file = self.today_dir / f"compliance_{article.id}.json"
with open(compliance_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info(f"合规报告已保存: {compliance_file}")
except Exception as e:
logger.error(f"合规审查失败: {e}")
def main():
"""主函数"""
try:
creator = ContentCreator()
success = creator.run()
if success:
print(f"SUCCESS: Created {len(creator.articles)} articles for {TODAY}")
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()