Add platform config website_url, admin tab, fix writer DB config fallback, add trigger endpoints

This commit is contained in:
Yuzhiran Dev
2026-05-20 09:38:56 +08:00
parent 55e5f3d166
commit 498165440f
84 changed files with 1988 additions and 242 deletions
+85 -62
View File
@@ -35,21 +35,33 @@ logger = logging.getLogger(__name__)
_md_parser = mistune.create_markdown()
PLATFORM_CONFIG = {
"zhihu": {
"max_chars": 3000,
"style": "深度长文分析",
},
"wechat": {
"max_chars": 1500,
"style": "亲切口语化",
},
"xiaohongshu": {
"max_chars": 800,
"style": "图文笔记,emoji+标签",
},
_FALLBACK_PLATFORM_CONFIG = {
"zhihu": {"max_chars": 3000, "style": "深度长文分析", "min_chars": 1500},
"wechat": {"max_chars": 1500, "style": "亲切口语化", "min_chars": 800},
"xiaohongshu": {"max_chars": 800, "style": "图文笔记,emoji+标签", "min_chars": 300},
}
def _load_platform_config() -> dict:
try:
from app.database import SessionLocal
from app.models import PlatformConfig
db = SessionLocal()
configs = db.query(PlatformConfig).all()
db.close()
result = {}
for c in configs:
result[c.platform] = {
"max_chars": c.max_words or _FALLBACK_PLATFORM_CONFIG.get(c.platform, {}).get("max_chars", 3000),
"style": c.default_format or _FALLBACK_PLATFORM_CONFIG.get(c.platform, {}).get("style", "深度内容"),
"min_chars": c.min_words or _FALLBACK_PLATFORM_CONFIG.get(c.platform, {}).get("min_chars", 300),
}
return result
except Exception:
pass
return dict(_FALLBACK_PLATFORM_CONFIG)
PLATFORM_CONFIG = _load_platform_config()
class Writer:
def __init__(self, topic_id: str):
self.topic_id = topic_id
@@ -96,86 +108,97 @@ class Writer:
sections.append(current)
return sections
@staticmethod
def _clean_markdown(text: str) -> str:
lines = text.split('\n')
cleaned = []
for line in lines:
line = re.sub(r'^#{1,6}\s+', '', line)
line = re.sub(r'^[\-\*\+]\s+', '', line)
line = re.sub(r'^\d+[\.\)]\s+', '', line)
line = re.sub(r'\*{1,3}([^*]+)\*{1,3}', r'\1', line)
cleaned.append(line)
return '\n'.join(cleaned).strip()
@staticmethod
def _is_outline_noise(line: str) -> bool:
stripped = line.strip()
if not stripped:
return True
if stripped.startswith('---'):
return True
if '大纲生成时间' in stripped:
return True
if stripped.startswith('*大纲'):
return True
return False
def _expand_section(self, section: Dict) -> str:
content = section.get('content', '').strip()
if len(content) > 200:
return content
if HAVE_LLM and len(content) < 150:
logger.info(f"使用 LLM 扩写章节: {section['title']}")
prompt = f"""你是一个真人写作者+行业观察者,正在写一篇关于「{self.topic['title']}」的文章。现在写「{section['title']}」这一节。
prompt = f"""你是一个资深作者,正在写一篇关于「{self.topic['title']}」的文章。写「{section['title']}」这一节。
⚠️ 今天日期:{datetime.datetime.now().strftime('%Y年%m月%d')}当前年份:{datetime.datetime.now().year}年。
今天日期:{datetime.datetime.now().strftime('%Y年%m月%d')}
笔记要点:
{content}
要求(逐条对照,每一条都不能跳过):
### 热点与时效
- **必须引用{datetime.datetime.now().year-1}-{datetime.datetime.now().year}年最新数据/事件/政策/行业报告**,禁用一切过时数据
- 体现当前国内外正在讨论什么、最新的趋势变化
- 每个论点必须配一个真实发生的最新案例(附数据来源),严禁使用虚构数据
- 写作前先确认:这个数据和案例是否是最近{datetime.datetime.now().year-1}-{datetime.datetime.now().year}年的?
【输出要求】
输出3-5段纯粹、流畅的段落文字,共400-800字。
### 独特观点
- 有自己的判断和立场,拒绝"车轱辘话""正确废话"
- 至少提供一个"大多数人没想到"的角度
- 宁可尖锐,也不要平庸
格式:
- 禁止任何标题/列表/格式标记(#、-、*、1.、**等)
- 每段3-5句,段间空行分隔
- 用「你」或「我们」视角,自然口语化
### 价值
- 回答读者一个具体问题或解决一个困惑
- 每段回答一个"所以呢?"——读者看完能带走什么
- 结束时读者要有「学到了+想转发」的感觉
内容要求(让文章在各平台能被推荐):
- 开头直接切入痛点或反常识观点,抓住注意力
- 每个观点配具体案例或数据(用「据统计」「调研显示」等),不要空泛说理
- 有独特判断和立场,避免正确废话
- 回答「所以呢」——读者看完能带走什么
- 结尾有情绪感召力,让人想点赞/收藏/转发
### 真人感
- 用「你」或「我们」视角,不要用「我」
- 像人在自然说话,不是AI组装文字
- 避免「首先」「其次」「总的来说」「综上所述」「值得注意的是」
- 段落短,2-4句一段,节奏有变化
- 适当用反问或口语化表达
### SEO
- 自然融入1-2个目标搜索词,不生硬堆砌
- 第一句包含核心关键词
### 专业与简洁
- 语言精准,不注水,不为了字数凑内容
- 200-400字,写到点子上就停
### 平台推荐友好
- 节奏不能平,要有起承转合
- 结尾要让人有点赞/收藏/转发的冲动
直接输出段落正文。"""
直接输出段落正文,不要任何附加说明。"""
try:
expanded = call_llm(prompt, temperature=0.6, max_tokens=1500)
if expanded and len(expanded.strip()) > len(content):
return expanded.strip()
expanded = call_llm(prompt, temperature=0.6)
if expanded:
cleaned = self._clean_markdown(expanded.strip())
if cleaned:
return cleaned
except Exception as e:
logger.warning(f"LLM 扩写失败: {e}")
# Fallback: 将 bullet points 展开为段落
lines = [l.strip() for l in content.split('\n') if l.strip()]
# Fallback: 将 bullet points 展开为段落(过滤噪音行)
lines = [l.strip() for l in content.split('\n') if not self._is_outline_noise(l)]
if lines:
sentences = []
for line in lines:
text = line.lstrip('- *').strip()
text = line
for prefix in ['- ', '* ', '1. ', '2. ', '3. ', '4. ', '5. ']:
if line.startswith(prefix):
text = line[len(prefix):]
break
text = text.strip()
if text:
for prefix in ['- ', '* ', '1. ', '2. ', '3. ', '4. ', '5. ']:
if line.startswith(prefix):
text = line[len(prefix):]
break
if text[-1] not in '。!?;':
text += ''
sentences.append(text)
if sentences:
return ' '.join(sentences)
return content
return ''
def generate_full_markdown(self) -> str:
sections = self._parse_outline_sections()
parts = []
for sec in sections:
if sec['level'] == 1:
if sec.get('content'):
expanded = self._expand_section(sec)
if expanded:
parts.append(expanded + "\n")
continue
heading = f"{'#' * sec['level']} {sec['title']}"
parts.append(heading)
@@ -246,7 +269,7 @@ class Writer:
if HAVE_LLM:
prompt = tag_prompts.get(platform, f"根据文章信息生成适合{platform}的标签。标题:{title} 领域:{field} 核心观点:{core} 直接输出标签,空格分隔。")
try:
tags_text = call_llm(prompt, temperature=0.2, max_tokens=1000)
tags_text = call_llm(prompt, temperature=0.2)
if tags_text:
tags = [t.strip('#') for t in tags_text.strip().split() if t.strip('#')]
if tags:
@@ -342,7 +365,7 @@ class Writer:
prompt = title_templates.get(platform, f"给以下文章改个吸引人的{platform}标题:{original}")
try:
resp = call_llm(prompt, temperature=0.7, max_tokens=1000)
resp = call_llm(prompt, temperature=0.7)
titles = []
for line in resp.strip().split('\n'):
line = line.strip()