fix: content quality, image format, task monitor, calendar data source, search UI & sort
This commit is contained in:
+125
-16
@@ -1,3 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Prompt 配置加载器
|
||||
|
||||
架构:DB 为主,代码仅作初始种子。
|
||||
- _PROMPT_DEFAULTS: 仅用于首次自动写入 DB(种子数据),不作为运行时 fallback
|
||||
- prompt_configs 表: 运行时唯一来源,修改 prompt 直接改 DB
|
||||
- DB 不可用时: 退化到代码默认值(仅用于紧急回退,不是日常模式)
|
||||
|
||||
新增 prompt 流程:
|
||||
1. 在 _PROMPT_DEFAULTS 添加定义
|
||||
2. 重启后自动补入 DB(仅当该 key 不在 DB 中时)
|
||||
3. 后续修改直接在 DB 操作,不再改代码
|
||||
|
||||
修改 prompt 流程:
|
||||
直接 UPDATE prompt_configs SET content = '...' WHERE key = '...';
|
||||
"""
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
@@ -6,6 +23,11 @@ PROJECT_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
|
||||
|
||||
# ============================================================
|
||||
# 种子数据(初始默认值)
|
||||
# 修改 prompt 请直接在 DB 操作,不要改这里
|
||||
# 仅在表里没有该 key 时用于首次填充
|
||||
# ============================================================
|
||||
_PROMPT_DEFAULTS = {
|
||||
"topics_trends": {
|
||||
"content": "你是中文互联网趋势分析师。请列出今天({date})中文互联网上最值得创作的10个话题。\n\n要求:\n1. 覆盖领域:{domains}\n2. 从真实用户角度出发\n3. 每个话题需包含:\n - \"domain\": 领域\n - \"topic\": 话题名称\n - \"reason\": 为什么现在讨论这个(1句话,有具体事件/数据支撑)\n - \"hot_keywords\": 3-5个搜索词(含1-2个长尾词)\n - \"platform\": 最适合分发的平台(知乎/小红书/微信/多平台)\n - \"seo_angle\": 从什么角度切入能获得搜索流量(1句话)\n - \"engagement\": 高/中/低\n\n输出 JSON 数组。只输出 JSON,不要其他文字。",
|
||||
@@ -23,22 +45,22 @@ _PROMPT_DEFAULTS = {
|
||||
"variables": ["gaps"],
|
||||
},
|
||||
"section_expansion": {
|
||||
"content": "你是一个资深作者,正在写一篇关于「{topic_title}」的文章。请写「{section_title}」这一节。\n\n今天日期:{date}。\n\n笔记要点:\n{content}\n\n【输出要求】\n输出3-6段纯粹、流畅的段落文字,每节内容根据平台需求控制在200-800字之间。\n\n格式:\n- 禁止任何标题/列表/格式标记(#、-、*、1.、**等)\n- 每段3-5句,段间空行分隔\n- 用「你」或「我们」视角,自然口语化\n\n内容要求(让文章在各平台能被推荐):\n- 开头直接切入痛点或反常识观点,抓住注意力\n- 每个观点配具体案例或数据(用「据统计」「调研显示」等),不要空泛说理\n- 有独特判断和立场,避免正确废话\n- 回答「所以呢」——读者看完能带走什么\n- 结尾有情绪感召力,让人想点赞/收藏/转发\n\n直接输出段落正文,不要任何附加说明。",
|
||||
"content": "你是一个资深作者,正在写一篇关于「{topic_title}」的文章。请写「{section_title}」这一节。\n\n今天日期:{date}。\n\n笔记要点:\n{content}\n\n【输出要求】\n输出3-6段纯粹、流畅的段落文字,每节内容根据平台需求控制在200-800字之间。\n\n格式:\n- 禁止任何标题/列表/格式标记(#、-、*、1.、**等)\n- 每段3-5句,段间空行分隔\n- 用「你」或「我」视角,自然口语化。全文统一使用「你」称呼读者,不要混用「你们」\n- **禁止以「引言」「核心观点」「受众痛点」「总结」「开头钩子」「独特视角」这类结构标签开头**——直接从场景或痛点切入\n\n内容要求(让文章在各平台能被推荐):\n- 开头直接切入场景或痛点,一句话抓住注意力,不要铺垫\n- 每个观点配具体案例或数据(用「据统计」「调研显示」等),不要空泛说理\n- 有独特判断和立场,避免正确废话\n- 回答「所以呢」——读者看完能带走什么\n- **避免AI套话**:不要出现「一个真实的XX案例很能说明问题」「说回到XX这件事」「这就是XX被XX后的样子」「如果你也XX」「值得注意的是」「首先其次最后」「综上所述」\n- 结尾有情绪感召力,让人想点赞/收藏/转发\n- 引用数据或案例时,在行内用(来源:报告/案例名称)标注\n\n直接输出段落正文,不要任何附加说明。\n\n【注意:如果你是写最后一节,在正文写完后加一行 --- 分隔,然后写 **参考资料**,每行一个来源:- 来源名称(简要说明)。前面几节不要加这个。】",
|
||||
"temperature": 0.75, "max_tokens": 3000,
|
||||
"variables": ["topic_title", "section_title", "date", "content"],
|
||||
},
|
||||
"title_optimize_zhihu": {
|
||||
"content": "你是一个知乎内容专家。为以下文章起3个高点击率标题。\n\n标题:{title}\n核心观点:{core}\n受众痛点:{pain}\n领域:{field}\n\n要求:\n- 信息密度高,SEO关键词靠前\n- 偏好数字、对比、悬念、痛点类标题\n- 20字以内\n- 不要「如何...」开头\n- 有独特视角和差异化\n- 能引发讨论\n\n输出3个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
|
||||
"content": "你是一个知乎高赞标题专家。为以下文章起3个高收藏率标题。\n\n标题:{title}\n核心观点:{core}\n受众痛点:{pain}\n领域:{field}\n\n要求:\n- 信息密度高,包含搜索关键词\n- 优先使用数字、对比、悬念、痛点\n- 20字以内\n- 不要「如何...」开头\n- 有独特判断和立场,能引发讨论或反对\n- 制造「不点开就亏了」的紧迫感\n\n输出3个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
|
||||
"temperature": 0.8, "max_tokens": 1500,
|
||||
"variables": ["title", "core", "pain", "field"],
|
||||
},
|
||||
"title_optimize_wechat": {
|
||||
"content": "你是一个公众号资深作者。为以下文章起3个10万+潜力标题。\n\n标题:{title}\n核心观点:{core}\n\n要求:\n- 制造好奇心和话题感\n- 包含微信SEO关键词\n- 口语化,避免感叹号堆砌\n- 15-25字\n- 有情感共鸣或争议性\n\n输出3个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
|
||||
"content": "你是一个10万+公众号爆款标题专家。为以下文章起3个高打开率标题。\n\n文章主题:{title}\n核心观点:{core}\n\n要求:\n- 包含身份标签(如「打工人」「30岁后」「职场人」「普通上班族」)\n- 包含情绪钩子(焦虑/反常识/后悔/稀缺)\n- 包含微信SEO关键词(用户会搜的词)\n- 15-25字\n- 口语化,避免感叹号堆砌\n- 忌笼统——越具体越好\n\n格式模板参考:\n- 「身份+痛点+方案」:打工人学了一堆AI工具,为什么还在加班?\n- 「反常识+数据」:用了AI效率反而更低了?73%的上班族掉进了这个坑\n- 「结果+身份」:每天省出2小时后,我才发现自己以前有多傻\n\n输出3个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
|
||||
"temperature": 0.8, "max_tokens": 1500,
|
||||
"variables": ["title", "core"],
|
||||
},
|
||||
"title_optimize_xhs": {
|
||||
"content": "你是一个小红书爆款专家。为以下文章起3个热门标题。\n\n标题:{title}\n核心观点:{core}\n\n要求:\n- 20字以内\n- 爆款模式:数字+结果 / 痛点+方案 / 反常识\n- 包含小红书SEO关键词\n- 1个精确emoji\n- 有场景感、结果感、满足感\n- 不要「必看/收藏/码住」\n\n输出3个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
|
||||
"content": "你是一个小红书爆款笔记专家。为以下文章起3个高赞标题。\n\n标题:{title}\n核心观点:{core}\n\n要求:\n- 18字以内\n- 爆款公式:身份/场景+数字+结果,或痛点+方案+反差\n- 包含小红书SEO关键词\n- 1个精确emoji(不要用🔥💥❌❓这几个滥用的)\n- 有场景感、结果感、获得感\n- 忌笼统:不要「必看/收藏/码住/绝了」\n\n输出3个选项,格式:\n1. 标题A\n2. 标题B\n3. 标题C\n只输出标题,不要其他文字。",
|
||||
"temperature": 0.8, "max_tokens": 1500,
|
||||
"variables": ["title", "core"],
|
||||
},
|
||||
@@ -53,7 +75,7 @@ _PROMPT_DEFAULTS = {
|
||||
"variables": ["date", "year", "title", "field", "core", "pain", "angle", "cases_summary"],
|
||||
},
|
||||
"compliance_fix": {
|
||||
"content": "你是一个专业的内容合规优化助手。以下文章存在合规问题,请逐一修复并输出完整HTML。\n\n需修复的问题:\n{issues_desc}\n\n原文:\n{html}\n\n要求:\n- 只修复上述问题,不改变文章结构和核心内容\n- 保持<h2>, <h3>, <p>等标签结构不变\n- 修复后内容依然保持可读性和自然语感(不要因为合规变成生硬的表达)\n- 替换敏感词时选择意思相近的替代词,不删节重要信息",
|
||||
"content": "你是一个专业的内容合规与质量优化助手。以下文章存在需要优化的地方,请逐一修复并输出完整HTML。\n\n需修复的问题:\n{issues_desc}\n\n原文:\n{html}\n\n要求:\n- 只修复上述问题,不改变文章结构和核心内容\n- 保持<h2>, <h3>, <p>等标签结构不变\n- 修复后内容依然保持可读性和自然语感\n- 替换敏感词时选择意思相近的替代词,不删节重要信息\n- 如果问题是AI套话,直接删除或改写那些词句\n- 如果问题是人称混用,统一为「你」\n- 如果问题是缺少配图,在关键位置插入 `<p></p>` 空段落占位,配图由后续流程统一处理",
|
||||
"temperature": 0.3, "max_tokens": 8000,
|
||||
"variables": ["issues_desc", "html"],
|
||||
},
|
||||
@@ -68,7 +90,7 @@ _PROMPT_DEFAULTS = {
|
||||
"variables": ["n", "cat_names", "n2", "src_summary", "year"],
|
||||
},
|
||||
"tags_generation": {
|
||||
"content": "为以下文章生成{platform}标签(3-5个)。\n\n标题:{title}\n领域:{field}\n核心观点:{core}\n\n要求:每个2-4字。直接输出标签,空格分隔。不要输出思考过程。",
|
||||
"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,
|
||||
"variables": ["platform", "title", "field", "core"],
|
||||
},
|
||||
@@ -76,12 +98,45 @@ _PROMPT_DEFAULTS = {
|
||||
|
||||
_DB_CACHE: Dict[str, Dict[str, Any]] = {}
|
||||
_CACHE_LOADED = False
|
||||
_DB_AVAILABLE = False # True if DB was successfully loaded at least once
|
||||
|
||||
|
||||
def _seed_missing_prompts():
|
||||
"""将代码默认值中不存在的 prompt 自动补入 DB"""
|
||||
try:
|
||||
if os.getenv('USE_POSTGRES', 'true') == 'true':
|
||||
from app.database import SessionLocal
|
||||
from app.models import PromptConfig
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing_keys = {p.key for p in db.query(PromptConfig).all()}
|
||||
for key, cfg in _PROMPT_DEFAULTS.items():
|
||||
if key not in existing_keys:
|
||||
pc = PromptConfig(
|
||||
key=key,
|
||||
content=cfg['content'],
|
||||
temperature=cfg.get('temperature'),
|
||||
max_tokens=cfg.get('max_tokens'),
|
||||
enabled=True,
|
||||
module_id='default',
|
||||
variables=cfg.get('variables', []),
|
||||
)
|
||||
db.add(pc)
|
||||
print(f"[prompt_loader] auto-seeded prompt: {key}")
|
||||
if any(key not in existing_keys for key in _PROMPT_DEFAULTS):
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _ensure_db_loaded():
|
||||
global _DB_CACHE, _CACHE_LOADED
|
||||
global _DB_CACHE, _CACHE_LOADED, _DB_AVAILABLE
|
||||
if _CACHE_LOADED:
|
||||
return
|
||||
# 自动补缺:将代码默认值中不存在的 prompt 写入 DB
|
||||
_seed_missing_prompts()
|
||||
try:
|
||||
if os.getenv('USE_POSTGRES', 'true') == 'true':
|
||||
from app.database import SessionLocal
|
||||
@@ -94,6 +149,7 @@ def _ensure_db_loaded():
|
||||
"temperature": p.temperature,
|
||||
"max_tokens": p.max_tokens,
|
||||
}
|
||||
_DB_AVAILABLE = True
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
@@ -102,16 +158,30 @@ def _ensure_db_loaded():
|
||||
|
||||
|
||||
def get_prompt(key: str, **kwargs) -> str:
|
||||
"""获取 prompt 内容
|
||||
|
||||
优先级:DB → 代码默认值(仅 DB 不可用时)→ 空字符串
|
||||
"""
|
||||
_ensure_db_loaded()
|
||||
cleaned = []
|
||||
for k, v in kwargs.items():
|
||||
cleaned.append((k, str(v)))
|
||||
kwargs = dict(cleaned)
|
||||
|
||||
if key in _DB_CACHE:
|
||||
content = _DB_CACHE[key]["content"]
|
||||
elif key in _PROMPT_DEFAULTS:
|
||||
for k, v in kwargs.items():
|
||||
content = content.replace("{" + k + "}", str(v))
|
||||
return content
|
||||
|
||||
# 仅 DB 不可用时回退代码默认值
|
||||
if not _DB_AVAILABLE and key in _PROMPT_DEFAULTS:
|
||||
content = _PROMPT_DEFAULTS[key]["content"]
|
||||
else:
|
||||
return ""
|
||||
for k, v in kwargs.items():
|
||||
content = content.replace("{" + k + "}", str(v))
|
||||
return content
|
||||
for k, v in kwargs.items():
|
||||
content = content.replace("{" + k + "}", str(v))
|
||||
return content
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def get_prompt_params(key: str) -> Dict[str, Any]:
|
||||
@@ -121,7 +191,7 @@ def get_prompt_params(key: str) -> Dict[str, Any]:
|
||||
"temperature": _DB_CACHE[key].get("temperature"),
|
||||
"max_tokens": _DB_CACHE[key].get("max_tokens"),
|
||||
}
|
||||
if key in _PROMPT_DEFAULTS:
|
||||
if not _DB_AVAILABLE and key in _PROMPT_DEFAULTS:
|
||||
return {
|
||||
"temperature": _PROMPT_DEFAULTS[key].get("temperature"),
|
||||
"max_tokens": _PROMPT_DEFAULTS[key].get("max_tokens"),
|
||||
@@ -129,8 +199,47 @@ def get_prompt_params(key: str) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
def seed_prompts(force: bool = False):
|
||||
"""手动触发种子同步(用于 CLI 或 API)
|
||||
|
||||
force=True: 用代码默认值覆盖 DB
|
||||
force=False: 仅补充 DB 中不存在的 key
|
||||
"""
|
||||
try:
|
||||
from app.database import SessionLocal
|
||||
from app.models import PromptConfig
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing = {p.key: p for p in db.query(PromptConfig).all()}
|
||||
for key, cfg in _PROMPT_DEFAULTS.items():
|
||||
if force or key not in existing:
|
||||
if key in existing:
|
||||
p = existing[key]
|
||||
p.content = cfg['content']
|
||||
p.temperature = cfg.get('temperature')
|
||||
p.max_tokens = cfg.get('max_tokens')
|
||||
else:
|
||||
p = PromptConfig(
|
||||
key=key,
|
||||
content=cfg['content'],
|
||||
temperature=cfg.get('temperature'),
|
||||
max_tokens=cfg.get('max_tokens'),
|
||||
enabled=True,
|
||||
module_id='default',
|
||||
variables=cfg.get('variables', []),
|
||||
)
|
||||
db.add(p)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
return {"ok": True, "action": "force" if force else "seed"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
|
||||
def reload_prompts():
|
||||
global _CACHE_LOADED, _DB_CACHE
|
||||
global _CACHE_LOADED, _DB_CACHE, _DB_AVAILABLE
|
||||
_CACHE_LOADED = False
|
||||
_DB_CACHE = {}
|
||||
_ensure_db_loaded()
|
||||
_DB_AVAILABLE = False
|
||||
_ensure_db_loaded()
|
||||
|
||||
Reference in New Issue
Block a user