Add platform config website_url, admin tab, fix writer DB config fallback, add trigger endpoints
This commit is contained in:
+85
-62
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user