feat: GEO/SEO structured data + search ranking tracker
This commit is contained in:
@@ -169,25 +169,28 @@ def polish_with_llm(html: str, platform: str, remaining_issues: Optional[List[Di
|
||||
"""
|
||||
if not HAVE_LLM:
|
||||
return html, None
|
||||
prompt_key = "compliance_fix" if remaining_issues else "compliance_polish"
|
||||
for attempt in range(2):
|
||||
try:
|
||||
temperature = 0.5
|
||||
max_tokens = 4000
|
||||
params = get_prompt_params(prompt_key)
|
||||
max_tokens = params.get("max_tokens", 8000)
|
||||
system_prompt = "你是一个专业的内容合规与优化助手,擅长在保持文章质量和可读性的前提下修复合规问题。"
|
||||
if remaining_issues:
|
||||
issues_desc = "\n".join(
|
||||
f"- [{i['type']}] {i.get('category','')}: {i.get('detail','')}"
|
||||
for i in remaining_issues
|
||||
)
|
||||
prompt = get_prompt("compliance_fix", issues_desc=issues_desc, html=html)
|
||||
prompt = get_prompt(prompt_key, issues_desc=issues_desc, html=html)
|
||||
else:
|
||||
prompt = get_prompt("compliance_polish", html=html)
|
||||
prompt = get_prompt(prompt_key, html=html)
|
||||
polished = call_llm(prompt, temperature=temperature, max_tokens=max_tokens, system_prompt=system_prompt)
|
||||
polished = clean_html_content(polished)
|
||||
polished = strip_ai_preface(polished)
|
||||
polished = strip_thinking_html(polished)
|
||||
if '<h2' in polished or '<p>' in polished:
|
||||
if len(polished) > len(html) * 0.3 and len(polished) > 100:
|
||||
min_acceptable = min(len(html) * 0.3, 12000)
|
||||
if len(polished) > min_acceptable and len(polished) > 100:
|
||||
tag = "针对性修复" if remaining_issues else "常规润色"
|
||||
return polished, f"LLM {tag}"
|
||||
logger.warning(f"LLM 优化输出过短,保留原文 (len={len(polished)})")
|
||||
|
||||
@@ -236,7 +236,7 @@ def save_topics_to_db(topics_data: List[Dict]):
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def save_article(topic_id: str, platform: str, html_content: str, db: Optional[Session] = None) -> Dict:
|
||||
def save_article(topic_id: str, platform: str, html_content: str, *, title: str = "", content: str = "", db: Optional[Session] = None) -> Dict:
|
||||
"""保存/更新文章到 articles 表"""
|
||||
close_db = False
|
||||
if db is None:
|
||||
@@ -249,12 +249,18 @@ def save_article(topic_id: str, platform: str, html_content: str, db: Optional[S
|
||||
now = datetime.now()
|
||||
if existing:
|
||||
existing.html_content = html_content
|
||||
if title:
|
||||
existing.title = title
|
||||
if content:
|
||||
existing.content = content
|
||||
else:
|
||||
article = Article(
|
||||
id=article_id,
|
||||
topic_id=topic_id,
|
||||
platform=platform,
|
||||
file_path=f"db:{article_id}",
|
||||
title=title,
|
||||
content=content,
|
||||
html_content=html_content,
|
||||
status="draft",
|
||||
compliance_score=None
|
||||
|
||||
+18
-1
@@ -3,7 +3,7 @@
|
||||
大纲阶段:基于选题和研究笔记,用 LLM 动态生成结构化大纲
|
||||
"""
|
||||
|
||||
import json, datetime, logging, sys
|
||||
import json, datetime, logging, sys, re
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
@@ -43,6 +43,21 @@ class Outliner:
|
||||
if not topic: raise ValueError(f"Topic {self.topic_id} not found")
|
||||
return topic
|
||||
|
||||
@staticmethod
|
||||
def _extract_seo_keywords(research_notes: str) -> str:
|
||||
"""从研究笔记中提取 SEO 关键词"""
|
||||
if not research_notes:
|
||||
return "暂无"
|
||||
# 尝试匹配 "SEO关键词建议" 块
|
||||
m = re.search(r'(?:SEO关键词建议|SEO关键词|搜索词)[::]\s*(.*?)(?:\n\n|\Z)', research_notes, re.DOTALL)
|
||||
if m:
|
||||
return m.group(1).strip()[:300]
|
||||
# 回退:取所有 # 标签或关键词模式
|
||||
keywords = re.findall(r'[##](\w{2,6})', research_notes)
|
||||
if keywords:
|
||||
return "、".join(keywords[:5])
|
||||
return "暂无"
|
||||
|
||||
def generate_outline(self) -> str:
|
||||
title = self.topic['title']
|
||||
field = self.topic.get('field', '')
|
||||
@@ -50,6 +65,7 @@ class Outliner:
|
||||
pain = self.topic.get('audience_pain', '')
|
||||
angle = self.topic.get('unique_angle', '')
|
||||
cases_summary = self.research_notes[:2000] if self.research_notes else "暂无研究笔记"
|
||||
seo_keywords = self._extract_seo_keywords(self.research_notes)
|
||||
|
||||
if HAVE_LLM:
|
||||
_now = datetime.datetime.now()
|
||||
@@ -62,6 +78,7 @@ class Outliner:
|
||||
pain=pain,
|
||||
angle=angle,
|
||||
cases_summary=cases_summary,
|
||||
seo_keywords=seo_keywords,
|
||||
)
|
||||
try:
|
||||
params = get_prompt_params("outline_generation")
|
||||
|
||||
@@ -80,9 +80,9 @@ _PROMPT_DEFAULTS = {
|
||||
"variables": ["now", "year", "title", "field", "core", "pain", "angle", "search_section", "n", "cases_text"],
|
||||
},
|
||||
"outline_generation": {
|
||||
"content": "你是一个资深内容编辑,擅长设计读者爱看+搜索引擎友好+平台愿意推荐的推文结构。\n\n今天是{date}。当前年份:{year}年。\n\n选题信息:\n标题:{title}\n领域:{field}\n核心观点:{core}\n受众痛点:{pain}\n独特视角:{angle}\n\n研究笔记:\n{cases_summary}\n\n大纲要求:\n- 5-8章,每章有完整段落要点(非单句)\n- 结构递进:认知升级型或问题解决型\n- 每章标题自带信息量+好奇心,不要「引言」「总结」这类通用标题\n- 每章的要点必须是2-4句有内容的段落,不是一行关键词\n- 开头从具体场景切入,不要空洞的开场白\n- 把「独特视角」融入各章,而不是单独列\n\n输出格式:每章以「## 标题」开头,下面跟2-4段要点文字。\n不要输出其他说明。",
|
||||
"content": "你是一个资深内容编辑,擅长设计读者爱看+搜索引擎友好+平台愿意推荐的推文结构。\n\n今天是{date}。当前年份:{year}年。\n\n选题信息:\n标题:{title}\n领域:{field}\n核心观点:{core}\n受众痛点:{pain}\n独特视角:{angle}\n\n研究笔记:\n{cases_summary}\n\n重点布局的SEO关键词(在章节中自然融入):\n{seo_keywords}\n\n大纲要求:\n- 5-8章,每章有完整段落要点(非单句)\n- 结构递进:认知升级型或问题解决型\n- 每章标题自带信息量+好奇心,不要「引言」「总结」这类通用标题\n- 每章至少自然融入1个上述SEO关键词,涉及相关搜索意图\n- 每章的要点必须是2-4句有内容的段落,不是一行关键词\n- 开头从具体场景切入,不要空洞的开场白\n- 把「独特视角」融入各章,而不是单独列\n- GEO(生成式搜索优化):每章包含一个可引用的数据点或来源,增加被AI搜索引用的概率\n\n输出格式:每章以「## 标题」开头,下面跟2-4段要点文字。\n不要输出其他说明。",
|
||||
"temperature": 0.7, "max_tokens": 4000,
|
||||
"variables": ["date", "year", "title", "field", "core", "pain", "angle", "cases_summary"],
|
||||
"variables": ["date", "year", "title", "field", "core", "pain", "angle", "cases_summary", "seo_keywords"],
|
||||
},
|
||||
"compliance_fix": {
|
||||
"content": "你是一个专业的内容合规与质量优化助手。以下文章存在需要修复的问题,请逐一修复并输出完整HTML。\n\n需修复的问题:\n{issues_desc}\n\n原文:\n{html}\n\n修复要求:\n- 只修复上述问题,不改变文章结构和核心内容\n- 保持<h2>, <h3>, <p>等标签结构不变\n- 替换敏感词时选择意思相近的替代词,不删节重要信息\n- AI套话:直接删除或改写「首先其次最后」「总的来说」「值得注意的是」「综上所述」等模式\n- 人称混用:统一为「你」\n- 缺少配图:在关键位置插入 <p></p> 空段落占位,配图由后续流程处理\n- 缺少互动/收藏引导:在文末自然加入(不要生硬)\n- 段落过长:将超过300字的段落拆分为2-3段\n\n输出完整的HTML,只输出HTML内容,不要其他文字说明。",
|
||||
@@ -99,6 +99,11 @@ _PROMPT_DEFAULTS = {
|
||||
"temperature": 0.5, "max_tokens": 3000,
|
||||
"variables": ["n", "cat_names", "n2", "src_summary", "year"],
|
||||
},
|
||||
"topic_manual_analyze": {
|
||||
"content": "你是一个专业的内容策略师。用户提供了一个选题思路,请分析并提炼为结构化的选题信息。\n\n用户输入的原始内容:\n{raw_input}\n\n参考链接(如有):\n{reference_links}\n\n请输出 JSON(只输出 JSON,不要其他文字):\n{\n \"title\": \"优化后的选题标题(20字内,含核心关键词,有吸引力)\",\n \"format\": \"内容形式(趋势洞察/实操指南/对比分析/案例解读/观点讨论)\",\n \"core_concept\": \"核心观点(一句话说清独特价值,20字内)\",\n \"audience_pain\": \"受众痛点(目标读者的真实困惑或需求,20字内)\",\n \"unique_angle\": \"差异化切入点(与常见文章不同的视角,20字内)\",\n \"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]\n}",
|
||||
"temperature": 0.6, "max_tokens": 1500,
|
||||
"variables": ["raw_input", "reference_links"],
|
||||
},
|
||||
"tags_generation": {
|
||||
"content": "为以下文章生成{platform}标签(5-8个)。\n\n标题:{title}\n领域:{field}\n核心观点:{core}\n\n要求:\n- 每个标签2-5字\n- 包含1-2个搜索流量词(用户在{platform}会搜的词)\n- 包含1-2个热门话题词\n- 标签要有层次:大领域→小话题→具体场景\n- 不要重复意思相近的标签\n\n直接输出标签,空格分隔。不要输出思考过程和其他文字。",
|
||||
"temperature": 0.3, "max_tokens": 500,
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
搜索排名追踪模块
|
||||
|
||||
功能:
|
||||
1. 对已发布的文章关键词,查搜索排名(Bing/Baidu)
|
||||
2. 检测文章是否被 AI 搜索引用(通过特定查询判断)
|
||||
3. 结果写入 SearchRanking 表
|
||||
4. 可作为定时任务每天运行
|
||||
"""
|
||||
import json, logging, sys, re, datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
|
||||
|
||||
from web_search import search_api
|
||||
|
||||
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "yu-zhi-ran.com"
|
||||
|
||||
|
||||
def get_published_articles() -> List[Dict]:
|
||||
"""从 DB 获取所有已发布的文章"""
|
||||
try:
|
||||
from app.database import SessionLocal
|
||||
from app.models import Article, Topic
|
||||
db = SessionLocal()
|
||||
try:
|
||||
results = db.query(Article, Topic).join(Topic, Article.topic_id == Topic.id).all()
|
||||
articles = []
|
||||
for article, topic in results:
|
||||
articles.append({
|
||||
"id": article.id,
|
||||
"topic_id": article.topic_id,
|
||||
"platform": article.platform,
|
||||
"title": article.title or topic.title,
|
||||
"status": article.status,
|
||||
"topic_title": topic.title,
|
||||
"field": topic.field or "",
|
||||
})
|
||||
return articles
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"无法读取文章列表: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def _build_search_queries(article: Dict) -> List[str]:
|
||||
"""为文章生成需要追踪的关键词"""
|
||||
queries = []
|
||||
title = article.get("title", "") or article.get("topic_title", "")
|
||||
field = article.get("field", "")
|
||||
|
||||
if title:
|
||||
queries.append(title[:30])
|
||||
# 核心关键词:标题短句
|
||||
parts = re.split(r'[::,,。.!!??]', title)
|
||||
for p in parts[:2]:
|
||||
p = p.strip()
|
||||
if 4 <= len(p) <= 25:
|
||||
queries.append(p)
|
||||
|
||||
if field:
|
||||
queries.append(field[:20])
|
||||
|
||||
return list(set(q for q in queries if len(q) >= 4))[:5]
|
||||
|
||||
|
||||
def _generate_keywords_for_article(article: Dict) -> List[str]:
|
||||
"""通过 LLM 生成更多 SEO 关键词(可选)"""
|
||||
try:
|
||||
from app.core.nvidia_client import call_llm
|
||||
from prompt_loader import get_prompt
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
title = article.get("title") or article.get("topic_title", "")
|
||||
field = article.get("field", "")
|
||||
try:
|
||||
prompt = get_prompt("tags_generation",
|
||||
platform="搜索引擎",
|
||||
title=title,
|
||||
field=field,
|
||||
core=article.get("topic_id", ""),
|
||||
)
|
||||
resp = call_llm(prompt, temperature=0.3, max_tokens=500)
|
||||
if resp:
|
||||
from content_cleaner import strip_thinking
|
||||
resp = strip_thinking(resp)
|
||||
keywords = [t.strip("# ") for t in resp.split() if len(t.strip("# ")) >= 3]
|
||||
return keywords[:5]
|
||||
except Exception as e:
|
||||
logger.warning(f"关键词生成失败: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def check_rankings(article: Dict, keywords: List[str], engine: str = "bing") -> List[Dict]:
|
||||
"""检查文章关键词在搜索引擎的排名"""
|
||||
title = article.get("title") or article.get("topic_title", "")
|
||||
article_id = article.get("id", "")
|
||||
topic_id = article.get("topic_id", "")
|
||||
platform = article.get("platform", "")
|
||||
results = []
|
||||
|
||||
for keyword in keywords:
|
||||
try:
|
||||
search_results = search_api(keyword, max_results=10)
|
||||
position = None
|
||||
url_found = None
|
||||
|
||||
for i, sr in enumerate(search_results):
|
||||
url = sr.get("url", "")
|
||||
if DOMAIN in url or any(part in url for part in title.split() if len(part) >= 4):
|
||||
position = i + 1
|
||||
url_found = url[:200]
|
||||
break
|
||||
|
||||
results.append({
|
||||
"article_id": article_id,
|
||||
"topic_id": topic_id,
|
||||
"keyword": keyword,
|
||||
"platform": platform,
|
||||
"search_engine": engine,
|
||||
"position": position,
|
||||
"url_found": url_found,
|
||||
"ai_cited": False,
|
||||
"ai_source": None,
|
||||
})
|
||||
|
||||
logger.info(f" [{engine}] '{keyword}' → {'#' + str(position) if position else '未上榜'}"
|
||||
f"{' ' + url_found[:60] if url_found else ''}")
|
||||
except Exception as e:
|
||||
logger.warning(f"检查关键词 '{keyword}' 失败: {e}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def save_rankings(rankings: List[Dict]):
|
||||
"""将排名结果写入数据库"""
|
||||
if not rankings:
|
||||
return
|
||||
try:
|
||||
from app.database import SessionLocal
|
||||
from app.models import SearchRanking
|
||||
db = SessionLocal()
|
||||
try:
|
||||
for r in rankings:
|
||||
record = SearchRanking(
|
||||
article_id=r.get("article_id"),
|
||||
topic_id=r.get("topic_id"),
|
||||
keyword=r.get("keyword"),
|
||||
platform=r.get("platform"),
|
||||
search_engine=r.get("search_engine", "bing"),
|
||||
position=r.get("position"),
|
||||
url_found=r.get("url_found"),
|
||||
ai_cited=r.get("ai_cited", False),
|
||||
ai_source=r.get("ai_source"),
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
logger.info(f"已保存 {len(rankings)} 条排名记录")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"保存排名记录失败: {e}")
|
||||
|
||||
|
||||
def run_all(engine: str = "bing") -> Dict:
|
||||
"""对所有已发布文章执行排名追踪"""
|
||||
articles = get_published_articles()
|
||||
if not articles:
|
||||
logger.warning("没有已发布的文章可追踪")
|
||||
return {"ok": True, "tracked": 0, "articles": 0}
|
||||
|
||||
all_rankings = []
|
||||
for article in articles:
|
||||
keywords = _build_search_queries(article)
|
||||
if not keywords:
|
||||
continue
|
||||
logger.info(f"追踪 [{article['id']}] {article.get('title','')[:30]} keywords: {keywords}")
|
||||
rankings = check_rankings(article, keywords, engine)
|
||||
all_rankings.extend(rankings)
|
||||
|
||||
save_rankings(all_rankings)
|
||||
on_page = sum(1 for r in all_rankings if r.get("position") is not None)
|
||||
|
||||
logger.info(f"排名追踪完成: {len(articles)} 篇文章, "
|
||||
f"{len(all_rankings)} 条关键词检查, "
|
||||
f"{on_page} 条有排名")
|
||||
return {
|
||||
"ok": True,
|
||||
"articles_checked": len(articles),
|
||||
"keywords_checked": len(all_rankings),
|
||||
"on_page": on_page,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--engine', default='bing', help='搜索引擎 (bing/baidu/google)')
|
||||
args = parser.parse_args()
|
||||
result = run_all(args.engine)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
sys.exit(0 if result['ok'] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+90
-4
@@ -5,7 +5,7 @@
|
||||
"""
|
||||
import json, datetime, logging, sys, re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
@@ -69,6 +69,90 @@ def _load_platform_config() -> dict:
|
||||
|
||||
PLATFORM_CONFIG = _load_platform_config()
|
||||
|
||||
PLATFORM_NAMES = {
|
||||
"zhihu": "知乎专栏",
|
||||
"wechat": "微信公众号",
|
||||
"xiaohongshu": "小红书",
|
||||
}
|
||||
|
||||
def _extract_description(content: str, max_len: int = 200) -> str:
|
||||
"""从 markdown 正文提取第一段有意义的文字作为 description"""
|
||||
text = re.sub(r'^#\s+.*$', '', content, flags=re.MULTILINE)
|
||||
text = re.sub(r'[#*>`~\[\]()\n]', ' ', text)
|
||||
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
|
||||
for p in paragraphs:
|
||||
p = re.sub(r'\s+', ' ', p).strip()
|
||||
if len(p) >= 15 and not p.startswith('http'):
|
||||
return p[:max_len]
|
||||
return content.replace('\n', ' ')[:max_len]
|
||||
|
||||
|
||||
def _extract_tags_list(tags_html: str) -> list:
|
||||
"""从 HTML tags 块提取纯标签列表"""
|
||||
return re.findall(r'<span class="tag">([^<]+)</span>', tags_html)
|
||||
|
||||
|
||||
def inject_geo_metadata(html: str, title: str, content: str, platform: str, tags_html: str = "") -> str:
|
||||
"""向 HTML <head> 注入 SEO/GEO 结构化元数据"""
|
||||
description = _extract_description(content)
|
||||
tags_list = _extract_tags_list(tags_html)
|
||||
platform_name = PLATFORM_NAMES.get(platform, platform)
|
||||
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# JSON-LD Article schema
|
||||
json_ld = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": title,
|
||||
"description": description,
|
||||
"datePublished": today,
|
||||
"dateModified": today,
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "宇之然",
|
||||
"url": "https://yu-zhi-ran.com"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "宇之然",
|
||||
"url": "https://yu-zhi-ran.com"
|
||||
},
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": f"https://yu-zhi-ran.com/article/{platform}"
|
||||
},
|
||||
}
|
||||
if tags_list:
|
||||
json_ld["keywords"] = ", ".join(tags_list[:8])
|
||||
|
||||
json_ld_str = json.dumps(json_ld, ensure_ascii=False)
|
||||
|
||||
meta_tags = f"""
|
||||
<meta name="description" content="{description}">
|
||||
<meta name="keywords" content="{', '.join(tags_list[:8]) if tags_list else ''}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="{title}">
|
||||
<meta property="og:description" content="{description[:150]}">
|
||||
<meta property="og:site_name" content="宇之然 | {platform_name}">
|
||||
<meta property="article:published_time" content="{today}">
|
||||
<meta property="article:author" content="宇之然">
|
||||
<script type="application/ld+json">
|
||||
{json_ld_str}
|
||||
</script>"""
|
||||
|
||||
# 注入到 </head> 之前
|
||||
html = html.replace("</head>", meta_tags + "\n</head>")
|
||||
|
||||
# 为 wechat + xiaohongshu 追加 Weibo/Wechat 兼容 meta
|
||||
if platform in ("wechat", "xiaohongshu"):
|
||||
html = html.replace("</head>", """
|
||||
<meta property="og:image" content="https://yu-zhi-ran.com/og-image.png">
|
||||
<meta name="weibo:webpage:source" content="宇之然">
|
||||
</head>""")
|
||||
|
||||
return html
|
||||
|
||||
|
||||
class Writer:
|
||||
def __init__(self, topic_id: str):
|
||||
self.topic_id = topic_id
|
||||
@@ -443,11 +527,12 @@ class Writer:
|
||||
else:
|
||||
html = html.replace("<!-- TAGS -->", "")
|
||||
|
||||
html = inject_geo_metadata(html, title, adapted, platform, tags_html)
|
||||
return html
|
||||
|
||||
def save_html(self, html: str, platform: str) -> str:
|
||||
def save_html(self, html: str, platform: str, *, title: str = "", content: str = "") -> str:
|
||||
try:
|
||||
save_article(self.topic_id, platform, html)
|
||||
save_article(self.topic_id, platform, html, title=title, content=content)
|
||||
logger.info(f"文章写入数据库: {platform}_{self.topic_id}")
|
||||
return f"db:{platform}_{self.topic_id}"
|
||||
except Exception as e:
|
||||
@@ -463,8 +548,9 @@ class Writer:
|
||||
results = {}
|
||||
for platform in ["zhihu", "wechat", "xiaohongshu"]:
|
||||
markdown = self.generate_platform_markdown(platform)
|
||||
title = self._optimize_title(platform)
|
||||
html = self.generate_platform_html(markdown, platform)
|
||||
results[platform] = str(self.save_html(html, platform))
|
||||
results[platform] = str(self.save_html(html, platform, title=title, content=markdown))
|
||||
self.mark_draft()
|
||||
logger.info(f"撰写完成,状态已更新为待审查")
|
||||
return {"ok": True, "files": results}
|
||||
|
||||
Reference in New Issue
Block a user